Compare commits

..

2 Commits

Author SHA1 Message Date
30fa0658b0 remove attestation from docker workflow
I don't know what this is or what value this adds, so removing it for now as it doesn't work on gitea
2026-02-23 19:04:15 -08:00
44a0c44036 update docker.yml to create and publish a docker image to dockerhub 2026-02-23 18:54:19 -08:00
121 changed files with 2055 additions and 6971 deletions

View File

@@ -51,7 +51,7 @@ jobs:
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
choco install innosetup --version=6.7.0 --allow-downgrade
choco install innosetup --version=6.2.2 --allow-downgrade
- name: Build
run: |
python -m pip install --upgrade pip

View File

@@ -11,144 +11,43 @@ on:
- "!.github/workflows/**"
- ".github/workflows/docker.yml"
branches:
- "main"
- "dock-dev"
tags:
- "v?[0-9]+.[0-9]+.[0-9]*"
workflow_dispatch:
env:
REGISTRY: ghcr.io
jobs:
prepare:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
outputs:
image-name: ${{ steps.image.outputs.name }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
package-name: ${{ steps.package.outputs.name }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set lowercase image name
id: image
run: |
echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
- name: Set package name
id: package
run: |
echo "name=$(basename ${GITHUB_REPOSITORY,,})" >> $GITHUB_OUTPUT
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
tags: |
type=ref,event=branch,enable={{is_not_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=nightly,enable={{is_default_branch}}
- name: Compute final tags
id: final-tags
run: |
readarray -t tags <<< "${{ steps.meta.outputs.tags }}"
if [[ "${{ github.ref_type }}" == "tag" ]]; then
tag="${{ github.ref_name }}"
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
full_latest="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:latest"
# Check if latest is already in tags to avoid duplicates
if ! printf '%s\n' "${tags[@]}" | grep -q "^$full_latest$"; then
tags+=("$full_latest")
fi
fi
fi
# Set multiline output
echo "tags<<EOF" >> $GITHUB_OUTPUT
printf '%s\n' "${tags[@]}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
build:
needs: prepare
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- platform: amd64
runner: ubuntu-latest
suffix: amd64
cache-scope: amd64
- platform: arm64
runner: ubuntu-24.04-arm
suffix: arm64
cache-scope: arm64
contents: read
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check out the repo
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Compute suffixed tags
id: tags
run: |
readarray -t tags <<< "${{ needs.prepare.outputs.tags }}"
suffixed=()
for t in "${tags[@]}"; do
suffixed+=("$t-${{ matrix.suffix }}")
done
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ubufugu/dockipelago
- name: Build and push Docker image
uses: docker/build-push-action@v5
id: push
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
file: ./Dockerfile
platforms: linux/${{ matrix.platform }}
push: true
tags: ${{ steps.tags.outputs.tags }}
labels: ${{ needs.prepare.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.cache-scope }}
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
provenance: false
manifest:
needs: [prepare, build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push multi-arch manifest
run: |
readarray -t tag_array <<< "${{ needs.prepare.outputs.tags }}"
for tag in "${tag_array[@]}"; do
docker manifest create "$tag" \
"$tag-amd64" \
"$tag-arm64"
docker manifest push "$tag"
done
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

1
.gitignore vendored
View File

@@ -45,7 +45,6 @@ EnemizerCLI/
/SNI/
/sni-*/
/appimagetool*
/VC_redist.x64.exe
/host.yaml
/options.yaml
/config.yaml

View File

@@ -727,7 +727,6 @@ class CollectionState():
advancements: Set[Location]
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
"""Internal cache for Advancement Locations already checked by this CollectionState. Not for use in logic."""
stale: Dict[int, bool]
allow_partial_entrances: bool
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []

View File

@@ -773,7 +773,7 @@ class CommonContext:
if len(parts) == 1:
parts = title.split(', ', 1)
if len(parts) > 1:
text = f"{parts[1]}\n\n{text}" if text else parts[1]
text = parts[1] + '\n\n' + text
title = parts[0]
# display error
self._messagebox = MessageBox(title, text, error=True)
@@ -896,8 +896,6 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
"May not be running Archipelago on that address or port.")
except websockets.InvalidURI:
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
except asyncio.TimeoutError:
ctx.handle_connection_loss("Failed to connect to the multiworld server. Connection timed out.")
except OSError:
ctx.handle_connection_loss("Failed to connect to the multiworld server")
except Exception:

View File

@@ -280,7 +280,6 @@ def remaining_fill(multiworld: MultiWorld,
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None
# going through locations in the same order as the provided `locations` argument
for i, location in enumerate(locations):
if location_can_fill_item(location, item_to_place):
# popping by index is faster than removing by content,

View File

@@ -207,9 +207,6 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
else:
logger.info("Progression balancing skipped.")
AutoWorld.call_all(multiworld, "finalize_multiworld")
AutoWorld.call_all(multiworld, "pre_output")
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
multiworld.random.passthrough = False

View File

@@ -21,7 +21,6 @@ import time
import typing
import weakref
import zlib
from signal import SIGINT, SIGTERM, signal
import ModuleUpdate
@@ -497,8 +496,7 @@ class Context:
self.read_data = {}
# there might be a better place to put this.
race_mode = decoded_obj.get("race_mode", 0)
self.read_data["race_mode"] = lambda: race_mode
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > version_tuple:
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
@@ -1303,13 +1301,6 @@ class CommandMeta(type):
commands.update(base.commands)
commands.update({command_name[5:]: method for command_name, method in attrs.items() if
command_name.startswith("_cmd_")})
for command_name, method in commands.items():
# wrap async def functions so they run on default asyncio loop
if inspect.iscoroutinefunction(method):
def _wrapper(self, *args, _method=method, **kwargs):
return async_start(_method(self, *args, **kwargs))
functools.update_wrapper(_wrapper, method)
commands[command_name] = _wrapper
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)
@@ -2572,8 +2563,6 @@ async def console(ctx: Context):
input_text = await queue.get()
queue.task_done()
ctx.commandprocessor(input_text)
except asyncio.exceptions.CancelledError:
ctx.logger.info("ConsoleTask cancelled")
except:
import traceback
traceback.print_exc()
@@ -2740,26 +2729,6 @@ async def main(args: argparse.Namespace):
console_task = asyncio.create_task(console(ctx))
if ctx.auto_shutdown:
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
def stop():
try:
for remove_signal in [SIGINT, SIGTERM]:
asyncio.get_event_loop().remove_signal_handler(remove_signal)
except NotImplementedError:
pass
ctx.commandprocessor._cmd_exit()
def shutdown(signum, frame):
stop()
try:
for sig in [SIGINT, SIGTERM]:
asyncio.get_event_loop().add_signal_handler(sig, stop)
except NotImplementedError:
# add_signal_handler is only implemented for UNIX platforms
for sig in [SIGINT, SIGTERM]:
signal(sig, shutdown)
await ctx.exit_event.wait()
console_task.cancel()
if ctx.shutdown_task:

View File

@@ -29,7 +29,7 @@ import webbrowser
import re
from urllib.parse import urlparse
from worlds.AutoWorld import AutoWorldRegister, World
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList,
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, Removed,
OptionCounter, Visibility)
@@ -318,28 +318,26 @@ class OptionsCreator(ThemedApp):
else:
self.show_result_snack("Name cannot be longer than 16 characters.")
def create_range(self, option: typing.Type[Range], name: str, bind=True):
def create_range(self, option: typing.Type[Range], name: str):
def update_text(range_box: VisualRange):
self.options[name] = int(range_box.slider.value)
range_box.tag.text = str(int(range_box.slider.value))
return
box = VisualRange(option=option, name=name)
if bind:
box.slider.bind(value=lambda _, _1: update_text(box))
box.slider.bind(on_touch_move=lambda _, _1: update_text(box))
self.options[name] = option.default
return box
def create_named_range(self, option: typing.Type[NamedRange], name: str):
def set_to_custom(range_box: VisualNamedRange):
range_box.range.tag.text = str(int(range_box.range.slider.value))
if range_box.range.slider.value in option.special_range_names.values():
value = next(key for key, val in option.special_range_names.items()
if val == range_box.range.slider.value)
self.options[name] = value
set_button_text(box.choice, value.title())
else:
if (not self.options[name] == range_box.range.slider.value) \
and (not self.options[name] in option.special_range_names or
range_box.range.slider.value != option.special_range_names[self.options[name]]):
# we should validate the touch here,
# but this is much cheaper
self.options[name] = int(range_box.range.slider.value)
range_box.range.tag.text = str(int(range_box.range.slider.value))
set_button_text(range_box.choice, "Custom")
def set_button_text(button: MDButton, text: str):
@@ -348,7 +346,7 @@ class OptionsCreator(ThemedApp):
def set_value(text: str, range_box: VisualNamedRange):
range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start),
option.range_end)
range_box.range.tag.text = str(option.special_range_names[text.lower()])
range_box.range.tag.text = str(int(range_box.range.slider.value))
set_button_text(range_box.choice, text)
self.options[name] = text.lower()
range_box.range.slider.dropdown.dismiss()
@@ -357,18 +355,13 @@ class OptionsCreator(ThemedApp):
# for some reason this fixes an issue causing some to not open
box.range.slider.dropdown.open()
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name, bind=False))
default: int | str = option.default
if default in option.special_range_names:
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
if option.default in option.special_range_names:
# value can get mismatched in this case
box.range.slider.value = min(max(option.special_range_names[default], option.range_start),
box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
option.range_end)
box.range.tag.text = str(int(box.range.slider.value))
elif default in option.special_range_names.values():
# better visual
default = next(key for key, val in option.special_range_names.items() if val == option.default)
set_button_text(box.choice, default.title())
box.range.slider.bind(value=lambda _, _2: set_to_custom(box))
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
items = [
{
"text": choice.title(),
@@ -378,7 +371,7 @@ class OptionsCreator(ThemedApp):
]
box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items)
box.choice.bind(on_release=open_dropdown)
self.options[name] = default
self.options[name] = option.default
return box
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
@@ -454,12 +447,8 @@ class OptionsCreator(ThemedApp):
valid_keys = sorted(option.valid_keys)
if option.verify_item_name:
valid_keys += list(world.item_name_to_id.keys())
if option.convert_name_groups:
valid_keys += list(world.item_name_groups.keys())
if option.verify_location_name:
valid_keys += list(world.location_name_to_id.keys())
if option.convert_name_groups:
valid_keys += list(world.location_name_groups.keys())
if not issubclass(option, OptionCounter):
def apply_changes(button):
@@ -481,6 +470,14 @@ class OptionsCreator(ThemedApp):
dialog.scrollbox.layout.spacing = dp(5)
dialog.scrollbox.layout.padding = [0, dp(5), 0, 0]
if name not in self.options:
# convert from non-mutable to mutable
# We use list syntax even for sets, set behavior is enforced through GUI
if issubclass(option, OptionCounter):
self.options[name] = deepcopy(option.default)
else:
self.options[name] = sorted(option.default)
if issubclass(option, OptionCounter):
for value in sorted(self.options[name]):
dialog.add_set_item(value, self.options[name].get(value, None))
@@ -494,15 +491,6 @@ class OptionsCreator(ThemedApp):
def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] |
typing.Type[OptionCounter], name: str, world: typing.Type[World]):
main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world))
if name not in self.options:
# convert from non-mutable to mutable
# We use list syntax even for sets, set behavior is enforced through GUI
if issubclass(option, OptionCounter):
self.options[name] = deepcopy(option.default)
else:
self.options[name] = sorted(option.default)
return main_button
def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget:

View File

@@ -85,7 +85,6 @@ Currently, the following games are supported:
* APQuest
* Satisfactory
* EarthBound
* Mega Man 3
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

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import os
import sys
import time
import asyncio
import typing
import bsdiff4
@@ -16,9 +15,6 @@ from CommonClient import CommonContext, server_loop, \
gui_enabled, ClientCommandProcessor, logger, get_base_parser
from Utils import async_start
# Heartbeat for position sharing via bounces, in seconds
UNDERTALE_STATUS_INTERVAL = 30.0
UNDERTALE_ONLINE_TIMEOUT = 60.0
class UndertaleCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
@@ -113,11 +109,6 @@ class UndertaleContext(CommonContext):
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
# self.save_game_folder: files go in this path to pass data between us and the actual game
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
self.last_sent_position: typing.Optional[tuple] = None
self.last_room: typing.Optional[str] = None
self.last_status_write: float = 0.0
self.other_undertale_status: dict[int, dict] = {}
def patch_game(self):
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
@@ -228,9 +219,6 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
str(ctx.slot)+" RoutesDone pacifist",
str(ctx.slot)+" RoutesDone genocide"]}])
if any(info.game == "Undertale" and slot != ctx.slot
for slot, info in ctx.slot_info.items()):
ctx.set_notify("undertale_room_status")
if args["slot_data"]["only_flakes"]:
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
f.close()
@@ -275,12 +263,6 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
if "undertale_room_status" in args["keys"] and args["keys"]["undertale_room_status"]:
status = args["keys"]["undertale_room_status"]
ctx.other_undertale_status = {
int(key): val for key, val in status.items()
if int(key) != ctx.slot
}
elif cmd == "SetReply":
if args["value"] is not None:
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
@@ -289,19 +271,17 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
ctx.completed_routes["genocide"] = args["value"]
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
ctx.completed_routes["neutral"] = args["value"]
if args.get("key") == "undertale_room_status" and args.get("value"):
ctx.other_undertale_status = {
int(key): val for key, val in args["value"].items()
if int(key) != ctx.slot
}
elif cmd == "ReceivedItems":
start_index = args["index"]
if start_index == 0:
ctx.items_received = []
elif start_index != len(ctx.items_received):
await ctx.check_locations(ctx.locations_checked)
await ctx.send_msgs([{"cmd": "Sync"}])
sync_msg = [{"cmd": "Sync"}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks",
"locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
if start_index == len(ctx.items_received):
counter = -1
placedWeapon = 0
@@ -388,8 +368,9 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
f.close()
elif cmd == "Bounced":
data = args.get("data", {})
if "x" in data and "room" in data:
tags = args.get("tags", [])
if "Online" in tags:
data = args.get("data", {})
if data["player"] != ctx.slot and data["player"] is not None:
filename = f"FRISK" + str(data["player"]) + ".playerspot"
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
@@ -400,63 +381,21 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
async def multi_watcher(ctx: UndertaleContext):
while not ctx.exit_event.is_set():
if "Online" in ctx.tags and any(
info.game == "Undertale" and slot != ctx.slot
for slot, info in ctx.slot_info.items()):
now = time.time()
path = ctx.save_game_folder
for root, dirs, files in os.walk(path):
for file in files:
if "spots.mine" in file:
with open(os.path.join(root, file), "r") as mine:
this_x = mine.readline()
this_y = mine.readline()
this_room = mine.readline()
this_sprite = mine.readline()
this_frame = mine.readline()
if this_room != ctx.last_room or \
now - ctx.last_status_write >= UNDERTALE_STATUS_INTERVAL:
ctx.last_room = this_room
ctx.last_status_write = now
await ctx.send_msgs([{
"cmd": "Set",
"key": "undertale_room_status",
"default": {},
"want_reply": False,
"operations": [{"operation": "update",
"value": {str(ctx.slot): {"room": this_room,
"time": now}}}]
}])
# If player was visible but timed out (heartbeat) or left the room, remove them.
for slot, entry in ctx.other_undertale_status.items():
if entry.get("room") != this_room or \
now - entry.get("time", now) > UNDERTALE_ONLINE_TIMEOUT:
playerspot = os.path.join(ctx.save_game_folder,
f"FRISK{slot}.playerspot")
if os.path.exists(playerspot):
os.remove(playerspot)
current_position = (this_x, this_y, this_room, this_sprite, this_frame)
if current_position == ctx.last_sent_position:
continue
# Empty status dict = no data yet → send to bootstrap.
online_in_room = any(
entry.get("room") == this_room and
now - entry.get("time", now) <= UNDERTALE_ONLINE_TIMEOUT
for entry in ctx.other_undertale_status.values()
)
if ctx.other_undertale_status and not online_in_room:
continue
message = [{"cmd": "Bounce", "games": ["Undertale"],
"data": {"player": ctx.slot, "x": this_x, "y": this_y,
"room": this_room, "spr": this_sprite,
"frm": this_frame}}]
await ctx.send_msgs(message)
ctx.last_sent_position = current_position
path = ctx.save_game_folder
for root, dirs, files in os.walk(path):
for file in files:
if "spots.mine" in file and "Online" in ctx.tags:
with open(os.path.join(root, file), "r") as mine:
this_x = mine.readline()
this_y = mine.readline()
this_room = mine.readline()
this_sprite = mine.readline()
this_frame = mine.readline()
mine.close()
message = [{"cmd": "Bounce", "tags": ["Online"],
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
"spr": this_sprite, "frm": this_frame}}]
await ctx.send_msgs(message)
await asyncio.sleep(0.1)
@@ -470,9 +409,10 @@ async def game_watcher(ctx: UndertaleContext):
for file in files:
if ".item" in file:
os.remove(os.path.join(root, file))
await ctx.check_locations(ctx.locations_checked)
await ctx.send_msgs([{"cmd": "Sync"}])
sync_msg = [{"cmd": "Sync"}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
if ctx.got_deathlink:
ctx.got_deathlink = False
@@ -507,7 +447,7 @@ async def game_watcher(ctx: UndertaleContext):
for l in lines:
sending = sending+[(int(l.rstrip('\n')))+12000]
finally:
await ctx.check_locations(sending)
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
if "victory" in file and str(ctx.route) in file:
victory = True
if ".playerspot" in file and "Online" not in ctx.tags:

View File

@@ -18,14 +18,11 @@ import logging
import warnings
from argparse import Namespace
from datetime import datetime, timezone
from settings import Settings, get_settings
from time import sleep
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
from yaml import load, load_all, dump
from pathspec import PathSpec, GitIgnoreSpec
from typing_extensions import deprecated
try:
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
@@ -318,7 +315,6 @@ def get_public_ipv6() -> str:
return ip
@deprecated("Utils.get_options() is deprecated. Use the settings API instead.")
def get_options() -> Settings:
deprecate("Utils.get_options() is deprecated. Use the settings API instead.")
return get_settings()
@@ -1007,7 +1003,6 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
def deprecate(message: str, add_stacklevels: int = 0):
"""also use typing_extensions.deprecated wherever you use this"""
if __debug__:
raise Exception(message)
warnings.warn(message, stacklevel=2 + add_stacklevels)
@@ -1072,7 +1067,6 @@ def _extend_freeze_support() -> None:
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop
@deprecated("Use multiprocessing.freeze_support() instead")
def freeze_support() -> None:
"""This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load."""
import multiprocessing
@@ -1293,15 +1287,6 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]
return isinstance(obj, typing.Iterable)
def utcnow() -> datetime:
"""
Implementation of Python's datetime.utcnow() function for use after deprecation.
Needed for timezone-naive UTC datetimes stored in databases with PonyORM (upstream).
https://ponyorm.org/ponyorm-list/2014-August/000113.html
"""
return datetime.now(timezone.utc).replace(tzinfo=None)
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
"""
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.

View File

@@ -11,7 +11,6 @@ from pony.flask import Pony
from werkzeug.routing import BaseConverter
from Utils import title_sorted, get_file_safe_name
from .cli import CLI
UPLOAD_FOLDER = os.path.relpath('uploads')
LOGS_FOLDER = os.path.relpath('logs')
@@ -46,8 +45,6 @@ app.config["SELFGEN"] = True # application process is in charge of scheduling G
app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
# maximum time in seconds since last activity for a room to be hosted
app.config["MAX_ROOM_TIMEOUT"] = 259200
# memory limit for generator processes in bytes
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
@@ -67,7 +64,6 @@ app.config["ASSET_RIGHTS"] = False
cache = Cache()
Compress(app)
CLI(app)
def to_python(value: str) -> uuid.UUID:

View File

@@ -2,20 +2,10 @@
from typing import List, Tuple
from flask import Blueprint
from flask_cors import CORS
from ..models import Seed, Slot
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
cors = CORS(api_endpoints, resources={
r"/api/datapackage/*": {"origins": "*"},
r"/api/datapackage": {"origins": "*"},
r"/api/datapackage_checksum/*": {"origins": "*"},
r"/api/room_status/*": {"origins": "*"},
r"/api/tracker/*": {"origins": "*"},
r"/api/static_tracker/*": {"origins": "*"},
r"/api/slot_data_tracker/*": {"origins": "*"}
})
def get_players(seed: Seed) -> List[Tuple[str, str]]:

View File

@@ -4,14 +4,14 @@ import json
import logging
import multiprocessing
import typing
from datetime import timedelta
from datetime import timedelta, datetime
from threading import Event, Thread
from typing import Any
from uuid import UUID
from pony.orm import db_session, select, commit, PrimaryKey, desc
from pony.orm import db_session, select, commit, PrimaryKey
from Utils import restricted_loads, utcnow
from Utils import restricted_loads
from .locker import Locker, AlreadyRunningException
_stop_event = Event()
@@ -129,11 +129,10 @@ def autohost(config: dict):
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= utcnow() - timedelta(
seconds=config["MAX_ROOM_TIMEOUT"])).order_by(desc(Room.last_port))
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5):
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:

View File

@@ -1,8 +0,0 @@
from flask import Flask
class CLI:
def __init__(self, app: Flask) -> None:
from .stats import stats_cli
app.cli.add_command(stats_cli)

View File

@@ -1,36 +0,0 @@
import click
from flask.cli import AppGroup
from pony.orm import raw_sql
from Utils import format_SI_prefix
stats_cli = AppGroup("stats")
@stats_cli.command("show")
def show() -> None:
from pony.orm import db_session, select
from WebHostLib.models import GameDataPackage
total_games_package_count: int = 0
total_games_package_size: int
top_10_package_sizes: list[tuple[int, str]] = []
with db_session:
data_length = raw_sql("LENGTH(data)")
data_length_desc = raw_sql("LENGTH(data) DESC")
data_length_sum = raw_sql("SUM(LENGTH(data))")
total_games_package_count = GameDataPackage.select().count()
total_games_package_size = select(data_length_sum for _ in GameDataPackage).first() # type: ignore
top_10_package_sizes = list(
select((data_length, dp.checksum) for dp in GameDataPackage) # type: ignore
.order_by(lambda _, _2: data_length_desc)
.limit(10)
)
click.echo(f"Total number of games packages: {total_games_package_count}")
click.echo(f"Total size of games packages: {format_SI_prefix(total_games_package_size, power=1024)}B")
click.echo(f"Top {len(top_10_package_sizes)} biggest games packages:")
for size, checksum in top_10_package_sizes:
click.echo(f" {checksum}: {size:>8d}")

View File

@@ -172,7 +172,7 @@ class WebHostContext(Context):
room.multisave = pickle.dumps(self.get_save())
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
room.last_activity = Utils.utcnow()
room.last_activity = datetime.datetime.utcnow()
return True
def get_save(self) -> dict:
@@ -367,7 +367,8 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
with db_session:
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
room.last_activity = Utils.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
room.last_activity = datetime.datetime.utcnow() - \
datetime.timedelta(minutes=1, seconds=room.timeout)
del room
tear_down_logging(room_id)
logging.info(f"Shutting down room {room_id} on {name}.")

View File

@@ -1,9 +1,8 @@
from datetime import timedelta
from datetime import timedelta, datetime
from flask import render_template
from pony.orm import count
from Utils import utcnow
from WebHostLib import app, cache
from .models import Room, Seed
@@ -11,6 +10,6 @@ from .models import Room, Seed
@app.route('/', methods=['GET', 'POST'])
@cache.cached(timeout=300) # cache has to appear under app route for caching to work
def landing():
rooms = count(room for room in Room if room.creation_time >= utcnow() - timedelta(days=7))
seeds = count(seed for seed in Seed if seed.creation_time >= utcnow() - timedelta(days=7))
rooms = count(room for room in Room if room.creation_time >= datetime.utcnow() - timedelta(days=7))
seeds = count(seed for seed in Seed if seed.creation_time >= datetime.utcnow() - timedelta(days=7))
return render_template("landing.html", rooms=rooms, seeds=seeds)

View File

@@ -9,12 +9,11 @@ from flask import request, redirect, url_for, render_template, Response, session
from pony.orm import count, commit, db_session
from werkzeug.utils import secure_filename
from worlds.AutoWorld import AutoWorldRegister, World
from . import app, cache
from .markdown import render_markdown
from .models import Seed, Room, Command, UUID, uuid4
from Utils import title_sorted, utcnow
from Utils import title_sorted
class WebWorldTheme(StrEnum):
DIRT = "dirt"
@@ -234,12 +233,11 @@ def host_room(room: UUID):
if room is None:
return abort(404)
now = utcnow()
now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port
should_refresh = (
(not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout)
)
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
if now - room.last_activity > datetime.timedelta(minutes=1):
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error
# due to "pony.orm.core.OptimisticCheckError: Object Room was updated outside of current transaction"

View File

@@ -2,8 +2,6 @@ from datetime import datetime
from uuid import UUID, uuid4
from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr
from Utils import utcnow
db = Database()
STATE_QUEUED = 0
@@ -22,8 +20,8 @@ class Slot(db.Entity):
class Room(db.Entity):
id = PrimaryKey(UUID, default=uuid4)
last_activity: datetime = Required(datetime, default=lambda: utcnow(), index=True)
creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
owner = Required(UUID, index=True)
commands = Set('Command')
seed = Required('Seed', index=True)
@@ -40,7 +38,7 @@ class Seed(db.Entity):
rooms = Set(Room)
multidata = Required(bytes, lazy=True)
owner = Required(UUID, index=True)
creation_time: datetime = Required(datetime, default=lambda: utcnow(), index=True) # index used by landing page
creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page
slots = Set(Slot)
spoiler = Optional(LongStr, lazy=True)
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags

View File

@@ -6,7 +6,6 @@ waitress>=3.0.2
Flask-Caching>=2.3.0
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py
Flask-Limiter>=3.12
Flask-Cors>=6.0.2
bokeh>=3.6.3
markupsafe>=3.0.2
setproctitle>=1.3.5

View File

@@ -10,7 +10,7 @@ from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
from Utils import restricted_loads, KeyedDefaultDict, utcnow
from Utils import restricted_loads, KeyedDefaultDict
from . import app, cache
from .models import GameDataPackage, Room
@@ -273,10 +273,9 @@ class TrackerData:
Does not include players who have no activity recorded.
"""
last_activity: Dict[TeamPlayer, datetime.timedelta] = {}
now = utcnow()
now = datetime.datetime.utcnow()
for (team, player), timestamp in self._multisave.get("client_activity_timers", []):
from_timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).replace(tzinfo=None)
last_activity[team, player] = now - from_timestamp
last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
return last_activity

View File

@@ -41,8 +41,16 @@ http {
# server_name example.com www.example.com;
keepalive_timeout 5;
# path for static files
root /app/WebHostLib;
location / {
# checks for static file, if not found proxy to app
try_files $uri @proxy_to_app;
}
location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
@@ -52,15 +60,5 @@ http {
proxy_pass http://app_server;
}
location /static/ {
root /app/WebHostLib/;
autoindex off;
}
location = /favicon.ico {
alias /app/WebHostLib/static/static/favicon.ico;
access_log off;
}
}
}

View File

@@ -134,9 +134,6 @@
# Mega Man 2
/worlds/mm2/ @Silvris
# Mega Man 3
/worlds/mm3/ @Silvris
# MegaMan Battle Network 3
/worlds/mmbn3/ @digiholic

View File

@@ -87,8 +87,7 @@ The world is your game integration for the Archipelago generator, webhost, and m
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/` (see [running from source](/docs/running%20from%20source.md)
for setup).
repository and creating a new world package in `/worlds/`.
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

View File

@@ -46,8 +46,8 @@ which is the correct way to package your `.apworld` as a world developer. Do not
### "Build APWorlds" Launcher Component
In the Archipelago Launcher (on [source only](/docs/running%20from%20source.md)), there is a "Build APWorlds"
component that will package all world folders to `.apworld`, and add `archipelago.json` manifest files to them.
In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
and add `archipelago.json` manifest files to them.
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
The `archipelago.json` file in each .apworld will automatically include the appropriate
`version` and `compatible_version`.

View File

@@ -491,10 +491,9 @@ class MyGameWorld(World):
base_id = 1234
# instead of dynamic numbering, IDs could be part of data
# The following two dicts are required for the generation to know which items exist.
# They can be generated with arbitrary code during world load, but keep in mind that
# anything expensive (e.g. parsing non-python data files) will delay world loading.
# They can include events, but don't have to since events will be placed manually.
# The following two dicts are required for the generation to know which
# items exist. They could be generated from json or something else. They can
# include events, but don't have to since events will be placed manually.
item_name_to_id = {name: id for
id, name in enumerate(mygame_items, base_id)}
location_name_to_id = {name: id for
@@ -771,7 +770,6 @@ class MyGameState(LogicMixin):
new_state.mygame_defeatable_enemies = {
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
}
return new_state
```
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.

View File

@@ -186,20 +186,9 @@ class ERPlacementState:
self.pairings = []
self.world = world
self.coupled = coupled
self.collection_state = world.multiworld.get_all_state(False, True)
self.entrance_lookup = entrance_lookup
# Construct an 'all state', similar to MultiWorld.get_all_state(), but only for the world which is having its
# entrances randomized.
single_player_all_state = CollectionState(world.multiworld, True)
player = world.player
for item in world.multiworld.itempool:
if item.player == player:
world.collect(single_player_all_state, item)
for item in world.get_pre_fill_items():
world.collect(single_player_all_state, item)
single_player_all_state.sweep_for_advancements(world.get_locations())
self.collection_state = single_player_all_state
@property
def placed_regions(self) -> set[Region]:
return self.collection_state.reachable_regions[self.world.player]
@@ -237,7 +226,7 @@ class ERPlacementState:
copied_state.blocked_connections[self.world.player].remove(source_exit)
copied_state.blocked_connections[self.world.player].update(target_entrance.connected_region.exits)
copied_state.update_reachable_regions(self.world.player)
copied_state.sweep_for_advancements(self.world.get_locations())
copied_state.sweep_for_advancements()
# test that at there are newly reachable randomized exits that are ACTUALLY reachable
available_randomized_exits = copied_state.blocked_connections[self.world.player]
for _exit in available_randomized_exits:
@@ -413,7 +402,7 @@ def randomize_entrances(
placed_exits, paired_entrances = er_state.connect(source_exit, target_entrance)
# propagate new connections
er_state.collection_state.update_reachable_regions(world.player)
er_state.collection_state.sweep_for_advancements(world.get_locations())
er_state.collection_state.sweep_for_advancements()
if on_connect:
change = on_connect(er_state, placed_exits, paired_entrances)
if change:

View File

@@ -213,11 +213,6 @@ Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archi
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apmm3"; ValueData: "{#MyAppName}mm3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm3patch"; ValueData: "Archipelago Mega Man 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}mm3patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.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: "";

View File

@@ -248,7 +248,6 @@ class WorldTestBase(unittest.TestCase):
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),

View File

@@ -88,7 +88,6 @@ class TestIDs(unittest.TestCase):
multiworld = setup_solo_multiworld(world_type)
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
call_all(multiworld, "finalize_multiworld")
datapackage = world_type.get_data_package_data()
for item_group, item_names in datapackage["item_name_groups"].items():
self.assertIsInstance(item_group, str,

View File

@@ -46,8 +46,6 @@ class TestImplemented(unittest.TestCase):
with self.subTest(game=game_name, seed=multiworld.seed):
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
call_all(multiworld, "finalize_multiworld")
call_all(multiworld, "pre_output")
for key, data in multiworld.worlds[1].fill_slot_data().items():
self.assertIsInstance(key, str, "keys in slot data must be a string")
convert_to_base_types(data) # only put base data types into slot data
@@ -95,7 +93,6 @@ class TestImplemented(unittest.TestCase):
with self.subTest(game=game_name, seed=multiworld.seed):
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
call_all(multiworld, "finalize_multiworld")
# Note: `multiworld.get_spheres()` iterates a set of locations, so the order that locations are checked
# is nondeterministic and may vary between runs with the same seed.

View File

@@ -123,7 +123,6 @@ class TestBase(unittest.TestCase):
call_all(multiworld, "pre_fill")
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
call_all(multiworld, "finalize_multiworld")
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
for game_name, world_type in AutoWorldRegister.world_types.items():

View File

@@ -1,7 +1,7 @@
import unittest
from BaseClasses import PlandoOptions
from Options import Choice, TextChoice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
from Options import Choice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
from Utils import restricted_dumps
from worlds.AutoWorld import AutoWorldRegister
@@ -16,29 +16,6 @@ class TestOptions(unittest.TestCase):
with self.subTest(game=gamename, option=option_key):
self.assertTrue(option.__doc__)
def test_option_defaults(self):
"""Test that defaults for submitted options are valid."""
for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
if issubclass(option, TextChoice):
self.assertTrue(option.default in option.name_lookup,
f"Default value {option.default} for TextChoice option {option.__name__} in"
f" {gamename} does not resolve to a listed value!"
)
# Standard "can default generate" test
err_raised = None
try:
option.from_any(option.default)
except Exception as ex:
err_raised = ex
self.assertIsNone(err_raised,
f"Default value {option.default} for option {option.__name__} in {gamename}"
f" is not valid! Exception: {err_raised}"
)
def test_options_are_not_set_by_world(self):
"""Test that options attribute is not already set"""
for gamename, world_type in AutoWorldRegister.world_types.items():

View File

@@ -6,7 +6,6 @@ import zipfile
from pathlib import Path
from typing import TYPE_CHECKING, Iterable, Optional, cast
from Utils import utcnow
from WebHostLib import to_python
if TYPE_CHECKING:
@@ -134,7 +133,7 @@ def stop_room(app_client: "FlaskClient",
room_id: str,
timeout: Optional[float] = None,
simulate_idle: bool = True) -> None:
from datetime import timedelta
from datetime import datetime, timedelta
from time import sleep
from pony.orm import db_session
@@ -152,11 +151,10 @@ def stop_room(app_client: "FlaskClient",
with db_session:
room: Room = Room.get(id=room_uuid)
now = utcnow()
if simulate_idle:
new_last_activity = now - timedelta(seconds=room.timeout + 5)
new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
else:
new_last_activity = now - timedelta(days=3)
new_last_activity = datetime.utcnow() - timedelta(days=3)
room.last_activity = new_last_activity
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
if address:
@@ -190,7 +188,6 @@ def stop_room(app_client: "FlaskClient",
if address:
room.timeout = original_timeout
room.last_activity = new_last_activity
room.commands.clear() # make sure there is no leftover /exit
print("timeout restored")

View File

@@ -61,7 +61,6 @@ class TestAllGamesMultiworld(MultiworldTestBase):
with self.subTest("filling multiworld", seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
@@ -79,5 +78,4 @@ class TestTwoPlayerMulti(MultiworldTestBase):
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
call_all(self.multiworld, "finalize_multiworld")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")

View File

@@ -363,7 +363,7 @@ class World(metaclass=AutoWorldRegister):
def __getattr__(self, item: str) -> Any:
if item == "settings":
return getattr(self.__class__, item)
return self.__class__.settings
raise AttributeError
# overridable methods that get called by Main.py, sorted by execution order
@@ -430,23 +430,6 @@ class World(metaclass=AutoWorldRegister):
This happens before progression balancing, so the items may not be in their final locations yet.
"""
def finalize_multiworld(self) -> None:
"""
Optional Method that is called after fill and progression balancing.
This is the last stage of generation where worlds may change logically relevant data,
such as item placements and connections. To not break assumptions,
only ever increase accessibility, never decrease it.
"""
pass
def pre_output(self):
"""
Optional method that is called before output generation.
Items and connections are not meant to be moved anymore,
anything that would affect logical spheres is forbidden at this point.
"""
pass
def generate_output(self, output_directory: str) -> None:
"""
This method gets called from a threadpool, do not use multiworld.random here.

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,15 @@
import collections
from BaseClasses import MultiWorld
from .Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
from .SubClasses import LTTPRegionType
def create_inverted_regions(multiworld: MultiWorld, player: int):
def create_inverted_regions(world, player):
multiworld.regions += [
create_dw_region(multiworld, player, 'Menu', None,
world.regions += [
create_dw_region(world, player, 'Menu', None,
['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']),
create_lw_region(multiworld, player, 'Light World',
create_lw_region(world, player, 'Light World',
['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest',
'Bombos Tablet'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam',
@@ -36,184 +35,184 @@ def create_inverted_regions(multiworld: MultiWorld, player: int):
'Hyrule Castle Entrance (South)', 'Secret Passage Outer Bushes',
'Bush Covered Lawn Outer Bushes',
'Potion Shop Outer Bushes', 'Graveyard Cave Outer Bushes', 'Bomb Hut Outer Bushes']),
create_lw_region(multiworld, player, 'Bush Covered Lawn', None,
create_lw_region(world, player, 'Bush Covered Lawn', None,
['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']),
create_lw_region(multiworld, player, 'Bomb Hut Area', None,
create_lw_region(world, player, 'Bomb Hut Area', None,
['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']),
create_lw_region(multiworld, player, 'Hyrule Castle Secret Entrance Area', None,
create_lw_region(world, player, 'Hyrule Castle Secret Entrance Area', None,
['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']),
create_lw_region(multiworld, player, 'Death Mountain Entrance', None,
create_lw_region(world, player, 'Death Mountain Entrance', None,
['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']),
create_lw_region(multiworld, player, 'Lake Hylia Central Island', None,
create_lw_region(world, player, 'Lake Hylia Central Island', None,
['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']),
create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
"Blind\'s Hideout - Left",
"Blind\'s Hideout - Right",
"Blind\'s Hideout - Far Left",
"Blind\'s Hideout - Far Right"]),
create_lw_region(multiworld, player, 'Northeast Light World', None,
create_lw_region(world, player, 'Northeast Light World', None,
['Zoras River', 'Waterfall of Wishing Cave', 'Potion Shop Outer Rock', 'Catfish Mirror Spot',
'Northeast Light World Warp']),
create_lw_region(multiworld, player, 'Waterfall of Wishing Cave', None,
create_lw_region(world, player, 'Waterfall of Wishing Cave', None,
['Waterfall of Wishing', 'Northeast Light World Return']),
create_lw_region(multiworld, player, 'Potion Shop Area', None,
create_lw_region(world, player, 'Potion Shop Area', None,
['Potion Shop', 'Potion Shop Inner Bushes', 'Potion Shop Inner Rock',
'Potion Shop Mirror Spot', 'Potion Shop River Drop']),
create_lw_region(multiworld, player, 'Graveyard Cave Area', None,
create_lw_region(world, player, 'Graveyard Cave Area', None,
['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']),
create_lw_region(multiworld, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
create_lw_region(world, player, 'River', None, ['Light World Pier', 'Potion Shop Pier']),
create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit',
['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests',
create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests',
['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
create_cave_region(multiworld, player, 'Inverted Links House', 'your house', ['Link\'s House'],
create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
create_cave_region(world, player, 'Inverted Links House', 'your house', ['Link\'s House'],
['Inverted Links House Exit']),
create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
create_cave_region(multiworld, player, 'Elder House', 'a connector', None,
create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
create_cave_region(world, player, 'Elder House', 'a connector', None,
['Elder House Exit (East)', 'Elder House Exit (West)']),
create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'),
create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'),
create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'),
create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'),
create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'),
create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'),
create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla',
create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla',
['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right',
'Sahasrahla']),
create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit',
create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit',
['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']),
create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'],
['Lost Woods Hideout (top to bottom)']),
create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None,
['Lost Woods Hideout Exit']),
create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'],
['Lumberjack Tree (top to bottom)']),
create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items',
create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items',
['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'),
create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'),
create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
create_cave_region(multiworld, player, 'Library', 'the library', ['Library']),
create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'),
create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']),
create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None,
create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
create_cave_region(world, player, 'Library', 'the library', ['Library']),
create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
create_cave_region(world, player, 'Two Brothers House', 'a connector', None,
['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'],
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'],
['Two Brothers House (West)', 'Maze Race Mirror Spot']),
create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'),
create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'],
create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'],
['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)',
'Desert Ledge Drop']),
create_lw_region(multiworld, player, 'Desert Palace Stairs', None,
create_lw_region(world, player, 'Desert Palace Stairs', None,
['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']),
create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None,
create_lw_region(world, player, 'Desert Palace Lone Stairs', None,
['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None,
create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None,
['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks',
'Desert Palace North Mirror Spot']),
create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace',
create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace',
['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key',
'Desert Palace - Desert Tiles 2 Pot Key',
'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace',
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace',
['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
'Eastern Palace - Cannonball Chest',
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
'Eastern Palace - Big Key Chest',
'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'],
['Eastern Palace Exit']),
create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'),
create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle',
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Inverted Ganons Tower', 'Hyrule Castle Ledge Courtyard Drop', 'Inverted Pyramid Hole']),
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle',
['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest',
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Big Key Drop'],
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)',
'Throne Room']),
create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
'Sewers - Secret Room - Right']),
create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
create_dungeon_region(multiworld, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'],
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
create_dungeon_region(world, player, 'Inverted Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Inverted Agahnims Tower Exit']),
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'],
['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
create_cave_region(multiworld, player, 'Old Man House', 'a connector', None,
create_cave_region(world, player, 'Old Man House', 'a connector', None,
['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None,
create_cave_region(world, player, 'Old Man House Back', 'a connector', None,
['Old Man House Exit (Top)', 'Old Man House Back to Front']),
create_lw_region(multiworld, player, 'Death Mountain', None,
create_lw_region(world, player, 'Death Mountain', None,
['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
'Death Mountain Return Cave (East)', 'Spectacle Rock Cave',
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)',
'Death Mountain Mirror Spot']),
create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None,
create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None,
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None,
create_lw_region(world, player, 'Death Mountain Return Ledge', None,
['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)',
'Bumper Cave Ledge Mirror Spot']),
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'],
['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None,
['Spectacle Rock Cave Exit']),
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None,
['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None,
create_lw_region(world, player, 'East Death Mountain (Bottom)', None,
['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
'East Death Mountain Mirror Spot (Bottom)', 'Hookshot Fairy',
'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'),
create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None,
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None,
['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)',
'Light World Death Mountain Shop']),
create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right',
'Paradox Cave Lower - Far Right',
@@ -221,273 +220,273 @@ def create_inverted_regions(multiworld: MultiWorld, player: int):
'Paradox Cave Upper - Left',
'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None,
create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'),
create_lw_region(multiworld, player, 'East Death Mountain (Top)', ['Floating Island'],
create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
create_lw_region(world, player, 'East Death Mountain (Top)', ['Floating Island'],
['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
'East Death Mountain Drop', 'East Death Mountain Mirror Spot (Top)',
'Fairy Ascension Ledge Access', 'Mimic Cave Ledge Access',
'Floating Island Mirror Spot']),
create_lw_region(multiworld, player, 'Spiral Cave Ledge', None,
create_lw_region(world, player, 'Spiral Cave Ledge', None,
['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']),
create_lw_region(multiworld, player, 'Mimic Cave Ledge', None,
create_lw_region(world, player, 'Mimic Cave Ledge', None,
['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']),
create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None,
create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
create_lw_region(world, player, 'Fairy Ascension Plateau', None,
['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
['Fairy Ascension Cave Pots']),
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None,
create_lw_region(world, player, 'Fairy Ascension Ledge', None,
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']),
create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'],
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop',
'Death Mountain (Top) Mirror Spot']),
create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']),
create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', ['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'], ['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', ['Tower of Hera - Big Key Chest']),
create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera', ['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss', 'Tower of Hera - Prize']),
create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'],
create_dw_region(world, player, 'East Dark World', ['Pyramid'],
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
'Dark Lake Hylia Drop (East)',
'Dark Lake Hylia Fairy', 'Palace of Darkness Hint', 'East Dark World Hint',
'Northeast Dark World Broken Bridge Pass', 'East Dark World Teleporter', 'EDW Flute']),
create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
create_dw_region(multiworld, player, 'Northeast Dark World', None,
create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
create_dw_region(world, player, 'Northeast Dark World', None,
['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
'NEDW Flute', 'Dark Lake Hylia Teleporter', 'Catfish Entrance Rock']),
create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'),
create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'),
create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'],
create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
'East Dark World Bridge', 'Inverted Links House', 'Archery Game', 'Bonk Fairy (Dark)',
'Dark Lake Hylia Shop', 'South Dark World Teleporter', 'Post Aga Teleporter', 'SDW Flute']),
create_cave_region(multiworld, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'),
create_dw_region(multiworld, player, 'Dark Lake Hylia', None,
create_cave_region(world, player, 'Inverted Big Bomb Shop', 'the bomb shop'),
create_cave_region(world, player, 'Archery Game', 'a game of skill'),
create_dw_region(world, player, 'Dark Lake Hylia', None,
['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace Missing Wall']),
create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None,
create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
['Dark Lake Hylia Shallows', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']),
create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None,
create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
'Dark Lake Hylia Ledge Spike Cave', 'DLHL Flute']),
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items',
create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
create_dw_region(multiworld, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
create_dw_region(world, player, 'West Dark World', ['Frog', 'Flute Activation Spot'],
['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock',
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)',
'Dark World Lumberjack Shop',
'West Dark World Teleporter', 'WDW Flute']),
create_dw_region(multiworld, player, 'Dark Grassy Lawn', None,
create_dw_region(world, player, 'Dark Grassy Lawn', None,
['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']),
create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']),
create_dw_region(multiworld, player, 'Bumper Cave Entrance', None,
create_dw_region(world, player, 'Bumper Cave Entrance', None,
['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']),
create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'),
create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests',
create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']),
create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'),
create_cave_region(multiworld, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
create_cave_region(world, player, 'Inverted Dark Sanctuary', 'a storyteller', None,
['Inverted Dark Sanctuary Exit']),
create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None,
create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
create_dw_region(multiworld, player, 'Skull Woods Forest', None,
create_dw_region(world, player, 'Skull Woods Forest', None,
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
'Skull Woods First Section Hole (North)',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None,
create_dw_region(world, player, 'Skull Woods Forest (West)', None,
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
'Skull Woods Final Section']),
create_dw_region(multiworld, player, 'Dark Desert', None,
create_dw_region(world, player, 'Dark Desert', None,
['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']),
create_dw_region(multiworld, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests',
create_dw_region(world, player, 'Dark Desert Ledge', None, ['Dark Desert Drop', 'Dark Desert Teleporter']),
create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
['Mire Shed - Left', 'Mire Shed - Right']),
create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'),
create_dw_region(multiworld, player, 'Dark Death Mountain', None,
create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
create_dw_region(world, player, 'Dark Death Mountain', None,
['Dark Death Mountain Drop (East)', 'Inverted Agahnims Tower', 'Superbunny Cave (Top)',
'Hookshot Cave', 'Turtle Rock',
'Spike Cave', 'Dark Death Mountain Fairy', 'Dark Death Mountain Teleporter (West)',
'Turtle Rock Tail Drop', 'DDM Flute']),
create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None,
create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']),
create_dw_region(multiworld, player, 'Turtle Rock (Top)', None,
create_dw_region(world, player, 'Turtle Rock (Top)', None,
['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']),
create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None,
create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
['Turtle Rock Isolated Ledge Entrance']),
create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None,
create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
'Dark Death Mountain Teleporter (East Bottom)', 'EDDM Flute']),
create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector',
create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None,
create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector',
create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
'Hookshot Cave Bomb Wall (North)']),
create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None,
create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
['Floating Island Drop', 'Hookshot Cave Back Entrance']),
create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
'Swamp Palace - Boss', 'Swamp Palace - Prize']),
create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
'Thieves\' Town - Map Chest',
'Thieves\' Town - Compass Chest',
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
'Thieves\' Town - Big Chest',
'Thieves\' Town - Hallway Pot Key',
'Thieves\' Town - Spike Switch Pot Key',
'Thieves\' Town - Blind\'s Cell'],
['Blind Fight']),
create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
create_dungeon_region(multiworld, 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(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
create_dungeon_region(multiworld, 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(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
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 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',
'Ice Palace - Many Pots Pot Key',
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
'Turtle Rock - Roller Room - Right'],
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'],
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
'Turtle Rock Second Section Bomb Wall']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
create_dungeon_region(multiworld, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
create_dungeon_region(world, player, 'Inverted Ganons Tower (Entrance)', 'Ganon\'s Tower',
['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door',
'Inverted Ganons Tower Exit']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'],
['Ganons Tower (Tile Room) Key Door']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower',
['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Left',
'Ganons Tower - Compass Room - Bottom Right',
'Ganons Tower - Conveyor Star Pits Pot Key'],
['Ganons Tower (Bottom) (East)']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower',
['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - Double Switch Pot Key'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower',
['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower',
['Ganons Tower - Randomizer Room - Top Left',
'Ganons Tower - Randomizer Room - Top Right',
'Ganons Tower - Randomizer Room - Bottom Left',
'Ganons Tower - Randomizer Room - Bottom Right'],
['Ganons Tower (Bottom) (West)']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower',
['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest',
'Ganons Tower - Big Key Room - Left',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower',
['Ganons Tower - Mini Helmasaur Room - Left',
'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'],
['Ganons Tower Moldorm Door']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Drop']), # houlihan room exits here in inverted
# to simplify flute connections
create_cave_region(multiworld, player, 'The Sky', 'A Dark Sky', None,
create_cave_region(world, player, 'The Sky', 'A Dark Sky', None,
['DDM Landing', 'NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing',
'DLHL Landing']),
create_lw_region(multiworld, player, 'Desert Northern Cliffs'),
create_lw_region(multiworld, player, 'Death Mountain Bunny Descent Area')
create_lw_region(world, player, 'Desert Northern Cliffs'),
create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
]
def mark_dark_world_regions(multiworld: MultiWorld, player: int):
def mark_dark_world_regions(world, player):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules.
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
seen = set(queue)
while queue:
current = queue.popleft()
@@ -500,7 +499,7 @@ def mark_dark_world_regions(multiworld: MultiWorld, player: int):
seen.add(exit.connected_region)
queue.append(exit.connected_region)
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld)
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
seen = set(queue)
while queue:
current = queue.popleft()

View File

@@ -1,9 +1,8 @@
from collections import namedtuple
import logging
from BaseClasses import ItemClassification, MultiWorld
from BaseClasses import ItemClassification
from Options import OptionError
from typing import TYPE_CHECKING
from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
@@ -15,9 +14,6 @@ from .Options import small_key_shuffle, compass_shuffle, big_key_shuffle, map_sh
from .StateHelpers import has_triforce_pieces, has_melee_weapon
from .Regions import key_drop_data
if TYPE_CHECKING:
from . import ALTTPWorld
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@@ -226,7 +222,7 @@ items_reduction_table = (
)
def generate_itempool(world: "ALTTPWorld"):
def generate_itempool(world):
player: int = world.player
multiworld = world.multiworld
@@ -535,7 +531,7 @@ take_any_locations_inverted.sort()
take_any_locations.sort()
def set_up_take_anys(multiworld: MultiWorld, world: "ALTTPWorld", player: int):
def set_up_take_anys(multiworld, world, player):
# these are references, do not modify these lists in-place
if world.options.mode == 'inverted':
take_any_locs = take_any_locations_inverted
@@ -589,15 +585,15 @@ def set_up_take_anys(multiworld: MultiWorld, world: "ALTTPWorld", player: int):
location.place_locked_item(item_factory("Boss Heart Container", world))
def get_pool_core(multiworld: MultiWorld, player: int):
shuffle = multiworld.worlds[player].options.entrance_shuffle.current_key
difficulty = multiworld.worlds[player].options.item_pool.current_key
timer = multiworld.worlds[player].options.timer.current_key
goal = multiworld.worlds[player].options.goal.current_key
mode = multiworld.worlds[player].options.mode.current_key
swordless = multiworld.worlds[player].options.swordless
retro_bow = multiworld.worlds[player].options.retro_bow
logic = multiworld.worlds[player].options.glitches_required
def get_pool_core(world, player: int):
shuffle = world.worlds[player].options.entrance_shuffle.current_key
difficulty = world.worlds[player].options.item_pool.current_key
timer = world.worlds[player].options.timer.current_key
goal = world.worlds[player].options.goal.current_key
mode = world.worlds[player].options.mode.current_key
swordless = world.worlds[player].options.swordless
retro_bow = world.worlds[player].options.retro_bow
logic = world.worlds[player].options.glitches_required
pool = []
placed_items = {}
@@ -614,13 +610,13 @@ def get_pool_core(multiworld: MultiWorld, player: int):
placed_items[loc] = item
# provide boots to major glitch dependent seeds
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and multiworld.worlds[player].options.glitch_boots:
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots:
precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots')
pool.append('Rupees (20)')
want_progressives = multiworld.worlds[player].options.progressive.want_progressives
want_progressives = world.worlds[player].options.progressive.want_progressives
if want_progressives(multiworld.random):
if want_progressives(world.random):
pool.extend(diff.progressiveglove)
else:
pool.extend(diff.basicglove)
@@ -644,27 +640,27 @@ def get_pool_core(multiworld: MultiWorld, player: int):
thisbottle = None
for _ in range(diff.bottle_count):
if not diff.same_bottle or not thisbottle:
thisbottle = multiworld.random.choice(diff.bottles)
thisbottle = world.random.choice(diff.bottles)
pool.append(thisbottle)
if want_progressives(multiworld.random):
if want_progressives(world.random):
pool.extend(diff.progressiveshield)
else:
pool.extend(diff.basicshield)
if want_progressives(multiworld.random):
if want_progressives(world.random):
pool.extend(diff.progressivearmor)
else:
pool.extend(diff.basicarmor)
if want_progressives(multiworld.random):
if want_progressives(world.random):
pool.extend(diff.progressivemagic)
else:
pool.extend(diff.basicmagic)
if want_progressives(multiworld.random):
if want_progressives(world.random):
pool.extend(diff.progressivebow)
multiworld.worlds[player].has_progressive_bows = True
world.worlds[player].has_progressive_bows = True
elif (swordless or logic == 'no_glitches'):
swordless_bows = ['Bow', 'Silver Bow']
if difficulty == "easy":
@@ -676,7 +672,7 @@ def get_pool_core(multiworld: MultiWorld, player: int):
if swordless:
pool.extend(diff.swordless)
else:
progressive_swords = want_progressives(multiworld.random)
progressive_swords = want_progressives(world.random)
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
extraitems = total_items_to_place - len(pool) - len(placed_items)
@@ -692,29 +688,29 @@ def get_pool_core(multiworld: MultiWorld, player: int):
additional_pieces_to_place = 0
if 'triforce_hunt' in goal:
if multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
treasure_hunt_total = (multiworld.worlds[player].options.triforce_pieces_required.value
+ multiworld.worlds[player].options.triforce_pieces_extra.value)
elif multiworld.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
percentage = float(multiworld.worlds[player].options.triforce_pieces_percentage.value) / 100
treasure_hunt_total = int(round(multiworld.worlds[player].options.triforce_pieces_required.value * percentage, 0))
if world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
treasure_hunt_total = (world.worlds[player].options.triforce_pieces_required.value
+ world.worlds[player].options.triforce_pieces_extra.value)
elif world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
percentage = float(world.worlds[player].options.triforce_pieces_percentage.value) / 100
treasure_hunt_total = int(round(world.worlds[player].options.triforce_pieces_required.value * percentage, 0))
else: # available
treasure_hunt_total = multiworld.worlds[player].options.triforce_pieces_available.value
treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value
triforce_pieces = min(90, max(treasure_hunt_total, multiworld.worlds[player].options.triforce_pieces_required.value))
triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value))
pieces_in_core = min(extraitems, triforce_pieces)
additional_pieces_to_place = triforce_pieces - pieces_in_core
pool.extend(["Triforce Piece"] * pieces_in_core)
extraitems -= pieces_in_core
treasure_hunt_required = multiworld.worlds[player].options.triforce_pieces_required.value
treasure_hunt_required = world.worlds[player].options.triforce_pieces_required.value
for extra in diff.extras:
if extraitems >= len(extra):
pool.extend(extra)
extraitems -= len(extra)
elif extraitems > 0:
pool.extend(multiworld.random.sample(extra, extraitems))
pool.extend(world.random.sample(extra, extraitems))
break
else:
break
@@ -733,25 +729,25 @@ def get_pool_core(multiworld: MultiWorld, player: int):
else:
break
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
pool.extend(diff.universal_keys)
if mode == 'standard':
if multiworld.worlds[player].options.key_drop_shuffle:
if world.worlds[player].options.key_drop_shuffle:
key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
key_location = multiworld.random.choice(key_locations)
key_location = world.random.choice(key_locations)
key_locations.remove(key_location)
place_item(key_location, "Small Key (Universal)")
key_locations += ['Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Boomerang Chest',
'Hyrule Castle - Map Chest']
key_location = multiworld.random.choice(key_locations)
key_location = world.random.choice(key_locations)
key_locations.remove(key_location)
place_item(key_location, "Small Key (Universal)")
key_locations += ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']
key_location = multiworld.random.choice(key_locations)
key_location = world.random.choice(key_locations)
key_locations.remove(key_location)
place_item(key_location, "Small Key (Universal)")
key_locations += ['Sewers - Key Rat Key Drop']
key_location = multiworld.random.choice(key_locations)
key_location = world.random.choice(key_locations)
place_item(key_location, "Small Key (Universal)")
pool = pool[:-3]

View File

@@ -1,24 +1,24 @@
import typing
from BaseClasses import MultiWorld, ItemClassification as IC
from BaseClasses import ItemClassification as IC
from worlds.AutoWorld import World
def GetBeemizerItem(multiworld: MultiWorld, player: int, item):
def GetBeemizerItem(world, player: int, item):
item_name = item if isinstance(item, str) else item.name
if item_name not in trap_replaceable or player in multiworld.groups:
if item_name not in trap_replaceable or player in world.groups:
return item
# first roll - replaceable item should be replaced, within beemizer_total_chance
if not multiworld.worlds[player].options.beemizer_total_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_total_chance / 100):
if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100):
return item
# second roll - bee replacement should be trap, within beemizer_trap_chance
if not multiworld.worlds[player].options.beemizer_trap_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_trap_chance / 100):
return "Bee" if isinstance(item, str) else multiworld.create_item("Bee", player)
if not world.worlds[player].options.beemizer_trap_chance or world.random.random() > (world.worlds[player].options.beemizer_trap_chance / 100):
return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
else:
return "Bee Trap" if isinstance(item, str) else multiworld.create_item("Bee Trap", player)
return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
def item_factory(items: typing.Union[str, typing.Iterable[str]], world: World):

View File

@@ -154,13 +154,13 @@ class OpenPyramid(Choice):
alias_true = option_open
alias_false = option_closed
def to_bool(self, multiworld: MultiWorld, player: int) -> bool:
def to_bool(self, world: MultiWorld, player: int) -> bool:
if self.value == self.option_goal:
return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
elif self.value == self.option_auto:
return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
and (multiworld.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
multiworld.shuffle_ganon)
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
and (world.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
world.shuffle_ganon)
elif self.value == self.option_open:
return True
else:

View File

@@ -2,7 +2,6 @@
Helper functions to deliver entrance/exit/region sets to OWG rules.
"""
from BaseClasses import MultiWorld
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
@@ -201,7 +200,7 @@ def get_mirror_offset_spots_dw():
yield ('Dark Death Mountain Offset Mirror', 'Dark Death Mountain (West Bottom)', 'East Dark World')
def get_mirror_offset_spots_lw(player: int):
def get_mirror_offset_spots_lw(player):
"""
Mirror shenanigans placing a mirror portal with a broken camera
"""
@@ -219,54 +218,54 @@ def get_invalid_bunny_revival_dungeons():
yield 'Sanctuary'
def overworld_glitch_connections(multiworld: MultiWorld, player: int):
def overworld_glitch_connections(world, player):
# Boots-accessible locations.
create_owg_connections(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'))
create_owg_connections(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player))
create_owg_connections(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player))
# Glitched speed drops.
create_owg_connections(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'))
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'))
# Mirror clip spots.
if multiworld.worlds[player].options.mode != 'inverted':
create_owg_connections(player, multiworld, get_mirror_clip_spots_dw())
create_owg_connections(player, multiworld, get_mirror_offset_spots_dw())
if world.worlds[player].options.mode != 'inverted':
create_owg_connections(player, world, get_mirror_clip_spots_dw())
create_owg_connections(player, world, get_mirror_offset_spots_dw())
else:
create_owg_connections(player, multiworld, get_mirror_offset_spots_lw(player))
create_owg_connections(player, world, get_mirror_offset_spots_lw(player))
def overworld_glitches_rules(multiworld: MultiWorld, player: int):
def overworld_glitches_rules(world, player):
# Boots-accessible locations.
set_owg_connection_rules(player, multiworld, get_boots_clip_exits_lw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
set_owg_connection_rules(player, multiworld, get_boots_clip_exits_dw(multiworld.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
# Glitched speed drops.
set_owg_connection_rules(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
if multiworld.worlds[player].options.mode != 'inverted':
add_alternate_rule(multiworld.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
if world.worlds[player].options.mode != 'inverted':
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
# Mirror clip spots.
if multiworld.worlds[player].options.mode != 'inverted':
set_owg_connection_rules(player, multiworld, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
if world.worlds[player].options.mode != 'inverted':
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
else:
set_owg_connection_rules(player, multiworld, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
# Regions that require the boots and some other stuff.
if multiworld.worlds[player].options.mode != 'inverted':
multiworld.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
if world.worlds[player].options.mode != 'inverted':
world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
else:
add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
multiworld.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player)
add_alternate_rule(multiworld.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
add_alternate_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player))
world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player)
add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player))
# Zora's Ledge via waterwalk setup.
add_alternate_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
def add_alternate_rule(entrance, rule):
@@ -274,22 +273,22 @@ def add_alternate_rule(entrance, rule):
entrance.access_rule = lambda state: old_rule(state) or rule(state)
def create_no_logic_connections(player: int, multiworld: MultiWorld, connections):
def create_no_logic_connections(player, world, connections):
for entrance, parent_region, target_region, *rule_override in connections:
parent = multiworld.get_region(parent_region, player)
target = multiworld.get_region(target_region, player)
parent = world.get_region(parent_region, player)
target = world.get_region(target_region, player)
parent.connect(target, entrance)
def create_owg_connections(player: int, multiworld: MultiWorld, connections):
def create_owg_connections(player, world, connections):
for entrance, parent_region, target_region, *rule_override in connections:
parent = multiworld.get_region(parent_region, player)
target = multiworld.get_region(target_region, player)
parent = world.get_region(parent_region, player)
target = world.get_region(target_region, player)
parent.connect(target, entrance)
def set_owg_connection_rules(player: int, multiworld: MultiWorld, connections, default_rule):
def set_owg_connection_rules(player, world, connections, default_rule):
for entrance, _, _, *rule_override in connections:
connection = multiworld.get_entrance(entrance, player)
connection = world.get_entrance(entrance, player)
rule = rule_override[0] if len(rule_override) > 0 else default_rule
connection.access_rule = rule

View File

@@ -9,11 +9,11 @@ def is_main_entrance(entrance: LTTPEntrance) -> bool:
return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
def create_regions(multiworld: MultiWorld, player: int):
def create_regions(world, player):
multiworld.regions += [
create_lw_region(multiworld, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
create_lw_region(multiworld, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
world.regions += [
create_lw_region(world, player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
create_lw_region(world, player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure',
'Purple Chest', 'Flute Activation Spot'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
@@ -24,122 +24,122 @@ def create_regions(multiworld: MultiWorld, player: int):
'Elder House (East)', 'Elder House (West)', 'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Tavern (Front)',
'Bush Covered House', 'Light World Bomb Hut', 'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)', 'Waterfall of Wishing', 'Hyrule Castle Main Gate',
'Bonk Fairy (Light)', '50 Rupee Cave', 'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy', 'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']),
create_lw_region(multiworld, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
create_lw_region(multiworld, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
create_cave_region(multiworld, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
create_lw_region(world, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop']),
create_lw_region(world, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Teleporter']),
create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top",
"Blind\'s Hideout - Left",
"Blind\'s Hideout - Right",
"Blind\'s Hideout - Far Left",
"Blind\'s Hideout - Far Right"]),
create_cave_region(multiworld, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
create_lw_region(multiworld, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
create_cave_region(multiworld, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
create_lw_region(multiworld, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region(multiworld, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(multiworld, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
create_cave_region(multiworld, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
create_cave_region(multiworld, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
create_cave_region(multiworld, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
create_cave_region(multiworld, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
create_cave_region(multiworld, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
create_cave_region(multiworld, player, 'Snitch Lady (East)', 'a boring house'),
create_cave_region(multiworld, player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(multiworld, player, 'Bush Covered House', 'the grass man'),
create_cave_region(multiworld, player, 'Tavern (Front)', 'the tavern'),
create_cave_region(multiworld, player, 'Light World Bomb Hut', 'a restock room'),
create_cave_region(multiworld, player, 'Kakariko Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Fortune Teller (Light)', 'a fortune teller'),
create_cave_region(multiworld, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
create_cave_region(multiworld, player, 'Lumberjack House', 'a boring house'),
create_cave_region(multiworld, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Swamp Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
create_cave_region(multiworld, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
create_cave_region(multiworld, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
create_cave_region(multiworld, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
create_cave_region(world, player, 'Hyrule Castle Secret Entrance', 'a drop\'s exit', ['Link\'s Uncle', 'Secret Passage'], ['Hyrule Castle Secret Entrance Exit']),
create_lw_region(world, player, 'Zoras River', ['King Zora', 'Zora\'s Ledge']),
create_cave_region(world, player, 'Waterfall of Wishing', 'a cave with two chests', ['Waterfall Fairy - Left', 'Waterfall Fairy - Right']),
create_lw_region(world, player, 'Kings Grave Area', None, ['Kings Grave', 'Kings Grave Inner Rocks']),
create_cave_region(world, player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(world, player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
create_cave_region(world, player, 'Dam', 'the dam', ['Floodgate', 'Floodgate Chest']),
create_cave_region(world, player, 'Links House', 'your house', ['Link\'s House'], ['Links House Exit']),
create_cave_region(world, player, 'Chris Houlihan Room', 'I AM ERROR', None, ['Chris Houlihan Room Exit']),
create_cave_region(world, player, 'Tavern', 'the tavern', ['Kakariko Tavern']),
create_cave_region(world, player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
create_cave_region(world, player, 'Snitch Lady (East)', 'a boring house'),
create_cave_region(world, player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(world, player, 'Bush Covered House', 'the grass man'),
create_cave_region(world, player, 'Tavern (Front)', 'the tavern'),
create_cave_region(world, player, 'Light World Bomb Hut', 'a restock room'),
create_cave_region(world, player, 'Kakariko Shop', 'a common shop'),
create_cave_region(world, player, 'Fortune Teller (Light)', 'a fortune teller'),
create_cave_region(world, player, 'Lake Hylia Fortune Teller', 'a fortune teller'),
create_cave_region(world, player, 'Lumberjack House', 'a boring house'),
create_cave_region(world, player, 'Bonk Fairy (Light)', 'a fairy fountain'),
create_cave_region(world, player, 'Bonk Fairy (Dark)', 'a fairy fountain'),
create_cave_region(world, player, 'Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Swamp Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Dark Lake Hylia Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Dark Lake Hylia Ledge Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Dark Desert Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(world, player, 'Chicken House', 'a house with a chest', ['Chicken House']),
create_cave_region(world, player, 'Aginahs Cave', 'a cave with a chest', ['Aginah\'s Cave']),
create_cave_region(world, player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
create_cave_region(world, player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle',
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']),
create_cave_region(multiworld, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(multiworld, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
create_lw_region(multiworld, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
create_cave_region(multiworld, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
create_cave_region(multiworld, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(multiworld, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
create_lw_region(multiworld, player, 'Hobo Bridge', ['Hobo']),
create_cave_region(multiworld, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
create_cave_region(multiworld, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
create_cave_region(multiworld, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
create_cave_region(multiworld, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
create_lw_region(multiworld, player, 'Cave 45 Ledge', None, ['Cave 45']),
create_cave_region(multiworld, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
create_lw_region(multiworld, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
create_cave_region(multiworld, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
create_cave_region(multiworld, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
create_cave_region(multiworld, player, 'Long Fairy Cave', 'a fairy fountain'),
create_cave_region(multiworld, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
create_cave_region(world, player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(world, player, 'Blacksmiths Hut', 'the smith', ['Blacksmith', 'Missing Smith']),
create_lw_region(world, player, 'Bat Cave Drop Ledge', None, ['Bat Cave Drop']),
create_cave_region(world, player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']),
create_cave_region(world, player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(world, player, 'Sick Kids House', 'the sick kid', ['Sick Kid']),
create_lw_region(world, player, 'Hobo Bridge', ['Hobo']),
create_cave_region(world, player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']),
create_cave_region(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, ['Lost Woods Hideout Exit']),
create_cave_region(world, player, 'Lumberjack Tree (top)', 'a drop\'s exit', ['Lumberjack Tree'], ['Lumberjack Tree (top to bottom)']),
create_cave_region(world, player, 'Lumberjack Tree (bottom)', 'a drop\'s exit', None, ['Lumberjack Tree Exit']),
create_lw_region(world, player, 'Cave 45 Ledge', None, ['Cave 45']),
create_cave_region(world, player, 'Cave 45', 'a cave with an item', ['Cave 45']),
create_lw_region(world, player, 'Graveyard Ledge', None, ['Graveyard Cave']),
create_cave_region(world, player, 'Graveyard Cave', 'a cave with an item', ['Graveyard Cave']),
create_cave_region(world, player, 'Checkerboard Cave', 'a cave with an item', ['Checkerboard Cave']),
create_cave_region(world, player, 'Long Fairy Cave', 'a fairy fountain'),
create_cave_region(world, player, 'Mini Moldorm Cave', 'a bounty of five items', ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']),
create_cave_region(multiworld, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
create_cave_region(multiworld, player, 'Good Bee Cave', 'a cold bee'),
create_cave_region(multiworld, player, '20 Rupee Cave', 'a cave with some cash'),
create_cave_region(multiworld, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
create_cave_region(multiworld, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
create_cave_region(multiworld, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
create_cave_region(multiworld, player, 'Library', 'the library', ['Library']),
create_cave_region(multiworld, player, 'Kakariko Gamble Game', 'a game of chance'),
create_cave_region(multiworld, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_lw_region(multiworld, player, 'Lake Hylia Island', ['Lake Hylia Island']),
create_cave_region(multiworld, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
create_cave_region(multiworld, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
create_cave_region(multiworld, player, '50 Rupee Cave', 'a cave with some cash'),
create_lw_region(multiworld, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
create_lw_region(multiworld, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
create_lw_region(multiworld, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
create_lw_region(multiworld, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
create_dungeon_region(multiworld, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
create_cave_region(world, player, 'Ice Rod Cave', 'a cave with a chest', ['Ice Rod Cave']),
create_cave_region(world, player, 'Good Bee Cave', 'a cold bee'),
create_cave_region(world, player, '20 Rupee Cave', 'a cave with some cash'),
create_cave_region(world, player, 'Cave Shop (Lake Hylia)', 'a common shop'),
create_cave_region(world, player, 'Cave Shop (Dark Death Mountain)', 'a common shop'),
create_cave_region(world, player, 'Bonk Rock Cave', 'a cave with a chest', ['Bonk Rock Cave']),
create_cave_region(world, player, 'Library', 'the library', ['Library']),
create_cave_region(world, player, 'Kakariko Gamble Game', 'a game of chance'),
create_cave_region(world, player, 'Potion Shop', 'the potion shop', ['Potion Shop']),
create_lw_region(world, player, 'Lake Hylia Island', ['Lake Hylia Island']),
create_cave_region(world, player, 'Capacity Upgrade', 'the queen of fairies', ['Capacity Upgrade Shop']),
create_cave_region(world, player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_lw_region(world, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)']),
create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'),
create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], ['Desert Palace Entrance (North) Rocks', 'Desert Palace Entrance (West)']),
create_lw_region(world, player, 'Desert Ledge (Northeast)', None, ['Checkerboard Cave']),
create_lw_region(world, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)']),
create_lw_region(world, player, 'Desert Palace Lone Stairs', None, ['Desert Palace Stairs Drop', 'Desert Palace Entrance (East)']),
create_lw_region(world, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks']),
create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'],
['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']),
create_dungeon_region(multiworld, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
create_dungeon_region(multiworld, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
create_dungeon_region(world, player, 'Desert Palace Main (Inner)', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Palace Pots (Inner)']),
create_dungeon_region(world, player, 'Desert Palace East', 'Desert Palace', ['Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest']),
create_dungeon_region(world, player, 'Desert Palace North', 'Desert Palace', ['Desert Palace - Desert Tiles 1 Pot Key', 'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']),
create_dungeon_region(multiworld, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop', 'Eastern Palace - Big Key Chest',
'Eastern Palace - Map Chest', 'Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Palace Exit']),
create_lw_region(multiworld, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(multiworld, player, 'Lost Woods Gamble', 'a game of chance'),
create_lw_region(multiworld, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
create_lw_region(multiworld, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
create_dungeon_region(multiworld, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
create_lw_region(world, player, 'Master Sword Meadow', ['Master Sword Pedestal']),
create_cave_region(world, player, 'Lost Woods Gamble', 'a game of chance'),
create_lw_region(world, player, 'Hyrule Castle Courtyard', None, ['Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Entrance (South)']),
create_lw_region(world, player, 'Hyrule Castle Ledge', None, ['Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)', 'Agahnims Tower', 'Hyrule Castle Ledge Courtyard Drop']),
create_dungeon_region(world, player, 'Hyrule Castle', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest',
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Big Key Drop'],
['Hyrule Castle Exit (East)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (South)', 'Throne Room']),
create_dungeon_region(multiworld, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
create_dungeon_region(multiworld, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
create_dungeon_region(multiworld, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
create_dungeon_region(multiworld, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
create_dungeon_region(world, player, 'Sewer Drop', 'a drop\'s exit', None, ['Sewer Drop']), # This exists only to be referenced for access checks
create_dungeon_region(world, player, 'Sewers (Dark)', 'a drop\'s exit', ['Sewers - Dark Cross', 'Sewers - Key Rat Key Drop'], ['Sewers Door']),
create_dungeon_region(world, player, 'Sewers', 'a drop\'s exit', None, ['Sanctuary Push Door', 'Sewers Back Door', 'Sewers Secret Room']),
create_dungeon_region(world, player, 'Sewers Secret Room', 'a drop\'s exit', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
'Sewers - Secret Room - Right']),
create_dungeon_region(multiworld, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
create_dungeon_region(multiworld, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
create_dungeon_region(multiworld, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
create_cave_region(multiworld, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
create_cave_region(multiworld, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
create_cave_region(multiworld, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
create_lw_region(multiworld, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
create_cave_region(multiworld, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
create_cave_region(multiworld, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
create_lw_region(multiworld, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
create_cave_region(multiworld, player, 'Hookshot Fairy', 'fairies deep in a cave'),
create_cave_region(multiworld, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
create_cave_region(multiworld, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
create_dungeon_region(world, player, 'Sanctuary', 'a drop\'s exit', ['Sanctuary'], ['Sanctuary Exit']),
create_dungeon_region(world, player, 'Agahnims Tower', 'Castle Tower', ['Castle Tower - Room 03', 'Castle Tower - Dark Maze', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], ['Agahnim 1', 'Agahnims Tower Exit']),
create_dungeon_region(world, player, 'Agahnim 1', 'Castle Tower', ['Agahnim 1'], None),
create_cave_region(world, player, 'Old Man Cave', 'a connector', ['Old Man'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']),
create_cave_region(world, player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
create_cave_region(world, player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
create_lw_region(world, player, 'Death Mountain', None, ['Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)', 'Death Mountain Return Cave (East)', 'Spectacle Rock Cave', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Broken Bridge (West)', 'Death Mountain Teleporter']),
create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']),
create_lw_region(world, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)']),
create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
create_cave_region(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
create_lw_region(world, player, 'East Death Mountain (Bottom)', None, ['Broken Bridge (East)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'East Death Mountain Teleporter', 'Hookshot Fairy', 'Fairy Ascension Rocks', 'Spiral Cave (Bottom)']),
create_cave_region(world, player, 'Hookshot Fairy', 'fairies deep in a cave'),
create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
create_cave_region(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left',
'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right',
'Paradox Cave Lower - Far Right',
@@ -147,267 +147,267 @@ def create_regions(multiworld: MultiWorld, player: int):
'Paradox Cave Upper - Left',
'Paradox Cave Upper - Right'],
['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']),
create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None,
create_cave_region(world, player, 'Paradox Cave', 'a connector', None,
['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'),
create_lw_region(multiworld, player, 'East Death Mountain (Top)', None,
create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'),
create_lw_region(world, player, 'East Death Mountain (Top)', None,
['Paradox Cave (Top)', 'Death Mountain (Top)', 'Spiral Cave Ledge Access',
'East Death Mountain Drop', 'Turtle Rock Teleporter', 'Fairy Ascension Ledge']),
create_lw_region(multiworld, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
create_lw_region(world, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']),
create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'],
['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']),
create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None,
create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']),
create_lw_region(world, player, 'Fairy Ascension Plateau', None,
['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']),
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None,
['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None,
['Fairy Ascension Cave Pots']),
create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None,
['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']),
create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None,
create_lw_region(world, player, 'Fairy Ascension Ledge', None,
['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)']),
create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet'],
create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet'],
['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop']),
create_lw_region(multiworld, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
create_dungeon_region(multiworld, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
create_lw_region(world, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']),
create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera',
['Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest'],
['Tower of Hera Small Key Door', 'Tower of Hera Big Key Door', 'Tower of Hera Exit']),
create_dungeon_region(multiworld, player, 'Tower of Hera (Basement)', 'Tower of Hera',
create_dungeon_region(world, player, 'Tower of Hera (Basement)', 'Tower of Hera',
['Tower of Hera - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Tower of Hera (Top)', 'Tower of Hera',
create_dungeon_region(world, player, 'Tower of Hera (Top)', 'Tower of Hera',
['Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss',
'Tower of Hera - Prize']),
create_dw_region(multiworld, player, 'East Dark World', ['Pyramid'],
create_dw_region(world, player, 'East Dark World', ['Pyramid'],
['Pyramid Fairy', 'South Dark World Bridge', 'Palace of Darkness',
'Dark Lake Hylia Drop (East)',
'Hyrule Castle Ledge Mirror Spot', 'Dark Lake Hylia Fairy', 'Palace of Darkness Hint',
'East Dark World Hint', 'Pyramid Hole', 'Northeast Dark World Broken Bridge Pass', ]),
create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
create_dw_region(multiworld, player, 'Northeast Dark World', None,
create_dw_region(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']),
create_dw_region(world, player, 'Northeast Dark World', None,
['West Dark World Gap', 'Dark World Potion Shop', 'East Dark World Broken Bridge Pass',
'Catfish Entrance Rock', 'Dark Lake Hylia Teleporter']),
create_cave_region(multiworld, player, 'Palace of Darkness Hint', 'a storyteller'),
create_cave_region(multiworld, player, 'East Dark World Hint', 'a storyteller'),
create_dw_region(multiworld, player, 'South Dark World', ['Stumpy', 'Digging Game'],
create_cave_region(world, player, 'Palace of Darkness Hint', 'a storyteller'),
create_cave_region(world, player, 'East Dark World Hint', 'a storyteller'),
create_dw_region(world, player, 'South Dark World', ['Stumpy', 'Digging Game'],
['Dark Lake Hylia Drop (South)', 'Hype Cave', 'Swamp Palace', 'Village of Outcasts Heavy Rock',
'Maze Race Mirror Spot',
'Cave 45 Mirror Spot', 'East Dark World Bridge', 'Big Bomb Shop', 'Archery Game',
'Bonk Fairy (Dark)', 'Dark Lake Hylia Shop',
'Bombos Tablet Mirror Spot']),
create_lw_region(multiworld, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
create_cave_region(multiworld, player, 'Big Bomb Shop', 'the bomb shop'),
create_cave_region(multiworld, player, 'Archery Game', 'a game of skill'),
create_dw_region(multiworld, player, 'Dark Lake Hylia', None,
create_lw_region(world, player, 'Bombos Tablet Ledge', ['Bombos Tablet']),
create_cave_region(world, player, 'Big Bomb Shop', 'the bomb shop'),
create_cave_region(world, player, 'Archery Game', 'a game of skill'),
create_dw_region(world, player, 'Dark Lake Hylia', None,
['Lake Hylia Island Mirror Spot', 'East Dark World Pier', 'Dark Lake Hylia Ledge']),
create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None,
create_dw_region(world, player, 'Dark Lake Hylia Central Island', None,
['Ice Palace', 'Lake Hylia Central Island Mirror Spot']),
create_dw_region(multiworld, player, 'Dark Lake Hylia Ledge', None,
create_dw_region(world, player, 'Dark Lake Hylia Ledge', None,
['Dark Lake Hylia Ledge Drop', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
'Dark Lake Hylia Ledge Spike Cave']),
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
create_cave_region(multiworld, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
create_cave_region(multiworld, player, 'Hype Cave', 'a bounty of five items',
create_cave_region(world, player, 'Dark Lake Hylia Ledge Hint', 'a storyteller'),
create_cave_region(world, player, 'Dark Lake Hylia Ledge Spike Cave', 'a spiky hint'),
create_cave_region(world, player, 'Hype Cave', 'a bounty of five items',
['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left',
'Hype Cave - Bottom', 'Hype Cave - Generous Guy']),
create_dw_region(multiworld, player, 'West Dark World', ['Frog'],
create_dw_region(world, player, 'West Dark World', ['Frog'],
['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House',
'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot',
'Bumper Cave Entrance Rock',
'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks',
'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)',
'Dark World Lumberjack Shop']),
create_dw_region(multiworld, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
create_dw_region(world, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']),
create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'],
['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']),
create_dw_region(multiworld, player, 'Bumper Cave Entrance', None,
create_dw_region(world, player, 'Bumper Cave Entrance', None,
['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']),
create_cave_region(multiworld, player, 'Fortune Teller (Dark)', 'a fortune teller'),
create_cave_region(multiworld, player, 'Village of Outcasts Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Dark Lake Hylia Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Dark World Lumberjack Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Dark World Potion Shop', 'a common shop'),
create_cave_region(multiworld, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
create_cave_region(multiworld, player, 'Pyramid Fairy', 'a cave with two chests',
create_cave_region(world, player, 'Fortune Teller (Dark)', 'a fortune teller'),
create_cave_region(world, player, 'Village of Outcasts Shop', 'a common shop'),
create_cave_region(world, player, 'Dark Lake Hylia Shop', 'a common shop'),
create_cave_region(world, player, 'Dark World Lumberjack Shop', 'a common shop'),
create_cave_region(world, player, 'Dark World Potion Shop', 'a common shop'),
create_cave_region(world, player, 'Dark World Hammer Peg Cave', 'a cave with an item', ['Peg Cave']),
create_cave_region(world, player, 'Pyramid Fairy', 'a cave with two chests',
['Pyramid Fairy - Left', 'Pyramid Fairy - Right']),
create_cave_region(multiworld, player, 'Brewery', 'a house with a chest', ['Brewery']),
create_cave_region(multiworld, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
create_cave_region(multiworld, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(multiworld, player, 'Red Shield Shop', 'the rare shop'),
create_cave_region(multiworld, player, 'Dark Sanctuary Hint', 'a storyteller'),
create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None,
create_cave_region(world, player, 'Brewery', 'a house with a chest', ['Brewery']),
create_cave_region(world, player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
create_cave_region(world, player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(world, player, 'Red Shield Shop', 'the rare shop'),
create_cave_region(world, player, 'Dark Sanctuary Hint', 'a storyteller'),
create_cave_region(world, player, 'Bumper Cave', 'a connector', None,
['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'],
['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot']),
create_dw_region(multiworld, player, 'Skull Woods Forest', None,
create_dw_region(world, player, 'Skull Woods Forest', None,
['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)',
'Skull Woods First Section Hole (North)',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None,
create_dw_region(world, player, 'Skull Woods Forest (West)', None,
['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)',
'Skull Woods Final Section']),
create_dw_region(multiworld, player, 'Dark Desert', None,
create_dw_region(world, player, 'Dark Desert', None,
['Misery Mire', 'Mire Shed', 'Desert Ledge (Northeast) Mirror Spot',
'Desert Ledge Mirror Spot', 'Desert Palace Stairs Mirror Spot',
'Desert Palace Entrance (North) Mirror Spot', 'Dark Desert Hint', 'Dark Desert Fairy']),
create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests',
create_cave_region(world, player, 'Mire Shed', 'a cave with two chests',
['Mire Shed - Left', 'Mire Shed - Right']),
create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'),
create_dw_region(multiworld, player, 'Dark Death Mountain (West Bottom)', None,
create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'),
create_dw_region(world, player, 'Dark Death Mountain (West Bottom)', None,
['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']),
create_dw_region(multiworld, player, 'Dark Death Mountain (Top)', None,
create_dw_region(world, player, 'Dark Death Mountain (Top)', None,
['Dark Death Mountain Drop (East)', 'Dark Death Mountain Drop (West)', 'Ganons Tower',
'Superbunny Cave (Top)',
'Hookshot Cave', 'East Death Mountain (Top) Mirror Spot', 'Turtle Rock']),
create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None,
create_dw_region(world, player, 'Dark Death Mountain Ledge', None,
['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)',
'Mimic Cave Mirror Spot', 'Spiral Cave Mirror Spot']),
create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None,
create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None,
['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']),
create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None,
create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None,
['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)',
'Fairy Ascension Mirror Spot']),
create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector',
create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector',
['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None,
create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None,
['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(multiworld, player, 'Hookshot Cave', 'a connector',
create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(world, player, 'Hookshot Cave', 'a connector',
['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
['Hookshot Cave Exit (South)', 'Hookshot Cave Bomb Wall (South)']),
create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
create_cave_region(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)',
'Hookshot Cave Bomb Wall (North)']),
create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None,
create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None,
['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
create_lw_region(multiworld, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
create_dw_region(multiworld, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
create_lw_region(multiworld, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
create_lw_region(world, player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
create_dw_region(world, player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
create_lw_region(world, player, 'Mimic Cave Ledge', None, ['Mimic Cave']),
create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
create_dungeon_region(multiworld, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
create_dungeon_region(multiworld, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
create_dungeon_region(multiworld, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
create_dungeon_region(world, player, 'Swamp Palace (Entrance)', 'Swamp Palace', None, ['Swamp Palace Moat', 'Swamp Palace Exit']),
create_dungeon_region(world, player, 'Swamp Palace (First Room)', 'Swamp Palace', ['Swamp Palace - Entrance'], ['Swamp Palace Small Key Door']),
create_dungeon_region(world, player, 'Swamp Palace (Starting Area)', 'Swamp Palace', ['Swamp Palace - Map Chest', 'Swamp Palace - Pot Row Pot Key',
'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']),
create_dungeon_region(multiworld, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key',
'Swamp Palace - Trench 2 Pot Key'], ['Swamp Palace (North)', 'Swamp Palace (West)']),
create_dungeon_region(multiworld, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
create_dungeon_region(multiworld, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
create_dungeon_region(world, player, 'Swamp Palace (West)', 'Swamp Palace', ['Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest']),
create_dungeon_region(world, player, 'Swamp Palace (North)', 'Swamp Palace', ['Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room',
'Swamp Palace - Boss', 'Swamp Palace - Prize']),
create_dungeon_region(multiworld, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest',
'Thieves\' Town - Map Chest',
'Thieves\' Town - Compass Chest',
'Thieves\' Town - Ambush Chest'], ['Thieves Town Big Key Door', 'Thieves Town Exit']),
create_dungeon_region(multiworld, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
create_dungeon_region(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic',
'Thieves\' Town - Big Chest',
'Thieves\' Town - Hallway Pot Key',
'Thieves\' Town - Spike Switch Pot Key',
'Thieves\' Town - Blind\'s Cell'], ['Blind Fight']),
create_dungeon_region(multiworld, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
create_dungeon_region(multiworld, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
create_dungeon_region(multiworld, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
create_dungeon_region(multiworld, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
create_dungeon_region(multiworld, 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(multiworld, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
create_dungeon_region(multiworld, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
create_dungeon_region(multiworld, 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(multiworld, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
create_dungeon_region(multiworld, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',
create_dungeon_region(world, player, 'Blind Fight', 'Thieves\' Town', ['Thieves\' Town - Boss', 'Thieves\' Town - Prize']),
create_dungeon_region(world, player, 'Skull Woods First Section', 'Skull Woods', ['Skull Woods - Map Chest'], ['Skull Woods First Section Exit', 'Skull Woods First Section Bomb Jump', 'Skull Woods First Section South Door', 'Skull Woods First Section West Door']),
create_dungeon_region(world, player, 'Skull Woods First Section (Right)', 'Skull Woods', ['Skull Woods - Pinball Room'], ['Skull Woods First Section (Right) North Door']),
create_dungeon_region(world, player, 'Skull Woods First Section (Left)', 'Skull Woods', ['Skull Woods - Compass Chest', 'Skull Woods - Pot Prison'], ['Skull Woods First Section (Left) Door to Exit', 'Skull Woods First Section (Left) Door to Right']),
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 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',
'Ice Palace - Many Pots Pot Key',
'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']),
create_dungeon_region(multiworld, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
create_dungeon_region(multiworld, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
create_dungeon_region(multiworld, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
create_dungeon_region(multiworld, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
create_dungeon_region(multiworld, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
create_dungeon_region(world, player, 'Ice Palace (East)', 'Ice Palace', ['Ice Palace - Spike Room'], ['Ice Palace (East Top)']),
create_dungeon_region(world, player, 'Ice Palace (East Top)', 'Ice Palace', ['Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Hammer Block Key Drop']),
create_dungeon_region(world, player, 'Ice Palace (Kholdstare)', 'Ice Palace', ['Ice Palace - Boss', 'Ice Palace - Prize']),
create_dungeon_region(world, player, 'Misery Mire (Entrance)', 'Misery Mire', None, ['Misery Mire Entrance Gap', 'Misery Mire Exit']),
create_dungeon_region(world, player, 'Misery Mire (Main)', 'Misery Mire', ['Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby',
'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
'Misery Mire - Conveyor Crystal Key Drop'], ['Misery Mire (West)', 'Misery Mire Big Key Door']),
create_dungeon_region(multiworld, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
create_dungeon_region(multiworld, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
create_dungeon_region(world, player, 'Misery Mire (West)', 'Misery Mire', ['Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest']),
create_dungeon_region(world, player, 'Misery Mire (Final Area)', 'Misery Mire', None, ['Misery Mire (Vitreous)']),
create_dungeon_region(world, player, 'Misery Mire (Vitreous)', 'Misery Mire', ['Misery Mire - Boss', 'Misery Mire - Prize']),
create_dungeon_region(world, player, 'Turtle Rock (Entrance)', 'Turtle Rock', None, ['Turtle Rock Entrance Gap', 'Turtle Rock Exit (Front)']),
create_dungeon_region(world, player, 'Turtle Rock (First Section)', 'Turtle Rock', ['Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left',
'Turtle Rock - Roller Room - Right'],
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
create_dungeon_region(multiworld, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
create_dungeon_region(world, player, 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest']),
create_dungeon_region(world, player, 'Palace of Darkness (Bonk Section)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest'], ['Palace of Darkness Hammer Peg Drop']),
create_dungeon_region(world, player, 'Palace of Darkness (North)', 'Palace of Darkness', ['Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right'],
['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
create_dungeon_region(multiworld, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
create_dungeon_region(world, player, 'Palace of Darkness (Maze)', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest']),
create_dungeon_region(world, player, 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness', ['Palace of Darkness - Harmless Hellway']),
create_dungeon_region(world, player, 'Palace of Darkness (Final Section)', 'Palace of Darkness', ['Palace of Darkness - Boss', 'Palace of Darkness - Prize']),
create_dungeon_region(world, player, 'Ganons Tower (Entrance)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left',
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Conveyor Cross Pot Key'],
['Ganons Tower (Tile Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower Big Key Door', 'Ganons Tower Exit']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
create_dungeon_region(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['Ganons Tower (Tile Room) Key Door']),
create_dungeon_region(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
'Ganons Tower - Conveyor Star Pits Pot Key'],
['Ganons Tower (Bottom) (East)']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
create_dungeon_region(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - Double Switch Pot Key'],
['Ganons Tower (Map Room)', 'Ganons Tower (Double Switch Room)']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
create_dungeon_region(world, player, 'Ganons Tower (Map Room)', 'Ganon\'s Tower', ['Ganons Tower - Map Chest']),
create_dungeon_region(world, player, 'Ganons Tower (Firesnake Room)', 'Ganon\'s Tower', ['Ganons Tower - Firesnake Room'], ['Ganons Tower (Firesnake Room)']),
create_dungeon_region(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'],
['Ganons Tower (Bottom) (West)']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
create_dungeon_region(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
create_dungeon_region(world, player, 'Ganons Tower (Top)', 'Ganon\'s Tower', None, ['Ganons Tower Torch Rooms']),
create_dungeon_region(world, player, 'Ganons Tower (Before Moldorm)', 'Ganon\'s Tower', ['Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']),
create_dungeon_region(multiworld, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
create_dungeon_region(multiworld, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(multiworld, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(multiworld, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(multiworld, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
create_lw_region(multiworld, player, 'Desert Northern Cliffs'),
create_dw_region(multiworld, player, 'Dark Death Mountain Bunny Descent Area')
create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']),
create_dungeon_region(world, player, 'Agahnim 2', 'Ganon\'s Tower', ['Ganons Tower - Validation Chest', 'Agahnim 2'], None),
create_cave_region(world, player, 'Pyramid', 'a drop\'s exit', ['Ganon'], ['Ganon Drop']),
create_cave_region(world, player, 'Bottom of Pyramid', 'a drop\'s exit', None, ['Pyramid Exit']),
create_dw_region(world, player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop']),
create_lw_region(world, player, 'Desert Northern Cliffs'),
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
]
def create_lw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(multiworld, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)
def create_dw_region(multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(multiworld, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
def create_dw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(world, player, name, LTTPRegionType.DarkWorld, 'Dark World', locations, exits)
def create_cave_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
return _create_region(multiworld, player, name, LTTPRegionType.Cave, hint, locations, exits)
def create_cave_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
return _create_region(world, player, name, LTTPRegionType.Cave, hint, locations, exits)
def create_dungeon_region(multiworld: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
return _create_region(multiworld, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
def create_dungeon_region(world: MultiWorld, player: int, name: str, hint: str, locations=None, exits=None):
return _create_region(world, player, name, LTTPRegionType.Dungeon, hint, locations, exits)
def _create_region(multiworld: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None,
exits=None):
from .SubClasses import ALttPLocation
ret = LTTPRegion(name, type, hint, player, multiworld)
ret = LTTPRegion(name, type, hint, player, world)
if exits:
for exit in exits:
ret.create_exit(exit)
@@ -422,10 +422,10 @@ def _create_region(multiworld: MultiWorld, player: int, name: str, type: LTTPReg
return ret
def mark_light_world_regions(multiworld: MultiWorld, player: int):
def mark_light_world_regions(world, player: int):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
# That is ok. the bunny logic will check for this case and incorporate special rules.
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld)
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld)
seen = set(queue)
while queue:
current = queue.popleft()
@@ -438,7 +438,7 @@ def mark_light_world_regions(multiworld: MultiWorld, player: int):
seen.add(exit.connected_region)
queue.append(exit.connected_region)
queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld)
seen = set(queue)
while queue:
current = queue.popleft()

View File

@@ -19,7 +19,7 @@ import subprocess
import threading
import concurrent.futures
import bsdiff4
from typing import Collection, Optional, List, SupportsIndex, TYPE_CHECKING
from typing import Collection, Optional, List, SupportsIndex
from BaseClasses import CollectionState, Region, Location, MultiWorld
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
@@ -39,9 +39,6 @@ from .Items import item_table, item_name_groups, progression_items
from .EntranceShuffle import door_addresses
from .Options import small_key_shuffle
if TYPE_CHECKING:
from . import ALTTPWorld
try:
from maseya import z3pr
from maseya.z3pr.palette_randomizer import build_offset_collections
@@ -795,13 +792,13 @@ def get_nonnative_item_sprite(code: int) -> int:
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool):
local_random = multiworld.worlds[player].random
local_world = multiworld.worlds[player]
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
local_random = world.worlds[player].random
local_world = world.worlds[player]
# patch items
for location in multiworld.get_locations(player):
for location in world.get_locations(player):
if location.address is None or location.shop_slot is not None:
continue
@@ -855,7 +852,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
# patch entrance/exits/holes
for region in multiworld.get_regions(player):
for region in world.get_regions(player):
for exit in region.exits:
if exit.target is not None:
if isinstance(exit.addresses, tuple):
@@ -888,7 +885,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
rom.write_int16(0x15DB5 + 2 * offset, 0x0640)
elif room_id == 0x00d6 and local_world.fix_trock_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x0134)
elif room_id == 0x000c and multiworld.shuffle_ganon: # fix ganons tower exit point
elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4)
else:
rom.write_int16(0x15DB5 + 2 * offset, link_y)
@@ -908,9 +905,9 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
# patch door table
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
if local_world.options.mode == 'inverted':
patch_shuffled_dark_sanc(multiworld, rom, player)
patch_shuffled_dark_sanc(world, rom, player)
write_custom_shops(rom, multiworld, player)
write_custom_shops(rom, world, player)
def credits_digit(num):
# top: $54 is 1, 55 2, etc , so 57=4, 5C=9
@@ -984,11 +981,11 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
if local_world.options.mode in ['open', 'inverted']:
rom.write_byte(0x180032, 0x01) # open mode
if local_world.options.mode == 'inverted':
set_inverted_mode(multiworld, player, rom)
set_inverted_mode(world, player, rom)
elif local_world.options.mode == 'standard':
rom.write_byte(0x180032, 0x00) # standard mode
uncle_location = multiworld.get_location('Link\'s Uncle', player)
uncle_location = world.get_location('Link\'s Uncle', player)
if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword',
'Fighter Sword', 'Golden Sword',
'Progressive Sword']:
@@ -1283,7 +1280,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
# set up goals for treasure hunt
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece")))
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
@@ -1312,7 +1309,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(multiworld, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(world, player) else 0x00) # pre-open Pyramid Hole
rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
@@ -1330,7 +1327,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
starting_max_bombs = 0 if local_world.options.bombless_start else 10
starting_max_arrows = 30
startingstate = CollectionState(multiworld)
startingstate = CollectionState(world)
if startingstate.has('Silver Bow', player):
equip[0x340] = 1
@@ -1378,7 +1375,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
equip[0x37B] = 1
equip[0x36E] = 0x80
for item in multiworld.precollected_items[player]:
for item in world.precollected_items[player]:
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
'Titans Mitts', 'Power Glove', 'Progressive Glove',
@@ -1593,7 +1590,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
}
def get_reveal_bytes(itemName):
locations = multiworld.find_item_locations(itemName, player)
locations = world.find_item_locations(itemName, player)
if len(locations) < 1:
return 0x0000
location = locations[0]
@@ -1670,7 +1667,7 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
rom.write_byte(0x18004C, 0x01)
# set correct flag for hera basement item
hera_basement = multiworld.get_location('Tower of Hera - Basement Cage', player)
hera_basement = world.get_location('Tower of Hera - Basement Cage', player)
if hera_basement.item is not None and hera_basement.item.name == 'Small Key (Tower of Hera)' and hera_basement.item.player == player:
rom.write_byte(0x4E3BB, 0xE4)
else:
@@ -1687,26 +1684,27 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
rom.write_byte(0xFEE41, 0x2A) # bombable exit
if local_world.options.tile_shuffle:
tile_set = TileSet.get_random_tile_set(multiworld.worlds[player].random)
tile_set = TileSet.get_random_tile_set(world.worlds[player].random)
rom.write_byte(0x4BA21, tile_set.get_speed())
rom.write_byte(0x4BA1D, tile_set.get_len())
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
write_strings(rom, multiworld, player)
write_strings(rom, world, player)
# remote items flag, does not currently work
rom.write_byte(0x18637C, 0)
# set rom name
# 21 bytes
rom.name = bytearray(f'AP{local_world.world_version.as_simple_string().replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21]
from Utils import __version__
rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
# set player names
encoded_players = multiworld.players + len(multiworld.groups)
encoded_players = world.players + len(world.groups)
for p in range(1, min(encoded_players, ROM_PLAYER_LIMIT) + 1):
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(multiworld.player_name[p]))
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p]))
if encoded_players > ROM_PLAYER_LIMIT:
rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago"))
@@ -1725,9 +1723,9 @@ def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool
return rom
def patch_race_rom(rom: LocalRom, multiworld: MultiWorld, player: int):
def patch_race_rom(rom, world, player):
rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed
rom.encrypt(multiworld, player)
rom.encrypt(world, player)
def get_price_data(price: int, price_type: int) -> List[int]:
@@ -1740,8 +1738,8 @@ def get_price_data(price: int, price_type: int) -> List[int]:
return int16_as_bytes(price)
def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
shops = sorted([shop for shop in multiworld.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
def write_custom_shops(rom, world, player):
shops = sorted([shop for shop in world.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset)
shop_data = bytearray()
items_data = bytearray()
@@ -1760,9 +1758,9 @@ def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
slot = 0 if shop.type == ShopType.TakeAny else index
if item is None:
break
if multiworld.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
count_shop = (shop.region.name != 'Potion Shop' or multiworld.worlds[player].options.include_witch_hut) and \
(shop.region.name != 'Capacity Upgrade' or multiworld.worlds[player].options.shuffle_capacity_upgrades)
if world.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
count_shop = (shop.region.name != 'Potion Shop' or world.worlds[player].options.include_witch_hut) and \
(shop.region.name != 'Capacity Upgrade' or world.worlds[player].options.shuffle_capacity_upgrades)
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
if item['item'] == 'Single Arrow' and item['player'] == 0:
arrow_mask |= 1 << index
@@ -1775,11 +1773,11 @@ def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
price_data = get_price_data(item['price'], item["price_type"])
replacement_price_data = get_price_data(item['replacement_price'], item['replacement_price_type'])
slot = 0 if shop.type == ShopType.TakeAny else index
if item['player'] and multiworld.game[item['player']] != "A Link to the Past": # item not native to ALTTP
item_code = get_nonnative_item_sprite(multiworld.worlds[item['player']].item_name_to_id[item['item']])
if item['player'] and world.game[item['player']] != "A Link to the Past": # item not native to ALTTP
item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
else:
item_code = item_table[item["item"]].item_code
if item['item'] == 'Single Arrow' and item['player'] == 0 and multiworld.worlds[player].options.retro_bow:
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow:
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
item_data = [shop_id, item_code] + price_data + \
@@ -1792,12 +1790,12 @@ def write_custom_shops(rom: LocalRom, multiworld: MultiWorld, player: int):
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
rom.write_bytes(0x184900, items_data)
if multiworld.worlds[player].options.retro_bow:
if world.worlds[player].options.retro_bow:
retro_shop_slots.append(0xFF)
rom.write_bytes(0x186540, retro_shop_slots)
def hud_format_text(text: str):
def hud_format_text(text):
output = bytes()
for char in text.lower():
if 'a' <= char <= 'z':
@@ -1814,7 +1812,7 @@ def hud_format_text(text: str):
output += b'\x7f\x00'
return output[:32]
def apply_oof_sfx(rom: LocalRom, oof: str):
def apply_oof_sfx(rom, oof: str):
with open(oof, 'rb') as stream:
oof_bytes = bytearray(stream.read())
@@ -1864,10 +1862,9 @@ def apply_oof_sfx(rom: LocalRom, oof: str):
rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08])
def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, menuspeed: str, music: bool, sprite: str,
oof: str, palettes_options: dict[str, str], world: "ALTTPWorld | None" = None, player: int = 1,
allow_random_on_event: bool = False, reduceflashing: bool = False, triforcehud: str = None,
deathlink: bool = False, allowcollect: bool = False):
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
local_random = random if not world else world.worlds[player].random
disable_music: bool = not music
# enable instant item menu
@@ -1951,7 +1948,7 @@ def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, me
rom.write_byte(0x180167, triforce_flag)
if z3pr:
def buildAndRandomize(option_name: str, mode: str):
def buildAndRandomize(option_name, mode):
options = {
option_name: True
}
@@ -2015,7 +2012,7 @@ def apply_rom_settings(rom: LocalRom, beep: str, color: str, quickswap: bool, me
rom.write_crc()
def restore_maseya_colors(rom: LocalRom, offsets_array: list[list[int]]):
def restore_maseya_colors(rom, offsets_array):
if not rom.orig_buffer:
return
for offsetC in offsets_array:
@@ -2023,7 +2020,7 @@ def restore_maseya_colors(rom: LocalRom, offsets_array: list[list[int]]):
rom.write_bytes(address, rom.orig_buffer[address:address + 2])
def set_color(rom: LocalRom, address: int, color: tuple[int, int, int], shade: int):
def set_color(rom, address, color, shade):
r = round(min(color[0], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
g = round(min(color[1], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
b = round(min(color[2], 0xFF) * pow(0.8, shade) * 0x1F / 0xFF)
@@ -2031,7 +2028,7 @@ def set_color(rom: LocalRom, address: int, color: tuple[int, int, int], shade: i
rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False))
def default_ow_palettes(rom: LocalRom):
def default_ow_palettes(rom):
if not rom.orig_buffer:
return
rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4])
@@ -2040,7 +2037,7 @@ def default_ow_palettes(rom: LocalRom):
rom.write_bytes(address, rom.orig_buffer[address:address + 2])
def randomize_ow_palettes(rom: LocalRom, local_random: random.Random):
def randomize_ow_palettes(rom, local_random):
grass, grass2, grass3, dirt, dirt2, water, clouds, dwdirt, \
dwgrass, dwwater, dwdmdirt, dwdmgrass, dwdmclouds1, dwdmclouds2 = [[local_random.randint(60, 215) for _ in range(3)]
for _ in range(14)]
@@ -2116,7 +2113,7 @@ def randomize_ow_palettes(rom: LocalRom, local_random: random.Random):
set_color(rom, address, color, shade)
def blackout_ow_palettes(rom: LocalRom):
def blackout_ow_palettes(rom):
rom.write_bytes(0xDE604, [0] * 0xC4)
for i in range(0xDE6C8, 0xDE86C, 70):
rom.write_bytes(i, [0] * 64)
@@ -2127,13 +2124,13 @@ def blackout_ow_palettes(rom: LocalRom):
rom.write_bytes(address, [0, 0])
def default_uw_palettes(rom: LocalRom):
def default_uw_palettes(rom):
if not rom.orig_buffer:
return
rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544])
def randomize_uw_palettes(rom: LocalRom, local_random: random.Random):
def randomize_uw_palettes(rom, local_random):
for dungeon in range(20):
wall, pot, chest, floor1, floor2, floor3 = [[local_random.randint(60, 240) for _ in range(3)] for _ in range(6)]
@@ -2180,7 +2177,7 @@ def randomize_uw_palettes(rom: LocalRom, local_random: random.Random):
set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4)
def blackout_uw_palettes(rom: LocalRom):
def blackout_uw_palettes(rom):
for i in range(0xDD734, 0xDE544, 180):
rom.write_bytes(i, [0] * 38)
rom.write_bytes(i + 44, [0] * 76)
@@ -2191,25 +2188,25 @@ def get_hash_string(hash):
return ", ".join([hash_alphabet[code & 0x1F] for code in hash])
def write_string_to_rom(rom: LocalRom, target: str, string: str):
def write_string_to_rom(rom, target, string):
address, maxbytes = text_addresses[target]
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
def write_strings(rom, world, player):
from . import ALTTPWorld
local_random = multiworld.worlds[player].random
w: ALTTPWorld = multiworld.worlds[player]
local_random = world.worlds[player].random
w: ALTTPWorld = world.worlds[player]
tt = TextTable()
tt.removeUnwantedText()
# Let's keep this guy's text accurate to the shuffle setting.
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
if multiworld.worlds[player].options.mode == 'inverted':
if world.worlds[player].options.mode == 'inverted':
tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n'
def hint_text(dest, ped_hint=False):
@@ -2221,45 +2218,45 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
hint = dest.hint_text
if dest.player != player:
if ped_hint:
hint += f" for {multiworld.player_name[dest.player]}!"
hint += f" for {world.player_name[dest.player]}!"
elif isinstance(dest, (Region, Location)):
hint += f" in {multiworld.player_name[dest.player]}'s world"
hint += f" in {world.player_name[dest.player]}'s world"
else:
hint += f" for {multiworld.player_name[dest.player]}"
hint += f" for {world.player_name[dest.player]}"
return hint
if multiworld.worlds[player].options.scams.gives_king_zora_hint:
if world.worlds[player].options.scams.gives_king_zora_hint:
# Zora hint
zora_location = multiworld.get_location("King Zora", player)
zora_location = world.get_location("King Zora", player)
tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
if multiworld.worlds[player].options.scams.gives_bottle_merchant_hint:
if world.worlds[player].options.scams.gives_bottle_merchant_hint:
# Bottle Vendor hint
vendor_location = multiworld.get_location("Bottle Merchant", player)
vendor_location = world.get_location("Bottle Merchant", player)
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
f"\n ≥ I want\n no way!\n{{CHOICE}}"
# First we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
if multiworld.worlds[player].options.hints:
if multiworld.worlds[player].options.hints.value >= 2:
if multiworld.worlds[player].options.hints == "full":
if world.worlds[player].options.hints:
if world.worlds[player].options.hints.value >= 2:
if world.worlds[player].options.hints == "full":
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!'
else:
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
hint_locations = HintLocations.copy()
local_random.shuffle(hint_locations)
all_entrances = list(multiworld.get_entrances(player))
all_entrances = list(world.get_entrances(player))
local_random.shuffle(all_entrances)
# First we take care of the one inconvenient dungeon in the appropriately simple shuffles.
entrances_to_hint = {}
entrances_to_hint.update(InconvenientDungeonEntrances)
if multiworld.shuffle_ganon:
if multiworld.worlds[player].options.mode == 'inverted':
if world.shuffle_ganon:
if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
else:
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
if multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
if world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
@@ -2269,9 +2266,9 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
break
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
entrances_to_hint.update(InconvenientOtherEntrances)
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
hint_count = 0
elif multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
hint_count = 2
else:
hint_count = 4
@@ -2288,31 +2285,31 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
# Next we handle hints for randomly selected other entrances,
# curating the selection intelligently based on shuffle.
if multiworld.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(DungeonEntrances)
if multiworld.worlds[player].options.mode == 'inverted':
if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
else:
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
elif multiworld.worlds[player].options.entrance_shuffle == 'restricted':
elif world.worlds[player].options.entrance_shuffle == 'restricted':
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(OtherEntrances)
if multiworld.worlds[player].options.mode == 'inverted':
if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'})
else:
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
if multiworld.worlds[player].options.entrance_shuffle != 'insanity':
if world.worlds[player].options.entrance_shuffle != 'insanity':
entrances_to_hint.update(InsanityEntrances)
if multiworld.shuffle_ganon:
if multiworld.worlds[player].options.mode == 'inverted':
if world.shuffle_ganon:
if world.worlds[player].options.mode == 'inverted':
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
else:
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
hint_count = 4 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
hint_count = 4 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 0
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
@@ -2327,77 +2324,77 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
locations_to_hint = InconvenientLocations.copy()
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
locations_to_hint.extend(InconvenientVanillaLocations)
local_random.shuffle(locations_to_hint)
hint_count = 3 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
hint_count = 3 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 5
for location in locations_to_hint[:hint_count]:
if location == 'Swamp Left':
if local_random.randint(0, 1):
first_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item)
second_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item)
first_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
second_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
else:
second_item = hint_text(multiworld.get_location('Swamp Palace - West Chest', player).item)
first_item = hint_text(multiworld.get_location('Swamp Palace - Big Key Chest', player).item)
second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.')
tt[hint_locations.pop(0)] = this_hint
elif location == 'Mire Left':
if local_random.randint(0, 1):
first_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item)
second_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item)
first_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
second_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
else:
second_item = hint_text(multiworld.get_location('Misery Mire - Compass Chest', player).item)
first_item = hint_text(multiworld.get_location('Misery Mire - Big Key Chest', player).item)
second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.')
tt[hint_locations.pop(0)] = this_hint
elif location == 'Tower of Hera - Big Key Chest':
this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text(
multiworld.get_location(location, player).item) + '.'
world.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Ganons Tower - Big Chest':
this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text(
multiworld.get_location(location, player).item) + '.'
world.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Thieves\' Town - Big Chest':
this_hint = 'The big chest in Thieves\' Town contains ' + hint_text(
multiworld.get_location(location, player).item) + '.'
world.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Ice Palace - Big Chest':
this_hint = 'The big chest in Ice Palace contains ' + hint_text(
multiworld.get_location(location, player).item) + '.'
world.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Eastern Palace - Big Key Chest':
this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text(
multiworld.get_location(location, player).item) + '.'
world.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Sahasrahla':
this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text(
multiworld.get_location(location, player).item) + '.'
world.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
elif location == 'Graveyard Cave':
this_hint = 'The cave north of the graveyard contains ' + hint_text(
multiworld.get_location(location, player).item) + '.'
world.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
else:
this_hint = location + ' contains ' + hint_text(multiworld.get_location(location, player).item) + '.'
this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.'
tt[hint_locations.pop(0)] = this_hint
# Lastly we write hints to show where certain interesting items are.
items_to_hint = RelevantItems.copy()
if multiworld.worlds[player].options.small_key_shuffle.hints_useful:
if world.worlds[player].options.small_key_shuffle.hints_useful:
items_to_hint |= item_name_groups["Small Keys"]
if multiworld.worlds[player].options.big_key_shuffle.hints_useful:
if world.worlds[player].options.big_key_shuffle.hints_useful:
items_to_hint |= item_name_groups["Big Keys"]
if multiworld.worlds[player].options.hints == "full":
if world.worlds[player].options.hints == "full":
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
else:
hint_count = 5 if multiworld.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
'dungeons_crossed'] else 8
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
if hint_count:
locations = multiworld.find_items_in_locations(items_to_hint, player, True)
locations = world.find_items_in_locations(items_to_hint, player, True)
local_random.shuffle(locations)
# make locked locations less likely to appear as hint,
# chances are the lock means the player already knows.
@@ -2417,15 +2414,15 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
# We still need the older hints of course. Those are done here.
silverarrows = multiworld.find_item_locations('Silver Bow', player, True)
silverarrows = world.find_item_locations('Silver Bow', player, True)
local_random.shuffle(silverarrows)
silverarrow_hint = (
' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
if multiworld.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
multiworld.worlds[player].options.swordless or multiworld.worlds[player].options.glitches_required == 'no_glitches')):
prog_bow_locs = multiworld.find_item_locations('Progressive Bow', player, True)
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
world.worlds[player].options.swordless or world.worlds[player].options.glitches_required == 'no_glitches')):
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
local_random.shuffle(prog_bow_locs)
found_bow = False
found_bow_alt = False
@@ -2440,34 +2437,34 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
silverarrow_hint = (' %s?' % hint_text(bow_loc).replace('Ganon\'s', 'my'))
tt[target] = 'Did you find the silver arrows%s' % silverarrow_hint
crystal5 = multiworld.find_item('Crystal 5', player)
crystal6 = multiworld.find_item('Crystal 6', player)
crystal5 = world.find_item('Crystal 5', player)
crystal6 = world.find_item('Crystal 6', player)
tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (
crystal5.hint_text, crystal6.hint_text)
greenpendant = multiworld.find_item('Green Pendant', player)
greenpendant = world.find_item('Green Pendant', player)
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
if multiworld.worlds[player].options.crystals_needed_for_gt == 1:
if world.worlds[player].options.crystals_needed_for_gt == 1:
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
else:
tt['sign_ganons_tower'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
tt['sign_ganons_tower'] = f'You need {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
if multiworld.worlds[player].options.goal == 'bosses':
if world.worlds[player].options.goal == 'bosses':
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
elif multiworld.worlds[player].options.goal == 'ganon_pedestal':
elif world.worlds[player].options.goal == 'ganon_pedestal':
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
elif multiworld.worlds[player].options.goal == "ganon":
if multiworld.worlds[player].options.crystals_needed_for_ganon == 1:
elif world.worlds[player].options.goal == "ganon":
if world.worlds[player].options.crystals_needed_for_ganon == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.'
else:
tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
f'have beaten Agahnim atop Ganons Tower'
else:
if multiworld.worlds[player].options.crystals_needed_for_ganon == 1:
if world.worlds[player].options.crystals_needed_for_ganon == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
else:
tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)]
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)]
@@ -2478,12 +2475,12 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)]
triforce_pieces_required = max(0, w.treasure_hunt_required -
sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece"))
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
if multiworld.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
if multiworld.worlds[player].options.goal == 'triforce_hunt' and multiworld.players > 1:
if world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
else:
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
@@ -2497,7 +2494,7 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
(triforce_pieces_required, w.treasure_hunt_total)
elif multiworld.worlds[player].options.goal in ['pedestal']:
elif world.worlds[player].options.goal in ['pedestal']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
@@ -2506,44 +2503,44 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
if triforce_pieces_required > 1:
if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1:
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
else:
if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.players > 1:
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
elif multiworld.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
(triforce_pieces_required, w.treasure_hunt_total)
tt['kakariko_tavern_fisherman'] = TavernMan_texts[local_random.randint(0, len(TavernMan_texts) - 1)]
pedestalitem = multiworld.get_location('Master Sword Pedestal', player).item
pedestalitem = world.get_location('Master Sword Pedestal', player).item
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem,
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
tt['mastersword_pedestal_translated'] = pedestal_text
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \
w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item')
etheritem = multiworld.get_location('Ether Tablet', player).item
etheritem = world.get_location('Ether Tablet', player).item
ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem,
True) if etheritem.pedestal_hint_text is not None else 'Unknown Item'
tt['tablet_ether_book'] = ether_text
bombositem = multiworld.get_location('Bombos Tablet', player).item
bombositem = world.get_location('Bombos Tablet', player).item
bombos_text = 'Some Hot Air' if bombositem is None else hint_text(bombositem,
True) if bombositem.pedestal_hint_text is not None else 'Unknown Item'
tt['tablet_bombos_book'] = bombos_text
# inverted spawn menu changes
if multiworld.worlds[player].options.mode == 'inverted':
if world.worlds[player].options.mode == 'inverted':
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
for at, text, _ in multiworld.worlds[player].options.plando_texts:
for at, text, _ in world.worlds[player].options.plando_texts:
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
@@ -2554,22 +2551,22 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
credits = Credits()
sickkiditem = multiworld.get_location('Sick Kid', player).item
sickkiditem = world.get_location('Sick Kid', player).item
sickkiditem_text = local_random.choice(SickKid_texts) \
if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \
else w.sickkid_credit_texts[sickkiditem.code]
zoraitem = multiworld.get_location('King Zora', player).item
zoraitem = world.get_location('King Zora', player).item
zoraitem_text = local_random.choice(Zora_texts) \
if zoraitem is None or zoraitem.code not in w.zora_credit_texts \
else w.zora_credit_texts[zoraitem.code]
magicshopitem = multiworld.get_location('Potion Shop', player).item
magicshopitem = world.get_location('Potion Shop', player).item
magicshopitem_text = local_random.choice(MagicShop_texts) \
if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \
else w.magicshop_credit_texts[magicshopitem.code]
fluteboyitem = multiworld.get_location('Flute Spot', player).item
fluteboyitem = world.get_location('Flute Spot', player).item
fluteboyitem_text = local_random.choice(FluteBoy_texts) \
if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \
else w.fluteboy_credit_texts[fluteboyitem.code]
@@ -2598,7 +2595,7 @@ def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int):
rom.write_bytes(0x76CC0, [byte for p in pointers for byte in [p & 0xFF, p >> 8 & 0xFF]])
def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
def set_inverted_mode(world, player, rom):
rom.write_byte(snes_to_pc(0x0283E0), 0xF0) # residual portals
rom.write_byte(snes_to_pc(0x02B34D), 0xF0)
rom.write_byte(snes_to_pc(0x06DB78), 0x8B)
@@ -2616,12 +2613,12 @@ def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
# the following bytes should only be written in vanilla
# or they'll overwrite the randomizer's shuffles
if multiworld.worlds[player].options.entrance_shuffle == 'vanilla':
if world.worlds[player].options.entrance_shuffle == 'vanilla':
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
rom.write_byte(0xDBB73 + 0x36, 0x24)
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0x15B8C, 0x6C)
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
rom.write_byte(0xDBB73 + 0x52, 0x01)
@@ -2679,7 +2676,7 @@ def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
# keep the old man spawn point at old man house unless shuffle is vanilla
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
@@ -2742,7 +2739,7 @@ def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
rom.write_int16(snes_to_pc(0x308320), 0x001B)
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(snes_to_pc(0x308340), 0x7B)
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
@@ -2779,10 +2776,10 @@ def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom):
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_byte(0xDBB73 + 0x35, 0x36)
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
rom.write_byte(0x15B8C + 0x37, 0x1B)
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ from Utils import int16_as_bytes
from worlds.generic.Rules import add_rule
from BaseClasses import CollectionState, Item, MultiWorld
from BaseClasses import CollectionState
from .SubClasses import ALttPLocation
from .Items import item_name_groups
@@ -159,7 +159,7 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
ShopType.TakeAny: TakeAny}
def push_shop_inventories(multiworld: MultiWorld):
def push_shop_inventories(multiworld):
all_shops = []
for world in multiworld.get_game_worlds(ALttPLocation.game):
all_shops.extend(world.shops)
@@ -183,7 +183,7 @@ def push_shop_inventories(multiworld: MultiWorld):
world.pushed_shop_inventories.set()
def create_shops(multiworld: MultiWorld, player: int):
def create_shops(multiworld, player: int):
from .Options import RandomizeShopInventories
player_shop_table = shop_table.copy()
if multiworld.worlds[player].options.include_witch_hut:
@@ -306,7 +306,7 @@ shop_generation_types = {
}
def set_up_shops(multiworld: MultiWorld, player: int):
def set_up_shops(multiworld, player: int):
from .Options import small_key_shuffle
# TODO: move hard+ mode changes for shields here, utilizing the new shops
@@ -408,7 +408,7 @@ price_rate_display = {
}
def get_price_modifier(item: Item) -> float:
def get_price_modifier(item) -> float:
if item.game == "A Link to the Past":
if any(x in item.name for x in
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
@@ -428,7 +428,7 @@ def get_price_modifier(item: Item) -> float:
return 0.25
def get_price(multiworld: MultiWorld, item: Item, player: int, price_type=None):
def get_price(multiworld, item, player: int, price_type=None):
"""Converts a raw Rupee price into a special price type"""
from .Options import small_key_shuffle
if price_type:

View File

@@ -1,4 +1,3 @@
from BaseClasses import MultiWorld, CollectionState
from worlds.generic.Rules import set_rule, add_rule
from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
from .SubClasses import LTTPEntrance
@@ -6,27 +5,27 @@ from .SubClasses import LTTPEntrance
# We actually need the logic to properly "mark" these regions as Light or Dark world.
# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules.
def underworld_glitch_connections(multiworld: MultiWorld, player: int):
specrock = multiworld.get_region('Spectacle Rock Cave (Bottom)', player)
mire = multiworld.get_region('Misery Mire (West)', player)
def underworld_glitch_connections(world, player):
specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
mire = world.get_region('Misery Mire (West)', player)
kikiskip = specrock.create_exit('Kiki Skip')
mire_to_hera = mire.create_exit('Mire to Hera Clip')
mire_to_swamp = mire.create_exit('Hera to Swamp Clip')
if multiworld.worlds[player].fix_fake_world:
kikiskip.connect(multiworld.get_entrance('Palace of Darkness Exit', player).connected_region)
mire_to_hera.connect(multiworld.get_entrance('Tower of Hera Exit', player).connected_region)
mire_to_swamp.connect(multiworld.get_entrance('Swamp Palace Exit', player).connected_region)
if world.worlds[player].fix_fake_world:
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
mire_to_hera.connect(world.get_entrance('Tower of Hera Exit', player).connected_region)
mire_to_swamp.connect(world.get_entrance('Swamp Palace Exit', player).connected_region)
else:
kikiskip.connect(multiworld.get_region('Palace of Darkness (Entrance)', player))
mire_to_hera.connect(multiworld.get_region('Tower of Hera (Bottom)', player))
mire_to_swamp.connect(multiworld.get_region('Swamp Palace (Entrance)', player))
kikiskip.connect(world.get_region('Palace of Darkness (Entrance)', player))
mire_to_hera.connect(world.get_region('Tower of Hera (Bottom)', player))
mire_to_swamp.connect(world.get_region('Swamp Palace (Entrance)', player))
# For some entrances, we need to fake having pearl, because we're in fake DW/LW.
# This creates a copy of the input state that has Moon Pearl.
def fake_pearl_state(state: CollectionState, player: int):
def fake_pearl_state(state, player):
if state.has('Moon Pearl', player):
return state
fake_state = state.copy()
@@ -36,11 +35,11 @@ def fake_pearl_state(state: CollectionState, player: int):
# Sets the rules on where we can actually go using this clip.
# Behavior differs based on what type of ER shuffle we're playing.
def dungeon_reentry_rules(multiworld: MultiWorld, player: int, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
fix_dungeon_exits = multiworld.worlds[player].fix_palaceofdarkness_exit
fix_fake_worlds = multiworld.worlds[player].fix_fake_world
def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
fix_fake_worlds = world.worlds[player].fix_fake_world
dungeon_entrance = [r for r in multiworld.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0]
if not fix_dungeon_exits: # vanilla, simple, restricted, dungeons_simple; should never have fake worlds fix
# Dungeons are only shuffled among themselves. We need to check SW, MM, and AT because they can't be reentered trivially.
if dungeon_entrance.name == 'Skull Woods Final Section':
@@ -50,64 +49,64 @@ def dungeon_reentry_rules(multiworld: MultiWorld, player: int, clip: LTTPEntranc
elif dungeon_entrance.name == 'Agahnims Tower':
add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier
# Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally.
add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
elif not fix_fake_worlds: # full, dungeons_full; fixed dungeon exits, but no fake worlds fix
# Entry requires the entrance's requirements plus a fake pearl, but you don't gain logical access to the surrounding region.
add_rule(clip, lambda state: dungeon_entrance.access_rule(fake_pearl_state(state, player)))
# exiting restriction
add_rule(multiworld.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
# Otherwise, the shuffle type is crossed, dungeons_crossed, or insanity; all of these do not need additional rules on where we can go,
# since the clip links directly to the exterior region.
def underworld_glitches_rules(multiworld: MultiWorld, player: int):
def underworld_glitches_rules(world, player):
# Ice Palace Entrance Clip
# This is the easiest one since it's a simple internal clip.
# Need to also add melting to freezor chest since it's otherwise assumed.
# Also can pick up the first jelly key from behind.
add_rule(multiworld.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or')
add_rule(multiworld.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
add_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, multiworld.get_region('Ice Palace (Entrance)', player), player), combine='or')
add_rule(world.get_entrance('Ice Palace (Main)', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
add_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
# Kiki Skip
kikiskip = multiworld.get_entrance('Kiki Skip', player)
kikiskip = world.get_entrance('Kiki Skip', player)
set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player))
dungeon_reentry_rules(multiworld, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
# Mire -> Hera -> Swamp
# Using mire keys on other dungeon doors
mire = multiworld.get_region('Misery Mire (West)', player)
mire = world.get_region('Misery Mire (West)', player)
mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and can_bomb_clip(state, mire, player) and has_fire_source(state, player)
hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, multiworld.get_region('Tower of Hera (Top)', player), player)
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or')
add_rule(multiworld.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
add_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, world.get_region('Tower of Hera (Top)', player), player)
add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or')
add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
# Build the rule for SP moat.
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
if not multiworld.worlds[player].swamp_patch_required:
if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
if not world.worlds[player].swamp_patch_required:
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
rule_map = {
'Misery Mire (Entrance)': (lambda state: True),
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
}
inverted = multiworld.worlds[player].options.mode == 'inverted'
inverted = world.worlds[player].options.mode == 'inverted'
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
rule_map.get(multiworld.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \
rule_map.get(multiworld.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
rule_map.get(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state)
mirrorless_moat_rule = lambda state: state.can_reach('Old Man S&Q', 'Entrance', player) and mire_clip(state) and (hera_rule(state) or gt_rule(state))
add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state))
else:
add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
# Using the entrances for various ER types. Hera -> Swamp never matters because you can only logically traverse with the mire keys
mire_to_hera = multiworld.get_entrance('Mire to Hera Clip', player)
mire_to_swamp = multiworld.get_entrance('Hera to Swamp Clip', player)
mire_to_hera = world.get_entrance('Mire to Hera Clip', player)
mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player)
set_rule(mire_to_hera, mire_clip)
set_rule(mire_to_swamp, lambda state: mire_clip(state) and state.has('Flippers', player))
dungeon_reentry_rules(multiworld, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
dungeon_reentry_rules(multiworld, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')
dungeon_reentry_rules(world, player, mire_to_hera, 'Tower of Hera (Bottom)', 'Tower of Hera Exit')
dungeon_reentry_rules(world, player, mire_to_swamp, 'Swamp Palace (Entrance)', 'Swamp Palace Exit')

View File

@@ -1,6 +0,0 @@
{
"game": "A Link to the Past",
"minimum_ap_version": "0.6.6",
"world_version": "5.1.0",
"authors": ["Berserker"]
}

View File

@@ -2025,13 +2025,13 @@ location_tables: Dict[str, List[DS3LocationData]] = {
DS3LocationData("LC: Rusted Coin - chapel", "Rusted Coin x2"),
DS3LocationData("LC: Braille Divine Tome of Lothric - wyvern room",
"Braille Divine Tome of Lothric", hidden=True), # Hidden fall
DS3LocationData("LC: Red Tearstone Ring - chapel, balcony before drop", "Red Tearstone Ring"),
DS3LocationData("LC: Red Tearstone Ring - chapel, drop onto roof", "Red Tearstone Ring"),
DS3LocationData("LC: Twinkling Titanite - moat, left side", "Twinkling Titanite x2"),
DS3LocationData("LC: Large Soul of a Nameless Soldier - plaza left, by pillar",
"Large Soul of a Nameless Soldier"),
DS3LocationData("LC: Titanite Scale - altar", "Titanite Scale x3"),
DS3LocationData("LC: Titanite Scale - chapel, chest", "Titanite Scale"),
DS3LocationData("LC: Hood of Prayer - ascent, chest at beginning", "Hood of Prayer"),
DS3LocationData("LC: Hood of Prayer", "Hood of Prayer"),
DS3LocationData("LC: Robe of Prayer - ascent, chest at beginning", "Robe of Prayer"),
DS3LocationData("LC: Skirt of Prayer - ascent, chest at beginning", "Skirt of Prayer"),
DS3LocationData("LC: Spirit Tree Crest Shield - basement, chest",

View File

@@ -6,7 +6,6 @@ from logging import warning
from typing import cast, Any, Callable, Dict, Set, List, Optional, TextIO, Union
from BaseClasses import CollectionState, MultiWorld, Region, Location, LocationProgressType, Entrance, Tutorial, ItemClassification
from Fill import remaining_fill
from worlds.AutoWorld import World, WebWorld
from worlds.generic.Rules import CollectionRule, ItemRule, add_rule, add_item_rule
@@ -1474,7 +1473,6 @@ class DarkSouls3World(World):
f"contain smoothed items, but only {len(converted_item_order)} items to smooth."
)
sorted_spheres = []
for sphere in locations_by_sphere:
locations = [loc for loc in sphere if loc.item.name in names]
@@ -1482,12 +1480,12 @@ class DarkSouls3World(World):
offworld = ds3_world._shuffle([loc for loc in locations if loc.game != "Dark Souls III"])
onworld = sorted((loc for loc in locations if loc.game == "Dark Souls III"),
key=lambda loc: loc.data.region_value)
# Give offworld regions the last (best) items within a given sphere
sorted_spheres.extend(onworld)
sorted_spheres.extend(offworld)
converted_item_order.reverse()
remaining_fill(multiworld, sorted_spheres, converted_item_order, name="DS3 Smoothing", check_location_can_fill=True)
# Give offworld regions the last (best) items within a given sphere
for location in onworld + offworld:
new_item = ds3_world._pop_item(location, converted_item_order)
location.item = new_item
new_item.location = location
if ds3_world.options.smooth_upgrade_items:
base_names = {
@@ -1520,6 +1518,19 @@ class DarkSouls3World(World):
self.random.shuffle(copy)
return copy
def _pop_item(
self,
location: Location,
items: List[DarkSouls3Item]
) -> DarkSouls3Item:
"""Returns the next item in items that can be assigned to location."""
for i, item in enumerate(items):
if location.can_fill(self.multiworld.state, item, False):
return items.pop(i)
# If we can't find a suitable item, give up and assign an unsuitable one.
return items.pop(0)
def _get_our_locations(self) -> List[DarkSouls3Location]:
return cast(List[DarkSouls3Location], self.multiworld.get_locations(self.player))

View File

@@ -1,5 +0,0 @@
{
"game": "Final Fantasy",
"world_version": "1.0.0",
"authors": ["Rosalie"]
}

View File

@@ -26,10 +26,7 @@ class GenericWeb(WebWorld):
'English', 'setup_en.md', 'setup/en', ['alwaysintreble'])
triggers = Tutorial('Archipelago Triggers Guide', 'A guide to setting up and using triggers in your game settings.',
'English', 'triggers_en.md', 'triggers/en', ['alwaysintreble'])
other = Tutorial('Other Games and Tools',
'A guide to additional games and tools that can be used with Archipelago.',
'English', 'other_en.md', 'other/en', ['Berserker'])
tutorials = [setup, mac, commands, advanced_settings, triggers, plando, other]
tutorials = [setup, mac, commands, advanced_settings, triggers, plando]
class GenericWorld(World):

View File

@@ -1,24 +0,0 @@
# Other Games and Tools
This guide provides information on additional community resources, tools, and games that function with Archipelago.
## Community Resources
The Archipelago community is active across several platforms where you can find support, new games, and tools.
### Discord Servers
Archipelago has two primary Discord servers for community interaction, game support, and hosting public games:
- **[Archipelago Official Discord](https://discord.gg/8Z65BR2)**: The main hub for the community, including general discussion, support, and public multiworld hosting.
- **[Archipelago After Dark Discord](https://discord.gg/fqvNCCRsu4)**: An adults-only server for 18+ and unrated content.
Both servers feature an **#apworld-index** channel. These channels are repositories for "APWorlds" — additional game implementations that can be easily added to your Archipelago installation to support more games.
### Documentation
- **[Archipelago Wiki](https://archipelago.miraheze.org/)**: A community-maintained wiki.
## Community Tools
These community-developed tools are frequently used alongside Archipelago to improve the player experience.
### PopTracker
**[PopTracker](https://github.com/black-sliver/PopTracker)** is a universal multi-platform tracking application designed for randomizers. It supports many Archipelago games through tracker packs, providing both manual and automatic autotracking capabilities by connecting directly to an Archipelago server.

View File

@@ -216,28 +216,6 @@ dungeon major item chests. Because the from_pool value is `false`, a copy of the
while the originals remain in the item pool to be shuffled. The second block will place the Kokiri Sword in the Deku
Tree Slingshot Chest, again not from the pool.
```yaml
plando_items:
# Example block - Hollow Knight
- items:
Claw : true
world:
- BobsWitness
- BobsRogueLegacy
```
This block will attempt to place all items in the Claw item group into any locations within the game slots named
"BobsWitness" and "BobsRogueLegacy."
**NOTE:** As item groups may contain items that are not currently present in the item pool, use of `true` with
item groups, as shown here, is strongly recommended to avoid creation of unintended items.
For example, the Claw item group for Hollow Knight includes Mantis_Claw, Left_Mantis_Claw, and Right_Mantis_Claw.
Depending on a different yaml setting, the Generator will create either one Mantis_Claw item, or one each of the
Left_Mantis_Claw and Right_Mantis_Claw items. By default, the Generator will create any missing item(s) in addition
to using the intended item(s), resulting in placement of all three items from the item group: Mantis_Claw,
Left_Mantis_Claw and Right_Mantis_Claw. Use of the true value, as shown in the example, restricts the Generator to
using only the items from the item group that are already present in the item pool.
## Boss Plando
This is currently only supported by A Link to the Past and Kirby's Dream Land 3. Boss plando allows a player to place a

View File

@@ -1281,7 +1281,7 @@ exclusion_table = {
LocationName.HadesCupTrophyParadoxCups,
LocationName.MusicalOrichalcumPlus,
],
"HitlistCasual": [
"HitlistCasual": {
LocationName.FuturePete,
LocationName.BetwixtandBetweenBondofFlame,
LocationName.GrimReaper2,
@@ -1299,7 +1299,7 @@ exclusion_table = {
LocationName.MCP,
LocationName.Lvl50,
LocationName.Lvl99
],
},
"Cups": {
LocationName.ProtectBeltPainandPanicCup,
LocationName.SerenityGemPainandPanicCup,

View File

@@ -1,6 +0,0 @@
{
"game": "Lingo",
"authors": ["hatkirby"],
"minimum_ap_version": "0.6.3",
"world_version": "5.0.0"
}

View File

@@ -4470,10 +4470,6 @@
panel: SEVEN (1)
- room: Outside The Initiated
panel: SEVEN (2)
First Eight:
event: True
panels:
- EIGHT
Nines:
id:
- Count Up Room Area Doors/Door_nine_hider
@@ -4616,7 +4612,7 @@
enter_only: True
orientation: east
required_door:
door: First Eight
door: Eights
progression:
Progressive Number Hunt:
panel_doors:

Binary file not shown.

View File

@@ -1,8 +1,7 @@
import logging
from typing import Any, ClassVar, TextIO
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial, \
PlandoOptions
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial
from Options import Accessibility
from Utils import output_path
from settings import FilePath, Group
@@ -19,7 +18,6 @@ from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
from .subclasses import MessengerItem, MessengerRegion, MessengerShopLocation
from .transitions import disconnect_entrances, shuffle_transitions
from .universal_tracker import reverse_portal_exits_into_portal_plando, reverse_transitions_into_plando_connections
components.append(
Component(
@@ -153,10 +151,6 @@ class MessengerWorld(World):
reachable_locs: bool = False
filler: dict[str, int]
@staticmethod
def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]:
return slot_data
def generate_early(self) -> None:
if self.options.goal == Goal.option_power_seal_hunt:
self.total_seals = self.options.total_seals.value
@@ -194,11 +188,6 @@ class MessengerWorld(World):
self.spoiler_portal_mapping = {}
self.transitions = []
if hasattr(self.multiworld, "re_gen_passthrough"):
slot_data = self.multiworld.re_gen_passthrough.get(self.game)
if slot_data:
self.starting_portals = slot_data["starting_portals"]
def create_regions(self) -> None:
# MessengerRegion adds itself to the multiworld
# create simple regions
@@ -290,16 +279,6 @@ class MessengerWorld(World):
def connect_entrances(self) -> None:
if self.options.shuffle_transitions:
disconnect_entrances(self)
keep_entrance_logic = False
if hasattr(self.multiworld, "re_gen_passthrough"):
slot_data = self.multiworld.re_gen_passthrough.get(self.game)
if slot_data:
self.multiworld.plando_options |= PlandoOptions.connections
self.options.portal_plando.value = reverse_portal_exits_into_portal_plando(slot_data["portal_exits"])
self.options.plando_connections.value = reverse_transitions_into_plando_connections(slot_data["transitions"])
keep_entrance_logic = True
add_closed_portal_reqs(self)
# i need portal shuffle to happen after rules exist so i can validate it
attempts = 20
@@ -316,7 +295,7 @@ class MessengerWorld(World):
raise RuntimeError("Unable to generate valid portal output.")
if self.options.shuffle_transitions:
shuffle_transitions(self, keep_entrance_logic)
shuffle_transitions(self)
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
if self.options.available_portals < 6:
@@ -484,7 +463,7 @@ class MessengerWorld(World):
"loc_data": {loc.address: {loc.item.name: [loc.item.code, loc.item.flags]}
for loc in multiworld.get_filled_locations() if loc.address},
}
output = orjson.dumps(data, option=orjson.OPT_NON_STR_KEYS)
with open(out_path, "wb") as f:
f.write(output)

View File

@@ -1,4 +0,0 @@
{
"game": "The Messenger",
"authors": ["alwaysintreble"]
}

View File

@@ -28,8 +28,6 @@ def create_yes_no_popup(title: str, text: str, callback: Callable[[str], None])
def launch_game(*args) -> None:
"""Check the game installation, then launch it"""
prompt: ButtonsPrompt | None = None
def courier_installed() -> bool:
"""Check if Courier is installed"""
assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll")
@@ -192,7 +190,7 @@ def launch_game(*args) -> None:
def launch(answer: str | None = None) -> None:
"""Launch the game."""
nonlocal args, prompt
nonlocal args
if prompt:
prompt.dismiss()
@@ -258,5 +256,3 @@ def launch_game(*args) -> None:
prompt = create_yes_no_popup("Launch Game",
"Mod installed and up to date. Would you like to launch the game now?",
launch)
else:
launch()

View File

@@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from BaseClasses import CollectionState, CollectionRule
from worlds.generic.Rules import add_rule, allow_self_locking_items
from BaseClasses import CollectionState
from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items
from .constants import NOTES, PHOBEKINS
from .options import MessengerAccessibility

View File

@@ -1,7 +1,8 @@
from functools import cached_property
from typing import TYPE_CHECKING
from BaseClasses import CollectionState, Item, ItemClassification, Location, Region
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region
from entrance_rando import ERPlacementState
from .regions import LOCATIONS, MEGA_SHARDS
from .shop import FIGURINES, SHOP_ITEMS

View File

@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from BaseClasses import Region, CollectionRule
from BaseClasses import Entrance, Region
from entrance_rando import EntranceType, randomize_entrances
from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS
from .options import ShuffleTransitions, TransitionPlando
@@ -26,6 +26,7 @@ def disconnect_entrances(world: "MessengerWorld") -> None:
entrance.randomization_type = er_type
mock_entrance.randomization_type = er_type
for parent, child in RANDOMIZED_CONNECTIONS.items():
if child == "Corrupted Future":
entrance = world.get_entrance("Artificer's Portal")
@@ -35,9 +36,8 @@ def disconnect_entrances(world: "MessengerWorld") -> None:
entrance = world.get_entrance(f"{parent} -> {child}")
disconnect_entrance()
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando, keep_logic: bool = False) -> None:
def remove_dangling_exit(region: Region) -> CollectionRule:
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None:
def remove_dangling_exit(region: Region) -> None:
# find the disconnected exit and remove references to it
for _exit in region.exits:
if not _exit.connected_region:
@@ -45,7 +45,6 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando
else:
raise ValueError(f"Unable to find randomized transition for {plando_connection}")
region.exits.remove(_exit)
return _exit.access_rule
def remove_dangling_entrance(region: Region) -> None:
# find the disconnected entrance and remove references to it
@@ -66,35 +65,30 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando
else:
dangling_exit = world.get_entrance("Artificer's Challenge")
reg1.exits.remove(dangling_exit)
access_rule = dangling_exit.access_rule
else:
reg1 = world.get_region(plando_connection.entrance)
access_rule = remove_dangling_exit(reg1)
remove_dangling_exit(reg1)
reg2 = world.get_region(plando_connection.exit)
remove_dangling_entrance(reg2)
# connect the regions
new_exit1 = reg1.connect(reg2)
if keep_logic:
new_exit1.access_rule = access_rule
reg1.connect(reg2)
# pretend the user set the plando direction as "both" regardless of what they actually put on coupled
if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled
or plando_connection.direction == "both")
and plando_connection.exit in RANDOMIZED_CONNECTIONS):
access_rule = remove_dangling_exit(reg2)
remove_dangling_exit(reg2)
remove_dangling_entrance(reg1)
new_exit2 = reg2.connect(reg1)
if keep_logic:
new_exit2.access_rule = access_rule
reg2.connect(reg1)
def shuffle_transitions(world: "MessengerWorld", keep_logic: bool = False) -> None:
def shuffle_transitions(world: "MessengerWorld") -> None:
coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled
plando = world.options.plando_connections
if plando:
connect_plando(world, plando, keep_logic)
connect_plando(world, plando)
result = randomize_entrances(world, coupled, {0: [0]})

View File

@@ -1,41 +0,0 @@
from Options import PlandoConnection
from .connections import RANDOMIZED_CONNECTIONS
from .portals import REGION_ORDER, SHOP_POINTS, CHECKPOINTS
from .transitions import TRANSITIONS
REVERSED_RANDOMIZED_CONNECTIONS = {v: k for k, v in RANDOMIZED_CONNECTIONS.items()}
def find_spot(portal_key: int) -> str:
"""finds the spot associated with the portal key"""
parent = REGION_ORDER[portal_key // 100]
if portal_key % 100 == 0:
return f"{parent} Portal"
if portal_key % 100 // 10 == 1:
return SHOP_POINTS[parent][portal_key % 10]
return CHECKPOINTS[parent][portal_key % 10]
def reverse_portal_exits_into_portal_plando(portal_exits: list[int]) -> list[PlandoConnection]:
return [
PlandoConnection("Autumn Hills", find_spot(portal_exits[0]), "both"),
PlandoConnection("Riviere Turquoise", find_spot(portal_exits[1]), "both"),
PlandoConnection("Howling Grotto", find_spot(portal_exits[2]), "both"),
PlandoConnection("Sunken Shrine", find_spot(portal_exits[3]), "both"),
PlandoConnection("Searing Crags", find_spot(portal_exits[4]), "both"),
PlandoConnection("Glacial Peak", find_spot(portal_exits[5]), "both"),
]
def reverse_transitions_into_plando_connections(transitions: list[list[int]]) -> list[PlandoConnection]:
plando_connections = []
for connection in [
PlandoConnection(REVERSED_RANDOMIZED_CONNECTIONS[TRANSITIONS[transition[0]]], TRANSITIONS[transition[1]], "both")
for transition in transitions
]:
if connection.exit in {con.entrance for con in plando_connections}:
continue
plando_connections.append(connection)
return plando_connections

View File

@@ -1,11 +1,11 @@
import asyncio
from typing import TYPE_CHECKING, Optional, Set, List, Dict
import struct
from typing import TYPE_CHECKING, Optional, Set, List, Dict
from NetUtils import ClientStatus
from .Locations import roomCount, nonBlock, beanstones, roomException, shop, badge, pants, eReward
from .Items import items_by_id
import asyncio
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
@@ -41,6 +41,8 @@ class MLSSClient(BizHawkClient):
self.local_events = []
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
try:
# Check ROM name/patch version
rom_name_bytes = await bizhawk.read(ctx.bizhawk_ctx, [(0xA0, 14, "ROM")])
@@ -70,15 +72,20 @@ class MLSSClient(BizHawkClient):
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
ctx.auth = self.player_name
def on_package(self, ctx, cmd, args) -> None:
if cmd == "RoomInfo":
ctx.seed_name = args["seed_name"]
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
from CommonClient import logger
try:
if ctx.server_seed_name is None:
if ctx.seed_name is None:
return
if not self.seed_verify:
seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.server_seed_name), "ROM")])
seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")])
seed = seed[0].decode("UTF-8")
if seed not in ctx.server_seed_name:
if seed not in ctx.seed_name:
logger.info(
"ERROR: The ROM you loaded is for a different game of AP. "
"Please make sure the host has sent you the correct patch file, "

View File

@@ -140,8 +140,8 @@ def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
"""Request a refill from EnergyLink."""
from worlds._bizhawk.context import BizHawkClientContext
"""Request a refill from EnergyLink."""
if self.ctx.game != "Mega Man 2":
logger.warning("This command can only be used when playing Mega Man 2.")
return

View File

@@ -1 +0,0 @@
/src/*

View File

@@ -1,275 +0,0 @@
import hashlib
import logging
from copy import deepcopy
from typing import Any, Sequence, ClassVar
from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location
from worlds.AutoWorld import World, WebWorld
from .names import (gamma, gemini_man_stage, needle_man_stage, hard_man_stage, magnet_man_stage, top_man_stage,
snake_man_stage, spark_man_stage, shadow_man_stage, rush_marine, rush_jet, rush_coil)
from .items import (item_table, item_names, MM3Item, filler_item_weights, robot_master_weapon_table,
stage_access_table, rush_item_table, lookup_item_to_id)
from .locations import (MM3Location, mm3_regions, MM3Region, lookup_location_to_id,
location_groups)
from .rom import patch_rom, MM3ProcedurePatch, MM3LCHASH, MM3VCHASH, PROTEUSHASH, MM3NESHASH
from .options import MM3Options, Consumables
from .client import MegaMan3Client
from .rules import set_rules, weapon_damage, robot_masters, weapons_to_name, minimum_weakness_requirement
import os
import threading
import base64
import settings
logger = logging.getLogger("Mega Man 3")
class MM3Settings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the MM3 EN rom"""
description = "Mega Man 3 ROM File"
copy_to: str | None = "Mega Man 3 (USA).nes"
md5s = [MM3NESHASH, MM3LCHASH, PROTEUSHASH, MM3VCHASH]
def browse(self: settings.T,
filetypes: Sequence[tuple[str, Sequence[str]]] | None = None,
**kwargs: Any) -> settings.T | None:
if not filetypes:
file_types = [("NES", [".nes"]), ("Program", [".exe"])] # LC1 is only a windows executable, no linux
return super().browse(file_types, **kwargs)
else:
return super().browse(filetypes, **kwargs)
@classmethod
def validate(cls, path: str) -> None:
"""Try to open and validate file against hashes"""
with open(path, "rb", buffering=0) as f:
try:
f.seek(0)
if f.read(4) == b"NES\x1A":
f.seek(16)
else:
f.seek(0)
cls._validate_stream_hashes(f)
base_rom_bytes = f.read()
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() == PROTEUSHASH:
# we need special behavior here
cls.copy_to = None
except ValueError:
raise ValueError(f"File hash does not match for {path}")
rom_file: RomFile = RomFile(RomFile.copy_to)
class MM3WebWorld(WebWorld):
theme = "partyTime"
tutorials = [
Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Mega Man 3 randomizer connected to an Archipelago Multiworld.",
"English",
"setup_en.md",
"setup/en",
["Silvris"]
)
]
class MM3World(World):
"""
Following his second defeat by Mega Man, Dr. Wily has finally come to his senses. He and Dr. Light begin work on
Gamma, a giant peacekeeping robot. However, Gamma's power source, the Energy Elements, are being guarded by the
Robot Masters sent to retrieve them. It's up to Mega Man to retrieve the Energy Elements and defeat the mastermind
behind the Robot Masters' betrayal.
"""
game = "Mega Man 3"
settings: ClassVar[MM3Settings]
options_dataclass = MM3Options
options: MM3Options
item_name_to_id = lookup_item_to_id
location_name_to_id = lookup_location_to_id
item_name_groups = item_names
location_name_groups = location_groups
web = MM3WebWorld()
rom_name: bytearray
def __init__(self, world: MultiWorld, player: int):
self.rom_name = bytearray()
self.rom_name_available_event = threading.Event()
super().__init__(world, player)
self.weapon_damage = deepcopy(weapon_damage)
self.wily_4_weapons: dict[int, list[int]] = {}
def create_regions(self) -> None:
menu = MM3Region("Menu", self.player, self.multiworld)
self.multiworld.regions.append(menu)
location: MM3Location
for name, region in mm3_regions.items():
stage = MM3Region(name, self.player, self.multiworld)
if not region.parent:
menu.connect(stage, f"To {name}",
lambda state, req=tuple(region.required_items): state.has_all(req, self.player))
else:
old_stage = self.get_region(region.parent)
old_stage.connect(stage, f"To {name}",
lambda state, req=tuple(region.required_items): state.has_all(req, self.player))
stage.add_locations({loc: data.location_id for loc, data in region.locations.items()
if (not data.energy or self.options.consumables.value in (Consumables.option_weapon_health, Consumables.option_all))
and (not data.oneup_tank or self.options.consumables.value in (Consumables.option_1up_etank, Consumables.option_all))})
for location in stage.get_locations():
if location.address is None and location.name != gamma:
location.place_locked_item(MM3Item(location.name, ItemClassification.progression,
None, self.player))
self.multiworld.regions.append(stage)
goal_location = self.get_location(gamma)
goal_location.place_locked_item(MM3Item("Victory", ItemClassification.progression, None, self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
def create_item(self, name: str, force_non_progression: bool = False) -> MM3Item:
item = item_table[name]
classification = ItemClassification.filler
if item.progression and not force_non_progression:
classification = ItemClassification.progression_skip_balancing \
if item.skip_balancing else ItemClassification.progression
if item.useful:
classification |= ItemClassification.useful
return MM3Item(name, classification, item.code, self.player)
def get_filler_item_name(self) -> str:
return self.random.choices(list(filler_item_weights.keys()),
weights=list(filler_item_weights.values()))[0]
def create_items(self) -> None:
itempool = []
# grab first robot master
robot_master = self.item_id_to_name[0x0101 + self.options.starting_robot_master.value]
self.multiworld.push_precollected(self.create_item(robot_master))
itempool.extend([self.create_item(name) for name in stage_access_table.keys()
if name != robot_master])
itempool.extend([self.create_item(name) for name in robot_master_weapon_table.keys()])
itempool.extend([self.create_item(name) for name in rush_item_table.keys()])
total_checks = 31
if self.options.consumables in (Consumables.option_1up_etank,
Consumables.option_all):
total_checks += 33
if self.options.consumables in (Consumables.option_weapon_health,
Consumables.option_all):
total_checks += 106
remaining = total_checks - len(itempool)
itempool.extend([self.create_item(name)
for name in self.random.choices(list(filler_item_weights.keys()),
weights=list(filler_item_weights.values()),
k=remaining)])
self.multiworld.itempool += itempool
set_rules = set_rules
def generate_early(self) -> None:
if (self.options.starting_robot_master.current_key == "gemini_man"
and not any(item in self.options.start_inventory for item in rush_item_table.keys())) or \
(self.options.starting_robot_master.current_key == "hard_man"
and not any(item in self.options.start_inventory for item in [rush_coil, rush_jet])):
robot_master_pool = [0, 1, 4, 5, 6, 7, ]
if rush_marine in self.options.start_inventory:
robot_master_pool.append(2)
self.options.starting_robot_master.value = self.random.choice(robot_master_pool)
logger.warning(
f"Incompatible starting Robot Master, changing to "
f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}")
def fill_hook(self,
prog_item_pool: list["Item"],
useful_item_pool: list["Item"],
filler_item_pool: list["Item"],
fill_locations: list["Location"]) -> None:
# on a solo gen, fill can try to force Wily into sphere 2, but for most generations this is impossible
# MM3 is worse than MM2 here, some of the RBMs can also require Rush
if self.multiworld.players > 1:
return # Don't need to change anything on a multi gen, fill should be able to solve it with a 4 sphere 1
rbm_to_item = {
0: needle_man_stage,
1: magnet_man_stage,
2: gemini_man_stage,
3: hard_man_stage,
4: top_man_stage,
5: snake_man_stage,
6: spark_man_stage,
7: shadow_man_stage
}
affected_rbm = [2, 3] # Gemini and Hard will always have this happen
possible_rbm = [0, 7] # Needle and Shadow are always valid targets, due to Rush Marine/Jet receive
if self.options.consumables:
possible_rbm.extend([4, 5]) # every stage has at least one of each consumable
if self.options.consumables in (Consumables.option_weapon_health, Consumables.option_all):
possible_rbm.extend([1, 6])
else:
affected_rbm.extend([1, 6])
else:
affected_rbm.extend([1, 4, 5, 6]) # only two checks on non consumables
if self.options.starting_robot_master.value in affected_rbm:
rbm_names = list(map(lambda s: rbm_to_item[s], possible_rbm))
valid_second = [item for item in prog_item_pool
if item.name in rbm_names
and item.player == self.player]
placed_item = self.random.choice(valid_second)
rbm_defeated = (f"{robot_masters[self.options.starting_robot_master.value].replace(' Defeated', '')}"
f" - Defeated")
rbm_location = self.get_location(rbm_defeated)
rbm_location.place_locked_item(placed_item)
prog_item_pool.remove(placed_item)
fill_locations.remove(rbm_location)
target_rbm = (placed_item.code & 0xF) - 1
if self.options.strict_weakness or (self.options.random_weakness
and not (self.weapon_damage[0][target_rbm] > 0)):
# we need to find a weakness for this boss
weaknesses = [weapon for weapon in range(1, 9)
if self.weapon_damage[weapon][target_rbm] >= minimum_weakness_requirement[weapon]]
weapons = list(map(lambda s: weapons_to_name[s], weaknesses))
valid_weapons = [item for item in prog_item_pool
if item.name in weapons
and item.player == self.player]
placed_weapon = self.random.choice(valid_weapons)
weapon_name = next(name for name, idx in lookup_location_to_id.items()
if idx == 0x0101 + self.options.starting_robot_master.value)
weapon_location = self.get_location(weapon_name)
weapon_location.place_locked_item(placed_weapon)
prog_item_pool.remove(placed_weapon)
fill_locations.remove(weapon_location)
def generate_output(self, output_directory: str) -> None:
try:
patch = MM3ProcedurePatch(player=self.player, player_name=self.player_name)
patch_rom(self, patch)
self.rom_name = patch.name
patch.write(os.path.join(output_directory,
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
except Exception:
raise
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def fill_slot_data(self) -> dict[str, Any]:
return {
"death_link": self.options.death_link.value,
"weapon_damage": self.weapon_damage,
"wily_4_weapons": self.wily_4_weapons
}
@staticmethod
def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]:
local_weapon = {int(key): value for key, value in slot_data["weapon_damage"].items()}
local_wily = {int(key): value for key, value in slot_data["wily_4_weapons"].items()}
return {"weapon_damage": local_weapon, "wily_4_weapons": local_wily}
def modify_multidata(self, multidata: dict[str, Any]) -> None:
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
rom_name = getattr(self, "rom_name", None)
# we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name:
new_name = base64.b64encode(bytes(self.rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name]

View File

@@ -1,6 +0,0 @@
{
"game": "Mega Man 3",
"authors": ["Silvris"],
"world_version": "0.1.7",
"minimum_ap_version": "0.6.4"
}

View File

@@ -1,783 +0,0 @@
import logging
import time
from enum import IntEnum
from base64 import b64encode
from typing import TYPE_CHECKING, Any
from NetUtils import ClientStatus, color, NetworkItem
from worlds._bizhawk.client import BizHawkClient
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
nes_logger = logging.getLogger("NES")
logger = logging.getLogger("Client")
MM3_CURRENT_STAGE = 0x22
MM3_MEGAMAN_STATE = 0x30
MM3_PROG_STATE = 0x60
MM3_ROBOT_MASTERS_DEFEATED = 0x61
MM3_DOC_STATUS = 0x62
MM3_HEALTH = 0xA2
MM3_WEAPON_ENERGY = 0xA3
MM3_WEAPONS = {
1: 1,
2: 3,
3: 0,
4: 2,
5: 4,
6: 5,
7: 7,
8: 9,
0x11: 6,
0x12: 8,
0x13: 10,
}
MM3_DOC_REMAP = {
0: 0,
1: 1,
2: 2,
3: 3,
4: 6,
5: 7,
6: 4,
7: 5
}
MM3_LIVES = 0xAE
MM3_E_TANKS = 0xAF
MM3_ENERGY_BAR = 0xB2
MM3_CONSUMABLES = 0x150
MM3_ROBOT_MASTERS_UNLOCKED = 0x680
MM3_DOC_ROBOT_UNLOCKED = 0x681
MM3_ENERGYLINK = 0x682
MM3_LAST_WILY = 0x683
MM3_RBM_STROBE = 0x684
MM3_SFX_QUEUE = 0x685
MM3_DOC_ROBOT_DEFEATED = 0x686
MM3_COMPLETED_STAGES = 0x687
MM3_RECEIVED_ITEMS = 0x688
MM3_RUSH_RECEIVED = 0x689
MM3_CONSUMABLE_TABLE: dict[int, dict[int, tuple[int, int]]] = {
# Stage:
# Item: (byte offset, bit mask)
0: {
0x0200: (0, 5),
0x0201: (3, 2),
},
1: {
0x0202: (2, 6),
0x0203: (2, 5),
0x0204: (2, 4),
0x0205: (2, 3),
0x0206: (3, 6),
0x0207: (3, 5),
0x0208: (3, 7),
0x0209: (4, 0)
},
2: {
0x020A: (2, 7),
0x020B: (3, 0),
0x020C: (3, 1),
0x020D: (3, 2),
0x020E: (4, 2),
0x020F: (4, 3),
0x0210: (4, 7),
0x0211: (5, 1),
0x0212: (6, 1),
0x0213: (7, 0)
},
3: {
0x0214: (0, 6),
0x0215: (1, 5),
0x0216: (2, 3),
0x0217: (2, 7),
0x0218: (2, 6),
0x0219: (2, 5),
0x021A: (4, 5),
},
4: {
0x021B: (1, 3),
0x021C: (1, 5),
0x021D: (1, 7),
0x021E: (2, 0),
0x021F: (1, 6),
0x0220: (2, 4),
0x0221: (2, 5),
0x0222: (4, 5)
},
5: {
0x0223: (3, 0),
0x0224: (3, 2),
0x0225: (4, 5),
0x0226: (4, 6),
0x0227: (6, 4),
},
6: {
0x0228: (2, 0),
0x0229: (2, 1),
0x022A: (3, 1),
0x022B: (3, 2),
0x022C: (3, 3),
0x022D: (3, 4),
},
7: {
0x022E: (3, 5),
0x022F: (3, 4),
0x0230: (3, 3),
0x0231: (3, 2),
},
8: {
0x0232: (1, 4),
0x0233: (2, 1),
0x0234: (2, 2),
0x0235: (2, 5),
0x0236: (3, 5),
0x0237: (4, 2),
0x0238: (4, 4),
0x0239: (5, 3),
0x023A: (6, 0),
0x023B: (6, 1),
0x023C: (7, 5),
},
9: {
0x023D: (3, 2),
0x023E: (3, 6),
0x023F: (4, 5),
0x0240: (5, 4),
},
10: {
0x0241: (0, 2),
0x0242: (2, 4)
},
11: {
0x0243: (4, 1),
0x0244: (6, 0),
0x0245: (6, 1),
0x0246: (6, 2),
0x0247: (6, 3),
},
12: {
0x0248: (0, 0),
0x0249: (0, 3),
0x024A: (0, 5),
0x024B: (1, 6),
0x024C: (2, 7),
0x024D: (2, 3),
0x024E: (2, 1),
0x024F: (2, 2),
0x0250: (3, 5),
0x0251: (3, 4),
0x0252: (3, 6),
0x0253: (3, 7)
},
13: {
0x0254: (0, 3),
0x0255: (0, 6),
0x0256: (1, 0),
0x0257: (3, 0),
0x0258: (3, 2),
0x0259: (3, 3),
0x025A: (3, 4),
0x025B: (3, 5),
0x025C: (3, 6),
0x025D: (4, 0),
0x025E: (3, 7),
0x025F: (4, 1),
0x0260: (4, 2),
},
14: {
0x0261: (0, 3),
0x0262: (0, 2),
0x0263: (0, 6),
0x0264: (1, 2),
0x0265: (1, 7),
0x0266: (2, 0),
0x0267: (2, 1),
0x0268: (2, 2),
0x0269: (2, 3),
0x026A: (5, 2),
0x026B: (5, 3),
},
15: {
0x026C: (0, 0),
0x026D: (0, 1),
0x026E: (0, 2),
0x026F: (0, 3),
0x0270: (0, 4),
0x0271: (0, 6),
0x0272: (1, 0),
0x0273: (1, 2),
0x0274: (1, 3),
0x0275: (1, 1),
0x0276: (0, 7),
0x0277: (3, 2),
0x0278: (2, 2),
0x0279: (2, 3),
0x027A: (2, 4),
0x027B: (2, 5),
0x027C: (3, 1),
0x027D: (3, 0),
0x027E: (2, 7),
0x027F: (2, 6),
},
16: {
0x0280: (0, 0),
0x0281: (0, 3),
0x0282: (0, 1),
0x0283: (0, 2),
},
17: {
0x0284: (0, 2),
0x0285: (0, 6),
0x0286: (0, 1),
0x0287: (0, 5),
0x0288: (0, 3),
0x0289: (0, 0),
0x028A: (0, 4)
}
}
def to_oneup_format(val: int) -> int:
return ((val // 10) * 0x10) + val % 10
def from_oneup_format(val: int) -> int:
return ((val // 0x10) * 10) + val % 0x10
class MM3EnergyLinkType(IntEnum):
Life = 0
NeedleCannon = 1
MagnetMissile = 2
GeminiLaser = 3
HardKnuckle = 4
TopSpin = 5
SearchSnake = 6
SparkShot = 7
ShadowBlade = 8
OneUP = 12
RushCoil = 0x11
RushMarine = 0x12
RushJet = 0x13
request_to_name: dict[str, str] = {
"HP": "health",
"NE": "Needle Cannon energy",
"MA": "Magnet Missile energy",
"GE": "Gemini Laser energy",
"HA": "Hard Knuckle energy",
"TO": "Top Spin energy",
"SN": "Search Snake energy",
"SP": "Spark Shot energy",
"SH": "Shadow Blade energy",
"RC": "Rush Coil energy",
"RM": "Rush Marine energy",
"RJ": "Rush Jet energy",
"1U": "lives"
}
HP_EXCHANGE_RATE = 500000000
WEAPON_EXCHANGE_RATE = 250000000
ONEUP_EXCHANGE_RATE = 14000000000
def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
"""Check the current pool of EnergyLink, and requestable refills from it."""
if self.ctx.game != "Mega Man 3":
logger.warning("This command can only be used when playing Mega Man 3.")
return
if not self.ctx.server or not self.ctx.slot:
logger.warning("You must be connected to a server to use this command.")
return
energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0)
health_points = energylink // HP_EXCHANGE_RATE
weapon_points = energylink // WEAPON_EXCHANGE_RATE
lives = energylink // ONEUP_EXCHANGE_RATE
logger.info(f"Healing available: {health_points}\n"
f"Weapon refill available: {weapon_points}\n"
f"Lives available: {lives}")
def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
"""Request a refill from EnergyLink."""
from worlds._bizhawk.context import BizHawkClientContext
if self.ctx.game != "Mega Man 3":
logger.warning("This command can only be used when playing Mega Man 3.")
return
if not self.ctx.server or not self.ctx.slot:
logger.warning("You must be connected to a server to use this command.")
return
valid_targets: dict[str, MM3EnergyLinkType] = {
"HP": MM3EnergyLinkType.Life,
"NE": MM3EnergyLinkType.NeedleCannon,
"MA": MM3EnergyLinkType.MagnetMissile,
"GE": MM3EnergyLinkType.GeminiLaser,
"HA": MM3EnergyLinkType.HardKnuckle,
"TO": MM3EnergyLinkType.TopSpin,
"SN": MM3EnergyLinkType.SearchSnake,
"SP": MM3EnergyLinkType.SparkShot,
"SH": MM3EnergyLinkType.ShadowBlade,
"RC": MM3EnergyLinkType.RushCoil,
"RM": MM3EnergyLinkType.RushMarine,
"RJ": MM3EnergyLinkType.RushJet,
"1U": MM3EnergyLinkType.OneUP
}
if target.upper() not in valid_targets:
logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}")
return
ctx = self.ctx
assert isinstance(ctx, BizHawkClientContext)
client = ctx.client_handler
assert isinstance(client, MegaMan3Client)
client.refill_queue.append((valid_targets[target.upper()], int(amount)))
logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.")
def cmd_autoheal(self: "BizHawkClientCommandProcessor") -> None:
"""Enable auto heal from EnergyLink."""
if self.ctx.game != "Mega Man 3":
logger.warning("This command can only be used when playing Mega Man 3.")
return
if not self.ctx.server or not self.ctx.slot:
logger.warning("You must be connected to a server to use this command.")
return
else:
assert isinstance(self.ctx.client_handler, MegaMan3Client)
if self.ctx.client_handler.auto_heal:
self.ctx.client_handler.auto_heal = False
logger.info(f"Auto healing disabled.")
else:
self.ctx.client_handler.auto_heal = True
logger.info(f"Auto healing enabled.")
def get_sfx_writes(sfx: int) -> tuple[int, bytes, str]:
return MM3_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"
class MegaMan3Client(BizHawkClient):
game = "Mega Man 3"
system = "NES"
patch_suffix = ".apmm3"
item_queue: list[NetworkItem] = []
pending_death_link: bool = False
# default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once
sending_death_link: bool = True
death_link: bool = False
energy_link: bool = False
rom: bytes | None = None
weapon_energy: int = 0
health_energy: int = 0
auto_heal: bool = False
refill_queue: list[tuple[MM3EnergyLinkType, int]] = []
last_wily: int | None = None # default to wily 1
doc_status: int | None = None # default to no doc progress
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from worlds._bizhawk import RequestFailedError, read, get_memory_size
from . import MM3World
try:
if (await get_memory_size(ctx.bizhawk_ctx, "PRG ROM")) < 0x3FFB0:
# not the entire size, but enough to check validation
if "pool" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("pool")
if "request" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("request")
if "autoheal" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("autoheal")
return False
game_name, version = (await read(ctx.bizhawk_ctx, [(0x3F320, 21, "PRG ROM"),
(0x3F33C, 3, "PRG ROM")]))
if game_name[:3] != b"MM3" or version != bytes(MM3World.world_version):
if game_name[:3] == b"MM3":
# I think this is an easier check than the other?
older_version = f"{version[0]}.{version[1]}.{version[2]}"
logger.warning(f"This Mega Man 3 patch was generated for an different version of the apworld. "
f"Please use that version to connect instead.\n"
f"Patch version: ({older_version})\n"
f"Client version: ({'.'.join([str(i) for i in MM3World.world_version])})")
if "pool" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("pool")
if "request" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("request")
if "autoheal" in ctx.command_processor.commands:
ctx.command_processor.commands.pop("autoheal")
return False
except UnicodeDecodeError:
return False
except RequestFailedError:
return False # Should verify on the next pass
ctx.game = self.game
self.rom = game_name
ctx.items_handling = 0b111
ctx.want_slot_data = False
deathlink = (await read(ctx.bizhawk_ctx, [(0x3F336, 1, "PRG ROM")]))[0][0]
if deathlink & 0x01:
self.death_link = True
await ctx.update_death_link(self.death_link)
if deathlink & 0x02:
self.energy_link = True
if self.energy_link:
if "pool" not in ctx.command_processor.commands:
ctx.command_processor.commands["pool"] = cmd_pool
if "request" not in ctx.command_processor.commands:
ctx.command_processor.commands["request"] = cmd_request
if "autoheal" not in ctx.command_processor.commands:
ctx.command_processor.commands["autoheal"] = cmd_autoheal
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
if self.rom:
ctx.auth = b64encode(self.rom).decode()
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict[str, Any]) -> None:
if cmd == "Bounced":
if "tags" in args:
assert ctx.slot is not None
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
self.on_deathlink(ctx)
elif cmd == "Retrieved":
if f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]:
self.last_wily = args["keys"][f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]
if f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}" in args["keys"]:
self.doc_status = args["keys"][f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"]
elif cmd == "Connected":
if self.energy_link:
ctx.set_notify(f"EnergyLink{ctx.team}")
if ctx.ui:
ctx.ui.enable_energy_link()
async def send_deathlink(self, ctx: "BizHawkClientContext") -> None:
self.sending_death_link = True
ctx.last_death_link = time.time()
await ctx.send_death("Mega Man was defeated.")
def on_deathlink(self, ctx: "BizHawkClientContext") -> None:
ctx.last_death_link = time.time()
self.pending_death_link = True
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
from worlds._bizhawk import read, write
if ctx.server is None:
return
if ctx.slot is None:
return
# get our relevant bytes
(prog_state, robot_masters_unlocked, robot_masters_defeated, doc_status, doc_robo_unlocked, doc_robo_defeated,
rush_acquired, received_items, completed_stages, consumable_checks,
e_tanks, lives, weapon_energy, health, state, bar_state, current_stage,
energy_link_packet, last_wily) = await read(ctx.bizhawk_ctx, [
(MM3_PROG_STATE, 1, "RAM"),
(MM3_ROBOT_MASTERS_UNLOCKED, 1, "RAM"),
(MM3_ROBOT_MASTERS_DEFEATED, 1, "RAM"),
(MM3_DOC_STATUS, 1, "RAM"),
(MM3_DOC_ROBOT_UNLOCKED, 1, "RAM"),
(MM3_DOC_ROBOT_DEFEATED, 1, "RAM"),
(MM3_RUSH_RECEIVED, 1, "RAM"),
(MM3_RECEIVED_ITEMS, 1, "RAM"),
(MM3_COMPLETED_STAGES, 0x1, "RAM"),
(MM3_CONSUMABLES, 16, "RAM"), # Could be more but 16 definitely catches all current
(MM3_E_TANKS, 1, "RAM"),
(MM3_LIVES, 1, "RAM"),
(MM3_WEAPON_ENERGY, 11, "RAM"),
(MM3_HEALTH, 1, "RAM"),
(MM3_MEGAMAN_STATE, 1, "RAM"),
(MM3_ENERGY_BAR, 2, "RAM"),
(MM3_CURRENT_STAGE, 1, "RAM"),
(MM3_ENERGYLINK, 1, "RAM"),
(MM3_LAST_WILY, 1, "RAM"),
])
if bar_state[0] not in (0x00, 0x80):
return # Game is not initialized
# Bit of a trick here, bar state can only be 0x00 or 0x80 (display health bar, or don't)
# This means it can double as init guard and in-stage tracker
if not ctx.finished_game and completed_stages[0] & 0x20:
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL
}])
writes = []
# deathlink
# only handle deathlink in bar state 0x80 (in stage)
if bar_state[0] == 0x80:
if self.pending_death_link:
writes.append((MM3_MEGAMAN_STATE, bytes([0x0E]), "RAM"))
self.pending_death_link = False
self.sending_death_link = True
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
if state[0] == 0x0E and not self.sending_death_link:
await self.send_deathlink(ctx)
elif state[0] != 0x0E:
self.sending_death_link = False
if self.last_wily != last_wily[0]:
if self.last_wily is None:
# revalidate last wily from data storage
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
{"operation": "default", "value": 0xC}
]}])
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}"]}])
elif last_wily[0] == 0:
writes.append((MM3_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM"))
else:
# correct our setting
self.last_wily = last_wily[0]
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
{"operation": "replace", "value": self.last_wily}
]}])
if self.doc_status != doc_status[0]:
if self.doc_status is None:
# revalidate doc status from data storage
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [
{"operation": "default", "value": 0}
]}])
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}"]}])
elif doc_status[0] == 0:
writes.append((MM3_DOC_STATUS, self.doc_status.to_bytes(1, "little"), "RAM"))
else:
# correct our setting
# shouldn't be possible to desync, but we'll account for it anyways
self.doc_status |= doc_status[0]
await ctx.send_msgs([{"cmd": "Set", "key": f"MM3_DOC_STATUS_{ctx.team}_{ctx.slot}", "operations": [
{"operation": "replace", "value": self.doc_status}
]}])
weapon_energy = bytearray(weapon_energy)
# handle receiving items
recv_amount = received_items[0]
if recv_amount < len(ctx.items_received):
item = ctx.items_received[recv_amount]
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names.lookup_in_slot(item.item), 'red', 'bold'),
color(ctx.player_names[item.player], 'yellow'),
ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
if item.item & 0x120 == 0:
# Robot Master Weapon, or Rush
new_weapons = item.item & 0xFF
weapon_energy[MM3_WEAPONS[new_weapons]] |= 0x9C
writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
writes.append(get_sfx_writes(0x32))
elif item.item & 0x20 == 0:
# Robot Master Stage Access
# Catch the Doc Robo here
if item.item & 0x10:
ptr = MM3_DOC_ROBOT_UNLOCKED
unlocked = doc_robo_unlocked
else:
ptr = MM3_ROBOT_MASTERS_UNLOCKED
unlocked = robot_masters_unlocked
new_stages = unlocked[0] | (1 << ((item.item & 0xF) - 1))
print(new_stages)
writes.append((ptr, new_stages.to_bytes(1, 'little'), "RAM"))
writes.append(get_sfx_writes(0x34))
writes.append((MM3_RBM_STROBE, b"\x01", "RAM"))
else:
# append to the queue, so we handle it later
self.item_queue.append(item)
recv_amount += 1
writes.append((MM3_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM"))
if energy_link_packet[0]:
pickup = energy_link_packet[0]
if pickup in (0x64, 0x65):
# Health pickups
if pickup == 0x65:
value = 2
else:
value = 10
exchange_rate = HP_EXCHANGE_RATE
elif pickup in (0x66, 0x67):
# Weapon Energy
if pickup == 0x67:
value = 2
else:
value = 10
exchange_rate = WEAPON_EXCHANGE_RATE
elif pickup == 0x69:
# 1-Up
value = 1
exchange_rate = ONEUP_EXCHANGE_RATE
else:
# if we managed to pickup something else, we should just fall through
value = 0
exchange_rate = 0
contribution = (value * exchange_rate) >> 1
if contribution:
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
[{"operation": "add", "value": contribution},
{"operation": "max", "value": 0}]}])
logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.")
writes.append((MM3_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM"))
if self.weapon_energy:
# Weapon Energy
# We parse the whole thing to spread it as thin as possible
current_energy = self.weapon_energy
for i, weapon in zip(range(len(weapon_energy)), weapon_energy):
if weapon & 0x80 and (weapon & 0x7F) < 0x1C:
missing = 0x1C - (weapon & 0x7F)
if missing > self.weapon_energy:
missing = self.weapon_energy
self.weapon_energy -= missing
weapon_energy[i] = weapon + missing
if not self.weapon_energy:
writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
break
else:
if current_energy != self.weapon_energy:
writes.append((MM3_WEAPON_ENERGY, weapon_energy, "RAM"))
if self.health_energy or self.auto_heal:
# Health Energy
# We save this if the player has not taken any damage
current_health = health[0]
if 0 < (current_health & 0x7F) < 0x1C:
health_diff = 0x1C - (current_health & 0x7F)
if self.health_energy:
if health_diff > self.health_energy:
health_diff = self.health_energy
self.health_energy -= health_diff
else:
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
if health_diff * HP_EXCHANGE_RATE > pool:
health_diff = int(pool // HP_EXCHANGE_RATE)
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
[{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE},
{"operation": "max", "value": 0}]}])
current_health += health_diff
writes.append((MM3_HEALTH, current_health.to_bytes(1, 'little'), "RAM"))
if self.refill_queue:
refill_type, refill_amount = self.refill_queue.pop()
if refill_type == MM3EnergyLinkType.Life:
exchange_rate = HP_EXCHANGE_RATE
elif refill_type == MM3EnergyLinkType.OneUP:
exchange_rate = ONEUP_EXCHANGE_RATE
else:
exchange_rate = WEAPON_EXCHANGE_RATE
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
request = exchange_rate * refill_amount
if request > pool:
logger.warning(
f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}")
else:
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
[{"operation": "add", "value": -request},
{"operation": "max", "value": 0}]}])
if refill_type == MM3EnergyLinkType.Life:
refill_ptr = MM3_HEALTH
elif refill_type == MM3EnergyLinkType.OneUP:
refill_ptr = MM3_LIVES
else:
refill_ptr = MM3_WEAPON_ENERGY + MM3_WEAPONS[refill_type]
current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0]
if refill_type == MM3EnergyLinkType.OneUP:
current_value = from_oneup_format(current_value)
new_value = min(0x9C if refill_type != MM3EnergyLinkType.OneUP else 99, current_value + refill_amount)
if refill_type == MM3EnergyLinkType.OneUP:
new_value = to_oneup_format(new_value)
writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM"))
if len(self.item_queue):
item = self.item_queue.pop(0)
idx = item.item & 0xF
if idx == 0:
# 1-Up
current_lives = from_oneup_format(lives[0])
if current_lives > 99:
self.item_queue.append(item)
else:
current_lives += 1
current_lives = to_oneup_format(current_lives)
writes.append((MM3_LIVES, current_lives.to_bytes(1, 'little'), "RAM"))
writes.append(get_sfx_writes(0x14))
elif idx == 1:
self.weapon_energy += 0xE
writes.append(get_sfx_writes(0x1C))
elif idx == 2:
self.health_energy += 0xE
writes.append(get_sfx_writes(0x1C))
elif idx == 3:
current_tanks = from_oneup_format(e_tanks[0])
if current_tanks > 99:
self.item_queue.append(item)
else:
current_tanks += 1
current_tanks = to_oneup_format(current_tanks)
writes.append((MM3_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM"))
writes.append(get_sfx_writes(0x14))
await write(ctx.bizhawk_ctx, writes)
new_checks = []
# check for locations
for i in range(8):
flag = 1 << i
if robot_masters_defeated[0] & flag:
rbm_id = 0x0001 + i
if rbm_id not in ctx.checked_locations:
new_checks.append(rbm_id)
wep_id = 0x0101 + i
if wep_id not in ctx.checked_locations:
new_checks.append(wep_id)
if doc_robo_defeated[0] & flag:
doc_id = 0x0010 + MM3_DOC_REMAP[i]
if doc_id not in ctx.checked_locations:
new_checks.append(doc_id)
for i in range(2):
flag = 1 << i
if rush_acquired[0] & flag:
itm_id = 0x0111 + i
if itm_id not in ctx.checked_locations:
new_checks.append(itm_id)
for i in (0, 1, 2, 4):
# Wily 4 does not have a boss check
boss_id = 0x0009 + i
if completed_stages[0] & (1 << i) != 0:
if boss_id not in ctx.checked_locations:
new_checks.append(boss_id)
if completed_stages[0] & 0x80 and 0x000F not in ctx.checked_locations:
new_checks.append(0x000F)
if bar_state[0] == 0x80: # currently in stage
if (prog_state[0] > 0x00 and current_stage[0] >= 8) or prog_state[0] == 0x00:
# need to block the specific state of Break Man prog=0x12 stage=0x5
# it doesn't clean the consumable table and he doesn't have any anyways
for consumable in MM3_CONSUMABLE_TABLE[current_stage[0]]:
consumable_info = MM3_CONSUMABLE_TABLE[current_stage[0]][consumable]
if consumable not in ctx.checked_locations:
is_checked = consumable_checks[consumable_info[0]] & (1 << consumable_info[1])
if is_checked:
new_checks.append(consumable)
for new_check_id in new_checks:
ctx.locations_checked.add(new_check_id)
location = ctx.location_names.lookup_in_game(new_check_id)
nes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/'
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])

View File

@@ -1,331 +0,0 @@
import sys
from typing import TYPE_CHECKING
from . import names
from zlib import crc32
import struct
import logging
if TYPE_CHECKING:
from . import MM3World
from .rom import MM3ProcedurePatch
HTML_TO_NES: dict[str, int] = {
'SNOW': 0x20,
'LINEN': 0x36,
'SEASHELL': 0x36,
'AZURE': 0x3C,
'LAVENDER': 0x33,
'WHITE': 0x30,
'BLACK': 0x0F,
'GREY': 0x00,
'GRAY': 0x00,
'ROYALBLUE': 0x12,
'BLUE': 0x11,
'SKYBLUE': 0x21,
'LIGHTBLUE': 0x31,
'TURQUOISE': 0x2B,
'CYAN': 0x2C,
'AQUAMARINE': 0x3B,
'DARKGREEN': 0x0A,
'GREEN': 0x1A,
'YELLOW': 0x28,
'GOLD': 0x28,
'WHEAT': 0x37,
'TAN': 0x37,
'CHOCOLATE': 0x07,
'BROWN': 0x07,
'SALMON': 0x26,
'ORANGE': 0x27,
'CORAL': 0x36,
'TOMATO': 0x16,
'RED': 0x16,
'PINK': 0x25,
'MAROON': 0x06,
'MAGENTA': 0x24,
'FUSCHIA': 0x24,
'VIOLET': 0x24,
'PLUM': 0x33,
'PURPLE': 0x14,
'THISTLE': 0x34,
'DARKBLUE': 0x01,
'SILVER': 0x10,
'NAVY': 0x02,
'TEAL': 0x1C,
'OLIVE': 0x18,
'LIME': 0x2A,
'AQUA': 0x2C,
# can add more as needed
}
MM3_COLORS: dict[str, tuple[int, int]] = {
names.gemini_laser: (0x30, 0x21),
names.needle_cannon: (0x30, 0x17),
names.hard_knuckle: (0x10, 0x01),
names.magnet_missile: (0x10, 0x16),
names.top_spin: (0x36, 0x00),
names.search_snake: (0x30, 0x19),
names.rush_coil: (0x30, 0x15),
names.spark_shock: (0x30, 0x26),
names.rush_marine: (0x30, 0x15),
names.shadow_blade: (0x34, 0x14),
names.rush_jet: (0x30, 0x15),
names.needle_man_stage: (0x3C, 0x11),
names.magnet_man_stage: (0x30, 0x15),
names.gemini_man_stage: (0x30, 0x21),
names.hard_man_stage: (0x10, 0xC),
names.top_man_stage: (0x30, 0x26),
names.snake_man_stage: (0x30, 0x29),
names.spark_man_stage: (0x30, 0x26),
names.shadow_man_stage: (0x30, 0x11),
names.doc_needle_stage: (0x27, 0x15),
names.doc_gemini_stage: (0x27, 0x15),
names.doc_spark_stage: (0x27, 0x15),
names.doc_shadow_stage: (0x27, 0x15),
}
MM3_KNOWN_COLORS: dict[str, tuple[int, int]] = {
**MM3_COLORS,
# Metroid series
"Varia Suit": (0x27, 0x16),
"Gravity Suit": (0x14, 0x16),
"Phazon Suit": (0x06, 0x1D),
# Street Fighter, technically
"Hadouken": (0x3C, 0x11),
"Shoryuken": (0x38, 0x16),
# X Series
"Z-Saber": (0x20, 0x16),
"Helmet Upgrade": (0x20, 0x01),
"Body Upgrade": (0x20, 0x01),
"Arms Upgrade": (0x20, 0x01),
"Plasma Shot Upgrade": (0x20, 0x01),
"Stock Charge Upgrade": (0x20, 0x01),
"Legs Upgrade": (0x20, 0x01),
# X1
"Homing Torpedo": (0x3D, 0x37),
"Chameleon Sting": (0x3B, 0x1A),
"Rolling Shield": (0x3A, 0x25),
"Fire Wave": (0x37, 0x26),
"Storm Tornado": (0x34, 0x14),
"Electric Spark": (0x3D, 0x28),
"Boomerang Cutter": (0x3B, 0x2D),
"Shotgun Ice": (0x28, 0x2C),
# X2
"Crystal Hunter": (0x33, 0x21),
"Bubble Splash": (0x35, 0x28),
"Spin Wheel": (0x34, 0x1B),
"Silk Shot": (0x3B, 0x27),
"Sonic Slicer": (0x27, 0x01),
"Strike Chain": (0x30, 0x23),
"Magnet Mine": (0x28, 0x2D),
"Speed Burner": (0x31, 0x16),
# X3
"Acid Burst": (0x28, 0x2A),
"Tornado Fang": (0x28, 0x2C),
"Triad Thunder": (0x2B, 0x23),
"Spinning Blade": (0x20, 0x16),
"Ray Splasher": (0x28, 0x17),
"Gravity Well": (0x38, 0x14),
"Parasitic Bomb": (0x31, 0x28),
"Frost Shield": (0x23, 0x2C),
# X4
"Lightning Web": (0x3D, 0x28),
"Aiming Laser": (0x2C, 0x14),
"Double Cyclone": (0x28, 0x1A),
"Rising Fire": (0x20, 0x16),
"Ground Hunter": (0x2C, 0x15),
"Soul Body": (0x37, 0x27),
"Twin Slasher": (0x28, 0x00),
"Frost Tower": (0x3D, 0x2C),
}
if "worlds.mm2" in sys.modules:
# is this the proper way to do this? who knows!
try:
mm2 = sys.modules["worlds.mm2"]
MM3_KNOWN_COLORS.update(mm2.color.MM2_COLORS)
for item in MM3_COLORS:
mm2.color.add_color_to_mm2(item, MM3_COLORS[item])
except AttributeError:
# pass through if an old MM2 is found
pass
palette_pointers: dict[str, list[int]] = {
"Mega Buster": [0x7C8A8, 0x4650],
"Gemini Laser": [0x4654],
"Needle Cannon": [0x4658],
"Hard Knuckle": [0x465C],
"Magnet Missile": [0x4660],
"Top Spin": [0x4664],
"Search Snake": [0x4668],
"Rush Coil": [0x466C],
"Spark Shock": [0x4670],
"Rush Marine": [0x4674],
"Shadow Blade": [0x4678],
"Rush Jet": [0x467C],
"Needle Man": [0x216C],
"Magnet Man": [0x215C],
"Gemini Man": [0x217C],
"Hard Man": [0x2164],
"Top Man": [0x2194],
"Snake Man": [0x2174],
"Spark Man": [0x2184],
"Shadow Man": [0x218C],
"Doc Robot": [0x20B8]
}
def add_color_to_mm3(name: str, color: tuple[int, int]) -> None:
"""
Add a color combo for Mega Man 3 to recognize as the color to display for a given item.
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
"""
MM3_KNOWN_COLORS[name] = validate_colors(*color)
def extrapolate_color(color: int) -> tuple[int, int]:
if color > 0x1F:
color_1 = color
color_2 = color_1 - 0x10
else:
color_2 = color
color_1 = color_2 + 0x10
return color_1, color_2
def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> tuple[int, int]:
# Black should be reserved for outlines, a gray should suffice
if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
color_1 = 0x10
if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
color_2 = 0x10
# one final check, make sure we don't have two matching
if not allow_match and color_1 == color_2:
color_1 = 0x30 # color 1 to white works with about any paired color
return color_1, color_2
def expand_colors(color_1: int, color_2: int) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
if color_2 >= 0x30:
color_a = color_b = color_2
else:
color_a = color_2 + 0x10
color_b = color_2
if color_1 < 0x10:
color_c = color_1 + 0x10
color_d = color_1
color_e = color_1 + 0x20
elif color_1 >= 0x30:
color_c = color_1 - 0x10
color_d = color_1 - 0x20
color_e = color_1
else:
color_c = color_1
color_d = color_1 - 0x10
color_e = color_1 + 0x10
return (0x30, color_a, color_b), (color_d, color_e, color_c)
def get_colors_for_item(name: str) -> tuple[tuple[int, int, int], tuple[int, int, int]]:
if name in MM3_KNOWN_COLORS:
return expand_colors(*MM3_KNOWN_COLORS[name])
check_colors = {color: color in name.upper().replace(" ", '') for color in HTML_TO_NES}
colors = [color for color in check_colors if check_colors[color]]
if colors:
# we have at least one color pattern matched
if len(colors) > 1:
# we have at least 2
color_1 = HTML_TO_NES[colors[0]]
color_2 = HTML_TO_NES[colors[1]]
else:
color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]])
else:
# generate hash
crc_hash = crc32(name.encode('utf-8'))
hash_color = struct.pack("I", crc_hash)
color_1 = hash_color[0] % 0x3F
color_2 = hash_color[1] % 0x3F
if color_1 < color_2:
temp = color_1
color_1 = color_2
color_2 = temp
color_1, color_2 = validate_colors(color_1, color_2)
return expand_colors(color_1, color_2)
def parse_color(colors: list[str]) -> tuple[int, int]:
color_a = colors[0]
if color_a.startswith("$"):
color_1 = int(color_a[1:], 16)
else:
# assume it's in our list of colors
color_1 = HTML_TO_NES[color_a.upper()]
if len(colors) == 1:
color_1, color_2 = extrapolate_color(color_1)
else:
color_b = colors[1]
if color_b.startswith("$"):
color_2 = int(color_b[1:], 16)
else:
color_2 = HTML_TO_NES[color_b.upper()]
return color_1, color_2
def write_palette_shuffle(world: "MM3World", rom: "MM3ProcedurePatch") -> None:
palette_shuffle: int | str = world.options.palette_shuffle.value
palettes_to_write: dict[str, tuple[int, int]] = {}
if isinstance(palette_shuffle, str):
color_sets = palette_shuffle.split(";")
if len(color_sets) == 1:
palette_shuffle = world.options.palette_shuffle.option_none
# singularity is more correct, but this is faster
else:
palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()]
for color_set in color_sets:
if "-" in color_set:
character, color = color_set.split("-")
if character.title() not in palette_pointers:
logging.warning(f"Player {world.player_name} "
f"attempted to set color for unrecognized option {character}")
colors = color.split("|")
real_colors = validate_colors(*parse_color(colors), allow_match=True)
palettes_to_write[character.title()] = real_colors
else:
# If color is provided with no character, assume singularity
colors = color_set.split("|")
real_colors = validate_colors(*parse_color(colors), allow_match=True)
for character in palette_pointers:
palettes_to_write[character] = real_colors
# Now we handle the real values
if palette_shuffle != 0:
if palette_shuffle > 1:
if palette_shuffle == 3:
# singularity
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
for character in palette_pointers:
if character not in palettes_to_write:
palettes_to_write[character] = real_colors
else:
for character in palette_pointers:
if character not in palettes_to_write:
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
palettes_to_write[character] = real_colors
else:
shuffled_colors = list(MM3_COLORS.values())[:-3] # only include one Doc Robot
shuffled_colors.append((0x2C, 0x11)) # Mega Buster
world.random.shuffle(shuffled_colors)
for character in palette_pointers:
if character not in palettes_to_write:
palettes_to_write[character] = shuffled_colors.pop()
for character in palettes_to_write:
for pointer in palette_pointers[character]:
rom.write_bytes(pointer + 2, bytes(palettes_to_write[character]))

View File

@@ -1,131 +0,0 @@
# Mega Man 3
## 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?
Weapons received from Robot Masters, access to each individual stage (including Doc Robot stages), and Items from Dr. Light are randomized
into the multiworld. Access to the Wily Stages is locked behind clearing the 4 Doc Robot stages and defeating Break Man. The game is complete upon
viewing the ending sequence after defeating Gamma.
## What Mega Man 3 items can appear in other players' worlds?
- Robot Master weapons
- Robot Master Access Codes (stage access)
- Doc Robot Access Codes (stage access)
- Rush Coil/Jet/Marine
- 1-Ups
- E-Tanks
- Health Energy (L)
- Weapon Energy (L)
## What is considered a location check in Mega Man 3?
- The defeat of a Robot Master, Doc Robot, or Wily Boss
- Receiving a weapon or Rush item from Dr. Light
- Optionally, 1-Ups and E-Tanks present within stages
- Optionally, Weapon and Health Energy pickups present within stages
## When the player receives an item, what happens?
A sound effect will play based on the type of item received, and the effects of the item will be immediately applied,
such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving
Health Energy while at full health), the remaining are withheld until they can be applied.
## How do I access the Doc Robot stages?
By pressing Select on the Robot Master screen, the screen will transition between Robot Masters and
Doc Robots.
## Useful Information
* **NesHawk is the recommended core for this game!** Players using QuickNes (or QuickerNes) will experience graphical
glitches while in Gemini Man's stage and fighting Gamma.
* Pressing A+B+Start+Select while in a stage will take you to the Game Over screen, allowing you to leave the stage.
Your E-Tanks will be preserved.
* Your current progress through the Wily stages is saved to the multiworld, allowing you to return to the last stage you
reached should you need to leave and enter a Robot Master stage. If you need to return to an earlier Wily stage, holding
Select while entering Break Man's stage will take you to Wily 1.
* When Random Weaknesses are enabled, Break Man's weakness will be changed from Mega Buster to one random weapon.
## What is EnergyLink?
EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man
3, when enabled, drops from enemies are not applied directly to Mega Man and are instead deposited into the EnergyLink.
Half of the energy that would be gained is lost upon transfer to the EnergyLink.
Energy from the EnergyLink storage can be converted into health, weapon energy, and lives at different conversion rates.
You can find out how much of each type you can pull using `/pool` in the client. Additionally, you can have it
automatically pull from the EnergyLink storage to keep Mega Man healed using the `/autoheal` command in the client.
Finally, you can use the `/request` command to request a certain type of energy from the storage.
## Plando Palettes
The palette shuffle option supports specifying a specific palette for a given weapon/Robot Master. The format for doing
so is `Character-Color1|Color2;Option`. Character is the individual that this should apply to, and can only be one of
the following:
- Mega Buster
- Gemini Laser
- Needle Cannon
- Hard Knuckle
- Magnet Missile
- Top Spin
- Search Snake
- Spark Shot
- Shadow Blade
- Rush Coil
- Rush Jet
- Rush Marine
- Needle Man
- Magnet Man
- Gemini Man
- Hard Man
- Top Man
- Snake Man
- Spark Man
- Shadow Man
- Doc Robot
Colors attempt to map a list of HTML-defined colors to what the NES can render. A full list of applicable colors can be
found [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/mm2/Color.py#L11). Alternatively, colors can
be supplied directly using `$xx` format. A full list of NES colors can be found [here](https://www.nesdev.org/wiki/PPU_palettes#2C02).
You can also pass only one color (such as `Mega Buster-Red`) and it will interpret a second color based off of the color
given. Additionally, passing only colors (such as `Red|Blue`) and not any specific boss/weapon will apply that color to
all weapons/bosses that did not have a prior color specified.
The option is the method to be used to set the palettes of the remaining bosses/weapons, and will not overwrite any
plando placements.
## Plando Weaknesses
Plando Weaknesses allows you to override the amount of damage a boss should take from a given weapon, ignoring prior
weaknesses generated by strict/random weakness options. Formatting for this is as follows:
```yaml
plando_weakness:
Needle Man:
Top Spin: 0
Hard Knuckle: 4
```
This would cause Air Man to take 4 damage from Hard Knuckle, and 0 from Top Spin.
Note: it is possible that plando weakness is not be respected should the plando create a situation in which the game
becomes impossible to complete. In this situation, the damage would be boosted to the minimum required to defeat the
Robot Master.
## Unique Local Commands
- `/pool` Only present with EnergyLink, prints the max amount of each type of request that could be fulfilled.
- `/autoheal` Only present with EnergyLink, will automatically drain energy from the EnergyLink in order to
restore Mega Man's health.
- `/request <amount> <type>` Only present with EnergyLink, sends a request of a certain type of energy to be pulled from
the EnergyLink. Types are as follows:
- `HP` Health
- `NE` Needle Cannon
- `MA` Magnet Missile
- `GE` Gemini Laser
- `HA` Hard Knuckle
- `TO` Top Spin
- `SN` Search Snake
- `SP` Spark Shot
- `SH` Shadow Blade
- `RC` Rush Coil
- `RM` Rush Marine
- `RJ` Rush Jet
- `1U` Lives

View File

@@ -1,53 +0,0 @@
# Mega Man 3 Setup Guide
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- An English Mega Man 3 ROM. Alternatively, the [Mega Man Legacy Collection](https://store.steampowered.com/app/363440/Mega_Man_Legacy_Collection/) on Steam.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later. Bizhawk 2.10
### 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 `.nes` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
`Controllers…`, load any `.nes` 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.
## Generating and Patching a Game
1. Create your options file (YAML). You can make one on the
[Mega Man 3 options page](../../../games/Mega%20Man%203/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 `.apmm3` 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. If you are using the Legacy
Collection, provide `Proteus.exe` in place of your rom.
6. A patched `.nes` 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.
## 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. Mega Man 3 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 and client will eventually connect to each other. The BizHawk Client window should indicate that it
connected and recognized Mega Man 3.
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.

View File

@@ -1,80 +0,0 @@
from BaseClasses import Item
from typing import NamedTuple
from .names import (needle_cannon, magnet_missile, gemini_laser, hard_knuckle, top_spin, search_snake, spark_shock,
shadow_blade, rush_coil, rush_marine, rush_jet, needle_man_stage, magnet_man_stage,
gemini_man_stage, hard_man_stage, top_man_stage, snake_man_stage, spark_man_stage, shadow_man_stage,
doc_needle_stage, doc_gemini_stage, doc_spark_stage, doc_shadow_stage, e_tank, weapon_energy,
health_energy, one_up)
class ItemData(NamedTuple):
code: int
progression: bool
useful: bool = False # primarily use this for incredibly useful items of their class, like Metal Blade
skip_balancing: bool = False
class MM3Item(Item):
game = "Mega Man 3"
robot_master_weapon_table = {
needle_cannon: ItemData(0x0001, True),
magnet_missile: ItemData(0x0002, True, True),
gemini_laser: ItemData(0x0003, True),
hard_knuckle: ItemData(0x0004, True),
top_spin: ItemData(0x0005, True, True),
search_snake: ItemData(0x0006, True),
spark_shock: ItemData(0x0007, True),
shadow_blade: ItemData(0x0008, True, True),
}
stage_access_table = {
needle_man_stage: ItemData(0x0101, True),
magnet_man_stage: ItemData(0x0102, True),
gemini_man_stage: ItemData(0x0103, True),
hard_man_stage: ItemData(0x0104, True),
top_man_stage: ItemData(0x0105, True),
snake_man_stage: ItemData(0x0106, True),
spark_man_stage: ItemData(0x0107, True),
shadow_man_stage: ItemData(0x0108, True),
doc_needle_stage: ItemData(0x0111, True, True),
doc_gemini_stage: ItemData(0x0113, True, True),
doc_spark_stage: ItemData(0x0117, True, True),
doc_shadow_stage: ItemData(0x0118, True, True),
}
rush_item_table = {
rush_coil: ItemData(0x0011, True, True),
rush_marine: ItemData(0x0012, True),
rush_jet: ItemData(0x0013, True, True),
}
filler_item_table = {
one_up: ItemData(0x0020, False),
weapon_energy: ItemData(0x0021, False),
health_energy: ItemData(0x0022, False),
e_tank: ItemData(0x0023, False, True),
}
filler_item_weights = {
one_up: 1,
weapon_energy: 4,
health_energy: 1,
e_tank: 2,
}
item_table = {
**robot_master_weapon_table,
**stage_access_table,
**rush_item_table,
**filler_item_table,
}
item_names = {
"Weapons": {name for name in robot_master_weapon_table.keys()},
"Stages": {name for name in stage_access_table.keys()},
"Rush": {name for name in rush_item_table.keys()}
}
lookup_item_to_id: dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code}

View File

@@ -1,312 +0,0 @@
from BaseClasses import Location, Region
from typing import NamedTuple
from . import names
class MM3Location(Location):
game = "Mega Man 3"
class MM3Region(Region):
game = "Mega Man 3"
class LocationData(NamedTuple):
location_id: int | None
energy: bool = False
oneup_tank: bool = False
class RegionData(NamedTuple):
locations: dict[str, LocationData]
required_items: list[str]
parent: str = ""
mm3_regions: dict[str, RegionData] = {
"Needle Man Stage": RegionData({
names.needle_man: LocationData(0x0001),
names.get_needle_cannon: LocationData(0x0101),
names.get_rush_jet: LocationData(0x0111),
names.needle_man_c1: LocationData(0x0200, energy=True),
names.needle_man_c2: LocationData(0x0201, oneup_tank=True),
}, [names.needle_man_stage]),
"Magnet Man Stage": RegionData({
names.magnet_man: LocationData(0x0002),
names.get_magnet_missile: LocationData(0x0102),
names.magnet_man_c1: LocationData(0x0202, energy=True),
names.magnet_man_c2: LocationData(0x0203, energy=True),
names.magnet_man_c3: LocationData(0x0204, energy=True),
names.magnet_man_c4: LocationData(0x0205, energy=True),
names.magnet_man_c5: LocationData(0x0206, energy=True),
names.magnet_man_c6: LocationData(0x0207, energy=True),
names.magnet_man_c7: LocationData(0x0208, energy=True),
names.magnet_man_c8: LocationData(0x0209, energy=True),
}, [names.magnet_man_stage]),
"Gemini Man Stage": RegionData({
names.gemini_man: LocationData(0x0003),
names.get_gemini_laser: LocationData(0x0103),
names.gemini_man_c1: LocationData(0x020A, oneup_tank=True),
names.gemini_man_c2: LocationData(0x020B, energy=True),
names.gemini_man_c3: LocationData(0x020C, oneup_tank=True),
names.gemini_man_c4: LocationData(0x020D, energy=True),
names.gemini_man_c5: LocationData(0x020E, energy=True),
names.gemini_man_c6: LocationData(0x020F, oneup_tank=True),
names.gemini_man_c7: LocationData(0x0210, oneup_tank=True),
names.gemini_man_c8: LocationData(0x0211, energy=True),
names.gemini_man_c9: LocationData(0x0212, energy=True),
names.gemini_man_c10: LocationData(0x0213, oneup_tank=True),
}, [names.gemini_man_stage]),
"Hard Man Stage": RegionData({
names.hard_man: LocationData(0x0004),
names.get_hard_knuckle: LocationData(0x0104),
names.hard_man_c1: LocationData(0x0214, energy=True),
names.hard_man_c2: LocationData(0x0215, energy=True),
names.hard_man_c3: LocationData(0x0216, oneup_tank=True),
names.hard_man_c4: LocationData(0x0217, energy=True),
names.hard_man_c5: LocationData(0x0218, energy=True),
names.hard_man_c6: LocationData(0x0219, energy=True),
names.hard_man_c7: LocationData(0x021A, energy=True),
}, [names.hard_man_stage]),
"Top Man Stage": RegionData({
names.top_man: LocationData(0x0005),
names.get_top_spin: LocationData(0x0105),
names.top_man_c1: LocationData(0x021B, energy=True),
names.top_man_c2: LocationData(0x021C, energy=True),
names.top_man_c3: LocationData(0x021D, energy=True),
names.top_man_c4: LocationData(0x021E, energy=True),
names.top_man_c5: LocationData(0x021F, energy=True),
names.top_man_c6: LocationData(0x0220, oneup_tank=True),
names.top_man_c7: LocationData(0x0221, energy=True),
names.top_man_c8: LocationData(0x0222, energy=True),
}, [names.top_man_stage]),
"Snake Man Stage": RegionData({
names.snake_man: LocationData(0x0006),
names.get_search_snake: LocationData(0x0106),
names.snake_man_c1: LocationData(0x0223, energy=True),
names.snake_man_c2: LocationData(0x0224, energy=True),
names.snake_man_c3: LocationData(0x0225, oneup_tank=True),
names.snake_man_c4: LocationData(0x0226, oneup_tank=True),
names.snake_man_c5: LocationData(0x0227, energy=True),
}, [names.snake_man_stage]),
"Spark Man Stage": RegionData({
names.spark_man: LocationData(0x0007),
names.get_spark_shock: LocationData(0x0107),
names.spark_man_c1: LocationData(0x0228, energy=True),
names.spark_man_c2: LocationData(0x0229, energy=True),
names.spark_man_c3: LocationData(0x022A, energy=True),
names.spark_man_c4: LocationData(0x022B, energy=True),
names.spark_man_c5: LocationData(0x022C, energy=True),
names.spark_man_c6: LocationData(0x022D, energy=True),
}, [names.spark_man_stage]),
"Shadow Man Stage": RegionData({
names.shadow_man: LocationData(0x0008),
names.get_shadow_blade: LocationData(0x0108),
names.get_rush_marine: LocationData(0x0112),
names.shadow_man_c1: LocationData(0x022E, energy=True),
names.shadow_man_c2: LocationData(0x022F, energy=True),
names.shadow_man_c3: LocationData(0x0230, energy=True),
names.shadow_man_c4: LocationData(0x0231, energy=True),
}, [names.shadow_man_stage]),
"Doc Robot (Needle) - Air": RegionData({
names.doc_air: LocationData(0x0010),
names.doc_needle_c1: LocationData(0x0232, energy=True),
names.doc_needle_c2: LocationData(0x0233, oneup_tank=True),
names.doc_needle_c3: LocationData(0x0234, oneup_tank=True),
}, [names.doc_needle_stage]),
"Doc Robot (Needle) - Crash": RegionData({
names.doc_crash: LocationData(0x0011),
names.doc_needle: LocationData(None),
names.doc_needle_c4: LocationData(0x0235, energy=True),
names.doc_needle_c5: LocationData(0x0236, energy=True),
names.doc_needle_c6: LocationData(0x0237, energy=True),
names.doc_needle_c7: LocationData(0x0238, energy=True),
names.doc_needle_c8: LocationData(0x0239, energy=True),
names.doc_needle_c9: LocationData(0x023A, energy=True),
names.doc_needle_c10: LocationData(0x023B, energy=True),
names.doc_needle_c11: LocationData(0x023C, energy=True),
}, [], parent="Doc Robot (Needle) - Air"),
"Doc Robot (Gemini) - Flash": RegionData({
names.doc_flash: LocationData(0x0012),
names.doc_gemini_c1: LocationData(0x023D, oneup_tank=True),
names.doc_gemini_c2: LocationData(0x023E, oneup_tank=True),
}, [names.doc_gemini_stage]),
"Doc Robot (Gemini) - Bubble": RegionData({
names.doc_bubble: LocationData(0x0013),
names.doc_gemini: LocationData(None),
names.doc_gemini_c3: LocationData(0x023F, energy=True),
names.doc_gemini_c4: LocationData(0x0240, energy=True),
}, [], parent="Doc Robot (Gemini) - Flash"),
"Doc Robot (Shadow) - Wood": RegionData({
names.doc_wood: LocationData(0x0014),
}, [names.doc_shadow_stage]),
"Doc Robot (Shadow) - Heat": RegionData({
names.doc_heat: LocationData(0x0015),
names.doc_shadow: LocationData(None),
names.doc_shadow_c1: LocationData(0x0243, energy=True),
names.doc_shadow_c2: LocationData(0x0244, energy=True),
names.doc_shadow_c3: LocationData(0x0245, energy=True),
names.doc_shadow_c4: LocationData(0x0246, energy=True),
names.doc_shadow_c5: LocationData(0x0247, energy=True),
}, [], parent="Doc Robot (Shadow) - Wood"),
"Doc Robot (Spark) - Metal": RegionData({
names.doc_metal: LocationData(0x0016),
names.doc_spark_c1: LocationData(0x0241, energy=True),
}, [names.doc_spark_stage]),
"Doc Robot (Spark) - Quick": RegionData({
names.doc_quick: LocationData(0x0017),
names.doc_spark: LocationData(None),
names.doc_spark_c2: LocationData(0x0242, energy=True),
}, [], parent="Doc Robot (Spark) - Metal"),
"Break Man": RegionData({
names.break_man: LocationData(0x000F),
names.break_stage: LocationData(None),
}, [names.doc_needle, names.doc_gemini, names.doc_spark, names.doc_shadow]),
"Wily Stage 1": RegionData({
names.wily_1_boss: LocationData(0x0009),
names.wily_stage_1: LocationData(None),
names.wily_1_c1: LocationData(0x0248, oneup_tank=True),
names.wily_1_c2: LocationData(0x0249, oneup_tank=True),
names.wily_1_c3: LocationData(0x024A, energy=True),
names.wily_1_c4: LocationData(0x024B, oneup_tank=True),
names.wily_1_c5: LocationData(0x024C, energy=True),
names.wily_1_c6: LocationData(0x024D, energy=True),
names.wily_1_c7: LocationData(0x024E, energy=True),
names.wily_1_c8: LocationData(0x024F, oneup_tank=True),
names.wily_1_c9: LocationData(0x0250, energy=True),
names.wily_1_c10: LocationData(0x0251, energy=True),
names.wily_1_c11: LocationData(0x0252, energy=True),
names.wily_1_c12: LocationData(0x0253, energy=True),
}, [names.break_stage], parent="Break Man"),
"Wily Stage 2": RegionData({
names.wily_2_boss: LocationData(0x000A),
names.wily_stage_2: LocationData(None),
names.wily_2_c1: LocationData(0x0254, energy=True),
names.wily_2_c2: LocationData(0x0255, energy=True),
names.wily_2_c3: LocationData(0x0256, oneup_tank=True),
names.wily_2_c4: LocationData(0x0257, energy=True),
names.wily_2_c5: LocationData(0x0258, energy=True),
names.wily_2_c6: LocationData(0x0259, energy=True),
names.wily_2_c7: LocationData(0x025A, energy=True),
names.wily_2_c8: LocationData(0x025B, energy=True),
names.wily_2_c9: LocationData(0x025C, oneup_tank=True),
names.wily_2_c10: LocationData(0x025D, energy=True),
names.wily_2_c11: LocationData(0x025E, oneup_tank=True),
names.wily_2_c12: LocationData(0x025F, energy=True),
names.wily_2_c13: LocationData(0x0260, energy=True),
}, [names.wily_stage_1], parent="Wily Stage 1"),
"Wily Stage 3": RegionData({
names.wily_3_boss: LocationData(0x000B),
names.wily_stage_3: LocationData(None),
names.wily_3_c1: LocationData(0x0261, energy=True),
names.wily_3_c2: LocationData(0x0262, energy=True),
names.wily_3_c3: LocationData(0x0263, oneup_tank=True),
names.wily_3_c4: LocationData(0x0264, oneup_tank=True),
names.wily_3_c5: LocationData(0x0265, energy=True),
names.wily_3_c6: LocationData(0x0266, energy=True),
names.wily_3_c7: LocationData(0x0267, energy=True),
names.wily_3_c8: LocationData(0x0268, energy=True),
names.wily_3_c9: LocationData(0x0269, energy=True),
names.wily_3_c10: LocationData(0x026A, oneup_tank=True),
names.wily_3_c11: LocationData(0x026B, oneup_tank=True)
}, [names.wily_stage_2], parent="Wily Stage 2"),
"Wily Stage 4": RegionData({
names.wily_stage_4: LocationData(None),
names.wily_4_c1: LocationData(0x026C, energy=True),
names.wily_4_c2: LocationData(0x026D, energy=True),
names.wily_4_c3: LocationData(0x026E, energy=True),
names.wily_4_c4: LocationData(0x026F, energy=True),
names.wily_4_c5: LocationData(0x0270, energy=True),
names.wily_4_c6: LocationData(0x0271, energy=True),
names.wily_4_c7: LocationData(0x0272, energy=True),
names.wily_4_c8: LocationData(0x0273, energy=True),
names.wily_4_c9: LocationData(0x0274, energy=True),
names.wily_4_c10: LocationData(0x0275, oneup_tank=True),
names.wily_4_c11: LocationData(0x0276, energy=True),
names.wily_4_c12: LocationData(0x0277, oneup_tank=True),
names.wily_4_c13: LocationData(0x0278, energy=True),
names.wily_4_c14: LocationData(0x0279, energy=True),
names.wily_4_c15: LocationData(0x027A, energy=True),
names.wily_4_c16: LocationData(0x027B, energy=True),
names.wily_4_c17: LocationData(0x027C, energy=True),
names.wily_4_c18: LocationData(0x027D, energy=True),
names.wily_4_c19: LocationData(0x027E, energy=True),
names.wily_4_c20: LocationData(0x027F, energy=True),
}, [names.wily_stage_3], parent="Wily Stage 3"),
"Wily Stage 5": RegionData({
names.wily_5_boss: LocationData(0x000D),
names.wily_stage_5: LocationData(None),
names.wily_5_c1: LocationData(0x0280, energy=True),
names.wily_5_c2: LocationData(0x0281, energy=True),
names.wily_5_c3: LocationData(0x0282, oneup_tank=True),
names.wily_5_c4: LocationData(0x0283, oneup_tank=True),
}, [names.wily_stage_4], parent="Wily Stage 4"),
"Wily Stage 6": RegionData({
names.gamma: LocationData(None),
names.wily_6_c1: LocationData(0x0284, oneup_tank=True),
names.wily_6_c2: LocationData(0x0285, oneup_tank=True),
names.wily_6_c3: LocationData(0x0286, energy=True),
names.wily_6_c4: LocationData(0x0287, energy=True),
names.wily_6_c5: LocationData(0x0288, oneup_tank=True),
names.wily_6_c6: LocationData(0x0289, oneup_tank=True),
names.wily_6_c7: LocationData(0x028A, energy=True),
}, [names.wily_stage_5], parent="Wily Stage 5"),
}
def get_boss_locations(region: str) -> list[str]:
return [location for location, data in mm3_regions[region].locations.items()
if not data.energy and not data.oneup_tank]
def get_energy_locations(region: str) -> list[str]:
return [location for location, data in mm3_regions[region].locations.items() if data.energy]
def get_oneup_locations(region: str) -> list[str]:
return [location for location, data in mm3_regions[region].locations.items() if data.oneup_tank]
location_table: dict[str, int | None] = {
location: data.location_id for region in mm3_regions.values() for location, data in region.locations.items()
}
location_groups = {
"Get Equipped": {
names.get_needle_cannon,
names.get_magnet_missile,
names.get_gemini_laser,
names.get_hard_knuckle,
names.get_top_spin,
names.get_search_snake,
names.get_spark_shock,
names.get_shadow_blade,
names.get_rush_marine,
names.get_rush_jet,
},
**{name: {location for location, data in region.locations.items() if data.location_id} for name, region in mm3_regions.items()}
}
lookup_location_to_id: dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None}

View File

@@ -1,221 +0,0 @@
# Robot Master Weapons
gemini_laser = "Gemini Laser"
needle_cannon = "Needle Cannon"
hard_knuckle = "Hard Knuckle"
magnet_missile = "Magnet Missile"
top_spin = "Top Spin"
search_snake = "Search Snake"
spark_shock = "Spark Shock"
shadow_blade = "Shadow Blade"
# Rush
rush_coil = "Rush Coil"
rush_jet = "Rush Jet"
rush_marine = "Rush Marine"
# Access Codes
needle_man_stage = "Needle Man Access Codes"
magnet_man_stage = "Magnet Man Access Codes"
gemini_man_stage = "Gemini Man Access Codes"
hard_man_stage = "Hard Man Access Codes"
top_man_stage = "Top Man Access Codes"
snake_man_stage = "Snake Man Access Codes"
spark_man_stage = "Spark Man Access Codes"
shadow_man_stage = "Shadow Man Access Codes"
doc_needle_stage = "Doc Robot (Needle) Access Codes"
doc_gemini_stage = "Doc Robot (Gemini) Access Codes"
doc_spark_stage = "Doc Robot (Spark) Access Codes"
doc_shadow_stage = "Doc Robot (Shadow) Access Codes"
# Misc. Items
one_up = "1-Up"
weapon_energy = "Weapon Energy (L)"
health_energy = "Health Energy (L)"
e_tank = "E-Tank"
needle_man = "Needle Man - Defeated"
magnet_man = "Magnet Man - Defeated"
gemini_man = "Gemini Man - Defeated"
hard_man = "Hard Man - Defeated"
top_man = "Top Man - Defeated"
snake_man = "Snake Man - Defeated"
spark_man = "Spark Man - Defeated"
shadow_man = "Shadow Man - Defeated"
doc_air = "Doc Robot (Air) - Defeated"
doc_crash = "Doc Robot (Crash) - Defeated"
doc_flash = "Doc Robot (Flash) - Defeated"
doc_bubble = "Doc Robot (Bubble) - Defeated"
doc_wood = "Doc Robot (Wood) - Defeated"
doc_heat = "Doc Robot (Heat) - Defeated"
doc_metal = "Doc Robot (Metal) - Defeated"
doc_quick = "Doc Robot (Quick) - Defeated"
break_man = "Break Man - Defeated"
wily_1_boss = "Kamegoro Maker - Defeated"
wily_2_boss = "Yellow Devil MK-II - Defeated"
wily_3_boss = "Holograph Mega Man - Defeated"
wily_5_boss = "Wily Machine 3 - Defeated"
gamma = "Gamma - Defeated"
get_gemini_laser = "Gemini Laser - Received"
get_needle_cannon = "Needle Cannon - Received"
get_hard_knuckle = "Hard Knuckle - Received"
get_magnet_missile = "Magnet Missile - Received"
get_top_spin = "Top Spin - Received"
get_search_snake = "Search Snake - Received"
get_spark_shock = "Spark Shock - Received"
get_shadow_blade = "Shadow Blade - Received"
get_rush_jet = "Rush Jet - Received"
get_rush_marine = "Rush Marine - Received"
# Wily Stage Event Items
doc_needle = "Doc Robot (Needle) - Completed"
doc_gemini = "Doc Robot (Gemini) - Completed"
doc_spark = "Doc Robot (Spark) - Completed"
doc_shadow = "Doc Robot (Shadow) - Completed"
break_stage = "Break Man"
wily_stage_1 = "Wily Stage 1 - Completed"
wily_stage_2 = "Wily Stage 2 - Completed"
wily_stage_3 = "Wily Stage 3 - Completed"
wily_stage_4 = "Wily Stage 4 - Completed"
wily_stage_5 = "Wily Stage 5 - Completed"
# Consumable Locations
needle_man_c1 = "Needle Man Stage - Weapon Energy 1"
needle_man_c2 = "Needle Man Stage - E-Tank"
magnet_man_c1 = "Magnet Man Stage - Health Energy 1"
magnet_man_c2 = "Magnet Man Stage - Health Energy 2"
magnet_man_c3 = "Magnet Man Stage - Health Energy 3"
magnet_man_c4 = "Magnet Man Stage - Health Energy 4"
magnet_man_c5 = "Magnet Man Stage - Weapon Energy 1"
magnet_man_c6 = "Magnet Man Stage - Weapon Energy 2"
magnet_man_c7 = "Magnet Man Stage - Weapon Energy 3"
magnet_man_c8 = "Magnet Man Stage - Health Energy 5"
gemini_man_c1 = "Gemini Man Stage - 1-Up 1"
gemini_man_c2 = "Gemini Man Stage - Health Energy 1"
gemini_man_c3 = "Gemini Man Stage - Mystery Tank"
gemini_man_c4 = "Gemini Man Stage - Weapon Energy 1"
gemini_man_c5 = "Gemini Man Stage - Health Energy 2"
gemini_man_c6 = "Gemini Man Stage - 1-Up 2"
gemini_man_c7 = "Gemini Man Stage - E-Tank 1"
gemini_man_c8 = "Gemini Man Stage - Weapon Energy 2"
gemini_man_c9 = "Gemini Man Stage - Weapon Energy 3"
gemini_man_c10 = "Gemini Man Stage - E-Tank 2"
hard_man_c1 = "Hard Man Stage - Health Energy 1"
hard_man_c2 = "Hard Man Stage - Health Energy 2"
hard_man_c3 = "Hard Man Stage - E-Tank"
hard_man_c4 = "Hard Man Stage - Health Energy 3"
hard_man_c5 = "Hard Man Stage - Health Energy 4"
hard_man_c6 = "Hard Man Stage - Health Energy 5"
hard_man_c7 = "Hard Man Stage - Health Energy 6"
top_man_c1 = "Top Man Stage - Health Energy 1"
top_man_c2 = "Top Man Stage - Health Energy 2"
top_man_c3 = "Top Man Stage - Health Energy 3"
top_man_c4 = "Top Man Stage - Health Energy 4"
top_man_c5 = "Top Man Stage - Weapon Energy 1"
top_man_c6 = "Top Man Stage - 1-Up"
top_man_c7 = "Top Man Stage - Health Energy 5"
top_man_c8 = "Top Man Stage - Health Energy 6"
snake_man_c1 = "Snake Man Stage - Health Energy 1"
snake_man_c2 = "Snake Man Stage - Health Energy 2"
snake_man_c3 = "Snake Man Stage - Mystery Tank 1"
snake_man_c4 = "Snake Man Stage - Mystery Tank 2"
snake_man_c5 = "Snake Man Stage - Health Energy 3"
spark_man_c1 = "Spark Man Stage - Health Energy 1"
spark_man_c2 = "Spark Man Stage - Weapon Energy 1"
spark_man_c3 = "Spark Man Stage - Weapon Energy 2"
spark_man_c4 = "Spark Man Stage - Weapon Energy 3"
spark_man_c5 = "Spark Man Stage - Weapon Energy 4"
spark_man_c6 = "Spark Man Stage - Weapon Energy 5"
shadow_man_c1 = "Shadow Man Stage - Weapon Energy 1"
shadow_man_c2 = "Shadow Man Stage - Weapon Energy 2"
shadow_man_c3 = "Shadow Man Stage - Weapon Energy 3"
shadow_man_c4 = "Shadow Man Stage - Weapon Energy 4"
doc_needle_c1 = "Doc Robot (Needle) - Health Energy 1"
doc_needle_c2 = "Doc Robot (Needle) - 1-Up 1"
doc_needle_c3 = "Doc Robot (Needle) - E-Tank 1"
doc_needle_c4 = "Doc Robot (Needle) - Weapon Energy 1"
doc_needle_c5 = "Doc Robot (Needle) - Weapon Energy 2"
doc_needle_c6 = "Doc Robot (Needle) - Weapon Energy 3"
doc_needle_c7 = "Doc Robot (Needle) - Weapon Energy 4"
doc_needle_c8 = "Doc Robot (Needle) - Weapon Energy 5"
doc_needle_c9 = "Doc Robot (Needle) - Weapon Energy 6"
doc_needle_c10 = "Doc Robot (Needle) - Weapon Energy 7"
doc_needle_c11 = "Doc Robot (Needle) - Health Energy 2"
doc_gemini_c1 = "Doc Robot (Gemini) - Mystery Tank 1"
doc_gemini_c2 = "Doc Robot (Gemini) - Mystery Tank 2"
doc_gemini_c3 = "Doc Robot (Gemini) - Weapon Energy 1"
doc_gemini_c4 = "Doc Robot (Gemini) - Weapon Energy 2"
doc_spark_c1 = "Doc Robot (Spark) - Health Energy 1"
doc_spark_c2 = "Doc Robot (Spark) - Health Energy 2"
doc_shadow_c1 = "Doc Robot (Shadow) - Health Energy 1"
doc_shadow_c2 = "Doc Robot (Shadow) - Weapon Energy 1"
doc_shadow_c3 = "Doc Robot (Shadow) - Weapon Energy 2"
doc_shadow_c4 = "Doc Robot (Shadow) - Weapon Energy 3"
doc_shadow_c5 = "Doc Robot (Shadow) - Weapon Energy 4"
wily_1_c1 = "Wily Stage 1 - 1-Up 1"
wily_1_c2 = "Wily Stage 1 - E-Tank 1"
wily_1_c3 = "Wily Stage 1 - Weapon Energy 1"
wily_1_c4 = "Wily Stage 1 - 1-Up 2" # Hard Knuckle
wily_1_c5 = "Wily Stage 1 - Health Energy 1" # Hard Knuckle
wily_1_c6 = "Wily Stage 1 - Weapon Energy 2" # Hard Knuckle & Rush Vertical
wily_1_c7 = "Wily Stage 1 - Health Energy 2" # Hard Knuckle & Rush Vertical
wily_1_c8 = "Wily Stage 1 - E-Tank 2" # Hard Knuckle & Rush Vertical
wily_1_c9 = "Wily Stage 1 - Health Energy 3"
wily_1_c10 = "Wily Stage 1 - Health Energy 4"
wily_1_c11 = "Wily Stage 1 - Weapon Energy 3" # Rush Vertical
wily_1_c12 = "Wily Stage 1 - Weapon Energy 4" # Rush Vertical
wily_2_c1 = "Wily Stage 2 - Weapon Energy 1"
wily_2_c2 = "Wily Stage 2 - Weapon Energy 2"
wily_2_c3 = "Wily Stage 2 - 1-Up 1"
wily_2_c4 = "Wily Stage 2 - Weapon Energy 3"
wily_2_c5 = "Wily Stage 2 - Health Energy 1"
wily_2_c6 = "Wily Stage 2 - Health Energy 2"
wily_2_c7 = "Wily Stage 2 - Health Energy 3"
wily_2_c8 = "Wily Stage 2 - Weapon Energy 4"
wily_2_c9 = "Wily Stage 2 - E-Tank 1"
wily_2_c10 = "Wily Stage 2 - Weapon Energy 5"
wily_2_c11 = "Wily Stage 2 - E-Tank 2"
wily_2_c12 = "Wily Stage 2 - Weapon Energy 6"
wily_2_c13 = "Wily Stage 2 - Weapon Energy 7"
wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # Hard Knuckle
wily_3_c2 = "Wily Stage 3 - Weapon Energy 2" # Hard Knuckle
wily_3_c3 = "Wily Stage 3 - E-Tank 1"
wily_3_c4 = "Wily Stage 3 - 1-Up 1"
wily_3_c5 = "Wily Stage 3 - Health Energy 1"
wily_3_c6 = "Wily Stage 3 - Health Energy 2"
wily_3_c7 = "Wily Stage 3 - Health Energy 3"
wily_3_c8 = "Wily Stage 3 - Health Energy 4"
wily_3_c9 = "Wily Stage 3 - Weapon Energy 3"
wily_3_c10 = "Wily Stage 3 - Mystery Tank 1" # Hard Knuckle
wily_3_c11 = "Wily Stage 3 - Mystery Tank 2" # Hard Knuckle
wily_4_c1 = "Wily Stage 4 - Weapon Energy 1"
wily_4_c2 = "Wily Stage 4 - Weapon Energy 2"
wily_4_c3 = "Wily Stage 4 - Weapon Energy 3"
wily_4_c4 = "Wily Stage 4 - Weapon Energy 4"
wily_4_c5 = "Wily Stage 4 - Weapon Energy 5"
wily_4_c6 = "Wily Stage 4 - Health Energy 1"
wily_4_c7 = "Wily Stage 4 - Health Energy 2"
wily_4_c8 = "Wily Stage 4 - Health Energy 3"
wily_4_c9 = "Wily Stage 4 - Health Energy 4"
wily_4_c10 = "Wily Stage 4 - Mystery Tank"
wily_4_c11 = "Wily Stage 4 - Weapon Energy 6"
wily_4_c12 = "Wily Stage 4 - 1-Up"
wily_4_c13 = "Wily Stage 4 - Weapon Energy 7"
wily_4_c14 = "Wily Stage 4 - Weapon Energy 8"
wily_4_c15 = "Wily Stage 4 - Weapon Energy 9"
wily_4_c16 = "Wily Stage 4 - Weapon Energy 10"
wily_4_c17 = "Wily Stage 4 - Weapon Energy 11"
wily_4_c18 = "Wily Stage 4 - Weapon Energy 12"
wily_4_c19 = "Wily Stage 4 - Weapon Energy 13"
wily_4_c20 = "Wily Stage 4 - Weapon Energy 14"
wily_5_c1 = "Wily Stage 5 - Weapon Energy 1"
wily_5_c2 = "Wily Stage 5 - Weapon Energy 2"
wily_5_c3 = "Wily Stage 5 - Mystery Tank 1"
wily_5_c4 = "Wily Stage 5 - Mystery Tank 2"
wily_6_c1 = "Wily Stage 6 - Mystery Tank 1"
wily_6_c2 = "Wily Stage 6 - Mystery Tank 2"
wily_6_c3 = "Wily Stage 6 - Weapon Energy 1"
wily_6_c4 = "Wily Stage 6 - Weapon Energy 2"
wily_6_c5 = "Wily Stage 6 - 1-Up"
wily_6_c6 = "Wily Stage 6 - E-Tank"
wily_6_c7 = "Wily Stage 6 - Health Energy"

View File

@@ -1,164 +0,0 @@
from dataclasses import dataclass
from Options import Choice, Toggle, DeathLink, TextChoice, Range, OptionDict, PerGameCommonOptions
from schema import Schema, And, Use, Optional
from .rules import bosses, weapons_to_id
class EnergyLink(Toggle):
"""
Enables EnergyLink support.
When enabled, pickups dropped from enemies are sent to the EnergyLink pool, and healing/weapon energy/1-Ups can
be requested from the EnergyLink pool.
Some of the energy sent to the pool will be lost on transfer.
"""
display_name = "EnergyLink"
class StartingRobotMaster(Choice):
"""
The initial stage unlocked at the start.
"""
display_name = "Starting Robot Master"
option_needle_man = 0
option_magnet_man = 1
option_gemini_man = 2
option_hard_man = 3
option_top_man = 4
option_snake_man = 5
option_spark_man = 6
option_shadow_man = 7
default = "random"
class Consumables(Choice):
"""
When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks.
"""
display_name = "Consumables"
option_none = 0
option_1up_etank = 1
option_weapon_health = 2
option_all = 3
default = 1
alias_true = 3
alias_false = 0
@classmethod
def get_option_name(cls, value: int) -> str:
if value == 1:
return "1-Ups/E-Tanks"
elif value == 2:
return "Weapon/Health Energy"
return super().get_option_name(value)
class PaletteShuffle(TextChoice):
"""
Change the color of Mega Man and the Robot Masters.
None: The palettes are unchanged.
Shuffled: Palette colors are shuffled amongst the robot masters.
Randomized: Random (usually good) palettes are generated for each robot master.
Singularity: one palette is generated and used for all robot masters.
Supports custom palettes using HTML named colors in the
following format: Mega Buster-Lavender|Violet;randomized
The first value is the character whose palette you'd like to define, then separated by - is a set of 2 colors for
that character. separate every color with a pipe, and separate every character as well as the remaining shuffle with
a semicolon.
"""
display_name = "Palette Shuffle"
option_none = 0
option_shuffled = 1
option_randomized = 2
option_singularity = 3
class EnemyWeaknesses(Toggle):
"""
Randomizes the damage dealt to enemies by weapons. Certain enemies will always take damage from the buster.
"""
display_name = "Random Enemy Weaknesses"
class StrictWeaknesses(Toggle):
"""
Only your starting Robot Master will take damage from the Mega Buster, the rest must be defeated with weapons.
Weapons that only do 1-3 damage to bosses no longer deal damage (aside from Wily/Gamma).
"""
display_name = "Strict Boss Weaknesses"
class RandomWeaknesses(Choice):
"""
None: Bosses will have their regular weaknesses.
Shuffled: Weapon damage will be shuffled amongst the weapons, so Shadow Blade may do Top Spin damage.
Randomized: Weapon damage will be fully randomized.
"""
display_name = "Random Boss Weaknesses"
option_none = 0
option_shuffled = 1
option_randomized = 2
alias_false = 0
alias_true = 2
class Wily4Requirement(Range):
"""
Change the amount of Robot Masters that are required to be defeated for
the door to the Wily Machine to open.
"""
display_name = "Wily 4 Requirement"
default = 8
range_start = 1
range_end = 8
class WeaknessPlando(OptionDict):
"""
Specify specific damage numbers for boss damage. Can be used even without strict/random weaknesses.
plando_weakness:
Robot Master:
Weapon: Damage
"""
display_name = "Plando Weaknesses"
schema = Schema({
Optional(And(str, Use(str.title), lambda s: s in bosses)): {
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(0, 14))
}
})
default = {}
class ReduceFlashing(Toggle):
"""
Reduce flashing seen in gameplay, such as in stages and when defeating certain bosses.
"""
display_name = "Reduce Flashing"
class MusicShuffle(Choice):
"""
Shuffle the music that plays in every stage
"""
display_name = "Music Shuffle"
option_none = 0
option_shuffled = 1
option_randomized = 2
option_no_music = 3
default = 0
@dataclass
class MM3Options(PerGameCommonOptions):
death_link: DeathLink
energy_link: EnergyLink
starting_robot_master: StartingRobotMaster
consumables: Consumables
enemy_weakness: EnemyWeaknesses
strict_weakness: StrictWeaknesses
random_weakness: RandomWeaknesses
wily_4_requirement: Wily4Requirement
plando_weakness: WeaknessPlando
palette_shuffle: PaletteShuffle
reduce_flashing: ReduceFlashing
music_shuffle: MusicShuffle

View File

@@ -1,374 +0,0 @@
import pkgutil
from typing import TYPE_CHECKING, Iterable
import hashlib
import Utils
import os
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
from . import names
from .rules import bosses
from .text import MM3TextEntry
from .color import get_colors_for_item, write_palette_shuffle
from .options import Consumables
if TYPE_CHECKING:
from . import MM3World
MM3LCHASH = "5266687de215e790b2008284402f3917"
PROTEUSHASH = "b69fff40212b80c94f19e786d1efbf61"
MM3NESHASH = "4a53b6f58067d62c9a43404fe835dd5c"
MM3VCHASH = "c50008f1ac86fae8d083232cdd3001a5"
enemy_weakness_ptrs: dict[int, int] = {
0: 0x14100,
1: 0x14200,
2: 0x14300,
3: 0x14400,
4: 0x14500,
5: 0x14600,
6: 0x14700,
7: 0x14800,
8: 0x14900,
}
enemy_addresses: dict[str, int] = {
"Dada": 0x12,
"Potton": 0x13,
"New Shotman": 0x15,
"Hammer Joe": 0x16,
"Peterchy": 0x17,
"Bubukan": 0x18,
"Vault Pole": 0x19, # Capcom..., why did you name an enemy Pole?
"Bomb Flier": 0x1A,
"Yambow": 0x1D,
"Metall 2": 0x1E,
"Cannon": 0x22,
"Jamacy": 0x25,
"Jamacy 2": 0x26, # dunno what this is, but I won't question
"Jamacy 3": 0x27,
"Jamacy 4": 0x28, # tf is this Capcom
"Mag Fly": 0x2A,
"Egg": 0x2D,
"Gyoraibo 2": 0x2E,
"Junk Golem": 0x2F,
"Pickelman Bull": 0x30,
"Nitron": 0x35,
"Pole": 0x37,
"Gyoraibo": 0x38,
"Hari Harry": 0x3A,
"Penpen Maker": 0x3B,
"Returning Monking": 0x3C,
"Have 'Su' Bee": 0x3E,
"Hive": 0x3F,
"Bolton-Nutton": 0x40,
"Walking Bomb": 0x44,
"Elec'n": 0x45,
"Mechakkero": 0x47,
"Chibee": 0x4B,
"Swimming Penpen": 0x4D,
"Top": 0x52,
"Penpen": 0x56,
"Komasaburo": 0x57,
"Parasyu": 0x59,
"Hologran (Static)": 0x5A,
"Hologran (Moving)": 0x5B,
"Bomber Pepe": 0x5C,
"Metall DX": 0x5D,
"Petit Snakey": 0x5E,
"Proto Man": 0x62,
"Break Man": 0x63,
"Metall": 0x7D,
"Giant Springer": 0x83,
"Springer Missile": 0x85,
"Giant Snakey": 0x99,
"Tama": 0x9A,
"Doc Robot (Flash)": 0xB0,
"Doc Robot (Wood)": 0xB1,
"Doc Robot (Crash)": 0xB2,
"Doc Robot (Metal)": 0xB3,
"Doc Robot (Bubble)": 0xC0,
"Doc Robot (Heat)": 0xC1,
"Doc Robot (Quick)": 0xC2,
"Doc Robot (Air)": 0xC3,
"Snake": 0xCA,
"Needle Man": 0xD0,
"Magnet Man": 0xD1,
"Top Man": 0xD2,
"Shadow Man": 0xD3,
"Top Man's Top": 0xD5,
"Shadow Man (Sliding)": 0xD8, # Capcom I swear
"Hard Man": 0xE0,
"Spark Man": 0xE2,
"Snake Man": 0xE4,
"Gemini Man": 0xE6,
"Gemini Man (Clone)": 0xE7, # Capcom why
"Yellow Devil MK-II": 0xF1,
"Wily Machine 3": 0xF3,
"Gamma": 0xF8,
"Kamegoro": 0x101,
"Kamegoro Shell": 0x102,
"Holograph Mega Man": 0x105,
"Giant Metall": 0x10C, # This is technically FC but we're +16 from the rom header
}
# addresses printed when assembling basepatch
wily_4_ptr: int = 0x7F570
consumables_ptr: int = 0x7FDEA
energylink_ptr: int = 0x7FDF9
class MM3ProcedurePatch(APProcedurePatch, APTokenMixin):
hash = [MM3LCHASH, MM3NESHASH, MM3VCHASH]
game = "Mega Man 3"
patch_file_ending = ".apmm3"
result_file_ending = ".nes"
name: bytearray
procedure = [
("apply_bsdiff4", ["mm3_basepatch.bsdiff4"]),
("apply_tokens", ["token_patch.bin"]),
]
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_bytes()
def write_byte(self, offset: int, value: int) -> None:
self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little"))
def write_bytes(self, offset: int, value: Iterable[int]) -> None:
self.write_token(APTokenTypes.WRITE, offset, bytes(value))
def patch_rom(world: "MM3World", patch: MM3ProcedurePatch) -> None:
patch.write_file("mm3_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm3_basepatch.bsdiff4")))
# text writing
base_address = 0x3C000
color_address = 0x31BC7
for i, offset, location in zip([0, 8, 1, 2,
3, 4, 5, 6,
7, 9],
[0x10, 0x50, 0x91, 0xD2,
0x113, 0x154, 0x195, 0x1D6,
0x217, 0x257],
[
names.get_needle_cannon,
names.get_rush_jet,
names.get_magnet_missile,
names.get_gemini_laser,
names.get_hard_knuckle,
names.get_top_spin,
names.get_search_snake,
names.get_spark_shock,
names.get_shadow_blade,
names.get_rush_marine,
]):
item = world.get_location(location).item
if item:
if len(item.name) <= 13:
# we want to just place it in the center
first_str = ""
second_str = item.name
third_str = ""
elif len(item.name) <= 26:
# spread across second and third
first_str = ""
second_str = item.name[:13]
third_str = item.name[13:]
else:
# all three
first_str = item.name[:13]
second_str = item.name[13:26]
third_str = item.name[26:]
if len(third_str) > 13:
third_str = third_str[:13]
player_str = world.multiworld.get_player_name(item.player)
if len(player_str) > 13:
player_str = player_str[:13]
y_coords = 0xA5
row = 0x21
if location in [names.get_rush_marine, names.get_rush_jet]:
y_coords = 0x45
row = 0x22
patch.write_bytes(base_address + offset, MM3TextEntry(first_str, y_coords, row).resolve())
patch.write_bytes(base_address + 16 + offset, MM3TextEntry(second_str, y_coords + 0x20, row).resolve())
patch.write_bytes(base_address + 32 + offset, MM3TextEntry(third_str, y_coords + 0x40, row).resolve())
if y_coords + 0x60 > 0xFF:
row += 1
y_coords = 0x01
patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords, row).resolve())
colors_high, colors_low = get_colors_for_item(item.name)
patch.write_bytes(color_address + (i * 8) + 1, colors_high)
patch.write_bytes(color_address + (i * 8) + 5, colors_low)
else:
patch.write_bytes(base_address + 48 + offset, MM3TextEntry(player_str, y_coords + 0x60, row).resolve())
write_palette_shuffle(world, patch)
enemy_weaknesses: dict[str, dict[int, int]] = {}
if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness:
# we need to write boss weaknesses
for boss in bosses:
if boss == "Kamegoro Maker":
enemy_weaknesses["Kamegoro"] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
enemy_weaknesses["Kamegoro Shell"] = {i: world.weapon_damage[i][bosses[boss]]
for i in world.weapon_damage}
elif boss == "Gemini Man":
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
enemy_weaknesses["Gemini Man (Clone)"] = {i: world.weapon_damage[i][bosses[boss]]
for i in world.weapon_damage}
elif boss == "Shadow Man":
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
enemy_weaknesses["Shadow Man (Sliding)"] = {i: world.weapon_damage[i][bosses[boss]]
for i in world.weapon_damage}
else:
enemy_weaknesses[boss] = {i: world.weapon_damage[i][bosses[boss]] for i in world.weapon_damage}
if world.options.enemy_weakness:
for enemy in enemy_addresses:
if enemy in [*bosses.keys(), "Kamegoro", "Kamegoro Shell", "Gemini Man (Clone)", "Shadow Man (Sliding)"]:
continue
enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
if enemy in ["Tama", "Giant Snakey", "Proto Man", "Giant Metall"] and enemy_weaknesses[enemy][0] <= 0:
enemy_weaknesses[enemy][0] = 1
elif enemy == "Jamacy 2":
# bruh
if not enemy_weaknesses[enemy][8] > 0:
enemy_weaknesses[enemy][8] = 1
if not enemy_weaknesses[enemy][3] > 0:
enemy_weaknesses[enemy][3] = 1
for enemy, damage in enemy_weaknesses.items():
for weapon in enemy_weakness_ptrs:
if damage[weapon] < 0:
damage[weapon] = 256 + damage[weapon]
patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage[weapon])
if world.options.consumables != Consumables.option_all:
value_a = 0x64
value_b = 0x6A
if world.options.consumables in (Consumables.option_none, Consumables.option_1up_etank):
value_a = 0x68
if world.options.consumables in (Consumables.option_none, Consumables.option_weapon_health):
value_b = 0x67
patch.write_byte(consumables_ptr - 3, value_a)
patch.write_byte(consumables_ptr + 1, value_b)
patch.write_byte(wily_4_ptr + 1, world.options.wily_4_requirement.value)
patch.write_byte(energylink_ptr + 1, world.options.energy_link.value)
if world.options.reduce_flashing:
# Spark Man
patch.write_byte(0x12649, 8)
patch.write_byte(0x1264E, 8)
patch.write_byte(0x12653, 8)
# Shadow Man
patch.write_byte(0x12658, 0x10)
# Gemini Man
patch.write_byte(0x12637, 0x20)
patch.write_byte(0x1263D, 0x20)
patch.write_byte(0x12643, 0x20)
# Gamma
patch.write_byte(0x7DA4A, 0xF)
if world.options.music_shuffle:
if world.options.music_shuffle.current_key == "no_music":
pool = [0xF0] * 18
elif world.options.music_shuffle.current_key == "randomized":
pool = world.random.choices(range(1, 0xC), k=18)
else:
pool = [1, 2, 3, 4, 5, 6, 7, 8, 1, 3, 7, 8, 9, 9, 10, 10, 11, 11]
world.random.shuffle(pool)
patch.write_bytes(0x7CD1C, pool)
from Utils import __version__
patch.name = bytearray(f'MM3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
'utf8')[:21]
patch.name.extend([0] * (21 - len(patch.name)))
patch.write_bytes(0x3F330, patch.name) # We changed this section, but this pointer is still valid!
deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
patch.write_byte(0x3F346, deathlink_byte)
patch.write_bytes(0x3F34C, world.world_version)
version_map = {
"0": 0x00,
"1": 0x01,
"2": 0x02,
"3": 0x03,
"4": 0x04,
"5": 0x05,
"6": 0x06,
"7": 0x07,
"8": 0x08,
"9": 0x09,
".": 0x26
}
patch.write_token(APTokenTypes.RLE, 0x653B, (11, 0x25))
patch.write_token(APTokenTypes.RLE, 0x6549, (25, 0x25))
# BY SILVRIS
patch.write_bytes(0x653B, [0x0B, 0x22, 0x25, 0x1C, 0x12, 0x15, 0x1F, 0x1B, 0x12, 0x1C])
# ARCHIPELAGO x.x.x
patch.write_bytes(0x654D,
[0x0A, 0x1B, 0x0C, 0x11, 0x12, 0x19, 0x0E, 0x15, 0x0A, 0x10, 0x18])
patch.write_bytes(0x6559, list(map(lambda c: version_map[c], __version__)))
patch.write_file("token_patch.bin", patch.get_token_binary())
header = b"\x4E\x45\x53\x1A\x10\x10\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00"
def read_headerless_nes_rom(rom: bytes) -> bytes:
if rom[:4] == b"NES\x1A":
return rom[16:]
else:
return rom
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes: bytes | None = 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 = read_headerless_nes_rom(bytes(open(file_name, "rb").read()))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() == PROTEUSHASH:
base_rom_bytes = extract_mm3(base_rom_bytes)
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() not in {MM3LCHASH, MM3NESHASH, MM3VCHASH}:
print(basemd5.hexdigest())
raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. "
"Get the correct game and version, then dump it")
headered_rom = bytearray(base_rom_bytes)
headered_rom[0:0] = header
setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom))
return bytes(headered_rom)
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
from . import MM3World
if not file_name:
file_name = MM3World.settings.rom_file
if not os.path.exists(file_name):
file_name = Utils.user_path(file_name)
return file_name
prg_offset = 0xCF1B0
prg_size = 0x40000
chr_offset = 0x10F1B0
chr_size = 0x20000
def extract_mm3(proteus: bytes) -> bytes:
mm3 = bytearray(proteus[prg_offset:prg_offset + prg_size])
mm3.extend(proteus[chr_offset:chr_offset + chr_size])
return bytes(mm3)

View File

@@ -1,388 +0,0 @@
from math import ceil
from typing import TYPE_CHECKING
from . import names
from .locations import get_boss_locations, get_oneup_locations, get_energy_locations
from worlds.generic.Rules import add_rule
if TYPE_CHECKING:
from . import MM3World
from BaseClasses import CollectionState
bosses: dict[str, int] = {
"Needle Man": 0,
"Magnet Man": 1,
"Gemini Man": 2,
"Hard Man": 3,
"Top Man": 4,
"Snake Man": 5,
"Spark Man": 6,
"Shadow Man": 7,
"Doc Robot (Metal)": 8,
"Doc Robot (Quick)": 9,
"Doc Robot (Air)": 10,
"Doc Robot (Crash)": 11,
"Doc Robot (Flash)": 12,
"Doc Robot (Bubble)": 13,
"Doc Robot (Wood)": 14,
"Doc Robot (Heat)": 15,
"Break Man": 16,
"Kamegoro Maker": 17,
"Yellow Devil MK-II": 18,
"Holograph Mega Man": 19,
"Wily Machine 3": 20,
"Gamma": 21
}
weapons_to_id: dict[str, int] = {
"Mega Buster": 0,
"Needle Cannon": 1,
"Magnet Missile": 2,
"Gemini Laser": 3,
"Hard Knuckle": 4,
"Top Spin": 5,
"Search Snake": 6,
"Spark Shot": 7,
"Shadow Blade": 8,
}
weapon_damage: dict[int, list[int]] = {
0: [1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 3, 1, 1, 1, 0, ], # Mega Buster
1: [4, 1, 1, 0, 2, 4, 2, 1, 0, 1, 1, 2, 4, 2, 4, 2, 0, 3, 1, 1, 1, 0, ], # Needle Cannon
2: [1, 4, 2, 4, 1, 0, 0, 1, 4, 2, 4, 1, 1, 0, 0, 1, 0, 3, 1, 0, 1, 0, ], # Magnet Missile
3: [7, 2, 4, 1, 0, 1, 1, 1, 1, 4, 2, 0, 4, 1, 1, 1, 0, 3, 1, 1, 1, 0, ], # Gemini Laser
4: [0, 2, 2, 4, 7, 2, 2, 2, 4, 1, 2, 7, 0, 2, 2, 2, 0, 1, 5, 4, 7, 4, ], # Hard Knuckle
5: [1, 1, 2, 0, 4, 2, 1, 7, 0, 1, 1, 4, 1, 1, 2, 7, 0, 1, 0, 7, 0, 2, ], # Top Spin
6: [1, 1, 5, 0, 1, 4, 0, 1, 0, 4, 1, 1, 1, 0, 4, 1, 0, 1, 0, 7, 4, 2, ], # Search Snake
7: [0, 7, 1, 0, 1, 1, 4, 1, 2, 1, 4, 1, 0, 4, 1, 1, 0, 0, 0, 0, 7, 0, ], # Spark Shot
8: [2, 7, 2, 0, 1, 2, 4, 4, 2, 2, 0, 1, 2, 4, 2, 4, 0, 1, 3, 2, 2, 2, ], # Shadow Blade
}
weapons_to_name: dict[int, str] = {
1: names.needle_cannon,
2: names.magnet_missile,
3: names.gemini_laser,
4: names.hard_knuckle,
5: names.top_spin,
6: names.search_snake,
7: names.spark_shock,
8: names.shadow_blade
}
minimum_weakness_requirement: dict[int, int] = {
0: 1, # Mega Buster is free
1: 1, # 112 shots of Needle Cannon
2: 2, # 14 shots of Magnet Missile
3: 2, # 14 shots of Gemini Laser
4: 2, # 14 uses of Hard Knuckle
5: 4, # an unknown amount of Top Spin (4 means you should be able to be fine)
6: 1, # 56 uses of Search Snake
7: 2, # 14 functional uses of Spark Shot (fires in twos)
8: 1, # 56 uses of Shadow Blade
}
robot_masters: dict[int, str] = {
0: "Needle Man Defeated",
1: "Magnet Man Defeated",
2: "Gemini Man Defeated",
3: "Hard Man Defeated",
4: "Top Man Defeated",
5: "Snake Man Defeated",
6: "Spark Man Defeated",
7: "Shadow Man Defeated"
}
weapon_costs = {
0: 0,
1: 0.25,
2: 2,
3: 2,
4: 2,
5: 7, # Not really, but we can really only rely on Top for one RBM
6: 0.5,
7: 2,
8: 0.5,
}
def can_defeat_enough_rbms(state: "CollectionState", player: int,
required: int, boss_requirements: dict[int, list[int]]) -> bool:
can_defeat = 0
for boss, reqs in boss_requirements.items():
if boss in robot_masters:
if state.has_all(map(lambda x: weapons_to_name[x], reqs), player):
can_defeat += 1
if can_defeat >= required:
return True
return False
def has_rush_vertical(state: "CollectionState", player: int) -> bool:
return state.has_any([names.rush_coil, names.rush_jet], player)
def can_traverse_long_water(state: "CollectionState", player: int) -> bool:
return state.has_any([names.rush_marine, names.rush_jet], player)
def has_any_rush(state: "CollectionState", player: int) -> bool:
return state.has_any([names.rush_coil, names.rush_jet, names.rush_marine], player)
def has_rush_jet(state: "CollectionState", player: int) -> bool:
return state.has(names.rush_jet, player)
def set_rules(world: "MM3World") -> None:
# most rules are set on region, so we only worry about rules required within stage access
# or rules variable on settings
if hasattr(world.multiworld, "re_gen_passthrough"):
slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 3"]
world.weapon_damage = slot_data["weapon_damage"]
else:
if world.options.random_weakness == world.options.random_weakness.option_shuffled:
weapon_tables = [table.copy() for weapon, table in weapon_damage.items() if weapon != 0]
world.random.shuffle(weapon_tables)
for i in range(1, 9):
world.weapon_damage[i] = weapon_tables.pop()
elif world.options.random_weakness == world.options.random_weakness.option_randomized:
world.weapon_damage = {i: [] for i in range(9)}
for boss in range(22):
for weapon in world.weapon_damage:
world.weapon_damage[weapon].append(min(14, max(0, int(world.random.normalvariate(3, 3)))))
if not any([world.weapon_damage[weapon][boss] >= 4
for weapon in range(1, 9)]):
# failsafe, there should be at least one defined non-Buster weakness
weapon = world.random.randint(1, 7)
world.weapon_damage[weapon][boss] = world.random.randint(4, 14) # Force weakness
# handle Break Man
boss = 16
for weapon in world.weapon_damage:
world.weapon_damage[weapon][boss] = 0
weapon = world.random.choice(list(world.weapon_damage.keys()))
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
if world.options.strict_weakness:
for weapon in weapon_damage:
for i in range(22):
if i == 16:
continue # Break is only weak to buster on non-random, and minimal damage on random
elif weapon == 0:
world.weapon_damage[weapon][i] = 0
elif i in (20, 21) and not world.options.random_weakness:
continue
# Gamma and Wily Machine need all weaknesses present, so allow
elif not world.options.random_weakness == world.options.random_weakness.option_randomized \
and i == 17:
if 3 > world.weapon_damage[weapon][i] > 0:
# Kamegoros take 3 max from weapons on non-random
world.weapon_damage[weapon][i] = 0
elif 4 > world.weapon_damage[weapon][i] > 0:
world.weapon_damage[weapon][i] = 0
for p_boss in world.options.plando_weakness:
for p_weapon in world.options.plando_weakness[p_boss]:
if not any(w for w in world.weapon_damage
if w != weapons_to_id[p_weapon]
and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]):
# we need to replace this weakness
weakness = world.random.choice([key for key in world.weapon_damage
if key != weapons_to_id[p_weapon]])
world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness]
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
= world.options.plando_weakness[p_boss][p_weapon]
# handle special cases
for boss in range(22):
for weapon in range(1, 9):
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon]
for i in range(1, 8) if i != weapon)):
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
world.weapon_damage[0][world.options.starting_robot_master.value] = 1
# weakness validation, it is better to confirm a completable seed than respect plando
boss_health = {boss: 0x1C for boss in range(8)}
weapon_energy = {key: float(0x1C) for key in weapon_costs}
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
for boss in range(8)}
flexibility = {
boss: (
sum(damage_value > 0 for damage_value in
weapon_damages.values()) # Amount of weapons that hit this boss
* sum(weapon_damages.values()) # Overall damage that those weapons do
)
for boss, weapon_damages in weapon_boss.items()
}
boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
used_weapons: dict[int, set[int]] = {i: set() for i in range(8)}
for boss in boss_flexibility:
boss_damage = weapon_boss[boss]
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
boss_damage.items() if weapon_energy[weapon] > 0}
while boss_health[boss] > 0:
if boss_damage[0] > 0:
boss_health[boss] = 0 # if we can buster, we should buster
continue
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
uses = weapon_energy[wp] // weapon_costs[wp]
if int(uses * boss_damage[wp]) >= boss_health[boss]:
used = ceil(boss_health[boss] / boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] = 0
used_weapons[boss].add(wp)
elif highest <= 0:
# we are out of weapons that can actually damage the boss
# so find the weapon that has the most uses, and apply that as an additional weakness
# it should be impossible to be out of energy
max_uses, wp = max((weapon_energy[weapon] // weapon_costs[weapon], weapon)
for weapon in weapon_weight
if weapon != 0)
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
used = min(int(weapon_energy[wp] // weapon_costs[wp]),
ceil(boss_health[boss] / minimum_weakness_requirement[wp]))
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
weapon_weight.pop(wp)
used_weapons[boss].add(wp)
else:
# drain the weapon and continue
boss_health[boss] -= int(uses * boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * uses
weapon_weight.pop(wp)
used_weapons[boss].add(wp)
world.wily_4_weapons = {boss: sorted(weapons) for boss, weapons in used_weapons.items()}
for i, boss_locations in zip(range(22), [
get_boss_locations("Needle Man Stage"),
get_boss_locations("Magnet Man Stage"),
get_boss_locations("Gemini Man Stage"),
get_boss_locations("Hard Man Stage"),
get_boss_locations("Top Man Stage"),
get_boss_locations("Snake Man Stage"),
get_boss_locations("Spark Man Stage"),
get_boss_locations("Shadow Man Stage"),
get_boss_locations("Doc Robot (Spark) - Metal"),
get_boss_locations("Doc Robot (Spark) - Quick"),
get_boss_locations("Doc Robot (Needle) - Air"),
get_boss_locations("Doc Robot (Needle) - Crash"),
get_boss_locations("Doc Robot (Gemini) - Flash"),
get_boss_locations("Doc Robot (Gemini) - Bubble"),
get_boss_locations("Doc Robot (Shadow) - Wood"),
get_boss_locations("Doc Robot (Shadow) - Heat"),
get_boss_locations("Break Man"),
get_boss_locations("Wily Stage 1"),
get_boss_locations("Wily Stage 2"),
get_boss_locations("Wily Stage 3"),
get_boss_locations("Wily Stage 5"),
get_boss_locations("Wily Stage 6")
]):
if world.weapon_damage[0][i] > 0:
continue # this can always be in logic
weapons = []
for weapon in range(1, 9):
if world.weapon_damage[weapon][i] > 0:
if world.weapon_damage[weapon][i] < minimum_weakness_requirement[weapon]:
continue
weapons.append(weapons_to_name[weapon])
if not weapons:
raise Exception(f"Attempted to have boss {i} with no weakness! Seed: {world.multiworld.seed}")
for location in boss_locations:
if i in (20, 21):
# multi-phase fights, get all potential weaknesses
# we should probably do this smarter, but this works for now
add_rule(world.get_location(location),
lambda state, weps=tuple(weapons): state.has_all(weps, world.player))
else:
add_rule(world.get_location(location),
lambda state, weps=tuple(weapons): state.has_any(weps, world.player))
# Need to defeat x amount of robot masters for Wily 4
add_rule(world.get_location(names.wily_stage_4),
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_4_requirement.value,
world.wily_4_weapons))
# Handle Doc Robo stage connections
for entrance, location in (("To Doc Robot (Needle) - Crash", names.doc_air),
("To Doc Robot (Gemini) - Bubble", names.doc_flash),
("To Doc Robot (Shadow) - Heat", names.doc_wood),
("To Doc Robot (Spark) - Quick", names.doc_metal)):
entrance_object = world.get_entrance(entrance)
add_rule(entrance_object, lambda state, loc=location: state.can_reach(loc, "Location", world.player))
# finally, real logic
for location in get_boss_locations("Hard Man Stage"):
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
for location in get_boss_locations("Gemini Man Stage"):
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
add_rule(world.get_entrance("To Doc Robot (Spark) - Metal"),
lambda state: has_rush_vertical(state, world.player) and
state.has_any([names.shadow_blade, names.gemini_laser], world.player))
add_rule(world.get_entrance("To Doc Robot (Needle) - Air"),
lambda state: has_rush_vertical(state, world.player))
add_rule(world.get_entrance("To Doc Robot (Needle) - Crash"),
lambda state: has_rush_jet(state, world.player))
add_rule(world.get_entrance("To Doc Robot (Gemini) - Bubble"),
lambda state: has_rush_vertical(state, world.player) and can_traverse_long_water(state, world.player))
for location in get_boss_locations("Wily Stage 1"):
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
for location in get_boss_locations("Wily Stage 2"):
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
# Wily 3 technically needs vertical
# However, Wily 3 requires beating Wily 2, and Wily 2 explicitly needs Jet
# So we can skip the additional rule on Wily 3
if world.options.consumables in (world.options.consumables.option_1up_etank,
world.options.consumables.option_all):
add_rule(world.get_location(names.needle_man_c2), lambda state: has_rush_jet(state, world.player))
add_rule(world.get_location(names.gemini_man_c1), lambda state: has_rush_jet(state, world.player))
add_rule(world.get_location(names.gemini_man_c3),
lambda state: has_rush_vertical(state, world.player)
or state.has_any([names.gemini_laser, names.shadow_blade], world.player))
for location in (names.gemini_man_c6, names.gemini_man_c7, names.gemini_man_c10):
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
for location in get_oneup_locations("Hard Man Stage"):
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
add_rule(world.get_location(names.top_man_c6), lambda state: has_rush_vertical(state, world.player))
add_rule(world.get_location(names.doc_needle_c2), lambda state: has_rush_jet(state, world.player))
add_rule(world.get_location(names.doc_needle_c3), lambda state: has_rush_jet(state, world.player))
add_rule(world.get_location(names.doc_gemini_c1), lambda state: has_rush_vertical(state, world.player))
add_rule(world.get_location(names.doc_gemini_c2), lambda state: has_rush_vertical(state, world.player))
add_rule(world.get_location(names.wily_1_c8), lambda state: has_rush_vertical(state, world.player))
for location in [names.wily_1_c4, names.wily_1_c8]:
add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player))
for location in get_oneup_locations("Wily Stage 2"):
if location == names.wily_2_c3:
continue
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))
if world.options.consumables in (world.options.consumables.option_weapon_health,
world.options.consumables.option_all):
add_rule(world.get_location(names.gemini_man_c2), lambda state: has_rush_vertical(state, world.player))
add_rule(world.get_location(names.gemini_man_c4), lambda state: has_rush_vertical(state, world.player))
add_rule(world.get_location(names.gemini_man_c5), lambda state: has_rush_vertical(state, world.player))
for location in (names.gemini_man_c8, names.gemini_man_c9):
add_rule(world.get_location(location), lambda state: has_any_rush(state, world.player))
for location in get_energy_locations("Hard Man Stage"):
if location == names.hard_man_c1:
continue
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
for location in (names.spark_man_c1, names.spark_man_c2):
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
for location in [names.top_man_c2, names.top_man_c3, names.top_man_c4, names.top_man_c7]:
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
for location in [names.wily_1_c5, names.wily_1_c6, names.wily_1_c7]:
add_rule(world.get_location(location), lambda state: state.has(names.hard_knuckle, world.player))
for location in [names.wily_1_c6, names.wily_1_c7, names.wily_1_c11, names.wily_1_c12]:
add_rule(world.get_location(location), lambda state: has_rush_vertical(state, world.player))
for location in get_energy_locations("Wily Stage 2"):
if location in (names.wily_2_c1, names.wily_2_c2, names.wily_2_c4):
continue
add_rule(world.get_location(location), lambda state: has_rush_jet(state, world.player))

View File

@@ -1,781 +0,0 @@
norom
!headersize = 16
!controller_flip = $14 ; only on first frame of input, used by crash man, etc
!controller_mirror = $16
!current_stage = $22
!current_state = $60
!completed_rbm_stages = $61
!completed_doc_stages = $62
!current_wily = $75
!received_rbm_stages = $680
!received_doc_stages = $681
; !deathlink = $30, set to $0E
!energylink_packet = $682
!last_wily = $683
!rbm_strobe = $684
!sound_effect_strobe = $685
!doc_robo_kills = $686
!wily_stage_completion = $687
;!received_items = $688
!acquired_rush = $689
!current_weapon = $A0
!current_health = $A2
!received_weapons = $A3
'0' = $00
'1' = $01
'2' = $02
'3' = $03
'4' = $04
'5' = $05
'6' = $06
'7' = $07
'8' = $08
'9' = $09
'A' = $0A
'B' = $0B
'C' = $0C
'D' = $0D
'E' = $0E
'F' = $0F
'G' = $10
'H' = $11
'I' = $12
'J' = $13
'K' = $14
'L' = $15
'M' = $16
'N' = $17
'O' = $18
'P' = $19
'Q' = $1A
'R' = $1B
'S' = $1C
'T' = $1D
'U' = $1E
'V' = $1F
'W' = $20
'X' = $21
'Y' = $22
'Z' = $23
' ' = $25
'.' = $26
',' = $27
'!' = $29
'r' = $2A
':' = $2B
; !consumable_checks = $0F80 ; have to find in-stage solutions for this, there's literally not enough ram
!CONTROLLER_SELECT = #$20
!CONTROLLER_SELECT_START = #$30
!CONTROLLER_ALL_BUTTON = #$F0
!PpuControl_2000 = $2000
!PpuMask_2001 = $2001
!PpuAddr_2006 = $2006
!PpuData_2007 = $2007
;!LOAD_BANK = $C000
macro org(address,bank)
if <bank> == $3E
org <address>-$C000+($2000*<bank>)+!headersize ; org sets the position in the output file to write to (in norom, at least)
base <address> ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
else
if <bank> == $3F
org <address>-$E000+($2000*<bank>)+!headersize ; org sets the position in the output file to write to (in norom, at least)
base <address> ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
else
if <address> >= $A000
org <address>-$A000+($2000*<bank>)+!headersize
base <address>
else
org <address>-$8000+($2000*<bank>)+!headersize
base <address>
endif
endif
endif
endmacro
; capcom.....
; i can't keep defending you like this
;P
%org($BEBA, $13)
RemoveP:
db $25
;A
%org($BD7D, $13)
RemoveA:
db $25
;S
%org($BE7D, $13)
RemoveS1:
db $25
;S
%org($BDD5, $13)
RemoveS2:
db $25
;W
%org($BDC7, $13)
RemoveW:
db $25
;O
%org($BEC7, $13)
RemoveO:
db $25
;R
%org($BDCF, $13)
RemoveR:
db $25
;D
%org($BECF, $13)
RemoveD:
db $25
%org($A17C, $02)
AdjustWeaponRefill:
; compare vs unreceived instead. Since the stage ends anyways, this just means you aren't granted the weapon if you don't have it already
CMP #$1C
BCS WeaponRefillJump
%org($A18B, $02)
WeaponRefillJump:
; just as a branch target
%org($A3BF, $02)
FixPseudoSnake:
JMP CheckFirstWep
NOP
%org($A3CB, $02)
FixPseudoRush:
JMP CheckRushWeapon
NOP
%org($BF80, $02)
CheckRushWeapon:
AND #$01
BNE .Rush
JMP $A3CF
.Rush:
LDA $A1
CLC
ADC $B4
TAY
LDA $00A2, Y
BNE .Skip
DEC $A1
.Skip:
JMP $A477
; don't even try to go past this point
%org($802F, $0B)
HookBreakMan:
JSR SetBreakMan
NOP
%org($90BC, $18)
BlockPassword:
AND #$08 ; originally 0C, just block down inputs
%org($9258, $18)
HookStageSelect:
JSR ChangeStageMode
NOP
%org($92F2, $18)
AccessStageTarget:
%org($9316, $18)
AccessStage:
JSR RewireDocRobotAccess
NOP #2
BEQ AccessStageTarget
%org($9468, $18)
HookWeaponGet:
JSR WeaponReceived
NOP #4
%org($9917, $18)
GameOverStageSelect:
; fix it returning to Wily 1
CMP #$16
%org($9966, $18)
SwapSelectTiles:
; swaps when stage select face tiles should be shown
JMP InvertSelectTiles
NOP
%org($9A54, $18)
SwapSelectSprites:
JMP InvertSelectSprites
NOP
%org($9AFF, $18)
BreakManSelect:
JSR ApplyLastWily
NOP
%org($BE22, $1D)
ConsumableHook:
JMP CheckConsumable
%org($BE32, $1D)
EnergyLinkHook:
JSR EnergyLink
%org($A000, $1E)
db $21, $A5, $0C, "PLACEHOLDER 1"
db $21, $C5, $0C, "PLACEHOLDER 2"
db $21, $E5, $0C, "PLACEHOLDER 3"
db $22, $05, $0C, "PLACEHOLDER P"
db $22, $45, $0C, "PLACEHOLDER 1"
db $22, $65, $0C, "PLACEHOLDER 2"
db $22, $85, $0C, "PLACEHOLDER 3"
db $22, $A5, $0C, "PLACEHOLDER P", $FF
db $21, $A5, $0C, "PLACEHOLDER 1"
db $21, $C5, $0C, "PLACEHOLDER 2"
db $21, $E5, $0C, "PLACEHOLDER 3"
db $22, $05, $0C, "PLACEHOLDER P", $FF
db $21, $A5, $0C, "PLACEHOLDER 1"
db $21, $C5, $0C, "PLACEHOLDER 2"
db $21, $E5, $0C, "PLACEHOLDER 3"
db $22, $05, $0C, "PLACEHOLDER P", $FF
db $21, $A5, $0C, "PLACEHOLDER 1"
db $21, $C5, $0C, "PLACEHOLDER 2"
db $21, $E5, $0C, "PLACEHOLDER 3"
db $22, $05, $0C, "PLACEHOLDER P", $FF
db $21, $A5, $0C, "PLACEHOLDER 1"
db $21, $C5, $0C, "PLACEHOLDER 2"
db $21, $E5, $0C, "PLACEHOLDER 3"
db $22, $05, $0C, "PLACEHOLDER P", $FF
db $21, $A5, $0C, "PLACEHOLDER 1"
db $21, $C5, $0C, "PLACEHOLDER 2"
db $21, $E5, $0C, "PLACEHOLDER 3"
db $22, $05, $0C, "PLACEHOLDER P", $FF
db $21, $A5, $0C, "PLACEHOLDER 1"
db $21, $C5, $0C, "PLACEHOLDER 2"
db $21, $E5, $0C, "PLACEHOLDER 3"
db $22, $05, $0C, "PLACEHOLDER P", $FF
db $21, $A5, $0C, "PLACEHOLDER 1"
db $21, $C5, $0C, "PLACEHOLDER 2"
db $21, $E5, $0C, "PLACEHOLDER 3"
db $22, $05, $0C, "PLACEHOLDER P"
db $22, $45, $0C, "PLACEHOLDER 1"
db $22, $65, $0C, "PLACEHOLDER 2"
db $22, $85, $0C, "PLACEHOLDER 3"
db $22, $A5, $0C, "PLACEHOLDER P", $FF
ShowItemString:
STY $04
LDA ItemLower,X
STA $02
LDA ItemUpper,X
STA $03
LDY #$00
.LoadString:
LDA ($02),Y
ORA $10
STA $0780,Y
BMI .Return
INY
LDA ($02),Y
STA $0780,Y
INY
LDA ($02),Y
STA $0780,Y
STA $00
INY
.LoadCharacters:
LDA ($02),Y
STA $0780,Y
INY
DEC $00
BPL .LoadCharacters
BMI .LoadString
.Return:
STA $19
LDY $04
RTS
ItemUpper:
db $A0, $A0, $A0, $A1, $A1, $A1, $A1, $A2, $A2
ItemLower:
db $00, $81, $C2, $03, $44, $85, $C6, $07, $47
%org($C8F7, $3E)
RemoveRushCoil:
NOP #4
%org($CA73, $3E)
HookController:
JMP ControllerHook
NOP
%org($DA18, $3E)
NullWeaponGet:
NOP #5 ; TODO: see if I can reroute this write instead for nicer timings
%org($DB99, $3E)
HookMidDoc:
JSR SetMidDoc
NOP
%org($DBB0, $3E)
HoodEndDoc:
JSR SetEndDoc
NOP
%org($DC57, $3E)
RerouteStageComplete:
LDA $60
JSR SetStageComplete
NOP #2
%org($DC6F, $3E)
RerouteRushMarine:
JMP SetRushMarine
NOP
%org($DC6A, $3E)
RerouteRushJet:
JMP SetRushJet
NOP
%org($DC78, $3E)
RerouteWilyComplete:
JMP SetEndWily
NOP
EndWilyReturn:
%org($DF81, $3E)
NullBreak:
NOP #5 ; nop break man giving every weapon
%org($E15F, $3F)
Wily4:
JMP Wily4Comparison
NOP
%org($F340, $3F)
RewireDocRobotAccess:
LDA !current_state
BNE .DocRobo
LDA !received_rbm_stages
SEC
BCS .Return
.DocRobo:
LDA !received_doc_stages
.Return:
AND $9DED,Y
RTS
ChangeStageMode:
; also handles hot reload of stage select
; kinda broken, sprites don't disappear and palettes go wonky with Break Man access
; but like, it functions!
LDA !sound_effect_strobe
BEQ .Continue
JSR $F89A
LDA #$00
STA !sound_effect_strobe
.Continue:
LDA $14
AND #$20
BEQ .Next
LDA !current_state
BNE .Set
LDA !completed_doc_stages
CMP #$C5
BEQ .BreakMan
LDA #$09
SEC
BCS .Set
.EarlyReturn:
LDA $14
AND #$90
RTS
.BreakMan:
LDA #$12
.Set:
EOR !current_state
STA !current_state
LDA #$01
STA !rbm_strobe
.Next:
LDA !rbm_strobe
BEQ .EarlyReturn
LDA #$00
STA !rbm_strobe
; Clear the sprite buffer
LDX #$98
.Loop:
LDA #$00
STA $01FF, X
DEX
STA $01FF, X
DEX
STA $01FF, X
DEX
LDA #$F8
STA $01FF, X
DEX
CPX #$00
BNE .Loop
; Break Man Sprites
LDX #$24
.Loop2:
LDA #$00
STA $02DB, X
DEX
STA $02DB, X
DEX
STA $02DB, X
DEX
LDA #$F8
STA $02DB, X
DEX
CPX #$00
BNE .Loop2
; Swap out the tilemap and write sprites
LDY #$10
LDA $11
BMI .B1
LDA $FD
EOR #$01
ASL A
ASL A
STA $10
LDA #$01
JSR $E8B4
LDA #$00
STA $70
STA $EE
.B3:
LDA $10
PHA
JSR $EF8C
PLA
STA $10
JSR $FF21
LDA $70
BNE .B3
JSR $995C
LDX #$03
JSR $939E
JSR $FF21
LDX #$04
JSR $939E
LDA $FD
EOR #$01
STA $FD
LDY #$00
LDA #$7E
STA $E9
JSR $FF3C
.B1:
LDX #$00
; palettes
.B2:
LDA $9C33,Y
STA $0600,X
LDA $9C23,Y
STA $0610,X
INY
INX
CPX #$10
BNE .B2
LDA #$FF
STA $18
LDA #$01
STA $12
LDA #$03
STA $13
LDA $11
JSR $99FA
LDA $14
AND #$90
RTS
InvertSelectTiles:
LDY !current_state
BNE .DocRobo
AND !received_rbm_stages
SEC
BCS .Compare
.DocRobo:
AND !received_doc_stages
.Compare:
BNE .False
JMP $996A
.False:
JMP $99BA
InvertSelectSprites:
LDY !current_state
BNE .DocRobo
AND !received_rbm_stages
SEC
BCS .Compare
.DocRobo:
AND !received_doc_stages
.Compare:
BNE .False
JMP $9A58
.False:
JMP $9A6D
SetStageComplete:
CMP #$00
BNE .DocRobo
LDA !completed_rbm_stages
ORA $DEC2, Y
STA !completed_rbm_stages
SEC
BCS .Return
.DocRobo:
LDA !completed_doc_stages
ORA $DEC2, Y
STA !completed_doc_stages
.Return:
RTS
ControllerHook:
; Jump in here too for sfx
LDA !sound_effect_strobe
BEQ .Next
JSR $F89A
LDA #$00
STA !sound_effect_strobe
.Next:
LDA !controller_mirror
CMP !CONTROLLER_ALL_BUTTON
BNE .Continue
JMP $CBB1
.Continue:
LDA !controller_flip
AND #$10 ; start
JMP $CA77
SetRushMarine:
LDA #$01
SEC
BCS SetRushAcquire
SetRushJet:
LDA #$02
SEC
BCS SetRushAcquire
SetRushAcquire:
ORA !acquired_rush
STA !acquired_rush
RTS
ApplyLastWily:
LDA !controller_mirror
AND !CONTROLLER_SELECT
BEQ .LastWily
.Default:
LDA #$00
SEC
BCS .Set
.LastWily:
LDA !last_wily
BEQ .Default
SEC
SBC #$0C
.Set:
STA $75 ; wily index
LDA #$03
STA !current_stage
RTS
SetMidDoc:
LDA !current_stage
SEC
SBC #$08
ASL
TAY
LDA #$01
.Loop:
CPY #$00
BEQ .Return
DEY
ASL
SEC
BCS .Loop
.Return:
ORA !doc_robo_kills
STA !doc_robo_kills
LDA #$00
STA $30
RTS
SetEndDoc:
LDA !current_stage
SEC
SBC #$08
ASL
TAY
INY
LDA #$01
.Loop:
CPY #$00
BEQ .Set
DEY
ASL
SEC
BCS .Loop
.Set:
ORA !doc_robo_kills
STA !doc_robo_kills
.Return:
LDA #$0D
STA $30
RTS
SetEndWily:
LDA !current_wily
PHA
CLC
ADC #$0C
STA !last_wily
PLA
TAX
LDA #$01
.WLoop:
CPX #$00
BEQ .WContinue
DEX
ASL A
SEC
BCS .WLoop
.WContinue:
ORA !wily_stage_completion
STA !wily_stage_completion
INC !current_wily
LDA #$9C
JMP EndWilyReturn
SetBreakMan:
LDA #$80
ORA !wily_stage_completion
STA !wily_stage_completion
LDA #$16
STA $22
RTS
CheckFirstWep:
LDA $B4
BEQ .SetNone
TAY
.Loop:
LDA $00A2,Y
BMI .SetNew
INY
CPY #$0C
BEQ .SetSame
BCC .Loop
.SetSame:
LDA #$80
STA $A1
JMP $A3A1
.SetNew:
TYA
SEC
SBC $B4
BCS .Set
.SetNone:
LDA #$00
.Set:
STA $A1
JMP $A3DE
Wily4Comparison:
TYA
PHA
TXA
PHA
LDY #$00
LDX #$08
LDA #$01
.Loop:
PHA
AND $6E
BEQ .Skip
INY
.Skip:
PLA
ASL
DEX
BNE .Loop
print "Wily 4 Requirement:", hex(realbase())
CPY #$08
BCC .Return
LDA #$FF
STA $6E
.Return:
PLA
TAX
PLA
TAY
LDA #$0C
STA $EC
RTS
; out of space here :(
%org($FDBA, $3F)
WeaponReceived:
TAX
LDA $F5
PHA
LDA #$1E
STA $F5
JSR $FF6B
TXA
JSR ShowItemString
PLA
STA $F5
JSR $FF6B
RTS
CheckConsumable:
STA $0150, Y
LDA $0320, X
CMP #$64
BMI .Return
print "Consumables (replace 67): ", hex(realbase())
CMP #$6A
BPL .Return
LDA #$00
STA $0300, X
JMP $BE49
.Return:
JMP $BE25
EnergyLink:
print "Energylink: ", hex(realbase())
LDA #$01
BEQ .Return
TYA
STA !energylink_packet
LDA #$49
STA $00
.Return:
LDA $BDEC, Y
RTS
; out of room here :(

View File

@@ -1,8 +0,0 @@
import os
os.chdir(os.path.dirname(os.path.realpath(__file__)))
mm3 = bytearray(open("Mega Man 3 (USA).nes", 'rb').read())
mm3[0x3C010:0x3C010] = [0] * 0x40000
mm3[0x4] = 0x20 # have to do it here, because we don't this in the basepatch itself
open("mm3_basepatch.nes", 'wb').write(mm3)

View File

@@ -1,5 +0,0 @@
from test.bases import WorldTestBase
class MM3TestBase(WorldTestBase):
game = "Mega Man 3"

View File

@@ -1,105 +0,0 @@
from math import ceil
from .bases import MM3TestBase
from ..rules import minimum_weakness_requirement, bosses
# Need to figure out how this test should work
def validate_wily_4(base: MM3TestBase) -> None:
world = base.multiworld.worlds[base.player]
weapon_damage = world.weapon_damage
weapon_costs = {
0: 0,
1: 0.25,
2: 2,
3: 1,
4: 2,
5: 7, # Not really, but we can really only rely on Top for one RBM
6: 0.5,
7: 2,
8: 0.5,
}
boss_health = {boss: 0x1C for boss in range(8)}
weapon_energy = {key: float(0x1C) for key in weapon_costs}
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
for boss in range(8)}
flexibility = {
boss: (
sum(damage_value > 0 for damage_value in
weapon_damages.values()) # Amount of weapons that hit this boss
* sum(weapon_damages.values()) # Overall damage that those weapons do
)
for boss, weapon_damages in weapon_boss.items()
}
boss_flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
used_weapons: dict[int, set[int]] = {i: set() for i in range(8)}
for boss in boss_flexibility:
boss_damage = weapon_boss[boss]
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
boss_damage.items() if weapon_energy[weapon] > 0}
while boss_health[boss] > 0:
if boss_damage[0] > 0:
boss_health[boss] = 0 # if we can buster, we should buster
continue
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
uses = weapon_energy[wp] // weapon_costs[wp]
used_weapons[boss].add(wp)
if int(uses * boss_damage[wp]) > boss_health[boss]:
used = ceil(boss_health[boss] / boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * used
boss_health[boss] = 0
elif highest <= 0:
# we are out of weapons that can actually damage the boss
base.fail(f"Ran out of weapon energy to damage "
f"{next(name for name in bosses if bosses[name] == boss)}\n"
f"Seed: {base.multiworld.seed}\n"
f"Damage Table: {weapon_damage}")
else:
# drain the weapon and continue
boss_health[boss] -= int(uses * boss_damage[wp])
weapon_energy[wp] -= weapon_costs[wp] * uses
weapon_weight.pop(wp)
class WeaknessTests(MM3TestBase):
def test_that_every_boss_has_a_weakness(self) -> None:
world = self.multiworld.worlds[self.player]
weapon_damage = world.weapon_damage
for boss in range(22):
if not any(weapon_damage[weapon][boss] >= minimum_weakness_requirement[weapon] for weapon in range(9)):
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
def test_wily_4(self) -> None:
validate_wily_4(self)
class StrictWeaknessTests(WeaknessTests):
options = {
"strict_weakness": True,
}
class RandomWeaknessTests(WeaknessTests):
options = {
"random_weakness": "randomized"
}
class ShuffledWeaknessTests(WeaknessTests):
options = {
"random_weakness": "shuffled"
}
class RandomStrictWeaknessTests(WeaknessTests):
options = {
"strict_weakness": True,
"random_weakness": "randomized",
}
class ShuffledStrictWeaknessTests(WeaknessTests):
options = {
"strict_weakness": True,
"random_weakness": "shuffled"
}

View File

@@ -1,63 +0,0 @@
from collections import defaultdict
from typing import DefaultDict
MM3_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x25, {
'0': 0x00,
'1': 0x01,
'2': 0x02,
'3': 0x03,
'4': 0x04,
'5': 0x05,
'6': 0x06,
'7': 0x07,
'8': 0x08,
'9': 0x09,
'A': 0x0A,
'B': 0x0B,
'C': 0x0C,
'D': 0x0D,
'E': 0x0E,
'F': 0x0F,
'G': 0x10,
'H': 0x11,
'I': 0x12,
'J': 0x13,
'K': 0x14,
'L': 0x15,
'M': 0x16,
'N': 0x17,
'O': 0x18,
'P': 0x19,
'Q': 0x1A,
'R': 0x1B,
'S': 0x1C,
'T': 0x1D,
'U': 0x1E,
'V': 0x1F,
'W': 0x20,
'X': 0x21,
'Y': 0x22,
'Z': 0x23,
' ': 0x25,
'.': 0x26,
',': 0x27,
'\'': 0x28,
'!': 0x29,
':': 0x2B
})
class MM3TextEntry:
def __init__(self, text: str = "", y_coords: int = 0xA5, row: int = 0x21):
self.target_area: int = row # don't change
self.coords: int = y_coords # 0xYX, Y can only be increments of 0x20
self.text: str = text
def resolve(self) -> bytes:
data = bytearray()
data.append(self.target_area)
data.append(self.coords)
data.append(12)
data.extend([MM3_WEAPON_ENCODING[x] for x in self.text.upper()])
data.extend([0x25] * (13 - len(self.text)))
return bytes(data)

View File

@@ -28,7 +28,6 @@ class MuseDashCollections:
"Miku in Museland", # Paid DLC not included in Muse Plus
"Rin Len's Mirrorland", # Paid DLC not included in Muse Plus
"MSR Anthology_Vol.02", # Goes away January 26, 2026.
"MD-level Tactical Training Blu-ray", # Goes away December 27, 2025.
]
REMOVED_SONGS = [
@@ -39,7 +38,6 @@ class MuseDashCollections:
"Tsukuyomi Ni Naru Replaced",
"Heart Message feat. Aoi Tokimori Secret",
"Meow Rock feat. Chun Ge, Yuan Shen",
"Stra Stella Secret",
]
song_items = SONG_DATA

View File

@@ -625,7 +625,7 @@ SONG_DATA: Dict[str, SongData] = {
"Synthesis.": SongData(2900749, "83-1", "Cosmic Radio 2024", True, 6, 8, 10),
"COSMiC FANFARE!!!!": SongData(2900750, "83-2", "Cosmic Radio 2024", False, 7, 9, 11),
"Sharp Bubbles": SongData(2900751, "83-3", "Cosmic Radio 2024", True, 7, 9, 11),
"Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", False, 5, 7, 9),
"Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", True, 5, 7, 9),
"Cosmic Dusty Girl": SongData(2900753, "83-5", "Cosmic Radio 2024", True, 5, 7, 9),
"Meow Rock feat. Chun Ge, Yuan Shen": SongData(2900754, "84-0", "Muse Dash・Legend", True, None, None, None),
"Even if you make an old radio song with AI": SongData(2900755, "84-1", "Muse Dash・Legend", False, 3, 6, 8),
@@ -677,30 +677,4 @@ SONG_DATA: Dict[str, SongData] = {
"City Lights": SongData(2900801, "90-3", "MEDIUM5 Echoes", True, 4, 6, 9),
"Polaris Wandering Night": SongData(2900802, "90-4", "MEDIUM5 Echoes", True, 5, 8, 10),
"Chasing the Moonlight": SongData(2900803, "90-5", "MEDIUM5 Echoes", True, 4, 6, 8),
"WILDCARD": SongData(2900804, "91-0", "48 Hours After Discharge", True, 3, 6, 9),
"It was all just a dream!": SongData(2900805, "91-1", "48 Hours After Discharge", True, 5, 7, 9),
"Science": SongData(2900806, "91-2", "48 Hours After Discharge", False, 4, 7, 9),
"Hit Maker": SongData(2900807, "91-3", "48 Hours After Discharge", False, 4, 6, 9),
"THX 4 playing": SongData(2900808, "91-4", "48 Hours After Discharge", True, 3, 5, 8),
"Theory of Existence": SongData(2900809, "91-5", "48 Hours After Discharge", True, 4, 6, 9),
"Kirakira Noel Story!!": SongData(2900810, "43-68", "MD Plus Project", False, 6, 8, 10),
"Fantasista LAST END": SongData(2900811, "92-0", "HARDCORE MOTTO TANO*C", True, 7, 9, 11),
"Colorful Universe": SongData(2900812, "92-1", "HARDCORE MOTTO TANO*C", True, 3, 6, 9),
"Future Flux": SongData(2900813, "92-2", "HARDCORE MOTTO TANO*C", True, 5, 8, 10),
"SOMEONE STOP ME!!!": SongData(2900814, "92-3", "HARDCORE MOTTO TANO*C", True, 6, 8, 10),
"Azathoth": SongData(2900815, "92-4", "HARDCORE MOTTO TANO*C", True, 6, 8, 10),
"Change the Game feat. Iori Matsunaga": SongData(2900816, "92-5", "HARDCORE MOTTO TANO*C", False, 6, 8, 10),
"Stra Stella Secret": SongData(2900817, "0-59", "Default Music", False, 6, 8, 10),
"Stra Stella": SongData(2900818, "0-60", "Default Music", False, 1, 4, None),
"Ultra-Digital Super Detox": SongData(2900819, "43-69", "MD Plus Project", False, 3, 6, 9),
"Otsukimi Koete Otsukiai": SongData(2900820, "43-70", "MD Plus Project", True, 6, 8, 10),
"Obenkyou Time": SongData(2900821, "43-71", "MD Plus Project", False, 6, 8, 11),
"Retry Now": SongData(2900822, "43-72", "MD Plus Project", False, 3, 6, 9),
"Master Bancho's Sushi Class ": SongData(2900823, "93-0", "Welcome to the Blue Hole!", False, None, None, None),
"CHAOTiC BATTLE": SongData(2900824, "94-0", "Cosmic Radio 2025", False, 7, 9, 11),
"FATAL GAME": SongData(2900825, "94-1", "Cosmic Radio 2025", False, 3, 6, 9),
"Aria": SongData(2900826, "94-2", "Cosmic Radio 2025", False, 4, 6, 9),
"+1 UNKNOWN -NUMBER": SongData(2900827, "94-3", "Cosmic Radio 2025", True, 4, 7, 10),
"To the Beyond, from the Nameless Seaside": SongData(2900828, "94-4", "Cosmic Radio 2025", False, 5, 8, 10),
"REK421": SongData(2900829, "94-5", "Cosmic Radio 2025", True, 7, 9, 11),
}

View File

@@ -1,6 +1,6 @@
{
"game": "Muse Dash",
"authors": ["DeamonHunter"],
"world_version": "1.5.29",
"world_version": "1.5.26",
"minimum_ap_version": "0.6.3"
}

View File

@@ -272,7 +272,7 @@ def patch_rom(world, rom):
world_str = ""
rom.write_bytes(rom.sym('WORLD_STRING_TXT'), makebytes(world_str, 12))
time_str = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M") + " UTC"
time_str = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M") + " UTC"
rom.write_bytes(rom.sym('TIME_STRING_TXT'), makebytes(time_str, 25))
rom.write_byte(rom.sym('CFG_SHOW_SETTING_INFO'), 0x01)

View File

@@ -7,7 +7,7 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
## Benötigte Software
- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.10 und neuer werden unterstützt. Version 2.10 ist empfohlen.
- Version 2.3.1 und später werden unterstützt. Version 2.10 ist empfohlen.
- Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden.
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
den obrigen Link gefunden werden.
@@ -19,6 +19,11 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
Sobald Bizhawk einmal installiert wurde, öffne **EmuHawk** und ändere die folgenen Einsteluungen:
- (≤ 2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu
`"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäß funktionieren.
**ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und**
**wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die**
**Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.**
- Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann
den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal
abstürzen.

View File

@@ -7,7 +7,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
## Required Software
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.10 and later are supported. Version 2.10 is recommended for stability.
- Version 2.3.1 and later are supported. Version 2.10 is recommended for stability.
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
@@ -17,6 +17,11 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
Once BizHawk has been installed, open EmuHawk and change the following settings:
- (≤ 2.8) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
"Lua+LuaInterface". Then restart EmuHawk. This is required for the Lua script to function correctly.
**NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
**of newer versions of EmuHawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
**"NLua+KopiLua" until this step is done.**
- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
This reduces the possibility of losing save data in emulator crashes.
- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to

View File

@@ -7,7 +7,7 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
## Logiciel requis
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Les versions 2.10 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de stabilité.
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de stabilité.
- Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus.
- Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus.
- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
@@ -18,6 +18,10 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants :
- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" à
"Lua+LuaInterface". Puis redémarrez EmuHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement.
**REMARQUE : Même si « Lua+LuaInterface » est déjà sélectionné, basculez entre les deux options et resélectionnez-la. Nouvelles installations**
**des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions**
- Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s.
Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur.
- Sous Config > Personnaliser, cochez les cases « Exécuter en arrière-plan » et « Accepter la saisie en arrière-plan ». Cela vous permettra continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée.

View File

@@ -123,7 +123,6 @@ class PokemonEmeraldWorld(World):
blacklisted_wilds: Set[int]
blacklisted_starters: Set[int]
blacklisted_opponent_pokemon: Set[int]
allowed_dexsanity_species: set[int]
hm_requirements: Dict[str, Union[int, List[str]]]
auth: bytes
@@ -143,7 +142,6 @@ class PokemonEmeraldWorld(World):
self.blacklisted_wilds = set()
self.blacklisted_starters = set()
self.blacklisted_opponent_pokemon = set()
self.allowed_dexsanity_species = set()
self.modified_maps = copy.deepcopy(emerald_data.maps)
self.modified_species = copy.deepcopy(emerald_data.species)
self.modified_tmhm_moves = []
@@ -267,7 +265,6 @@ class PokemonEmeraldWorld(World):
from .regions import create_regions
all_regions = create_regions(self)
randomize_wild_encounters(self)
# Categories with progression items always included
categories = {
LocationCategory.BADGE,
@@ -497,6 +494,7 @@ class PokemonEmeraldWorld(World):
set_rules(self)
def connect_entrances(self):
randomize_wild_encounters(self)
self.shuffle_badges_hms()
# For entrance randomization, disconnect entrances here, randomize map, then
# undo badge/HM placement and re-shuffle them in the new map.

View File

@@ -110,7 +110,7 @@ def create_locations_by_category(world: "PokemonEmeraldWorld", regions: Dict[str
national_dex_id = int(location_name[-3:]) # Location names are formatted POKEDEX_REWARD_###
# Don't create this pokedex location if player can't find it in the wild
if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds or NATIONAL_ID_TO_SPECIES_ID[national_dex_id] not in world.allowed_dexsanity_species:
if NATIONAL_ID_TO_SPECIES_ID[national_dex_id] in world.blacklisted_wilds:
continue
location_id += POKEDEX_OFFSET + national_dex_id

View File

@@ -63,7 +63,7 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None:
if len(merged_blacklist) < NUM_REAL_SPECIES:
break
else:
merged_blacklist: Set[int] = set()
raise RuntimeError("This should never happen")
candidates = [
species

Some files were not shown because too many files have changed in this diff Show More