mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-11 12:28:21 -07:00
@@ -643,13 +643,13 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
|
||||
ctx.username = server_url.username
|
||||
if server_url.password:
|
||||
ctx.password = server_url.password
|
||||
port = server_url.port or 38281
|
||||
|
||||
def reconnect_hint() -> str:
|
||||
return ", type /connect to reconnect" if ctx.server_address else ""
|
||||
|
||||
logger.info(f'Connecting to Archipelago server at {address}')
|
||||
try:
|
||||
port = server_url.port or 38281 # raises ValueError if invalid
|
||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
|
||||
ssl=get_ssl_context() if address.startswith("wss://") else None)
|
||||
if ctx.ui is not None:
|
||||
|
||||
33
Fill.py
33
Fill.py
@@ -198,10 +198,16 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
if multiworld.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
|
||||
f"Not all items placed. Game beatable anyway.\nCould not place:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}")
|
||||
else:
|
||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||
f"Unplaced items:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
||||
f"Unfilled locations:\n"
|
||||
f"{', '.join(str(location) for location in locations)}\n"
|
||||
f"Already placed {len(placements)}:\n"
|
||||
f"{', '.join(str(place) for place in placements)}")
|
||||
|
||||
item_pool.extend(unplaced_items)
|
||||
|
||||
@@ -273,8 +279,13 @@ def remaining_fill(multiworld: MultiWorld,
|
||||
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
|
||||
f"Unplaced items:\n"
|
||||
f"{', '.join(str(item) for item in unplaced_items)}\n"
|
||||
f"Unfilled locations:\n"
|
||||
f"{', '.join(str(location) for location in locations)}\n"
|
||||
f"Already placed {len(placements)}:\n"
|
||||
f"{', '.join(str(place) for place in placements)}")
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
@@ -457,7 +468,9 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
|
||||
f"Not enough locations for progression items. "
|
||||
f"There are {len(progitempool)} more progression items than there are available locations."
|
||||
)
|
||||
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
for location in lock_later:
|
||||
@@ -470,7 +483,9 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
|
||||
f"Not enough filler items for excluded locations. "
|
||||
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
|
||||
)
|
||||
|
||||
restitempool = filleritempool + usefulitempool
|
||||
|
||||
@@ -481,13 +496,13 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
|
||||
if unplaced or unfilled:
|
||||
logging.warning(
|
||||
f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}")
|
||||
items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item)
|
||||
locations_counter = Counter(location.player for location in multiworld.get_locations())
|
||||
items_counter.update(item.player for item in unplaced)
|
||||
locations_counter.update(location.player for location in unfilled)
|
||||
print_data = {"items": items_counter, "locations": locations_counter}
|
||||
logging.info(f'Per-Player counts: {print_data})')
|
||||
logging.info(f"Per-Player counts: {print_data})")
|
||||
|
||||
|
||||
def flood_items(multiworld: MultiWorld) -> None:
|
||||
|
||||
@@ -26,6 +26,7 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.generic import PlandoConnection
|
||||
from worlds import failed_world_loads
|
||||
|
||||
|
||||
def mystery_argparse():
|
||||
@@ -458,7 +459,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
|
||||
ret.game = get_choice("game", weights)
|
||||
if ret.game not in AutoWorldRegister.world_types:
|
||||
picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0]
|
||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
|
||||
if picks[0] in failed_world_loads:
|
||||
raise Exception(f"No functional world found to handle game {ret.game}. "
|
||||
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
|
||||
f"If so, it appears the world failed to initialize correctly.")
|
||||
raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? "
|
||||
f"Check your spelling or installation of that world.")
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ components.extend([
|
||||
# Functions
|
||||
Component("Open host.yaml", func=open_host_yaml),
|
||||
Component("Open Patch", func=open_patch),
|
||||
Component("Generate Template Settings", func=generate_yamls),
|
||||
Component("Generate Template Options", func=generate_yamls),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Browse Files", func=browse_files),
|
||||
|
||||
@@ -62,6 +62,9 @@ Currently, the following games are supported:
|
||||
* Kirby's Dream Land 3
|
||||
* Celeste 64
|
||||
* Zork Grand Inquisitor
|
||||
* Castlevania 64
|
||||
* A Short Hike
|
||||
* Yoshi's Island
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
@@ -83,9 +86,9 @@ We recognize that there is a strong community of incredibly smart people that ha
|
||||
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
|
||||
|
||||
## Running Archipelago
|
||||
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
|
||||
For most people, all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer, or AppImage for Linux-based systems.
|
||||
|
||||
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
|
||||
If you are a developer or are running on a platform with no compiled releases available, please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
|
||||
|
||||
## Related Repositories
|
||||
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
|
||||
|
||||
@@ -28,7 +28,7 @@ def check():
|
||||
results, _ = roll_options(options)
|
||||
if len(options) > 1:
|
||||
# offer combined file back
|
||||
combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
|
||||
combined_yaml = "\n---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
|
||||
for file_name, file_content in options.items())
|
||||
combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode()
|
||||
else:
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<td>{{ sc2_icon('Neosteel Bunker (Bunker)') }}</td>
|
||||
<td>{{ sc2_icon('Shrike Turret (Bunker)') }}</td>
|
||||
<td>{{ sc2_icon('Fortified Bunker (Bunker)') }}</td>
|
||||
<td colspan="3"></td>
|
||||
<td></td>
|
||||
<td>{{ sc2_icon('Missile Turret') }}</td>
|
||||
<td>{{ sc2_icon('Titanium Housing (Missile Turret)') }}</td>
|
||||
<td>{{ sc2_icon('Hellstorm Batteries (Missile Turret)') }}</td>
|
||||
@@ -121,12 +121,13 @@
|
||||
<td>{{ sc2_icon('Planetary Fortress') }}</td>
|
||||
<td {% if augmented_thrusters_planetary_fortress_level == 1 %}class="tint-terran"{% endif %}>{{ sc2_progressive_icon_with_custom_name('Progressive Augmented Thrusters (Planetary Fortress)', augmented_thrusters_planetary_fortress_url, augmented_thrusters_planetary_fortress_name) }}</td>
|
||||
<td>{{ sc2_icon('Advanced Targeting (Planetary Fortress)') }}</td>
|
||||
<td colspan="2"></td>
|
||||
<td>{{ sc2_icon('Micro-Filtering') }}</td>
|
||||
<td>{{ sc2_icon('Automated Refinery') }}</td>
|
||||
<td></td>
|
||||
<td>{{ sc2_icon('Advanced Construction (SCV)') }}</td>
|
||||
<td>{{ sc2_icon('Dual-Fusion Welders (SCV)') }}</td>
|
||||
<td></td>
|
||||
<td>{{ sc2_icon('Micro-Filtering') }}</td>
|
||||
<td>{{ sc2_icon('Automated Refinery') }}</td>
|
||||
<td>{{ sc2_icon('Hostile Environment Adaptation (SCV)') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ sc2_icon('Sensor Tower') }}</td>
|
||||
@@ -180,7 +181,7 @@
|
||||
<td>{{ sc2_icon('Nano Projector (Medic)') }}</td>
|
||||
<td colspan="6"></td>
|
||||
<td>{{ sc2_icon('Vulture') }}</td>
|
||||
<td>{{ sc2_progressive_icon_with_custom_name('Progressive Replenishable Magazine (Vulture)', replenishable_magazine_vulture_url, replenishable_magazine_vulture_name) }}</td>
|
||||
<td class="{{ sc2_tint_level(replenishable_magazine_vulture_level) }}">{{ sc2_progressive_icon_with_custom_name('Progressive Replenishable Magazine (Vulture)', replenishable_magazine_vulture_url, replenishable_magazine_vulture_name) }}</td>
|
||||
<td>{{ sc2_icon('Ion Thrusters (Vulture)') }}</td>
|
||||
<td>{{ sc2_icon('Auto Launchers (Vulture)') }}</td>
|
||||
<td>{{ sc2_icon('Auto-Repair (Vulture)') }}</td>
|
||||
@@ -293,7 +294,8 @@
|
||||
<td>{{ sc2_icon('HERC') }}</td>
|
||||
<td>{{ sc2_icon('Juggernaut Plating (HERC)') }}</td>
|
||||
<td>{{ sc2_icon('Kinetic Foam (HERC)') }}</td>
|
||||
<td colspan="5"></td>
|
||||
<td>{{ sc2_icon('Resource Efficiency (HERC)') }}</td>
|
||||
<td colspan="4"></td>
|
||||
<td>{{ sc2_icon('Widow Mine') }}</td>
|
||||
<td>{{ sc2_icon('Drilling Claws (Widow Mine)') }}</td>
|
||||
<td>{{ sc2_icon('Concealment (Widow Mine)') }}</td>
|
||||
|
||||
@@ -124,10 +124,13 @@ class TrackerData:
|
||||
@_cache_results
|
||||
def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter:
|
||||
"""Retrieves a dictionary of all items received by their id and their received count."""
|
||||
items = self.get_player_received_items(team, player)
|
||||
received_items = self.get_player_received_items(team, player)
|
||||
starting_items = self.get_player_starting_inventory(team, player)
|
||||
inventory = collections.Counter()
|
||||
for item in items:
|
||||
for item in received_items:
|
||||
inventory[item.item] += 1
|
||||
for item in starting_items:
|
||||
inventory[item] += 1
|
||||
|
||||
return inventory
|
||||
|
||||
@@ -358,10 +361,13 @@ def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]:
|
||||
def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
|
||||
game = tracker_data.get_player_game(team, player)
|
||||
|
||||
# Add received index to all received items, excluding starting inventory.
|
||||
received_items_in_order = {}
|
||||
for received_index, network_item in enumerate(tracker_data.get_player_received_items(team, player), start=1):
|
||||
received_items_in_order[network_item.item] = received_index
|
||||
starting_inventory = tracker_data.get_player_starting_inventory(team, player)
|
||||
for index, item in enumerate(starting_inventory):
|
||||
received_items_in_order[item] = index
|
||||
for index, network_item in enumerate(tracker_data.get_player_received_items(team, player),
|
||||
start=len(starting_inventory)):
|
||||
received_items_in_order[network_item.item] = index
|
||||
|
||||
return render_template(
|
||||
template_name_or_list="genericTracker.html",
|
||||
@@ -1606,6 +1612,7 @@ if "Starcraft 2" in network_data_package["games"]:
|
||||
"Hellstorm Batteries (Missile Turret)": github_icon_base_url + "blizzard/btn-ability-stetmann-corruptormissilebarrage.png",
|
||||
"Advanced Construction (SCV)": github_icon_base_url + "blizzard/btn-ability-mengsk-trooper-advancedconstruction.png",
|
||||
"Dual-Fusion Welders (SCV)": github_icon_base_url + "blizzard/btn-upgrade-swann-scvdoublerepair.png",
|
||||
"Hostile Environment Adaptation (SCV)": github_icon_base_url + "blizzard/btn-upgrade-swann-hellarmor.png",
|
||||
"Fire-Suppression System Level 1": organics_icon_base_url + "Fire-SuppressionSystem.png",
|
||||
"Fire-Suppression System Level 2": github_icon_base_url + "blizzard/btn-upgrade-swann-firesuppressionsystem.png",
|
||||
|
||||
@@ -1673,6 +1680,7 @@ if "Starcraft 2" in network_data_package["games"]:
|
||||
"Resource Efficiency (Spectre)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
|
||||
"Juggernaut Plating (HERC)": organics_icon_base_url + "JuggernautPlating.png",
|
||||
"Kinetic Foam (HERC)": organics_icon_base_url + "KineticFoam.png",
|
||||
"Resource Efficiency (HERC)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
|
||||
|
||||
"Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
|
||||
"Vulture": github_icon_base_url + "blizzard/btn-unit-terran-vulture.png",
|
||||
@@ -2333,12 +2341,12 @@ if "Starcraft 2" in network_data_package["games"]:
|
||||
"Progressive Zerg Armor Upgrade": 106 + SC2HOTS_ITEM_ID_OFFSET,
|
||||
"Progressive Zerg Ground Upgrade": 107 + SC2HOTS_ITEM_ID_OFFSET,
|
||||
"Progressive Zerg Flyer Upgrade": 108 + SC2HOTS_ITEM_ID_OFFSET,
|
||||
"Progressive Zerg Weapon/Armor Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Weapon Upgrade": 105 + SC2HOTS_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Armor Upgrade": 106 + SC2HOTS_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Ground Upgrade": 107 + SC2HOTS_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Air Upgrade": 108 + SC2HOTS_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Weapon/Armor Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET,
|
||||
"Progressive Zerg Weapon/Armor Upgrade": 109 + SC2HOTS_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Weapon Upgrade": 105 + SC2LOTV_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Armor Upgrade": 106 + SC2LOTV_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Ground Upgrade": 107 + SC2LOTV_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Air Upgrade": 108 + SC2LOTV_ITEM_ID_OFFSET,
|
||||
"Progressive Protoss Weapon/Armor Upgrade": 109 + SC2LOTV_ITEM_ID_OFFSET,
|
||||
}
|
||||
grouped_item_replacements = {
|
||||
"Progressive Terran Weapon Upgrade": ["Progressive Terran Infantry Weapon",
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
# Bumper Stickers
|
||||
/worlds/bumpstik/ @FelicitusNeko
|
||||
|
||||
# Castlevania 64
|
||||
/worlds/cv64/ @LiquidCat64
|
||||
|
||||
# Celeste 64
|
||||
/worlds/celeste64/ @PoryGone
|
||||
|
||||
@@ -131,6 +134,9 @@
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
|
||||
# A Short Hike
|
||||
/worlds/shorthike/ @chandler05
|
||||
|
||||
# Sonic Adventure 2 Battle
|
||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||
|
||||
@@ -171,7 +177,7 @@
|
||||
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
|
||||
|
||||
# TUNIC
|
||||
/worlds/tunic/ @silent-destroyer
|
||||
/worlds/tunic/ @silent-destroyer @ScipioWright
|
||||
|
||||
# Undertale
|
||||
/worlds/undertale/ @jonloveslegos
|
||||
@@ -185,6 +191,9 @@
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
# Yoshi's Island
|
||||
/worlds/yoshisisland/ @PinkSwitch
|
||||
|
||||
# Zillion
|
||||
/worlds/zillion/ @beauxq
|
||||
|
||||
|
||||
@@ -1,269 +1,78 @@
|
||||
# How do I add a game to Archipelago?
|
||||
|
||||
This guide is going to try and be a broad summary of how you can do just that.
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
Refer to the following documents as well:
|
||||
|
||||
- [network protocol.md](/docs/network%20protocol.md) for network communication between client and server.
|
||||
- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package.
|
||||
|
||||
# Game Modification
|
||||
|
||||
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
|
||||
typically done through a modding API or other modification process, described further down.
|
||||
|
||||
As an example, modifications to a game typically include (more on this later):
|
||||
|
||||
- Hooking into when a 'location check' is completed.
|
||||
- Networking with the Archipelago server.
|
||||
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
|
||||
|
||||
In order to determine how to modify a game, refer to the following sections.
|
||||
|
||||
## Engine Identification
|
||||
|
||||
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is
|
||||
critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s
|
||||
important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
|
||||
Examples are provided below.
|
||||
|
||||
### Creepy Castle
|
||||
|
||||

|
||||
|
||||
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case
|
||||
scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have
|
||||
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty
|
||||
nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other
|
||||
examples of game releases.
|
||||
|
||||
### Heavy Bullets
|
||||
|
||||

|
||||
|
||||
Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
|
||||
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
|
||||
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
|
||||
information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never
|
||||
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
|
||||
“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam.
|
||||
The directory “HEAVY_BULLETS_Data”, however, has some good news.
|
||||
|
||||

|
||||
|
||||
Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that
|
||||
what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which
|
||||
affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered,
|
||||
extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools
|
||||
and information to help you on your journey can be found at this
|
||||
[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking)
|
||||
|
||||
### Stardew Valley
|
||||
|
||||

|
||||
|
||||
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
|
||||
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good
|
||||
news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx
|
||||
and MonoMod.
|
||||
|
||||
### Gato Roboto
|
||||
|
||||

|
||||
|
||||
Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
|
||||
The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. For
|
||||
modifying GameMaker games the [Undertale Mod Tool](https://github.com/krzys-h/UndertaleModTool) is incredibly helpful.
|
||||
|
||||
This isn't all you'll ever see looking at game files, but it's a good place to start.
|
||||
As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
|
||||
This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
|
||||
advantage!
|
||||
|
||||
## Open or Leaked Source Games
|
||||
|
||||
As a side note, many games have either been made open source, or have had source files leaked at some point.
|
||||
This can be a boon to any would-be modder, for obvious reasons. Always be sure to check - a quick internet search for
|
||||
"(Game) Source Code" might not give results often, but when it does, you're going to have a much better time.
|
||||
|
||||
Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
|
||||
so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
|
||||
|
||||
## Modifying Release Versions of Games
|
||||
|
||||
However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install
|
||||
directory. Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
|
||||
but these are often not geared to the kind of work you'll be doing and may not help much.
|
||||
|
||||
As a general rule, any modding tool that lets you write actual code is something worth using.
|
||||
|
||||
### Research
|
||||
|
||||
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
|
||||
it's possible other motivated parties have concocted useful tools for your game already.
|
||||
Always be sure to search the Internet for the efforts of other modders.
|
||||
|
||||
### Other helpful tools
|
||||
|
||||
Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to
|
||||
existing game tools.
|
||||
|
||||
#### [CheatEngine](https://cheatengine.org/)
|
||||
|
||||
CheatEngine is a tool with a very long and storied history.
|
||||
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
|
||||
malware (because this behavior is most commonly found in malware and rarely used by other programs).
|
||||
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
|
||||
including binary data formats, addressing, and assembly language programming.
|
||||
|
||||
The tool itself is highly complex and even I have not yet charted its expanses.
|
||||
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
|
||||
modifying the actual game itself.
|
||||
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
|
||||
anything with it.
|
||||
|
||||
### What Modifications You Should Make to the Game
|
||||
|
||||
We talked about this briefly in [Game Modification](#game-modification) section.
|
||||
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
|
||||
|
||||
- Know when the player has checked a location, and react accordingly
|
||||
- Be able to receive items from the server on the fly
|
||||
- Keep an index for items received in order to resync from disconnections
|
||||
- Add interface for connecting to the Archipelago server with passwords and sessions
|
||||
- Add commands for manually rewarding, re-syncing, releasing, and other actions
|
||||
|
||||
Refer to the [Network Protocol documentation](/docs/network%20protocol.md) for how to communicate with Archipelago's
|
||||
servers.
|
||||
|
||||
## But my Game is a console game. Can I still add it?
|
||||
|
||||
That depends – what console?
|
||||
|
||||
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
|
||||
|
||||
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
|
||||
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
|
||||
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console
|
||||
games.
|
||||
|
||||
### My Game isn’t that old, it’s for the Wii/PS2/360/etc
|
||||
|
||||
This is very complex, but doable.
|
||||
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
|
||||
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
|
||||
|
||||
### My Game is a classic for the SNES/Sega Genesis/etc
|
||||
|
||||
That’s a lot more feasible.
|
||||
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
|
||||
community will have figured out the bulk of the console’s secrets.
|
||||
Look for debugging tools, but be ready to learn assembly.
|
||||
Old consoles usually have their own unique dialects of ASM you’ll need to get used to.
|
||||
|
||||
Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these
|
||||
older consoles to the Internet.
|
||||
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a
|
||||
computer, but these will require the same sort of interface software to be written in order to work properly; from your
|
||||
perspective the two won't really look any different.
|
||||
|
||||
### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that-
|
||||
|
||||
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
|
||||
Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be
|
||||
working from scratch.
|
||||
|
||||
## How to Distribute Game Modifications
|
||||
|
||||
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
|
||||
|
||||
This is a good way to get any project you're working on sued out from under you.
|
||||
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
|
||||
to copy them wholesale, is as patches.
|
||||
|
||||
There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that
|
||||
wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding
|
||||
the issue of distributing someone else’s original work.
|
||||
|
||||
Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play.
|
||||
|
||||
### Patches
|
||||
|
||||
#### IPS
|
||||
|
||||
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
|
||||
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
|
||||
fine.
|
||||
|
||||
#### UPS, BPS, VCDIFF (xdelta), bsdiff
|
||||
|
||||
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
|
||||
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
|
||||
compression, so this format is used by APBP.
|
||||
|
||||
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
|
||||
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
|
||||
|
||||
#### APBP Archipelago Binary Patch
|
||||
|
||||
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
|
||||
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
|
||||
bsdiff between the original and the randomized ROM.
|
||||
|
||||
To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`.
|
||||
|
||||
### Mod files
|
||||
|
||||
Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere.
|
||||
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
|
||||
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
|
||||
generated per seed. If at all possible, it's generally best practice to collect your world information from `slot_data`
|
||||
so that the users don't have to move files around in order to play.
|
||||
|
||||
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
|
||||
integration into the Webhost by inheriting from `worlds.Files.APContainer`.
|
||||
|
||||
## Archipelago Integration
|
||||
|
||||
In order for your game to communicate with the Archipelago server and generate the necessary randomized information,
|
||||
you must create a world package in the main Archipelago repo. This section will cover the requisites and expectations
|
||||
and show the basics of a world. More in depth documentation on the available API can be read in
|
||||
the [world api doc.](/docs/world%20api.md)
|
||||
For setting up your working environment with Archipelago refer
|
||||
to [running from source](/docs/running%20from%20source.md) and the [style guide](/docs/style.md).
|
||||
|
||||
### Requirements
|
||||
|
||||
A world implementation requires a few key things from its implementation
|
||||
|
||||
- A folder within `worlds` that contains an `__init__.py`
|
||||
- This is what defines it as a Python package and how it's able to be imported
|
||||
into Archipelago's generation system. During generation time only code that is
|
||||
defined within this file will be run. It's suggested to split up your information
|
||||
into more files to improve readability, but all of that information can be
|
||||
imported at its base level within your world.
|
||||
- A `World` subclass where you create your world and define all of its rules
|
||||
and the following requirements:
|
||||
- Your items and locations need a `item_name_to_id` and `location_name_to_id`,
|
||||
respectively, mapping.
|
||||
- An `option_definitions` mapping of your game options with the format
|
||||
`{name: Class}`, where `name` uses Python snake_case.
|
||||
- You must define your world's `create_item` method, because this may be called
|
||||
by the generator in certain circumstances
|
||||
- When creating your world you submit items and regions to the Multiworld.
|
||||
- These are lists of said objects which you can access at
|
||||
`self.multiworld.itempool` and `self.multiworld.regions`. Best practice for
|
||||
adding to these lists is with either `append` or `extend`, where `append` is a
|
||||
single object and `extend` is a list.
|
||||
- Do not use `=` as this will delete other worlds' items and regions.
|
||||
- Regions are containers for holding your world's Locations.
|
||||
- Locations are where players will "check" for items and must exist within
|
||||
a region. It's also important for your world's submitted items to be the same as
|
||||
its submitted locations count.
|
||||
- You must always have a "Menu" Region from which the generation algorithm
|
||||
uses to enter the game and access locations.
|
||||
- Make sure to check out [world maintainer.md](/docs/world%20maintainer.md) before publishing.
|
||||
# Adding Games
|
||||
|
||||
Adding a new game to Archipelago has two major parts:
|
||||
|
||||
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
|
||||
* Archipelago Generation and Server integration plugin (hereafter referred to as "world")
|
||||
|
||||
This document will attempt to illustrate the bare minimum requirements and expectations of both parts of a new world
|
||||
integration. As game modification wildly varies by system and engine, and has no bearing on the Archipelago protocol,
|
||||
it will not be detailed here.
|
||||
|
||||
## Client
|
||||
|
||||
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
||||
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
||||
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
|
||||
to behave as expected are:
|
||||
|
||||
* Handle both secure and unsecure websocket connections
|
||||
* Detect and react when a location has been "checked" by the player by sending a network packet to the server
|
||||
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
|
||||
demand
|
||||
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
|
||||
normally expect from features such as starting inventory, item link replacement, or item cheating
|
||||
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
|
||||
a player or location attributed to them
|
||||
* Be able to change the port for saved connection info
|
||||
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
|
||||
privilege can be lost, requiring the room to be moved to a new port
|
||||
* Reconnect if the connection is unstable and lost while playing
|
||||
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
|
||||
order.
|
||||
* Receive items that were sent to the player while they were not connected to the server
|
||||
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
|
||||
strictly required
|
||||
* Send a status update packet alerting the server that the player has completed their goal
|
||||
|
||||
Libraries for most modern languages and the spec for various packets can be found in the
|
||||
[network protocol](/docs/network%20protocol.md) API reference document.
|
||||
|
||||
## World
|
||||
|
||||
The world is your game integration for the Archipelago generator, webhost, and multiworld server. It contains all the
|
||||
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
||||
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
||||
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
||||
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
|
||||
following requirements:
|
||||
|
||||
* A folder within `/worlds/` that contains an `__init__.py`
|
||||
* A `World` subclass where you create your world and define all of its rules
|
||||
* A unique game name
|
||||
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
|
||||
definition
|
||||
* The game_info doc must follow the format `{language_code}_{game_name}.md`
|
||||
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
|
||||
`item_name_to_id` and `location_name_to_id`, respectively.
|
||||
* Create an item when `create_item` is called both by your code and externally
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* A `Region` for your player with the name "Menu" to start from
|
||||
* Create a non-zero number of locations and add them to your regions
|
||||
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
|
||||
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
|
||||
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
|
||||
|
||||
Notable caveats:
|
||||
* The "Menu" region will always be considered the "start" for the player
|
||||
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
||||
start of the game from anywhere
|
||||
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
|
||||
`append`, `extend`, or `+=`. **Do not use `=`**
|
||||
* Regions are simply containers for locations that share similar access rules. They do not have to map to
|
||||
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
|
||||
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
|
||||
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md).
|
||||
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
@@ -31,6 +31,9 @@ There are also a number of community-supported libraries available that implemen
|
||||
| GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | |
|
||||
|
||||
## Synchronizing Items
|
||||
After a client connects, it will receive all previously collected items for its associated slot in a [ReceivedItems](#ReceivedItems) packet. This will include items the client may have already processed in a previous play session.
|
||||
To ensure the client is able to reject those items if it needs to, each item in the packet has an associated `index` argument. You will need to find a way to save the "last processed item index" to the player's local savegame, a local file, or something to that effect. Before connecting, you should load that "last processed item index" value and compare against it in your received items handling.
|
||||
|
||||
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
|
||||
|
||||
Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption.
|
||||
|
||||
@@ -10,10 +10,9 @@ Archipelago will be abbreviated as "AP" from now on.
|
||||
## Option Definitions
|
||||
Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you
|
||||
need to create:
|
||||
- A new option class with a docstring detailing what the option will do to your user.
|
||||
- A `display_name` to be displayed on the webhost.
|
||||
- A new entry in the `option_definitions` dict for your World.
|
||||
By style and convention, the internal names should be snake_case.
|
||||
- A new option class, with a docstring detailing what the option does, to be exposed to the user.
|
||||
- A new entry in the `options_dataclass` definition for your World.
|
||||
By style and convention, the dataclass attributes should be `snake_case`.
|
||||
|
||||
### Option Creation
|
||||
- If the option supports having multiple sub_options, such as Choice options, these can be defined with
|
||||
@@ -43,7 +42,7 @@ from Options import Toggle, Range, Choice, PerGameCommonOptions
|
||||
|
||||
class StartingSword(Toggle):
|
||||
"""Adds a sword to your starting inventory."""
|
||||
display_name = "Start With Sword"
|
||||
display_name = "Start With Sword" # this is the option name as it's displayed to the user on the webhost and in the spoiler log
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
@@ -204,7 +203,7 @@ For example:
|
||||
```python
|
||||
range_start = 1
|
||||
range_end = 99
|
||||
special_range_names: {
|
||||
special_range_names = {
|
||||
"normal": 20,
|
||||
"extreme": 99,
|
||||
"unlimited": -1,
|
||||
|
||||
@@ -169,6 +169,11 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Ar
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
@@ -184,6 +189,11 @@ Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apyi"; ValueData: "{#MyAppName}yipatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archipelago Yoshi's Island Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
6
kvui.py
6
kvui.py
@@ -705,6 +705,12 @@ class HintLog(RecycleView):
|
||||
def hint_sorter(element: dict) -> str:
|
||||
return ""
|
||||
|
||||
def fix_heights(self):
|
||||
"""Workaround fix for divergent texture and layout heights"""
|
||||
for element in self.children[0].children:
|
||||
max_height = max(child.texture_size[1] for child in element.children)
|
||||
element.height = max_height
|
||||
|
||||
|
||||
class E(ExceptionHandler):
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
# What is this file?
|
||||
# This file contains options which allow you to configure your multiworld experience while allowing others
|
||||
# to play how they want as well.
|
||||
|
||||
# How do I use it?
|
||||
# The options in this file are weighted. This means the higher number you assign to a value, the more
|
||||
# chances you have for that option to be chosen. For example, an option like this:
|
||||
#
|
||||
# map_shuffle:
|
||||
# on: 5
|
||||
# off: 15
|
||||
#
|
||||
# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off
|
||||
|
||||
# I've never seen a file like this before. What characters am I allowed to use?
|
||||
# This is a .yaml file. You are allowed to use most characters.
|
||||
# To test if your yaml is valid or not, you can use this website:
|
||||
# http://www.yamllint.com/
|
||||
|
||||
description: Template Name # Used to describe your yaml. Useful if you have multiple files
|
||||
name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
#{player} will be replaced with the player's slot number.
|
||||
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
|
||||
#{number} will be replaced with the counter value of the name.
|
||||
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
|
||||
game: # Pick a game to play
|
||||
A Link to the Past: 1
|
||||
requires:
|
||||
version: 0.4.4 # Version of Archipelago required for this yaml to work as expected.
|
||||
A Link to the Past:
|
||||
progression_balancing:
|
||||
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
# A lower setting means more getting stuck. A higher setting means less getting stuck.
|
||||
#
|
||||
# You can define additional values between the minimum and maximum values.
|
||||
# Minimum value is 0
|
||||
# Maximum value is 99
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
disabled: 0 # equivalent to 0
|
||||
normal: 50 # equivalent to 50
|
||||
extreme: 0 # equivalent to 99
|
||||
|
||||
accessibility:
|
||||
# Set rules for reachability of your items/locations.
|
||||
# Locations: ensure everything can be reached and acquired.
|
||||
# Items: ensure all logically relevant items can be acquired.
|
||||
# Minimal: ensure what is needed to reach your goal can be acquired.
|
||||
locations: 0
|
||||
items: 50
|
||||
minimal: 0
|
||||
|
||||
local_items:
|
||||
# Forces these items to be in their native world.
|
||||
[ ]
|
||||
|
||||
non_local_items:
|
||||
# Forces these items to be outside their native world.
|
||||
[ ]
|
||||
|
||||
start_inventory:
|
||||
# Start with these items.
|
||||
{ }
|
||||
|
||||
start_hints:
|
||||
# Start with these item's locations prefilled into the !hint command.
|
||||
[ ]
|
||||
|
||||
start_location_hints:
|
||||
# Start with these locations and their item prefilled into the !hint command
|
||||
[ ]
|
||||
|
||||
exclude_locations:
|
||||
# Prevent these locations from having an important item
|
||||
[ ]
|
||||
|
||||
priority_locations:
|
||||
# Prevent these locations from having an unimportant item
|
||||
[ ]
|
||||
|
||||
item_links:
|
||||
# Share part of your item pool with other players.
|
||||
[ ]
|
||||
|
||||
### Logic Section ###
|
||||
glitches_required: # Determine the logic required to complete the seed
|
||||
none: 50 # No glitches required
|
||||
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
|
||||
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
|
||||
hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons.
|
||||
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
|
||||
# Other players items are placed into your world under HMG logic
|
||||
dark_room_logic: # Logic for unlit dark rooms
|
||||
lamp: 50 # require the Lamp for these rooms to be considered accessible.
|
||||
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
|
||||
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
|
||||
restrict_dungeon_item_on_boss: # aka ambrosia boss items
|
||||
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
|
||||
off: 50
|
||||
### End of Logic Section ###
|
||||
bigkey_shuffle: # Big Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
smallkey_shuffle: # Small Key Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
universal: 0
|
||||
start_with: 0
|
||||
key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies
|
||||
off: 50
|
||||
on: 0
|
||||
compass_shuffle: # Compass Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
map_shuffle: # Map Placement
|
||||
original_dungeon: 50
|
||||
own_dungeons: 0
|
||||
own_world: 0
|
||||
any_world: 0
|
||||
different_world: 0
|
||||
start_with: 0
|
||||
dungeon_counters:
|
||||
on: 0 # Always display amount of items checked in a dungeon
|
||||
pickup: 50 # Show when compass is picked up
|
||||
default: 0 # Show when compass is picked up if the compass itself is shuffled
|
||||
off: 0 # Never show item count in dungeons
|
||||
progressive: # Enable or disable progressive items (swords, shields, bow)
|
||||
on: 50 # All items are progressive
|
||||
off: 0 # No items are progressive
|
||||
grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
|
||||
entrance_shuffle:
|
||||
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
|
||||
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
|
||||
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
|
||||
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
|
||||
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
|
||||
restricted: 0 # Less strict than simple
|
||||
full: 0 # Less strict than restricted
|
||||
crossed: 0 # Less strict than full
|
||||
insanity: 0 # Very few grouping rules. Good luck
|
||||
# you can also define entrance shuffle seed, like so:
|
||||
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
|
||||
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
|
||||
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
|
||||
goals:
|
||||
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||
crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
|
||||
bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
|
||||
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
|
||||
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
|
||||
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
|
||||
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
|
||||
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
||||
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
||||
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
|
||||
open_pyramid:
|
||||
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
|
||||
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
|
||||
open: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
|
||||
closed: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
|
||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||
available: 50 # available = triforce_pieces_available
|
||||
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
0: 0
|
||||
5: 50
|
||||
10: 50
|
||||
15: 0
|
||||
20: 0
|
||||
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
|
||||
# Format "pieces: chance"
|
||||
100: 0 #No extra
|
||||
150: 50 #Half the required will be added as extra
|
||||
200: 0 #There are the double of the required ones available.
|
||||
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
25: 0
|
||||
30: 50
|
||||
40: 0
|
||||
50: 0
|
||||
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
||||
# Format "pieces: chance"
|
||||
15: 0
|
||||
20: 50
|
||||
30: 0
|
||||
40: 0
|
||||
50: 0
|
||||
crystals_needed_for_gt: # Crystals required to open GT
|
||||
0: 0
|
||||
7: 50
|
||||
random: 0
|
||||
random-low: 0 # any valid number, weighted towards the lower end
|
||||
random-middle: 0 # any valid number, weighted towards the central range
|
||||
random-high: 0 # any valid number, weighted towards the higher end
|
||||
crystals_needed_for_ganon: # Crystals required to hurt Ganon
|
||||
0: 0
|
||||
7: 50
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-middle: 0
|
||||
random-high: 0
|
||||
mode:
|
||||
standard: 0 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
|
||||
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
|
||||
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
|
||||
retro_bow:
|
||||
on: 0 # Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees.
|
||||
off: 50
|
||||
retro_caves:
|
||||
on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion.
|
||||
off: 50
|
||||
hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
|
||||
'on': 50
|
||||
'off': 0
|
||||
full: 0
|
||||
scams: # If on, these Merchants will no longer tell you what they're selling.
|
||||
'off': 50
|
||||
'king_zora': 0
|
||||
'bottle_merchant': 0
|
||||
'all': 0
|
||||
swordless:
|
||||
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
|
||||
off: 1
|
||||
item_pool:
|
||||
easy: 0 # Doubled upgrades, progressives, and etc
|
||||
normal: 50 # Item availability remains unchanged from vanilla game
|
||||
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
|
||||
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
|
||||
item_functionality:
|
||||
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
|
||||
normal: 50 # Vanilla item functionality
|
||||
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
|
||||
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
|
||||
tile_shuffle: # Randomize the tile layouts in flying tile rooms
|
||||
on: 0
|
||||
off: 50
|
||||
misery_mire_medallion: # required medallion to open Misery Mire front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
|
||||
random: 50
|
||||
Ether: 0
|
||||
Bombos: 0
|
||||
Quake: 0
|
||||
### Enemizer Section ###
|
||||
boss_shuffle:
|
||||
none: 50 # Vanilla bosses
|
||||
basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
|
||||
full: 0 # 3 bosses can occur twice
|
||||
chaos: 0 # Any boss can appear any amount of times
|
||||
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
|
||||
enemy_shuffle: # Randomize enemy placement
|
||||
on: 0
|
||||
off: 50
|
||||
killable_thieves: # Make thieves killable
|
||||
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
|
||||
off: 50
|
||||
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
|
||||
on: 0
|
||||
off: 50
|
||||
enemy_damage:
|
||||
default: 50 # Vanilla enemy damage
|
||||
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
|
||||
chaos: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
|
||||
enemy_health:
|
||||
default: 50 # Vanilla enemy HP
|
||||
easy: 0 # Enemies have reduced health
|
||||
hard: 0 # Enemies have increased health
|
||||
expert: 0 # Enemies have greatly increased health
|
||||
pot_shuffle:
|
||||
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
|
||||
'off': 50 # Default pot item locations
|
||||
### End of Enemizer Section ###
|
||||
### Beemizer ###
|
||||
# can add weights for any whole number between 0 and 100
|
||||
beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
|
||||
0: 50 # No junk fill items are replaced (Beemizer is off)
|
||||
25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
|
||||
100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees
|
||||
beemizer_trap_chance:
|
||||
60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
|
||||
70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
|
||||
80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee
|
||||
90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee
|
||||
100: 0 # All beemizer replacements are traps
|
||||
### Shop Settings ###
|
||||
shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
|
||||
0: 50
|
||||
5: 0
|
||||
15: 0
|
||||
30: 0
|
||||
random: 0 # 0 to 30 evenly distributed
|
||||
shop_price_modifier: # Percentage modifier for shuffled item prices in shops
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
400: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
100: 50
|
||||
shop_shuffle:
|
||||
none: 50
|
||||
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
|
||||
f: 0 # Generate new default inventories for every shop independently
|
||||
i: 0 # Shuffle default inventories of the shops around
|
||||
p: 0 # Randomize the prices of the items in shop inventories
|
||||
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
|
||||
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
|
||||
P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees
|
||||
ip: 0 # Shuffle inventories and randomize prices
|
||||
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
|
||||
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
|
||||
# You can add more combos
|
||||
### End of Shop Section ###
|
||||
shuffle_prizes: # aka drops
|
||||
none: 0 # do not shuffle prize packs
|
||||
g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
|
||||
b: 0 # shuffle "bonk" prize packs
|
||||
bg: 0 # shuffle both
|
||||
timer:
|
||||
none: 50 # No timer will be displayed.
|
||||
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
|
||||
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
|
||||
ohko: 0 # Timer always at zero. Permanent OHKO.
|
||||
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
|
||||
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
|
||||
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
|
||||
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
|
||||
10: 50
|
||||
20: 0
|
||||
30: 0
|
||||
60: 0
|
||||
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
|
||||
-2: 50
|
||||
1: 0
|
||||
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
|
||||
1: 0
|
||||
2: 50
|
||||
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
|
||||
4: 50
|
||||
10: 0
|
||||
15: 0
|
||||
glitch_boots:
|
||||
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
|
||||
off: 0
|
||||
# rom options section
|
||||
random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below.
|
||||
enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool)
|
||||
on: 0
|
||||
off: 1
|
||||
on_hit: # Random sprite on hit. Being hit by things that cause 0 damage still counts.
|
||||
on: 1
|
||||
off: 0
|
||||
on_enter: # Random sprite on underworld entry. Note that entering hobo counts.
|
||||
on: 0
|
||||
off: 1
|
||||
on_exit: # Random sprite on underworld exit. Exiting hobo does not count.
|
||||
on: 0
|
||||
off: 1
|
||||
on_slash: # Random sprite on sword slash. Note, it still counts if you attempt to slash while swordless.
|
||||
on: 0
|
||||
off: 1
|
||||
on_item: # Random sprite on getting an item. Anything that causes you to hold an item above your head counts.
|
||||
on: 0
|
||||
off: 1
|
||||
on_bonk: # Random sprite on bonk.
|
||||
on: 0
|
||||
off: 1
|
||||
on_everything: # Random sprite on ALL currently implemented events, even if not documented at present time.
|
||||
on: 0
|
||||
off: 1
|
||||
use_weighted_sprite_pool: # Always on if no sprite_pool exists, otherwise it controls whether to use sprite as a weighted sprite pool
|
||||
on: 0
|
||||
off: 1
|
||||
#sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this.
|
||||
# - link
|
||||
# - pride link
|
||||
# - penguin link
|
||||
# - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool.
|
||||
sprite: # Enter the name of your preferred sprite and weight it appropriately
|
||||
random: 0
|
||||
randomonhit: 0 # Random sprite on hit
|
||||
randomonenter: 0 # Random sprite on entering the underworld.
|
||||
randomonexit: 0 # Random sprite on exiting the underworld.
|
||||
randomonslash: 0 # Random sprite on sword slashes
|
||||
randomonitem: 0 # Random sprite on getting items.
|
||||
randomonbonk: 0 # Random sprite on bonk.
|
||||
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
|
||||
randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
|
||||
Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
|
||||
music: # If "off", all in-game music will be disabled
|
||||
on: 50
|
||||
off: 0
|
||||
quickswap: # Enable switching items by pressing the L+R shoulder buttons
|
||||
on: 50
|
||||
off: 0
|
||||
triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala
|
||||
normal: 0 # original behavior (always visible)
|
||||
hide_goal: 50 # hide counter until a piece is collected or speaking to Murahadala
|
||||
hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala
|
||||
hide_both: 0 # Hide both under above circumstances
|
||||
reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more.
|
||||
on: 50
|
||||
off: 0
|
||||
menuspeed: # Controls how fast the item menu opens and closes
|
||||
normal: 50
|
||||
instant: 0
|
||||
double: 0
|
||||
triple: 0
|
||||
quadruple: 0
|
||||
half: 0
|
||||
heartcolor: # Controls the color of your health hearts
|
||||
red: 50
|
||||
blue: 0
|
||||
green: 0
|
||||
yellow: 0
|
||||
random: 0
|
||||
heartbeep: # Controls the frequency of the low-health beeping
|
||||
double: 0
|
||||
normal: 50
|
||||
half: 0
|
||||
quarter: 0
|
||||
off: 0
|
||||
ow_palettes: # Change the colors of the overworld
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
uw_palettes: # Change the colors of caves and dungeons
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
hud_palettes: # Change the colors of the hud
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
sword_palettes: # Change the colors of swords
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
shield_palettes: # Change the colors of shields
|
||||
default: 50 # No changes
|
||||
good: 0 # Shuffle the colors, with harmony in mind
|
||||
blackout: 0 # everything black / blind mode
|
||||
grayscale: 0
|
||||
negative: 0
|
||||
classic: 0
|
||||
dizzy: 0
|
||||
sick: 0
|
||||
puke: 0
|
||||
|
||||
# triggers that replace options upon rolling certain options
|
||||
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
|
||||
trigger_disabled: 50
|
||||
randomized: 0 # Swords are placed randomly throughout the world
|
||||
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
|
||||
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
||||
swordless: 0 # swordless mode
|
||||
|
||||
death_link:
|
||||
false: 50
|
||||
true: 0
|
||||
|
||||
allow_collect: # Allows for !collect / co-op to auto-open chests containing items for other players.
|
||||
# Off by default, because it currently crashes on real hardware.
|
||||
false: 50
|
||||
true: 0
|
||||
|
||||
linked_options:
|
||||
- name: crosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
A Link to the Past:
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
percentage: 0 # Set this to the percentage chance you want crosskeys
|
||||
- name: localcrosskeys
|
||||
options: # These overwrite earlier options if the percentage chance triggers
|
||||
A Link to the Past:
|
||||
entrance_shuffle: crossed
|
||||
bigkey_shuffle: true
|
||||
compass_shuffle: true
|
||||
map_shuffle: true
|
||||
smallkey_shuffle: true
|
||||
local_items: # Forces keys to be local to your own world
|
||||
- "Small Keys"
|
||||
- "Big Keys"
|
||||
percentage: 0 # Set this to the percentage chance you want local crosskeys
|
||||
- name: enemizer
|
||||
options:
|
||||
A Link to the Past:
|
||||
boss_shuffle: # Subchances can be injected too, which then get rolled
|
||||
basic: 1
|
||||
full: 1
|
||||
chaos: 1
|
||||
singularity: 1
|
||||
enemy_damage:
|
||||
shuffled: 1
|
||||
chaos: 1
|
||||
enemy_health:
|
||||
easy: 1
|
||||
hard: 1
|
||||
expert: 1
|
||||
percentage: 0 # Set this to the percentage chance you want enemizer
|
||||
triggers:
|
||||
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
|
||||
- option_name: legacy_weapons
|
||||
option_result: randomized
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
- option_name: legacy_weapons
|
||||
option_result: assured
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
start_inventory:
|
||||
Progressive Sword: 1
|
||||
- option_name: legacy_weapons
|
||||
option_result: vanilla
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
plando_items:
|
||||
- items:
|
||||
Progressive Sword: 4
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Pyramid Fairy - Left
|
||||
- Blacksmith
|
||||
- Link's Uncle
|
||||
- option_name: legacy_weapons
|
||||
option_result: swordless
|
||||
option_category: A Link to the Past
|
||||
options:
|
||||
A Link to the Past:
|
||||
swordless: on
|
||||
# end of legacy weapons block
|
||||
- option_name: enemy_damage # targets enemy_damage
|
||||
option_category: A Link to the Past
|
||||
option_result: shuffled # if it rolls shuffled
|
||||
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
|
||||
options: # then inserts these options
|
||||
A Link to the Past:
|
||||
swordless: off
|
||||
@@ -11,4 +11,4 @@ certifi>=2023.11.17
|
||||
cython>=3.0.8
|
||||
cymem>=2.0.8
|
||||
orjson>=3.9.10
|
||||
typing-extensions>=4.7.0
|
||||
typing_extensions>=4.7.0
|
||||
|
||||
288
worlds/Files.py
288
worlds/Files.py
@@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
import abc
|
||||
import json
|
||||
import zipfile
|
||||
from enum import IntEnum
|
||||
import os
|
||||
import threading
|
||||
|
||||
from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO
|
||||
from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload, Sequence
|
||||
|
||||
import bsdiff4
|
||||
|
||||
@@ -38,6 +39,34 @@ class AutoPatchRegister(abc.ABCMeta):
|
||||
return None
|
||||
|
||||
|
||||
class AutoPatchExtensionRegister(abc.ABCMeta):
|
||||
extension_types: ClassVar[Dict[str, AutoPatchExtensionRegister]] = {}
|
||||
required_extensions: Tuple[str, ...] = ()
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchExtensionRegister:
|
||||
# construct class
|
||||
new_class = super().__new__(mcs, name, bases, dct)
|
||||
if "game" in dct:
|
||||
AutoPatchExtensionRegister.extension_types[dct["game"]] = new_class
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
def get_handler(game: Optional[str]) -> Union[AutoPatchExtensionRegister, List[AutoPatchExtensionRegister]]:
|
||||
if not game:
|
||||
return APPatchExtension
|
||||
handler = AutoPatchExtensionRegister.extension_types.get(game, APPatchExtension)
|
||||
if handler.required_extensions:
|
||||
handlers = [handler]
|
||||
for required in handler.required_extensions:
|
||||
ext = AutoPatchExtensionRegister.extension_types.get(required)
|
||||
if not ext:
|
||||
raise NotImplementedError(f"No handler for {required}.")
|
||||
handlers.append(ext)
|
||||
return handlers
|
||||
else:
|
||||
return handler
|
||||
|
||||
|
||||
container_version: int = 6
|
||||
|
||||
|
||||
@@ -157,27 +186,14 @@ class APAutoPatchInterface(APPatch, abc.ABC, metaclass=AutoPatchRegister):
|
||||
""" create the output file with the file name `target` """
|
||||
|
||||
|
||||
class APDeltaPatch(APAutoPatchInterface):
|
||||
"""An implementation of `APAutoPatchInterface` that additionally
|
||||
has delta.bsdiff4 containing a delta patch to get the desired file."""
|
||||
|
||||
class APProcedurePatch(APAutoPatchInterface):
|
||||
"""
|
||||
An APPatch that defines a procedure to produce the desired file.
|
||||
"""
|
||||
hash: Optional[str] # base checksum of source file
|
||||
patch_file_ending: str = ""
|
||||
delta: Optional[bytes] = None
|
||||
source_data: bytes
|
||||
procedure = None # delete this line when APPP is added
|
||||
|
||||
def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
|
||||
self.patched_path = patched_path
|
||||
super(APDeltaPatch, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
manifest = super(APDeltaPatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
manifest["compatible_version"] = 5 # delete this line when APPP is added
|
||||
return manifest
|
||||
patch_file_ending: str = ""
|
||||
files: Dict[str, bytes]
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
@@ -190,21 +206,223 @@ class APDeltaPatch(APAutoPatchInterface):
|
||||
cls.source_data = cls.get_source_data()
|
||||
return cls.source_data
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(APDeltaPatch, self).write_contents(opened_zipfile)
|
||||
# write Delta
|
||||
opened_zipfile.writestr("delta.bsdiff4",
|
||||
bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()),
|
||||
compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super(APProcedurePatch, self).__init__(*args, **kwargs)
|
||||
self.files = {}
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(APDeltaPatch, self).read_contents(opened_zipfile)
|
||||
self.delta = opened_zipfile.read("delta.bsdiff4")
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
manifest = super(APProcedurePatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
manifest["procedure"] = self.procedure
|
||||
if self.procedure == APDeltaPatch.procedure:
|
||||
manifest["compatible_version"] = 5
|
||||
return manifest
|
||||
|
||||
def patch(self, target: str):
|
||||
"""Base + Delta -> Patched"""
|
||||
if not self.delta:
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
super(APProcedurePatch, self).read_contents(opened_zipfile)
|
||||
with opened_zipfile.open("archipelago.json", "r") as f:
|
||||
manifest = json.load(f)
|
||||
if "procedure" not in manifest:
|
||||
# support patching files made before moving to procedures
|
||||
self.procedure = [("apply_bsdiff4", ["delta.bsdiff4"])]
|
||||
else:
|
||||
self.procedure = manifest["procedure"]
|
||||
for file in opened_zipfile.namelist():
|
||||
if file not in ["archipelago.json"]:
|
||||
self.files[file] = opened_zipfile.read(file)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
super(APProcedurePatch, self).write_contents(opened_zipfile)
|
||||
for file in self.files:
|
||||
opened_zipfile.writestr(file, self.files[file],
|
||||
compress_type=zipfile.ZIP_STORED if file.endswith(".bsdiff4") else None)
|
||||
|
||||
def get_file(self, file: str) -> bytes:
|
||||
""" Retrieves a file from the patch container."""
|
||||
if file not in self.files:
|
||||
self.read()
|
||||
result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta)
|
||||
with open(target, "wb") as f:
|
||||
f.write(result)
|
||||
return self.files[file]
|
||||
|
||||
def write_file(self, file_name: str, file: bytes) -> None:
|
||||
""" Writes a file to the patch container, to be retrieved upon patching. """
|
||||
self.files[file_name] = file
|
||||
|
||||
def patch(self, target: str) -> None:
|
||||
self.read()
|
||||
base_data = self.get_source_data_with_cache()
|
||||
patch_extender = AutoPatchExtensionRegister.get_handler(self.game)
|
||||
assert not isinstance(self.procedure, str), f"{type(self)} must define procedures"
|
||||
for step, args in self.procedure:
|
||||
if isinstance(patch_extender, list):
|
||||
extension = next((item for item in [getattr(extender, step, None) for extender in patch_extender]
|
||||
if item is not None), None)
|
||||
else:
|
||||
extension = getattr(patch_extender, step, None)
|
||||
if extension is not None:
|
||||
base_data = extension(self, base_data, *args)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown procedure {step} for {self.game}.")
|
||||
with open(target, 'wb') as f:
|
||||
f.write(base_data)
|
||||
|
||||
|
||||
class APDeltaPatch(APProcedurePatch):
|
||||
"""An APProcedurePatch that additionally has delta.bsdiff4
|
||||
containing a delta patch to get the desired file, often a rom."""
|
||||
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["delta.bsdiff4"])
|
||||
]
|
||||
|
||||
def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
|
||||
super(APDeltaPatch, self).__init__(*args, **kwargs)
|
||||
self.patched_path = patched_path
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
self.write_file("delta.bsdiff4",
|
||||
bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()))
|
||||
super(APDeltaPatch, self).write_contents(opened_zipfile)
|
||||
|
||||
|
||||
class APTokenTypes(IntEnum):
|
||||
WRITE = 0
|
||||
COPY = 1
|
||||
RLE = 2
|
||||
AND_8 = 3
|
||||
OR_8 = 4
|
||||
XOR_8 = 5
|
||||
|
||||
|
||||
class APTokenMixin:
|
||||
"""
|
||||
A class that defines functions for generating a token binary, for use in patches.
|
||||
"""
|
||||
_tokens: Sequence[
|
||||
Tuple[APTokenTypes, int, Union[
|
||||
bytes, # WRITE
|
||||
Tuple[int, int], # COPY, RLE
|
||||
int # AND_8, OR_8, XOR_8
|
||||
]]] = ()
|
||||
|
||||
def get_token_binary(self) -> bytes:
|
||||
"""
|
||||
Returns the token binary created from stored tokens.
|
||||
:return: A bytes object representing the token data.
|
||||
"""
|
||||
data = bytearray()
|
||||
data.extend(len(self._tokens).to_bytes(4, "little"))
|
||||
for token_type, offset, args in self._tokens:
|
||||
data.append(token_type)
|
||||
data.extend(offset.to_bytes(4, "little"))
|
||||
if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]:
|
||||
assert isinstance(args, int), f"Arguments to AND/OR/XOR must be of type int, not {type(args)}"
|
||||
data.extend(int.to_bytes(1, 4, "little"))
|
||||
data.append(args)
|
||||
elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]:
|
||||
assert isinstance(args, tuple), f"Arguments to COPY/RLE must be of type tuple, not {type(args)}"
|
||||
data.extend(int.to_bytes(8, 4, "little"))
|
||||
data.extend(args[0].to_bytes(4, "little"))
|
||||
data.extend(args[1].to_bytes(4, "little"))
|
||||
elif token_type == APTokenTypes.WRITE:
|
||||
assert isinstance(args, bytes), f"Arguments to WRITE must be of type bytes, not {type(args)}"
|
||||
data.extend(len(args).to_bytes(4, "little"))
|
||||
data.extend(args)
|
||||
else:
|
||||
raise ValueError(f"Unknown token type {token_type}")
|
||||
return bytes(data)
|
||||
|
||||
@overload
|
||||
def write_token(self,
|
||||
token_type: Literal[APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8],
|
||||
offset: int,
|
||||
data: int) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def write_token(self,
|
||||
token_type: Literal[APTokenTypes.COPY, APTokenTypes.RLE],
|
||||
offset: int,
|
||||
data: Tuple[int, int]) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def write_token(self,
|
||||
token_type: Literal[APTokenTypes.WRITE],
|
||||
offset: int,
|
||||
data: bytes) -> None:
|
||||
...
|
||||
|
||||
def write_token(self, token_type: APTokenTypes, offset: int, data: Union[bytes, Tuple[int, int], int]) -> None:
|
||||
"""
|
||||
Stores a token to be used by patching.
|
||||
"""
|
||||
if not isinstance(self._tokens, list):
|
||||
assert len(self._tokens) == 0, f"{type(self)}._tokens was tampered with."
|
||||
self._tokens = []
|
||||
self._tokens.append((token_type, offset, data))
|
||||
|
||||
|
||||
class APPatchExtension(metaclass=AutoPatchExtensionRegister):
|
||||
"""Class that defines patch extension functions for a given game.
|
||||
Patch extension functions must have the following two arguments in the following order:
|
||||
|
||||
caller: APProcedurePatch (used to retrieve files from the patch container)
|
||||
|
||||
rom: bytes (the data to patch)
|
||||
|
||||
Further arguments are passed in from the procedure as defined.
|
||||
|
||||
Patch extension functions must return the changed bytes.
|
||||
"""
|
||||
game: str
|
||||
required_extensions: ClassVar[Tuple[str, ...]] = ()
|
||||
|
||||
@staticmethod
|
||||
def apply_bsdiff4(caller: APProcedurePatch, rom: bytes, patch: str) -> bytes:
|
||||
"""Applies the given bsdiff4 from the patch onto the current file."""
|
||||
return bsdiff4.patch(rom, caller.get_file(patch))
|
||||
|
||||
@staticmethod
|
||||
def apply_tokens(caller: APProcedurePatch, rom: bytes, token_file: str) -> bytes:
|
||||
"""Applies the given token file from the patch onto the current file."""
|
||||
token_data = caller.get_file(token_file)
|
||||
rom_data = bytearray(rom)
|
||||
token_count = int.from_bytes(token_data[0:4], "little")
|
||||
bpr = 4
|
||||
for _ in range(token_count):
|
||||
token_type = token_data[bpr:bpr + 1][0]
|
||||
offset = int.from_bytes(token_data[bpr + 1:bpr + 5], "little")
|
||||
size = int.from_bytes(token_data[bpr + 5:bpr + 9], "little")
|
||||
data = token_data[bpr + 9:bpr + 9 + size]
|
||||
if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]:
|
||||
arg = data[0]
|
||||
if token_type == APTokenTypes.AND_8:
|
||||
rom_data[offset] = rom_data[offset] & arg
|
||||
elif token_type == APTokenTypes.OR_8:
|
||||
rom_data[offset] = rom_data[offset] | arg
|
||||
else:
|
||||
rom_data[offset] = rom_data[offset] ^ arg
|
||||
elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]:
|
||||
length = int.from_bytes(data[:4], "little")
|
||||
value = int.from_bytes(data[4:], "little")
|
||||
if token_type == APTokenTypes.COPY:
|
||||
rom_data[offset: offset + length] = rom_data[value: value + length]
|
||||
else:
|
||||
rom_data[offset: offset + length] = bytes([value] * length)
|
||||
else:
|
||||
rom_data[offset:offset + len(data)] = data
|
||||
bpr += 9 + size
|
||||
return bytes(rom_data)
|
||||
|
||||
@staticmethod
|
||||
def calc_snes_crc(caller: APProcedurePatch, rom: bytes) -> bytes:
|
||||
"""Calculates and applies a valid CRC for the SNES rom header."""
|
||||
rom_data = bytearray(rom)
|
||||
if len(rom) < 0x8000:
|
||||
raise Exception("Tried to calculate SNES CRC on file too small to be a SNES ROM.")
|
||||
crc = (sum(rom_data[:0x7FDC] + rom_data[0x7FE0:]) + 0x01FE) & 0xFFFF
|
||||
inv = crc ^ 0xFFFF
|
||||
rom_data[0x7FDC:0x7FE0] = [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]
|
||||
return bytes(rom_data)
|
||||
|
||||
@@ -20,9 +20,13 @@ __all__ = {
|
||||
"user_folder",
|
||||
"GamesPackage",
|
||||
"DataPackage",
|
||||
"failed_world_loads",
|
||||
}
|
||||
|
||||
|
||||
failed_world_loads: List[str] = []
|
||||
|
||||
|
||||
class GamesPackage(TypedDict, total=False):
|
||||
item_name_groups: Dict[str, List[str]]
|
||||
item_name_to_id: Dict[str, int]
|
||||
@@ -87,6 +91,7 @@ class WorldSource:
|
||||
file_like.seek(0)
|
||||
import logging
|
||||
logging.exception(file_like.read())
|
||||
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Adventure
|
||||
|
||||
## Where is the settings page?
|
||||
The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file.
|
||||
## Where is the options page?
|
||||
The [player options page for Adventure](../player-options) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All
|
||||
Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized,
|
||||
Adventure items are added to the multiworld item pool. Depending on the `dragon_rando_type` value, dragon locations may be randomized,
|
||||
slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates'
|
||||
can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist
|
||||
to reduce their speeds.
|
||||
@@ -15,7 +15,7 @@ Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle
|
||||
|
||||
## Which items can be in another player's world?
|
||||
All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on
|
||||
settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
|
||||
options, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
|
||||
|
||||
## What is considered a location check in Adventure?
|
||||
Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item.
|
||||
@@ -41,7 +41,7 @@ A message is shown in the client log. While empty handed, the player can press
|
||||
order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to
|
||||
return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions.
|
||||
|
||||
## What are recommended settings to tweak for beginners to the rando?
|
||||
## What are recommended options to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
@@ -41,7 +41,7 @@ an experience customized for their taste, and different players in the same mult
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings)
|
||||
You can generate a yaml or download a template by visiting the [Adventure Options Page](/games/Adventure/player-options)
|
||||
|
||||
### What are recommended settings to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
|
||||
@@ -3,6 +3,8 @@ from collections import defaultdict
|
||||
|
||||
from .OverworldGlitchRules import overworld_glitch_connections
|
||||
from .UnderworldGlitchRules import underworld_glitch_connections
|
||||
from .Regions import mark_light_world_regions
|
||||
from .InvertedRegions import mark_dark_world_regions
|
||||
|
||||
|
||||
def link_entrances(world, player):
|
||||
@@ -1827,6 +1829,10 @@ def plando_connect(world, player: int):
|
||||
func(world, connection.entrance, connection.exit, player)
|
||||
except Exception as e:
|
||||
raise Exception(f"Could not connect using {connection}") from e
|
||||
if world.mode[player] != 'inverted':
|
||||
mark_light_world_regions(world, player)
|
||||
else:
|
||||
mark_dark_world_regions(world, player)
|
||||
|
||||
|
||||
LW_Dungeon_Entrances = ['Desert Palace Entrance (South)',
|
||||
|
||||
@@ -381,8 +381,8 @@ def create_inverted_regions(world, player):
|
||||
create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room', 'Skull Woods - Spike Corner Key Drop'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
|
||||
create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
|
||||
create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
|
||||
|
||||
@@ -508,7 +508,7 @@ def generate_itempool(world):
|
||||
multiworld.itempool += items
|
||||
|
||||
if multiworld.retro_caves[player]:
|
||||
set_up_take_anys(multiworld, player) # depends on world.itempool to be set
|
||||
set_up_take_anys(multiworld, world, player) # depends on world.itempool to be set
|
||||
|
||||
|
||||
take_any_locations = {
|
||||
@@ -528,30 +528,30 @@ take_any_locations_inverted.sort()
|
||||
take_any_locations.sort()
|
||||
|
||||
|
||||
def set_up_take_anys(world, player):
|
||||
def set_up_take_anys(multiworld, world, player):
|
||||
# these are references, do not modify these lists in-place
|
||||
if world.mode[player] == 'inverted':
|
||||
if multiworld.mode[player] == 'inverted':
|
||||
take_any_locs = take_any_locations_inverted
|
||||
else:
|
||||
take_any_locs = take_any_locations
|
||||
|
||||
regions = world.random.sample(take_any_locs, 5)
|
||||
regions = multiworld.random.sample(take_any_locs, 5)
|
||||
|
||||
old_man_take_any = LTTPRegion("Old Man Sword Cave", LTTPRegionType.Cave, 'the sword cave', player, world)
|
||||
world.regions.append(old_man_take_any)
|
||||
old_man_take_any = LTTPRegion("Old Man Sword Cave", LTTPRegionType.Cave, 'the sword cave', player, multiworld)
|
||||
multiworld.regions.append(old_man_take_any)
|
||||
|
||||
reg = regions.pop()
|
||||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance.name, old_man_take_any.name, player)
|
||||
entrance = multiworld.get_region(reg, player).entrances[0]
|
||||
connect_entrance(multiworld, entrance.name, old_man_take_any.name, player)
|
||||
entrance.target = 0x58
|
||||
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
|
||||
world.shops.append(old_man_take_any.shop)
|
||||
multiworld.shops.append(old_man_take_any.shop)
|
||||
|
||||
swords = [item for item in world.itempool if item.player == player and item.type == 'Sword']
|
||||
swords = [item for item in multiworld.itempool if item.player == player and item.type == 'Sword']
|
||||
if swords:
|
||||
sword = world.random.choice(swords)
|
||||
world.itempool.remove(sword)
|
||||
world.itempool.append(item_factory('Rupees (20)', world))
|
||||
sword = multiworld.random.choice(swords)
|
||||
multiworld.itempool.remove(sword)
|
||||
multiworld.itempool.append(item_factory('Rupees (20)', world))
|
||||
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0)
|
||||
loc_name = "Old Man Sword Cave"
|
||||
location = ALttPLocation(player, loc_name, shop_table_by_location[loc_name], parent=old_man_take_any)
|
||||
@@ -562,16 +562,16 @@ def set_up_take_anys(world, player):
|
||||
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0)
|
||||
|
||||
for num in range(4):
|
||||
take_any = LTTPRegion("Take-Any #{}".format(num+1), LTTPRegionType.Cave, 'a cave of choice', player, world)
|
||||
world.regions.append(take_any)
|
||||
take_any = LTTPRegion("Take-Any #{}".format(num+1), LTTPRegionType.Cave, 'a cave of choice', player, multiworld)
|
||||
multiworld.regions.append(take_any)
|
||||
|
||||
target, room_id = world.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])
|
||||
target, room_id = multiworld.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])
|
||||
reg = regions.pop()
|
||||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance.name, take_any.name, player)
|
||||
entrance = multiworld.get_region(reg, player).entrances[0]
|
||||
connect_entrance(multiworld, entrance.name, take_any.name, player)
|
||||
entrance.target = target
|
||||
take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1)
|
||||
world.shops.append(take_any.shop)
|
||||
multiworld.shops.append(take_any.shop)
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
|
||||
location = ALttPLocation(player, take_any.name, shop_table_by_location[take_any.name], parent=take_any)
|
||||
|
||||
@@ -649,7 +649,7 @@ class ALTTPWorld(World):
|
||||
if not multiworld.ganonstower_vanilla[player] or \
|
||||
world.options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', "no_logic"}:
|
||||
pass
|
||||
elif 'triforce_hunt' in world.options.goal.current_key and ('local' in world.options.goal.current_key or world.players == 1):
|
||||
elif 'triforce_hunt' in world.options.goal.current_key and ('local' in world.options.goal.current_key or multiworld.players == 1):
|
||||
trash_counts[player] = multiworld.random.randint(world.options.crystals_needed_for_gt * 2,
|
||||
world.options.crystals_needed_for_gt * 4)
|
||||
else:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# A Link to the Past
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
@@ -6,8 +6,8 @@ ArchipIDLE was originally the 2022 Archipelago April Fools' Day joke. It is an i
|
||||
on regular intervals. Updated annually with more items, gimmicks, and features, the game is visible
|
||||
only during the month of April.
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure
|
||||
and export a config file.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ArchipIdle Setup Guide
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
1. Generate a `.yaml` file from the [ArchipIDLE Player Settings Page](/games/ArchipIDLE/player-settings)
|
||||
1. Generate a `.yaml` file from the [ArchipIDLE Player Options Page](/games/ArchipIDLE/player-options)
|
||||
2. Open the ArchipIDLE Client in your web browser by either:
|
||||
- Navigate to the [ArchipIDLE Client](http://idle.multiworld.link)
|
||||
- Download the client and run it locally from the
|
||||
|
||||
@@ -8,6 +8,6 @@ BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku cli
|
||||
|
||||
After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to.
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
There is no settings page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
|
||||
There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Blasphemous
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Bumper Stickers
|
||||
|
||||
## Where is the settings page?
|
||||
The [player settings page for Bumper Stickers](../player-settings) contains all the options you need to configure and export a config file.
|
||||
## Where is the options page?
|
||||
The [player options page for Bumper Stickers](../player-options) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Playing this in Archipelago is a very different experience from Classic mode. You start with a very small board and a set of tasks. Completing those tasks will give you a larger board and more, harder tasks. In addition, special types of bumpers exist that must be cleared in order to progress.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# ChecksFinder
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What is considered a location check in ChecksFinder?
|
||||
|
||||
@@ -15,7 +15,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can customize your settings by visiting the [ChecksFinder Player Settings Page](/games/ChecksFinder/player-settings)
|
||||
You can customize your options by visiting the [ChecksFinder Player Options Page](/games/ChecksFinder/player-options)
|
||||
|
||||
### Generating a ChecksFinder game
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ wait for someone else in the multiworld to "activate" their button before they c
|
||||
|
||||
Clique can be played on most modern HTML5-capable browsers.
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure
|
||||
and export a config file.
|
||||
|
||||
327
worlds/cv64/__init__.py
Normal file
327
worlds/cv64/__init__.py
Normal 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 Castlevania franchise. As either whip-wielding Belmont
|
||||
descendant Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you
|
||||
make your way to Dracula's chamber and stop his rule of terror!
|
||||
"""
|
||||
game = "Castlevania 64"
|
||||
item_name_groups = {
|
||||
"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
666
worlds/cv64/aesthetics.py
Normal 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
207
worlds/cv64/client.py
Normal 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
|
||||
3
worlds/cv64/data/APLogo-LICENSE.txt
Normal file
3
worlds/cv64/data/APLogo-LICENSE.txt
Normal 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.
|
||||
BIN
worlds/cv64/data/ap_icons.bin
Normal file
BIN
worlds/cv64/data/ap_icons.bin
Normal file
Binary file not shown.
71
worlds/cv64/data/ename.py
Normal file
71
worlds/cv64/data/ename.py
Normal 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
49
worlds/cv64/data/iname.py
Normal 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
479
worlds/cv64/data/lname.py
Normal 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
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
63
worlds/cv64/data/rname.py
Normal 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"
|
||||
148
worlds/cv64/docs/en_Castlevania 64.md
Normal file
148
worlds/cv64/docs/en_Castlevania 64.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Castlevania 64
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
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>: Filler
|
||||
- <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 by any options. The basement arena crack will always logically expect two Nitros
|
||||
and two Mandragoras even with Hard Logic on due to the possibility of wasting a pair on the upper wall, after managing
|
||||
to skip past it. And plus, the RNG manip may not even be possible after picking up all the items in the Nitro room.
|
||||
|
||||
## 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>
|
||||
429
worlds/cv64/docs/obscure_checks.md
Normal file
429
worlds/cv64/docs/obscure_checks.md
Normal 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!
|
||||
63
worlds/cv64/docs/setup_en.md
Normal file
63
worlds/cv64/docs/setup_en.md
Normal 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 options file (YAML). You can make one on the
|
||||
[Castlevania 64 options page](../../../games/Castlevania%2064/player-options).
|
||||
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
|
||||
This will generate an output file for you. Your patch file will have the `.apcv64` file extension.
|
||||
3. Open `ArchipelagoLauncher.exe`
|
||||
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
149
worlds/cv64/entrances.py
Normal 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
214
worlds/cv64/items.py
Normal 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
699
worlds/cv64/locations.py
Normal 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
266
worlds/cv64/lzkn64.py
Normal 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]
|
||||
491
worlds/cv64/options.py
Normal file
491
worlds/cv64/options.py
Normal file
@@ -0,0 +1,491 @@
|
||||
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 18 of these throughout
|
||||
the game, adding up to 79 or 80 checks (depending on sub-weapons
|
||||
being shuffled anywhere or not) in total with all stages.
|
||||
The game will be modified to
|
||||
remember exactly which of their items you've picked up instead of simply whether they were broken or not."""
|
||||
display_name = "Multi-hit Breakables"
|
||||
|
||||
|
||||
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 by any options."""
|
||||
display_name = "Big Toss"
|
||||
|
||||
|
||||
class PantherDash(Choice):
|
||||
"""Hold C-right at any time to sprint way faster. Any tricks that might be
|
||||
possible with it are NOT considered in logic by any options and any boss
|
||||
fights with boss health meters, if started, are expected to be finished
|
||||
before leaving their arenas if Dracula's Condition is bosses. Jumpless will
|
||||
prevent jumping while moving at the increased speed to ensure logic cannot be broken with it."""
|
||||
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
517
worlds/cv64/regions.py
Normal 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
959
worlds/cv64/rom.py
Normal 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
103
worlds/cv64/rules.py
Normal 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)
|
||||
69
worlds/cv64/src/drop_sub_weapon.c
Normal file
69
worlds/cv64/src/drop_sub_weapon.c
Normal 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
116
worlds/cv64/src/print.c
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
worlds/cv64/src/print_text_ovl.c
Normal file
26
worlds/cv64/src/print_text_ovl.c
Normal 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
490
worlds/cv64/stages.py
Normal 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
|
||||
6
worlds/cv64/test/__init__.py
Normal file
6
worlds/cv64/test/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class CV64TestBase(WorldTestBase):
|
||||
game = "Castlevania 64"
|
||||
player: int = 1
|
||||
250
worlds/cv64/test/test_access.py
Normal file
250
worlds/cv64/test/test_access.py
Normal 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
95
worlds/cv64/text.py
Normal 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
|
||||
@@ -1,8 +1,8 @@
|
||||
# Dark Souls III
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
@@ -13,7 +13,7 @@ location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter
|
||||
happens when you randomize Estus Shards and Undead Bone Shards.
|
||||
|
||||
It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have
|
||||
one). Additionally, there are settings that can make the randomized experience more convenient or more interesting, such as
|
||||
one). Additionally, there are options that can make the randomized experience more convenient or more interesting, such as
|
||||
removing weapon requirements or auto-equipping whatever equipment you most recently received.
|
||||
|
||||
The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder.
|
||||
|
||||
@@ -51,5 +51,5 @@ add it at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK
|
||||
|
||||
## Where do I get a config file?
|
||||
|
||||
The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
|
||||
configure your personal settings and export them into a config file.
|
||||
The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to
|
||||
configure your personal options and export them into a config file.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Donkey Kong Country 3
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The Player Settings page on the website allows you to configure your personal settings and export a config file from
|
||||
them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings)
|
||||
The Player Options page on the website allows you to configure your personal options and export a config file from
|
||||
them. Player options page: [Donkey Kong Country 3 Player Options Page](/games/Donkey%20Kong%20Country%203/player-options)
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
@@ -55,8 +55,8 @@ validator page: [YAML Validation page](/check)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
|
||||
- Player Settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings)
|
||||
1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button.
|
||||
- Player Options page: [Donkey Kong Country 3 Player Options Page](/games/Donkey%20Kong%20Country%203/player-options)
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your patch file.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# DLC Quest
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
@@ -19,7 +19,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can customize your settings by visiting the [DLC Quest Player Settings Page](/games/DLCQuest/player-settings)
|
||||
You can customize your options by visiting the [DLC Quest Player Options Page](/games/DLCQuest/player-options)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# DOOM 1993
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page](../player-settings) contains the options needed to configure your game session.
|
||||
The [player options page](../player-options) contains the options needed to configure your game session.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# DOOM II
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page](../player-settings) contains the options needed to configure your game session.
|
||||
The [player options page](../player-options) contains the options needed to configure your game session.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import Utils
|
||||
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
|
||||
from MultiServer import mark_raw
|
||||
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
|
||||
from Utils import async_start
|
||||
from Utils import async_start, get_file_safe_name
|
||||
|
||||
|
||||
def check_stdin() -> None:
|
||||
@@ -120,7 +120,7 @@ class FactorioContext(CommonContext):
|
||||
|
||||
@property
|
||||
def savegame_name(self) -> str:
|
||||
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
|
||||
return get_file_safe_name(f"AP_{self.seed_name}_{self.auth}")+"_Save.zip"
|
||||
|
||||
def print_to_game(self, text):
|
||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Factorio
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
@@ -36,7 +36,7 @@ inventory.
|
||||
## What is EnergyLink?
|
||||
|
||||
EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld.
|
||||
In Factorio, if enabled in the player settings, EnergyLink Bridge buildings can be crafted and placed, which allow
|
||||
In Factorio, if enabled in the player options, EnergyLink Bridge buildings can be crafted and placed, which allow
|
||||
depositing excess energy and supplementing energy deficits, much like Accumulators.
|
||||
|
||||
Each placed EnergyLink Bridge provides 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy
|
||||
|
||||
@@ -25,8 +25,8 @@ options.
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The Player Settings page on the website allows you to configure your personal settings and export a config file from
|
||||
them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/player-settings)
|
||||
The Player Options page on the website allows you to configure your personal options and export a config file from
|
||||
them. Factorio player options page: [Factorio Options Page](/games/Factorio/player-options)
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
@@ -133,7 +133,7 @@ This allows you to host your own Factorio game.
|
||||
For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP server,
|
||||
you can also issue the `!help` command to learn about additional commands like `!hint`.
|
||||
For more information about the commands you can use, see the [Commands Guide](/tutorial/Archipelago/commands/en) and
|
||||
[Other Settings](#other-settings).
|
||||
[Other Options](#other-options).
|
||||
|
||||
## Allowing Other People to Join Your Game
|
||||
|
||||
@@ -148,11 +148,11 @@ For more information about the commands you can use, see the [Commands Guide](/t
|
||||
By default, peaceful mode is disabled. There are two methods to enable peaceful mode:
|
||||
|
||||
### By config file
|
||||
You can specify Factorio game settings such as peaceful mode and terrain and resource generation parameters in your
|
||||
config .yaml file by including the `world_gen` setting. This setting is currently not supported by the web UI, so you'll
|
||||
You can specify Factorio game options such as peaceful mode and terrain and resource generation parameters in your
|
||||
config .yaml file by including the `world_gen` option. This option is currently not supported by the web UI, so you'll
|
||||
have to manually create or edit your config file with a text editor of your choice.
|
||||
The [template file](/static/generated/configs/Factorio.yaml) is a good starting point and contains the default value of
|
||||
the `world_gen` setting. If you already have a config file you may also just copy that setting over from the template.
|
||||
the `world_gen` option. If you already have a config file you may also just copy that option over from the template.
|
||||
To enable peaceful mode, simply replace `peaceful_mode: false` with `peaceful_mode: true`. Finally, use the
|
||||
[.yaml checker](/check) to ensure your file is valid.
|
||||
|
||||
@@ -165,7 +165,7 @@ enable peaceful mode by entering the following commands into your Archipelago Fa
|
||||
```
|
||||
(If this warns you that these commands may disable achievements, you may need to repeat them for them to take effect.)
|
||||
|
||||
## Other Settings
|
||||
## Other Options
|
||||
|
||||
### filter_item_sends
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Final Fantasy 1 (NES)
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
Unlike most games on Archipelago.gg, Final Fantasy 1's settings are controlled entirely by the original randomzier. You
|
||||
can find an exhaustive list of documented settings on the FFR
|
||||
Unlike most games on Archipelago.gg, Final Fantasy 1's options are controlled entirely by the original randomzier. You
|
||||
can find an exhaustive list of documented options on the FFR
|
||||
website: [FF1R Website](https://finalfantasyrandomizer.com/)
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Final Fantasy Mystic Quest
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
@@ -39,8 +39,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The Player Settings page on the website allows you to configure your personal settings and export a config file from
|
||||
them. Player settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings)
|
||||
The Player Options page on the website allows you to configure your personal options and export a config file from
|
||||
them. Player options page: [Final Fantasy Mystic Quest Player Options Page](/games/Final%20Fantasy%20Mystic%20Quest/player-options)
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
@@ -49,8 +49,8 @@ validator page: [YAML Validation page](/mysterycheck)
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
|
||||
- Player Settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings)
|
||||
1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button.
|
||||
- Player Options page: [Final Fantasy Mystic Quest Player Options Page](/games/Final%20Fantasy%20Mystic%20Quest/player-options)
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
4. You will be presented with a server page, from which you can download your `.apmq` patch file.
|
||||
|
||||
@@ -201,7 +201,7 @@ Kirby's Dream Land 3:
|
||||
As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the
|
||||
relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
|
||||
|
||||
## Connections Plando
|
||||
## Connection Plando
|
||||
|
||||
This is currently only supported by a few games, including A Link to the Past, Minecraft, and Ocarina of Time. As the way that these games interact with their
|
||||
connections is different, only the basics are explained here. More specific information for connection plando in A Link to the Past can be found in
|
||||
|
||||
@@ -39,7 +39,7 @@ to your Archipelago installation.
|
||||
### What is a YAML?
|
||||
|
||||
YAML is the file format which Archipelago uses in order to configure a player's world. It allows you to dictate which
|
||||
game you will be playing as well as the settings you would like for that game.
|
||||
game you will be playing as well as the options you would like for that game.
|
||||
|
||||
YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the
|
||||
validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website:
|
||||
@@ -48,10 +48,10 @@ validity of your YAML file you may check the file by uploading it to the check p
|
||||
### Creating a YAML
|
||||
|
||||
YAML files may be generated on the Archipelago website by visiting the [games page](/games) and clicking the
|
||||
"Settings Page" link under the relevant game. Clicking "Export Settings" in a game's settings page will download the
|
||||
"Options Page" link under the relevant game. Clicking "Export Options" in a game's options page will download the
|
||||
YAML to your system.
|
||||
|
||||
Alternatively, you can run `ArchipelagoLauncher.exe` and click on `Generate Template Settings` to create a set of template
|
||||
Alternatively, you can run `ArchipelagoLauncher.exe` and click on `Generate Template Options` to create a set of template
|
||||
YAMLs for each game in your Archipelago install (including for APWorlds). These will be placed in your `Players/Templates` folder.
|
||||
|
||||
In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's
|
||||
@@ -66,17 +66,17 @@ each player is planning on playing their own game then they will each need a YAM
|
||||
#### On the website
|
||||
|
||||
The easiest way to get started playing an Archipelago generated game, after following the base setup from the game's
|
||||
setup guide, is to find the game on the [Archipelago Games List](/games), click on `Settings Page`, set the settings for
|
||||
setup guide, is to find the game on the [Archipelago Games List](/games), click on `Options Page`, set the options for
|
||||
how you want to play, and click `Generate Game` at the bottom of the page. This will create a page for the seed, from
|
||||
which you can create a room, and then [connect](#connecting-to-an-archipelago-server).
|
||||
|
||||
If you have downloaded the settings, or have created a settings file manually, this file can be uploaded on the
|
||||
If you have downloaded the options, or have created an options file manually, this file can be uploaded on the
|
||||
[Generation Page](/generate) where you can also set any specific hosting settings.
|
||||
|
||||
#### On your local installation
|
||||
|
||||
To generate a game on your local machine, make sure to install the Archipelago software. Navigate to your Archipelago
|
||||
installation (usually C:\ProgramData\Archipelago), and place the settings file you have either created or downloaded
|
||||
installation (usually C:\ProgramData\Archipelago), and place the options file you have either created or downloaded
|
||||
from the website in the `Players` folder.
|
||||
|
||||
Run `ArchipelagoGenerate.exe`, or click on `Generate` in the launcher, and it will inform you whether the generation
|
||||
@@ -97,7 +97,7 @@ resources, and host the resulting multiworld on the website.
|
||||
|
||||
#### Gather All Player YAMLs
|
||||
|
||||
All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they
|
||||
All players that wish to play in the generated multiworld must have a YAML file which contains the options that they
|
||||
wish to play with. One person should gather all files from all participants in the generated multiworld. It is possible
|
||||
for a single player to have multiple games, or even multiple slots of a single game, but each YAML must have a unique
|
||||
player name.
|
||||
@@ -129,7 +129,7 @@ need the corresponding ROM files.
|
||||
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode,
|
||||
auto-release, plando support, or setting a password.
|
||||
|
||||
All of these settings, plus other options, may be changed by modifying the `host.yaml` file in the Archipelago
|
||||
All of these settings, plus more, can be changed by modifying the `host.yaml` file in the Archipelago
|
||||
installation folder. You can quickly access this file by clicking on `Open host.yaml` in the launcher. The settings
|
||||
chosen here are baked into the `.archipelago` file that gets output with the other files after generation, so if you
|
||||
are rolling locally, ensure this file is edited to your liking **before** rolling the seed. This file is overwritten
|
||||
@@ -207,4 +207,4 @@ when creating your [YAML file](#creating-a-yaml). If the game is hosted on the w
|
||||
room page. The name is case-sensitive.
|
||||
* `Password` is the password set by the host in order to join the multiworld. By default, this will be empty and is almost
|
||||
never required, but one can be set when generating the game. Generally, leave this field blank when it exists,
|
||||
unless you know that a password was set, and what that password is.
|
||||
unless you know that a password was set, and what that password is.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Heretic
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page](../player-settings) contains the options needed to configure your game session.
|
||||
The [player options page](../player-options) contains the options needed to configure your game session.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import typing
|
||||
import re
|
||||
from .ExtractedData import logic_options, starts, pool_options
|
||||
from .Rules import cost_terms
|
||||
|
||||
@@ -11,12 +12,16 @@ if typing.TYPE_CHECKING:
|
||||
else:
|
||||
Random = typing.Any
|
||||
|
||||
|
||||
locations = {"option_" + start: i for i, start in enumerate(starts)}
|
||||
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
|
||||
StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
|
||||
"__doc__": "Choose your start location. "
|
||||
"This is currently only locked to King's Pass."})
|
||||
StartLocation = type("StartLocation", (Choice,), {
|
||||
"__module__": __name__,
|
||||
"auto_display_name": False,
|
||||
"display_name": "Start Location",
|
||||
"__doc__": "Choose your start location. "
|
||||
"This is currently only locked to King's Pass.",
|
||||
**locations,
|
||||
})
|
||||
del (locations)
|
||||
|
||||
option_docstrings = {
|
||||
@@ -49,8 +54,7 @@ option_docstrings = {
|
||||
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
|
||||
"pool and open their locations\n for randomization.",
|
||||
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
|
||||
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
|
||||
"Mimic Grubs are always placed\n in your own game.",
|
||||
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization.",
|
||||
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
|
||||
" and buy an item\n that is randomized into that location as well.",
|
||||
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
|
||||
@@ -99,8 +103,12 @@ default_on = {
|
||||
"RandomizeKeys",
|
||||
"RandomizeMaskShards",
|
||||
"RandomizeVesselFragments",
|
||||
"RandomizeCharmNotches",
|
||||
"RandomizePaleOre",
|
||||
"RandomizeRelics"
|
||||
"RandomizeRancidEggs"
|
||||
"RandomizeRelics",
|
||||
"RandomizeStags",
|
||||
"RandomizeLifebloodCocoons"
|
||||
}
|
||||
|
||||
shop_to_option = {
|
||||
@@ -117,6 +125,7 @@ shop_to_option = {
|
||||
|
||||
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
|
||||
|
||||
splitter_pattern = re.compile(r'(?<!^)(?=[A-Z])')
|
||||
for option_name, option_data in pool_options.items():
|
||||
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
|
||||
if option_name in option_docstrings:
|
||||
@@ -125,6 +134,7 @@ for option_name, option_data in pool_options.items():
|
||||
option = type(option_name, (DefaultOnToggle,), extra_data)
|
||||
else:
|
||||
option = type(option_name, (Toggle,), extra_data)
|
||||
option.display_name = splitter_pattern.sub(" ", option_name)
|
||||
globals()[option.__name__] = option
|
||||
hollow_knight_randomize_options[option.__name__] = option
|
||||
|
||||
@@ -133,11 +143,14 @@ for option_name in logic_options.values():
|
||||
if option_name in hollow_knight_randomize_options:
|
||||
continue
|
||||
extra_data = {"__module__": __name__}
|
||||
# some options, such as elevator pass, appear in logic_options despite explicitly being
|
||||
# handled below as classes.
|
||||
if option_name in option_docstrings:
|
||||
extra_data["__doc__"] = option_docstrings[option_name]
|
||||
option = type(option_name, (Toggle,), extra_data)
|
||||
globals()[option.__name__] = option
|
||||
hollow_knight_logic_options[option.__name__] = option
|
||||
option.display_name = splitter_pattern.sub(" ", option_name)
|
||||
globals()[option.__name__] = option
|
||||
hollow_knight_logic_options[option.__name__] = option
|
||||
|
||||
|
||||
class RandomizeElevatorPass(Toggle):
|
||||
@@ -269,11 +282,11 @@ class RandomCharmCosts(NamedRange):
|
||||
random_source.shuffle(charms)
|
||||
return charms
|
||||
else:
|
||||
charms = [0]*self.charm_count
|
||||
charms = [0] * self.charm_count
|
||||
for x in range(self.value):
|
||||
index = random_source.randint(0, self.charm_count-1)
|
||||
index = random_source.randint(0, self.charm_count - 1)
|
||||
while charms[index] > 5:
|
||||
index = random_source.randint(0, self.charm_count-1)
|
||||
index = random_source.randint(0, self.charm_count - 1)
|
||||
charms[index] += 1
|
||||
return charms
|
||||
|
||||
@@ -404,6 +417,7 @@ class WhitePalace(Choice):
|
||||
|
||||
class ExtraPlatforms(DefaultOnToggle):
|
||||
"""Places additional platforms to make traveling throughout Hallownest more convenient."""
|
||||
display_name = "Extra Platforms"
|
||||
|
||||
|
||||
class AddUnshuffledLocations(Toggle):
|
||||
@@ -413,6 +427,7 @@ class AddUnshuffledLocations(Toggle):
|
||||
Note: This will increase the number of location checks required to purchase
|
||||
hints to the total maximum.
|
||||
"""
|
||||
display_name = "Add Unshuffled Locations"
|
||||
|
||||
|
||||
class DeathLinkShade(Choice):
|
||||
@@ -430,6 +445,7 @@ class DeathLinkShade(Choice):
|
||||
option_shadeless = 1
|
||||
option_shade = 2
|
||||
default = 2
|
||||
display_name = "Deathlink Shade Handling"
|
||||
|
||||
|
||||
class DeathLinkBreaksFragileCharms(Toggle):
|
||||
@@ -439,6 +455,7 @@ class DeathLinkBreaksFragileCharms(Toggle):
|
||||
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
|
||||
will continue to do so.
|
||||
"""
|
||||
display_name = "Deathlink Breaks Fragile Charms"
|
||||
|
||||
|
||||
class StartingGeo(Range):
|
||||
@@ -462,18 +479,20 @@ class CostSanity(Choice):
|
||||
alias_yes = 1
|
||||
option_shopsonly = 2
|
||||
option_notshops = 3
|
||||
display_name = "Cost Sanity"
|
||||
display_name = "Costsanity"
|
||||
|
||||
|
||||
class CostSanityHybridChance(Range):
|
||||
"""The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence"""
|
||||
range_end = 100
|
||||
default = 10
|
||||
display_name = "Costsanity Hybrid Chance"
|
||||
|
||||
|
||||
cost_sanity_weights: typing.Dict[str, type(Option)] = {}
|
||||
for term, cost in cost_terms.items():
|
||||
option_name = f"CostSanity{cost.option}Weight"
|
||||
display_name = f"Costsanity {cost.option} Weight"
|
||||
extra_data = {
|
||||
"__module__": __name__, "range_end": 1000,
|
||||
"__doc__": (
|
||||
@@ -486,10 +505,10 @@ for term, cost in cost_terms.items():
|
||||
extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop."
|
||||
|
||||
option = type(option_name, (Range,), extra_data)
|
||||
option.display_name = display_name
|
||||
globals()[option.__name__] = option
|
||||
cost_sanity_weights[option.__name__] = option
|
||||
|
||||
|
||||
hollow_knight_options: typing.Dict[str, type(Option)] = {
|
||||
**hollow_knight_randomize_options,
|
||||
RandomizeElevatorPass.__name__: RandomizeElevatorPass,
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
# Hollow Knight
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Randomization swaps around the locations of items. The items being swapped around are chosen within your YAML.
|
||||
Shop costs are presently always randomized.
|
||||
Shop costs are presently always randomized. Items which could be randomized, but are not, will remain unmodified in
|
||||
their usual locations. In particular, when the items at Grubfather and Seer are partially randomized, randomized items
|
||||
will be obtained from a chest in the room, while unrandomized items will be given by the NPC as normal.
|
||||
|
||||
## What Hollow Knight items can appear in other players' worlds?
|
||||
|
||||
This is dependent entirely upon your YAML settings. Some examples include: charms, grubs, lifeblood cocoons, geo, etc.
|
||||
This is dependent entirely upon your YAML options. Some examples include: charms, grubs, lifeblood cocoons, geo, etc.
|
||||
|
||||
## What does another world's item look like in Hollow Knight?
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
## Required Software
|
||||
* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
|
||||
* A legal copy of Hollow Knight.
|
||||
* Steam, Gog, and Xbox Game Pass versions of the game are supported.
|
||||
* Windows, Mac, and Linux (including Steam Deck) are supported.
|
||||
|
||||
## Installing the Archipelago Mod using Lumafly
|
||||
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
||||
@@ -10,28 +12,28 @@
|
||||
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
|
||||
3. Launch the game, you're all set!
|
||||
|
||||
### What to do if Lumafly fails to find your XBox Game Pass installation directory
|
||||
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
|
||||
2. Click the three points then click "Manage".
|
||||
3. Go to the "Files" tab and select "Browse...".
|
||||
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
||||
5. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 4.
|
||||
|
||||
#### Alternative Method:
|
||||
1. Click on your profile then "Settings".
|
||||
2. Go to the "General" tab and select "CHANGE FOLDER".
|
||||
3. Look for a folder where you want to install the game (preferably inside a folder on your desktop) and copy the path.
|
||||
4. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 3.
|
||||
|
||||
Note: The path folder needs to have the "Hollow Knight_Data" folder inside.
|
||||
### What to do if Lumafly fails to find your installation directory
|
||||
1. Find the directory manually.
|
||||
* Xbox Game Pass:
|
||||
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
|
||||
2. Click the three points then click "Manage".
|
||||
3. Go to the "Files" tab and select "Browse...".
|
||||
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
||||
* Steam:
|
||||
1. You likely put your Steam library in a non-standard place. If this is the case, you probably know where
|
||||
it is. Find your steam library and then find the Hollow Knight folder and copy the path.
|
||||
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
||||
* Linux/Steam Deck - ~/.local/share/Steam/steamapps/common/Hollow Knight
|
||||
* Mac - ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app
|
||||
2. Run Lumafly as an administrator and, when it asks you for the path, paste the path you copied.
|
||||
|
||||
## Configuring your YAML File
|
||||
### What is a YAML and why do I need one?
|
||||
You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
|
||||
about why Archipelago uses YAML files and what they're for.
|
||||
An YAML file is the way that you provide your player options to Archipelago.
|
||||
See the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn more.
|
||||
|
||||
### Where do I get a YAML?
|
||||
You can use the [game settings page for Hollow Knight](/games/Hollow%20Knight/player-settings) here on the Archipelago
|
||||
You can use the [game options page for Hollow Knight](/games/Hollow%20Knight/player-options) here on the Archipelago
|
||||
website to generate a YAML using a graphical interface.
|
||||
|
||||
### Joining an Archipelago Game in Hollow Knight
|
||||
@@ -44,9 +46,7 @@ website to generate a YAML using a graphical interface.
|
||||
* If you are waiting for a countdown then wait for it to lapse before hitting Start.
|
||||
* Or hit Start then pause the game once you're in it.
|
||||
|
||||
## Commands
|
||||
While playing the multiworld you can interact with the server using various commands listed in the
|
||||
[commands guide](/tutorial/Archipelago/commands/en). As this game does not have an in-game text client at the moment,
|
||||
You can optionally connect to the multiworld using the text client, which can be found in the
|
||||
[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases) as Archipelago Text Client to
|
||||
enter these commands.
|
||||
## Hints and other commands
|
||||
While playing in a multiworld, you can interact with the server using various commands listed in the
|
||||
[commands guide](/tutorial/Archipelago/commands/en). You can use the Archipelago Text Client to do this,
|
||||
which is included in the latest release of the [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
|
||||
|
||||
@@ -34,8 +34,6 @@ class Hylics2World(World):
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in all_locations.items()}
|
||||
option_definitions = Options.hylics2_options
|
||||
|
||||
topology_present: bool = True
|
||||
|
||||
data_version = 3
|
||||
|
||||
start_location = "Waynehouse"
|
||||
@@ -51,10 +49,6 @@ class Hylics2World(World):
|
||||
return Hylics2Item(name, self.all_items[item_id]["classification"], item_id, player=self.player)
|
||||
|
||||
|
||||
def add_item(self, name: str, classification: ItemClassification, code: int) -> "Item":
|
||||
return Hylics2Item(name, classification, code, self.player)
|
||||
|
||||
|
||||
def create_event(self, event: str):
|
||||
return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player)
|
||||
|
||||
@@ -62,7 +56,7 @@ class Hylics2World(World):
|
||||
# set random starting location if option is enabled
|
||||
def generate_early(self):
|
||||
if self.multiworld.random_start[self.player]:
|
||||
i = self.multiworld.random.randint(0, 3)
|
||||
i = self.random.randint(0, 3)
|
||||
if i == 0:
|
||||
self.start_location = "Waynehouse"
|
||||
elif i == 1:
|
||||
@@ -77,26 +71,26 @@ class Hylics2World(World):
|
||||
pool = []
|
||||
|
||||
# add regular items
|
||||
for i, data in Items.item_table.items():
|
||||
if data["count"] > 0:
|
||||
for j in range(data["count"]):
|
||||
pool.append(self.add_item(data["name"], data["classification"], i))
|
||||
for item in Items.item_table.values():
|
||||
if item["count"] > 0:
|
||||
for _ in range(item["count"]):
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
# add party members if option is enabled
|
||||
if self.multiworld.party_shuffle[self.player]:
|
||||
for i, data in Items.party_item_table.items():
|
||||
pool.append(self.add_item(data["name"], data["classification"], i))
|
||||
for item in Items.party_item_table.values():
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
# handle gesture shuffle
|
||||
if not self.multiworld.gesture_shuffle[self.player]: # add gestures to pool like normal
|
||||
for i, data in Items.gesture_item_table.items():
|
||||
pool.append(self.add_item(data["name"], data["classification"], i))
|
||||
for item in Items.gesture_item_table.values():
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
# add '10 Bones' items if medallion shuffle is enabled
|
||||
if self.multiworld.medallion_shuffle[self.player]:
|
||||
for i, data in Items.medallion_item_table.items():
|
||||
for j in range(data["count"]):
|
||||
pool.append(self.add_item(data["name"], data["classification"], i))
|
||||
for item in Items.medallion_item_table.values():
|
||||
for _ in range(item["count"]):
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
# add to world's pool
|
||||
self.multiworld.itempool += pool
|
||||
@@ -107,48 +101,45 @@ class Hylics2World(World):
|
||||
if self.multiworld.gesture_shuffle[self.player] == 2: # vanilla locations
|
||||
gestures = Items.gesture_item_table
|
||||
self.multiworld.get_location("Waynehouse: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200678]["name"], gestures[200678]["classification"], 200678))
|
||||
.place_locked_item(self.create_item("POROMER BLEB"))
|
||||
self.multiworld.get_location("Afterlife: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200683]["name"], gestures[200683]["classification"], 200683))
|
||||
.place_locked_item(self.create_item("TELEDENUDATE"))
|
||||
self.multiworld.get_location("New Muldul: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200679]["name"], gestures[200679]["classification"], 200679))
|
||||
.place_locked_item(self.create_item("SOUL CRISPER"))
|
||||
self.multiworld.get_location("Viewax's Edifice: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200680]["name"], gestures[200680]["classification"], 200680))
|
||||
.place_locked_item(self.create_item("TIME SIGIL"))
|
||||
self.multiworld.get_location("TV Island: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200681]["name"], gestures[200681]["classification"], 200681))
|
||||
.place_locked_item(self.create_item("CHARGE UP"))
|
||||
self.multiworld.get_location("Juice Ranch: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200682]["name"], gestures[200682]["classification"], 200682))
|
||||
.place_locked_item(self.create_item("FATE SANDBOX"))
|
||||
self.multiworld.get_location("Foglast: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200684]["name"], gestures[200684]["classification"], 200684))
|
||||
.place_locked_item(self.create_item("LINK MOLLUSC"))
|
||||
self.multiworld.get_location("Drill Castle: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200688]["name"], gestures[200688]["classification"], 200688))
|
||||
.place_locked_item(self.create_item("NEMATODE INTERFACE"))
|
||||
self.multiworld.get_location("Sage Airship: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200685]["name"], gestures[200685]["classification"], 200685))
|
||||
.place_locked_item(self.create_item("BOMBO - GENESIS"))
|
||||
|
||||
elif self.multiworld.gesture_shuffle[self.player] == 1: # TVs only
|
||||
gestures = list(Items.gesture_item_table.items())
|
||||
tvs = list(Locations.tv_location_table.items())
|
||||
gestures = [gesture["name"] for gesture in Items.gesture_item_table.values()]
|
||||
tvs = [tv["name"] for tv in Locations.tv_location_table.values()]
|
||||
|
||||
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
|
||||
# placed at Sage Airship: TV or Foglast: TV
|
||||
if self.multiworld.extra_items_in_logic[self.player]:
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
gest = gestures.index((200681, Items.gesture_item_table[200681]))
|
||||
while tv[1]["name"] == "Sage Airship: TV" or tv[1]["name"] == "Foglast: TV":
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
self.multiworld.get_location(tv[1]["name"], self.player)\
|
||||
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
|
||||
gestures[gest]))
|
||||
gestures.remove(gestures[gest])
|
||||
tv = self.random.choice(tvs)
|
||||
while tv == "Sage Airship: TV" or tv == "Foglast: TV":
|
||||
tv = self.random.choice(tvs)
|
||||
self.multiworld.get_location(tv, self.player)\
|
||||
.place_locked_item(self.create_item("CHARGE UP"))
|
||||
gestures.remove("CHARGE UP")
|
||||
tvs.remove(tv)
|
||||
|
||||
for i in range(len(gestures)):
|
||||
gest = self.multiworld.random.choice(gestures)
|
||||
tv = self.multiworld.random.choice(tvs)
|
||||
self.multiworld.get_location(tv[1]["name"], self.player)\
|
||||
.place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[0]))
|
||||
gestures.remove(gest)
|
||||
tvs.remove(tv)
|
||||
self.random.shuffle(gestures)
|
||||
self.random.shuffle(tvs)
|
||||
while gestures:
|
||||
gesture = gestures.pop()
|
||||
tv = tvs.pop()
|
||||
self.get_location(tv).place_locked_item(self.create_item(gesture))
|
||||
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Hylics 2
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
|
||||
@@ -36,8 +36,10 @@ KDL3_STARS_FLAG = SRAM_1_START + 0x901A
|
||||
KDL3_GIFTING_FLAG = SRAM_1_START + 0x901C
|
||||
KDL3_LEVEL_ADDR = SRAM_1_START + 0x9020
|
||||
KDL3_IS_DEMO = SRAM_1_START + 0x5AD5
|
||||
KDL3_GAME_STATE = SRAM_1_START + 0x36D0
|
||||
KDL3_GAME_SAVE = SRAM_1_START + 0x3617
|
||||
KDL3_CURRENT_WORLD = SRAM_1_START + 0x363F
|
||||
KDL3_CURRENT_LEVEL = SRAM_1_START + 0x3641
|
||||
KDL3_GAME_STATE = SRAM_1_START + 0x36D0
|
||||
KDL3_LIFE_COUNT = SRAM_1_START + 0x39CF
|
||||
KDL3_KIRBY_HP = SRAM_1_START + 0x39D1
|
||||
KDL3_BOSS_HP = SRAM_1_START + 0x39D5
|
||||
@@ -46,8 +48,6 @@ KDL3_LIFE_VISUAL = SRAM_1_START + 0x39E3
|
||||
KDL3_HEART_STARS = SRAM_1_START + 0x53A7
|
||||
KDL3_WORLD_UNLOCK = SRAM_1_START + 0x53CB
|
||||
KDL3_LEVEL_UNLOCK = SRAM_1_START + 0x53CD
|
||||
KDL3_CURRENT_WORLD = SRAM_1_START + 0x53CF
|
||||
KDL3_CURRENT_LEVEL = SRAM_1_START + 0x53D3
|
||||
KDL3_BOSS_STATUS = SRAM_1_START + 0x53D5
|
||||
KDL3_INVINCIBILITY_TIMER = SRAM_1_START + 0x54B1
|
||||
KDL3_MG5_STATUS = SRAM_1_START + 0x5EE4
|
||||
@@ -74,7 +74,9 @@ deathlink_messages = defaultdict(lambda: " was defeated.", {
|
||||
0x0202: " was out-numbered by Pon & Con.",
|
||||
0x0203: " was defeated by Ado's powerful paintings.",
|
||||
0x0204: " was clobbered by King Dedede.",
|
||||
0x0205: " lost their battle against Dark Matter."
|
||||
0x0205: " lost their battle against Dark Matter.",
|
||||
0x0300: " couldn't overcome the Boss Butch.",
|
||||
0x0400: " is bad at jumping.",
|
||||
})
|
||||
|
||||
|
||||
@@ -281,6 +283,11 @@ class KDL3SNIClient(SNIClient):
|
||||
for i in range(5):
|
||||
level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14)
|
||||
self.levels[i] = unpack("HHHHHHH", level_data)
|
||||
self.levels[5] = [0x0205, # Hyper Zone
|
||||
0, # MG-5, can't send from here
|
||||
0x0300, # Boss Butch
|
||||
0x0400, # Jumping
|
||||
0, 0, 0]
|
||||
|
||||
if self.consumables is None:
|
||||
consumables = await snes_read(ctx, KDL3_CONSUMABLE_FLAG, 1)
|
||||
@@ -314,7 +321,7 @@ class KDL3SNIClient(SNIClient):
|
||||
current_world = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_WORLD, 2))[0]
|
||||
current_level = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_LEVEL, 2))[0]
|
||||
currently_dead = current_hp[0] == 0x00
|
||||
message = deathlink_messages[self.levels[current_world][current_level - 1]]
|
||||
message = deathlink_messages[self.levels[current_world][current_level]]
|
||||
await ctx.handle_deathlink_state(currently_dead, f"{ctx.player_names[ctx.slot]}{message}")
|
||||
|
||||
recv_count = await snes_read(ctx, KDL3_RECV_COUNT, 2)
|
||||
|
||||
@@ -28,16 +28,30 @@ first_stage_blacklist = {
|
||||
0x77001C, # 5-4 needs Burning
|
||||
}
|
||||
|
||||
first_world_limit = {
|
||||
# We need to limit the number of very restrictive stages in level 1 on solo gens
|
||||
*first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks
|
||||
0x770007,
|
||||
0x770008,
|
||||
0x770013,
|
||||
0x77001E,
|
||||
|
||||
def generate_valid_level(level, stage, possible_stages, slot_random):
|
||||
new_stage = slot_random.choice(possible_stages)
|
||||
if level == 1 and stage == 0 and new_stage in first_stage_blacklist:
|
||||
return generate_valid_level(level, stage, possible_stages, slot_random)
|
||||
else:
|
||||
return new_stage
|
||||
}
|
||||
|
||||
|
||||
def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing.Dict[int, Region]):
|
||||
def generate_valid_level(world: "KDL3World", level, stage, possible_stages, placed_stages):
|
||||
new_stage = world.random.choice(possible_stages)
|
||||
if level == 1:
|
||||
if stage == 0 and new_stage in first_stage_blacklist:
|
||||
return generate_valid_level(world, level, stage, possible_stages, placed_stages)
|
||||
elif not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and \
|
||||
new_stage in first_world_limit and \
|
||||
sum(p_stage in first_world_limit for p_stage in placed_stages) >= 2:
|
||||
return generate_valid_level(world, level, stage, possible_stages, placed_stages)
|
||||
return new_stage
|
||||
|
||||
|
||||
def generate_rooms(world: "KDL3World", level_regions: typing.Dict[int, Region]):
|
||||
level_names = {LocationName.level_names[level]: level for level in LocationName.level_names}
|
||||
room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json")))
|
||||
rooms: typing.Dict[str, KDL3Room] = dict()
|
||||
@@ -49,8 +63,8 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing
|
||||
room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else
|
||||
None for location in room_entry["locations"]
|
||||
if (not any(x in location for x in ["1-Up", "Maxim"]) or
|
||||
world.options.consumables.value) and ("Star" not in location
|
||||
or world.options.starsanity.value)},
|
||||
world.options.consumables.value) and ("Star" not in location
|
||||
or world.options.starsanity.value)},
|
||||
KDL3Location)
|
||||
rooms[room.name] = room
|
||||
for location in room.locations:
|
||||
@@ -62,33 +76,25 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing
|
||||
world.multiworld.regions.extend(world.rooms)
|
||||
|
||||
first_rooms: typing.Dict[int, KDL3Room] = dict()
|
||||
if door_shuffle:
|
||||
# first, we need to generate the notable edge cases
|
||||
# 5-6 is the first, being the most restrictive
|
||||
# half of its rooms are required to be vanilla, but can be in different orders
|
||||
# the room before it *must* contain the copy ability required to unlock the room's goal
|
||||
|
||||
raise NotImplementedError()
|
||||
else:
|
||||
for name, room in rooms.items():
|
||||
if room.room == 0:
|
||||
if room.stage == 7:
|
||||
first_rooms[0x770200 + room.level - 1] = room
|
||||
else:
|
||||
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room
|
||||
exits = dict()
|
||||
for def_exit in room.default_exits:
|
||||
target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
|
||||
access_rule = tuple(def_exit["access_rule"])
|
||||
exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player)
|
||||
room.add_exits(
|
||||
exits.keys(),
|
||||
exits
|
||||
)
|
||||
if world.options.open_world:
|
||||
if any("Complete" in location.name for location in room.locations):
|
||||
room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None},
|
||||
KDL3Location)
|
||||
for name, room in rooms.items():
|
||||
if room.room == 0:
|
||||
if room.stage == 7:
|
||||
first_rooms[0x770200 + room.level - 1] = room
|
||||
else:
|
||||
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room
|
||||
exits = dict()
|
||||
for def_exit in room.default_exits:
|
||||
target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
|
||||
access_rule = tuple(def_exit["access_rule"])
|
||||
exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player)
|
||||
room.add_exits(
|
||||
exits.keys(),
|
||||
exits
|
||||
)
|
||||
if world.options.open_world:
|
||||
if any("Complete" in location.name for location in room.locations):
|
||||
room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None},
|
||||
KDL3Location)
|
||||
|
||||
for level in world.player_levels:
|
||||
for stage in range(6):
|
||||
@@ -102,7 +108,7 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing
|
||||
if world.options.open_world or stage == 0:
|
||||
level_regions[level].add_exits([first_rooms[proper_stage].name])
|
||||
else:
|
||||
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage-1]],
|
||||
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]],
|
||||
world.player).parent_region.add_exits([first_rooms[proper_stage].name])
|
||||
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
|
||||
|
||||
@@ -141,8 +147,7 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte
|
||||
or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage)
|
||||
or (enforce_pattern == enforce_world)
|
||||
]
|
||||
new_stage = generate_valid_level(level, stage, stage_candidates,
|
||||
world.random)
|
||||
new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level])
|
||||
possible_stages.remove(new_stage)
|
||||
levels[level][stage] = new_stage
|
||||
except Exception:
|
||||
@@ -218,7 +223,7 @@ def create_levels(world: "KDL3World") -> None:
|
||||
level_shuffle == 1,
|
||||
level_shuffle == 2)
|
||||
|
||||
generate_rooms(world, False, levels)
|
||||
generate_rooms(world, levels)
|
||||
|
||||
level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location)
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ def set_rules(world: "KDL3World") -> None:
|
||||
for r in [range(1, 31), range(44, 51)]:
|
||||
for i in r:
|
||||
set_rule(world.multiworld.get_location(f"Cloudy Park 4 - Star {i}", world.player),
|
||||
lambda state: can_reach_clean(state, world.player))
|
||||
lambda state: can_reach_coo(state, world.player))
|
||||
for i in [18, *list(range(20, 25))]:
|
||||
set_rule(world.multiworld.get_location(f"Cloudy Park 6 - Star {i}", world.player),
|
||||
lambda state: can_reach_ice(state, world.player))
|
||||
|
||||
@@ -206,6 +206,8 @@ class KDL3World(World):
|
||||
locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
|
||||
items = [self.create_item(animal) for animal in animal_pool]
|
||||
allstate = self.multiworld.get_all_state(False)
|
||||
self.random.shuffle(locations)
|
||||
self.random.shuffle(items)
|
||||
fill_restrictive(self.multiworld, allstate, locations, items, True, True)
|
||||
else:
|
||||
animal_friends = animal_friend_spawns.copy()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Kirby's Dream Land 3
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
@@ -15,6 +15,7 @@ as Heart Stars, 1-Ups, and Invincibility Candy will be shuffled into the pool fo
|
||||
- Purifying a boss after acquiring a certain number of Heart Stars
|
||||
(indicated by their portrait flashing in the level select)
|
||||
- If enabled, 1-Ups and Maxim Tomatoes
|
||||
- If enabled, every single Star Piece within a stage
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
A sound effect will play, and Kirby will immediately receive the effects of that item, such as being able to receive Copy Abilities from enemies that
|
||||
|
||||
@@ -43,8 +43,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The [Player Settings](/games/Kirby's%20Dream%20Land%203/player-settings) page on the website allows you to configure
|
||||
your personal settings and export a config file from them.
|
||||
The [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page on the website allows you to configure
|
||||
your personal options and export a config file from them.
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
@@ -53,7 +53,7 @@ If you would like to validate your config file to make sure it works, you may do
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the [Player Settings](/games/Kirby's%20Dream%20Land%203/player-settings) page, configure your options,
|
||||
1. Navigate to the [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page, configure your options,
|
||||
and click the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
|
||||
@@ -33,7 +33,8 @@ class TestLocations(KDL3TestBase):
|
||||
self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"])
|
||||
self.run_location_test(LocationName.iceberg_samus, ["Ice"])
|
||||
self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"])
|
||||
self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", "Stone", "Ice"])
|
||||
self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean",
|
||||
"Stone", "Ice"])
|
||||
|
||||
def run_location_test(self, location: str, itempool: typing.List[str]):
|
||||
items = itempool.copy()
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
This randomizer creates a more dynamic play experience by randomizing the locations of most items in Kingdom Hearts 2. Currently all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels are randomized. This allows abilities that Sora would normally have to be placed on Keyblades with random stats. Additionally, there are several options for ways to finish the game, allowing for different goals beyond beating the final boss.
|
||||
|
||||
<h2 style="text-transform:none";>Where is the settings page</h2>
|
||||
<h2 style="text-transform:none";>Where is the options page</h2>
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
|
||||
|
||||
|
||||
<h2 style="text-transform:none";>What is randomized in this game?</h2>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2 style="text-transform:none";>Quick Links</h2>
|
||||
|
||||
- [Game Info Page](../../../../games/Kingdom%20Hearts%202/info/en)
|
||||
- [Player Settings Page](../../../../games/Kingdom%20Hearts%202/player-settings)
|
||||
- [Player Options Page](../../../../games/Kingdom%20Hearts%202/player-options)
|
||||
|
||||
<h2 style="text-transform:none";>Required Software:</h2>
|
||||
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts)
|
||||
|
||||
@@ -184,19 +184,22 @@ class LinksAwakeningWorld(World):
|
||||
self.pre_fill_items = []
|
||||
# For any and different world, set item rule instead
|
||||
|
||||
for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
|
||||
option = "shuffle_" + option
|
||||
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
|
||||
option = "shuffle_" + dungeon_item_type
|
||||
option = self.player_options[option]
|
||||
|
||||
dungeon_item_types[option.ladxr_item] = option.value
|
||||
|
||||
# The color dungeon does not contain an instrument
|
||||
num_items = 8 if dungeon_item_type == "instruments" else 9
|
||||
|
||||
if option.value == DungeonItemShuffle.option_own_world:
|
||||
self.multiworld.local_items[self.player].value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10)
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
elif option.value == DungeonItemShuffle.option_different_world:
|
||||
self.multiworld.non_local_items[self.player].value |= {
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10)
|
||||
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
|
||||
}
|
||||
# option_original_dungeon = 0
|
||||
# option_own_dungeons = 1
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Links Awakening DX
|
||||
|
||||
## Where is the settings page?
|
||||
## Where is the options page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
@@ -85,7 +85,7 @@ Title screen graphics by toomanyteeth✨ (https://instagram.com/toomanyyyteeth)
|
||||
<p>The walrus is moved a bit, so that you can access the desert without taking Marin on a date.</p>
|
||||
|
||||
<h3>Logic</h3>
|
||||
<p>Depending on your settings, you can only steal after you find the sword, always, or never.</p>
|
||||
<p>Depending on your options, you can only steal after you find the sword, always, or never.</p>
|
||||
<p>Do not forget that there are two items in the rafting ride. You can access this with just Hookshot or Flippers.</p>
|
||||
<p>Killing enemies with bombs is in normal logic. You can switch to casual logic if you do not want this.</p>
|
||||
<p>D7 confuses some people, but by dropping down pits on the 2nd floor you can access almost all of this dungeon, even without feather and power bracelet.</p>
|
||||
|
||||
@@ -35,8 +35,8 @@ options.
|
||||
|
||||
### Where do I get a config file?
|
||||
|
||||
The [Player Settings](/games/Links%20Awakening%20DX/player-settings) page on the website allows you to configure
|
||||
your personal settings and export a config file from them.
|
||||
The [Player Options](/games/Links%20Awakening%20DX/player-options) page on the website allows you to configure
|
||||
your personal options and export a config file from them.
|
||||
|
||||
### Verifying your config file
|
||||
|
||||
@@ -45,7 +45,7 @@ If you would like to validate your config file to make sure it works, you may do
|
||||
|
||||
## Generating a Single-Player Game
|
||||
|
||||
1. Navigate to the [Player Settings](/games/Links%20Awakening%20DX/player-settings) page, configure your options,
|
||||
1. Navigate to the [Player Options](/games/Links%20Awakening%20DX/player-options) page, configure your options,
|
||||
and click the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page.
|
||||
3. Click the "Create New Room" link.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user