Castlevania 64: Implement New Game (#2472)

This commit is contained in:
LiquidCat64 2024-03-20 15:03:25 -06:00 committed by GitHub
parent 32315776ac
commit db02e9d2aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 9828 additions and 0 deletions

View File

@ -62,6 +62,7 @@ Currently, the following games are supported:
* Kirby's Dream Land 3 * Kirby's Dream Land 3
* Celeste 64 * Celeste 64
* Zork Grand Inquisitor * Zork Grand Inquisitor
* Castlevania 64
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -28,6 +28,9 @@
# Bumper Stickers # Bumper Stickers
/worlds/bumpstik/ @FelicitusNeko /worlds/bumpstik/ @FelicitusNeko
# Castlevania 64
/worlds/cv64/ @LiquidCat64
# Celeste 64 # Celeste 64
/worlds/celeste64/ @PoryGone /worlds/celeste64/ @PoryGone

View File

@ -169,6 +169,11 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Ar
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";

327
worlds/cv64/__init__.py Normal file
View File

@ -0,0 +1,327 @@
import os
import typing
import settings
import base64
import logging
from BaseClasses import Item, Region, MultiWorld, Tutorial, ItemClassification
from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts
from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id
from .entrances import verify_entrances, get_warp_entrances
from .options import CV64Options, CharacterStages, DraculasCondition, SubWeaponShuffle
from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_stage_order, \
shuffle_stages, generate_warps, get_region_names
from .regions import get_region_info
from .rules import CV64Rules
from .data import iname, rname, ename
from ..AutoWorld import WebWorld, World
from .aesthetics import randomize_lighting, shuffle_sub_weapons, rom_empty_breakables_flags, rom_sub_weapon_flags, \
randomize_music, get_start_inventory_data, get_location_data, randomize_shop_prices, get_loading_zone_bytes, \
get_countdown_numbers
from .rom import LocalRom, patch_rom, get_base_rom_path, CV64DeltaPatch
from .client import Castlevania64Client
class CV64Settings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the CV64 US 1.0 rom"""
copy_to = "Castlevania (USA).z64"
description = "CV64 (US 1.0) ROM File"
md5s = [CV64DeltaPatch.hash]
rom_file: RomFile = RomFile(RomFile.copy_to)
class CV64Web(WebWorld):
theme = "stone"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Archipleago Castlevania 64 randomizer on your computer and connecting it to a "
"multiworld.",
"English",
"setup_en.md",
"setup/en",
["Liquid Cat"]
)]
class CV64World(World):
"""
Castlevania for the Nintendo 64 is the first 3D game in the franchise. As either whip-wielding Belmont descendant
Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you make your
way to Dracula's chamber and stop his rule of terror!
"""
game = "Castlevania 64"
item_name_groups = {
"Bomb": {iname.magical_nitro, iname.mandragora},
"Ingredient": {iname.magical_nitro, iname.mandragora},
}
location_name_groups = {stage: set(get_locations_from_stage(stage)) for stage in vanilla_stage_order}
options_dataclass = CV64Options
options: CV64Options
settings: typing.ClassVar[CV64Settings]
topology_present = True
data_version = 1
item_name_to_id = get_item_names_to_ids()
location_name_to_id = get_location_names_to_ids()
active_stage_exits: typing.Dict[str, typing.Dict]
active_stage_list: typing.List[str]
active_warp_list: typing.List[str]
# Default values to possibly be updated in generate_early
reinhardt_stages: bool = True
carrie_stages: bool = True
branching_stages: bool = False
starting_stage: str = rname.forest_of_silence
total_s1s: int = 7
s1s_per_warp: int = 1
total_s2s: int = 0
required_s2s: int = 0
drac_condition: int = 0
auth: bytearray
web = CV64Web()
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def generate_early(self) -> None:
# Generate the player's unique authentication
self.auth = bytearray(self.multiworld.random.getrandbits(8) for _ in range(16))
self.total_s1s = self.options.total_special1s.value
self.s1s_per_warp = self.options.special1s_per_warp.value
self.drac_condition = self.options.draculas_condition.value
# If there are more S1s needed to unlock the whole warp menu than there are S1s in total, drop S1s per warp to
# something manageable.
if self.s1s_per_warp * 7 > self.total_s1s:
self.s1s_per_warp = self.total_s1s // 7
logging.warning(f"[{self.multiworld.player_name[self.player]}] Too many required Special1s "
f"({self.options.special1s_per_warp.value * 7}) for Special1s Per Warp setting: "
f"{self.options.special1s_per_warp.value} with Total Special1s setting: "
f"{self.options.total_special1s.value}. Lowering Special1s Per Warp to: "
f"{self.s1s_per_warp}")
self.options.special1s_per_warp.value = self.s1s_per_warp
# Set the total and required Special2s to 1 if the drac condition is the Crystal, to the specified YAML numbers
# if it's Specials, or to 0 if it's None or Bosses. The boss totals will be figured out later.
if self.drac_condition == DraculasCondition.option_crystal:
self.total_s2s = 1
self.required_s2s = 1
elif self.drac_condition == DraculasCondition.option_specials:
self.total_s2s = self.options.total_special2s.value
self.required_s2s = int(self.options.percent_special2s_required.value / 100 * self.total_s2s)
# Enable/disable character stages and branching paths accordingly
if self.options.character_stages == CharacterStages.option_reinhardt_only:
self.carrie_stages = False
elif self.options.character_stages == CharacterStages.option_carrie_only:
self.reinhardt_stages = False
elif self.options.character_stages == CharacterStages.option_both:
self.branching_stages = True
self.active_stage_exits = get_normal_stage_exits(self)
stage_1_blacklist = []
# Prevent Clock Tower from being Stage 1 if more than 4 S1s are needed to warp out of it.
if self.s1s_per_warp > 4 and not self.options.multi_hit_breakables:
stage_1_blacklist.append(rname.clock_tower)
# Shuffle the stages if the option is on.
if self.options.stage_shuffle:
self.active_stage_exits, self.starting_stage, self.active_stage_list = \
shuffle_stages(self, stage_1_blacklist)
else:
self.active_stage_list = [stage for stage in vanilla_stage_order if stage in self.active_stage_exits]
# Create a list of warps from the active stage list. They are in a random order by default and will never
# include the starting stage.
self.active_warp_list = generate_warps(self)
def create_regions(self) -> None:
# Add the Menu Region.
created_regions = [Region("Menu", self.player, self.multiworld)]
# Add every stage Region by checking to see if that stage is active.
created_regions.extend([Region(name, self.player, self.multiworld)
for name in get_region_names(self.active_stage_exits)])
# Add the Renon's shop Region if shopsanity is on.
if self.options.shopsanity:
created_regions.append(Region(rname.renon, self.player, self.multiworld))
# Add the Dracula's chamber (the end) Region.
created_regions.append(Region(rname.ck_drac_chamber, self.player, self.multiworld))
# Set up the Regions correctly.
self.multiworld.regions.extend(created_regions)
# Add the warp Entrances to the Menu Region (the one always at the start of the Region list).
created_regions[0].add_exits(get_warp_entrances(self.active_warp_list))
for reg in created_regions:
# Add the Entrances to all the Regions.
ent_names = get_region_info(reg.name, "entrances")
if ent_names is not None:
reg.add_exits(verify_entrances(self.options, ent_names, self.active_stage_exits))
# Add the Locations to all the Regions.
loc_names = get_region_info(reg.name, "locations")
if loc_names is None:
continue
verified_locs, events = verify_locations(self.options, loc_names)
reg.add_locations(verified_locs, CV64Location)
# Place event Items on all of their associated Locations.
for event_loc, event_item in events.items():
self.get_location(event_loc).place_locked_item(self.create_item(event_item, "progression"))
# If we're looking at a boss kill trophy, increment the total S2s and, if we're not already at the
# set number of required bosses, the total required number. This way, we can prevent gen failures
# should the player set more bosses required than there are total.
if event_item == iname.trophy:
self.total_s2s += 1
if self.required_s2s < self.options.bosses_required.value:
self.required_s2s += 1
# If Dracula's Condition is Bosses and there are less calculated required S2s than the value specified by the
# player (meaning there weren't enough bosses to reach the player's setting), throw a warning and lower the
# option value.
if self.options.draculas_condition == DraculasCondition.option_bosses and self.required_s2s < \
self.options.bosses_required.value:
logging.warning(f"[{self.multiworld.player_name[self.player]}] Not enough bosses for Bosses Required "
f"setting: {self.options.bosses_required.value}. Lowering to: {self.required_s2s}")
self.options.bosses_required.value = self.required_s2s
def create_item(self, name: str, force_classification: typing.Optional[str] = None) -> Item:
if force_classification is not None:
classification = getattr(ItemClassification, force_classification)
else:
classification = getattr(ItemClassification, get_item_info(name, "default classification"))
code = get_item_info(name, "code")
if code is not None:
code += base_id
created_item = CV64Item(name, classification, code, self.player)
return created_item
def create_items(self) -> None:
item_counts = get_item_counts(self)
# Set up the items correctly
self.multiworld.itempool += [self.create_item(item, classification) for classification in item_counts for item
in item_counts[classification] for _ in range(item_counts[classification][item])]
def set_rules(self) -> None:
# Set all the Entrance rules properly.
CV64Rules(self).set_cv64_rules()
def pre_fill(self) -> None:
# If we need more Special1s to warp out of Sphere 1 than there are locations available, then AP's fill
# algorithm may try placing the Special1s anyway despite placing the stage's single key always being an option.
# To get around this problem in the fill algorithm, the keys will be forced early in these situations to ensure
# the algorithm will pick them over the Special1s.
if self.starting_stage == rname.tower_of_science:
if self.s1s_per_warp > 3:
self.multiworld.local_early_items[self.player][iname.science_key2] = 1
elif self.starting_stage == rname.clock_tower:
if (self.s1s_per_warp > 2 and not self.options.multi_hit_breakables) or \
(self.s1s_per_warp > 8 and self.options.multi_hit_breakables):
self.multiworld.local_early_items[self.player][iname.clocktower_key1] = 1
elif self.starting_stage == rname.castle_wall:
if self.s1s_per_warp > 5 and not self.options.hard_logic and \
not self.options.multi_hit_breakables:
self.multiworld.local_early_items[self.player][iname.left_tower_key] = 1
def generate_output(self, output_directory: str) -> None:
active_locations = self.multiworld.get_locations(self.player)
# Location data and shop names, descriptions, and colors
offset_data, shop_name_list, shop_colors_list, shop_desc_list = \
get_location_data(self, active_locations)
# Shop prices
if self.options.shop_prices:
offset_data.update(randomize_shop_prices(self))
# Map lighting
if self.options.map_lighting:
offset_data.update(randomize_lighting(self))
# Sub-weapons
if self.options.sub_weapon_shuffle == SubWeaponShuffle.option_own_pool:
offset_data.update(shuffle_sub_weapons(self))
elif self.options.sub_weapon_shuffle == SubWeaponShuffle.option_anywhere:
offset_data.update(rom_sub_weapon_flags)
# Empty breakables
if self.options.empty_breakables:
offset_data.update(rom_empty_breakables_flags)
# Music
if self.options.background_music:
offset_data.update(randomize_music(self))
# Loading zones
offset_data.update(get_loading_zone_bytes(self.options, self.starting_stage, self.active_stage_exits))
# Countdown
if self.options.countdown:
offset_data.update(get_countdown_numbers(self.options, active_locations))
# Start Inventory
offset_data.update(get_start_inventory_data(self.player, self.options,
self.multiworld.precollected_items[self.player]))
cv64_rom = LocalRom(get_base_rom_path())
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.z64")
patch_rom(self, cv64_rom, offset_data, shop_name_list, shop_desc_list, shop_colors_list, active_locations)
cv64_rom.write_to_file(rompath)
patch = CV64DeltaPatch(os.path.splitext(rompath)[0] + CV64DeltaPatch.patch_file_ending, player=self.player,
player_name=self.multiworld.player_name[self.player], patched_path=rompath)
patch.write()
os.unlink(rompath)
def get_filler_item_name(self) -> str:
return self.random.choice(filler_item_names)
def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]):
# Attach each location's stage's position to its hint information if Stage Shuffle is on.
if not self.options.stage_shuffle:
return
stage_pos_data = {}
for loc in list(self.multiworld.get_locations(self.player)):
stage = get_region_info(loc.parent_region.name, "stage")
if stage is not None and loc.address is not None:
num = str(self.active_stage_exits[stage]["position"]).zfill(2)
path = self.active_stage_exits[stage]["path"]
stage_pos_data[loc.address] = f"Stage {num}"
if path != " ":
stage_pos_data[loc.address] += path
hint_data[self.player] = stage_pos_data
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]):
# Put the player's unique authentication in connect_names.
multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = \
multidata["connect_names"][self.multiworld.player_name[self.player]]
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
# Write the stage order to the spoiler log
spoiler_handle.write(f"\nCastlevania 64 stage & warp orders for {self.multiworld.player_name[self.player]}:\n")
for stage in self.active_stage_list:
num = str(self.active_stage_exits[stage]["position"]).zfill(2)
path = self.active_stage_exits[stage]["path"]
spoiler_handle.writelines(f"Stage {num}{path}:\t{stage}\n")
# Write the warp order to the spoiler log
spoiler_handle.writelines(f"\nStart :\t{self.active_stage_list[0]}\n")
for i in range(1, len(self.active_warp_list)):
spoiler_handle.writelines(f"Warp {i}:\t{self.active_warp_list[i]}\n")

666
worlds/cv64/aesthetics.py Normal file
View File

@ -0,0 +1,666 @@
import logging
from BaseClasses import ItemClassification, Location, Item
from .data import iname, rname
from .options import CV64Options, BackgroundMusic, Countdown, IceTrapAppearance, InvisibleItems, CharacterStages
from .stages import vanilla_stage_order, get_stage_info
from .locations import get_location_info, base_id
from .regions import get_region_info
from .items import get_item_info, item_info
from typing import TYPE_CHECKING, Dict, List, Tuple, Union, Iterable
if TYPE_CHECKING:
from . import CV64World
rom_sub_weapon_offsets = {
0x10C6EB: (0x10, rname.forest_of_silence), # Forest
0x10C6F3: (0x0F, rname.forest_of_silence),
0x10C6FB: (0x0E, rname.forest_of_silence),
0x10C703: (0x0D, rname.forest_of_silence),
0x10C81F: (0x0F, rname.castle_wall), # Castle Wall
0x10C827: (0x10, rname.castle_wall),
0x10C82F: (0x0E, rname.castle_wall),
0x7F9A0F: (0x0D, rname.castle_wall),
0x83A5D9: (0x0E, rname.villa), # Villa
0x83A5E5: (0x0D, rname.villa),
0x83A5F1: (0x0F, rname.villa),
0xBFC903: (0x10, rname.villa),
0x10C987: (0x10, rname.villa),
0x10C98F: (0x0D, rname.villa),
0x10C997: (0x0F, rname.villa),
0x10CF73: (0x10, rname.villa),
0x10CA57: (0x0D, rname.tunnel), # Tunnel
0x10CA5F: (0x0E, rname.tunnel),
0x10CA67: (0x10, rname.tunnel),
0x10CA6F: (0x0D, rname.tunnel),
0x10CA77: (0x0F, rname.tunnel),
0x10CA7F: (0x0E, rname.tunnel),
0x10CBC7: (0x0E, rname.castle_center), # Castle Center
0x10CC0F: (0x0D, rname.castle_center),
0x10CC5B: (0x0F, rname.castle_center),
0x10CD3F: (0x0E, rname.tower_of_execution), # Character towers
0x10CD65: (0x0D, rname.tower_of_execution),
0x10CE2B: (0x0E, rname.tower_of_science),
0x10CE83: (0x10, rname.duel_tower),
0x10CF8B: (0x0F, rname.room_of_clocks), # Room of Clocks
0x10CF93: (0x0D, rname.room_of_clocks),
0x99BC5A: (0x0D, rname.clock_tower), # Clock Tower
0x10CECB: (0x10, rname.clock_tower),
0x10CED3: (0x0F, rname.clock_tower),
0x10CEDB: (0x0E, rname.clock_tower),
0x10CEE3: (0x0D, rname.clock_tower),
}
rom_sub_weapon_flags = {
0x10C6EC: 0x0200FF04, # Forest of Silence
0x10C6FC: 0x0400FF04,
0x10C6F4: 0x0800FF04,
0x10C704: 0x4000FF04,
0x10C831: 0x08, # Castle Wall
0x10C829: 0x10,
0x10C821: 0x20,
0xBFCA97: 0x04,
# Villa
0xBFC926: 0xFF04,
0xBFC93A: 0x80,
0xBFC93F: 0x01,
0xBFC943: 0x40,
0xBFC947: 0x80,
0x10C989: 0x10,
0x10C991: 0x20,
0x10C999: 0x40,
0x10CF77: 0x80,
0x10CA58: 0x4000FF0E, # Tunnel
0x10CA6B: 0x80,
0x10CA60: 0x1000FF05,
0x10CA70: 0x2000FF05,
0x10CA78: 0x4000FF05,
0x10CA80: 0x8000FF05,
0x10CBCA: 0x02, # Castle Center
0x10CC10: 0x80,
0x10CC5C: 0x40,
0x10CE86: 0x01, # Duel Tower
0x10CD43: 0x02, # Tower of Execution
0x10CE2E: 0x20, # Tower of Science
0x10CF8E: 0x04, # Room of Clocks
0x10CF96: 0x08,
0x10CECE: 0x08, # Clock Tower
0x10CED6: 0x10,
0x10CEE6: 0x20,
0x10CEDE: 0x80,
}
rom_empty_breakables_flags = {
0x10C74D: 0x40FF05, # Forest of Silence
0x10C765: 0x20FF0E,
0x10C774: 0x0800FF0E,
0x10C755: 0x80FF05,
0x10C784: 0x0100FF0E,
0x10C73C: 0x0200FF0E,
0x10C8D0: 0x0400FF0E, # Villa foyer
0x10CF9F: 0x08, # Room of Clocks flags
0x10CFA7: 0x01,
0xBFCB6F: 0x04, # Room of Clocks candle property IDs
0xBFCB73: 0x05,
}
rom_axe_cross_lower_values = {
0x6: [0x7C7F97, 0x07], # Forest
0x8: [0x7C7FA6, 0xF9],
0x30: [0x83A60A, 0x71], # Villa hallway
0x27: [0x83A617, 0x26],
0x2C: [0x83A624, 0x6E],
0x16C: [0x850FE6, 0x07], # Villa maze
0x10A: [0x8C44D3, 0x08], # CC factory floor
0x109: [0x8C44E1, 0x08],
0x74: [0x8DF77C, 0x07], # CC invention area
0x60: [0x90FD37, 0x43],
0x55: [0xBFCC2B, 0x43],
0x65: [0x90FBA1, 0x51],
0x64: [0x90FBAD, 0x50],
0x61: [0x90FE56, 0x43]
}
rom_looping_music_fade_ins = {
0x10: None,
0x11: None,
0x12: None,
0x13: None,
0x14: None,
0x15: None,
0x16: 0x17,
0x18: 0x19,
0x1A: 0x1B,
0x21: 0x75,
0x27: None,
0x2E: 0x23,
0x39: None,
0x45: 0x63,
0x56: None,
0x57: 0x58,
0x59: None,
0x5A: None,
0x5B: 0x5C,
0x5D: None,
0x5E: None,
0x5F: None,
0x60: 0x61,
0x62: None,
0x64: None,
0x65: None,
0x66: None,
0x68: None,
0x69: None,
0x6D: 0x78,
0x6E: None,
0x6F: None,
0x73: None,
0x74: None,
0x77: None,
0x79: None
}
music_sfx_ids = [0x1C, 0x4B, 0x4C, 0x4D, 0x4E, 0x55, 0x6C, 0x76]
renon_item_dialogue = {
0x02: "More Sub-weapon uses!\n"
"Just what you need!",
0x03: "Galamoth told me it's\n"
"a heart in other times.",
0x04: "Who needs Warp Rooms\n"
"when you have these?",
0x05: "I was told to safeguard\n"
"this, but I dunno why.",
0x06: "Fresh off a Behemoth!\n"
"Those cows are weird.",
0x07: "Preserved with special\n"
" wall-based methods.",
0x08: "Don't tell Geneva\n"
"about this...",
0x09: "If this existed in 1094,\n"
"that whip wouldn't...",
0x0A: "For when some lizard\n"
"brain spits on your ego.",
0x0C: "It'd be a shame if you\n"
"lost it immediately...",
0x10C: "No consequences should\n"
"you perish with this!",
0x0D: "Arthur was far better\n"
"with it than you!",
0x0E: "Night Creatures handle\n"
"with care!",
0x0F: "Some may call it a\n"
"\"Banshee Boomerang.\"",
0x10: "No weapon triangle\n"
"advantages with this.",
0x12: "It looks sus? Trust me,"
"my wares are genuine.",
0x15: "This non-volatile kind\n"
"is safe to handle.",
0x16: "If you can soul-wield,\n"
"they have a good one!",
0x17: "Calls the morning sun\n"
"to vanquish the night.",
0x18: "1 on-demand horrible\n"
"night. Devils love it!",
0x1A: "Want to study here?\n"
"It will cost you.",
0x1B: "\"Let them eat cake!\"\n"
"Said no princess ever.",
0x1C: "Why do I suspect this\n"
"was a toilet room?",
0x1D: "When you see Coller,\n"
"tell him I said hi!",
0x1E: "Atomic number is 29\n"
"and weight is 63.546.",
0x1F: "One torture per pay!\n"
"Who will it be?",
0x20: "Being here feels like\n"
"time is slowing down.",
0x21: "Only one thing beind\n"
"this. Do you dare?",
0x22: "The key 2 Science!\n"
"Both halves of it!",
0x23: "This warehouse can\n"
"be yours for a fee.",
0x24: "Long road ahead if you\n"
"don't have the others.",
0x25: "Will you get the curse\n"
"of eternal burning?",
0x26: "What's beyond time?\n"
"Find out your",
0x27: "Want to take out a\n"
"loan? By all means!",
0x28: "The bag is green,\n"
"so it must be lucky!",
0x29: "(Does this fool realize?)\n"
"Oh, sorry.",
"prog": "They will absolutely\n"
"need it in time!",
"useful": "Now, this would be\n"
"useful to send...",
"common": "Every last little bit\n"
"helps, right?",
"trap": "I'll teach this fool\n"
" a lesson for a price!",
"dlc coin": "1 coin out of... wha!?\n"
"You imp, why I oughta!"
}
def randomize_lighting(world: "CV64World") -> Dict[int, int]:
"""Generates randomized data for the map lighting table."""
randomized_lighting = {}
for entry in range(67):
for sub_entry in range(19):
if sub_entry not in [3, 7, 11, 15] and entry != 4:
# The fourth entry in the lighting table affects the lighting on some item pickups; skip it
randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = \
world.random.randint(0, 255)
return randomized_lighting
def shuffle_sub_weapons(world: "CV64World") -> Dict[int, int]:
"""Shuffles the sub-weapons amongst themselves."""
sub_weapon_dict = {offset: rom_sub_weapon_offsets[offset][0] for offset in rom_sub_weapon_offsets if
rom_sub_weapon_offsets[offset][1] in world.active_stage_exits}
# Remove the one 3HB sub-weapon in Tower of Execution if 3HBs are not shuffled.
if not world.options.multi_hit_breakables and 0x10CD65 in sub_weapon_dict:
del (sub_weapon_dict[0x10CD65])
sub_bytes = list(sub_weapon_dict.values())
world.random.shuffle(sub_bytes)
return dict(zip(sub_weapon_dict, sub_bytes))
def randomize_music(world: "CV64World") -> Dict[int, int]:
"""Generates randomized or disabled data for all the music in the game."""
music_array = bytearray(0x7A)
for number in music_sfx_ids:
music_array[number] = number
if world.options.background_music == BackgroundMusic.option_randomized:
looping_songs = []
non_looping_songs = []
fade_in_songs = {}
# Create shuffle-able lists of all the looping, non-looping, and fade-in track IDs
for i in range(0x10, len(music_array)):
if i not in rom_looping_music_fade_ins.keys() and i not in rom_looping_music_fade_ins.values() and \
i != 0x72: # Credits song is blacklisted
non_looping_songs.append(i)
elif i in rom_looping_music_fade_ins.keys():
looping_songs.append(i)
elif i in rom_looping_music_fade_ins.values():
fade_in_songs[i] = i
# Shuffle the looping songs
rando_looping_songs = looping_songs.copy()
world.random.shuffle(rando_looping_songs)
looping_songs = dict(zip(looping_songs, rando_looping_songs))
# Shuffle the non-looping songs
rando_non_looping_songs = non_looping_songs.copy()
world.random.shuffle(rando_non_looping_songs)
non_looping_songs = dict(zip(non_looping_songs, rando_non_looping_songs))
non_looping_songs[0x72] = 0x72
# Figure out the new fade-in songs if applicable
for vanilla_song in looping_songs:
if rom_looping_music_fade_ins[vanilla_song]:
if rom_looping_music_fade_ins[looping_songs[vanilla_song]]:
fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = rom_looping_music_fade_ins[
looping_songs[vanilla_song]]
else:
fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = looping_songs[vanilla_song]
# Build the new music array
for i in range(0x10, len(music_array)):
if i in looping_songs.keys():
music_array[i] = looping_songs[i]
elif i in non_looping_songs.keys():
music_array[i] = non_looping_songs[i]
else:
music_array[i] = fade_in_songs[i]
del (music_array[0x00: 0x10])
# Convert the music array into a data dict
music_offsets = {}
for i in range(len(music_array)):
music_offsets[0xBFCD30 + i] = music_array[i]
return music_offsets
def randomize_shop_prices(world: "CV64World") -> Dict[int, int]:
"""Randomize the shop prices based on the minimum and maximum values chosen.
The minimum price will adjust if it's higher than the max."""
min_price = world.options.minimum_gold_price.value
max_price = world.options.maximum_gold_price.value
if min_price > max_price:
min_price = world.random.randint(0, max_price)
logging.warning(f"[{world.multiworld.player_name[world.player]}] The Minimum Gold Price "
f"({world.options.minimum_gold_price.value * 100}) is higher than the "
f"Maximum Gold Price ({max_price * 100}). Lowering the minimum to: {min_price * 100}")
world.options.minimum_gold_price.value = min_price
shop_price_list = [world.random.randint(min_price * 100, max_price * 100) for _ in range(7)]
# Convert the price list into a data dict. Which offset it starts from depends on how many bytes it takes up.
price_dict = {}
for i in range(len(shop_price_list)):
if shop_price_list[i] <= 0xFF:
price_dict[0x103D6E + (i*12)] = 0
price_dict[0x103D6F + (i*12)] = shop_price_list[i]
elif shop_price_list[i] <= 0xFFFF:
price_dict[0x103D6E + (i*12)] = shop_price_list[i]
else:
price_dict[0x103D6D + (i*12)] = shop_price_list[i]
return price_dict
def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, int]:
"""Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should
increase a number.
First, check the location's info to see if it has a countdown number override.
If not, then figure it out based on the parent region's stage's position in the vanilla stage order.
If the parent region is not part of any stage (as is the case for Renon's shop), skip the location entirely."""
countdown_list = [0 for _ in range(15)]
for loc in active_locations:
if loc.address is not None and (options.countdown == Countdown.option_all_locations or
(options.countdown == Countdown.option_majors
and loc.item.advancement)):
countdown_number = get_location_info(loc.name, "countdown")
if countdown_number is None:
stage = get_region_info(loc.parent_region.name, "stage")
if stage is not None:
countdown_number = vanilla_stage_order.index(stage)
if countdown_number is not None:
countdown_list[countdown_number] += 1
# Convert the Countdown list into a data dict
countdown_dict = {}
for i in range(len(countdown_list)):
countdown_dict[0xBFD818 + i] = countdown_list[i]
return countdown_dict
def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \
-> Tuple[Dict[int, int], List[str], List[bytearray], List[List[Union[int, str, None]]]]:
"""Gets ALL the item data to go into the ROM. Item data consists of two bytes: the first dictates the appearance of
the item, the second determines what the item actually is when picked up. All items from other worlds will be AP
items that do nothing when picked up other than set their flag, and their appearance will depend on whether it's
another CV64 player's item and, if so, what item it is in their game. Ice Traps can assume the form of any item that
is progression, non-progression, or either depending on the player's settings.
Appearance does not matter if it's one of the two NPC-given items (from either Vincent or Heinrich Meyer). For
Renon's shop items, a list containing the shop item names, descriptions, and colors will be returned alongside the
regular data."""
# Figure out the list of possible Ice Trap appearances to use based on the settings, first and foremost.
if world.options.ice_trap_appearance == IceTrapAppearance.option_major_only:
allowed_classifications = ["progression", "progression skip balancing"]
elif world.options.ice_trap_appearance == IceTrapAppearance.option_junk_only:
allowed_classifications = ["filler", "useful"]
else:
allowed_classifications = ["progression", "progression skip balancing", "filler", "useful"]
trap_appearances = []
for item in item_info:
if item_info[item]["default classification"] in allowed_classifications and item != "Ice Trap" and \
get_item_info(item, "code") is not None:
trap_appearances.append(item)
shop_name_list = []
shop_desc_list = []
shop_colors_list = []
location_bytes = {}
for loc in active_locations:
# If the Location is an event, skip it.
if loc.address is None:
continue
loc_type = get_location_info(loc.name, "type")
# Figure out the item ID bytes to put in each Location here. Write the item itself if either it's the player's
# very own, or it belongs to an Item Link that the player is a part of.
if loc.item.player == world.player or (loc.item.player in world.multiworld.groups and
world.player in world.multiworld.groups[loc.item.player]['players']):
if loc_type not in ["npc", "shop"] and get_item_info(loc.item.name, "pickup actor id") is not None:
location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "pickup actor id")
else:
location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code")
else:
# Make the item the unused Wooden Stake - our multiworld item.
location_bytes[get_location_info(loc.name, "offset")] = 0x11
# Figure out the item's appearance. If it's a CV64 player's item, change the multiworld item's model to
# match what it is. Otherwise, change it to an Archipelago progress or not progress icon. The model "change"
# has to be applied to even local items because this is how the game knows to count it on the Countdown.
if loc.item.game == "Castlevania 64":
location_bytes[get_location_info(loc.name, "offset") - 1] = get_item_info(loc.item.name, "code")
elif loc.item.advancement:
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x11 # Wooden Stakes are majors
else:
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12 # Roses are minors
# If it's a PermaUp, change the item's model to a big PowerUp no matter what.
if loc.item.game == "Castlevania 64" and loc.item.code == 0x10C + base_id:
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B
# If it's an Ice Trap, change its model to one of the appearances we determined before.
# Unless it's an NPC item, in which case use the Ice Trap's regular ID so that it won't decrement the majors
# Countdown due to how I set up the NPC items to work.
if loc.item.game == "Castlevania 64" and loc.item.code == 0x12 + base_id:
if loc_type == "npc":
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12
else:
location_bytes[get_location_info(loc.name, "offset") - 1] = \
get_item_info(world.random.choice(trap_appearances), "code")
# If we chose a PermaUp as our trap appearance, change it to its actual in-game ID of 0x0B.
if location_bytes[get_location_info(loc.name, "offset") - 1] == 0x10C:
location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B
# Apply the invisibility variable depending on the "invisible items" setting.
if (world.options.invisible_items == InvisibleItems.option_vanilla and loc_type == "inv") or \
(world.options.invisible_items == InvisibleItems.option_hide_all and loc_type not in ["npc", "shop"]):
location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80
elif world.options.invisible_items == InvisibleItems.option_chance and loc_type not in ["npc", "shop"]:
invisible = world.random.randint(0, 1)
if invisible:
location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80
# If it's an Axe or Cross in a higher freestanding location, lower it into grab range.
# KCEK made these spawn 3.2 units higher for some reason.
if loc.address & 0xFFF in rom_axe_cross_lower_values and loc.item.code & 0xFF in [0x0F, 0x10]:
location_bytes[rom_axe_cross_lower_values[loc.address & 0xFFF][0]] = \
rom_axe_cross_lower_values[loc.address & 0xFFF][1]
# Figure out the list of shop names, descriptions, and text colors here.
if loc.parent_region.name != rname.renon:
continue
shop_name = loc.item.name
if len(shop_name) > 18:
shop_name = shop_name[0:18]
shop_name_list.append(shop_name)
if loc.item.player == world.player:
shop_desc_list.append([get_item_info(loc.item.name, "code"), None])
elif loc.item.game == "Castlevania 64":
shop_desc_list.append([get_item_info(loc.item.name, "code"),
world.multiworld.get_player_name(loc.item.player)])
else:
if loc.item.game == "DLCQuest" and loc.item.name in ["DLC Quest: Coin Bundle",
"Live Freemium or Die: Coin Bundle"]:
if getattr(world.multiworld.worlds[loc.item.player].options, "coinbundlequantity") == 1:
shop_desc_list.append(["dlc coin", world.multiworld.get_player_name(loc.item.player)])
shop_colors_list.append(get_item_text_color(loc))
continue
if loc.item.advancement:
shop_desc_list.append(["prog", world.multiworld.get_player_name(loc.item.player)])
elif loc.item.classification == ItemClassification.useful:
shop_desc_list.append(["useful", world.multiworld.get_player_name(loc.item.player)])
elif loc.item.classification == ItemClassification.trap:
shop_desc_list.append(["trap", world.multiworld.get_player_name(loc.item.player)])
else:
shop_desc_list.append(["common", world.multiworld.get_player_name(loc.item.player)])
shop_colors_list.append(get_item_text_color(loc))
return location_bytes, shop_name_list, shop_colors_list, shop_desc_list
def get_loading_zone_bytes(options: CV64Options, starting_stage: str,
active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, int]:
"""Figure out all the bytes for loading zones and map transitions based on which stages are where in the exit data.
The same data was used earlier in figuring out the logic. Map transitions consist of two major components: which map
to send the player to, and which spot within the map to spawn the player at."""
# Write the byte for the starting stage to send the player to after the intro narration.
loading_zone_bytes = {0xB73308: get_stage_info(starting_stage, "start map id")}
for stage in active_stage_exits:
# Start loading zones
# If the start zone is the start of the line, have it simply refresh the map.
if active_stage_exits[stage]["prev"] == "Menu":
loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = 0xFF
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = 0x00
elif active_stage_exits[stage]["prev"]:
loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = \
get_stage_info(active_stage_exits[stage]["prev"], "end map id")
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = \
get_stage_info(active_stage_exits[stage]["prev"], "end spawn id")
# Change CC's end-spawn ID to put you at Carrie's exit if appropriate
if active_stage_exits[stage]["prev"] == rname.castle_center:
if options.character_stages == CharacterStages.option_carrie_only or \
active_stage_exits[rname.castle_center]["alt"] == stage:
loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] += 1
# End loading zones
if active_stage_exits[stage]["next"]:
loading_zone_bytes[get_stage_info(stage, "endzone map offset")] = \
get_stage_info(active_stage_exits[stage]["next"], "start map id")
loading_zone_bytes[get_stage_info(stage, "endzone spawn offset")] = \
get_stage_info(active_stage_exits[stage]["next"], "start spawn id")
# Alternate end loading zones
if active_stage_exits[stage]["alt"]:
loading_zone_bytes[get_stage_info(stage, "altzone map offset")] = \
get_stage_info(active_stage_exits[stage]["alt"], "start map id")
loading_zone_bytes[get_stage_info(stage, "altzone spawn offset")] = \
get_stage_info(active_stage_exits[stage]["alt"], "start spawn id")
return loading_zone_bytes
def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, int]:
"""Calculate and return the starting inventory values. Not every Item goes into the menu inventory, so everything
has to be handled appropriately."""
start_inventory_data = {0xBFD867: 0, # Jewels
0xBFD87B: 0, # PowerUps
0xBFD883: 0, # Sub-weapon
0xBFD88B: 0} # Ice Traps
inventory_items_array = [0 for _ in range(35)]
total_money = 0
items_max = 10
# Raise the items max if Increase Item Limit is enabled.
if options.increase_item_limit:
items_max = 99
for item in precollected_items:
if item.player != player:
continue
inventory_offset = get_item_info(item.name, "inventory offset")
sub_equip_id = get_item_info(item.name, "sub equip id")
# Starting inventory items
if inventory_offset is not None:
inventory_items_array[inventory_offset] += 1
if inventory_items_array[inventory_offset] > items_max and "Special" not in item.name:
inventory_items_array[inventory_offset] = items_max
if item.name == iname.permaup:
if inventory_items_array[inventory_offset] > 2:
inventory_items_array[inventory_offset] = 2
# Starting sub-weapon
elif sub_equip_id is not None:
start_inventory_data[0xBFD883] = sub_equip_id
# Starting PowerUps
elif item.name == iname.powerup:
start_inventory_data[0xBFD87B] += 1
if start_inventory_data[0xBFD87B] > 2:
start_inventory_data[0xBFD87B] = 2
# Starting Gold
elif "GOLD" in item.name:
total_money += int(item.name[0:4])
if total_money > 99999:
total_money = 99999
# Starting Jewels
elif "jewel" in item.name:
if "L" in item.name:
start_inventory_data[0xBFD867] += 10
else:
start_inventory_data[0xBFD867] += 5
if start_inventory_data[0xBFD867] > 99:
start_inventory_data[0xBFD867] = 99
# Starting Ice Traps
else:
start_inventory_data[0xBFD88B] += 1
if start_inventory_data[0xBFD88B] > 0xFF:
start_inventory_data[0xBFD88B] = 0xFF
# Convert the inventory items into data.
for i in range(len(inventory_items_array)):
start_inventory_data[0xBFE518 + i] = inventory_items_array[i]
# Convert the starting money into data. Which offset it starts from depends on how many bytes it takes up.
if total_money <= 0xFF:
start_inventory_data[0xBFE517] = total_money
elif total_money <= 0xFFFF:
start_inventory_data[0xBFE516] = total_money
else:
start_inventory_data[0xBFE515] = total_money
return start_inventory_data
def get_item_text_color(loc: Location) -> bytearray:
if loc.item.advancement:
return bytearray([0xA2, 0x0C])
elif loc.item.classification == ItemClassification.useful:
return bytearray([0xA2, 0x0A])
elif loc.item.classification == ItemClassification.trap:
return bytearray([0xA2, 0x0B])
else:
return bytearray([0xA2, 0x02])

207
worlds/cv64/client.py Normal file
View File

@ -0,0 +1,207 @@
from typing import TYPE_CHECKING, Set
from .locations import base_id
from .text import cv64_text_wrap, cv64_string_to_bytearray
from NetUtils import ClientStatus
import worlds._bizhawk as bizhawk
import base64
from worlds._bizhawk.client import BizHawkClient
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
class Castlevania64Client(BizHawkClient):
game = "Castlevania 64"
system = "N64"
patch_suffix = ".apcv64"
self_induced_death = False
received_deathlinks = 0
death_causes = []
currently_shopping = False
local_checked_locations: Set[int]
def __init__(self) -> None:
super().__init__()
self.local_checked_locations = set()
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
try:
# Check ROM name/patch version
game_names = await bizhawk.read(ctx.bizhawk_ctx, [(0x20, 0x14, "ROM"), (0xBFBFD0, 12, "ROM")])
if game_names[0].decode("ascii") != "CASTLEVANIA ":
return False
if game_names[1] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00':
logger.info("ERROR: You appear to be running an unpatched version of Castlevania 64. "
"You need to generate a patch file and use it to create a patched ROM.")
return False
if game_names[1].decode("ascii") != "ARCHIPELAGO1":
logger.info("ERROR: The patch file used to create this ROM is not compatible with "
"this client. Double check your client version against the version being "
"used by the generator.")
return False
except UnicodeDecodeError:
return False
except bizhawk.RequestFailedError:
return False # Should verify on the next pass
ctx.game = self.game
ctx.items_handling = 0b001
ctx.want_slot_data = False
ctx.watcher_timeout = 0.125
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(0xBFBFE0, 16, "ROM")]))[0]
ctx.auth = base64.b64encode(auth_raw).decode("utf-8")
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
if cmd != "Bounced":
return
if "tags" not in args:
return
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
self.received_deathlinks += 1
if "cause" in args["data"]:
cause = args["data"]["cause"]
if len(cause) > 88:
cause = cause[0x00:0x89]
else:
cause = f"{args['data']['source']} killed you!"
self.death_causes.append(cause)
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
try:
read_state = await bizhawk.read(ctx.bizhawk_ctx, [(0x342084, 4, "RDRAM"),
(0x389BDE, 6, "RDRAM"),
(0x389BE4, 224, "RDRAM"),
(0x389EFB, 1, "RDRAM"),
(0x389EEF, 1, "RDRAM"),
(0xBFBFDE, 2, "ROM")])
game_state = int.from_bytes(read_state[0], "big")
save_struct = read_state[2]
written_deathlinks = int.from_bytes(bytearray(read_state[1][4:6]), "big")
deathlink_induced_death = int.from_bytes(bytearray(read_state[1][0:1]), "big")
cutscene_value = int.from_bytes(read_state[3], "big")
current_menu = int.from_bytes(read_state[4], "big")
num_received_items = int.from_bytes(bytearray(save_struct[0xDA:0xDC]), "big")
rom_flags = int.from_bytes(read_state[5], "big")
# Make sure we are in the Gameplay or Credits states before detecting sent locations and/or DeathLinks.
# If we are in any other state, such as the Game Over state, set self_induced_death to false, so we can once
# again send a DeathLink once we are back in the Gameplay state.
if game_state not in [0x00000002, 0x0000000B]:
self.self_induced_death = False
return
# Enable DeathLink if the bit for it is set in our ROM flags.
if "DeathLink" not in ctx.tags and rom_flags & 0x0100:
await ctx.update_death_link(True)
# Scout the Renon shop locations if the shopsanity flag is written in the ROM.
if rom_flags & 0x0001 and ctx.locations_info == {}:
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": [base_id + i for i in range(0x1C8, 0x1CF)],
"create_as_hint": 0
}])
# Send a DeathLink if we died on our own independently of receiving another one.
if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \
deathlink_induced_death:
self.self_induced_death = True
if save_struct[0xA4] & 0x08:
# Special death message for dying while having the Vamp status.
await ctx.send_death(f"{ctx.player_names[ctx.slot]} became a vampire and drank your blood!")
else:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!")
# Write any DeathLinks received along with the corresponding death cause starting with the oldest.
# To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one.
if self.received_deathlinks and not self.self_induced_death and not written_deathlinks:
death_text, num_lines = cv64_text_wrap(self.death_causes[0], 96)
await bizhawk.write(ctx.bizhawk_ctx, [(0x389BE3, [0x01], "RDRAM"),
(0x389BDF, [0x11], "RDRAM"),
(0x18BF98, bytearray([0xA2, 0x0B]) +
cv64_string_to_bytearray(death_text, False), "RDRAM"),
(0x18C097, [num_lines], "RDRAM")])
self.received_deathlinks -= 1
del self.death_causes[0]
else:
# If the game hasn't received all items yet, the received item struct doesn't contain an item, the
# current number of received items still matches what we read before, and there are no open text boxes,
# then fill it with the next item and write the "item from player" text in its buffer. The game will
# increment the number of received items on its own.
if num_received_items < len(ctx.items_received):
next_item = ctx.items_received[num_received_items]
if next_item.flags & 0b001:
text_color = bytearray([0xA2, 0x0C])
elif next_item.flags & 0b010:
text_color = bytearray([0xA2, 0x0A])
elif next_item.flags & 0b100:
text_color = bytearray([0xA2, 0x0B])
else:
text_color = bytearray([0xA2, 0x02])
received_text, num_lines = cv64_text_wrap(f"{ctx.item_names[next_item.item]}\n"
f"from {ctx.player_names[next_item.player]}", 96)
await bizhawk.guarded_write(ctx.bizhawk_ctx,
[(0x389BE1, [next_item.item & 0xFF], "RDRAM"),
(0x18C0A8, text_color + cv64_string_to_bytearray(received_text, False),
"RDRAM"),
(0x18C1A7, [num_lines], "RDRAM")],
[(0x389BE1, [0x00], "RDRAM"), # Remote item reward buffer
(0x389CBE, save_struct[0xDA:0xDC], "RDRAM"), # Received items
(0x342891, [0x02], "RDRAM")]) # Textbox state
flag_bytes = bytearray(save_struct[0x00:0x44]) + bytearray(save_struct[0x90:0x9F])
locs_to_send = set()
# Check for set location flags.
for byte_i, byte in enumerate(flag_bytes):
for i in range(8):
and_value = 0x80 >> i
if byte & and_value != 0:
flag_id = byte_i * 8 + i
location_id = flag_id + base_id
if location_id in ctx.server_locations:
locs_to_send.add(location_id)
# Send locations if there are any to send.
if locs_to_send != self.local_checked_locations:
self.local_checked_locations = locs_to_send
if locs_to_send is not None:
await ctx.send_msgs([{
"cmd": "LocationChecks",
"locations": list(locs_to_send)
}])
# Check the menu value to see if we are in Renon's shop, and set currently_shopping to True if we are.
if current_menu == 0xA:
self.currently_shopping = True
# If we are currently shopping, and the current menu value is 0 (meaning we just left the shop), hint the
# un-bought shop locations that have progression.
if current_menu == 0 and self.currently_shopping:
await ctx.send_msgs([{
"cmd": "LocationScouts",
"locations": [loc for loc, n_item in ctx.locations_info.items() if n_item.flags & 0b001],
"create_as_hint": 2
}])
self.currently_shopping = False
# Send game clear if we're in either any ending cutscene or the credits state.
if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B):
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL
}])
except bizhawk.RequestFailedError:
# Exit handler and return to main loop to reconnect.
pass

View File

@ -0,0 +1,3 @@
The Archipelago Logo is © 2022 by Krista Corkos and Christopher Wilson is licensed under Attribution-NonCommercial 4.0 International. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/
Logo modified by Liquid Cat to fit artstyle and uses within the mod.

Binary file not shown.

71
worlds/cv64/data/ename.py Normal file
View File

@ -0,0 +1,71 @@
forest_dbridge_gate = "Descending bridge gate"
forest_werewolf_gate = "Werewolf gate"
forest_end = "Dracula's drawbridge"
cw_portcullis_c = "Central portcullis"
cw_lt_skip = "Do Left Tower Skip"
cw_lt_door = "Left Tower door"
cw_end = "End portcullis"
villa_dog_gates = "Front dog gates"
villa_snipe_dogs = "Orb snipe the dogs"
villa_to_storeroom = "To Storeroom door"
villa_to_archives = "To Archives door"
villa_renon = "Villa contract"
villa_to_maze = "To maze gate"
villa_from_storeroom = "From Storeroom door"
villa_from_maze = "From maze gate"
villa_servant_door = "Servants' door"
villa_copper_door = "Copper door"
villa_copper_skip = "Get Copper Skip"
villa_bridge_door = "From bridge door"
villa_end_r = "Villa Reinhardt (daytime) exit"
villa_end_c = "Villa Carrie (nighttime) exit"
tunnel_start_renon = "Tunnel start contract"
tunnel_gondolas = "Gondola ride"
tunnel_end_renon = "Tunnel end contract"
tunnel_end = "End Tunnel door"
uw_final_waterfall = "Final waterfall"
uw_waterfall_skip = "Do Waterfall Skip"
uw_renon = "Underground Waterway contract"
uw_end = "End Waterway door"
cc_tc_door = "Torture Chamber door"
cc_renon = "Castle Center contract"
cc_lower_wall = "Lower sealed cracked wall"
cc_upper_wall = "Upper cracked wall"
cc_elevator = "Activate crystal and ride elevator"
cc_exit_r = "Castle Center Reinhardt (Medusa Head) exit"
cc_exit_c = "Castle Center Carrie (Ghost) exit"
dt_start = "Duel Tower start passage"
dt_end = "Duel Tower end passage"
toe_start = "Tower of Execution start passage"
toe_gate = "Execution gate"
toe_gate_skip = "Just jump past the gate from above, bro!"
toe_end = "Tower of Execution end staircase"
tosci_start = "Tower of Science start passage"
tosci_key1_door = "Science Door 1"
tosci_to_key2_door = "To Science Door 2"
tosci_from_key2_door = "From Science Door 2"
tosci_key3_door = "Science Door 3"
tosci_end = "Tower of Science end passage"
tosor_start = "Tower of Sorcery start passage"
tosor_end = "Tower of Sorcery end passage"
roc_gate = "Defeat boss gate"
ct_to_door1 = "To Clocktower Door 1"
ct_from_door1 = "From Clocktower Door 1"
ct_to_door2 = "To Clocktower Door 2"
ct_from_door2 = "From Clocktower Door 2"
ct_renon = "Clock Tower contract"
ct_door_3 = "Clocktower Door 3"
ck_slope_jump = "Slope Jump to boss tower"
ck_drac_door = "Dracula's door"

49
worlds/cv64/data/iname.py Normal file
View File

@ -0,0 +1,49 @@
# Items
white_jewel = "White jewel"
special_one = "Special1"
special_two = "Special2"
red_jewel_s = "Red jewel(S)"
red_jewel_l = "Red jewel(L)"
roast_chicken = "Roast chicken"
roast_beef = "Roast beef"
healing_kit = "Healing kit"
purifying = "Purifying"
cure_ampoule = "Cure ampoule"
pot_pourri = "pot-pourri"
powerup = "PowerUp"
permaup = "PermaUp"
holy_water = "Holy water"
cross = "Cross"
axe = "Axe"
knife = "Knife"
wooden_stake = "Wooden stake"
rose = "Rose"
ice_trap = "Ice Trap"
the_contract = "The contract"
engagement_ring = "engagement ring"
magical_nitro = "Magical Nitro"
mandragora = "Mandragora"
sun_card = "Sun card"
moon_card = "Moon card"
incandescent_gaze = "Incandescent gaze"
five_hundred_gold = "500 GOLD"
three_hundred_gold = "300 GOLD"
one_hundred_gold = "100 GOLD"
archives_key = "Archives Key"
left_tower_key = "Left Tower Key"
storeroom_key = "Storeroom Key"
garden_key = "Garden Key"
copper_key = "Copper Key"
chamber_key = "Chamber Key"
execution_key = "Execution Key"
science_key1 = "Science Key1"
science_key2 = "Science Key2"
science_key3 = "Science Key3"
clocktower_key1 = "Clocktower Key1"
clocktower_key2 = "Clocktower Key2"
clocktower_key3 = "Clocktower Key3"
trophy = "Trophy"
crystal = "Crystal"
victory = "The Count Downed"

479
worlds/cv64/data/lname.py Normal file
View File

@ -0,0 +1,479 @@
# Forest of Silence main locations
forest_pillars_right = "Forest of Silence: Grab practice pillars - Right"
forest_pillars_top = "Forest of Silence: Grab practice pillars - Top"
forest_king_skeleton = "Forest of Silence: King Skeleton's bridge"
forest_lgaz_in = "Forest of Silence: Moon gazebo inside"
forest_lgaz_top = "Forest of Silence: Moon gazebo roof"
forest_hgaz_in = "Forest of Silence: Sun gazebo inside"
forest_hgaz_top = "Forest of Silence: Sun gazebo roof"
forest_weretiger_sw = "Forest of Silence: Were-tiger switch"
forest_weretiger_gate = "Forest of Silence: Dirge maiden gate"
forest_dirge_tomb_u = "Forest of Silence: Dirge maiden crypt - Upper"
forest_dirge_plaque = "Forest of Silence: Dirge maiden pedestal plaque"
forest_corpse_save = "Forest of Silence: Tri-corpse save junction"
forest_dbridge_wall = "Forest of Silence: Descending bridge wall side"
forest_dbridge_sw = "Forest of Silence: Descending bridge switch side"
forest_dbridge_gate_r = "Forest of Silence: Tri-corpse gate - Right"
forest_dbridge_tomb_uf = "Forest of Silence: Three-crypt plaza main path crypt - Upper-front"
forest_bface_tomb_lf = "Forest of Silence: Three-crypt plaza back-facing crypt - Lower-front"
forest_bface_tomb_u = "Forest of Silence: Three-crypt plaza back-facing crypt - Upper"
forest_ibridge = "Forest of Silence: Invisible bridge platform"
forest_werewolf_tomb_r = "Forest of Silence: Werewolf crypt - Right"
forest_werewolf_plaque = "Forest of Silence: Werewolf statue plaque"
forest_werewolf_tree = "Forest of Silence: Werewolf path near tree"
forest_final_sw = "Forest of Silence: Three-crypt plaza switch"
# Forest of Silence empty breakables
forest_dirge_tomb_l = "Forest of Silence: Dirge maiden crypt - Lower"
forest_dbridge_tomb_l = "Forest of Silence: Three-crypt plaza main path crypt - Lower"
forest_dbridge_tomb_ur = "Forest of Silence: Three-crypt plaza main path crypt - Upper-rear"
forest_bface_tomb_lr = "Forest of Silence: Three-crypt plaza back-facing crypt - Lower-rear"
forest_werewolf_tomb_lf = "Forest of Silence: Werewolf crypt - Left-front"
forest_werewolf_tomb_lr = "Forest of Silence: Werewolf crypt - Left-rear"
# Forest of Silence 3-hit breakables
forest_dirge_rock1 = "Forest of Silence: Dirge maiden rock - Item 1"
forest_dirge_rock2 = "Forest of Silence: Dirge maiden rock - Item 2"
forest_dirge_rock3 = "Forest of Silence: Dirge maiden rock - Item 3"
forest_dirge_rock4 = "Forest of Silence: Dirge maiden rock - Item 4"
forest_dirge_rock5 = "Forest of Silence: Dirge maiden rock - Item 5"
forest_bridge_rock1 = "Forest of Silence: Bat archway rock - Item 1"
forest_bridge_rock2 = "Forest of Silence: Bat archway rock - Item 2"
forest_bridge_rock3 = "Forest of Silence: Bat archway rock - Item 3"
forest_bridge_rock4 = "Forest of Silence: Bat archway rock - Item 4"
# Forest of Silence sub-weapons
forest_pillars_left = "Forest of Silence: Grab practice pillars - Left"
forest_dirge_ped = "Forest of Silence: Dirge maiden pedestal"
forest_dbridge_gate_l = "Forest of Silence: Tri-corpse gate - Left"
forest_werewolf_island = "Forest of Silence: Werewolf path island switch platforms"
# Castle Wall main locations
cw_ground_middle = "Castle Wall: Ground gatehouse - Middle"
cw_rrampart = "Castle Wall: Central rampart near right tower"
cw_lrampart = "Castle Wall: Central rampart near left tower"
cw_dragon_sw = "Castle Wall: White Dragons switch door"
cw_drac_sw = "Castle Wall: Dracula cutscene switch door"
cw_shelf_visible = "Castle Wall: Sandbag shelf - Visible"
cw_shelf_sandbags = "Castle Wall: Sandbag shelf - Invisible"
# Castle Wall towers main locations
cwr_bottom = "Castle Wall: Above bottom right tower door"
cwl_bottom = "Castle Wall: Above bottom left tower door"
cwl_bridge = "Castle Wall: Left tower child ledge"
# Castle Wall 3-hit breakables
cw_save_slab1 = "Castle Wall: Central rampart savepoint slab - Item 1"
cw_save_slab2 = "Castle Wall: Central rampart savepoint slab - Item 2"
cw_save_slab3 = "Castle Wall: Central rampart savepoint slab - Item 3"
cw_save_slab4 = "Castle Wall: Central rampart savepoint slab - Item 4"
cw_save_slab5 = "Castle Wall: Central rampart savepoint slab - Item 5"
cw_drac_slab1 = "Castle Wall: Dracula cutscene switch slab - Item 1"
cw_drac_slab2 = "Castle Wall: Dracula cutscene switch slab - Item 2"
cw_drac_slab3 = "Castle Wall: Dracula cutscene switch slab - Item 3"
cw_drac_slab4 = "Castle Wall: Dracula cutscene switch slab - Item 4"
cw_drac_slab5 = "Castle Wall: Dracula cutscene switch slab - Item 5"
# Castle Wall sub-weapons
cw_ground_left = "Castle Wall: Ground gatehouse - Left"
cw_ground_right = "Castle Wall: Ground gatehouse - Right"
cw_shelf_torch = "Castle Wall: Sandbag shelf floor torch"
cw_pillar = "Castle Wall: Central rampart broken pillar"
# Villa front yard main locations
villafy_outer_gate_l = "Villa: Outer front gate - Left"
villafy_outer_gate_r = "Villa: Outer front gate - Right"
villafy_inner_gate = "Villa: Inner front gate dog food"
villafy_dog_platform = "Villa: Outer front gate platform"
villafy_gate_marker = "Villa: Front yard cross grave near gates"
villafy_villa_marker = "Villa: Front yard cross grave near porch"
villafy_tombstone = "Villa: Front yard visitor's tombstone"
villafy_fountain_fl = "Villa: Midnight fountain - Front-left"
villafy_fountain_fr = "Villa: Midnight fountain - Front-right"
villafy_fountain_ml = "Villa: Midnight fountain - Middle-left"
villafy_fountain_mr = "Villa: Midnight fountain - Middle-right"
villafy_fountain_rl = "Villa: Midnight fountain - Rear-left"
villafy_fountain_rr = "Villa: Midnight fountain - Rear-right"
# Villa foyer main locations
villafo_sofa = "Villa: Foyer sofa"
villafo_pot_r = "Villa: Foyer upper-right pot"
villafo_pot_l = "Villa: Foyer upper-left pot"
villafo_rear_r = "Villa: Foyer lower level - Rear-right"
villafo_rear_l = "Villa: Foyer lower level - Rear-left"
villafo_mid_l = "Villa: Foyer lower level - Middle-left"
villafo_front_r = "Villa: Foyer lower level - Front-right"
villafo_front_l = "Villa: Foyer lower level - Front-left"
villafo_serv_ent = "Villa: Servants' entrance"
# Villa empty breakables
villafo_mid_r = "Villa: Foyer lower level - Middle-right"
# Villa 3-hit breakables
villafo_chandelier1 = "Villa: Foyer chandelier - Item 1"
villafo_chandelier2 = "Villa: Foyer chandelier - Item 2"
villafo_chandelier3 = "Villa: Foyer chandelier - Item 3"
villafo_chandelier4 = "Villa: Foyer chandelier - Item 4"
villafo_chandelier5 = "Villa: Foyer chandelier - Item 5"
# Villa living area main locations
villala_hallway_stairs = "Villa: Rose garden staircase bottom"
villala_bedroom_chairs = "Villa: Bedroom near chairs"
villala_bedroom_bed = "Villa: Bedroom near bed"
villala_vincent = "Villa: Vincent"
villala_slivingroom_table = "Villa: Mary's room table"
villala_storeroom_l = "Villa: Storeroom - Left"
villala_storeroom_r = "Villa: Storeroom - Right"
villala_storeroom_s = "Villa: Storeroom statue"
villala_diningroom_roses = "Villa: Dining room rose vase"
villala_archives_table = "Villa: Archives table"
villala_archives_rear = "Villa: Archives rear corner"
villala_llivingroom_lion = "Villa: Living room lion head"
villala_llivingroom_pot_r = "Villa: Living room - Right pot"
villala_llivingroom_pot_l = "Villa: Living room - Left pot"
villala_llivingroom_light = "Villa: Living room ceiling light"
villala_llivingroom_painting = "Villa: Living room clawed painting"
villala_exit_knight = "Villa: Maze garden exit knight"
# Villa maze main locations
villam_malus_torch = "Villa: Front maze garden - Malus start torch"
villam_malus_bush = "Villa: Front maze garden - Malus's hiding bush"
villam_frankieturf_r = "Villa: Front maze garden - Frankie's right dead-end"
villam_frankieturf_l = "Villa: Front maze garden - Frankie's left dead-end"
villam_frankieturf_ru = "Villa: Front maze garden - Frankie's right dead-end urn"
villam_fgarden_f = "Villa: Rear maze garden - Iron Thorn Fenced area - Front"
villam_fgarden_mf = "Villa: Rear maze garden - Iron Thorn Fenced area - Mid-front"
villam_fgarden_mr = "Villa: Rear maze garden - Iron Thorn Fenced area - Mid-rear"
villam_fgarden_r = "Villa: Rear maze garden - Iron Thorn Fenced area - Rear"
villam_rplatform_de = "Villa: Rear maze garden - Viewing platform dead-end"
villam_exit_de = "Villa: Rear maze garden - Past-exit dead-end"
villam_serv_path = "Villa: Servants' path small alcove"
villam_crypt_ent = "Villa: Crypt entrance"
villam_crypt_upstream = "Villa: Crypt bridge upstream"
# Villa crypt main locations
villac_ent_l = "Villa: Crypt - Left from entrance"
villac_ent_r = "Villa: Crypt - Right from entrance"
villac_wall_l = "Villa: Crypt - Left wall"
villac_wall_r = "Villa: Crypt - Right wall"
villac_coffin_r = "Villa: Crypt - Right of coffin"
# Villa sub-weapons
villala_hallway_l = "Villa: Hallway near rose garden stairs - Left"
villala_hallway_r = "Villa: Hallway near rose garden stairs - Right"
villala_slivingroom_mirror = "Villa: Mary's room corner"
villala_archives_entrance = "Villa: Archives near entrance"
villam_fplatform = "Villa: Front maze garden - Viewing platform"
villam_rplatform = "Villa: Rear maze garden - Viewing platform"
villac_coffin_l = "Villa: Crypt - Left of coffin"
# Tunnel main locations
tunnel_landing = "Tunnel: Landing point"
tunnel_landing_rc = "Tunnel: Landing point rock crusher"
tunnel_stone_alcove_l = "Tunnel: Stepping stone alcove - Left"
tunnel_twin_arrows = "Tunnel: Twin arrow signs"
tunnel_lonesome_bucket = "Tunnel: Near lonesome bucket"
tunnel_lbucket_quag = "Tunnel: Lonesome bucket poison pit"
tunnel_lbucket_albert = "Tunnel: Lonesome bucket-Albert junction"
tunnel_albert_camp = "Tunnel: Albert's campsite"
tunnel_albert_quag = "Tunnel: Albert's poison pit"
tunnel_gondola_rc_sdoor_r = "Tunnel: Gondola rock crusher sun door - Right"
tunnel_gondola_rc_sdoor_m = "Tunnel: Gondola rock crusher sun door - Middle"
tunnel_gondola_rc = "Tunnel: Gondola rock crusher"
tunnel_rgondola_station = "Tunnel: Red gondola station"
tunnel_gondola_transfer = "Tunnel: Gondola transfer point"
tunnel_corpse_bucket_quag = "Tunnel: Corpse bucket poison pit"
tunnel_corpse_bucket_mdoor_r = "Tunnel: Corpse bucket moon door - Right"
tunnel_shovel_quag_start = "Tunnel: Shovel poison pit start"
tunnel_exit_quag_start = "Tunnel: Exit door poison pit start"
tunnel_shovel_quag_end = "Tunnel: Shovel poison pit end"
tunnel_exit_quag_end = "Tunnel: Exit door poison pit end"
tunnel_shovel = "Tunnel: Shovel"
tunnel_shovel_save = "Tunnel: Shovel zone save junction"
tunnel_shovel_mdoor_l = "Tunnel: Shovel zone moon door - Left"
tunnel_shovel_sdoor_l = "Tunnel: Shovel zone sun door - Left"
tunnel_shovel_sdoor_m = "Tunnel: Shovel zone sun door - Middle"
# Tunnel 3-hit breakables
tunnel_arrows_rock1 = "Tunnel: Twin arrow signs rock - Item 1"
tunnel_arrows_rock2 = "Tunnel: Twin arrow signs rock - Item 2"
tunnel_arrows_rock3 = "Tunnel: Twin arrow signs rock - Item 3"
tunnel_arrows_rock4 = "Tunnel: Twin arrow signs rock - Item 4"
tunnel_arrows_rock5 = "Tunnel: Twin arrow signs rock - Item 5"
tunnel_bucket_quag_rock1 = "Tunnel: Lonesome bucket poison pit rock - Item 1"
tunnel_bucket_quag_rock2 = "Tunnel: Lonesome bucket poison pit rock - Item 2"
tunnel_bucket_quag_rock3 = "Tunnel: Lonesome bucket poison pit rock - Item 3"
# Tunnel sub-weapons
tunnel_stone_alcove_r = "Tunnel: Stepping stone alcove - Right"
tunnel_lbucket_mdoor_l = "Tunnel: Lonesome bucket moon door"
tunnel_gondola_rc_sdoor_l = "Tunnel: Gondola rock crusher sun door - Left"
tunnel_corpse_bucket_mdoor_l = "Tunnel: Corpse bucket moon door - Left"
tunnel_shovel_mdoor_r = "Tunnel: Shovel zone moon door - Right"
tunnel_shovel_sdoor_r = "Tunnel: Shovel zone sun door - Right"
# Underground Waterway main locations
uw_near_ent = "Underground Waterway: Near entrance corridor"
uw_across_ent = "Underground Waterway: Across from entrance"
uw_poison_parkour = "Underground Waterway: Across poison parkour ledges"
uw_waterfall_alcove = "Underground Waterway: Waterfall alcove ledge"
uw_carrie1 = "Underground Waterway: Carrie crawlspace corridor - First left"
uw_carrie2 = "Underground Waterway: Carrie crawlspace corridor - Second left"
uw_bricks_save = "Underground Waterway: Brick platforms save corridor"
uw_above_skel_ledge = "Underground Waterway: Above skeleton crusher ledge"
# Underground Waterway 3-hit breakables
uw_first_ledge1 = "Underground Waterway: First poison parkour ledge - Item 1"
uw_first_ledge2 = "Underground Waterway: First poison parkour ledge - Item 2"
uw_first_ledge3 = "Underground Waterway: First poison parkour ledge - Item 3"
uw_first_ledge4 = "Underground Waterway: First poison parkour ledge - Item 4"
uw_first_ledge5 = "Underground Waterway: First poison parkour ledge - Item 5"
uw_first_ledge6 = "Underground Waterway: First poison parkour ledge - Item 6"
uw_in_skel_ledge1 = "Underground Waterway: Inside skeleton crusher ledge - Item 1"
uw_in_skel_ledge2 = "Underground Waterway: Inside skeleton crusher ledge - Item 2"
uw_in_skel_ledge3 = "Underground Waterway: Inside skeleton crusher ledge - Item 3"
# Castle Center basement main locations
ccb_skel_hallway_ent = "Castle Center: Entrance hallway"
ccb_skel_hallway_jun = "Castle Center: Basement hallway junction"
ccb_skel_hallway_tc = "Castle Center: Torture chamber hallway"
ccb_behemoth_l_ff = "Castle Center: Behemoth arena - Left far-front torch"
ccb_behemoth_l_mf = "Castle Center: Behemoth arena - Left mid-front torch"
ccb_behemoth_l_mr = "Castle Center: Behemoth arena - Left mid-rear torch"
ccb_behemoth_l_fr = "Castle Center: Behemoth arena - Left far-rear torch"
ccb_behemoth_r_ff = "Castle Center: Behemoth arena - Right far-front torch"
ccb_behemoth_r_mf = "Castle Center: Behemoth arena - Right mid-front torch"
ccb_behemoth_r_mr = "Castle Center: Behemoth arena - Right mid-rear torch"
ccb_behemoth_r_fr = "Castle Center: Behemoth arena - Right far-rear torch"
ccb_mandrag_shelf_l = "Castle Center: Mandragora shelf - Left"
ccb_mandrag_shelf_r = "Castle Center: Mandragora shelf - Right"
ccb_torture_rack = "Castle Center: Torture chamber instrument rack"
ccb_torture_rafters = "Castle Center: Torture chamber rafters"
# Castle Center elevator room main locations
ccelv_near_machine = "Castle Center: Near elevator room machine"
ccelv_atop_machine = "Castle Center: Atop elevator room machine"
ccelv_pipes = "Castle Center: Elevator pipe device"
ccelv_staircase = "Castle Center: Elevator room staircase"
# Castle Center factory floor main locations
ccff_redcarpet_knight = "Castle Center: Red carpet hall knight"
ccff_gears_side = "Castle Center: Gear room side"
ccff_gears_mid = "Castle Center: Gear room center"
ccff_gears_corner = "Castle Center: Gear room corner"
ccff_lizard_knight = "Castle Center: Lizard locker knight"
ccff_lizard_pit = "Castle Center: Lizard locker room near pit"
ccff_lizard_corner = "Castle Center: Lizard locker room corner"
# Castle Center lizard lab main locations
ccll_brokenstairs_floor = "Castle Center: Broken staircase floor"
ccll_brokenstairs_knight = "Castle Center: Broken staircase knight"
ccll_brokenstairs_save = "Castle Center: Above broken staircase savepoint"
ccll_glassknight_l = "Castle Center: Stained Glass Knight room - Left"
ccll_glassknight_r = "Castle Center: Stained Glass Knight room - Right"
ccll_butlers_door = "Castle Center: Butler bros. room near door"
ccll_butlers_side = "Castle Center: Butler bros. room inner"
ccll_cwhall_butlerflames_past = "Castle Center: Past butler room flamethrowers"
ccll_cwhall_flamethrower = "Castle Center: Inside cracked wall hallway flamethrower"
ccll_cwhall_cwflames = "Castle Center: Past upper cracked wall flamethrowers"
ccll_cwhall_wall = "Castle Center: Inside upper cracked wall"
ccll_heinrich = "Castle Center: Heinrich Meyer"
# Castle Center library main locations
ccl_bookcase = "Castle Center: Library bookshelf"
# Castle Center invention area main locations
ccia_nitro_crates = "Castle Center: Nitro room crates"
ccia_nitro_shelf_h = "Castle Center: Magical Nitro shelf - Heinrich side"
ccia_nitro_shelf_i = "Castle Center: Magical Nitro shelf - Invention side"
ccia_nitrohall_torch = "Castle Center: Past nitro room flamethrowers"
ccia_nitrohall_flamethrower = "Castle Center: Inside nitro hallway flamethrower"
ccia_inventions_crusher = "Castle Center: Invention room spike crusher door"
ccia_inventions_maids = "Castle Center: Invention room maid sisters door"
ccia_inventions_round = "Castle Center: Invention room round machine"
ccia_inventions_famicart = "Castle Center: Invention room giant Famicart"
ccia_inventions_zeppelin = "Castle Center: Invention room zeppelin"
ccia_maids_outer = "Castle Center: Maid sisters room outer table"
ccia_maids_inner = "Castle Center: Maid sisters room inner table"
ccia_maids_vase = "Castle Center: Maid sisters room vase"
ccia_stairs_knight = "Castle Center: Hell Knight landing corner knight"
# Castle Center sub-weapons
ccb_skel_hallway_ba = "Castle Center: Behemoth arena hallway"
ccelv_switch = "Castle Center: Near elevator switch"
ccff_lizard_near_knight = "Castle Center: Near lizard locker knight"
# Castle Center lizard lockers
ccff_lizard_locker_nfr = "Castle Center: Far-right near-side lizard locker"
ccff_lizard_locker_nmr = "Castle Center: Mid-right near-side lizard locker"
ccff_lizard_locker_nml = "Castle Center: Mid-left near-side lizard locker"
ccff_lizard_locker_nfl = "Castle Center: Far-left near-side lizard locker"
ccff_lizard_locker_fl = "Castle Center: Left far-side lizard locker"
ccff_lizard_locker_fr = "Castle Center: Right far-side lizard locker"
# Castle Center 3-hit breakables
ccb_behemoth_crate1 = "Castle Center: Behemoth arena crate - Item 1"
ccb_behemoth_crate2 = "Castle Center: Behemoth arena crate - Item 2"
ccb_behemoth_crate3 = "Castle Center: Behemoth arena crate - Item 3"
ccb_behemoth_crate4 = "Castle Center: Behemoth arena crate - Item 4"
ccb_behemoth_crate5 = "Castle Center: Behemoth arena crate - Item 5"
ccelv_stand1 = "Castle Center: Elevator room unoccupied statue stand - Item 1"
ccelv_stand2 = "Castle Center: Elevator room unoccupied statue stand - Item 2"
ccelv_stand3 = "Castle Center: Elevator room unoccupied statue stand - Item 3"
ccff_lizard_slab1 = "Castle Center: Lizard locker room slab - Item 1"
ccff_lizard_slab2 = "Castle Center: Lizard locker room slab - Item 2"
ccff_lizard_slab3 = "Castle Center: Lizard locker room slab - Item 3"
ccff_lizard_slab4 = "Castle Center: Lizard locker room slab - Item 4"
# Duel Tower main locations
dt_stones_start = "Duel Tower: Stepping stone path start"
dt_werebull_arena = "Duel Tower: Above Were-bull arena"
dt_ibridge_l = "Duel Tower: Invisible bridge balcony - Left"
dt_ibridge_r = "Duel Tower: Invisible bridge balcony - Right"
# Duel Tower sub-weapons
dt_stones_end = "Duel Tower: Stepping stone path end"
# Tower of Execution main locations
toe_midsavespikes_r = "Tower of Execution: Past mid-savepoint spikes - Right"
toe_midsavespikes_l = "Tower of Execution: Past mid-savepoint spikes - Left"
toe_elec_grate = "Tower of Execution: Electric grate ledge"
toe_ibridge = "Tower of Execution: Invisible bridge ledge"
toe_top = "Tower of Execution: Guillotine tower top level"
toe_keygate_l = "Tower of Execution: Key gate alcove - Left"
# Tower of Execution 3-hit breakables
toe_ledge1 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 1"
toe_ledge2 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 2"
toe_ledge3 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 3"
toe_ledge4 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 4"
toe_ledge5 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 5"
# Tower of Execution sub-weapons
toe_keygate_r = "Tower of Execution: Key gate alcove - Right"
# Tower of Science main locations
tosci_elevator = "Tower of Science: Elevator hallway"
tosci_plain_sr = "Tower of Science: Plain sight side room"
tosci_stairs_sr = "Tower of Science: Staircase side room"
tosci_three_door_hall = "Tower of Science: Pick-a-door hallway locked middle room"
tosci_ibridge_t = "Tower of Science: Invisible bridge platform torch"
tosci_conveyor_sr = "Tower of Science: Spiky conveyor side room"
tosci_exit = "Tower of Science: Exit hallway"
tosci_key3_r = "Tower of Science: Locked Key3 room - Right"
tosci_key3_l = "Tower of Science: Locked Key3 room - Left"
# Tower of Science 3-hit breakables
tosci_ibridge_b1 = "Tower of Science: Invisible bridge platform crate - Item 1"
tosci_ibridge_b2 = "Tower of Science: Invisible bridge platform crate - Item 2"
tosci_ibridge_b3 = "Tower of Science: Invisible bridge platform crate - Item 3"
tosci_ibridge_b4 = "Tower of Science: Invisible bridge platform crate - Item 4"
tosci_ibridge_b5 = "Tower of Science: Invisible bridge platform crate - Item 5"
tosci_ibridge_b6 = "Tower of Science: Invisible bridge platform crate - Item 6"
# Tower of Science sub-weapons
tosci_key3_m = "Tower of Science: Locked Key3 room - Middle"
# Tower of Sorcery main locations
tosor_stained_tower = "Tower of Sorcery: Stained glass tower"
tosor_savepoint = "Tower of Sorcery: Mid-savepoint platform"
tosor_trickshot = "Tower of Sorcery: Trick shot from mid-savepoint platform"
tosor_yellow_bubble = "Tower of Sorcery: Above yellow bubble"
tosor_blue_platforms = "Tower of Sorcery: Above tiny blue platforms start"
tosor_side_isle = "Tower of Sorcery: Lone red platform side island"
tosor_ibridge = "Tower of Sorcery: Invisible bridge platform"
# Room of Clocks main locations
roc_ent_l = "Room of Clocks: Left from entrance hallway"
roc_cont_r = "Room of Clocks: Right of Contract"
roc_ent_r = "Room of Clocks: Right from entrance hallway"
# Room of Clocks sub-weapons
roc_elev_l = "Room of Clocks: Left of elevator hallway"
roc_elev_r = "Room of Clocks: Right of elevator hallway"
# Room of Clocks empty breakables
roc_cont_l = "Room of Clocks: Left of Contract"
roc_exit = "Room of Clocks: Left of exit"
# Clock Tower main locations
ct_gearclimb_corner = "Clock Tower: Gear climb room corner"
ct_gearclimb_side = "Clock Tower: Gear climb room side"
ct_bp_chasm_fl = "Clock Tower: Bone Pillar chasm room - Front-left"
ct_bp_chasm_fr = "Clock Tower: Bone Pillar chasm room - Front-right"
ct_bp_chasm_k = "Clock Tower: Bone Pillar chasm room key alcove"
ct_finalroom_platform = "Clock Tower: Final room key ledge"
# Clock Tower 3-hit breakables
ct_gearclimb_battery_slab1 = "Clock Tower: Gear climb room beneath battery slab - Item 1"
ct_gearclimb_battery_slab2 = "Clock Tower: Gear climb room beneath battery slab - Item 2"
ct_gearclimb_battery_slab3 = "Clock Tower: Gear climb room beneath battery slab - Item 3"
ct_gearclimb_door_slab1 = "Clock Tower: Gear climb room beneath door slab - Item 1"
ct_gearclimb_door_slab2 = "Clock Tower: Gear climb room beneath door slab - Item 2"
ct_gearclimb_door_slab3 = "Clock Tower: Gear climb room beneath door slab - Item 3"
ct_finalroom_door_slab1 = "Clock Tower: Final room entrance slab - Item 1"
ct_finalroom_door_slab2 = "Clock Tower: Final room entrance slab - Item 2"
ct_finalroom_renon_slab1 = "Clock Tower: Renon's final offers slab - Item 1"
ct_finalroom_renon_slab2 = "Clock Tower: Renon's final offers slab - Item 2"
ct_finalroom_renon_slab3 = "Clock Tower: Renon's final offers slab - Item 3"
ct_finalroom_renon_slab4 = "Clock Tower: Renon's final offers slab - Item 4"
ct_finalroom_renon_slab5 = "Clock Tower: Renon's final offers slab - Item 5"
ct_finalroom_renon_slab6 = "Clock Tower: Renon's final offers slab - Item 6"
ct_finalroom_renon_slab7 = "Clock Tower: Renon's final offers slab - Item 7"
ct_finalroom_renon_slab8 = "Clock Tower: Renon's final offers slab - Item 8"
# Clock Tower sub-weapons
ct_bp_chasm_rl = "Clock Tower: Bone Pillar chasm room - Rear-left"
ct_finalroom_fr = "Clock Tower: Final room floor - front-right"
ct_finalroom_fl = "Clock Tower: Final room floor - front-left"
ct_finalroom_rr = "Clock Tower: Final room floor - rear-right"
ct_finalroom_rl = "Clock Tower: Final room floor - rear-left"
# Castle Keep main locations
ck_flame_l = "Castle Keep: Left Dracula door flame"
ck_flame_r = "Castle Keep: Right Dracula door flame"
ck_behind_drac = "Castle Keep: Behind Dracula's chamber"
ck_cube = "Castle Keep: Dracula's floating cube"
# Renon's shop locations
renon1 = "Renon's shop: Roast Chicken purchase"
renon2 = "Renon's shop: Roast Beef purchase"
renon3 = "Renon's shop: Healing Kit purchase"
renon4 = "Renon's shop: Purifying purchase"
renon5 = "Renon's shop: Cure Ampoule purchase"
renon6 = "Renon's shop: Sun Card purchase"
renon7 = "Renon's shop: Moon Card purchase"
# Events
forest_boss_one = "Forest of Silence: King Skeleton 1"
forest_boss_two = "Forest of Silence: Were-tiger"
forest_boss_three = "Forest of Silence: King Skeleton 2"
cw_boss = "Castle Wall: Bone Dragons"
villa_boss_one = "Villa: J. A. Oldrey"
villa_boss_two = "Villa: Undead Maiden"
uw_boss = "Underground Waterway: Lizard-man trio"
cc_boss_one = "Castle Center: Behemoth"
cc_boss_two = "Castle Center: Rosa/Camilla"
dt_boss_one = "Duel Tower: Were-jaguar"
dt_boss_two = "Duel Tower: Werewolf"
dt_boss_three = "Duel Tower: Were-bull"
dt_boss_four = "Duel Tower: Were-tiger"
roc_boss = "Room of Clocks: Death/Actrise"
ck_boss_one = "Castle Keep: Renon"
ck_boss_two = "Castle Keep: Vincent"
cc_behind_the_seal = "Castle Center: Behind the seal"
the_end = "Dracula"

2865
worlds/cv64/data/patches.py Normal file

File diff suppressed because it is too large Load Diff

63
worlds/cv64/data/rname.py Normal file
View File

@ -0,0 +1,63 @@
forest_of_silence = "Forest of Silence"
forest_start = "Forest of Silence: first half"
forest_mid = "Forest of Silence: second half"
forest_end = "Forest of Silence: end area"
castle_wall = "Castle Wall"
cw_start = "Castle Wall: main area"
cw_exit = "Castle Wall: exit room"
cw_ltower = "Castle Wall: left tower"
villa = "Villa"
villa_start = "Villa: dog gates"
villa_main = "Villa: main interior"
villa_storeroom = "Villa: storeroom"
villa_archives = "Villa: archives"
villa_maze = "Villa: maze"
villa_servants = "Villa: servants entrance"
villa_crypt = "Villa: crypt"
tunnel = "Tunnel"
tunnel_start = "Tunnel: first half"
tunnel_end = "Tunnel: second half"
underground_waterway = "Underground Waterway"
uw_main = "Underground Waterway: main area"
uw_end = "Underground Waterway: end"
castle_center = "Castle Center"
cc_main = "Castle Center: main area"
cc_crystal = "Castle Center: big crystal"
cc_torture_chamber = "Castle Center: torture chamber"
cc_library = "Castle Center: library"
cc_elev_top = "Castle Center: elevator top"
duel_tower = "Duel Tower"
dt_main = "Duel Tower"
tower_of_sorcery = "Tower of Sorcery"
tosor_main = "Tower of Sorcery"
tower_of_execution = "Tower of Execution"
toe_main = "Tower of Execution: main area"
toe_ledge = "Tower of Execution: gated ledge"
tower_of_science = "Tower of Science"
tosci_start = "Tower of Science: turret lab"
tosci_three_doors = "Tower of Science: locked key1 room"
tosci_conveyors = "Tower of Science: spiky conveyors"
tosci_key3 = "Tower of Science: locked key3 room"
room_of_clocks = "Room of Clocks"
roc_main = "Room of Clocks"
clock_tower = "Clock Tower"
ct_start = "Clock Tower: start"
ct_middle = "Clock Tower: middle"
ct_end = "Clock Tower: end"
castle_keep = "Castle Keep"
ck_main = "Castle Keep: exterior"
ck_drac_chamber = "Castle Keep: Dracula's chamber"
renon = "Renon's shop"

View File

@ -0,0 +1,148 @@
# Castlevania 64
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
All items that you would normally pick up throughout the game, be it from candles, breakables, or sitting out, have been
moved around. This includes the key items that the player would normally need to find to progress in some stages, which can
now be found outside their own stages, so returning to previously-visited stages will very likely be necessary (see: [How do
I jump to a different stage?](#how-do-i-jump-to-a-different-stage?)). The positions of the stages can optionally be randomized
too, so you may start out in Duel Tower and get Forest of Silence as your penultimate stage before Castle Keep, amongst
many other possibilities.
## How do I jump to a different stage?
Instant travel to an earlier or later stage is made possible through the Warp Menu, a major addition to the game that can
be pulled up while not in a boss fight by pressing START while holding Z and R. By finding Special1 jewels (the item that
unlocks Hard Mode in vanilla Castlevania 64), more destinations become available to be selected on this menu. The destinations
on the list are randomized per seed and the first one, which requires no Special1s to select, will always be your starting
area.
NOTE: Regardless of which option on the menu you are currently highlighting, you can hold Z or R while making your selection
to return to Villa's crypt or Castle Center's top elevator room respectively, provided you've already been to that place at
least once. This can make checking out both character stages at the start of a route divergence far less of a hassle.
## Can I do everything as one character?
Yes! The Villa end-of-level coffin has had its behavior modified so that which character stage slot it sends you to
depends on the time of day, and likewise both bridges at the top of Castle Center's elevator are intact so both exits are
reachable regardless of who you are. With these changes in game behavior, every stage can be accessed by any character
in a singular run.
NOTE: By holding L while loading into a map (this can even be done while cancelling out of the Warp Menu), you can swap to
the other character you are not playing as, and/or hold C-Up to swap to and from the characters' alternate costumes. Unless
you have Carrie Logic enabled, and you are not playing as her, switching should never be necessary.
## What is the goal of Castlevania 64 when randomized?
Make it to Castle Keep, enter Dracula's chamber, and defeat him to trigger an ending and complete your goal. Whether you
get your character's good or bad ending does **not** matter; the goal will send regardless. Options exist to force a specific
ending for those who prefer a specific one.
Dracula's chamber's entrance door is initially locked until whichever of the following objectives that was specified on your
YAML under `draculas_condition` is completed:
- `crystal`: Activate the big crystal in the basement of Castle Center. Doing this entails finding two Magical Nitros and
two Mandragoras to blow up both cracked walls (see: [How does the Nitro transport work in this?](#how-does-the-nitro-transport-work-in-this?)).
Behemoth and Rosa/Camilla do **NOT** have to be defeated.
- `bosses`: Kill bosses with visible health meters to earn Trophies. The number of Trophies required can be specified under
`bosses_required`.
- `special2s`: Find enough Special2 jewels (the item that normally unlocks alternate costumes) that are shuffled in the
regular item pool. The total amount and percent needed can be specified under `total_special2s` and `percent_special2s_required` respectively.
If `none` was specified, then there is no objective. Dracula's chamber door is unlocked from the start, and you merely have to reach it.
## What items and locations get shuffled?
Inventory items, jewels, moneybags, and PowerUps are all placed in the item pool by default. Randomizing Sub-weapons is optional,
and they can be shuffled in their own separate pool or in the main item pool. An optional hack can be enabled to make your
old sub-weapon drop behind you when you receive a different one, so you can pick it up again if you still want it. Location
checks by default include freestanding items, items from one-hit breakables, and the very few items given through NPC text. Additional
locations that can be toggled are:
- Objects that break in three hits.
- Sub-weapon locations if they have been shuffled anywhere.
- Seven items sold by the shopkeeper Renon.
- The two items beyond the crawlspace in Waterway that normally require Carrie, if Carrie Logic is on.
- The six items inside the Lizard-man generators in Castle Center that open randomly to spawn Lizard-men. These are particularly annoying!
## How does the Nitro transport work in this?
Two Magical Nitros and two Mandragoras are placed into the item pool for blowing up the cracked walls in Castle Center
and two randomized items are placed on both of their shelves. The randomized Magical Nitro will **NOT** kill you upon landing
or taking damage, so don't panic when you receive one! Hazardous Waste Dispoal bins are disabled and the basement crack with
a seal will not let you set anything at it until said seal is removed so none of the limited ingredients can be wasted.
In short, Nitro is still in, explode-y business is not! Unless you turn on explosive DeathLink, that is...
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. The exception is if sub-weapons
are shuffled in their own pool, in which case they will only appear in your world in sub-weapon spots.
## What does another world's item look like in Castlevania 64?
An item belonging to another world will show up as that item if it's from another Castlevania 64 world, or one of two
Archipelago logo icons if it's from a different game entirely. If the icon is big and has an orange arrow in the top-right
corner, it is a progression item for that world; definitely get these! Otherwise, if it's small and with no arrow, it is
either filler, useful, or a trap.
When you pick up someone else's item, you will not receive anything and the item textbox will show up to announce what you
found and who it was for. The color of the text will tell you its classification:
- <font color="moccasin">Light brown-ish</font>: Common
- <font color="white">White</font>/<font color="yellow">Yellow</font>: Useful
- <font color="yellow">Yellow</font>/<font color="lime">Green</font>: Progression
- <font color="yellow">Yellow</font>/<font color="red">Red</font>: Trap
## When the player receives an item, what happens?
A textbox containing the name of the item and the player who sent it will appear, and they will get it.
Just like the textbox that appears when sending an item, the color of the text will tell you its classification.
NOTE: You can press B to close the item textbox instantly and get through your item queue quicker.
## What tricks and glitches should I know for Hard Logic?
The following tricks always have a chance to be required:
- Left Tower Skip in Castle Wall
- Copper Door Skip in Villa (both characters have their own methods for this)
- Waterfall Skip if you travel backwards into Underground Waterway
- Slope Jump to Room of Clocks from Castle Keep
- Jump to the gated ledge from the level above in Tower of Execution
Enabling Carrie Logic will also expect the following:
- Orb-sniping dogs through the front gates in Villa
Library Skip is **NOT** logically expected on any setting. The basement hallway crack will always logically expect two Nitros
and two Mandragoras even with Hard Logic on due to the possibility of wasting a pair on the upper wall, after managing
to skip past it. And plus, the RNG manip may not even be possible after picking up all the items in the Nitro room.
## What are the item name groups?
The groups you can use for Castlevania 64 are `bomb` and `ingredient`, both of which will hint randomly for either a
Magical Nitro or Mandragora.
## What are the location name groups?
In Castlevania 64, every location that is specific to a stage is part of a location group under that stage's name.
So if you want to exclude all of, say, Duel Tower, you can do so by just excluding "Duel Tower" as a whole.
## I'm stuck and/or I can't find this hinted location...is there a map tracker?
At the moment, no map tracker exists. [Here](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds/cv64/docs/obscure_checks.md)
is a list of many checks that someone could very likely miss, with instructions on how to find them. See if the check you
are missing is on there and if it isn't, or you still can't find it, reach out in the [Archipelago Discord server](https://discord.gg/archipelago)
to inquire about having the list updated if you think it should be.
If you are new to this randomizer, it is strongly recommended to play with the Countdown option enabled to at least give you a general
idea of where you should be looking if you get completely stuck. It can track the total number of unchecked locations in the
area you are currently in, or the total remaining majors.
## Why does the game stop working when I sit on the title screen for too long?
This is an issue that existed with Castlevania 64 on mupen64plus way back in 2017, and BizHawk never updated their
mupen64plus core since it was fixed way back then. This is a Castlevania 64 in general problem that happens even with the
vanilla ROM, so there's not much that can done about it besides opening an issue to them (which [has been done](https://github.com/TASEmulators/BizHawk/issues/3670))
and hoping they update their mupen64plus core one day...
## How the f*** do I set Nitro/Mandragora?
<font color="yellow">(>)</font>

View File

@ -0,0 +1,429 @@
# Obscure locations in the AP Castlevania 64 randomizer
## Forest of Silence
#### Invisible bridge platform
A square platform floating off to the side of the broken bridge between the three-crypt and Werewolf areas. There's an
invisible bridge connecting it with the middle piece on the broken bridge that you can cross over to reach it. This is
where you normally get the Special1 in vanilla that unlocks Hard Mode.
### Invisible Items
#### Dirge maiden pedestal plaque
This plaque in question can be found on the statue pedestal with a torch on it in the area right after the first switch gate,
near the cliff where the breakable rock can be found. The plaque reads "A maiden sings a dirge" if you check it after you
pick up the item there, hence the name of all the locations in this area.
#### Werewolf statue plaque
The plaque on the statue pedestal in the area inhabited by the Werewolf. Reading this plaque after picking up the item
says it's "the lady who blesses and restores."
### 3-Hit Breakables
#### Dirge maiden pedestal rock
This rock can be found near the cliff behind the empty above-mentioned dirge maiden pedestal. Normally has a ton of money
in vanilla, contains 5 checks in rando.
#### Bat archway rock
After the broken bridge containing the invisible pathway to the Special1 in vanilla, this rock is off to the side in front
of the gate frame with a swarm of bats that come at you, before the Werewolf's territory. Contains 4 checks. If you are new
to speedrunning the vanilla game and haven't yet learned the RNG manip strats, this is a guranteed spot to find a PowerUp at.
## Castle Wall
#### Above bottom right/left tower door
These checks are located on top of the bottom doorways inside the both the Left and Right Tower. You have to drop from above
to reach them. In the case of the left tower, it's probably easiest to wait on the green platform directly above it until it flips.
#### Left tower child ledge
When you reach the bridge of four rotating green platforms, look towards the pillar in the center of the room (hold C-up to
enter first person view), and you'll see this. There's an invisible bridge between the rotating platforms and the tiny ledge
that you can use to get to and from it. In Legacy of Darkness, it is on this ledge where one of Henry's children is found.
### Invisible Items
#### Sandbag shelf - Left
If you thought the PowerUp on this shelf in vanilla CV64 was the only thing here, then you'd be wrong! Hidden inside the
sandbags near the item is another item you can pick up before subsequent checks on this spot yield "only sand and gravel".
Legacy took this item out entirely, interestingly enough.
### 3-Hit Breakables
#### Upper rampart savepoint slab
After killing the two White Dragons and flipping their switch, drop down onto this platform from the top, and you'll find
it near the White Jewel. Contains 5 checks that are all normally Red Jewels in vanilla, making it an excellent place to
fuel up at if you're not doing Left Tower Skip. Just be careful of the infinitely spawning skeletons!
#### Dracula switch slab
Located behind the door that you come out of at the top of the left tower where you encounter Totally Real Dracula in a
cutscene. Contains 5 checks that are all normally money; take note of all these money spots if you're plaing vanilla and
plan on trying to trigger the Renon fight.
## Villa
#### Outer front gate platform
From the start of the level, turn right, and you'll see a platform with a torch above a torch on the ground. This upper torch
is reachable via an invisible platform that you can grab and pull yourself up onto. The PAL version and onwards removed
this secret entirely, interestingly enough.
#### Front yard cross grave near gates/porch
In the Villa's front yard area are two cross-shaped grave markers that are actually 1-hit breakables just like torches.
They contain a check each.
#### Midnight fountain
At exactly midnight (0:00 on the pause screen), a pillar in the fountain's base will rise to give you access to the six
checks on the upper parts of the fountain. If you're playing with Disable Time Requirements enabled, this pillar will be
raised regardless of the current time.
#### Vincent
Vincent has a check that he will give to you by speaking to him after triggering the Rosa cutscene at 3 AM in the rose
garden. With Disable Time Requirements enabled, the Rosa cutscene will trigger at any time.
#### Living room ceiling light
In the rectangular living room with ghosts and flying skulls that come at you, there are two yellow lights on the ceiling
and one red light between them. The red light can be broken for a check; just jump directly below it and use your c-left
attack to hit it.
#### Front maze garden - Frankie's right dead-end urn
When you first enter the maze, before going to trigger the Malus cutscene, go forward, right at the one-way door, then right
at the T-junction, and you'll reach a dead-end where the Gardner is just going about his business. The urn on the left
at this dead-end can be broken for a check; it's the ONLY urn in the entire maze that can be broken like this.
#### Crypt bridge upstream
After unlocking the Copper Door, follow the stream all the way past the bridge to end up at this torch.
I see many people miss this one.
### Invisible Items
#### Front yard visitor's tombstone
The tombstone closest to the Villa building itself, in the top-right corner if approaching from the gates. If you are
familiar with the puzzle here in Cornell's quest in Legacy, it's the tombstone prepared for "anybody else who drops by
to visit".
#### Foyer sofa
The first sofa in the foyer, on the upper floor to the right.
#### Mary's room table
The table closer to the mirror on the right in the small room adjacent to the bedroom, where Mary would normally be found
in Cornell's story in Legacy.
#### Dining room rose vase
The vase of roses in the dining room that a rose falls out of in the cutscene here to warn Reinhardt/Carrie of the vampire
villager.
#### Living room clawed painting
The painting with claw marks on it above the fireplace in the middle of the living room.
#### Living room lion head
The lion head on the left wall of the living room (if you entered from one of the doors to the main hallway).
#### Maze garden exit knight
The suit of armor in the stairs room before the Maze Garden, where Renon normally introduces himself.
#### Storeroom statue
The weird statue in the back of the Storeroom. If you check it again after taking its item, the game questions why would
someone make something like it.
#### Archives table
The table in the middle of the Archives. In Legacy, this is where Oldrey's diary normally sits if you are playing Cornell.
#### Malus's hiding bush
The bush that Reinhardt/Carrie find Malus hiding in at the start of the Maze Garden chase sequence.
### 3-Hit Breakables
#### Foyer chandelier
The big chandelier above the foyer can be broken for 5 assorted items, all of which become checks in rando with the multi
hits setting on. This is the only 3-hit breakable in the entire stage. <br>
Here's a fun fact about the chandelier: for some reason, KCEK made this thing a completely separate object from every other
3-hit breakable in the game, complete with its own distinct code that I had to modify entirely separately as I was making
this rando to make the 3-hit breakable setting feasible! What fun!
## Tunnel
#### Stepping stone alcove
After the first save following the initial Spider Women encounter, take the first right you see, and you'll arrive back at
the poison river where there's a second set of stepping stones similar to the one you just jumped across earlier. Jump on
these to find the secret alcove containing one or two checks depending on whether sub-weapons are randomized anywhere or not.
### Sun/Moon Doors
In total, there are six of these throughout the entire stage. One of them you are required to open in order to leave the stage,
while the other five lead to optional side rooms containing items. These are all very skippable in the vanilla game, but in a
rando context, it is obviously *very* important you learn where all of them are for when the day comes that a Hookshot
lands behind one! If it helps, two of them are before the gondolas, while all the rest are after them.
#### Lonesome bucket moon door
After you ride up on the second elevator, turn left at the first split path you see, and you will find, as I called it, the
"Lonesome bucket". Keep moving forward past this, and you will arrive at the moon door. The only thing of value, beside a shop
point, is a sub-weapon location. So if you don't have sub-weapons shuffled anywhere, you can very much skip this one.
#### Gondola rock crusher sun door
Once you get past the first poison pit that you are literally required to platform over, go forward at the next junction
instead of left (going left is progress and will take you to the rock crusher right before the gondolas). This door notably
hides two Roast Beefs normally, making it probably the most worthwhile one to visit in vanilla.
#### Corpse bucket moon door
After the poison pit immediately following the gondola ride, you will arrive at a bucket surrounded by corpses (hence the name).
Go left here, and you will arrive at this door.
#### Shovel zone moon door
On the straight path to the end-of-level sun door are two separate poison pits on the right that you can platform over.
Both of these lead to and from the same optional area, the "shovel zone" as I call it due to the random shovel you can find
here. Follow the path near the shovel that leads away from both poison pits, and you'll arrive at a junction with a save jewel.
Go straight on at this junction to arrive at this moon door. This particular one is more notable in Legacy of Darkness as it
contains one of the locations of Henry's children.
#### Shovel zone sun door
Same as the above moon door, but go left at the save jewel junction instead of straight.
### Invisible Items
#### Twin arrow signs
From the save point after the stepping stones following the initial Spider Women encounter, travel forward until you reach a
T-junction with two arrow signs at it. The right-pointing sign here contains an item on its post.
#### Near lonesome bucket
After riding the first upwards elevator following turning left at the twin arrow signs, you'll arrive at the lonesome bucket
area, with said bucket being found if you turn left at the first opportunity after said elevator. The item here is not
found *in* the bucket, but rather on a completely unmarked spot some meters from it. This had to have been a mistake,
seeing as Legacy moved it to actually be in the bucket.
#### Shovel
Can be found by taking either platforming course on the right side of the straightaway to the end after the gondolas.
This entire zone is noteable for the fact that there's no reason for Reinhardt to come here in either game; it's only ever
required for Henry to rescue one of his children.
### 3-Hit Breakables
#### Twin arrow signs rock
Turn right at the twin arrow signs junction, and you'll find this rock at the dead-end by the river. It contains a bunch of
healing and status items that translate into 5 rando checks.
#### Lonesome bucket poison pit rock
Near the lonesome bucket is the start of a platforming course over poison water that connects near Albert's campsite...which
you could reach anyway just by traveling forward at the prior junction instead of left. So what's the point of this poison
pit, then? Look out into the middle of it, and you'll see this rock on a tiny island out in the middle of it. If you choose
to take the hard way here, your reward will be three meat checks.
## Underground Waterway
#### Carrie Crawlspace
This is located shortly after the corridor following the ledges that let you reach the first waterfall's source alcove.
Notably, only Carrie is able to crouch and go through this, making these the only checks in the *entire* game that are
hard impossible without being a specific character. So if you have Carrie Logic on and your character is Reinhardt, you'll
have to hold L while loading into a map to change to Carrie just for this one secret. If Carrie Logic is off, then these
locations will not be added and you can just skip them entirely.
### 3-Hit Breakables
#### First poison parkour ledge
Near the start of the level is a series of ledges you can climb onto and platform across to reach a corner with a lantern
that you can normally get a Cure Ampoule from. The first of these ledges can be broken for an assortment of 6 things.
#### Inside skeleton crusher ledge
To the left of the hallway entrance leading to the third switch is a long shimmy-able ledge that you can grab onto and shimmy
for a whole eternity (I implemented a setting JUST to make shimmying this ledge faster!) to get to a couple stand on-able ledges,
one of which has a lantern above it containing a check. This ledge can be broken for 3 chickens. I'd highly suggest bringing
Holy Water for this because otherwise you're forced to break it from the other, lower ledge that's here. And this ledge
will drop endless crawling skeletons on you as long as you're on it.
## Castle Center
#### Atop elevator room machine
In the elevator room, right from the entrance coming in from the vampire triplets' room, is a machine that you can press
C-Right on to get dialog reading "An enormous machine." There's a torch on top of this machine that you can reach by
climbing onto the slanted part of the walls in the room.
#### Heinrich Meyer
The friendly lizard-man who normally gives you the Chamber Key in vanilla has a check for you just like Vincent.
Yes, he has a name! And you'd best not forget it!
#### Torture chamber rafters
A check can be found in the rafters in the room with the Mandragora shelf. Get onto and jump off the giant scythe or the
torture instrument shelf to make it up there. It's less annoying to do without Mandragora since having it will cause ghosts to
infinitely spawn in here.
### Invisible Items
#### Red carpet hall knight
The suit of armor in the red carpet hallway after the bottom elevator room, directly next to the door leading into the
Lizard Locker Room.
#### Lizard locker knight
The suit of armor in the Lizard Locker Room itself, directly across from the door connecting to the red carpet hallway.
#### Broken staircase knight
The suit of armor in the broken staircase room following the Lizard Locker Room.
#### Inside cracked wall hallway flamethrower
In the upper cracked wall hallway, it is in the lower flamethrower that is part of the pair between the Butler Bros. Room
and the main part of the hallway.
#### Nitro room crates
The wall of crates in the Nitro room on Heinrich Meyer's side. This is notable for being one of the very rare Healing Kits
that you can get for free in vanilla.
#### Hell Knight landing corner knight
The inactive suit of armor in the corner of the room before the Maid Sisters' Room, which also contains an active knight.
#### Maid sisters room vase
The lone vase in the vampire Maid Sisters' Room, directly across from the door leading to the Hell Knight Landing.
Yes, you are actually supposed to *check* this with C-right to get its item; not break it like you did to the pots in
the Villa earlier!
#### Invention room giant Famicart
The giant square-shaped thing in one corner of the invention room that looks vaguely like a massive video game cartridge.
A Famicom cartridge, perhaps?
#### Invention room round machine
The brown circular machine in the invention room, close to the middle of the wall on the side of the Spike Crusher Room.
#### Inside nitro hallway flamethrower
The lower flamethrower in the hallway between the Nitro room from the Spike Crusher Room, near the two doors to said rooms.
#### Torture chamber instrument rack
The shelf full of torture instruments in the torture chamber, to the right of the Mandragora shelf.
### 3-Hit Breakables
#### Behemoth arena crate
This large crate can be found in the back-right corner of Behemoth's arena and is pretty hard to miss. Break it to get 5
moneybags-turned-checks.
#### Elevator room unoccupied statue stand
In the bottom elevator room is a statue on a stand that will cry literal Bloody Tears if you get near it. On the opposite
side of the room from this, near the enormous machine, is a stand much like the one the aforementioned statue is on only
this one is completely vacant. This stand can be broken for 3 roast beefs-turned checks.
#### Lizard locker room slab
In the Lizard Locker Room, on top of the second locker from the side of the room with the door to the red carpet hallway,
is a metallic box-like slab thingy that can be broken normally for 4 status items. This 3HB is notable for being one of two
funny ones in the game that does NOT set a flag when you break it in vanilla, meaning you can keep breaking it over and
over again for infinite Purifyings and Cure Ampoules!
### The Lizard Lockers
If you turned on the Lizard Locker Items setting, then hoo boy, you are in for a FUN =) time! Inside each one of the six Lizard
Lockers is a check, and the way you get these checks is by camping near each of the lockers as you defeat said Lizards,
praying that one will emerge from it and cause it to open to give you a chance to grab it. It is *completely* luck-based,
you have a 1-in-6 (around 16%) chance per Lizard-man that emerges, and you have to repeat this process six times for each
check inside each locker. You can open and cancel the warp menu to make things easier, but other than that, enjoy playing
with the Lizards!
## Duel Tower
#### Invisible bridge balcony
Located between Werewolf and Were-bull's arenas. Use an invisible bridge to reach it; it starts at the highest platform
on the same wall as it that appears after defeating Werewolf. The balcony contains two checks and a save point that I
added specifically for this rando to make the level less frustrating.
#### Above Were-bull arena
The only check that can be permanently missed in the vanilla game depending on the order you do things, not counting any
points of no return. Between Werewolf and Were-bull's arenas is an alternate path that you can take downward and around
to avoid Were-bull completely and get on top of his still-raised arena, so you can reach this. In the rando, I set it up so
that his arena will go back to being raised if you leave the area and come back, and if you get the item later his arena flag
will be set then. If you're playing with Dracula's bosses condition, then you can only get one Special2 off of him the first
time you beat him and then none more after that.
## Tower of Execution
#### Invisible bridge ledge
There are two ways to reach this one; use the invisible bridge that starts at the walkway above the entrance, or jump to
it from the Execution Key gate alcove. Collecting this Special2 in vanilla unlocks Reinhardt's alternate costume.
#### Guillotine tower top level
This iron maiden is strategically placed in such a way that you will very likely miss it if you aren't looking carefully for
it, so I am including it here. When you make it to the top floor of the level, as you approach the tower for the final time,
look on the opposite side of it from where you approach it, and you will find this. The laggiest check in the whole game!
I'd dare someone to find some check in some other Archipelago game that lags harder than this.
### 3-Hit Breakables
#### Pre-mid-savepoint platforms ledge
Here's a really weird one that even I never found about until well after I finished the 3HB setting and moved on to deving
other things, and I'd honestly be shocked if ANYONE knew about outside the context of this rando! This square-shaped
platform can be found right before the second set of expanding and retracting wall platforms, leading up to the mid-save
point, after going towards the central tower structure for the second time on the second floor. Breaking it will drop an
assortment of 5 items, one of which is notable for being the ONE sub-weapon that drops from a 3HB. This meant I had to really
change how things work to account for sub-weapons being in 3HBs!
## Tower of Science
#### Invisible bridge platform
Following the hallway with a save jewel beyond the Science Key2 door, look to your right, and you'll see this. Mind the
gap separating the invisible bridge from the solid ground of the bottom part of this section!
### 3-Hit Breakables
#### Invisible bridge platform crate
Near the candle on the above-mentioned invisible bridge platform is a small metallic crate. Break it for a total of 6
checks, which in vanilla are 2 chickens, moneybags, and jewels.
## Tower of Sorcery
#### Trick shot from mid-savepoint platform
From the platform with the save jewel, look back towards the vanishing red platforms that you had to conquer to get up
there, and you'll see a breakable diamond floating high above a solid platform before it. An ultra-precise orb shot from
Carrie can hit it to reveal the check, so you can then go down and get it. If you are playing as Reinhardt, you'll have
to use a sub-weapon. Any sub-weapon that's not Holy Water will work. Sub-weapons and the necessary jewels can both be
farmed off the local Icemen if it really comes down to it.
#### Above yellow bubble
Directly above the yellow bubble that you break to raise the middle yellow large platform. Jump off the final red platform
in the series of red platforms right after the save point when said platform bobs all the way up, and you'll be able to
hit this diamond with good timing.
#### Above tiny blue platforms start
Above the large platform after the yellow ones from whence you can reach the first of the series of tiny blue platforms.
This diamond is low enough that you can reach it by simply jumping straight up to it.
#### Invisible bridge platform
Located at the very end of the stage, off to the side. Use the invisible bridge to reach it. The Special2 that unlocks
Carrie's costume can be normally found here (or Special3, depending on if you are playing the PAL version or not).
## Clock Tower
All the normal items here and in Room of Clocks are self-explanatory, but the 3HBs in Clock Tower are a whoooooooole 'nother story. So buckle up...
### 3-Hit Breakables
#### Gear climb room battery underside slab
In the first room, on the side you initially climb up, go up until you find a battery-like object that you can stand on
as a platform. From the platform right after this, you can hit this 3HB that can be found on the underside of the structure
in the corner, above the structure before the first set of gears that you initially start the climb on. 3 chickens/checks
can be gotten out of this one.
#### Gear climb room door underside slab
Now THIS one can be very annoying to get, doubly so if you're playing as Reinhardt, triply so if you're playing Hard Mode
on top of that because you will also have to deal with invincible, bone-throwing red skeletons here! This slab can be
found on the underside of the platform in the first room with the door leading out into the second area and drops 3
beefs/checks when you break it. Carrie is small enough to crouch under the platform and shoot the thing a few times
without *too* much hassle, but the only way I know of for Reinhardt to get it is to hang him off the side of the gear
and pull him up once he gets moved under the platform. Getting him to then face the breakable and whip it without falling
off due to the gear's rotation is next-to-impossible, I find, so the safest method to breaking it as him is to just rush
it with his sword, go back up, repeat 2 more times. Pray the aftermentioned Hard Mode skellies don't decide to cause *too*
much trouble during all of this!
#### Final room entrance slab
Simply next to the entrance when you come into the third and final room from the intended way. Drops 2 moneybags-turned-checks,
which funnily normally have their item IDs shared with the other 3HB in this room's items, so I had to separate those for
the rando.
#### Renon's final offers slab
At the top of the final room, on a platform near the Renon contract that would normally be the very last in the game.
This 3HB drops a whopping 8 items, more than any other 3HB in the entire game, and 6 of those are all moneybags. They
*really* shower you in gold in preparation for the finale, huh?
## Castle Keep
#### Behind Dracula's chamber/Dracula's floating cube
This game continues the CV tradition of having a hidden secret around Dracula's chamber that you can get helpful things
from before the final battle begins. Jump onto the torch ledges and use the thin walkway to reach the backside of Dracula's
chamber where these can both be found. The floating cube, in question, can be reached with an invisible bridge. The other
candle here is noteworthy for being the sole jewel candle in vanilla that doesn't set a flag, meaning you can keep going
back down and up the stairs to farm infinite sub-weapon ammo for Dracula like in old school Castlevania!
### Invisible Items
#### Left/Right Dracula door flame
Inside the torches on either side of the door to Dracula's chamber. Similar to the above-mentioned jewel torch, these do
not set flags. So you can get infinite healing kits for free by constantly going down and back up!

View File

@ -0,0 +1,63 @@
# Castlevania 64 Setup Guide
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- A Castlevania 64 ROM of the US 1.0 version specifically. The Archipelago community cannot provide this.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
### Configuring BizHawk
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
tabbed out of EmuHawk.
- Open a `.z64` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
`Controllers…`, load any `.z64` ROM first.
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
clear it.
- All non-Japanese versions of the N64 Castlevanias require a Controller Pak to save game data. To enable this, while
you still have the `.z64` ROM loaded, go to `N64 > Controller Settings...`, click the dropdown by `Controller 1`, and
click `Memory Card`. You must then restart EmuHawk for it to take effect.
- After enabling the `Memory Card` setting, next time you boot up your Castlevania 64 ROM, you will see the
No "CASTLEVANIA" Note Found screen. Pick `Create "CASTLEVANIA" Note Now > Yes` to create save data and enable saving at
the White Jewels.
## Generating and Patching a Game
1. Create your settings file (YAML). You can make one on the
[Castlevania 64 settings page](../../../games/Castlevania 64/player-settings).
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
This will generate an output file for you. Your patch file will have the `.apcv64` file extension.
3. Open `ArchipelagoLauncher.exe`
4. Select "Open Patch" on the left side and select your patch file.
5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
6. A patched `.z64` file will be created in the same place as the patch file.
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
BizHawk install.
If you're playing a single-player seed, and you don't care about hints, you can stop here, close the client, and load
the patched ROM in any emulator or EverDrive of your choice. However, for multiworlds and other Archipelago features,
continue below using BizHawk as your emulator.
## Connecting to a Server
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
in case you have to close and reopen a window mid-game for some reason.
1. Castlevania 64 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
you can re-open it from the launcher.
2. Ensure EmuHawk is running the patched ROM.
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
4. In the Lua Console window, go to `Script > Open Script…`.
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk
Client window should indicate that it connected and recognized Castlevania 64.
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
top text field of the client and click Connect.
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
perfectly safe to make progress offline; everything will re-sync when you reconnect.

149
worlds/cv64/entrances.py Normal file
View File

@ -0,0 +1,149 @@
from .data import ename, iname, rname
from .stages import get_stage_info
from .options import CV64Options
from typing import Dict, List, Tuple, Union
# # # KEY # # #
# "connection" = The name of the Region the Entrance connects into. If it's a Tuple[str, str], we take the stage in
# active_stage_exits given in the second string and then the stage given in that stage's slot given in
# the first string, and take the start or end Region of that stage.
# "rule" = What rule should be applied to the Entrance during set_rules, as defined in self.rules in the CV64Rules class
# definition in rules.py.
# "add conds" = A list of player options conditions that must be satisfied for the Entrance to be added. Can be of
# varying length depending on how many conditions need to be satisfied. In the add_conds dict's tuples,
# the first element is the name of the option, the second is the option value to check for, and the third
# is a boolean for whether we are evaluating for the option value or not.
entrance_info = {
# Forest of Silence
ename.forest_dbridge_gate: {"connection": rname.forest_mid},
ename.forest_werewolf_gate: {"connection": rname.forest_end},
ename.forest_end: {"connection": ("next", rname.forest_of_silence)},
# Castle Wall
ename.cw_portcullis_c: {"connection": rname.cw_exit},
ename.cw_lt_skip: {"connection": ("next", rname.castle_wall), "add conds": ["hard"]},
ename.cw_lt_door: {"connection": rname.cw_ltower, "rule": iname.left_tower_key},
ename.cw_end: {"connection": ("next", rname.castle_wall)},
# Villa
ename.villa_dog_gates: {"connection": rname.villa_main},
ename.villa_snipe_dogs: {"connection": rname.villa_start, "add conds": ["carrie", "hard"]},
ename.villa_to_storeroom: {"connection": rname.villa_storeroom, "rule": iname.storeroom_key},
ename.villa_to_archives: {"connection": rname.villa_archives, "rule": iname.archives_key},
ename.villa_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
ename.villa_to_maze: {"connection": rname.villa_maze, "rule": iname.garden_key},
ename.villa_from_storeroom: {"connection": rname.villa_main, "rule": iname.storeroom_key},
ename.villa_from_maze: {"connection": rname.villa_servants, "rule": iname.garden_key},
ename.villa_servant_door: {"connection": rname.villa_main},
ename.villa_copper_door: {"connection": rname.villa_crypt, "rule": iname.copper_key,
"add conds": ["not hard"]},
ename.villa_copper_skip: {"connection": rname.villa_crypt, "add conds": ["hard"]},
ename.villa_bridge_door: {"connection": rname.villa_maze},
ename.villa_end_r: {"connection": ("next", rname.villa)},
ename.villa_end_c: {"connection": ("alt", rname.villa)},
# Tunnel
ename.tunnel_start_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
ename.tunnel_gondolas: {"connection": rname.tunnel_end},
ename.tunnel_end_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
ename.tunnel_end: {"connection": ("next", rname.tunnel)},
# Underground Waterway
ename.uw_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
ename.uw_final_waterfall: {"connection": rname.uw_end},
ename.uw_waterfall_skip: {"connection": rname.uw_main, "add conds": ["hard"]},
ename.uw_end: {"connection": ("next", rname.underground_waterway)},
# Castle Center
ename.cc_tc_door: {"connection": rname.cc_torture_chamber, "rule": iname.chamber_key},
ename.cc_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
ename.cc_lower_wall: {"connection": rname.cc_crystal, "rule": "Bomb 2"},
ename.cc_upper_wall: {"connection": rname.cc_library, "rule": "Bomb 1"},
ename.cc_elevator: {"connection": rname.cc_elev_top},
ename.cc_exit_r: {"connection": ("next", rname.castle_center)},
ename.cc_exit_c: {"connection": ("alt", rname.castle_center)},
# Duel Tower
ename.dt_start: {"connection": ("prev", rname.duel_tower)},
ename.dt_end: {"connection": ("next", rname.duel_tower)},
# Tower of Execution
ename.toe_start: {"connection": ("prev", rname.tower_of_execution)},
ename.toe_gate: {"connection": rname.toe_ledge, "rule": iname.execution_key,
"add conds": ["not hard"]},
ename.toe_gate_skip: {"connection": rname.toe_ledge, "add conds": ["hard"]},
ename.toe_end: {"connection": ("next", rname.tower_of_execution)},
# Tower of Science
ename.tosci_start: {"connection": ("prev", rname.tower_of_science)},
ename.tosci_key1_door: {"connection": rname.tosci_three_doors, "rule": iname.science_key1},
ename.tosci_to_key2_door: {"connection": rname.tosci_conveyors, "rule": iname.science_key2},
ename.tosci_from_key2_door: {"connection": rname.tosci_start, "rule": iname.science_key2},
ename.tosci_key3_door: {"connection": rname.tosci_key3, "rule": iname.science_key3},
ename.tosci_end: {"connection": ("next", rname.tower_of_science)},
# Tower of Sorcery
ename.tosor_start: {"connection": ("prev", rname.tower_of_sorcery)},
ename.tosor_end: {"connection": ("next", rname.tower_of_sorcery)},
# Room of Clocks
ename.roc_gate: {"connection": ("next", rname.room_of_clocks)},
# Clock Tower
ename.ct_to_door1: {"connection": rname.ct_middle, "rule": iname.clocktower_key1},
ename.ct_from_door1: {"connection": rname.ct_start, "rule": iname.clocktower_key1},
ename.ct_to_door2: {"connection": rname.ct_end, "rule": iname.clocktower_key2},
ename.ct_from_door2: {"connection": rname.ct_middle, "rule": iname.clocktower_key2},
ename.ct_renon: {"connection": rname.renon, "add conds": ["shopsanity"]},
ename.ct_door_3: {"connection": ("next", rname.clock_tower), "rule": iname.clocktower_key3},
# Castle Keep
ename.ck_slope_jump: {"connection": rname.roc_main, "add conds": ["hard"]},
ename.ck_drac_door: {"connection": rname.ck_drac_chamber, "rule": "Dracula"}
}
add_conds = {"carrie": ("carrie_logic", True, True),
"hard": ("hard_logic", True, True),
"not hard": ("hard_logic", False, True),
"shopsanity": ("shopsanity", True, True)}
stage_connection_types = {"prev": "end region",
"next": "start region",
"alt": "start region"}
def get_entrance_info(entrance: str, info: str) -> Union[str, Tuple[str, str], List[str], None]:
return entrance_info[entrance].get(info, None)
def get_warp_entrances(active_warp_list: List[str]) -> Dict[str, str]:
# Create the starting stage Entrance.
warp_entrances = {get_stage_info(active_warp_list[0], "start region"): "Start stage"}
# Create the warp Entrances.
for i in range(1, len(active_warp_list)):
mid_stage_region = get_stage_info(active_warp_list[i], "mid region")
warp_entrances.update({mid_stage_region: f"Warp {i}"})
return warp_entrances
def verify_entrances(options: CV64Options, entrances: List[str],
active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[str, str]:
verified_entrances = {}
for ent_name in entrances:
ent_add_conds = get_entrance_info(ent_name, "add conds")
# Check any options that might be associated with the Entrance before adding it.
add_it = True
if ent_add_conds is not None:
for cond in ent_add_conds:
if not ((getattr(options, add_conds[cond][0]).value == add_conds[cond][1]) == add_conds[cond][2]):
add_it = False
if not add_it:
continue
# Add the Entrance to the verified Entrances if the above check passes.
connection = get_entrance_info(ent_name, "connection")
# If the Entrance is a connection to a different stage, get the corresponding other stage Region.
if isinstance(connection, tuple):
connecting_stage = active_stage_exits[connection[1]][connection[0]]
# Stages that lead backwards at the beginning of the line will appear leading to "Menu".
if connecting_stage in ["Menu", None]:
continue
connection = get_stage_info(connecting_stage, stage_connection_types[connection[0]])
verified_entrances.update({connection: ent_name})
return verified_entrances

214
worlds/cv64/items.py Normal file
View File

@ -0,0 +1,214 @@
from BaseClasses import Item
from .data import iname
from .locations import base_id, get_location_info
from .options import DraculasCondition, SpareKeys
from typing import TYPE_CHECKING, Dict, Union
if TYPE_CHECKING:
from . import CV64World
import math
class CV64Item(Item):
game: str = "Castlevania 64"
# # # KEY # # #
# "code" = The unique part of the Item's AP code attribute, as well as the value to call the in-game "prepare item
# textbox" function with to give the Item in-game. Add this + base_id to get the actual AP code.
# "default classification" = The AP Item Classification that gets assigned to instances of that Item in create_item
# by default, unless I deliberately override it (as is the case for some Special1s).
# "inventory offset" = What offset from the start of the in-game inventory array (beginning at 0x80389C4B) stores the
# current count for that Item. Used for start inventory purposes.
# "pickup actor id" = The ID for the Item's in-game Item pickup actor. If it's not in the Item's data dict, it's the
# same as the Item's code. This is what gets written in the ROM to replace non-NPC/shop items.
# "sub equip id" = For sub-weapons specifically, this is the number to put in the game's "current sub-weapon" value to
# indicate the player currently having that weapon. Used for start inventory purposes.
item_info = {
# White jewel
iname.red_jewel_s: {"code": 0x02, "default classification": "filler"},
iname.red_jewel_l: {"code": 0x03, "default classification": "filler"},
iname.special_one: {"code": 0x04, "default classification": "progression_skip_balancing",
"inventory offset": 0},
iname.special_two: {"code": 0x05, "default classification": "progression_skip_balancing",
"inventory offset": 1},
iname.roast_chicken: {"code": 0x06, "default classification": "filler", "inventory offset": 2},
iname.roast_beef: {"code": 0x07, "default classification": "filler", "inventory offset": 3},
iname.healing_kit: {"code": 0x08, "default classification": "useful", "inventory offset": 4},
iname.purifying: {"code": 0x09, "default classification": "filler", "inventory offset": 5},
iname.cure_ampoule: {"code": 0x0A, "default classification": "filler", "inventory offset": 6},
# pot-pourri
iname.powerup: {"code": 0x0C, "default classification": "filler"},
iname.permaup: {"code": 0x10C, "default classification": "useful", "pickup actor id": 0x0C,
"inventory offset": 8},
iname.knife: {"code": 0x0D, "default classification": "filler", "pickup actor id": 0x10,
"sub equip id": 1},
iname.holy_water: {"code": 0x0E, "default classification": "filler", "pickup actor id": 0x0D,
"sub equip id": 2},
iname.cross: {"code": 0x0F, "default classification": "filler", "pickup actor id": 0x0E,
"sub equip id": 3},
iname.axe: {"code": 0x10, "default classification": "filler", "pickup actor id": 0x0F,
"sub equip id": 4},
# Wooden stake (AP item)
iname.ice_trap: {"code": 0x12, "default classification": "trap"},
# The contract
# engagement ring
iname.magical_nitro: {"code": 0x15, "default classification": "progression", "inventory offset": 17},
iname.mandragora: {"code": 0x16, "default classification": "progression", "inventory offset": 18},
iname.sun_card: {"code": 0x17, "default classification": "filler", "inventory offset": 19},
iname.moon_card: {"code": 0x18, "default classification": "filler", "inventory offset": 20},
# Incandescent gaze
iname.archives_key: {"code": 0x1A, "default classification": "progression", "pickup actor id": 0x1D,
"inventory offset": 22},
iname.left_tower_key: {"code": 0x1B, "default classification": "progression", "pickup actor id": 0x1E,
"inventory offset": 23},
iname.storeroom_key: {"code": 0x1C, "default classification": "progression", "pickup actor id": 0x1F,
"inventory offset": 24},
iname.garden_key: {"code": 0x1D, "default classification": "progression", "pickup actor id": 0x20,
"inventory offset": 25},
iname.copper_key: {"code": 0x1E, "default classification": "progression", "pickup actor id": 0x21,
"inventory offset": 26},
iname.chamber_key: {"code": 0x1F, "default classification": "progression", "pickup actor id": 0x22,
"inventory offset": 27},
iname.execution_key: {"code": 0x20, "default classification": "progression", "pickup actor id": 0x23,
"inventory offset": 28},
iname.science_key1: {"code": 0x21, "default classification": "progression", "pickup actor id": 0x24,
"inventory offset": 29},
iname.science_key2: {"code": 0x22, "default classification": "progression", "pickup actor id": 0x25,
"inventory offset": 30},
iname.science_key3: {"code": 0x23, "default classification": "progression", "pickup actor id": 0x26,
"inventory offset": 31},
iname.clocktower_key1: {"code": 0x24, "default classification": "progression", "pickup actor id": 0x27,
"inventory offset": 32},
iname.clocktower_key2: {"code": 0x25, "default classification": "progression", "pickup actor id": 0x28,
"inventory offset": 33},
iname.clocktower_key3: {"code": 0x26, "default classification": "progression", "pickup actor id": 0x29,
"inventory offset": 34},
iname.five_hundred_gold: {"code": 0x27, "default classification": "filler", "pickup actor id": 0x1A},
iname.three_hundred_gold: {"code": 0x28, "default classification": "filler", "pickup actor id": 0x1B},
iname.one_hundred_gold: {"code": 0x29, "default classification": "filler", "pickup actor id": 0x1C},
iname.crystal: {"default classification": "progression"},
iname.trophy: {"default classification": "progression"},
iname.victory: {"default classification": "progression"}
}
filler_item_names = [iname.red_jewel_s, iname.red_jewel_l, iname.five_hundred_gold, iname.three_hundred_gold,
iname.one_hundred_gold]
def get_item_info(item: str, info: str) -> Union[str, int, None]:
return item_info[item].get(info, None)
def get_item_names_to_ids() -> Dict[str, int]:
return {name: get_item_info(name, "code")+base_id for name in item_info if get_item_info(name, "code") is not None}
def get_item_counts(world: "CV64World") -> Dict[str, Dict[str, int]]:
active_locations = world.multiworld.get_unfilled_locations(world.player)
item_counts = {
"progression": {},
"progression_skip_balancing": {},
"useful": {},
"filler": {},
"trap": {}
}
total_items = 0
extras_count = 0
# Get from each location its vanilla item and add it to the default item counts.
for loc in active_locations:
if loc.address is None:
continue
if world.options.hard_item_pool and get_location_info(loc.name, "hard item") is not None:
item_to_add = get_location_info(loc.name, "hard item")
else:
item_to_add = get_location_info(loc.name, "normal item")
classification = get_item_info(item_to_add, "default classification")
if item_to_add not in item_counts[classification]:
item_counts[classification][item_to_add] = 1
else:
item_counts[classification][item_to_add] += 1
total_items += 1
# Replace all but 2 PowerUps with junk if Permanent PowerUps is on and mark those two PowerUps as Useful.
if world.options.permanent_powerups:
for i in range(item_counts["filler"][iname.powerup] - 2):
item_counts["filler"][world.get_filler_item_name()] += 1
del(item_counts["filler"][iname.powerup])
item_counts["useful"][iname.permaup] = 2
# Add the total Special1s.
item_counts["progression_skip_balancing"][iname.special_one] = world.options.total_special1s.value
extras_count += world.options.total_special1s.value
# Add the total Special2s if Dracula's Condition is Special2s.
if world.options.draculas_condition == DraculasCondition.option_specials:
item_counts["progression_skip_balancing"][iname.special_two] = world.options.total_special2s.value
extras_count += world.options.total_special2s.value
# Determine the extra key counts if applicable. Doing this before moving Special1s will ensure only the keys and
# bomb components are affected by this.
for key in item_counts["progression"]:
spare_keys = 0
if world.options.spare_keys == SpareKeys.option_on:
spare_keys = item_counts["progression"][key]
elif world.options.spare_keys == SpareKeys.option_chance:
if item_counts["progression"][key] > 0:
for i in range(item_counts["progression"][key]):
spare_keys += world.random.randint(0, 1)
item_counts["progression"][key] += spare_keys
extras_count += spare_keys
# Move the total number of Special1s needed to warp everywhere to normal progression balancing if S1s per warp is
# 3 or lower.
if world.s1s_per_warp <= 3:
item_counts["progression_skip_balancing"][iname.special_one] -= world.s1s_per_warp * 7
item_counts["progression"][iname.special_one] = world.s1s_per_warp * 7
# Determine the total amounts of replaceable filler and non-filler junk.
total_filler_junk = 0
total_non_filler_junk = 0
for junk in item_counts["filler"]:
if junk in filler_item_names:
total_filler_junk += item_counts["filler"][junk]
else:
total_non_filler_junk += item_counts["filler"][junk]
# Subtract from the filler counts total number of "extra" items we've added. get_filler_item_name() filler will be
# subtracted from first until we run out of that, at which point we'll start subtracting from the rest. At this
# moment, non-filler item name filler cannot run out no matter the settings, so I haven't bothered adding handling
# for when it does yet.
available_filler_junk = filler_item_names.copy()
for i in range(extras_count):
if total_filler_junk > 0:
total_filler_junk -= 1
item_to_subtract = world.random.choice(available_filler_junk)
else:
total_non_filler_junk -= 1
item_to_subtract = world.random.choice(list(item_counts["filler"].keys()))
item_counts["filler"][item_to_subtract] -= 1
if item_counts["filler"][item_to_subtract] == 0:
del(item_counts["filler"][item_to_subtract])
if item_to_subtract in available_filler_junk:
available_filler_junk.remove(item_to_subtract)
# Determine the Ice Trap count by taking a certain % of the total filler remaining at this point.
item_counts["trap"][iname.ice_trap] = math.floor((total_filler_junk + total_non_filler_junk) *
(world.options.ice_trap_percentage.value / 100.0))
for i in range(item_counts["trap"][iname.ice_trap]):
# Subtract the remaining filler after determining the ice trap count.
item_to_subtract = world.random.choice(list(item_counts["filler"].keys()))
item_counts["filler"][item_to_subtract] -= 1
if item_counts["filler"][item_to_subtract] == 0:
del (item_counts["filler"][item_to_subtract])
return item_counts

699
worlds/cv64/locations.py Normal file
View File

@ -0,0 +1,699 @@
from BaseClasses import Location
from .data import lname, iname
from .options import CV64Options, SubWeaponShuffle, DraculasCondition, RenonFightCondition, VincentFightCondition
from typing import Dict, Optional, Union, List, Tuple
base_id = 0xC64000
class CV64Location(Location):
game: str = "Castlevania 64"
# # # KEY # # #
# "code" = The unique part of the Location's AP code attribute, as well as the in-game bitflag index starting from
# 0x80389BE4 that indicates the Location has been checked. Add this + base_id to get the actual AP code.
# "offset" = The offset in the ROM to overwrite to change the Item on that Location.
# "normal item" = The Item normally there in vanilla on most difficulties in most versions of the game. Used to
# determine the World's Item counts by checking what Locations are active.
# "hard item" = The Item normally there in Hard Mode in the PAL version of CV64 specifically. Used instead of the
# normal Item when the hard Item pool is enabled if it's in the Location's data dict.
# "add conds" = A list of player options conditions that must be satisfied for the Location to be added. Can be of
# varying length depending on how many conditions need to be satisfied. In the add_conds dict's tuples,
# the first element is the name of the option, the second is the option value to check for, and the third
# is a boolean for whether we are evaluating for the option value or not.
# "event" = What event Item to place on that Location, for Locations that are events specifically.
# "countdown" = What Countdown number in the array of Countdown numbers that Location contributes to. For the most part,
# this is figured out by taking that Location's corresponding stage's postion in the vanilla stage order,
# but there are some exceptions made for Locations in parts of Villa and Castle Center that split off into
# their own numbers.
# "type" = Anything special about this Location in-game, whether it be NPC-given, invisible, etc.
location_info = {
# Forest of Silence
lname.forest_pillars_right: {"code": 0x1C, "offset": 0x10C67B, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s},
lname.forest_pillars_left: {"code": 0x46, "offset": 0x10C6EB, "normal item": iname.knife,
"add conds": ["sub"]},
lname.forest_pillars_top: {"code": 0x13, "offset": 0x10C71B, "normal item": iname.roast_beef,
"hard item": iname.red_jewel_l},
lname.forest_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
lname.forest_king_skeleton: {"code": 0xC, "offset": 0x10C6BB, "normal item": iname.five_hundred_gold},
lname.forest_lgaz_in: {"code": 0x1A, "offset": 0x10C68B, "normal item": iname.moon_card},
lname.forest_lgaz_top: {"code": 0x19, "offset": 0x10C693, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s},
lname.forest_hgaz_in: {"code": 0xB, "offset": 0x10C6C3, "normal item": iname.sun_card},
lname.forest_hgaz_top: {"code": 0x3, "offset": 0x10C6E3, "normal item": iname.roast_chicken,
"hard item": iname.five_hundred_gold},
lname.forest_weretiger_sw: {"code": 0xA, "offset": 0x10C6CB, "normal item": iname.five_hundred_gold},
lname.forest_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
lname.forest_weretiger_gate: {"code": 0x7, "offset": 0x10C683, "normal item": iname.powerup},
lname.forest_dirge_tomb_l: {"code": 0x59, "offset": 0x10C74B, "normal item": iname.one_hundred_gold,
"add conds": ["empty"]},
lname.forest_dirge_tomb_u: {"code": 0x8, "offset": 0x10C743, "normal item": iname.one_hundred_gold},
lname.forest_dirge_plaque: {"code": 0x6, "offset": 0x7C7F9D, "normal item": iname.roast_chicken,
"hard item": iname.one_hundred_gold, "type": "inv"},
lname.forest_dirge_ped: {"code": 0x45, "offset": 0x10C6FB, "normal item": iname.cross,
"add conds": ["sub"]},
lname.forest_dirge_rock1: {"code": 0x221, "offset": 0x10C791, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.forest_dirge_rock2: {"code": 0x222, "offset": 0x10C793, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.forest_dirge_rock3: {"code": 0x223, "offset": 0x10C795, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.forest_dirge_rock4: {"code": 0x224, "offset": 0x10C797, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.forest_dirge_rock5: {"code": 0x225, "offset": 0x10C799, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.forest_corpse_save: {"code": 0xF, "offset": 0x10C6A3, "normal item": iname.red_jewel_s},
lname.forest_dbridge_wall: {"code": 0x18, "offset": 0x10C69B, "normal item": iname.red_jewel_s},
lname.forest_dbridge_sw: {"code": 0x9, "offset": 0x10C6D3, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold},
lname.forest_dbridge_gate_l: {"code": 0x44, "offset": 0x10C6F3, "normal item": iname.axe, "add conds": ["sub"]},
lname.forest_dbridge_gate_r: {"code": 0xE, "offset": 0x10C6AB, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s},
lname.forest_dbridge_tomb_l: {"code": 0xEA, "offset": 0x10C763, "normal item": iname.three_hundred_gold,
"add conds": ["empty"]},
lname.forest_dbridge_tomb_ur: {"code": 0xE4, "offset": 0x10C773, "normal item": iname.three_hundred_gold,
"add conds": ["empty"]},
lname.forest_dbridge_tomb_uf: {"code": 0x1B, "offset": 0x10C76B, "normal item": iname.red_jewel_s},
lname.forest_bface_tomb_lf: {"code": 0x10, "offset": 0x10C75B, "normal item": iname.roast_chicken},
lname.forest_bface_tomb_lr: {"code": 0x58, "offset": 0x10C753, "normal item": iname.three_hundred_gold,
"add conds": ["empty"]},
lname.forest_bface_tomb_u: {"code": 0x1E, "offset": 0x10C77B, "normal item": iname.one_hundred_gold},
lname.forest_ibridge: {"code": 0x2, "offset": 0x10C713, "normal item": iname.one_hundred_gold},
lname.forest_bridge_rock1: {"code": 0x227, "offset": 0x10C79D, "normal item": iname.red_jewel_l,
"add conds": ["3hb"]},
lname.forest_bridge_rock2: {"code": 0x228, "offset": 0x10C79F, "normal item": iname.five_hundred_gold,
"hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
lname.forest_bridge_rock3: {"code": 0x229, "offset": 0x10C7A1, "normal item": iname.powerup,
"hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
lname.forest_bridge_rock4: {"code": 0x22A, "offset": 0x10C7A3, "normal item": iname.roast_chicken,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.forest_werewolf_tomb_lf: {"code": 0xE7, "offset": 0x10C783, "normal item": iname.one_hundred_gold,
"add conds": ["empty"]},
lname.forest_werewolf_tomb_lr: {"code": 0xE6, "offset": 0x10C73B, "normal item": iname.three_hundred_gold,
"add conds": ["empty"]},
lname.forest_werewolf_tomb_r: {"code": 0x4, "offset": 0x10C733, "normal item": iname.sun_card},
lname.forest_werewolf_plaque: {"code": 0x1, "offset": 0xBFC8AF, "normal item": iname.roast_chicken,
"type": "inv"},
lname.forest_werewolf_tree: {"code": 0xD, "offset": 0x10C6B3, "normal item": iname.red_jewel_s},
lname.forest_werewolf_island: {"code": 0x41, "offset": 0x10C703, "normal item": iname.holy_water,
"add conds": ["sub"]},
lname.forest_final_sw: {"code": 0x12, "offset": 0x10C72B, "normal item": iname.roast_beef},
lname.forest_boss_three: {"event": iname.trophy, "add conds": ["boss"]},
# Castle Wall
lname.cwr_bottom: {"code": 0x1DD, "offset": 0x10C7E7, "normal item": iname.sun_card,
"hard item": iname.one_hundred_gold},
lname.cw_dragon_sw: {"code": 0x153, "offset": 0x10C817, "normal item": iname.roast_chicken},
lname.cw_boss: {"event": iname.trophy, "add conds": ["boss"]},
lname.cw_save_slab1: {"code": 0x22C, "offset": 0x10C84D, "normal item": iname.red_jewel_l,
"add conds": ["3hb"]},
lname.cw_save_slab2: {"code": 0x22D, "offset": 0x10C84F, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
lname.cw_save_slab3: {"code": 0x22E, "offset": 0x10C851, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
lname.cw_save_slab4: {"code": 0x22F, "offset": 0x10C853, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
lname.cw_save_slab5: {"code": 0x230, "offset": 0x10C855, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
lname.cw_rrampart: {"code": 0x156, "offset": 0x10C7FF, "normal item": iname.five_hundred_gold},
lname.cw_lrampart: {"code": 0x155, "offset": 0x10C807, "normal item": iname.moon_card,
"hard item": iname.one_hundred_gold},
lname.cw_pillar: {"code": 0x14D, "offset": 0x7F9A0F, "normal item": iname.holy_water, "add conds": ["sub"]},
lname.cw_shelf_visible: {"code": 0x158, "offset": 0x7F99A9, "normal item": iname.powerup},
lname.cw_shelf_sandbags: {"code": 0x14E, "offset": 0x7F9A3E, "normal item": iname.five_hundred_gold, "type": "inv"},
lname.cw_shelf_torch: {"code": 0x14C, "offset": 0x10C82F, "normal item": iname.cross, "add conds": ["sub"]},
lname.cw_ground_left: {"code": 0x14B, "offset": 0x10C827, "normal item": iname.knife, "add conds": ["sub"]},
lname.cw_ground_middle: {"code": 0x159, "offset": 0x10C7F7, "normal item": iname.left_tower_key},
lname.cw_ground_right: {"code": 0x14A, "offset": 0x10C81F, "normal item": iname.axe, "add conds": ["sub"]},
lname.cwl_bottom: {"code": 0x1DE, "offset": 0x10C7DF, "normal item": iname.moon_card},
lname.cwl_bridge: {"code": 0x1DC, "offset": 0x10C7EF, "normal item": iname.roast_beef},
lname.cw_drac_sw: {"code": 0x154, "offset": 0x10C80F, "normal item": iname.roast_chicken,
"hard item": iname.one_hundred_gold},
lname.cw_drac_slab1: {"code": 0x232, "offset": 0x10C859, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.cw_drac_slab2: {"code": 0x233, "offset": 0x10C85B, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.cw_drac_slab3: {"code": 0x234, "offset": 0x10C85D, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.cw_drac_slab4: {"code": 0x235, "offset": 0x10C85F, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.cw_drac_slab5: {"code": 0x236, "offset": 0x10C861, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
# Villa
lname.villafy_outer_gate_l: {"code": 0x133, "offset": 0x10C87F, "normal item": iname.red_jewel_l},
lname.villafy_outer_gate_r: {"code": 0x132, "offset": 0x10C887, "normal item": iname.red_jewel_l},
lname.villafy_dog_platform: {"code": 0x134, "offset": 0x10C89F, "normal item": iname.red_jewel_l},
lname.villafy_inner_gate: {"code": 0x138, "offset": 0xBFC8D7, "normal item": iname.roast_beef},
lname.villafy_gate_marker: {"code": 0x131, "offset": 0x10C8A7, "normal item": iname.powerup,
"hard item": iname.one_hundred_gold},
lname.villafy_villa_marker: {"code": 0x13E, "offset": 0x10C897, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold},
lname.villafy_tombstone: {"code": 0x12F, "offset": 0x8099CC, "normal item": iname.moon_card,
"type": "inv"},
lname.villafy_fountain_fl: {"code": 0x139, "offset": 0xBFC8CF, "normal item": iname.five_hundred_gold},
lname.villafy_fountain_fr: {"code": 0x130, "offset": 0x80997D, "normal item": iname.purifying},
lname.villafy_fountain_ml: {"code": 0x13A, "offset": 0x809956, "normal item": iname.sun_card},
lname.villafy_fountain_mr: {"code": 0x13D, "offset": 0x80992D, "normal item": iname.moon_card},
lname.villafy_fountain_rl: {"code": 0x13B, "offset": 0xBFC8D3, "normal item": iname.roast_beef,
"hard item": iname.five_hundred_gold},
lname.villafy_fountain_rr: {"code": 0x13C, "offset": 0x80993C, "normal item": iname.five_hundred_gold},
lname.villafo_front_r: {"code": 0x3D, "offset": 0x10C8E7, "normal item": iname.red_jewel_l,
"hard item": iname.five_hundred_gold},
lname.villafo_front_l: {"code": 0x3B, "offset": 0x10C8DF, "normal item": iname.red_jewel_s},
lname.villafo_mid_l: {"code": 0x3C, "offset": 0x10C8D7, "normal item": iname.red_jewel_s},
lname.villafo_mid_r: {"code": 0xE5, "offset": 0x10C8CF, "normal item": iname.three_hundred_gold,
"add conds": ["empty"]},
lname.villafo_rear_r: {"code": 0x38, "offset": 0x10C8C7, "normal item": iname.red_jewel_s},
lname.villafo_rear_l: {"code": 0x39, "offset": 0x10C8BF, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s},
lname.villafo_pot_r: {"code": 0x2E, "offset": 0x10C8AF, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s},
lname.villafo_pot_l: {"code": 0x2F, "offset": 0x10C8B7, "normal item": iname.red_jewel_s},
lname.villafo_sofa: {"code": 0x2D, "offset": 0x81F07C, "normal item": iname.purifying,
"type": "inv"},
lname.villafo_chandelier1: {"code": 0x27D, "offset": 0x10C8F5, "normal item": iname.red_jewel_l,
"add conds": ["3hb"]},
lname.villafo_chandelier2: {"code": 0x27E, "offset": 0x10C8F7, "normal item": iname.purifying,
"add conds": ["3hb"]},
lname.villafo_chandelier3: {"code": 0x27F, "offset": 0x10C8F9, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.villafo_chandelier4: {"code": 0x280, "offset": 0x10C8FB, "normal item": iname.cure_ampoule,
"add conds": ["3hb"]},
lname.villafo_chandelier5: {"code": 0x281, "offset": 0x10C8FD, "normal item": iname.roast_chicken,
"add conds": ["3hb"]},
lname.villala_hallway_stairs: {"code": 0x34, "offset": 0x10C927, "normal item": iname.red_jewel_l},
lname.villala_hallway_l: {"code": 0x40, "offset": 0xBFC903, "normal item": iname.knife,
"add conds": ["sub"]},
lname.villala_hallway_r: {"code": 0x4F, "offset": 0xBFC8F7, "normal item": iname.axe,
"add conds": ["sub"]},
lname.villala_bedroom_chairs: {"code": 0x33, "offset": 0x83A588, "normal item": iname.purifying,
"hard item": iname.three_hundred_gold},
lname.villala_bedroom_bed: {"code": 0x32, "offset": 0xBFC95B, "normal item": iname.red_jewel_l,
"hard item": iname.three_hundred_gold},
lname.villala_vincent: {"code": 0x23, "offset": 0xBFE42F, "normal item": iname.archives_key,
"type": "npc"},
lname.villala_slivingroom_table: {"code": 0x2B, "offset": 0xBFC96B, "normal item": iname.five_hundred_gold,
"type": "inv"},
lname.villala_slivingroom_mirror: {"code": 0x49, "offset": 0x83A5D9, "normal item": iname.cross,
"add conds": ["sub"]},
lname.villala_diningroom_roses: {"code": 0x2A, "offset": 0xBFC90B, "normal item": iname.purifying,
"hard item": iname.three_hundred_gold, "type": "inv"},
lname.villala_llivingroom_pot_r: {"code": 0x26, "offset": 0x10C90F, "normal item": iname.storeroom_key},
lname.villala_llivingroom_pot_l: {"code": 0x25, "offset": 0x10C917, "normal item": iname.roast_chicken},
lname.villala_llivingroom_painting: {"code": 0x2C, "offset": 0xBFC907, "normal item": iname.purifying,
"hard item": iname.one_hundred_gold, "type": "inv"},
lname.villala_llivingroom_light: {"code": 0x28, "offset": 0x10C91F, "normal item": iname.purifying},
lname.villala_llivingroom_lion: {"code": 0x30, "offset": 0x83A610, "normal item": iname.roast_chicken,
"hard item": iname.five_hundred_gold, "type": "inv"},
lname.villala_exit_knight: {"code": 0x27, "offset": 0xBFC967, "normal item": iname.purifying,
"type": "inv"},
lname.villala_storeroom_l: {"code": 0x36, "offset": 0xBFC95F, "normal item": iname.roast_beef},
lname.villala_storeroom_r: {"code": 0x37, "offset": 0xBFC8FF, "normal item": iname.roast_chicken,
"hard item": iname.five_hundred_gold},
lname.villala_storeroom_s: {"code": 0x31, "offset": 0xBFC963, "normal item": iname.purifying,
"hard item": iname.one_hundred_gold, "type": "inv"},
lname.villala_archives_entrance: {"code": 0x48, "offset": 0x83A5E5, "normal item": iname.holy_water,
"add conds": ["sub"]},
lname.villala_archives_table: {"code": 0x29, "offset": 0xBFC90F, "normal item": iname.purifying,
"type": "inv"},
lname.villala_archives_rear: {"code": 0x24, "offset": 0x83A5B1, "normal item": iname.garden_key},
lname.villam_malus_torch: {"code": 0x173, "offset": 0x10C967, "normal item": iname.red_jewel_s,
"countdown": 13},
lname.villam_malus_bush: {"code": 0x16C, "offset": 0x850FEC, "normal item": iname.roast_chicken,
"type": "inv", "countdown": 13},
lname.villam_fplatform: {"code": 0x16B, "offset": 0x10C987, "normal item": iname.knife,
"add conds": ["sub"], "countdown": 13},
lname.villam_frankieturf_l: {"code": 0x177, "offset": 0x10C947, "normal item": iname.three_hundred_gold,
"countdown": 13},
lname.villam_frankieturf_r: {"code": 0x16A, "offset": 0x10C98F, "normal item": iname.holy_water,
"add conds": ["sub"], "countdown": 13},
lname.villam_frankieturf_ru: {"code": 0x16E, "offset": 0x10C9A7, "normal item": iname.red_jewel_s,
"countdown": 13},
lname.villam_fgarden_f: {"code": 0x172, "offset": 0x10C96F, "normal item": iname.red_jewel_s,
"countdown": 13},
lname.villam_fgarden_mf: {"code": 0x171, "offset": 0x10C977, "normal item": iname.red_jewel_s,
"countdown": 13},
lname.villam_fgarden_mr: {"code": 0x174, "offset": 0x10C95F, "normal item": iname.roast_chicken,
"countdown": 13},
lname.villam_fgarden_r: {"code": 0x170, "offset": 0x10C97F, "normal item": iname.red_jewel_l,
"countdown": 13},
lname.villam_rplatform: {"code": 0x169, "offset": 0x10C997, "normal item": iname.axe,
"add conds": ["sub"], "countdown": 13},
lname.villam_rplatform_de: {"code": 0x176, "offset": 0x10C94F, "normal item": iname.five_hundred_gold,
"countdown": 13},
lname.villam_exit_de: {"code": 0x175, "offset": 0x10C957, "normal item": iname.three_hundred_gold,
"countdown": 13},
lname.villam_serv_path: {"code": 0x17A, "offset": 0x10C92F, "normal item": iname.copper_key,
"countdown": 13},
lname.villafo_serv_ent: {"code": 0x3E, "offset": 0x10C8EF, "normal item": iname.roast_chicken},
lname.villam_crypt_ent: {"code": 0x178, "offset": 0x10C93F, "normal item": iname.purifying,
"countdown": 13},
lname.villam_crypt_upstream: {"code": 0x179, "offset": 0x10C937, "normal item": iname.roast_beef,
"countdown": 13},
lname.villac_ent_l: {"code": 0xC9, "offset": 0x10CF4B, "normal item": iname.red_jewel_s,
"countdown": 13},
lname.villac_ent_r: {"code": 0xC0, "offset": 0x10CF63, "normal item": iname.five_hundred_gold,
"countdown": 13},
lname.villac_wall_l: {"code": 0xC2, "offset": 0x10CF6B, "normal item": iname.roast_chicken,
"countdown": 13},
lname.villac_wall_r: {"code": 0xC1, "offset": 0x10CF5B, "normal item": iname.red_jewel_l,
"countdown": 13},
lname.villac_coffin_l: {"code": 0xD8, "offset": 0x10CF73, "normal item": iname.knife,
"add conds": ["sub"], "countdown": 13},
lname.villac_coffin_r: {"code": 0xC8, "offset": 0x10CF53, "normal item": iname.red_jewel_s,
"countdown": 13},
lname.villa_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
lname.villa_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
# Tunnel
lname.tunnel_landing: {"code": 0x197, "offset": 0x10C9AF, "normal item": iname.red_jewel_l,
"hard item": iname.one_hundred_gold},
lname.tunnel_landing_rc: {"code": 0x196, "offset": 0x10C9B7, "normal item": iname.red_jewel_s,
"hard item": iname.one_hundred_gold},
lname.tunnel_stone_alcove_r: {"code": 0xE1, "offset": 0x10CA57, "normal item": iname.holy_water,
"add conds": ["sub"]},
lname.tunnel_stone_alcove_l: {"code": 0x187, "offset": 0x10CA9F, "normal item": iname.roast_beef,
"hard item": iname.roast_chicken},
lname.tunnel_twin_arrows: {"code": 0x195, "offset": 0xBFC993, "normal item": iname.cure_ampoule,
"type": "inv"},
lname.tunnel_arrows_rock1: {"code": 0x238, "offset": 0x10CABD, "normal item": iname.purifying,
"add conds": ["3hb"]},
lname.tunnel_arrows_rock2: {"code": 0x239, "offset": 0x10CABF, "normal item": iname.purifying,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.tunnel_arrows_rock3: {"code": 0x23A, "offset": 0x10CAC1, "normal item": iname.cure_ampoule,
"add conds": ["3hb"]},
lname.tunnel_arrows_rock4: {"code": 0x23B, "offset": 0x10CAC3, "normal item": iname.cure_ampoule,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.tunnel_arrows_rock5: {"code": 0x23C, "offset": 0x10CAC5, "normal item": iname.roast_chicken,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.tunnel_lonesome_bucket: {"code": 0x189, "offset": 0xBFC99B, "normal item": iname.cure_ampoule,
"type": "inv"},
lname.tunnel_lbucket_mdoor_l: {"code": 0x198, "offset": 0x10CA67, "normal item": iname.knife,
"add conds": ["sub"]},
lname.tunnel_lbucket_quag: {"code": 0x191, "offset": 0x10C9DF, "normal item": iname.red_jewel_l},
lname.tunnel_bucket_quag_rock1: {"code": 0x23E, "offset": 0x10CAC9, "normal item": iname.roast_beef,
"hard item": iname.roast_chicken, "add conds": ["3hb"]},
lname.tunnel_bucket_quag_rock2: {"code": 0x23F, "offset": 0x10CACB, "normal item": iname.roast_beef,
"hard item": iname.roast_chicken, "add conds": ["3hb"]},
lname.tunnel_bucket_quag_rock3: {"code": 0x240, "offset": 0x10CACD, "normal item": iname.roast_beef,
"hard item": iname.roast_chicken, "add conds": ["3hb"]},
lname.tunnel_lbucket_albert: {"code": 0x190, "offset": 0x10C9E7, "normal item": iname.red_jewel_s},
lname.tunnel_albert_camp: {"code": 0x192, "offset": 0x10C9D7, "normal item": iname.red_jewel_s},
lname.tunnel_albert_quag: {"code": 0x193, "offset": 0x10C9CF, "normal item": iname.red_jewel_l},
lname.tunnel_gondola_rc_sdoor_l: {"code": 0x53, "offset": 0x10CA5F, "normal item": iname.cross,
"add conds": ["sub"]},
lname.tunnel_gondola_rc_sdoor_m: {"code": 0x19E, "offset": 0x10CAA7, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold},
lname.tunnel_gondola_rc_sdoor_r: {"code": 0x188, "offset": 0x10CA27, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold},
lname.tunnel_gondola_rc: {"code": 0x19C, "offset": 0x10CAB7, "normal item": iname.powerup},
lname.tunnel_rgondola_station: {"code": 0x194, "offset": 0x10C9C7, "normal item": iname.red_jewel_s},
lname.tunnel_gondola_transfer: {"code": 0x186, "offset": 0x10CA2F, "normal item": iname.five_hundred_gold},
lname.tunnel_corpse_bucket_quag: {"code": 0x18E, "offset": 0x10C9F7, "normal item": iname.red_jewel_s},
lname.tunnel_corpse_bucket_mdoor_l: {"code": 0x52, "offset": 0x10CA6F, "normal item": iname.holy_water,
"add conds": ["sub"]},
lname.tunnel_corpse_bucket_mdoor_r: {"code": 0x185, "offset": 0x10CA37, "normal item": iname.sun_card,
"hard item": iname.one_hundred_gold},
lname.tunnel_shovel_quag_start: {"code": 0x18D, "offset": 0x10C9FF, "normal item": iname.red_jewel_l},
lname.tunnel_exit_quag_start: {"code": 0x18C, "offset": 0x10CA07, "normal item": iname.red_jewel_l},
lname.tunnel_shovel_quag_end: {"code": 0x18B, "offset": 0x10CA0F, "normal item": iname.red_jewel_l},
lname.tunnel_exit_quag_end: {"code": 0x184, "offset": 0x10CA3F, "normal item": iname.five_hundred_gold},
lname.tunnel_shovel: {"code": 0x18F, "offset": 0x86D8FC, "normal item": iname.roast_beef,
"type": "inv"},
lname.tunnel_shovel_save: {"code": 0x18A, "offset": 0x10CA17, "normal item": iname.red_jewel_l},
lname.tunnel_shovel_mdoor_l: {"code": 0x183, "offset": 0x10CA47, "normal item": iname.sun_card,
"hard item": iname.one_hundred_gold},
lname.tunnel_shovel_mdoor_r: {"code": 0x51, "offset": 0x10CA77, "normal item": iname.axe,
"add conds": ["sub"]},
lname.tunnel_shovel_sdoor_l: {"code": 0x182, "offset": 0x10CA4F, "normal item": iname.moon_card},
lname.tunnel_shovel_sdoor_m: {"code": 0x19D, "offset": 0x10CAAF, "normal item": iname.roast_chicken},
lname.tunnel_shovel_sdoor_r: {"code": 0x50, "offset": 0x10CA7F, "normal item": iname.cross,
"add conds": ["sub"]},
# Underground Waterway
lname.uw_near_ent: {"code": 0x4C, "offset": 0x10CB03, "normal item": iname.three_hundred_gold},
lname.uw_across_ent: {"code": 0x4E, "offset": 0x10CAF3, "normal item": iname.five_hundred_gold},
lname.uw_first_ledge1: {"code": 0x242, "offset": 0x10CB39, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.uw_first_ledge2: {"code": 0x243, "offset": 0x10CB3B, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.uw_first_ledge3: {"code": 0x244, "offset": 0x10CB3D, "normal item": iname.purifying,
"hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
lname.uw_first_ledge4: {"code": 0x245, "offset": 0x10CB3F, "normal item": iname.cure_ampoule,
"hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
lname.uw_first_ledge5: {"code": 0x246, "offset": 0x10CB41, "normal item": iname.purifying,
"hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
lname.uw_first_ledge6: {"code": 0x247, "offset": 0x10CB43, "normal item": iname.cure_ampoule,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.uw_poison_parkour: {"code": 0x4D, "offset": 0x10CAFB, "normal item": iname.cure_ampoule},
lname.uw_boss: {"event": iname.trophy, "add conds": ["boss"]},
lname.uw_waterfall_alcove: {"code": 0x57, "offset": 0x10CB23, "normal item": iname.five_hundred_gold},
lname.uw_carrie1: {"code": 0x4B, "offset": 0x10CB0B, "normal item": iname.moon_card,
"hard item": iname.five_hundred_gold, "add conds": ["carrie"]},
lname.uw_carrie2: {"code": 0x4A, "offset": 0x10CB13, "normal item": iname.roast_beef,
"hard item": iname.five_hundred_gold, "add conds": ["carrie"]},
lname.uw_bricks_save: {"code": 0x5A, "offset": 0x10CB33, "normal item": iname.powerup,
"hard item": iname.one_hundred_gold},
lname.uw_above_skel_ledge: {"code": 0x56, "offset": 0x10CB2B, "normal item": iname.roast_chicken},
lname.uw_in_skel_ledge1: {"code": 0x249, "offset": 0x10CB45, "normal item": iname.roast_chicken,
"add conds": ["3hb"]},
lname.uw_in_skel_ledge2: {"code": 0x24A, "offset": 0x10CB47, "normal item": iname.roast_chicken,
"add conds": ["3hb"]},
lname.uw_in_skel_ledge3: {"code": 0x24B, "offset": 0x10CB49, "normal item": iname.roast_chicken,
"add conds": ["3hb"]},
# Castle Center
lname.ccb_skel_hallway_ent: {"code": 0x1AF, "offset": 0x10CB67, "normal item": iname.red_jewel_s},
lname.ccb_skel_hallway_jun: {"code": 0x1A8, "offset": 0x10CBD7, "normal item": iname.powerup},
lname.ccb_skel_hallway_tc: {"code": 0x1AE, "offset": 0x10CB6F, "normal item": iname.red_jewel_l},
lname.ccb_skel_hallway_ba: {"code": 0x1B6, "offset": 0x10CBC7, "normal item": iname.cross,
"add conds": ["sub"]},
lname.ccb_behemoth_l_ff: {"code": 0x1AD, "offset": 0x10CB77, "normal item": iname.red_jewel_s},
lname.ccb_behemoth_l_mf: {"code": 0x1B3, "offset": 0x10CBA7, "normal item": iname.three_hundred_gold,
"hard item": iname.one_hundred_gold},
lname.ccb_behemoth_l_mr: {"code": 0x1AC, "offset": 0x10CB7F, "normal item": iname.red_jewel_l},
lname.ccb_behemoth_l_fr: {"code": 0x1B2, "offset": 0x10CBAF, "normal item": iname.three_hundred_gold,
"hard item": iname.one_hundred_gold},
lname.ccb_behemoth_r_ff: {"code": 0x1B1, "offset": 0x10CBB7, "normal item": iname.three_hundred_gold,
"hard item": iname.one_hundred_gold},
lname.ccb_behemoth_r_mf: {"code": 0x1AB, "offset": 0x10CB87, "normal item": iname.red_jewel_s},
lname.ccb_behemoth_r_mr: {"code": 0x1B0, "offset": 0x10CBBF, "normal item": iname.three_hundred_gold,
"hard item": iname.one_hundred_gold},
lname.ccb_behemoth_r_fr: {"code": 0x1AA, "offset": 0x10CB8F, "normal item": iname.red_jewel_l},
lname.ccb_behemoth_crate1: {"code": 0x24D, "offset": 0x10CBDD, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ccb_behemoth_crate2: {"code": 0x24E, "offset": 0x10CBDF, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ccb_behemoth_crate3: {"code": 0x24F, "offset": 0x10CBE1, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ccb_behemoth_crate4: {"code": 0x250, "offset": 0x10CBE3, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ccb_behemoth_crate5: {"code": 0x251, "offset": 0x10CBE5, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ccelv_near_machine: {"code": 0x11A, "offset": 0x10CBF7, "normal item": iname.red_jewel_s},
lname.ccelv_atop_machine: {"code": 0x118, "offset": 0x10CC17, "normal item": iname.powerup,
"hard item": iname.three_hundred_gold},
lname.ccelv_stand1: {"code": 0x253, "offset": 0x10CC1D, "normal item": iname.roast_beef,
"add conds": ["3hb"]},
lname.ccelv_stand2: {"code": 0x254, "offset": 0x10CC1F, "normal item": iname.roast_beef,
"hard item": iname.three_hundred_gold, "add conds": ["3hb"]},
lname.ccelv_stand3: {"code": 0x255, "offset": 0x10CC21, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.ccelv_pipes: {"code": 0x11B, "offset": 0x10CC07, "normal item": iname.one_hundred_gold},
lname.ccelv_switch: {"code": 0x100, "offset": 0x10CC0F, "normal item": iname.holy_water,
"add conds": ["sub"]},
lname.ccelv_staircase: {"code": 0x119, "offset": 0x10CBFF, "normal item": iname.red_jewel_l,
"hard item": iname.five_hundred_gold},
lname.ccff_redcarpet_knight: {"code": 0x10A, "offset": 0x8C44D9, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s, "type": "inv"},
lname.ccff_gears_side: {"code": 0x10F, "offset": 0x10CC33, "normal item": iname.red_jewel_s},
lname.ccff_gears_mid: {"code": 0x10E, "offset": 0x10CC3B, "normal item": iname.purifying,
"hard item": iname.one_hundred_gold},
lname.ccff_gears_corner: {"code": 0x10D, "offset": 0x10CC43, "normal item": iname.roast_chicken,
"hard item": iname.one_hundred_gold},
lname.ccff_lizard_knight: {"code": 0x109, "offset": 0x8C44E7, "normal item": iname.roast_chicken,
"hard item": iname.three_hundred_gold, "type": "inv"},
lname.ccff_lizard_near_knight: {"code": 0x101, "offset": 0x10CC5B, "normal item": iname.axe,
"add conds": ["sub"]},
lname.ccff_lizard_pit: {"code": 0x10C, "offset": 0x10CC4B, "normal item": iname.sun_card,
"hard item": iname.five_hundred_gold},
lname.ccff_lizard_corner: {"code": 0x10B, "offset": 0x10CC53, "normal item": iname.moon_card,
"hard item": iname.five_hundred_gold},
lname.ccff_lizard_locker_nfr: {"code": 0x104, "offset": 0x8C450A, "normal item": iname.red_jewel_l,
"add conds": ["liz"]},
lname.ccff_lizard_locker_nmr: {"code": 0x105, "offset": 0xBFC9C3, "normal item": iname.five_hundred_gold,
"add conds": ["liz"]},
lname.ccff_lizard_locker_nml: {"code": 0x106, "offset": 0xBFC9C7, "normal item": iname.red_jewel_l,
"hard item": iname.cure_ampoule, "add conds": ["liz"]},
lname.ccff_lizard_locker_nfl: {"code": 0x107, "offset": 0xBFCA07, "normal item": iname.powerup,
"add conds": ["liz"]},
lname.ccff_lizard_locker_fl: {"code": 0x102, "offset": 0xBFCA03, "normal item": iname.five_hundred_gold,
"add conds": ["liz"]},
lname.ccff_lizard_locker_fr: {"code": 0x103, "offset": 0x8C44F5, "normal item": iname.sun_card,
"hard item": iname.three_hundred_gold, "add conds": ["liz"]},
lname.ccff_lizard_slab1: {"code": 0x257, "offset": 0x10CC61, "normal item": iname.purifying,
"hard item": iname.roast_chicken, "add conds": ["3hb"]},
lname.ccff_lizard_slab2: {"code": 0x258, "offset": 0x10CC63, "normal item": iname.purifying,
"hard item": iname.powerup, "add conds": ["3hb"]},
lname.ccff_lizard_slab3: {"code": 0x259, "offset": 0x10CC65, "normal item": iname.cure_ampoule,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.ccff_lizard_slab4: {"code": 0x25A, "offset": 0x10CC67, "normal item": iname.cure_ampoule,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.ccb_mandrag_shelf_l: {"code": 0x1A0, "offset": 0xBFCBB3, "normal item": iname.mandragora},
lname.ccb_mandrag_shelf_r: {"code": 0x1A1, "offset": 0xBFCBAF, "normal item": iname.mandragora},
lname.ccb_torture_rack: {"code": 0x1A9, "offset": 0x8985E5, "normal item": iname.purifying,
"type": "inv"},
lname.ccb_torture_rafters: {"code": 0x1A2, "offset": 0x8985D6, "normal item": iname.roast_beef},
lname.cc_behind_the_seal: {"event": iname.crystal, "add conds": ["crystal"]},
lname.cc_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
lname.cc_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
lname.ccll_brokenstairs_floor: {"code": 0x7B, "offset": 0x10CC8F, "normal item": iname.red_jewel_l,
"countdown": 14},
lname.ccll_brokenstairs_knight: {"code": 0x74, "offset": 0x8DF782, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold, "type": "inv", "countdown": 14},
lname.ccll_brokenstairs_save: {"code": 0x7C, "offset": 0x10CC87, "normal item": iname.red_jewel_l,
"countdown": 14},
lname.ccll_glassknight_l: {"code": 0x7A, "offset": 0x10CC97, "normal item": iname.red_jewel_s,
"hard item": iname.five_hundred_gold, "countdown": 14},
lname.ccll_glassknight_r: {"code": 0x7E, "offset": 0x10CC77, "normal item": iname.red_jewel_s,
"hard item": iname.five_hundred_gold, "countdown": 14},
lname.ccll_butlers_door: {"code": 0x7D, "offset": 0x10CC7F, "normal item": iname.red_jewel_s,
"countdown": 14},
lname.ccll_butlers_side: {"code": 0x79, "offset": 0x10CC9F, "normal item": iname.purifying,
"hard item": iname.one_hundred_gold, "countdown": 14},
lname.ccll_cwhall_butlerflames_past: {"code": 0x78, "offset": 0x10CCA7, "normal item": iname.cure_ampoule,
"hard item": iname.red_jewel_l, "countdown": 14},
lname.ccll_cwhall_flamethrower: {"code": 0x73, "offset": 0x8DF580, "normal item": iname.five_hundred_gold,
"type": "inv", "countdown": 14},
lname.ccll_cwhall_cwflames: {"code": 0x77, "offset": 0x10CCAF, "normal item": iname.roast_chicken,
"hard item": iname.red_jewel_l, "countdown": 14},
lname.ccll_heinrich: {"code": 0x69, "offset": 0xBFE443, "normal item": iname.chamber_key,
"type": "npc", "countdown": 14},
lname.ccia_nitro_crates: {"code": 0x66, "offset": 0x90FCE9, "normal item": iname.healing_kit,
"hard item": iname.one_hundred_gold, "type": "inv", "countdown": 14},
lname.ccia_nitro_shelf_h: {"code": 0x55, "offset": 0xBFCC03, "normal item": iname.magical_nitro,
"countdown": 14},
lname.ccia_stairs_knight: {"code": 0x61, "offset": 0x90FE5C, "normal item": iname.five_hundred_gold,
"type": "inv", "countdown": 14},
lname.ccia_maids_vase: {"code": 0x63, "offset": 0x90FF1D, "normal item": iname.red_jewel_l,
"type": "inv", "countdown": 14},
lname.ccia_maids_outer: {"code": 0x6B, "offset": 0x10CCFF, "normal item": iname.purifying,
"hard item": iname.three_hundred_gold, "countdown": 14},
lname.ccia_maids_inner: {"code": 0x6A, "offset": 0x10CD07, "normal item": iname.cure_ampoule,
"hard item": iname.three_hundred_gold, "countdown": 14},
lname.ccia_inventions_maids: {"code": 0x6C, "offset": 0x10CCE7, "normal item": iname.moon_card,
"hard item": iname.one_hundred_gold, "countdown": 14},
lname.ccia_inventions_crusher: {"code": 0x6E, "offset": 0x10CCDF, "normal item": iname.sun_card,
"hard item": iname.one_hundred_gold, "countdown": 14},
lname.ccia_inventions_famicart: {"code": 0x64, "offset": 0x90FBB3, "normal item": iname.five_hundred_gold,
"type": "inv", "countdown": 14},
lname.ccia_inventions_zeppelin: {"code": 0x6D, "offset": 0x90FBC0, "normal item": iname.roast_beef,
"countdown": 14},
lname.ccia_inventions_round: {"code": 0x65, "offset": 0x90FBA7, "normal item": iname.roast_beef,
"hard item": iname.five_hundred_gold, "type": "inv", "countdown": 14},
lname.ccia_nitrohall_flamethrower: {"code": 0x62, "offset": 0x90FCDA, "normal item": iname.red_jewel_l,
"type": "inv", "countdown": 14},
lname.ccia_nitrohall_torch: {"code": 0x6F, "offset": 0x10CCD7, "normal item": iname.roast_chicken,
"hard item": iname.red_jewel_s, "countdown": 14},
lname.ccia_nitro_shelf_i: {"code": 0x60, "offset": 0xBFCBFF, "normal item": iname.magical_nitro,
"countdown": 14},
lname.ccll_cwhall_wall: {"code": 0x76, "offset": 0x10CCB7, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold, "countdown": 14},
lname.ccl_bookcase: {"code": 0x166, "offset": 0x8F1197, "normal item": iname.sun_card,
"countdown": 14},
# Duel Tower
lname.dt_boss_one: {"event": iname.trophy, "add conds": ["boss"]},
lname.dt_boss_two: {"event": iname.trophy, "add conds": ["boss"]},
lname.dt_ibridge_l: {"code": 0x81, "offset": 0x10CE8B, "normal item": iname.roast_beef,
"hard item": iname.five_hundred_gold},
lname.dt_ibridge_r: {"code": 0x80, "offset": 0x10CE93, "normal item": iname.powerup},
lname.dt_stones_start: {"code": 0x83, "offset": 0x10CE73, "normal item": iname.roast_chicken,
"hard item": iname.five_hundred_gold},
lname.dt_stones_end: {"code": 0x97, "offset": 0x10CE83, "normal item": iname.knife, "add conds": ["sub"]},
lname.dt_werebull_arena: {"code": 0x82, "offset": 0x10CE7B, "normal item": iname.roast_beef},
lname.dt_boss_three: {"event": iname.trophy, "add conds": ["boss"]},
lname.dt_boss_four: {"event": iname.trophy, "add conds": ["boss"]},
# Tower of Execution
lname.toe_ledge1: {"code": 0x25C, "offset": 0x10CD5D, "normal item": iname.red_jewel_l,
"add conds": ["3hb"]},
lname.toe_ledge2: {"code": 0x25D, "offset": 0x10CD5F, "normal item": iname.purifying,
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
lname.toe_ledge3: {"code": 0x25E, "offset": 0x10CD61, "normal item": iname.five_hundred_gold,
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
lname.toe_ledge4: {"code": 0x25F, "offset": 0x10CD63, "normal item": iname.cure_ampoule,
"hard item": iname.five_hundred_gold, "add conds": ["3hb"]},
lname.toe_ledge5: {"code": 0x260, "offset": 0x10CD65, "normal item": iname.holy_water,
"add conds": ["3hb", "sub"]},
lname.toe_midsavespikes_r: {"code": 0x9C, "offset": 0x10CD1F, "normal item": iname.five_hundred_gold},
lname.toe_midsavespikes_l: {"code": 0x9B, "offset": 0x10CD27, "normal item": iname.roast_chicken,
"hard item": iname.five_hundred_gold},
lname.toe_elec_grate: {"code": 0x99, "offset": 0x10CD17, "normal item": iname.execution_key},
lname.toe_ibridge: {"code": 0x98, "offset": 0x10CD47, "normal item": iname.one_hundred_gold},
lname.toe_top: {"code": 0x9D, "offset": 0x10CD4F, "normal item": iname.red_jewel_l},
lname.toe_keygate_l: {"code": 0x9A, "offset": 0x10CD37, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold},
lname.toe_keygate_r: {"code": 0x9E, "offset": 0x10CD3F, "normal item": iname.cross, "add conds": ["sub"]},
# Tower of Science
lname.tosci_elevator: {"code": 0x1FC, "offset": 0x10CE0B, "normal item": iname.three_hundred_gold},
lname.tosci_plain_sr: {"code": 0x1FF, "offset": 0x10CDF3, "normal item": iname.science_key1},
lname.tosci_stairs_sr: {"code": 0x1FB, "offset": 0x10CE13, "normal item": iname.three_hundred_gold},
lname.tosci_three_door_hall: {"code": 0x1FE, "offset": 0x10CDFB, "normal item": iname.science_key2},
lname.tosci_ibridge_t: {"code": 0x1F3, "offset": 0x10CE3B, "normal item": iname.roast_beef,
"hard item": iname.red_jewel_l},
lname.tosci_ibridge_b1: {"code": 0x262, "offset": 0x10CE59, "normal item": iname.red_jewel_l,
"add conds": ["3hb"]},
lname.tosci_ibridge_b2: {"code": 0x263, "offset": 0x10CE5B, "normal item": iname.red_jewel_l,
"add conds": ["3hb"]},
lname.tosci_ibridge_b3: {"code": 0x264, "offset": 0x10CE5D, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.tosci_ibridge_b4: {"code": 0x265, "offset": 0x10CE5F, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.tosci_ibridge_b5: {"code": 0x266, "offset": 0x10CE61, "normal item": iname.roast_chicken,
"add conds": ["3hb"]},
lname.tosci_ibridge_b6: {"code": 0x267, "offset": 0x10CE63, "normal item": iname.roast_chicken,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.tosci_conveyor_sr: {"code": 0x1F7, "offset": 0x10CE33, "normal item": iname.red_jewel_l,
"hard item": iname.red_jewel_s},
lname.tosci_exit: {"code": 0x1FD, "offset": 0x10CE03, "normal item": iname.science_key3},
lname.tosci_key3_r: {"code": 0x1FA, "offset": 0x10CE1B, "normal item": iname.five_hundred_gold},
lname.tosci_key3_m: {"code": 0x1F2, "offset": 0x10CE2B, "normal item": iname.cross, "add conds": ["sub"]},
lname.tosci_key3_l: {"code": 0x1F9, "offset": 0x10CE23, "normal item": iname.five_hundred_gold},
# Tower of Sorcery
lname.tosor_stained_tower: {"code": 0x96, "offset": 0x10CDB3, "normal item": iname.red_jewel_l},
lname.tosor_savepoint: {"code": 0x95, "offset": 0x10CDBB, "normal item": iname.red_jewel_l},
lname.tosor_trickshot: {"code": 0x92, "offset": 0x10CDD3, "normal item": iname.roast_beef},
lname.tosor_yellow_bubble: {"code": 0x91, "offset": 0x10CDDB, "normal item": iname.five_hundred_gold},
lname.tosor_blue_platforms: {"code": 0x94, "offset": 0x10CDC3, "normal item": iname.red_jewel_s},
lname.tosor_side_isle: {"code": 0x93, "offset": 0x10CDCB, "normal item": iname.red_jewel_s},
lname.tosor_ibridge: {"code": 0x90, "offset": 0x10CDE3, "normal item": iname.three_hundred_gold},
# Room of Clocks
lname.roc_ent_l: {"code": 0xC6, "offset": 0x10CF7B, "normal item": iname.roast_beef,
"hard item": iname.red_jewel_l},
lname.roc_ent_r: {"code": 0xC3, "offset": 0x10CFBB, "normal item": iname.powerup,
"hard item": iname.five_hundred_gold},
lname.roc_elev_r: {"code": 0xD4, "offset": 0x10CF93, "normal item": iname.holy_water, "add conds": ["sub"]},
lname.roc_elev_l: {"code": 0xD5, "offset": 0x10CF8B, "normal item": iname.axe, "add conds": ["sub"]},
lname.roc_cont_r: {"code": 0xC5, "offset": 0x10CFB3, "normal item": iname.powerup,
"hard item": iname.one_hundred_gold},
lname.roc_cont_l: {"code": 0xDF, "offset": 0x10CFA3, "normal item": iname.three_hundred_gold,
"add conds": ["empty"]},
lname.roc_exit: {"code": 0xDC, "offset": 0x10CF9B, "normal item": iname.three_hundred_gold,
"add conds": ["empty"]},
lname.roc_boss: {"event": iname.trophy, "add conds": ["boss"]},
# Clock Tower
lname.ct_gearclimb_battery_slab1: {"code": 0x269, "offset": 0x10CEF9, "normal item": iname.roast_chicken,
"add conds": ["3hb"]},
lname.ct_gearclimb_battery_slab2: {"code": 0x26A, "offset": 0x10CEFB, "normal item": iname.roast_chicken,
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
lname.ct_gearclimb_battery_slab3: {"code": 0x26B, "offset": 0x10CEFD, "normal item": iname.roast_chicken,
"hard item": iname.red_jewel_s, "add conds": ["3hb"]},
lname.ct_gearclimb_corner: {"code": 0xA7, "offset": 0x10CEB3, "normal item": iname.red_jewel_s},
lname.ct_gearclimb_side: {"code": 0xAD, "offset": 0x10CEC3, "normal item": iname.clocktower_key1},
lname.ct_gearclimb_door_slab1: {"code": 0x26D, "offset": 0x10CF01, "normal item": iname.roast_beef,
"add conds": ["3hb"]},
lname.ct_gearclimb_door_slab2: {"code": 0x26E, "offset": 0x10CF03, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.ct_gearclimb_door_slab3: {"code": 0x26F, "offset": 0x10CF05, "normal item": iname.roast_beef,
"hard item": iname.one_hundred_gold, "add conds": ["3hb"]},
lname.ct_bp_chasm_fl: {"code": 0xA5, "offset": 0x99BC4D, "normal item": iname.five_hundred_gold},
lname.ct_bp_chasm_fr: {"code": 0xA6, "offset": 0x99BC3E, "normal item": iname.red_jewel_l},
lname.ct_bp_chasm_rl: {"code": 0xA4, "offset": 0x99BC5A, "normal item": iname.holy_water,
"add conds": ["sub"]},
lname.ct_bp_chasm_k: {"code": 0xAC, "offset": 0x99BC30, "normal item": iname.clocktower_key2},
lname.ct_finalroom_door_slab1: {"code": 0x271, "offset": 0x10CEF5, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ct_finalroom_door_slab2: {"code": 0x272, "offset": 0x10CEF7, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ct_finalroom_fl: {"code": 0xB3, "offset": 0x10CED3, "normal item": iname.axe,
"add conds": ["sub"]},
lname.ct_finalroom_fr: {"code": 0xB4, "offset": 0x10CECB, "normal item": iname.knife,
"add conds": ["sub"]},
lname.ct_finalroom_rl: {"code": 0xB2, "offset": 0x10CEE3, "normal item": iname.holy_water,
"add conds": ["sub"]},
lname.ct_finalroom_rr: {"code": 0xB0, "offset": 0x10CEDB, "normal item": iname.cross,
"add conds": ["sub"]},
lname.ct_finalroom_platform: {"code": 0xAB, "offset": 0x10CEBB, "normal item": iname.clocktower_key3},
lname.ct_finalroom_renon_slab1: {"code": 0x274, "offset": 0x10CF09, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ct_finalroom_renon_slab2: {"code": 0x275, "offset": 0x10CF0B, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ct_finalroom_renon_slab3: {"code": 0x276, "offset": 0x10CF0D, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ct_finalroom_renon_slab4: {"code": 0x277, "offset": 0x10CF0F, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ct_finalroom_renon_slab5: {"code": 0x278, "offset": 0x10CF11, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ct_finalroom_renon_slab6: {"code": 0x279, "offset": 0x10CF13, "normal item": iname.five_hundred_gold,
"add conds": ["3hb"]},
lname.ct_finalroom_renon_slab7: {"code": 0x27A, "offset": 0x10CF15, "normal item": iname.red_jewel_l,
"add conds": ["3hb"]},
lname.ct_finalroom_renon_slab8: {"code": 0x27B, "offset": 0x10CF17, "normal item": iname.red_jewel_l,
"add conds": ["3hb"]},
# Castle Keep
lname.ck_boss_one: {"event": iname.trophy, "add conds": ["boss", "renon"]},
lname.ck_boss_two: {"event": iname.trophy, "add conds": ["boss", "vincent"]},
lname.ck_flame_l: {"code": 0xAF, "offset": 0x9778C8, "normal item": iname.healing_kit, "type": "inv"},
lname.ck_flame_r: {"code": 0xAE, "offset": 0xBFCA67, "normal item": iname.healing_kit, "type": "inv"},
lname.ck_behind_drac: {"code": 0xBF, "offset": 0x10CE9B, "normal item": iname.red_jewel_l},
lname.ck_cube: {"code": 0xB5, "offset": 0x10CEA3, "normal item": iname.healing_kit},
lname.renon1: {"code": 0x1C8, "offset": 0xBFD8E5, "normal item": iname.roast_chicken, "type": "shop"},
lname.renon2: {"code": 0x1C9, "offset": 0xBFD8E7, "normal item": iname.roast_beef, "type": "shop"},
lname.renon3: {"code": 0x1CA, "offset": 0xBFD8E9, "normal item": iname.healing_kit, "type": "shop"},
lname.renon4: {"code": 0x1CB, "offset": 0xBFD8EB, "normal item": iname.purifying, "type": "shop"},
lname.renon5: {"code": 0x1CC, "offset": 0xBFD8ED, "normal item": iname.cure_ampoule, "type": "shop"},
lname.renon6: {"code": 0x1CD, "offset": 0xBFD907, "normal item": iname.sun_card, "type": "shop"},
lname.renon7: {"code": 0x1CE, "offset": 0xBFD909, "normal item": iname.moon_card, "type": "shop"},
lname.the_end: {"event": iname.victory},
}
add_conds = {"carrie": ("carrie_logic", True, True),
"liz": ("lizard_locker_items", True, True),
"sub": ("sub_weapon_shuffle", SubWeaponShuffle.option_anywhere, True),
"3hb": ("multi_hit_breakables", True, True),
"empty": ("empty_breakables", True, True),
"shop": ("shopsanity", True, True),
"crystal": ("draculas_condition", DraculasCondition.option_crystal, True),
"boss": ("draculas_condition", DraculasCondition.option_bosses, True),
"renon": ("renon_fight_condition", RenonFightCondition.option_never, False),
"vincent": ("vincent_fight_condition", VincentFightCondition.option_never, False)}
def get_location_info(location: str, info: str) -> Union[int, str, List[str], None]:
return location_info[location].get(info, None)
def get_location_names_to_ids() -> Dict[str, int]:
return {name: get_location_info(name, "code")+base_id for name in location_info if get_location_info(name, "code")
is not None}
def verify_locations(options: CV64Options, locations: List[str]) -> Tuple[Dict[str, Optional[int]], Dict[str, str]]:
verified_locations = {}
events = {}
for loc in locations:
loc_add_conds = get_location_info(loc, "add conds")
loc_code = get_location_info(loc, "code")
# Check any options that might be associated with the Location before adding it.
add_it = True
if isinstance(loc_add_conds, list):
for cond in loc_add_conds:
if not ((getattr(options, add_conds[cond][0]).value == add_conds[cond][1]) == add_conds[cond][2]):
add_it = False
if not add_it:
continue
# Add the location to the verified Locations if the above check passes.
# If we are looking at an event Location, add its associated event Item to the events' dict.
# Otherwise, add the base_id to the Location's code.
if loc_code is None:
events[loc] = get_location_info(loc, "event")
else:
loc_code += base_id
verified_locations.update({loc: loc_code})
return verified_locations, events

266
worlds/cv64/lzkn64.py Normal file
View File

@ -0,0 +1,266 @@
# **************************************************************
# * LZKN64 Compression and Decompression Utility *
# * Original repo at https://github.com/Fluvian/lzkn64, *
# * converted from C to Python with permission from Fluvian. *
# **************************************************************
TYPE_COMPRESS = 1
TYPE_DECOMPRESS = 2
MODE_NONE = 0x7F
MODE_WINDOW_COPY = 0x00
MODE_RAW_COPY = 0x80
MODE_RLE_WRITE_A = 0xC0
MODE_RLE_WRITE_B = 0xE0
MODE_RLE_WRITE_C = 0xFF
WINDOW_SIZE = 0x3FF
COPY_SIZE = 0x21
RLE_SIZE = 0x101
# Compresses the data in the buffer specified in the arguments.
def compress_buffer(file_buffer: bytearray) -> bytearray:
# Size of the buffer to compress
buffer_size = len(file_buffer) - 1
# Position of the current read location in the buffer.
buffer_position = 0
# Position of the current write location in the written buffer.
write_position = 4
# Allocate write_buffer with size of 0xFFFFFF (24-bit).
write_buffer = bytearray(0xFFFFFF)
# Position in the input buffer of the last time one of the copy modes was used.
buffer_last_copy_position = 0
while buffer_position < buffer_size:
# Calculate maximum length we are able to copy without going out of bounds.
if COPY_SIZE < (buffer_size - 1) - buffer_position:
sliding_window_maximum_length = COPY_SIZE
else:
sliding_window_maximum_length = (buffer_size - 1) - buffer_position
# Calculate how far we are able to look back without going behind the start of the uncompressed buffer.
if buffer_position - WINDOW_SIZE > 0:
sliding_window_maximum_offset = buffer_position - WINDOW_SIZE
else:
sliding_window_maximum_offset = 0
# Calculate maximum length the forwarding looking window is able to search.
if RLE_SIZE < (buffer_size - 1) - buffer_position:
forward_window_maximum_length = RLE_SIZE
else:
forward_window_maximum_length = (buffer_size - 1) - buffer_position
sliding_window_match_position = -1
sliding_window_match_size = 0
forward_window_match_value = 0
forward_window_match_size = 0
# The current mode the compression algorithm prefers. (0x7F == None)
current_mode = MODE_NONE
# The current submode the compression algorithm prefers.
current_submode = MODE_NONE
# How many bytes will have to be copied in the raw copy command.
raw_copy_size = buffer_position - buffer_last_copy_position
# How many bytes we still have to copy in RLE matches with more than 0x21 bytes.
rle_bytes_left = 0
"""Go backwards in the buffer, is there a matching value?
If yes, search forward and check for more matching values in a loop.
If no, go further back and repeat."""
for search_position in range(buffer_position - 1, sliding_window_maximum_offset - 1, -1):
matching_sequence_size = 0
while file_buffer[search_position + matching_sequence_size] == file_buffer[buffer_position +
matching_sequence_size]:
matching_sequence_size += 1
if matching_sequence_size >= sliding_window_maximum_length:
break
# Once we find a match or a match that is bigger than the match before it, we save its position and length.
if matching_sequence_size > sliding_window_match_size:
sliding_window_match_position = search_position
sliding_window_match_size = matching_sequence_size
"""Look one step forward in the buffer, is there a matching value?
If yes, search further and check for a repeating value in a loop.
If no, continue to the rest of the function."""
matching_sequence_value = file_buffer[buffer_position]
matching_sequence_size = 0
while file_buffer[buffer_position + matching_sequence_size] == matching_sequence_value:
matching_sequence_size += 1
if matching_sequence_size >= forward_window_maximum_length:
break
# If we find a sequence of matching values, save them.
if matching_sequence_size >= 1:
forward_window_match_value = matching_sequence_value
forward_window_match_size = matching_sequence_size
# Try to pick which mode works best with the current values.
if sliding_window_match_size >= 3:
current_mode = MODE_WINDOW_COPY
elif forward_window_match_size >= 3:
current_mode = MODE_RLE_WRITE_A
if forward_window_match_value != 0x00 and forward_window_match_size <= COPY_SIZE:
current_submode = MODE_RLE_WRITE_A
elif forward_window_match_value != 0x00 and forward_window_match_size > COPY_SIZE:
current_submode = MODE_RLE_WRITE_A
rle_bytes_left = forward_window_match_size
elif forward_window_match_value == 0x00 and forward_window_match_size <= COPY_SIZE:
current_submode = MODE_RLE_WRITE_B
elif forward_window_match_value == 0x00 and forward_window_match_size > COPY_SIZE:
current_submode = MODE_RLE_WRITE_C
elif forward_window_match_size >= 2 and forward_window_match_value == 0x00:
current_mode = MODE_RLE_WRITE_A
current_submode = MODE_RLE_WRITE_B
"""Write a raw copy command when these following conditions are met:
The current mode is set and there are raw bytes available to be copied.
The raw byte length exceeds the maximum length that can be stored.
Raw bytes need to be written due to the proximity to the end of the buffer."""
if (current_mode != MODE_NONE and raw_copy_size >= 1) or raw_copy_size >= 0x1F or \
(buffer_position + 1) == buffer_size:
if buffer_position + 1 == buffer_size:
raw_copy_size = buffer_size - buffer_last_copy_position
write_buffer[write_position] = MODE_RAW_COPY | raw_copy_size & 0x1F
write_position += 1
for written_bytes in range(raw_copy_size):
write_buffer[write_position] = file_buffer[buffer_last_copy_position]
write_position += 1
buffer_last_copy_position += 1
if current_mode == MODE_WINDOW_COPY:
write_buffer[write_position] = MODE_WINDOW_COPY | ((sliding_window_match_size - 2) & 0x1F) << 2 | \
(((buffer_position - sliding_window_match_position) & 0x300) >> 8)
write_position += 1
write_buffer[write_position] = (buffer_position - sliding_window_match_position) & 0xFF
write_position += 1
buffer_position += sliding_window_match_size
buffer_last_copy_position = buffer_position
elif current_mode == MODE_RLE_WRITE_A:
if current_submode == MODE_RLE_WRITE_A:
if rle_bytes_left > 0:
while rle_bytes_left > 0:
# Dump raw bytes if we have less than two bytes left, not doing so would cause an underflow
# error.
if rle_bytes_left < 2:
write_buffer[write_position] = MODE_RAW_COPY | rle_bytes_left & 0x1F
write_position += 1
for writtenBytes in range(rle_bytes_left):
write_buffer[write_position] = forward_window_match_value & 0xFF
write_position += 1
rle_bytes_left = 0
break
if rle_bytes_left < COPY_SIZE:
write_buffer[write_position] = MODE_RLE_WRITE_A | (rle_bytes_left - 2) & 0x1F
write_position += 1
else:
write_buffer[write_position] = MODE_RLE_WRITE_A | (COPY_SIZE - 2) & 0x1F
write_position += 1
write_buffer[write_position] = forward_window_match_value & 0xFF
write_position += 1
rle_bytes_left -= COPY_SIZE
else:
write_buffer[write_position] = MODE_RLE_WRITE_A | (forward_window_match_size - 2) & 0x1F
write_position += 1
write_buffer[write_position] = forward_window_match_value & 0xFF
write_position += 1
elif current_submode == MODE_RLE_WRITE_B:
write_buffer[write_position] = MODE_RLE_WRITE_B | (forward_window_match_size - 2) & 0x1F
write_position += 1
elif current_submode == MODE_RLE_WRITE_C:
write_buffer[write_position] = MODE_RLE_WRITE_C
write_position += 1
write_buffer[write_position] = (forward_window_match_size - 2) & 0xFF
write_position += 1
buffer_position += forward_window_match_size
buffer_last_copy_position = buffer_position
else:
buffer_position += 1
# Write the compressed size.
write_buffer[1] = 0x00
write_buffer[1] = write_position >> 16 & 0xFF
write_buffer[2] = write_position >> 8 & 0xFF
write_buffer[3] = write_position & 0xFF
# Return the compressed write buffer.
return write_buffer[0:write_position]
# Decompresses the data in the buffer specified in the arguments.
def decompress_buffer(file_buffer: bytearray) -> bytearray:
# Position of the current read location in the buffer.
buffer_position = 4
# Position of the current write location in the written buffer.
write_position = 0
# Get compressed size.
compressed_size = (file_buffer[1] << 16) + (file_buffer[2] << 8) + file_buffer[3] - 1
# Allocate writeBuffer with size of 0xFFFFFF (24-bit).
write_buffer = bytearray(0xFFFFFF)
while buffer_position < compressed_size:
mode_command = file_buffer[buffer_position]
buffer_position += 1
if MODE_WINDOW_COPY <= mode_command < MODE_RAW_COPY:
copy_length = (mode_command >> 2) + 2
copy_offset = file_buffer[buffer_position] + (mode_command << 8) & 0x3FF
buffer_position += 1
for current_length in range(copy_length, 0, -1):
write_buffer[write_position] = write_buffer[write_position - copy_offset]
write_position += 1
elif MODE_RAW_COPY <= mode_command < MODE_RLE_WRITE_A:
copy_length = mode_command & 0x1F
for current_length in range(copy_length, 0, -1):
write_buffer[write_position] = file_buffer[buffer_position]
write_position += 1
buffer_position += 1
elif MODE_RLE_WRITE_A <= mode_command <= MODE_RLE_WRITE_C:
write_length = 0
write_value = 0x00
if MODE_RLE_WRITE_A <= mode_command < MODE_RLE_WRITE_B:
write_length = (mode_command & 0x1F) + 2
write_value = file_buffer[buffer_position]
buffer_position += 1
elif MODE_RLE_WRITE_B <= mode_command < MODE_RLE_WRITE_C:
write_length = (mode_command & 0x1F) + 2
elif mode_command == MODE_RLE_WRITE_C:
write_length = file_buffer[buffer_position] + 2
buffer_position += 1
for current_length in range(write_length, 0, -1):
write_buffer[write_position] = write_value
write_position += 1
# Return the current position of the write buffer, essentially giving us the size of the write buffer.
while write_position % 16 != 0:
write_position += 1
return write_buffer[0:write_position]

490
worlds/cv64/options.py Normal file
View File

@ -0,0 +1,490 @@
from dataclasses import dataclass
from Options import Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool
class CharacterStages(Choice):
"""Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end
of Villa and Castle Center."""
display_name = "Character Stages"
option_both = 0
option_branchless_both = 1
option_reinhardt_only = 2
option_carrie_only = 3
default = 0
class StageShuffle(Toggle):
"""Shuffles which stages appear in which stage slots. Villa and Castle Center will never appear in any character
stage slots if Character Stages is set to Both; they can only be somewhere on the main path.
Castle Keep will always be at the end of the line."""
display_name = "Stage Shuffle"
class StartingStage(Choice):
"""Which stage to start at if Stage Shuffle is turned on."""
display_name = "Starting Stage"
option_forest_of_silence = 0
option_castle_wall = 1
option_villa = 2
option_tunnel = 3
option_underground_waterway = 4
option_castle_center = 5
option_duel_tower = 6
option_tower_of_execution = 7
option_tower_of_science = 8
option_tower_of_sorcery = 9
option_room_of_clocks = 10
option_clock_tower = 11
default = "random"
class WarpOrder(Choice):
"""Arranges the warps in the warp menu in whichever stage order chosen,
thereby changing the order they are unlocked in."""
display_name = "Warp Order"
option_seed_stage_order = 0
option_vanilla_stage_order = 1
option_randomized_order = 2
default = 0
class SubWeaponShuffle(Choice):
"""Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool."""
display_name = "Sub-weapon Shuffle"
option_off = 0
option_own_pool = 1
option_anywhere = 2
default = 0
class SpareKeys(Choice):
"""Puts an additional copy of every non-Special key item in the pool for every key item that there is.
Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them."""
display_name = "Spare Keys"
option_off = 0
option_on = 1
option_chance = 2
default = 0
class HardItemPool(Toggle):
"""Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode
in the PAL version."""
display_name = "Hard Item Pool"
class Special1sPerWarp(Range):
"""Sets how many Special1 jewels are needed per warp menu option unlock."""
range_start = 1
range_end = 10
default = 1
display_name = "Special1s Per Warp"
class TotalSpecial1s(Range):
"""Sets how many Speical1 jewels are in the pool in total.
If this is set to be less than Special1s Per Warp x 7, it will decrease by 1 until it isn't."""
range_start = 7
range_end = 70
default = 7
display_name = "Total Special1s"
class DraculasCondition(Choice):
"""Sets the requirement for unlocking and opening the door to Dracula's chamber.
None: No requirement. Door is unlocked from the start.
Crystal: Activate the big crystal in Castle Center's basement. Neither boss afterwards has to be defeated.
Bosses: Kill a specified number of bosses with health bars and claim their Trophies.
Specials: Find a specified number of Special2 jewels shuffled in the main item pool."""
display_name = "Dracula's Condition"
option_none = 0
option_crystal = 1
option_bosses = 2
option_specials = 3
default = 1
class PercentSpecial2sRequired(Range):
"""Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s."""
range_start = 1
range_end = 100
default = 80
display_name = "Percent Special2s Required"
class TotalSpecial2s(Range):
"""How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s."""
range_start = 1
range_end = 70
default = 25
display_name = "Total Special2s"
class BossesRequired(Range):
"""How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses.
This will automatically adjust if there are fewer available bosses than the chosen number."""
range_start = 1
range_end = 16
default = 14
display_name = "Bosses Required"
class CarrieLogic(Toggle):
"""Adds the 2 checks inside Underground Waterway's crawlspace to the pool.
If you (and everyone else if racing the same seed) are planning to only ever play Reinhardt, don't enable this.
Can be combined with Hard Logic to include Carrie-only tricks."""
display_name = "Carrie Logic"
class HardLogic(Toggle):
"""Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include
Carrie-only tricks.
See the Game Page for a full list of tricks and glitches that may be logically required."""
display_name = "Hard Logic"
class MultiHitBreakables(Toggle):
"""Adds the items that drop from the objects that break in three hits to the pool. There are 17 of these throughout
the game, adding up to 74 checks in total with all stages.
The game will be modified to
remember exactly which of their items you've picked up instead of simply whether they were broken or not."""
display_name = "Multi-hit Breakables"
class EmptyBreakables(Toggle):
"""Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.)
and some additional Red Jewels and/or moneybags into the item pool to compensate."""
display_name = "Empty Breakables"
class LizardLockerItems(Toggle):
"""Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool.
Picking up all of these can be a very tedious luck-based process, so they are off by default."""
display_name = "Lizard Locker Items"
class Shopsanity(Toggle):
"""Adds 7 one-time purchases from Renon's shop into the location pool. After buying an item from a slot, it will
revert to whatever it is in the vanilla game."""
display_name = "Shopsanity"
class ShopPrices(Choice):
"""Randomizes the amount of gold each item costs in Renon's shop.
Use the below options to control how much or little an item can cost."""
display_name = "Shop Prices"
option_vanilla = 0
option_randomized = 1
default = 0
class MinimumGoldPrice(Range):
"""The lowest amount of gold an item can cost in Renon's shop, divided by 100."""
display_name = "Minimum Gold Price"
range_start = 1
range_end = 50
default = 2
class MaximumGoldPrice(Range):
"""The highest amount of gold an item can cost in Renon's shop, divided by 100."""
display_name = "Maximum Gold Price"
range_start = 1
range_end = 50
default = 30
class PostBehemothBoss(Choice):
"""Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating
Behemoth."""
display_name = "Post-Behemoth Boss"
option_vanilla = 0
option_inverted = 1
option_always_rosa = 2
option_always_camilla = 3
default = 0
class RoomOfClocksBoss(Choice):
"""Sets which boss is fought at Room of Clocks by which characters."""
display_name = "Room of Clocks Boss"
option_vanilla = 0
option_inverted = 1
option_always_death = 2
option_always_actrise = 3
default = 0
class RenonFightCondition(Choice):
"""Sets the condition on which the Renon fight will trigger."""
display_name = "Renon Fight Condition"
option_never = 0
option_spend_30k = 1
option_always = 2
default = 1
class VincentFightCondition(Choice):
"""Sets the condition on which the vampire Vincent fight will trigger."""
display_name = "Vincent Fight Condition"
option_never = 0
option_wait_16_days = 1
option_always = 2
default = 1
class BadEndingCondition(Choice):
"""Sets the condition on which the currently-controlled character's Bad Ending will trigger."""
display_name = "Bad Ending Condition"
option_never = 0
option_kill_vincent = 1
option_always = 2
default = 1
class IncreaseItemLimit(DefaultOnToggle):
"""Increases the holding limit of usable items from 10 to 99 of each item."""
display_name = "Increase Item Limit"
class NerfHealingItems(Toggle):
"""Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%."""
display_name = "Nerf Healing Items"
class LoadingZoneHeals(DefaultOnToggle):
"""Whether end-of-level loading zones restore health and cure status aliments or not.
Recommended off for those looking for more of a survival horror experience!"""
display_name = "Loading Zone Heals"
class InvisibleItems(Choice):
"""Sets which items are visible in their locations and which are invisible until picked up.
'Chance' gives each item a 50/50 chance of being visible or invisible."""
display_name = "Invisible Items"
option_vanilla = 0
option_reveal_all = 1
option_hide_all = 2
option_chance = 3
default = 0
class DropPreviousSubWeapon(Toggle):
"""When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired."""
display_name = "Drop Previous Sub-weapon"
class PermanentPowerUps(Toggle):
"""Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after
dying and/or continuing.
To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile."""
display_name = "Permanent PowerUps"
class IceTrapPercentage(Range):
"""Replaces a percentage of junk items with Ice Traps.
These will be visibly disguised as other items, and receiving one will freeze you
as if you were hit by Camilla's ice cloud attack."""
display_name = "Ice Trap Percentage"
range_start = 0
range_end = 100
default = 0
class IceTrapAppearance(Choice):
"""What items Ice Traps can possibly be disguised as."""
display_name = "Ice Trap Appearance"
option_major_only = 0
option_junk_only = 1
option_anything = 2
default = 0
class DisableTimeRestrictions(Toggle):
"""Disables the restriction on every event and door that requires the current time
to be within a specific range, so they can be triggered at any time.
This includes all sun/moon doors and, in the Villa, the meeting with Rosa and the fountain pillar.
The Villa coffin is not affected by this."""
display_name = "Disable Time Requirements"
class SkipGondolas(Toggle):
"""Makes jumping on and activating a gondola in Tunnel instantly teleport you
to the other station, thereby skipping the entire three-minute ride.
The item normally at the gondola transfer point is moved to instead be
near the red gondola at its station."""
display_name = "Skip Gondolas"
class SkipWaterwayBlocks(Toggle):
"""Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating
brick platforms won't have to be done. Shopping at the Contract on the other side of them may still be logically
required if Shopsanity is on."""
display_name = "Skip Waterway Blocks"
class Countdown(Choice):
"""Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items
or the total check locations remaining in the stage you are currently in."""
display_name = "Countdown"
option_none = 0
option_majors = 1
option_all_locations = 2
default = 0
class BigToss(Toggle):
"""Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge.
Press A while tossed to cancel the launch momentum and avoid being thrown off ledges.
Hold Z to have all incoming damage be treated as it normally would.
Any tricks that might be possible with it are NOT considered in logic on any setting."""
display_name = "Big Toss"
class PantherDash(Choice):
"""Hold C-right at any time to sprint way faster. Any tricks that might be
possible with it are NOT considered in logic on any setting and any boss
fights with boss health meters, if started, are expected to be finished
before leaving their arenas if Dracula's Condition is bosses. Jumpless will
prevent jumping while moving at the increased speed to ensure logic cannot be broken with it."""
display_name = "Panther Dash"
option_off = 0
option_on = 1
option_jumpless = 2
default = 0
class IncreaseShimmySpeed(Toggle):
"""Increases the speed at which characters shimmy left and right while hanging on ledges."""
display_name = "Increase Shimmy Speed"
class FallGuard(Toggle):
"""Removes fall damage from landing too hard. Note that falling for too long will still result in instant death."""
display_name = "Fall Guard"
class BackgroundMusic(Choice):
"""Randomizes or disables the music heard throughout the game.
Randomized music is split into two pools: songs that loop and songs that don't.
The "lead-in" versions of some songs will be paired accordingly."""
display_name = "Background Music"
option_normal = 0
option_disabled = 1
option_randomized = 2
default = 0
class MapLighting(Choice):
"""Randomizes the lighting color RGB values on every map during every time of day to be literally anything.
The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects."""
display_name = "Map Lighting"
option_normal = 0
option_randomized = 1
default = 0
class CinematicExperience(Toggle):
"""Enables an unused film reel effect on every cutscene in the game. Purely cosmetic."""
display_name = "Cinematic Experience"
class WindowColorR(Range):
"""The red value for the background color of the text windows during gameplay."""
display_name = "Window Color R"
range_start = 0
range_end = 15
default = 1
class WindowColorG(Range):
"""The green value for the background color of the text windows during gameplay."""
display_name = "Window Color G"
range_start = 0
range_end = 15
default = 5
class WindowColorB(Range):
"""The blue value for the background color of the text windows during gameplay."""
display_name = "Window Color B"
range_start = 0
range_end = 15
default = 15
class WindowColorA(Range):
"""The alpha value for the background color of the text windows during gameplay."""
display_name = "Window Color A"
range_start = 0
range_end = 15
default = 8
class DeathLink(Choice):
"""When you die, everyone dies. Of course the reverse is true too.
Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion
instead of the normal death animation."""
display_name = "DeathLink"
option_off = 0
alias_no = 0
alias_true = 1
alias_yes = 1
option_on = 1
option_explosive = 2
@dataclass
class CV64Options(PerGameCommonOptions):
character_stages: CharacterStages
stage_shuffle: StageShuffle
starting_stage: StartingStage
warp_order: WarpOrder
sub_weapon_shuffle: SubWeaponShuffle
spare_keys: SpareKeys
hard_item_pool: HardItemPool
special1s_per_warp: Special1sPerWarp
total_special1s: TotalSpecial1s
draculas_condition: DraculasCondition
percent_special2s_required: PercentSpecial2sRequired
total_special2s: TotalSpecial2s
bosses_required: BossesRequired
carrie_logic: CarrieLogic
hard_logic: HardLogic
multi_hit_breakables: MultiHitBreakables
empty_breakables: EmptyBreakables
lizard_locker_items: LizardLockerItems
shopsanity: Shopsanity
shop_prices: ShopPrices
minimum_gold_price: MinimumGoldPrice
maximum_gold_price: MaximumGoldPrice
post_behemoth_boss: PostBehemothBoss
room_of_clocks_boss: RoomOfClocksBoss
renon_fight_condition: RenonFightCondition
vincent_fight_condition: VincentFightCondition
bad_ending_condition: BadEndingCondition
increase_item_limit: IncreaseItemLimit
nerf_healing_items: NerfHealingItems
loading_zone_heals: LoadingZoneHeals
invisible_items: InvisibleItems
drop_previous_sub_weapon: DropPreviousSubWeapon
permanent_powerups: PermanentPowerUps
ice_trap_percentage: IceTrapPercentage
ice_trap_appearance: IceTrapAppearance
disable_time_restrictions: DisableTimeRestrictions
skip_gondolas: SkipGondolas
skip_waterway_blocks: SkipWaterwayBlocks
countdown: Countdown
big_toss: BigToss
panther_dash: PantherDash
increase_shimmy_speed: IncreaseShimmySpeed
background_music: BackgroundMusic
map_lighting: MapLighting
fall_guard: FallGuard
cinematic_experience: CinematicExperience
window_color_r: WindowColorR
window_color_g: WindowColorG
window_color_b: WindowColorB
window_color_a: WindowColorA
death_link: DeathLink
start_inventory_from_pool: StartInventoryPool

517
worlds/cv64/regions.py Normal file
View File

@ -0,0 +1,517 @@
from .data import lname, rname, ename
from typing import List, Union
# # # KEY # # #
# "stage" = What stage the Region is a part of. The Region and its corresponding Locations and Entrances will only be
# put in if its stage is active.
# "locations" = The Locations to add to that Region when putting in said Region (provided their add conditions pass).
# "entrances" = The Entrances to add to that Region when putting in said Region (provided their add conditions pass).
region_info = {
"Menu": {},
rname.forest_start: {"stage": rname.forest_of_silence,
"locations": [lname.forest_pillars_right,
lname.forest_pillars_left,
lname.forest_pillars_top,
lname.forest_king_skeleton,
lname.forest_boss_one,
lname.forest_lgaz_in,
lname.forest_lgaz_top,
lname.forest_hgaz_in,
lname.forest_hgaz_top,
lname.forest_weretiger_sw,
lname.forest_boss_two,
lname.forest_weretiger_gate,
lname.forest_dirge_tomb_l,
lname.forest_dirge_tomb_u,
lname.forest_dirge_plaque,
lname.forest_dirge_ped,
lname.forest_dirge_rock1,
lname.forest_dirge_rock2,
lname.forest_dirge_rock3,
lname.forest_dirge_rock4,
lname.forest_dirge_rock5,
lname.forest_corpse_save,
lname.forest_dbridge_wall,
lname.forest_dbridge_sw],
"entrances": [ename.forest_dbridge_gate]},
rname.forest_mid: {"stage": rname.forest_of_silence,
"locations": [lname.forest_dbridge_gate_l,
lname.forest_dbridge_gate_r,
lname.forest_dbridge_tomb_l,
lname.forest_dbridge_tomb_ur,
lname.forest_dbridge_tomb_uf,
lname.forest_bface_tomb_lf,
lname.forest_bface_tomb_lr,
lname.forest_bface_tomb_u,
lname.forest_ibridge,
lname.forest_bridge_rock1,
lname.forest_bridge_rock2,
lname.forest_bridge_rock3,
lname.forest_bridge_rock4,
lname.forest_werewolf_tomb_lf,
lname.forest_werewolf_tomb_lr,
lname.forest_werewolf_tomb_r,
lname.forest_werewolf_plaque,
lname.forest_werewolf_tree,
lname.forest_werewolf_island,
lname.forest_final_sw],
"entrances": [ename.forest_werewolf_gate]},
rname.forest_end: {"stage": rname.forest_of_silence,
"locations": [lname.forest_boss_three],
"entrances": [ename.forest_end]},
rname.cw_start: {"stage": rname.castle_wall,
"locations": [lname.cwr_bottom,
lname.cw_dragon_sw,
lname.cw_boss,
lname.cw_save_slab1,
lname.cw_save_slab2,
lname.cw_save_slab3,
lname.cw_save_slab4,
lname.cw_save_slab5,
lname.cw_rrampart,
lname.cw_lrampart,
lname.cw_pillar,
lname.cw_shelf_visible,
lname.cw_shelf_sandbags,
lname.cw_shelf_torch],
"entrances": [ename.cw_portcullis_c,
ename.cw_lt_skip,
ename.cw_lt_door]},
rname.cw_exit: {"stage": rname.castle_wall,
"locations": [lname.cw_ground_left,
lname.cw_ground_middle,
lname.cw_ground_right]},
rname.cw_ltower: {"stage": rname.castle_wall,
"locations": [lname.cwl_bottom,
lname.cwl_bridge,
lname.cw_drac_sw,
lname.cw_drac_slab1,
lname.cw_drac_slab2,
lname.cw_drac_slab3,
lname.cw_drac_slab4,
lname.cw_drac_slab5],
"entrances": [ename.cw_end]},
rname.villa_start: {"stage": rname.villa,
"locations": [lname.villafy_outer_gate_l,
lname.villafy_outer_gate_r,
lname.villafy_dog_platform,
lname.villafy_inner_gate],
"entrances": [ename.villa_dog_gates]},
rname.villa_main: {"stage": rname.villa,
"locations": [lname.villafy_gate_marker,
lname.villafy_villa_marker,
lname.villafy_tombstone,
lname.villafy_fountain_fl,
lname.villafy_fountain_fr,
lname.villafy_fountain_ml,
lname.villafy_fountain_mr,
lname.villafy_fountain_rl,
lname.villafy_fountain_rr,
lname.villafo_front_r,
lname.villafo_front_l,
lname.villafo_mid_l,
lname.villafo_mid_r,
lname.villafo_rear_r,
lname.villafo_rear_l,
lname.villafo_pot_r,
lname.villafo_pot_l,
lname.villafo_sofa,
lname.villafo_chandelier1,
lname.villafo_chandelier2,
lname.villafo_chandelier3,
lname.villafo_chandelier4,
lname.villafo_chandelier5,
lname.villala_hallway_stairs,
lname.villala_hallway_l,
lname.villala_hallway_r,
lname.villala_bedroom_chairs,
lname.villala_bedroom_bed,
lname.villala_vincent,
lname.villala_slivingroom_table,
lname.villala_slivingroom_mirror,
lname.villala_diningroom_roses,
lname.villala_llivingroom_pot_r,
lname.villala_llivingroom_pot_l,
lname.villala_llivingroom_painting,
lname.villala_llivingroom_light,
lname.villala_llivingroom_lion,
lname.villala_exit_knight],
"entrances": [ename.villa_snipe_dogs,
ename.villa_renon,
ename.villa_to_storeroom,
ename.villa_to_archives,
ename.villa_to_maze]},
rname.villa_storeroom: {"stage": rname.villa,
"locations": [lname.villala_storeroom_l,
lname.villala_storeroom_r,
lname.villala_storeroom_s],
"entrances": [ename.villa_from_storeroom]},
rname.villa_archives: {"stage": rname.villa,
"locations": [lname.villala_archives_entrance,
lname.villala_archives_table,
lname.villala_archives_rear]},
rname.villa_maze: {"stage": rname.villa,
"locations": [lname.villam_malus_torch,
lname.villam_malus_bush,
lname.villam_fplatform,
lname.villam_frankieturf_l,
lname.villam_frankieturf_r,
lname.villam_frankieturf_ru,
lname.villam_fgarden_f,
lname.villam_fgarden_mf,
lname.villam_fgarden_mr,
lname.villam_fgarden_r,
lname.villam_rplatform,
lname.villam_rplatform_de,
lname.villam_exit_de,
lname.villam_serv_path],
"entrances": [ename.villa_from_maze,
ename.villa_copper_door,
ename.villa_copper_skip]},
rname.villa_servants: {"stage": rname.villa,
"locations": [lname.villafo_serv_ent],
"entrances": [ename.villa_servant_door]},
rname.villa_crypt: {"stage": rname.villa,
"locations": [lname.villam_crypt_ent,
lname.villam_crypt_upstream,
lname.villac_ent_l,
lname.villac_ent_r,
lname.villac_wall_l,
lname.villac_wall_r,
lname.villac_coffin_l,
lname.villac_coffin_r,
lname.villa_boss_one,
lname.villa_boss_two],
"entrances": [ename.villa_bridge_door,
ename.villa_end_r,
ename.villa_end_c]},
rname.tunnel_start: {"stage": rname.tunnel,
"locations": [lname.tunnel_landing,
lname.tunnel_landing_rc,
lname.tunnel_stone_alcove_r,
lname.tunnel_stone_alcove_l,
lname.tunnel_twin_arrows,
lname.tunnel_arrows_rock1,
lname.tunnel_arrows_rock2,
lname.tunnel_arrows_rock3,
lname.tunnel_arrows_rock4,
lname.tunnel_arrows_rock5,
lname.tunnel_lonesome_bucket,
lname.tunnel_lbucket_mdoor_l,
lname.tunnel_lbucket_quag,
lname.tunnel_bucket_quag_rock1,
lname.tunnel_bucket_quag_rock2,
lname.tunnel_bucket_quag_rock3,
lname.tunnel_lbucket_albert,
lname.tunnel_albert_camp,
lname.tunnel_albert_quag,
lname.tunnel_gondola_rc_sdoor_l,
lname.tunnel_gondola_rc_sdoor_m,
lname.tunnel_gondola_rc_sdoor_r,
lname.tunnel_gondola_rc,
lname.tunnel_rgondola_station,
lname.tunnel_gondola_transfer],
"entrances": [ename.tunnel_start_renon,
ename.tunnel_gondolas]},
rname.tunnel_end: {"stage": rname.tunnel,
"locations": [lname.tunnel_corpse_bucket_quag,
lname.tunnel_corpse_bucket_mdoor_l,
lname.tunnel_corpse_bucket_mdoor_r,
lname.tunnel_shovel_quag_start,
lname.tunnel_exit_quag_start,
lname.tunnel_shovel_quag_end,
lname.tunnel_exit_quag_end,
lname.tunnel_shovel,
lname.tunnel_shovel_save,
lname.tunnel_shovel_mdoor_l,
lname.tunnel_shovel_mdoor_r,
lname.tunnel_shovel_sdoor_l,
lname.tunnel_shovel_sdoor_m,
lname.tunnel_shovel_sdoor_r],
"entrances": [ename.tunnel_end_renon,
ename.tunnel_end]},
rname.uw_main: {"stage": rname.underground_waterway,
"locations": [lname.uw_near_ent,
lname.uw_across_ent,
lname.uw_first_ledge1,
lname.uw_first_ledge2,
lname.uw_first_ledge3,
lname.uw_first_ledge4,
lname.uw_first_ledge5,
lname.uw_first_ledge6,
lname.uw_poison_parkour,
lname.uw_boss,
lname.uw_waterfall_alcove,
lname.uw_carrie1,
lname.uw_carrie2,
lname.uw_bricks_save,
lname.uw_above_skel_ledge,
lname.uw_in_skel_ledge1,
lname.uw_in_skel_ledge2,
lname.uw_in_skel_ledge3],
"entrances": [ename.uw_final_waterfall,
ename.uw_renon]},
rname.uw_end: {"stage": rname.underground_waterway,
"entrances": [ename.uw_waterfall_skip,
ename.uw_end]},
rname.cc_main: {"stage": rname.castle_center,
"locations": [lname.ccb_skel_hallway_ent,
lname.ccb_skel_hallway_jun,
lname.ccb_skel_hallway_tc,
lname.ccb_skel_hallway_ba,
lname.ccb_behemoth_l_ff,
lname.ccb_behemoth_l_mf,
lname.ccb_behemoth_l_mr,
lname.ccb_behemoth_l_fr,
lname.ccb_behemoth_r_ff,
lname.ccb_behemoth_r_mf,
lname.ccb_behemoth_r_mr,
lname.ccb_behemoth_r_fr,
lname.ccb_behemoth_crate1,
lname.ccb_behemoth_crate2,
lname.ccb_behemoth_crate3,
lname.ccb_behemoth_crate4,
lname.ccb_behemoth_crate5,
lname.ccelv_near_machine,
lname.ccelv_atop_machine,
lname.ccelv_stand1,
lname.ccelv_stand2,
lname.ccelv_stand3,
lname.ccelv_pipes,
lname.ccelv_switch,
lname.ccelv_staircase,
lname.ccff_redcarpet_knight,
lname.ccff_gears_side,
lname.ccff_gears_mid,
lname.ccff_gears_corner,
lname.ccff_lizard_knight,
lname.ccff_lizard_near_knight,
lname.ccff_lizard_pit,
lname.ccff_lizard_corner,
lname.ccff_lizard_locker_nfr,
lname.ccff_lizard_locker_nmr,
lname.ccff_lizard_locker_nml,
lname.ccff_lizard_locker_nfl,
lname.ccff_lizard_locker_fl,
lname.ccff_lizard_locker_fr,
lname.ccff_lizard_slab1,
lname.ccff_lizard_slab2,
lname.ccff_lizard_slab3,
lname.ccff_lizard_slab4,
lname.ccll_brokenstairs_floor,
lname.ccll_brokenstairs_knight,
lname.ccll_brokenstairs_save,
lname.ccll_glassknight_l,
lname.ccll_glassknight_r,
lname.ccll_butlers_door,
lname.ccll_butlers_side,
lname.ccll_cwhall_butlerflames_past,
lname.ccll_cwhall_flamethrower,
lname.ccll_cwhall_cwflames,
lname.ccll_heinrich,
lname.ccia_nitro_crates,
lname.ccia_nitro_shelf_h,
lname.ccia_stairs_knight,
lname.ccia_maids_vase,
lname.ccia_maids_outer,
lname.ccia_maids_inner,
lname.ccia_inventions_maids,
lname.ccia_inventions_crusher,
lname.ccia_inventions_famicart,
lname.ccia_inventions_zeppelin,
lname.ccia_inventions_round,
lname.ccia_nitrohall_flamethrower,
lname.ccia_nitrohall_torch,
lname.ccia_nitro_shelf_i],
"entrances": [ename.cc_tc_door,
ename.cc_lower_wall,
ename.cc_renon,
ename.cc_upper_wall]},
rname.cc_torture_chamber: {"stage": rname.castle_center,
"locations": [lname.ccb_mandrag_shelf_l,
lname.ccb_mandrag_shelf_r,
lname.ccb_torture_rack,
lname.ccb_torture_rafters]},
rname.cc_library: {"stage": rname.castle_center,
"locations": [lname.ccll_cwhall_wall,
lname.ccl_bookcase]},
rname.cc_crystal: {"stage": rname.castle_center,
"locations": [lname.cc_behind_the_seal,
lname.cc_boss_one,
lname.cc_boss_two],
"entrances": [ename.cc_elevator]},
rname.cc_elev_top: {"stage": rname.castle_center,
"entrances": [ename.cc_exit_r,
ename.cc_exit_c]},
rname.dt_main: {"stage": rname.duel_tower,
"locations": [lname.dt_boss_one,
lname.dt_boss_two,
lname.dt_ibridge_l,
lname.dt_ibridge_r,
lname.dt_stones_start,
lname.dt_stones_end,
lname.dt_werebull_arena,
lname.dt_boss_three,
lname.dt_boss_four],
"entrances": [ename.dt_start,
ename.dt_end]},
rname.toe_main: {"stage": rname.tower_of_execution,
"locations": [lname.toe_ledge1,
lname.toe_ledge2,
lname.toe_ledge3,
lname.toe_ledge4,
lname.toe_ledge5,
lname.toe_midsavespikes_r,
lname.toe_midsavespikes_l,
lname.toe_elec_grate,
lname.toe_ibridge,
lname.toe_top],
"entrances": [ename.toe_start,
ename.toe_gate,
ename.toe_gate_skip,
ename.toe_end]},
rname.toe_ledge: {"stage": rname.tower_of_execution,
"locations": [lname.toe_keygate_l,
lname.toe_keygate_r]},
rname.tosci_start: {"stage": rname.tower_of_science,
"locations": [lname.tosci_elevator,
lname.tosci_plain_sr,
lname.tosci_stairs_sr],
"entrances": [ename.tosci_start,
ename.tosci_key1_door,
ename.tosci_to_key2_door]},
rname.tosci_three_doors: {"stage": rname.tower_of_science,
"locations": [lname.tosci_three_door_hall]},
rname.tosci_conveyors: {"stage": rname.tower_of_science,
"locations": [lname.tosci_ibridge_t,
lname.tosci_ibridge_b1,
lname.tosci_ibridge_b2,
lname.tosci_ibridge_b3,
lname.tosci_ibridge_b4,
lname.tosci_ibridge_b5,
lname.tosci_ibridge_b6,
lname.tosci_conveyor_sr,
lname.tosci_exit],
"entrances": [ename.tosci_from_key2_door,
ename.tosci_key3_door,
ename.tosci_end]},
rname.tosci_key3: {"stage": rname.tower_of_science,
"locations": [lname.tosci_key3_r,
lname.tosci_key3_m,
lname.tosci_key3_l]},
rname.tosor_main: {"stage": rname.tower_of_sorcery,
"locations": [lname.tosor_stained_tower,
lname.tosor_savepoint,
lname.tosor_trickshot,
lname.tosor_yellow_bubble,
lname.tosor_blue_platforms,
lname.tosor_side_isle,
lname.tosor_ibridge],
"entrances": [ename.tosor_start,
ename.tosor_end]},
rname.roc_main: {"stage": rname.room_of_clocks,
"locations": [lname.roc_ent_l,
lname.roc_ent_r,
lname.roc_elev_r,
lname.roc_elev_l,
lname.roc_cont_r,
lname.roc_cont_l,
lname.roc_exit,
lname.roc_boss],
"entrances": [ename.roc_gate]},
rname.ct_start: {"stage": rname.clock_tower,
"locations": [lname.ct_gearclimb_battery_slab1,
lname.ct_gearclimb_battery_slab2,
lname.ct_gearclimb_battery_slab3,
lname.ct_gearclimb_side,
lname.ct_gearclimb_corner,
lname.ct_gearclimb_door_slab1,
lname.ct_gearclimb_door_slab2,
lname.ct_gearclimb_door_slab3],
"entrances": [ename.ct_to_door1]},
rname.ct_middle: {"stage": rname.clock_tower,
"locations": [lname.ct_bp_chasm_fl,
lname.ct_bp_chasm_fr,
lname.ct_bp_chasm_rl,
lname.ct_bp_chasm_k],
"entrances": [ename.ct_from_door1,
ename.ct_to_door2]},
rname.ct_end: {"stage": rname.clock_tower,
"locations": [lname.ct_finalroom_door_slab1,
lname.ct_finalroom_door_slab2,
lname.ct_finalroom_fl,
lname.ct_finalroom_fr,
lname.ct_finalroom_rl,
lname.ct_finalroom_rr,
lname.ct_finalroom_platform,
lname.ct_finalroom_renon_slab1,
lname.ct_finalroom_renon_slab2,
lname.ct_finalroom_renon_slab3,
lname.ct_finalroom_renon_slab4,
lname.ct_finalroom_renon_slab5,
lname.ct_finalroom_renon_slab6,
lname.ct_finalroom_renon_slab7,
lname.ct_finalroom_renon_slab8],
"entrances": [ename.ct_from_door2,
ename.ct_renon,
ename.ct_door_3]},
rname.ck_main: {"stage": rname.castle_keep,
"locations": [lname.ck_boss_one,
lname.ck_boss_two,
lname.ck_flame_l,
lname.ck_flame_r,
lname.ck_behind_drac,
lname.ck_cube],
"entrances": [ename.ck_slope_jump,
ename.ck_drac_door]},
rname.renon: {"locations": [lname.renon1,
lname.renon2,
lname.renon3,
lname.renon4,
lname.renon5,
lname.renon6,
lname.renon7]},
rname.ck_drac_chamber: {"locations": [lname.the_end]}
}
def get_region_info(region: str, info: str) -> Union[str, List[str], None]:
return region_info[region].get(info, None)

959
worlds/cv64/rom.py Normal file
View File

@ -0,0 +1,959 @@
import Utils
from BaseClasses import Location
from worlds.Files import APDeltaPatch
from typing import List, Dict, Union, Iterable, Collection, TYPE_CHECKING
import hashlib
import os
import pkgutil
from . import lzkn64
from .data import patches
from .stages import get_stage_info
from .text import cv64_string_to_bytearray, cv64_text_truncate, cv64_text_wrap
from .aesthetics import renon_item_dialogue, get_item_text_color
from .locations import get_location_info
from .options import CharacterStages, VincentFightCondition, RenonFightCondition, PostBehemothBoss, RoomOfClocksBoss, \
BadEndingCondition, DeathLink, DraculasCondition, InvisibleItems, Countdown, PantherDash
from settings import get_settings
if TYPE_CHECKING:
from . import CV64World
CV64US10HASH = "1cc5cf3b4d29d8c3ade957648b529dc1"
ROM_PLAYER_LIMIT = 65535
warp_map_offsets = [0xADF67, 0xADF77, 0xADF87, 0xADF97, 0xADFA7, 0xADFBB, 0xADFCB, 0xADFDF]
class LocalRom:
orig_buffer: None
buffer: bytearray
def __init__(self, file: str) -> None:
self.orig_buffer = None
with open(file, "rb") as stream:
self.buffer = bytearray(stream.read())
def read_bit(self, address: int, bit_number: int) -> bool:
bitflag = (1 << bit_number)
return (self.buffer[address] & bitflag) != 0
def read_byte(self, address: int) -> int:
return self.buffer[address]
def read_bytes(self, start_address: int, length: int) -> bytearray:
return self.buffer[start_address:start_address + length]
def write_byte(self, address: int, value: int) -> None:
self.buffer[address] = value
def write_bytes(self, start_address: int, values: Collection[int]) -> None:
self.buffer[start_address:start_address + len(values)] = values
def write_int16(self, address: int, value: int) -> None:
value = value & 0xFFFF
self.write_bytes(address, [(value >> 8) & 0xFF, value & 0xFF])
def write_int16s(self, start_address: int, values: List[int]) -> None:
for i, value in enumerate(values):
self.write_int16(start_address + (i * 2), value)
def write_int24(self, address: int, value: int) -> None:
value = value & 0xFFFFFF
self.write_bytes(address, [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF])
def write_int24s(self, start_address: int, values: List[int]) -> None:
for i, value in enumerate(values):
self.write_int24(start_address + (i * 3), value)
def write_int32(self, address, value: int) -> None:
value = value & 0xFFFFFFFF
self.write_bytes(address, [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF])
def write_int32s(self, start_address: int, values: list) -> None:
for i, value in enumerate(values):
self.write_int32(start_address + (i * 4), value)
def write_to_file(self, filepath: str) -> None:
with open(filepath, "wb") as outfile:
outfile.write(self.buffer)
def patch_rom(world: "CV64World", rom: LocalRom, offset_data: Dict[int, int], shop_name_list: List[str],
shop_desc_list: List[List[Union[int, str, None]]], shop_colors_list: List[bytearray],
active_locations: Iterable[Location]) -> None:
multiworld = world.multiworld
options = world.options
player = world.player
active_stage_exits = world.active_stage_exits
s1s_per_warp = world.s1s_per_warp
active_warp_list = world.active_warp_list
required_s2s = world.required_s2s
total_s2s = world.total_s2s
# NOP out the CRC BNEs
rom.write_int32(0x66C, 0x00000000)
rom.write_int32(0x678, 0x00000000)
# Always offer Hard Mode on file creation
rom.write_int32(0xC8810, 0x240A0100) # ADDIU T2, R0, 0x0100
# Disable Easy Mode cutoff point at Castle Center elevator
rom.write_int32(0xD9E18, 0x240D0000) # ADDIU T5, R0, 0x0000
# Disable the Forest, Castle Wall, and Villa intro cutscenes and make it possible to change the starting level
rom.write_byte(0xB73308, 0x00)
rom.write_byte(0xB7331A, 0x40)
rom.write_byte(0xB7332B, 0x4C)
rom.write_byte(0xB6302B, 0x00)
rom.write_byte(0x109F8F, 0x00)
# Prevent Forest end cutscene flag from setting so it can be triggered infinitely
rom.write_byte(0xEEA51, 0x01)
# Hack to make the Forest, CW and Villa intro cutscenes play at the start of their levels no matter what map came
# before them
rom.write_int32(0x97244, 0x803FDD60)
rom.write_int32s(0xBFDD60, patches.forest_cw_villa_intro_cs_player)
# Make changing the map ID to 0xFF reset the map. Helpful to work around a bug wherein the camera gets stuck when
# entering a loading zone that doesn't change the map.
rom.write_int32s(0x197B0, [0x0C0FF7E6, # JAL 0x803FDF98
0x24840008]) # ADDIU A0, A0, 0x0008
rom.write_int32s(0xBFDF98, patches.map_id_refresher)
# Enable swapping characters when loading into a map by holding L.
rom.write_int32(0x97294, 0x803FDFC4)
rom.write_int32(0x19710, 0x080FF80E) # J 0x803FE038
rom.write_int32s(0xBFDFC4, patches.character_changer)
# Villa coffin time-of-day hack
rom.write_byte(0xD9D83, 0x74)
rom.write_int32(0xD9D84, 0x080FF14D) # J 0x803FC534
rom.write_int32s(0xBFC534, patches.coffin_time_checker)
# Fix both Castle Center elevator bridges for both characters unless enabling only one character's stages. At which
# point one bridge will be always broken and one always repaired instead.
if options.character_stages == CharacterStages.option_reinhardt_only:
rom.write_int32(0x6CEAA0, 0x240B0000) # ADDIU T3, R0, 0x0000
elif options.character_stages == CharacterStages.option_carrie_only:
rom.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001
else:
rom.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001
rom.write_int32(0x6CEAA4, 0x240D0001) # ADDIU T5, R0, 0x0001
# Were-bull arena flag hack
rom.write_int32(0x6E38F0, 0x0C0FF157) # JAL 0x803FC55C
rom.write_int32s(0xBFC55C, patches.werebull_flag_unsetter)
rom.write_int32(0xA949C, 0x0C0FF380) # JAL 0x803FCE00
rom.write_int32s(0xBFCE00, patches.werebull_flag_pickup_setter)
# Enable being able to carry multiple Special jewels, Nitros, and Mandragoras simultaneously
rom.write_int32(0xBF1F4, 0x3C038039) # LUI V1, 0x8039
# Special1
rom.write_int32(0xBF210, 0x80659C4B) # LB A1, 0x9C4B (V1)
rom.write_int32(0xBF214, 0x24A50001) # ADDIU A1, A1, 0x0001
rom.write_int32(0xBF21C, 0xA0659C4B) # SB A1, 0x9C4B (V1)
# Special2
rom.write_int32(0xBF230, 0x80659C4C) # LB A1, 0x9C4C (V1)
rom.write_int32(0xBF234, 0x24A50001) # ADDIU A1, A1, 0x0001
rom.write_int32(0xbf23C, 0xA0659C4C) # SB A1, 0x9C4C (V1)
# Magical Nitro
rom.write_int32(0xBF360, 0x10000004) # B 0x8013C184
rom.write_int32(0xBF378, 0x25E50001) # ADDIU A1, T7, 0x0001
rom.write_int32(0xBF37C, 0x10000003) # B 0x8013C19C
# Mandragora
rom.write_int32(0xBF3A8, 0x10000004) # B 0x8013C1CC
rom.write_int32(0xBF3C0, 0x25050001) # ADDIU A1, T0, 0x0001
rom.write_int32(0xBF3C4, 0x10000003) # B 0x8013C1E4
# Give PowerUps their Legacy of Darkness behavior when attempting to pick up more than two
rom.write_int16(0xA9624, 0x1000)
rom.write_int32(0xA9730, 0x24090000) # ADDIU T1, R0, 0x0000
rom.write_int32(0xBF2FC, 0x080FF16D) # J 0x803FC5B4
rom.write_int32(0xBF300, 0x00000000) # NOP
rom.write_int32s(0xBFC5B4, patches.give_powerup_stopper)
# Rename the Wooden Stake and Rose to "You are a FOOL!"
rom.write_bytes(0xEFE34,
bytearray([0xFF, 0xFF, 0xA2, 0x0B]) + cv64_string_to_bytearray("You are a FOOL!", append_end=False))
# Capitalize the "k" in "Archives key" to be consistent with...literally every other key name!
rom.write_byte(0xEFF21, 0x2D)
# Skip the "There is a white jewel" text so checking one saves the game instantly.
rom.write_int32s(0xEFC72, [0x00020002 for _ in range(37)])
rom.write_int32(0xA8FC0, 0x24020001) # ADDIU V0, R0, 0x0001
# Skip the yes/no prompts when activating things.
rom.write_int32s(0xBFDACC, patches.map_text_redirector)
rom.write_int32(0xA9084, 0x24020001) # ADDIU V0, R0, 0x0001
rom.write_int32(0xBEBE8, 0x0C0FF6B4) # JAL 0x803FDAD0
# Skip Vincent and Heinrich's mandatory-for-a-check dialogue
rom.write_int32(0xBED9C, 0x0C0FF6DA) # JAL 0x803FDB68
# Skip the long yes/no prompt in the CC planetarium to set the pieces.
rom.write_int32(0xB5C5DF, 0x24030001) # ADDIU V1, R0, 0x0001
# Skip the yes/no prompt to activate the CC elevator.
rom.write_int32(0xB5E3FB, 0x24020001) # ADDIU V0, R0, 0x0001
# Skip the yes/no prompts to set Nitro/Mandragora at both walls.
rom.write_int32(0xB5DF3E, 0x24030001) # ADDIU V1, R0, 0x0001
# Custom message if you try checking the downstairs CC crack before removing the seal.
rom.write_bytes(0xBFDBAC, cv64_string_to_bytearray("The Furious Nerd Curse\n"
"prevents you from setting\n"
"anything until the seal\n"
"is removed!", True))
rom.write_int32s(0xBFDD20, patches.special_descriptions_redirector)
# Change the Stage Select menu options
rom.write_int32s(0xADF64, patches.warp_menu_rewrite)
rom.write_int32s(0x10E0C8, patches.warp_pointer_table)
for i in range(len(active_warp_list)):
if i == 0:
rom.write_byte(warp_map_offsets[i], get_stage_info(active_warp_list[i], "start map id"))
rom.write_byte(warp_map_offsets[i] + 4, get_stage_info(active_warp_list[i], "start spawn id"))
else:
rom.write_byte(warp_map_offsets[i], get_stage_info(active_warp_list[i], "mid map id"))
rom.write_byte(warp_map_offsets[i] + 4, get_stage_info(active_warp_list[i], "mid spawn id"))
# Play the "teleportation" sound effect when teleporting
rom.write_int32s(0xAE088, [0x08004FAB, # J 0x80013EAC
0x2404019E]) # ADDIU A0, R0, 0x019E
# Change the Stage Select menu's text to reflect its new purpose
rom.write_bytes(0xEFAD0, cv64_string_to_bytearray(f"Where to...?\t{active_warp_list[0]}\t"
f"`{str(s1s_per_warp).zfill(2)} {active_warp_list[1]}\t"
f"`{str(s1s_per_warp * 2).zfill(2)} {active_warp_list[2]}\t"
f"`{str(s1s_per_warp * 3).zfill(2)} {active_warp_list[3]}\t"
f"`{str(s1s_per_warp * 4).zfill(2)} {active_warp_list[4]}\t"
f"`{str(s1s_per_warp * 5).zfill(2)} {active_warp_list[5]}\t"
f"`{str(s1s_per_warp * 6).zfill(2)} {active_warp_list[6]}\t"
f"`{str(s1s_per_warp * 7).zfill(2)} {active_warp_list[7]}"))
# Lizard-man save proofing
rom.write_int32(0xA99AC, 0x080FF0B8) # J 0x803FC2E0
rom.write_int32s(0xBFC2E0, patches.boss_save_stopper)
# Disable or guarantee vampire Vincent's fight
if options.vincent_fight_condition == VincentFightCondition.option_never:
rom.write_int32(0xAACC0, 0x24010001) # ADDIU AT, R0, 0x0001
rom.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000
elif options.vincent_fight_condition == VincentFightCondition.option_always:
rom.write_int32(0xAACE0, 0x24180010) # ADDIU T8, R0, 0x0010
else:
rom.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000
# Disable or guarantee Renon's fight
rom.write_int32(0xAACB4, 0x080FF1A4) # J 0x803FC690
if options.renon_fight_condition == RenonFightCondition.option_never:
rom.write_byte(0xB804F0, 0x00)
rom.write_byte(0xB80632, 0x00)
rom.write_byte(0xB807E3, 0x00)
rom.write_byte(0xB80988, 0xB8)
rom.write_byte(0xB816BD, 0xB8)
rom.write_byte(0xB817CF, 0x00)
rom.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr)
elif options.renon_fight_condition == RenonFightCondition.option_always:
rom.write_byte(0xB804F0, 0x0C)
rom.write_byte(0xB80632, 0x0C)
rom.write_byte(0xB807E3, 0x0C)
rom.write_byte(0xB80988, 0xC4)
rom.write_byte(0xB816BD, 0xC4)
rom.write_byte(0xB817CF, 0x0C)
rom.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr)
else:
rom.write_int32s(0xBFC690, patches.renon_cutscene_checker)
# NOP the Easy Mode check when buying a thing from Renon, so he can be triggered even on this mode.
rom.write_int32(0xBD8B4, 0x00000000)
# Disable or guarantee the Bad Ending
if options.bad_ending_condition == BadEndingCondition.option_never:
rom.write_int32(0xAEE5C6, 0x3C0A0000) # LUI T2, 0x0000
elif options.bad_ending_condition == BadEndingCondition.option_always:
rom.write_int32(0xAEE5C6, 0x3C0A0040) # LUI T2, 0x0040
# Play Castle Keep's song if teleporting in front of Dracula's door outside the escape sequence
rom.write_int32(0x6E937C, 0x080FF12E) # J 0x803FC4B8
rom.write_int32s(0xBFC4B8, patches.ck_door_music_player)
# Increase item capacity to 100 if "Increase Item Limit" is turned on
if options.increase_item_limit:
rom.write_byte(0xBF30B, 0x63) # Most items
rom.write_byte(0xBF3F7, 0x63) # Sun/Moon cards
rom.write_byte(0xBF353, 0x64) # Keys (increase regardless)
# Change the item healing values if "Nerf Healing" is turned on
if options.nerf_healing_items:
rom.write_byte(0xB56371, 0x50) # Healing kit (100 -> 80)
rom.write_byte(0xB56374, 0x32) # Roast beef ( 80 -> 50)
rom.write_byte(0xB56377, 0x19) # Roast chicken ( 50 -> 25)
# Disable loading zone healing if turned off
if not options.loading_zone_heals:
rom.write_byte(0xD99A5, 0x00) # Skip all loading zone checks
rom.write_byte(0xA9DFFB, 0x40) # Disable free heal from King Skeleton by reading the unused magic meter value
# Disable spinning on the Special1 and 2 pickup models so colorblind people can more easily identify them
rom.write_byte(0xEE4F5, 0x00) # Special1
rom.write_byte(0xEE505, 0x00) # Special2
# Make the Special2 the same size as a Red jewel(L) to further distinguish them
rom.write_int32(0xEE4FC, 0x3FA66666)
# Prevent the vanilla Magical Nitro transport's "can explode" flag from setting
rom.write_int32(0xB5D7AA, 0x00000000) # NOP
# Ensure the vampire Nitro check will always pass, so they'll never not spawn and crash the Villa cutscenes
rom.write_byte(0xA6253D, 0x03)
# Enable the Game Over's "Continue" menu starting the cursor on whichever checkpoint is most recent
rom.write_int32(0xB4DDC, 0x0C060D58) # JAL 0x80183560
rom.write_int32s(0x106750, patches.continue_cursor_start_checker)
rom.write_int32(0x1C444, 0x080FF08A) # J 0x803FC228
rom.write_int32(0x1C2A0, 0x080FF08A) # J 0x803FC228
rom.write_int32s(0xBFC228, patches.savepoint_cursor_updater)
rom.write_int32(0x1C2D0, 0x080FF094) # J 0x803FC250
rom.write_int32s(0xBFC250, patches.stage_start_cursor_updater)
rom.write_byte(0xB585C8, 0xFF)
# Make the Special1 and 2 play sounds when you reach milestones with them.
rom.write_int32s(0xBFDA50, patches.special_sound_notifs)
rom.write_int32(0xBF240, 0x080FF694) # J 0x803FDA50
rom.write_int32(0xBF220, 0x080FF69E) # J 0x803FDA78
# Add data for White Jewel #22 (the new Duel Tower savepoint) at the end of the White Jewel ID data list
rom.write_int16s(0x104AC8, [0x0000, 0x0006,
0x0013, 0x0015])
# Take the contract in Waterway off of its 00400000 bitflag.
rom.write_byte(0x87E3DA, 0x00)
# Spawn coordinates list extension
rom.write_int32(0xD5BF4, 0x080FF103) # J 0x803FC40C
rom.write_int32s(0xBFC40C, patches.spawn_coordinates_extension)
rom.write_int32s(0x108A5E, patches.waterway_end_coordinates)
# Change the File Select stage numbers to match the new stage order. Also fix a vanilla issue wherein saving in a
# character-exclusive stage as the other character would incorrectly display the name of that character's equivalent
# stage on the save file instead of the one they're actually in.
rom.write_byte(0xC9FE3, 0xD4)
rom.write_byte(0xCA055, 0x08)
rom.write_byte(0xCA066, 0x40)
rom.write_int32(0xCA068, 0x860C17D0) # LH T4, 0x17D0 (S0)
rom.write_byte(0xCA06D, 0x08)
rom.write_byte(0x104A31, 0x01)
rom.write_byte(0x104A39, 0x01)
rom.write_byte(0x104A89, 0x01)
rom.write_byte(0x104A91, 0x01)
rom.write_byte(0x104A99, 0x01)
rom.write_byte(0x104AA1, 0x01)
for stage in active_stage_exits:
for offset in get_stage_info(stage, "save number offsets"):
rom.write_byte(offset, active_stage_exits[stage]["position"])
# CC top elevator switch check
rom.write_int32(0x6CF0A0, 0x0C0FF0B0) # JAL 0x803FC2C0
rom.write_int32s(0xBFC2C0, patches.elevator_flag_checker)
# Disable time restrictions
if options.disable_time_restrictions:
# Fountain
rom.write_int32(0x6C2340, 0x00000000) # NOP
rom.write_int32(0x6C257C, 0x10000023) # B [forward 0x23]
# Rosa
rom.write_byte(0xEEAAB, 0x00)
rom.write_byte(0xEEAAD, 0x18)
# Moon doors
rom.write_int32(0xDC3E0, 0x00000000) # NOP
rom.write_int32(0xDC3E8, 0x00000000) # NOP
# Sun doors
rom.write_int32(0xDC410, 0x00000000) # NOP
rom.write_int32(0xDC418, 0x00000000) # NOP
# Custom data-loading code
rom.write_int32(0x6B5028, 0x08060D70) # J 0x801835D0
rom.write_int32s(0x1067B0, patches.custom_code_loader)
# Custom remote item rewarding and DeathLink receiving code
rom.write_int32(0x19B98, 0x080FF000) # J 0x803FC000
rom.write_int32s(0xBFC000, patches.remote_item_giver)
rom.write_int32s(0xBFE190, patches.subweapon_surface_checker)
# Make received DeathLinks blow you to smithereens instead of kill you normally.
if options.death_link == DeathLink.option_explosive:
rom.write_int32(0x27A70, 0x10000008) # B [forward 0x08]
rom.write_int32s(0xBFC0D0, patches.deathlink_nitro_edition)
# Set the DeathLink ROM flag if it's on at all.
if options.death_link != DeathLink.option_off:
rom.write_byte(0xBFBFDE, 0x01)
# DeathLink counter decrementer code
rom.write_int32(0x1C340, 0x080FF8F0) # J 0x803FE3C0
rom.write_int32s(0xBFE3C0, patches.deathlink_counter_decrementer)
rom.write_int32(0x25B6C, 0x0080FF052) # J 0x803FC148
rom.write_int32s(0xBFC148, patches.nitro_fall_killer)
# Death flag un-setter on "Beginning of stage" state overwrite code
rom.write_int32(0x1C2B0, 0x080FF047) # J 0x803FC11C
rom.write_int32s(0xBFC11C, patches.death_flag_unsetter)
# Warp menu-opening code
rom.write_int32(0xB9BA8, 0x080FF099) # J 0x803FC264
rom.write_int32s(0xBFC264, patches.warp_menu_opener)
# NPC item textbox hack
rom.write_int32(0xBF1DC, 0x080FF904) # J 0x803FE410
rom.write_int32(0xBF1E0, 0x27BDFFE0) # ADDIU SP, SP, -0x20
rom.write_int32s(0xBFE410, patches.npc_item_hack)
# Sub-weapon check function hook
rom.write_int32(0xBF32C, 0x00000000) # NOP
rom.write_int32(0xBF330, 0x080FF05E) # J 0x803FC178
rom.write_int32s(0xBFC178, patches.give_subweapon_stopper)
# Warp menu Special1 restriction
rom.write_int32(0xADD68, 0x0C04AB12) # JAL 0x8012AC48
rom.write_int32s(0xADE28, patches.stage_select_overwrite)
rom.write_byte(0xADE47, s1s_per_warp)
# Dracula's door text pointer hijack
rom.write_int32(0xD69F0, 0x080FF141) # J 0x803FC504
rom.write_int32s(0xBFC504, patches.dracula_door_text_redirector)
# Dracula's chamber condition
rom.write_int32(0xE2FDC, 0x0804AB25) # J 0x8012AC78
rom.write_int32s(0xADE84, patches.special_goal_checker)
rom.write_bytes(0xBFCC48, [0xA0, 0x00, 0xFF, 0xFF, 0xA0, 0x01, 0xFF, 0xFF, 0xA0, 0x02, 0xFF, 0xFF, 0xA0, 0x03, 0xFF,
0xFF, 0xA0, 0x04, 0xFF, 0xFF, 0xA0, 0x05, 0xFF, 0xFF, 0xA0, 0x06, 0xFF, 0xFF, 0xA0, 0x07,
0xFF, 0xFF, 0xA0, 0x08, 0xFF, 0xFF, 0xA0, 0x09])
if options.draculas_condition == DraculasCondition.option_crystal:
rom.write_int32(0x6C8A54, 0x0C0FF0C1) # JAL 0x803FC304
rom.write_int32s(0xBFC304, patches.crystal_special2_giver)
rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
f"You'll need the power\n"
f"of the basement crystal\n"
f"to undo the seal.", True))
special2_name = "Crystal "
special2_text = "The crystal is on!\n" \
"Time to teach the old man\n" \
"a lesson!"
elif options.draculas_condition == DraculasCondition.option_bosses:
rom.write_int32(0xBBD50, 0x080FF18C) # J 0x803FC630
rom.write_int32s(0xBFC630, patches.boss_special2_giver)
rom.write_int32s(0xBFC55C, patches.werebull_flag_unsetter_special2_electric_boogaloo)
rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
f"You'll need to defeat\n"
f"{required_s2s} powerful monsters\n"
f"to undo the seal.", True))
special2_name = "Trophy "
special2_text = f"Proof you killed a powerful\n" \
f"Night Creature. Earn {required_s2s}/{total_s2s}\n" \
f"to battle Dracula."
elif options.draculas_condition == DraculasCondition.option_specials:
special2_name = "Special2"
rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n"
f"You'll need to find\n"
f"{required_s2s} Special2 jewels\n"
f"to undo the seal.", True))
special2_text = f"Need {required_s2s}/{total_s2s} to kill Dracula.\n" \
f"Looking closely, you see...\n" \
f"a piece of him within?"
else:
rom.write_byte(0xADE8F, 0x00)
special2_name = "Special2"
special2_text = "If you're reading this,\n" \
"how did you get a Special2!?"
rom.write_byte(0xADE8F, required_s2s)
# Change the Special2 name depending on the setting.
rom.write_bytes(0xEFD4E, cv64_string_to_bytearray(special2_name))
# Change the Special1 and 2 menu descriptions to tell you how many you need to unlock a warp and fight Dracula
# respectively.
special_text_bytes = cv64_string_to_bytearray(f"{s1s_per_warp} per warp unlock.\n"
f"{options.total_special1s.value} exist in total.\n"
f"Z + R + START to warp.") + cv64_string_to_bytearray(special2_text)
rom.write_bytes(0xBFE53C, special_text_bytes)
# On-the-fly TLB script modifier
rom.write_int32s(0xBFC338, patches.double_component_checker)
rom.write_int32s(0xBFC3D4, patches.downstairs_seal_checker)
rom.write_int32s(0xBFE074, patches.mandragora_with_nitro_setter)
rom.write_int32s(0xBFC700, patches.overlay_modifiers)
# On-the-fly actor data modifier hook
rom.write_int32(0xEAB04, 0x080FF21E) # J 0x803FC878
rom.write_int32s(0xBFC870, patches.map_data_modifiers)
# Fix to make flags apply to freestanding invisible items properly
rom.write_int32(0xA84F8, 0x90CC0039) # LBU T4, 0x0039 (A2)
# Fix locked doors to check the key counters instead of their vanilla key locations' bitflags
# Pickup flag check modifications:
rom.write_int32(0x10B2D8, 0x00000002) # Left Tower Door
rom.write_int32(0x10B2F0, 0x00000003) # Storeroom Door
rom.write_int32(0x10B2FC, 0x00000001) # Archives Door
rom.write_int32(0x10B314, 0x00000004) # Maze Gate
rom.write_int32(0x10B350, 0x00000005) # Copper Door
rom.write_int32(0x10B3A4, 0x00000006) # Torture Chamber Door
rom.write_int32(0x10B3B0, 0x00000007) # ToE Gate
rom.write_int32(0x10B3BC, 0x00000008) # Science Door1
rom.write_int32(0x10B3C8, 0x00000009) # Science Door2
rom.write_int32(0x10B3D4, 0x0000000A) # Science Door3
rom.write_int32(0x6F0094, 0x0000000B) # CT Door 1
rom.write_int32(0x6F00A4, 0x0000000C) # CT Door 2
rom.write_int32(0x6F00B4, 0x0000000D) # CT Door 3
# Item counter decrement check modifications:
rom.write_int32(0xEDA84, 0x00000001) # Archives Door
rom.write_int32(0xEDA8C, 0x00000002) # Left Tower Door
rom.write_int32(0xEDA94, 0x00000003) # Storeroom Door
rom.write_int32(0xEDA9C, 0x00000004) # Maze Gate
rom.write_int32(0xEDAA4, 0x00000005) # Copper Door
rom.write_int32(0xEDAAC, 0x00000006) # Torture Chamber Door
rom.write_int32(0xEDAB4, 0x00000007) # ToE Gate
rom.write_int32(0xEDABC, 0x00000008) # Science Door1
rom.write_int32(0xEDAC4, 0x00000009) # Science Door2
rom.write_int32(0xEDACC, 0x0000000A) # Science Door3
rom.write_int32(0xEDAD4, 0x0000000B) # CT Door 1
rom.write_int32(0xEDADC, 0x0000000C) # CT Door 2
rom.write_int32(0xEDAE4, 0x0000000D) # CT Door 3
# Fix ToE gate's "unlocked" flag in the locked door flags table
rom.write_int16(0x10B3B6, 0x0001)
rom.write_int32(0x10AB2C, 0x8015FBD4) # Maze Gates' check code pointer adjustments
rom.write_int32(0x10AB40, 0x8015FBD4)
rom.write_int32s(0x10AB50, [0x0D0C0000,
0x8015FBD4])
rom.write_int32s(0x10AB64, [0x0D0C0000,
0x8015FBD4])
rom.write_int32s(0xE2E14, patches.normal_door_hook)
rom.write_int32s(0xBFC5D0, patches.normal_door_code)
rom.write_int32s(0x6EF298, patches.ct_door_hook)
rom.write_int32s(0xBFC608, patches.ct_door_code)
# Fix key counter not decrementing if 2 or above
rom.write_int32(0xAA0E0, 0x24020000) # ADDIU V0, R0, 0x0000
# Make the Easy-only candle drops in Room of Clocks appear on any difficulty
rom.write_byte(0x9B518F, 0x01)
# Slightly move some once-invisible freestanding items to be more visible
if options.invisible_items == InvisibleItems.option_reveal_all:
rom.write_byte(0x7C7F95, 0xEF) # Forest dirge maiden statue
rom.write_byte(0x7C7FA8, 0xAB) # Forest werewolf statue
rom.write_byte(0x8099C4, 0x8C) # Villa courtyard tombstone
rom.write_byte(0x83A626, 0xC2) # Villa living room painting
# rom.write_byte(0x83A62F, 0x64) # Villa Mary's room table
rom.write_byte(0xBFCB97, 0xF5) # CC torture instrument rack
rom.write_byte(0x8C44D5, 0x22) # CC red carpet hallway knight
rom.write_byte(0x8DF57C, 0xF1) # CC cracked wall hallway flamethrower
rom.write_byte(0x90FCD6, 0xA5) # CC nitro hallway flamethrower
rom.write_byte(0x90FB9F, 0x9A) # CC invention room round machine
rom.write_byte(0x90FBAF, 0x03) # CC invention room giant famicart
rom.write_byte(0x90FE54, 0x97) # CC staircase knight (x)
rom.write_byte(0x90FE58, 0xFB) # CC staircase knight (z)
# Change bitflag on item in upper coffin in Forest final switch gate tomb to one that's not used by something else
rom.write_int32(0x10C77C, 0x00000002)
# Make the torch directly behind Dracula's chamber that normally doesn't set a flag set bitflag 0x08 in 0x80389BFA
rom.write_byte(0x10CE9F, 0x01)
# Change the CC post-Behemoth boss depending on the option for Post-Behemoth Boss
if options.post_behemoth_boss == PostBehemothBoss.option_inverted:
rom.write_byte(0xEEDAD, 0x02)
rom.write_byte(0xEEDD9, 0x01)
elif options.post_behemoth_boss == PostBehemothBoss.option_always_rosa:
rom.write_byte(0xEEDAD, 0x00)
rom.write_byte(0xEEDD9, 0x03)
# Put both on the same flag so changing character won't trigger a rematch with the same boss.
rom.write_byte(0xEED8B, 0x40)
elif options.post_behemoth_boss == PostBehemothBoss.option_always_camilla:
rom.write_byte(0xEEDAD, 0x03)
rom.write_byte(0xEEDD9, 0x00)
rom.write_byte(0xEED8B, 0x40)
# Change the RoC boss depending on the option for Room of Clocks Boss
if options.room_of_clocks_boss == RoomOfClocksBoss.option_inverted:
rom.write_byte(0x109FB3, 0x56)
rom.write_byte(0x109FBF, 0x44)
rom.write_byte(0xD9D44, 0x14)
rom.write_byte(0xD9D4C, 0x14)
elif options.room_of_clocks_boss == RoomOfClocksBoss.option_always_death:
rom.write_byte(0x109FBF, 0x44)
rom.write_byte(0xD9D45, 0x00)
# Put both on the same flag so changing character won't trigger a rematch with the same boss.
rom.write_byte(0x109FB7, 0x90)
rom.write_byte(0x109FC3, 0x90)
elif options.room_of_clocks_boss == RoomOfClocksBoss.option_always_actrise:
rom.write_byte(0x109FB3, 0x56)
rom.write_int32(0xD9D44, 0x00000000)
rom.write_byte(0xD9D4D, 0x00)
rom.write_byte(0x109FB7, 0x90)
rom.write_byte(0x109FC3, 0x90)
# Un-nerf Actrise when playing as Reinhardt.
# This is likely a leftover TGS demo feature in which players could battle Actrise as Reinhardt.
rom.write_int32(0xB318B4, 0x240E0001) # ADDIU T6, R0, 0x0001
# Tunnel gondola skip
if options.skip_gondolas:
rom.write_int32(0x6C5F58, 0x080FF7D0) # J 0x803FDF40
rom.write_int32s(0xBFDF40, patches.gondola_skipper)
# New gondola transfer point candle coordinates
rom.write_byte(0xBFC9A3, 0x04)
rom.write_bytes(0x86D824, [0x27, 0x01, 0x10, 0xF7, 0xA0])
# Waterway brick platforms skip
if options.skip_waterway_blocks:
rom.write_int32(0x6C7E2C, 0x00000000) # NOP
# Ambience silencing fix
rom.write_int32(0xD9270, 0x080FF840) # J 0x803FE100
rom.write_int32s(0xBFE100, patches.ambience_silencer)
# Fix for the door sliding sound playing infinitely if leaving the fan meeting room before the door closes entirely.
# Hooking this in the ambience silencer code does nothing for some reason.
rom.write_int32s(0xAE10C, [0x08004FAB, # J 0x80013EAC
0x3404829B]) # ORI A0, R0, 0x829B
rom.write_int32s(0xD9E8C, [0x08004FAB, # J 0x80013EAC
0x3404829B]) # ORI A0, R0, 0x829B
# Fan meeting room ambience fix
rom.write_int32(0x109964, 0x803FE13C)
# Make the Villa coffin cutscene skippable
rom.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
rom.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
# Increase shimmy speed
if options.increase_shimmy_speed:
rom.write_byte(0xA4241, 0x5A)
# Disable landing fall damage
if options.fall_guard:
rom.write_byte(0x27B23, 0x00)
# Enable the unused film reel effect on all cutscenes
if options.cinematic_experience:
rom.write_int32(0xAA33C, 0x240A0001) # ADDIU T2, R0, 0x0001
rom.write_byte(0xAA34B, 0x0C)
rom.write_int32(0xAA4C4, 0x24090001) # ADDIU T1, R0, 0x0001
# Permanent PowerUp stuff
if options.permanent_powerups:
# Make receiving PowerUps increase the unused menu PowerUp counter instead of the one outside the save struct
rom.write_int32(0xBF2EC, 0x806B619B) # LB T3, 0x619B (V1)
rom.write_int32(0xBFC5BC, 0xA06C619B) # SB T4, 0x619B (V1)
# Make Reinhardt's whip check the menu PowerUp counter
rom.write_int32(0x69FA08, 0x80CC619B) # LB T4, 0x619B (A2)
rom.write_int32(0x69FBFC, 0x80C3619B) # LB V1, 0x619B (A2)
rom.write_int32(0x69FFE0, 0x818C9C53) # LB T4, 0x9C53 (T4)
# Make Carrie's orb check the menu PowerUp counter
rom.write_int32(0x6AC86C, 0x8105619B) # LB A1, 0x619B (T0)
rom.write_int32(0x6AC950, 0x8105619B) # LB A1, 0x619B (T0)
rom.write_int32(0x6AC99C, 0x810E619B) # LB T6, 0x619B (T0)
rom.write_int32(0x5AFA0, 0x80639C53) # LB V1, 0x9C53 (V1)
rom.write_int32(0x5B0A0, 0x81089C53) # LB T0, 0x9C53 (T0)
rom.write_byte(0x391C7, 0x00) # Prevent PowerUps from dropping from regular enemies
rom.write_byte(0xEDEDF, 0x03) # Make any vanishing PowerUps that do show up L jewels instead
# Rename the PowerUp to "PermaUp"
rom.write_bytes(0xEFDEE, cv64_string_to_bytearray("PermaUp"))
# Replace the PowerUp in the Forest Special1 Bridge 3HB rock with an L jewel if 3HBs aren't randomized
if not options.multi_hit_breakables:
rom.write_byte(0x10C7A1, 0x03)
# Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other
# game PermaUps are distinguishable.
rom.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00])
# Write the randomized (or disabled) music ID list and its associated code
if options.background_music:
rom.write_int32(0x14588, 0x08060D60) # J 0x80183580
rom.write_int32(0x14590, 0x00000000) # NOP
rom.write_int32s(0x106770, patches.music_modifier)
rom.write_int32(0x15780, 0x0C0FF36E) # JAL 0x803FCDB8
rom.write_int32s(0xBFCDB8, patches.music_comparer_modifier)
# Enable storing item flags anywhere and changing the item model/visibility on any item instance.
rom.write_int32s(0xA857C, [0x080FF38F, # J 0x803FCE3C
0x94D90038]) # LHU T9, 0x0038 (A2)
rom.write_int32s(0xBFCE3C, patches.item_customizer)
rom.write_int32s(0xA86A0, [0x0C0FF3AF, # JAL 0x803FCEBC
0x95C40002]) # LHU A0, 0x0002 (T6)
rom.write_int32s(0xBFCEBC, patches.item_appearance_switcher)
rom.write_int32s(0xA8728, [0x0C0FF3B8, # JAL 0x803FCEE4
0x01396021]) # ADDU T4, T1, T9
rom.write_int32s(0xBFCEE4, patches.item_model_visibility_switcher)
rom.write_int32s(0xA8A04, [0x0C0FF3C2, # JAL 0x803FCF08
0x018B6021]) # ADDU T4, T4, T3
rom.write_int32s(0xBFCF08, patches.item_shine_visibility_switcher)
# Make Axes and Crosses in AP Locations drop to their correct height, and make items with changed appearances spin
# their correct speed.
rom.write_int32s(0xE649C, [0x0C0FFA03, # JAL 0x803FE80C
0x956C0002]) # LHU T4, 0x0002 (T3)
rom.write_int32s(0xA8B08, [0x080FFA0C, # J 0x803FE830
0x960A0038]) # LHU T2, 0x0038 (S0)
rom.write_int32s(0xE8584, [0x0C0FFA21, # JAL 0x803FE884
0x95D80000]) # LHU T8, 0x0000 (T6)
rom.write_int32s(0xE7AF0, [0x0C0FFA2A, # JAL 0x803FE8A8
0x958D0000]) # LHU T5, 0x0000 (T4)
rom.write_int32s(0xBFE7DC, patches.item_drop_spin_corrector)
# Disable the 3HBs checking and setting flags when breaking them and enable their individual items checking and
# setting flags instead.
if options.multi_hit_breakables:
rom.write_int32(0xE87F8, 0x00000000) # NOP
rom.write_int16(0xE836C, 0x1000)
rom.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34
rom.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter)
# Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one)
rom.write_int32(0xE7D54, 0x00000000) # NOP
rom.write_int16(0xE7908, 0x1000)
rom.write_byte(0xE7A5C, 0x10)
rom.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C
rom.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter)
# New flag values to put in each 3HB vanilla flag's spot
rom.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock
rom.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock
rom.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub
rom.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab
rom.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab
rom.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock
rom.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge
rom.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge
rom.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate
rom.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal
rom.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab
rom.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge
rom.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate
rom.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab
rom.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab
rom.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
rom.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
rom.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
rom.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
# Once-per-frame gameplay checks
rom.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
rom.write_int32(0xBFD058, 0x0801AEB5) # J 0x8006BAD4
# Everything related to dropping the previous sub-weapon
if options.drop_previous_sub_weapon:
rom.write_int32(0xBFD034, 0x080FF3FF) # J 0x803FCFFC
rom.write_int32(0xBFC190, 0x080FF3F2) # J 0x803FCFC8
rom.write_int32s(0xBFCFC4, patches.prev_subweapon_spawn_checker)
rom.write_int32s(0xBFCFFC, patches.prev_subweapon_fall_checker)
rom.write_int32s(0xBFD060, patches.prev_subweapon_dropper)
# Everything related to the Countdown counter
if options.countdown:
rom.write_int32(0xBFD03C, 0x080FF9DC) # J 0x803FE770
rom.write_int32(0xD5D48, 0x080FF4EC) # J 0x803FD3B0
rom.write_int32s(0xBFD3B0, patches.countdown_number_displayer)
rom.write_int32s(0xBFD6DC, patches.countdown_number_manager)
rom.write_int32s(0xBFE770, patches.countdown_demo_hider)
rom.write_int32(0xBFCE2C, 0x080FF5D2) # J 0x803FD748
rom.write_int32s(0xBB168, [0x080FF5F4, # J 0x803FD7D0
0x8E020028]) # LW V0, 0x0028 (S0)
rom.write_int32s(0xBB1D0, [0x080FF5FB, # J 0x803FD7EC
0x8E020028]) # LW V0, 0x0028 (S0)
rom.write_int32(0xBC4A0, 0x080FF5E6) # J 0x803FD798
rom.write_int32(0xBC4C4, 0x080FF5E6) # J 0x803FD798
rom.write_int32(0x19844, 0x080FF602) # J 0x803FD808
# If the option is set to "all locations", count it down no matter what the item is.
if options.countdown == Countdown.option_all_locations:
rom.write_int32s(0xBFD71C, [0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101,
0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101])
else:
# If it's majors, then insert this last minute check I threw together for the weird edge case of a CV64 ice
# trap for another CV64 player taking the form of a major.
rom.write_int32s(0xBFD788, [0x080FF717, # J 0x803FDC5C
0x2529FFFF]) # ADDIU T1, T1, 0xFFFF
rom.write_int32s(0xBFDC5C, patches.countdown_extra_safety_check)
rom.write_int32(0xA9ECC, 0x00000000) # NOP the pointless overwrite of the item actor appearance custom value.
# Ice Trap stuff
rom.write_int32(0x697C60, 0x080FF06B) # J 0x803FC18C
rom.write_int32(0x6A5160, 0x080FF06B) # J 0x803FC18C
rom.write_int32s(0xBFC1AC, patches.ice_trap_initializer)
rom.write_int32s(0xBFE700, patches.the_deep_freezer)
rom.write_int32s(0xB2F354, [0x3739E4C0, # ORI T9, T9, 0xE4C0
0x03200008, # JR T9
0x00000000]) # NOP
rom.write_int32s(0xBFE4C0, patches.freeze_verifier)
# Initial Countdown numbers
rom.write_int32(0xAD6A8, 0x080FF60A) # J 0x803FD828
rom.write_int32s(0xBFD828, patches.new_game_extras)
# Everything related to shopsanity
if options.shopsanity:
rom.write_byte(0xBFBFDF, 0x01)
rom.write_bytes(0x103868, cv64_string_to_bytearray("Not obtained. "))
rom.write_int32s(0xBFD8D0, patches.shopsanity_stuff)
rom.write_int32(0xBD828, 0x0C0FF643) # JAL 0x803FD90C
rom.write_int32(0xBD5B8, 0x0C0FF651) # JAL 0x803FD944
rom.write_int32(0xB0610, 0x0C0FF665) # JAL 0x803FD994
rom.write_int32s(0xBD24C, [0x0C0FF677, # J 0x803FD9DC
0x00000000]) # NOP
rom.write_int32(0xBD618, 0x0C0FF684) # JAL 0x803FDA10
shopsanity_name_text = []
shopsanity_desc_text = []
for i in range(len(shop_name_list)):
shopsanity_name_text += bytearray([0xA0, i]) + shop_colors_list[i] + \
cv64_string_to_bytearray(cv64_text_truncate(shop_name_list[i], 74))
shopsanity_desc_text += [0xA0, i]
if shop_desc_list[i][1] is not None:
shopsanity_desc_text += cv64_string_to_bytearray("For " + shop_desc_list[i][1] + ".\n",
append_end=False)
shopsanity_desc_text += cv64_string_to_bytearray(renon_item_dialogue[shop_desc_list[i][0]])
rom.write_bytes(0x1AD00, shopsanity_name_text)
rom.write_bytes(0x1A800, shopsanity_desc_text)
# Panther Dash running
if options.panther_dash:
rom.write_int32(0x69C8C4, 0x0C0FF77E) # JAL 0x803FDDF8
rom.write_int32(0x6AA228, 0x0C0FF77E) # JAL 0x803FDDF8
rom.write_int32s(0x69C86C, [0x0C0FF78E, # JAL 0x803FDE38
0x3C01803E]) # LUI AT, 0x803E
rom.write_int32s(0x6AA1D0, [0x0C0FF78E, # JAL 0x803FDE38
0x3C01803E]) # LUI AT, 0x803E
rom.write_int32(0x69D37C, 0x0C0FF79E) # JAL 0x803FDE78
rom.write_int32(0x6AACE0, 0x0C0FF79E) # JAL 0x803FDE78
rom.write_int32s(0xBFDDF8, patches.panther_dash)
# Jump prevention
if options.panther_dash == PantherDash.option_jumpless:
rom.write_int32(0xBFDE2C, 0x080FF7BB) # J 0x803FDEEC
rom.write_int32(0xBFD044, 0x080FF7B1) # J 0x803FDEC4
rom.write_int32s(0x69B630, [0x0C0FF7C6, # JAL 0x803FDF18
0x8CCD0000]) # LW T5, 0x0000 (A2)
rom.write_int32s(0x6A8EC0, [0x0C0FF7C6, # JAL 0x803FDF18
0x8CCC0000]) # LW T4, 0x0000 (A2)
# Fun fact: KCEK put separate code to handle coyote time jumping
rom.write_int32s(0x69910C, [0x0C0FF7C6, # JAL 0x803FDF18
0x8C4E0000]) # LW T6, 0x0000 (V0)
rom.write_int32s(0x6A6718, [0x0C0FF7C6, # JAL 0x803FDF18
0x8C4E0000]) # LW T6, 0x0000 (V0)
rom.write_int32s(0xBFDEC4, patches.panther_jump_preventer)
# Everything related to Big Toss.
if options.big_toss:
rom.write_int32s(0x27E90, [0x0C0FFA38, # JAL 0x803FE8E0
0xAFB80074]) # SW T8, 0x0074 (SP)
rom.write_int32(0x26F54, 0x0C0FFA4D) # JAL 0x803FE934
rom.write_int32s(0xBFE8E0, patches.big_tosser)
# Write all the new randomized bytes.
for offset, item_id in offset_data.items():
if item_id <= 0xFF:
rom.write_byte(offset, item_id)
elif item_id <= 0xFFFF:
rom.write_int16(offset, item_id)
elif item_id <= 0xFFFFFF:
rom.write_int24(offset, item_id)
else:
rom.write_int32(offset, item_id)
# Write the secondary name the client will use to distinguish a vanilla ROM from an AP one.
rom.write_bytes(0xBFBFD0, "ARCHIPELAGO1".encode("utf-8"))
# Write the slot authentication
rom.write_bytes(0xBFBFE0, world.auth)
# Write the specified window colors
rom.write_byte(0xAEC23, options.window_color_r.value << 4)
rom.write_byte(0xAEC33, options.window_color_g.value << 4)
rom.write_byte(0xAEC47, options.window_color_b.value << 4)
rom.write_byte(0xAEC43, options.window_color_a.value << 4)
# Write the item/player names for other game items
for loc in active_locations:
if loc.address is None or get_location_info(loc.name, "type") == "shop" or loc.item.player == player:
continue
if len(loc.item.name) > 67:
item_name = loc.item.name[0x00:0x68]
else:
item_name = loc.item.name
inject_address = 0xBB7164 + (256 * (loc.address & 0xFFF))
wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " + multiworld.get_player_name(loc.item.player), 96)
rom.write_bytes(inject_address, get_item_text_color(loc) + cv64_string_to_bytearray(wrapped_name))
rom.write_byte(inject_address + 255, num_lines)
# Everything relating to loading the other game items text
rom.write_int32(0xA8D8C, 0x080FF88F) # J 0x803FE23C
rom.write_int32(0xBEA98, 0x0C0FF8B4) # JAL 0x803FE2D0
rom.write_int32(0xBEAB0, 0x0C0FF8BD) # JAL 0x803FE2F8
rom.write_int32(0xBEACC, 0x0C0FF8C5) # JAL 0x803FE314
rom.write_int32s(0xBFE23C, patches.multiworld_item_name_loader)
rom.write_bytes(0x10F188, [0x00 for _ in range(264)])
rom.write_bytes(0x10F298, [0x00 for _ in range(264)])
# When the game normally JALs to the item prepare textbox function after the player picks up an item, set the
# "no receiving" timer to ensure the item textbox doesn't freak out if you pick something up while there's a queue
# of unreceived items.
rom.write_int32(0xA8D94, 0x0C0FF9F0) # JAL 0x803FE7C0
rom.write_int32s(0xBFE7C0, [0x3C088039, # LUI T0, 0x8039
0x24090020, # ADDIU T1, R0, 0x0020
0x0804EDCE, # J 0x8013B738
0xA1099BE0]) # SB T1, 0x9BE0 (T0)
class CV64DeltaPatch(APDeltaPatch):
hash = CV64US10HASH
patch_file_ending: str = ".apcv64"
result_file_ending: str = ".z64"
game = "Castlevania 64"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def patch(self, target: str):
super().patch(target)
rom = LocalRom(target)
# Extract the item models file, decompress it, append the AP icons, compress it back, re-insert it.
items_file = lzkn64.decompress_buffer(rom.read_bytes(0x9C5310, 0x3D28))
compressed_file = lzkn64.compress_buffer(items_file[0:0x69B6] + pkgutil.get_data(__name__, "data/ap_icons.bin"))
rom.write_bytes(0xBB2D88, compressed_file)
# Update the items' Nisitenma-Ichigo table entry to point to the new file's start and end addresses in the ROM.
rom.write_int32s(0x95F04, [0x80BB2D88, 0x00BB2D88 + len(compressed_file)])
# Update the items' decompressed file size tables with the new file's decompressed file size.
rom.write_int16(0x95706, 0x7BF0)
rom.write_int16(0x104CCE, 0x7BF0)
# Update the Wooden Stake and Roses' item appearance settings table to point to the Archipelago item graphics.
rom.write_int16(0xEE5BA, 0x7B38)
rom.write_int16(0xEE5CA, 0x7280)
# Change the items' sizes. The progression one will be larger than the non-progression one.
rom.write_int32(0xEE5BC, 0x3FF00000)
rom.write_int32(0xEE5CC, 0x3FA00000)
rom.write_to_file(target)
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(open(file_name, "rb").read())
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if CV64US10HASH != basemd5.hexdigest():
raise Exception("Supplied Base Rom does not match known MD5 for Castlevania 64 US 1.0."
"Get the correct game and version, then dump it.")
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
if not file_name:
file_name = get_settings()["cv64_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name

103
worlds/cv64/rules.py Normal file
View File

@ -0,0 +1,103 @@
from typing import Dict, TYPE_CHECKING
from BaseClasses import CollectionState
from worlds.generic.Rules import allow_self_locking_items, CollectionRule
from .options import DraculasCondition
from .entrances import get_entrance_info
from .data import iname, rname
if TYPE_CHECKING:
from . import CV64World
class CV64Rules:
player: int
world: "CV64World"
rules: Dict[str, CollectionRule]
s1s_per_warp: int
required_s2s: int
drac_condition: int
def __init__(self, world: "CV64World") -> None:
self.player = world.player
self.world = world
self.s1s_per_warp = world.s1s_per_warp
self.required_s2s = world.required_s2s
self.drac_condition = world.drac_condition
self.rules = {
iname.left_tower_key: lambda state: state.has(iname.left_tower_key, self.player),
iname.storeroom_key: lambda state: state.has(iname.storeroom_key, self.player),
iname.archives_key: lambda state: state.has(iname.archives_key, self.player),
iname.garden_key: lambda state: state.has(iname.garden_key, self.player),
iname.copper_key: lambda state: state.has(iname.copper_key, self.player),
iname.chamber_key: lambda state: state.has(iname.chamber_key, self.player),
"Bomb 1": lambda state: state.has_all({iname.magical_nitro, iname.mandragora}, self.player),
"Bomb 2": lambda state: state.has(iname.magical_nitro, self.player, 2)
and state.has(iname.mandragora, self.player, 2),
iname.execution_key: lambda state: state.has(iname.execution_key, self.player),
iname.science_key1: lambda state: state.has(iname.science_key1, self.player),
iname.science_key2: lambda state: state.has(iname.science_key2, self.player),
iname.science_key3: lambda state: state.has(iname.science_key3, self.player),
iname.clocktower_key1: lambda state: state.has(iname.clocktower_key1, self.player),
iname.clocktower_key2: lambda state: state.has(iname.clocktower_key2, self.player),
iname.clocktower_key3: lambda state: state.has(iname.clocktower_key3, self.player),
"Dracula": self.can_enter_dracs_chamber
}
def can_enter_dracs_chamber(self, state: CollectionState) -> bool:
drac_object_name = None
if self.drac_condition == DraculasCondition.option_crystal:
drac_object_name = "Crystal"
elif self.drac_condition == DraculasCondition.option_bosses:
drac_object_name = "Trophy"
elif self.drac_condition == DraculasCondition.option_specials:
drac_object_name = "Special2"
if drac_object_name is not None:
return state.has(drac_object_name, self.player, self.required_s2s)
return True
def set_cv64_rules(self) -> None:
multiworld = self.world.multiworld
for region in multiworld.get_regions(self.player):
# Set each entrance's rule if it should have one.
# Warp entrances have their own special handling.
for entrance in region.entrances:
if entrance.parent_region.name == "Menu":
if entrance.name.startswith("Warp "):
entrance.access_rule = lambda state, warp_num=int(entrance.name[5]): \
state.has(iname.special_one, self.player, self.s1s_per_warp * warp_num)
else:
ent_rule = get_entrance_info(entrance.name, "rule")
if ent_rule in self.rules:
entrance.access_rule = self.rules[ent_rule]
multiworld.completion_condition[self.player] = lambda state: state.has(iname.victory, self.player)
if self.world.options.accessibility: # not locations accessibility
self.set_self_locking_items()
def set_self_locking_items(self) -> None:
multiworld = self.world.multiworld
# Do the regions that we know for a fact always exist, and we always do no matter what.
allow_self_locking_items(multiworld.get_region(rname.villa_archives, self.player), iname.archives_key)
allow_self_locking_items(multiworld.get_region(rname.cc_torture_chamber, self.player), iname.chamber_key)
# Add this region if the world doesn't have the Villa Storeroom warp entrance.
if "Villa" not in self.world.active_warp_list[1:]:
allow_self_locking_items(multiworld.get_region(rname.villa_storeroom, self.player), iname.storeroom_key)
# Add this region if Hard Logic is on and Multi Hit Breakables are off.
if self.world.options.hard_logic and not self.world.options.multi_hit_breakables:
allow_self_locking_items(multiworld.get_region(rname.cw_ltower, self.player), iname.left_tower_key)
# Add these regions if Tower of Science is in the world.
if "Tower of Science" in self.world.active_stage_exits:
allow_self_locking_items(multiworld.get_region(rname.tosci_three_doors, self.player), iname.science_key1)
allow_self_locking_items(multiworld.get_region(rname.tosci_key3, self.player), iname.science_key3)
# Add this region if Tower of Execution is in the world and Hard Logic is not on.
if "Tower of Execution" in self.world.active_stage_exits and self.world.options.hard_logic:
allow_self_locking_items(multiworld.get_region(rname.toe_ledge, self.player), iname.execution_key)

View File

@ -0,0 +1,69 @@
// Written by Moisés
#include "include/game/module.h"
#include "include/game/math.h"
#include "cv64.h"
extern vec3f player_pos;
extern vec3s player_angle; // player_angle.y = Player's facing angle (yaw)
extern f32 player_height_with_respect_of_floor; // Stored negative in-game
#define SHT_MAX 32767.0f
#define SHT_MINV (1.0f / SHT_MAX)
void spawn_item_behind_player(s32 item) {
interactuablesModule* pickable_item = NULL;
const f32 spawnDistance = 8.0f;
vec3f player_backwards_dir;
pickable_item = (interactuablesModule*)module_createAndSetChild(moduleList_findFirstModuleByID(ACTOR_CREATOR), ACTOR_ITEM);
if (pickable_item != NULL) {
// Convert facing angle to a vec3f
// SHT_MINV needs to be negative here for the item to be spawned properly on the character's back
player_backwards_dir.x = coss(-player_angle.y) * -SHT_MINV;
player_backwards_dir.z = sins(-player_angle.y) * -SHT_MINV;
// Multiply facing vector with distance away from the player
vec3f_multiplyScalar(&player_backwards_dir, &player_backwards_dir, spawnDistance);
// Assign the position of the item relative to the player's current position.
vec3f_add(&pickable_item->position, &player_pos, &player_backwards_dir);
// The Y position of the item will be the same as the floor right under the player
// The player's height with respect of the flower under them is already stored negative in-game,
// so no need to substract
pickable_item->position.y = player_pos.y + 5.0f;
pickable_item->height = pickable_item->position.y;
// Assign item ID
pickable_item->item_ID = item;
}
}
const f32 droppingAccel = 0.05f;
const f32 maxDroppingSpeed = 1.5f;
f32 droppingSpeed = 0.0f;
f32 droppingTargetYPos = 0.0f;
u8 dropItemCalcFuncCalled = FALSE;
s32 drop_item_calc(interactuablesModule* pickable_item) {
if (dropItemCalcFuncCalled == FALSE) {
droppingTargetYPos = player_pos.y + player_height_with_respect_of_floor + 1.0f;
if (pickable_item->item_ID == CROSS || pickable_item->item_ID == AXE ||
pickable_item->item_ID == CROSS__VANISH || pickable_item->item_ID == AXE__VANISH) {
droppingTargetYPos += 3.0f;
}
dropItemCalcFuncCalled = TRUE;
return TRUE;
}
if (pickable_item->position.y <= droppingTargetYPos) {
droppingSpeed = 0.0f;
dropItemCalcFuncCalled = FALSE;
return FALSE;
}
else {
if (droppingSpeed < maxDroppingSpeed) {
droppingSpeed += droppingAccel;
}
pickable_item->position.y -= droppingSpeed;
pickable_item->height = pickable_item->position.y;
return TRUE;
}
}

116
worlds/cv64/src/print.c Normal file
View File

@ -0,0 +1,116 @@
// Written by Moisés.
// NOTE: This is an earlier version to-be-replaced.
#include <memory.h>
#include <textbox.h>
// Helper function
// https://decomp.me/scratch/9H1Uy
u32 convertUTF8StringToUTF16(char* src, u16* buffer) {
u32 string_length = 0;
// If the source string starts with a null char (0), we assume the string empty.
if (*src != 0) {
// Copy the char from the source string into the bufferination.
// Then advance to the next char until we find the null char (0).
do {
*buffer = *src;
src++;
buffer++;
string_length++;
} while (*src != 0);
}
// Make sure to add the null char at the end of the bufferination string,
// and then return the length of the string.
*buffer = 0;
return string_length;
}
// Begin printing ASCII text stored in a char*
textbox* print_text(const char* message, const s16 X_pos, const s16 Y_pos, const u8 number_of_lines, const s16 textbox_width, const u32 txtbox_flags, const void* module) {
textbox* (*ptr_textbox_create)(void*, void*, u32) = textbox_create;
void (*ptr_textbox_setPos)(textbox*, u16, u16, s32) = textbox_setPos;
void (*ptr_textbox_setDimensions)(textbox*, s8, s16, u8, u8) = textbox_setDimensions;
void (*ptr_textbox_setMessagePtr)(textbox*, u16*, s32, s16) = textbox_setMessagePtr;
u16* (*ptr_convertUTF16ToCustomTextFormat)(u16*) = convertUTF16ToCustomTextFormat;
void* (*ptr_malloc)(s32, u32) = malloc;
textbox* txtbox = NULL;
// Allocate memory for the text buffer
u16* text_buffer = (u16*) ptr_malloc(0, 100);
// Create the textbox data structure
if (module != NULL) {
txtbox = ptr_textbox_create(module, HUD_camera, txtbox_flags);
}
if (txtbox != NULL && text_buffer != NULL && message != NULL) {
// Set text position and dimensions
ptr_textbox_setPos(txtbox, X_pos, Y_pos, 1);
ptr_textbox_setDimensions(txtbox, number_of_lines, textbox_width, 0, 0);
// Convert the ASCII message to the CV64 custom format
convertUTF8StringToUTF16(message, text_buffer);
ptr_convertUTF16ToCustomTextFormat(text_buffer);
// Set the text buffer pointer to the textbox data structure
ptr_textbox_setMessagePtr(txtbox, text_buffer, 0, 0);
}
// We return the textbox so that we can modify its properties once it begins printing
// (say to show, hide the text)
return txtbox;
}
// Begin printing signed integer
textbox* print_number(const s32 number, u16* text_buffer, const s16 X_pos, const s16 Y_pos, const u8 number_of_digits, const u32 txtbox_flags, const u32 additional_text_flag, const void* module) {
textbox* (*ptr_textbox_create)(void*, void*, u32) = textbox_create;
void (*ptr_textbox_setPos)(textbox*, u16, u16, s32) = textbox_setPos;
void (*ptr_textbox_setDimensions)(textbox*, s8, s16, u8, u8) = textbox_setDimensions;
void (*ptr_textbox_setMessagePtr)(textbox*, u16*, s32, s16) = textbox_setMessagePtr;
void (*ptr_text_convertIntNumberToText)(u32, u16*, u8, u32) = text_convertIntNumberToText;
textbox* txtbox = NULL;
// Create the textbox data structure
if (module != NULL) {
txtbox = ptr_textbox_create(module, HUD_camera, txtbox_flags);
}
if (txtbox != NULL && text_buffer != NULL) {
// Set text position and dimensions
ptr_textbox_setPos(txtbox, X_pos, Y_pos, 1);
ptr_textbox_setDimensions(txtbox, 1, 100, 0, 0);
// Convert the number to the CV64 custom format
ptr_text_convertIntNumberToText(number, text_buffer, number_of_digits, additional_text_flag);
// Set the text buffer pointer to the textbox data structure
ptr_textbox_setMessagePtr(txtbox, text_buffer, 0, 0);
}
// We return the textbox so that we can modify its properties once it begins printing
// (say to show, hide the text)
return txtbox;
}
// Update the value of a number that began printing after calling "print_number()"
void update_printed_number(textbox* txtbox, const s32 number, u16* text_buffer, const u8 number_of_digits, const u32 additional_text_flag) {
void (*ptr_text_convertIntNumberToText)(u32, u16*, u8, u32) = text_convertIntNumberToText;
if (text_buffer != NULL) {
ptr_text_convertIntNumberToText(number, text_buffer, number_of_digits, additional_text_flag);
txtbox->flags |= 0x1000000; // Needed to make sure the number updates properly
}
}
void display_text(textbox* txtbox, const u8 display_textbox) {
if (txtbox != NULL) {
if (display_textbox == TRUE) {
// Show text
txtbox->flags &= ~HIDE_TEXTBOX;
}
else {
// Hide text
txtbox->flags |= HIDE_TEXTBOX;
}
}
}

View File

@ -0,0 +1,26 @@
// Written by Moisés
#include "print.h"
#include <textbox.h>
#include <memory.h>
#define counter_X_pos 30
#define counter_Y_pos 40
#define counter_number_of_digits 2
#define GOLD_JEWEL_FONT 0x14
extern u8 bytes[13];
u16* number_text_buffer = NULL;
textbox* txtbox = NULL;
void begin_print() {
// Allocate memory for the number text
number_text_buffer = (u16*) malloc(0, 12);
// Assuming that 0x80342814 = HUD Module
txtbox = print_number(0, number_text_buffer, counter_X_pos, counter_Y_pos, counter_number_of_digits, 0x08600000, GOLD_JEWEL_FONT, (void*) 0x80342814);
}
void update_print(u8 i) {
update_printed_number(txtbox, (s32) bytes[i], number_text_buffer, counter_number_of_digits, GOLD_JEWEL_FONT);
}

490
worlds/cv64/stages.py Normal file
View File

@ -0,0 +1,490 @@
import logging
from .data import rname
from .regions import get_region_info
from .locations import get_location_info
from .options import WarpOrder
from typing import TYPE_CHECKING, Dict, List, Tuple, Union
if TYPE_CHECKING:
from . import CV64World
# # # KEY # # #
# "start region" = The Region that the start of the stage is in. Used for connecting the previous stage's end and
# alternate end (if it exists) Entrances to the start of the next one.
# "start map id" = The map ID that the start of the stage is in.
# "start spawn id" = The player spawn location ID for the start of the stage. This and "start map id" are both written
# to the previous stage's end loading zone to make it send the player to the next stage in the
# world's determined stage order.
# "mid region" = The Region that the stage's middle warp point is in. Used for connecting the warp Entrances after the
# starting stage to where they should be connecting to.
# "mid map id" = The map ID that the stage's middle warp point is in.
# "mid spawn id" = The player spawn location ID for the stage's middle warp point. This and "mid map id" are both
# written to the warp menu code to make it send the player to where it should be sending them.
# "end region" = The Region that the end of the stage is in. Used for connecting the next stage's beginning Entrance
# (if it exists) to the end of the previous one.
# "end map id" = The map ID that the end of the stage is in.
# "end spawn id" = The player spawn location ID for the end of the stage. This and "end map id" are both written to the
# next stage's beginning loading zone (if it exists) to make it send the player to the previous stage
# in the world's determined stage order.
# startzone map offset = The offset in the ROM to overwrite to change where the start of the stage leads.
# startzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the previous map the
# start of the stage puts the player at.
# endzone map offset = The offset in the ROM to overwrite to change where the end of the stage leads.
# endzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the next map the end of
# the stage puts the player at.
# altzone map offset = The offset in the ROM to overwrite to change where the alternate end of the stage leads
# (if it exists).
# altzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the next map the alternate
# end of the stage puts the player at.
# character = What character that stage is exclusively meant for normally. Used in determining what stages to leave out
# depending on what character stage setting was chosen in the player options.
# save number offsets = The offsets to overwrite to change what stage number is displayed on the save file when saving
# at the stage's White Jewels.
# regions = All Regions that make up the stage. If the stage is in the world's active stages, its Regions and their
# corresponding Locations and Entrances will all be created.
stage_info = {
"Forest of Silence": {
"start region": rname.forest_start, "start map id": 0x00, "start spawn id": 0x00,
"mid region": rname.forest_mid, "mid map id": 0x00, "mid spawn id": 0x04,
"end region": rname.forest_end, "end map id": 0x00, "end spawn id": 0x01,
"endzone map offset": 0xB6302F, "endzone spawn offset": 0xB6302B,
"save number offsets": [0x1049C5, 0x1049CD, 0x1049D5],
"regions": [rname.forest_start,
rname.forest_mid,
rname.forest_end]
},
"Castle Wall": {
"start region": rname.cw_start, "start map id": 0x02, "start spawn id": 0x00,
"mid region": rname.cw_start, "mid map id": 0x02, "mid spawn id": 0x07,
"end region": rname.cw_exit, "end map id": 0x02, "end spawn id": 0x10,
"endzone map offset": 0x109A5F, "endzone spawn offset": 0x109A61,
"save number offsets": [0x1049DD, 0x1049E5, 0x1049ED],
"regions": [rname.cw_start,
rname.cw_exit,
rname.cw_ltower]
},
"Villa": {
"start region": rname.villa_start, "start map id": 0x03, "start spawn id": 0x00,
"mid region": rname.villa_storeroom, "mid map id": 0x05, "mid spawn id": 0x04,
"end region": rname.villa_crypt, "end map id": 0x1A, "end spawn id": 0x03,
"endzone map offset": 0xD9DA3, "endzone spawn offset": 0x109E81,
"altzone map offset": 0xD9DAB, "altzone spawn offset": 0x109E81,
"save number offsets": [0x1049F5, 0x1049FD, 0x104A05, 0x104A0D],
"regions": [rname.villa_start,
rname.villa_main,
rname.villa_storeroom,
rname.villa_archives,
rname.villa_maze,
rname.villa_servants,
rname.villa_crypt]
},
"Tunnel": {
"start region": rname.tunnel_start, "start map id": 0x07, "start spawn id": 0x00,
"mid region": rname.tunnel_end, "mid map id": 0x07, "mid spawn id": 0x03,
"end region": rname.tunnel_end, "end map id": 0x07, "end spawn id": 0x11,
"endzone map offset": 0x109B4F, "endzone spawn offset": 0x109B51, "character": "Reinhardt",
"save number offsets": [0x104A15, 0x104A1D, 0x104A25, 0x104A2D],
"regions": [rname.tunnel_start,
rname.tunnel_end]
},
"Underground Waterway": {
"start region": rname.uw_main, "start map id": 0x08, "start spawn id": 0x00,
"mid region": rname.uw_main, "mid map id": 0x08, "mid spawn id": 0x03,
"end region": rname.uw_end, "end map id": 0x08, "end spawn id": 0x01,
"endzone map offset": 0x109B67, "endzone spawn offset": 0x109B69, "character": "Carrie",
"save number offsets": [0x104A35, 0x104A3D],
"regions": [rname.uw_main,
rname.uw_end]
},
"Castle Center": {
"start region": rname.cc_main, "start map id": 0x19, "start spawn id": 0x00,
"mid region": rname.cc_main, "mid map id": 0x0E, "mid spawn id": 0x03,
"end region": rname.cc_elev_top, "end map id": 0x0F, "end spawn id": 0x02,
"endzone map offset": 0x109CB7, "endzone spawn offset": 0x109CB9,
"altzone map offset": 0x109CCF, "altzone spawn offset": 0x109CD1,
"save number offsets": [0x104A45, 0x104A4D, 0x104A55, 0x104A5D, 0x104A65, 0x104A6D, 0x104A75],
"regions": [rname.cc_main,
rname.cc_torture_chamber,
rname.cc_library,
rname.cc_crystal,
rname.cc_elev_top]
},
"Duel Tower": {
"start region": rname.dt_main, "start map id": 0x13, "start spawn id": 0x00,
"startzone map offset": 0x109DA7, "startzone spawn offset": 0x109DA9,
"mid region": rname.dt_main, "mid map id": 0x13, "mid spawn id": 0x15,
"end region": rname.dt_main, "end map id": 0x13, "end spawn id": 0x01,
"endzone map offset": 0x109D8F, "endzone spawn offset": 0x109D91, "character": "Reinhardt",
"save number offsets": [0x104ACD],
"regions": [rname.dt_main]
},
"Tower of Execution": {
"start region": rname.toe_main, "start map id": 0x10, "start spawn id": 0x00,
"startzone map offset": 0x109D17, "startzone spawn offset": 0x109D19,
"mid region": rname.toe_main, "mid map id": 0x10, "mid spawn id": 0x02,
"end region": rname.toe_main, "end map id": 0x10, "end spawn id": 0x12,
"endzone map offset": 0x109CFF, "endzone spawn offset": 0x109D01, "character": "Reinhardt",
"save number offsets": [0x104A7D, 0x104A85],
"regions": [rname.toe_main,
rname.toe_ledge]
},
"Tower of Science": {
"start region": rname.tosci_start, "start map id": 0x12, "start spawn id": 0x00,
"startzone map offset": 0x109D77, "startzone spawn offset": 0x109D79,
"mid region": rname.tosci_conveyors, "mid map id": 0x12, "mid spawn id": 0x03,
"end region": rname.tosci_conveyors, "end map id": 0x12, "end spawn id": 0x04,
"endzone map offset": 0x109D5F, "endzone spawn offset": 0x109D61, "character": "Carrie",
"save number offsets": [0x104A95, 0x104A9D, 0x104AA5],
"regions": [rname.tosci_start,
rname.tosci_three_doors,
rname.tosci_conveyors,
rname.tosci_key3]
},
"Tower of Sorcery": {
"start region": rname.tosor_main, "start map id": 0x11, "start spawn id": 0x00,
"startzone map offset": 0x109D47, "startzone spawn offset": 0x109D49,
"mid region": rname.tosor_main, "mid map id": 0x11, "mid spawn id": 0x01,
"end region": rname.tosor_main, "end map id": 0x11, "end spawn id": 0x13,
"endzone map offset": 0x109D2F, "endzone spawn offset": 0x109D31, "character": "Carrie",
"save number offsets": [0x104A8D],
"regions": [rname.tosor_main]
},
"Room of Clocks": {
"start region": rname.roc_main, "start map id": 0x1B, "start spawn id": 0x00,
"mid region": rname.roc_main, "mid map id": 0x1B, "mid spawn id": 0x02,
"end region": rname.roc_main, "end map id": 0x1B, "end spawn id": 0x14,
"endzone map offset": 0x109EAF, "endzone spawn offset": 0x109EB1,
"save number offsets": [0x104AC5],
"regions": [rname.roc_main]
},
"Clock Tower": {
"start region": rname.ct_start, "start map id": 0x17, "start spawn id": 0x00,
"mid region": rname.ct_middle, "mid map id": 0x17, "mid spawn id": 0x02,
"end region": rname.ct_end, "end map id": 0x17, "end spawn id": 0x03,
"endzone map offset": 0x109E37, "endzone spawn offset": 0x109E39,
"save number offsets": [0x104AB5, 0x104ABD],
"regions": [rname.ct_start,
rname.ct_middle,
rname.ct_end]
},
"Castle Keep": {
"start region": rname.ck_main, "start map id": 0x14, "start spawn id": 0x02,
"mid region": rname.ck_main, "mid map id": 0x14, "mid spawn id": 0x03,
"end region": rname.ck_drac_chamber,
"save number offsets": [0x104AAD],
"regions": [rname.ck_main]
},
}
vanilla_stage_order = ("Forest of Silence", "Castle Wall", "Villa", "Tunnel", "Underground Waterway", "Castle Center",
"Duel Tower", "Tower of Execution", "Tower of Science", "Tower of Sorcery", "Room of Clocks",
"Clock Tower", "Castle Keep")
# # # KEY # # #
# "prev" = The previous stage in the line.
# "next" = The next stage in the line.
# "alt" = The alternate next stage in the line (if one exists).
# "position" = The stage's number in the order of stages.
# "path" = Character indicating whether the stage is on the main path or an alternate path, similar to Rondo of Blood.
# Used in writing the randomized stage order to the spoiler.
vanilla_stage_exits = {rname.forest_of_silence: {"prev": None, "next": rname.castle_wall,
"alt": None, "position": 1, "path": " "},
rname.castle_wall: {"prev": None, "next": rname.villa,
"alt": None, "position": 2, "path": " "},
rname.villa: {"prev": None, "next": rname.tunnel,
"alt": rname.underground_waterway, "position": 3, "path": " "},
rname.tunnel: {"prev": None, "next": rname.castle_center,
"alt": None, "position": 4, "path": " "},
rname.underground_waterway: {"prev": None, "next": rname.castle_center,
"alt": None, "position": 4, "path": "'"},
rname.castle_center: {"prev": None, "next": rname.duel_tower,
"alt": rname.tower_of_science, "position": 5, "path": " "},
rname.duel_tower: {"prev": rname.castle_center, "next": rname.tower_of_execution,
"alt": None, "position": 6, "path": " "},
rname.tower_of_execution: {"prev": rname.duel_tower, "next": rname.room_of_clocks,
"alt": None, "position": 7, "path": " "},
rname.tower_of_science: {"prev": rname.castle_center, "next": rname.tower_of_sorcery,
"alt": None, "position": 6, "path": "'"},
rname.tower_of_sorcery: {"prev": rname.tower_of_science, "next": rname.room_of_clocks,
"alt": None, "position": 7, "path": "'"},
rname.room_of_clocks: {"prev": None, "next": rname.clock_tower,
"alt": None, "position": 8, "path": " "},
rname.clock_tower: {"prev": None, "next": rname.castle_keep,
"alt": None, "position": 9, "path": " "},
rname.castle_keep: {"prev": None, "next": None,
"alt": None, "position": 10, "path": " "}}
def get_stage_info(stage: str, info: str) -> Union[str, int, Union[List[int], List[str]], None]:
return stage_info[stage].get(info, None)
def get_locations_from_stage(stage: str) -> List[str]:
overall_locations = []
for region in get_stage_info(stage, "regions"):
stage_locations = get_region_info(region, "locations")
if stage_locations is not None:
overall_locations += stage_locations
final_locations = []
for loc in overall_locations:
if get_location_info(loc, "code") is not None:
final_locations.append(loc)
return final_locations
def verify_character_stage(world: "CV64World", stage: str) -> bool:
# Verify a character stage is in the world if the given stage is a character stage.
stage_char = get_stage_info(stage, "character")
return stage_char is None or (world.reinhardt_stages and stage_char == "Reinhardt") or \
(world.carrie_stages and stage_char == "Carrie")
def get_normal_stage_exits(world: "CV64World") -> Dict[str, dict]:
exits = {name: vanilla_stage_exits[name].copy() for name in vanilla_stage_exits}
non_branching_pos = 1
for stage in stage_info:
# Remove character stages that are not enabled.
if not verify_character_stage(world, stage):
del exits[stage]
continue
# If branching pathways are not enabled, update the exit info to converge said stages on a single path.
if world.branching_stages:
continue
if world.carrie_stages and not world.reinhardt_stages and exits[stage]["alt"] is not None:
exits[stage]["next"] = exits[stage]["alt"]
elif world.carrie_stages and world.reinhardt_stages and stage != rname.castle_keep:
exits[stage]["next"] = vanilla_stage_order[vanilla_stage_order.index(stage) + 1]
exits[stage]["alt"] = None
exits[stage]["position"] = non_branching_pos
exits[stage]["path"] = " "
non_branching_pos += 1
return exits
def shuffle_stages(world: "CV64World", stage_1_blacklist: List[str]) \
-> Tuple[Dict[str, Dict[str, Union[str, int, None]]], str, List[str]]:
"""Woah, this is a lot! I should probably summarize what's happening in here, huh?
So, in the vanilla game, all the stages are basically laid out on a linear "timeline" with some stages being
different depending on who you are playing as. The different character stages, in question, are the one following
Villa and the two following Castle Center. The ends of these two stages are considered the route divergences and, in
this rando, the game's behavior has been changed in such that both characters can access each other's exclusive
stages (thereby making the entire game playable in just one character run). With this in mind, when shuffling the
stages around, there is one particularly big rule that must be kept in mind to ensure things don't get too wacky.
That being:
Villa and Castle Center cannot appear in branching path stage slots; they can only be on "main" path slots.
So for this reason, generating a new stage layout is not as simple as just scrambling a list of stages around. It
must be done in such a way that whatever stages directly follow Villa or CC is not the other stage. The exception is
if branching stages are not a thing at all due to the player settings, in which case everything I said above does
not matter. Consider the following representation of a stage "timeline", wherein each "-" represents a main stage
and a "=" represents a pair of branching stages:
-==---=---
In the above example, CC is the first "-" and Villa is the fourth. CC and Villa can only be "-"s whereas every other
stage can be literally anywhere, including on one of the "=" dashes. Villa will always be followed by one pair of
branching stages and CC will be followed by two pairs.
This code starts by first generating a singular list of stages that fit the criteria of Castle Center not being in
the next two entries following Villa and Villa not being in the next four entries after Castle Center. Once that has
been figured out, it will then generate a dictionary of stages with the appropriate information regarding what
stages come before and after them to then be used for Entrance creation as well as what position in the list they
are in for the purposes of the spoiler log and extended hint information.
I opted to use the Rondo of Blood "'" stage notation to represent Carrie stage slots specifically. If a main stage
with a backwards connection connects backwards into a pair of branching stages, it will be the non-"'" stage
(Reinhardt's) that it connects to. The Carrie stage slot cannot be accessed this way.
If anyone has any ideas or suggestions on how to improve this, I'd love to hear them! Because it's only going to get
uglier come Legacy of Darkness and Cornell's funny side route later on.
"""
starting_stage_value = world.options.starting_stage.value
# Verify the starting stage is valid. If it isn't, pick a stage at random.
if vanilla_stage_order[starting_stage_value] not in stage_1_blacklist and \
verify_character_stage(world, vanilla_stage_order[starting_stage_value]):
starting_stage = vanilla_stage_order[starting_stage_value]
else:
logging.warning(f"[{world.multiworld.player_name[world.player]}] {vanilla_stage_order[starting_stage_value]} "
f"cannot be the starting stage with the chosen settings. Picking a different stage instead...")
possible_stages = []
for stage in vanilla_stage_order:
if stage in world.active_stage_exits and stage != rname.castle_keep:
possible_stages.append(stage)
starting_stage = world.random.choice(possible_stages)
world.options.starting_stage.value = vanilla_stage_order.index(starting_stage)
remaining_stage_pool = [stage for stage in world.active_stage_exits]
remaining_stage_pool.remove(rname.castle_keep)
total_stages = len(remaining_stage_pool)
new_stage_order = []
villa_cc_ids = [2, 3]
alt_villa_stage = []
alt_cc_stages = []
# If there are branching stages, remove Villa and CC from the list and determine their placements first.
if world.branching_stages:
villa_cc_ids = world.random.sample(range(1, 5), 2)
remaining_stage_pool.remove(rname.villa)
remaining_stage_pool.remove(rname.castle_center)
# Remove the starting stage from the remaining pool if it's in there at this point.
if starting_stage in remaining_stage_pool:
remaining_stage_pool.remove(starting_stage)
# If Villa or CC is our starting stage, force its respective ID to be 0 and re-randomize the other.
if starting_stage == rname.villa:
villa_cc_ids[0] = 0
villa_cc_ids[1] = world.random.randint(1, 5)
elif starting_stage == rname.castle_center:
villa_cc_ids[1] = 0
villa_cc_ids[0] = world.random.randint(1, 5)
for i in range(total_stages):
# If we're on Villa or CC's ID while in branching stage mode, put the respective stage in the slot.
if world.branching_stages and i == villa_cc_ids[0] and rname.villa not in new_stage_order:
new_stage_order.append(rname.villa)
villa_cc_ids[1] += 2
elif world.branching_stages and i == villa_cc_ids[1] and rname.castle_center not in new_stage_order:
new_stage_order.append(rname.castle_center)
villa_cc_ids[0] += 4
else:
# If neither of the above are true, if we're looking at Stage 1, append the starting stage.
# Otherwise, draw a random stage from the active list and delete it from there.
if i == 0:
new_stage_order.append(starting_stage)
else:
new_stage_order.append(world.random.choice(remaining_stage_pool))
remaining_stage_pool.remove(new_stage_order[i])
# If we're looking at an alternate stage slot, put the stage in one of these lists to indicate it as such
if not world.branching_stages:
continue
if i - 2 >= 0:
if new_stage_order[i - 2] == rname.villa:
alt_villa_stage.append(new_stage_order[i])
if i - 3 >= 0:
if new_stage_order[i - 3] == rname.castle_center:
alt_cc_stages.append(new_stage_order[i])
if i - 4 >= 0:
if new_stage_order[i - 4] == rname.castle_center:
alt_cc_stages.append(new_stage_order[i])
new_stage_order.append(rname.castle_keep)
# Update the dictionary of stage exits
current_stage_number = 1
for i in range(len(new_stage_order)):
# Stage position number and alternate path indicator
world.active_stage_exits[new_stage_order[i]]["position"] = current_stage_number
if new_stage_order[i] in alt_villa_stage + alt_cc_stages:
world.active_stage_exits[new_stage_order[i]]["path"] = "'"
else:
world.active_stage_exits[new_stage_order[i]]["path"] = " "
# Previous stage
if world.active_stage_exits[new_stage_order[i]]["prev"]:
if i - 1 < 0:
world.active_stage_exits[new_stage_order[i]]["prev"] = "Menu"
elif world.branching_stages:
if new_stage_order[i - 1] == alt_villa_stage[0] or new_stage_order[i] == alt_villa_stage[0]:
world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 2]
elif new_stage_order[i - 1] == alt_cc_stages[1] or new_stage_order[i] == alt_cc_stages[0]:
world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 3]
else:
world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 1]
else:
world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 1]
# Next stage
if world.active_stage_exits[new_stage_order[i]]["next"]:
if world.branching_stages:
if new_stage_order[i + 1] == alt_villa_stage[0]:
world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 2]
current_stage_number -= 1
elif new_stage_order[i + 1] == alt_cc_stages[0]:
world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 3]
current_stage_number -= 2
else:
world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 1]
else:
world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 1]
# Alternate next stage
if world.active_stage_exits[new_stage_order[i]]["alt"]:
if world.branching_stages:
if new_stage_order[i] == rname.villa:
world.active_stage_exits[new_stage_order[i]]["alt"] = alt_villa_stage[0]
else:
world.active_stage_exits[new_stage_order[i]]["alt"] = alt_cc_stages[0]
else:
world.active_stage_exits[new_stage_order[i]]["alt"] = None
current_stage_number += 1
return world.active_stage_exits, starting_stage, new_stage_order
def generate_warps(world: "CV64World") -> List[str]:
# Create a list of warps from the active stage list. They are in a random order by default and will never
# include the starting stage.
possible_warps = [stage for stage in world.active_stage_list]
# Remove the starting stage from the possible warps.
del (possible_warps[0])
active_warp_list = world.random.sample(possible_warps, 7)
if world.options.warp_order == WarpOrder.option_seed_stage_order:
# Arrange the warps to be in the seed's stage order
new_list = world.active_stage_list.copy()
for warp in world.active_stage_list:
if warp not in active_warp_list:
new_list.remove(warp)
active_warp_list = new_list
elif world.options.warp_order == WarpOrder.option_vanilla_stage_order:
# Arrange the warps to be in the vanilla game's stage order
new_list = list(vanilla_stage_order)
for warp in vanilla_stage_order:
if warp not in active_warp_list:
new_list.remove(warp)
active_warp_list = new_list
# Insert the starting stage at the start of the warp list
active_warp_list.insert(0, world.active_stage_list[0])
return active_warp_list
def get_region_names(active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> List[str]:
region_names = []
for stage in active_stage_exits:
stage_regions = get_stage_info(stage, "regions")
for region in stage_regions:
region_names.append(region)
return region_names

View File

@ -0,0 +1,6 @@
from test.bases import WorldTestBase
class CV64TestBase(WorldTestBase):
game = "Castlevania 64"
player: int = 1

View File

@ -0,0 +1,250 @@
from . import CV64TestBase
class WarpTest(CV64TestBase):
options = {
"special1s_per_warp": 3,
"total_special1s": 21
}
def test_warps(self) -> None:
for i in range(1, 8):
self.assertFalse(self.can_reach_entrance(f"Warp {i}"))
self.collect([self.get_item_by_name("Special1")] * 2)
self.assertFalse(self.can_reach_entrance(f"Warp {i}"))
self.collect([self.get_item_by_name("Special1")] * 1)
self.assertTrue(self.can_reach_entrance(f"Warp {i}"))
class CastleWallTest(CV64TestBase):
options = {
"stage_shuffle": True,
"starting_stage": 1
}
def test_doors(self) -> None:
self.assertFalse(self.can_reach_entrance(f"Left Tower door"))
self.collect([self.get_item_by_name("Left Tower Key")] * 1)
self.assertTrue(self.can_reach_entrance(f"Left Tower door"))
class VillaTest(CV64TestBase):
options = {
"stage_shuffle": True,
"starting_stage": 2
}
def test_doors(self) -> None:
self.assertFalse(self.can_reach_entrance("To Storeroom door"))
self.collect([self.get_item_by_name("Storeroom Key")] * 1)
self.assertTrue(self.can_reach_entrance("To Storeroom door"))
self.assertFalse(self.can_reach_entrance("To Archives door"))
self.collect([self.get_item_by_name("Archives Key")] * 1)
self.assertTrue(self.can_reach_entrance("To Archives door"))
self.assertFalse(self.can_reach_entrance("To maze gate"))
self.assertFalse(self.can_reach_entrance("Copper door"))
self.collect([self.get_item_by_name("Garden Key")] * 1)
self.assertTrue(self.can_reach_entrance("To maze gate"))
self.assertFalse(self.can_reach_entrance("Copper door"))
self.collect([self.get_item_by_name("Copper Key")] * 1)
self.assertTrue(self.can_reach_entrance("Copper door"))
class CastleCenterTest(CV64TestBase):
options = {
"stage_shuffle": True,
"starting_stage": 5
}
def test_doors(self) -> None:
self.assertFalse(self.can_reach_entrance("Torture Chamber door"))
self.collect([self.get_item_by_name("Chamber Key")] * 1)
self.assertTrue(self.can_reach_entrance("Torture Chamber door"))
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
self.assertFalse(self.can_reach_entrance("Upper cracked wall"))
self.collect([self.get_item_by_name("Magical Nitro")] * 1)
self.assertFalse(self.can_reach_entrance("Upper cracked wall"))
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
self.collect([self.get_item_by_name("Mandragora")] * 1)
self.assertTrue(self.can_reach_entrance("Upper cracked wall"))
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
self.collect([self.get_item_by_name("Magical Nitro")] * 1)
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
self.collect([self.get_item_by_name("Mandragora")] * 1)
self.assertTrue(self.can_reach_entrance("Upper cracked wall"))
class ExecutionTest(CV64TestBase):
options = {
"stage_shuffle": True,
"starting_stage": 7
}
def test_doors(self) -> None:
self.assertFalse(self.can_reach_entrance("Execution gate"))
self.collect([self.get_item_by_name("Execution Key")] * 1)
self.assertTrue(self.can_reach_entrance("Execution gate"))
class ScienceTest(CV64TestBase):
options = {
"stage_shuffle": True,
"starting_stage": 8
}
def test_doors(self) -> None:
self.assertFalse(self.can_reach_entrance("Science Door 1"))
self.collect([self.get_item_by_name("Science Key1")] * 1)
self.assertTrue(self.can_reach_entrance("Science Door 1"))
self.assertFalse(self.can_reach_entrance("To Science Door 2"))
self.assertFalse(self.can_reach_entrance("Science Door 3"))
self.collect([self.get_item_by_name("Science Key2")] * 1)
self.assertTrue(self.can_reach_entrance("To Science Door 2"))
self.assertFalse(self.can_reach_entrance("Science Door 3"))
self.collect([self.get_item_by_name("Science Key3")] * 1)
self.assertTrue(self.can_reach_entrance("Science Door 3"))
class ClocktowerTest(CV64TestBase):
options = {
"stage_shuffle": True,
"starting_stage": 11
}
def test_doors(self) -> None:
self.assertFalse(self.can_reach_entrance("To Clocktower Door 1"))
self.assertFalse(self.can_reach_entrance("To Clocktower Door 2"))
self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
self.collect([self.get_item_by_name("Clocktower Key1")] * 1)
self.assertTrue(self.can_reach_entrance("To Clocktower Door 1"))
self.assertFalse(self.can_reach_entrance("To Clocktower Door 2"))
self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
self.collect([self.get_item_by_name("Clocktower Key2")] * 1)
self.assertTrue(self.can_reach_entrance("To Clocktower Door 2"))
self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
self.collect([self.get_item_by_name("Clocktower Key3")] * 1)
self.assertTrue(self.can_reach_entrance("Clocktower Door 3"))
class DraculaNoneTest(CV64TestBase):
options = {
"draculas_condition": 0,
"stage_shuffle": True,
"starting_stage": 5,
}
def test_dracula_none_condition(self) -> None:
self.assertFalse(self.can_reach_entrance("Dracula's door"))
self.collect([self.get_item_by_name("Left Tower Key"),
self.get_item_by_name("Garden Key"),
self.get_item_by_name("Copper Key"),
self.get_item_by_name("Science Key1"),
self.get_item_by_name("Science Key2"),
self.get_item_by_name("Science Key3"),
self.get_item_by_name("Clocktower Key1"),
self.get_item_by_name("Clocktower Key2"),
self.get_item_by_name("Clocktower Key3")] * 1)
self.assertFalse(self.can_reach_entrance("Dracula's door"))
self.collect([self.get_item_by_name("Special1")] * 7)
self.assertTrue(self.can_reach_entrance("Dracula's door"))
class DraculaSpecialTest(CV64TestBase):
options = {
"draculas_condition": 3
}
def test_dracula_special_condition(self) -> None:
self.assertFalse(self.can_reach_entrance("Clocktower Door 3"))
self.collect([self.get_item_by_name("Left Tower Key"),
self.get_item_by_name("Garden Key"),
self.get_item_by_name("Copper Key"),
self.get_item_by_name("Magical Nitro"),
self.get_item_by_name("Mandragora"),
self.get_item_by_name("Clocktower Key1"),
self.get_item_by_name("Clocktower Key2"),
self.get_item_by_name("Clocktower Key3")] * 2)
self.assertTrue(self.can_reach_entrance("Clocktower Door 3"))
self.assertFalse(self.can_reach_entrance("Dracula's door"))
self.collect([self.get_item_by_name("Special2")] * 19)
self.assertFalse(self.can_reach_entrance("Dracula's door"))
self.collect([self.get_item_by_name("Special2")] * 1)
self.assertTrue(self.can_reach_entrance("Dracula's door"))
class DraculaCrystalTest(CV64TestBase):
options = {
"draculas_condition": 1,
"stage_shuffle": True,
"starting_stage": 5,
"hard_logic": True
}
def test_dracula_crystal_condition(self) -> None:
self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
self.collect([self.get_item_by_name("Left Tower Key"),
self.get_item_by_name("Garden Key"),
self.get_item_by_name("Copper Key"),
self.get_item_by_name("Science Key1"),
self.get_item_by_name("Science Key2"),
self.get_item_by_name("Science Key3"),
self.get_item_by_name("Clocktower Key1"),
self.get_item_by_name("Clocktower Key2"),
self.get_item_by_name("Clocktower Key3")] * 1)
self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
self.collect([self.get_item_by_name("Special1")] * 7)
self.assertTrue(self.can_reach_entrance("Slope Jump to boss tower"))
self.assertFalse(self.can_reach_entrance("Dracula's door"))
self.collect([self.get_item_by_name("Magical Nitro"),
self.get_item_by_name("Mandragora")] * 1)
self.assertFalse(self.can_reach_entrance("Dracula's door"))
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
self.collect([self.get_item_by_name("Magical Nitro"),
self.get_item_by_name("Mandragora")] * 1)
self.assertTrue(self.can_reach_entrance("Lower sealed cracked wall"))
self.assertTrue(self.can_reach_entrance("Dracula's door"))
class DraculaBossTest(CV64TestBase):
options = {
"draculas_condition": 2,
"stage_shuffle": True,
"starting_stage": 5,
"hard_logic": True,
"bosses_required": 16
}
def test_dracula_boss_condition(self) -> None:
self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
self.collect([self.get_item_by_name("Left Tower Key"),
self.get_item_by_name("Garden Key"),
self.get_item_by_name("Copper Key"),
self.get_item_by_name("Science Key1"),
self.get_item_by_name("Science Key2"),
self.get_item_by_name("Science Key3"),
self.get_item_by_name("Clocktower Key1"),
self.get_item_by_name("Clocktower Key2"),
self.get_item_by_name("Clocktower Key3")] * 1)
self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower"))
self.collect([self.get_item_by_name("Special1")] * 7)
self.assertTrue(self.can_reach_entrance("Slope Jump to boss tower"))
self.assertFalse(self.can_reach_entrance("Dracula's door"))
self.collect([self.get_item_by_name("Magical Nitro"),
self.get_item_by_name("Mandragora")] * 1)
self.assertFalse(self.can_reach_entrance("Dracula's door"))
self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall"))
self.collect([self.get_item_by_name("Magical Nitro"),
self.get_item_by_name("Mandragora")] * 1)
self.assertTrue(self.can_reach_entrance("Lower sealed cracked wall"))
self.assertTrue(self.can_reach_entrance("Dracula's door"))
class LizardTest(CV64TestBase):
options = {
"stage_shuffle": True,
"draculas_condition": 2,
"starting_stage": 4
}
def test_lizard_man_trio(self) -> None:
self.assertTrue(self.can_reach_location("Underground Waterway: Lizard-man trio"))

95
worlds/cv64/text.py Normal file
View File

@ -0,0 +1,95 @@
from typing import Tuple
cv64_char_dict = {"\n": (0x01, 0), " ": (0x02, 4), "!": (0x03, 2), '"': (0x04, 5), "#": (0x05, 6), "$": (0x06, 5),
"%": (0x07, 8), "&": (0x08, 7), "'": (0x09, 4), "(": (0x0A, 3), ")": (0x0B, 3), "*": (0x0C, 4),
"+": (0x0D, 5), ",": (0x0E, 3), "-": (0x0F, 4), ".": (0x10, 3), "/": (0x11, 6), "0": (0x12, 5),
"1": (0x13, 3), "2": (0x14, 5), "3": (0x15, 4), "4": (0x16, 5), "5": (0x17, 5), "6": (0x18, 5),
"7": (0x19, 5), "8": (0x1A, 5), "9": (0x1B, 5), ":": (0x1C, 3), ";": (0x1D, 3), "<": (0x1E, 3),
"=": (0x1F, 4), ">": (0x20, 3), "?": (0x21, 5), "@": (0x22, 8), "A": (0x23, 7), "B": (0x24, 6),
"C": (0x25, 5), "D": (0x26, 7), "E": (0x27, 5), "F": (0x28, 6), "G": (0x29, 6), "H": (0x2A, 7),
"I": (0x2B, 3), "J": (0x2C, 3), "K": (0x2D, 6), "L": (0x2E, 6), "M": (0x2F, 8), "N": (0x30, 7),
"O": (0x31, 6), "P": (0x32, 6), "Q": (0x33, 8), "R": (0x34, 6), "S": (0x35, 5), "T": (0x36, 6),
"U": (0x37, 6), "V": (0x38, 7), "W": (0x39, 8), "X": (0x3A, 6), "Y": (0x3B, 7), "Z": (0x3C, 6),
"[": (0x3D, 3), "\\": (0x3E, 6), "]": (0x3F, 3), "^": (0x40, 6), "_": (0x41, 5), "a": (0x43, 5),
"b": (0x44, 6), "c": (0x45, 4), "d": (0x46, 6), "e": (0x47, 5), "f": (0x48, 5), "g": (0x49, 5),
"h": (0x4A, 6), "i": (0x4B, 3), "j": (0x4C, 3), "k": (0x4D, 6), "l": (0x4E, 3), "m": (0x4F, 8),
"n": (0x50, 6), "o": (0x51, 5), "p": (0x52, 5), "q": (0x53, 5), "r": (0x54, 4), "s": (0x55, 4),
"t": (0x56, 4), "u": (0x57, 5), "v": (0x58, 6), "w": (0x59, 8), "x": (0x5A, 5), "y": (0x5B, 5),
"z": (0x5C, 4), "{": (0x5D, 4), "|": (0x5E, 2), "}": (0x5F, 3), "`": (0x61, 4), "": (0x62, 3),
"": (0x63, 3), "~": (0x65, 3), "": (0x72, 3), "°": (0x73, 3), "": (0x74, 8)}
# [0] = CV64's in-game ID for that text character.
# [1] = How much space towards the in-game line length limit it contributes.
def cv64_string_to_bytearray(cv64text: str, a_advance: bool = False, append_end: bool = True) -> bytearray:
"""Converts a string into a bytearray following CV64's string format."""
text_bytes = bytearray(0)
for i, char in enumerate(cv64text):
if char == "\t":
text_bytes.extend([0xFF, 0xFF])
else:
if char in cv64_char_dict:
text_bytes.extend([0x00, cv64_char_dict[char][0]])
else:
text_bytes.extend([0x00, 0x41])
if a_advance:
text_bytes.extend([0xA3, 0x00])
if append_end:
text_bytes.extend([0xFF, 0xFF])
return text_bytes
def cv64_text_truncate(cv64text: str, textbox_len_limit: int) -> str:
"""Truncates a string at a given in-game text line length."""
line_len = 0
for i in range(len(cv64text)):
line_len += cv64_char_dict[cv64text[i]][1]
if line_len > textbox_len_limit:
return cv64text[0x00:i]
return cv64text
def cv64_text_wrap(cv64text: str, textbox_len_limit: int) -> Tuple[str, int]:
"""Rebuilds a string with some of its spaces replaced with newlines to ensure the text wraps properly in an in-game
textbox of a given length."""
words = cv64text.split(" ")
new_text = ""
line_len = 0
num_lines = 1
for i in range(len(words)):
word_len = 0
word_divider = " "
if line_len != 0:
line_len += 4
else:
word_divider = ""
for char in words[i]:
if char in cv64_char_dict:
line_len += cv64_char_dict[char][1]
word_len += cv64_char_dict[char][1]
else:
line_len += 5
word_len += 5
if word_len > textbox_len_limit or char in ["\n", "\t"]:
word_len = 0
line_len = 0
if num_lines < 4:
num_lines += 1
if line_len > textbox_len_limit:
word_divider = "\n"
line_len = word_len
if num_lines < 4:
num_lines += 1
new_text += word_divider + words[i]
return new_text, num_lines