diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ebe42307dd3..772a6c0be359 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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.2.2 --allow-downgrade + choco install innosetup --version=6.7.0 --allow-downgrade - name: Build run: | python -m pip install --upgrade pip diff --git a/BaseClasses.py b/BaseClasses.py index d1b9b5f6d3f9..69b900212c50 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -727,6 +727,7 @@ 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]] = [] @@ -788,9 +789,11 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu self.multiworld.worlds[player].reached_region(self, new_region) # Retry connections if the new region can unblock them - for new_entrance in self.multiworld.indirect_connections.get(new_region, set()): - if new_entrance in blocked_connections and new_entrance not in queue: - queue.append(new_entrance) + entrances = self.multiworld.indirect_connections.get(new_region) + if entrances is not None: + relevant_entrances = entrances.intersection(blocked_connections) + relevant_entrances.difference_update(queue) + queue.extend(relevant_entrances) def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque[Entrance]): reachable_regions = self.reachable_regions[player] diff --git a/Fill.py b/Fill.py index 48ed7253d9d1..7bd575662708 100644 --- a/Fill.py +++ b/Fill.py @@ -280,6 +280,7 @@ def location_can_fill_item(location_to_fill: Location, item_to_fill: Item): 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, diff --git a/Main.py b/Main.py index 47a28813fce4..924def653b27 100644 --- a/Main.py +++ b/Main.py @@ -207,6 +207,9 @@ 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 diff --git a/MultiServer.py b/MultiServer.py index 52c80c55402a..ed14b6506ff5 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -21,6 +21,7 @@ import typing import weakref import zlib +from signal import SIGINT, SIGTERM, signal import ModuleUpdate @@ -496,7 +497,8 @@ def _load(self, decoded_obj: MultiData, game_data_packages: typing.Dict[str, typ self.read_data = {} # there might be a better place to put this. - self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0) + race_mode = decoded_obj.get("race_mode", 0) + self.read_data["race_mode"] = lambda: race_mode 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}, " @@ -1301,6 +1303,13 @@ def __new__(cls, name, bases, attrs): 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) @@ -2563,6 +2572,8 @@ 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() @@ -2729,6 +2740,26 @@ 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: diff --git a/OptionsCreator.py b/OptionsCreator.py index 4e56b680b879..94ca8ba7acc1 100644 --- a/OptionsCreator.py +++ b/OptionsCreator.py @@ -29,7 +29,7 @@ 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, Removed, +from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, OptionCounter, Visibility) @@ -318,26 +318,28 @@ def export_options(self, button: Widget) -> None: else: self.show_result_snack("Name cannot be longer than 16 characters.") - def create_range(self, option: typing.Type[Range], name: str): + def create_range(self, option: typing.Type[Range], name: str, bind=True): 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) - box.slider.bind(on_touch_move=lambda _, _1: update_text(box)) + if bind: + box.slider.bind(value=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): - 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 + 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: 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): @@ -346,7 +348,7 @@ def set_button_text(button: MDButton, text: str): 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(int(range_box.range.slider.value)) + range_box.range.tag.text = str(option.special_range_names[text.lower()]) set_button_text(range_box.choice, text) self.options[name] = text.lower() range_box.range.slider.dropdown.dismiss() @@ -355,13 +357,18 @@ def open_dropdown(button): # 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)) - if option.default in option.special_range_names: + 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: # value can get mismatched in this case - box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start), + box.range.slider.value = min(max(option.special_range_names[default], option.range_start), option.range_end) box.range.tag.text = str(int(box.range.slider.value)) - box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box)) + 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)) items = [ { "text": choice.title(), @@ -371,7 +378,7 @@ def open_dropdown(button): ] box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items) box.choice.bind(on_release=open_dropdown) - self.options[name] = option.default + self.options[name] = default return box def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str): @@ -447,8 +454,12 @@ def create_popup(self, option: typing.Type[OptionList] | typing.Type[OptionSet] 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): @@ -470,14 +481,6 @@ def apply_changes(button): 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)) @@ -491,6 +494,15 @@ def apply_changes(button): 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: diff --git a/README.md b/README.md index efa18bc1ef07..7a0c663db067 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ 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 diff --git a/UndertaleClient.py b/UndertaleClient.py index 1c522fac924d..b0efce206ae5 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -1,6 +1,7 @@ from __future__ import annotations import os import sys +import time import asyncio import typing import bsdiff4 @@ -15,6 +16,9 @@ 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): @@ -109,6 +113,11 @@ def __init__(self, server_address, password): 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: @@ -219,6 +228,9 @@ 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() @@ -263,6 +275,12 @@ 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"]: @@ -271,17 +289,19 @@ 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): - sync_msg = [{"cmd": "Sync"}] - if ctx.locations_checked: - sync_msg.append({"cmd": "LocationChecks", - "locations": list(ctx.locations_checked)}) - await ctx.send_msgs(sync_msg) + await ctx.check_locations(ctx.locations_checked) + await ctx.send_msgs([{"cmd": "Sync"}]) if start_index == len(ctx.items_received): counter = -1 placedWeapon = 0 @@ -368,9 +388,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): f.close() elif cmd == "Bounced": - tags = args.get("tags", []) - if "Online" in tags: - data = args.get("data", {}) + data = args.get("data", {}) + if "x" in data and "room" in 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: @@ -381,21 +400,63 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict): async def multi_watcher(ctx: UndertaleContext): while not ctx.exit_event.is_set(): - 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) + 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 await asyncio.sleep(0.1) @@ -409,10 +470,9 @@ async def game_watcher(ctx: UndertaleContext): for file in files: if ".item" in file: os.remove(os.path.join(root, file)) - sync_msg = [{"cmd": "Sync"}] - if ctx.locations_checked: - sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) - await ctx.send_msgs(sync_msg) + await ctx.check_locations(ctx.locations_checked) + await ctx.send_msgs([{"cmd": "Sync"}]) + ctx.syncing = False if ctx.got_deathlink: ctx.got_deathlink = False @@ -447,7 +507,7 @@ async def game_watcher(ctx: UndertaleContext): for l in lines: sending = sending+[(int(l.rstrip('\n')))+12000] finally: - await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}]) + await ctx.check_locations(sending) if "victory" in file and str(ctx.route) in file: victory = True if ".playerspot" in file and "Online" not in ctx.tags: diff --git a/Utils.py b/Utils.py index bf46d0832d27..627235f24925 100644 --- a/Utils.py +++ b/Utils.py @@ -18,11 +18,14 @@ 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 @@ -315,6 +318,7 @@ 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() @@ -1003,6 +1007,7 @@ 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) @@ -1067,6 +1072,7 @@ def _noop() -> 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 @@ -1287,6 +1293,15 @@ 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. diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index f856eea4c538..d10c17bff8ad 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -11,6 +11,7 @@ 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') @@ -64,6 +65,7 @@ cache = Cache() Compress(app) +CLI(app) def to_python(value: str) -> uuid.UUID: diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 54eb5c1de151..63914a06baef 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -2,10 +2,20 @@ 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]]: diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 96ffbe9e9540..b48c6a8cbbe1 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -4,14 +4,14 @@ import logging import multiprocessing import typing -from datetime import timedelta, datetime +from datetime import timedelta from threading import Event, Thread from typing import Any from uuid import UUID from pony.orm import db_session, select, commit, PrimaryKey -from Utils import restricted_loads +from Utils import restricted_loads, utcnow from .locker import Locker, AlreadyRunningException _stop_event = Event() @@ -129,10 +129,10 @@ def keep_running(): with db_session: rooms = select( room for room in Room if - room.last_activity >= datetime.utcnow() - timedelta(days=3)) + room.last_activity >= 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 >= datetime.utcnow() - timedelta(seconds=room.timeout + 5): + if room.last_activity >= utcnow() - timedelta(seconds=room.timeout + 5): hosters[room.id.int % len(hosters)].start_room(room.id) except AlreadyRunningException: diff --git a/WebHostLib/cli/__init__.py b/WebHostLib/cli/__init__.py new file mode 100644 index 000000000000..a210e1475c1b --- /dev/null +++ b/WebHostLib/cli/__init__.py @@ -0,0 +1,8 @@ +from flask import Flask + + +class CLI: + def __init__(self, app: Flask) -> None: + from .stats import stats_cli + + app.cli.add_command(stats_cli) diff --git a/WebHostLib/cli/stats.py b/WebHostLib/cli/stats.py new file mode 100644 index 000000000000..85edfb4348ec --- /dev/null +++ b/WebHostLib/cli/stats.py @@ -0,0 +1,36 @@ +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}") diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 7248bf3bacc6..4257c6aff3e4 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -89,19 +89,24 @@ def _load_game_data(self): setattr(self, key, value) self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names) - def listen_to_db_commands(self): + async def listen_to_db_commands(self): cmdprocessor = DBCommandProcessor(self) while not self.exit_event.is_set(): - with db_session: - commands = select(command for command in Command if command.room.id == self.room_id) - if commands: - for command in commands: - self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext) - command.delete() - commit() - del commands - time.sleep(5) + await self.main_loop.run_in_executor(None, self._process_db_commands, cmdprocessor) + try: + await asyncio.wait_for(self.exit_event.wait(), 5) + except asyncio.TimeoutError: + pass + + def _process_db_commands(self, cmdprocessor): + with db_session: + commands = select(command for command in Command if command.room.id == self.room_id) + if commands: + for command in commands: + self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext) + command.delete() + commit() @db_session def load(self, room_id: int): @@ -156,9 +161,9 @@ def init_save(self, enabled: bool = True): with db_session: savegame_data = Room.get(id=self.room_id).multisave if savegame_data: - self.set_save(restricted_loads(Room.get(id=self.room_id).multisave)) + self.set_save(restricted_loads(savegame_data)) self._start_async_saving(atexit_save=False) - threading.Thread(target=self.listen_to_db_commands, daemon=True).start() + asyncio.create_task(self.listen_to_db_commands()) @db_session def _save(self, exit_save: bool = False) -> bool: @@ -167,7 +172,7 @@ def _save(self, exit_save: bool = False) -> bool: 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 = datetime.datetime.utcnow() + room.last_activity = Utils.utcnow() return True def get_save(self) -> dict: @@ -229,6 +234,17 @@ def set_up_logging(room_id) -> logging.Logger: return logger +def tear_down_logging(room_id): + """Close logging handling for a room.""" + logger_name = f"RoomLogger {room_id}" + if logger_name in logging.Logger.manager.loggerDict: + logger = logging.getLogger(logger_name) + for handler in logger.handlers[:]: + logger.removeHandler(handler) + handler.close() + del logging.Logger.manager.loggerDict[logger_name] + + def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, cert_file: typing.Optional[str], cert_key_file: typing.Optional[str], host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue): @@ -343,12 +359,17 @@ async def start_room(room_id): ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup ctx.exit_event.set() # make sure the saving thread stops at some point # NOTE: async saving should probably be an async task and could be merged with shutdown_task + + if ctx.server and hasattr(ctx.server, "ws_server"): + ctx.server.ws_server.close() + await ctx.server.ws_server.wait_closed() + 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 = datetime.datetime.utcnow() - \ - datetime.timedelta(minutes=1, seconds=room.timeout) + room.last_activity = Utils.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}.") finally: await asyncio.sleep(5) diff --git a/WebHostLib/landing.py b/WebHostLib/landing.py index 14e90cc28df4..f1b8de21bfbf 100644 --- a/WebHostLib/landing.py +++ b/WebHostLib/landing.py @@ -1,8 +1,9 @@ -from datetime import timedelta, datetime +from datetime import timedelta from flask import render_template from pony.orm import count +from Utils import utcnow from WebHostLib import app, cache from .models import Room, Seed @@ -10,6 +11,6 @@ @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 >= datetime.utcnow() - timedelta(days=7)) - seeds = count(seed for seed in Seed if seed.creation_time >= datetime.utcnow() - timedelta(days=7)) + 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)) return render_template("landing.html", rooms=rooms, seeds=seeds) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index e30f1a6dd413..8d04fe984eb5 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -9,11 +9,12 @@ 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 +from Utils import title_sorted, utcnow class WebWorldTheme(StrEnum): DIRT = "dirt" @@ -233,11 +234,12 @@ def host_room(room: UUID): if room is None: return abort(404) - now = datetime.datetime.utcnow() + now = 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" diff --git a/WebHostLib/models.py b/WebHostLib/models.py index 7fa54f26a004..9060bc0ca4c5 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -2,6 +2,8 @@ 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 @@ -20,8 +22,8 @@ class Slot(db.Entity): class Room(db.Entity): id = PrimaryKey(UUID, default=uuid4) - 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 + 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 owner = Required(UUID, index=True) commands = Set('Command') seed = Required('Seed', index=True) @@ -38,7 +40,7 @@ class Seed(db.Entity): rooms = Set(Room) multidata = Required(bytes, lazy=True) owner = Required(UUID, index=True) - creation_time = Required(datetime, default=lambda: datetime.utcnow(), index=True) # index used by landing page + creation_time: datetime = Required(datetime, default=lambda: 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 diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index c4267dc2846b..c9a923680add 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -6,6 +6,7 @@ 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 diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index d1471aa6580a..cb40c8293f2d 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -10,7 +10,7 @@ from MultiServer import Context, get_saving_second from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType -from Utils import restricted_loads, KeyedDefaultDict +from Utils import restricted_loads, KeyedDefaultDict, utcnow from . import app, cache from .models import GameDataPackage, Room @@ -273,9 +273,10 @@ def get_room_last_activity(self) -> Dict[TeamPlayer, datetime.timedelta]: Does not include players who have no activity recorded. """ last_activity: Dict[TeamPlayer, datetime.timedelta] = {} - now = datetime.datetime.utcnow() + now = utcnow() for (team, player), timestamp in self._multisave.get("client_activity_timers", []): - last_activity[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) + from_timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).replace(tzinfo=None) + last_activity[team, player] = now - from_timestamp return last_activity diff --git a/deploy/example_nginx.conf b/deploy/example_nginx.conf index b0c0e8e5a043..d44d1ffbd1ef 100644 --- a/deploy/example_nginx.conf +++ b/deploy/example_nginx.conf @@ -41,16 +41,8 @@ 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; @@ -60,5 +52,15 @@ 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; + } } } diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 0e368386c54f..46afd3045692 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -134,6 +134,9 @@ # Mega Man 2 /worlds/mm2/ @Silvris +# Mega Man 3 +/worlds/mm3/ @Silvris + # MegaMan Battle Network 3 /worlds/mmbn3/ @digiholic diff --git a/docs/adding games.md b/docs/adding games.md index 9149e9f6260b..a977109bde9f 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -87,7 +87,8 @@ 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/`. +repository and creating a new world package in `/worlds/` (see [running from source](/docs/running%20from%20source.md) +for setup). 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 diff --git a/docs/apworld specification.md b/docs/apworld specification.md index 591ce51cae1c..7e13d6ccd34d 100644 --- a/docs/apworld specification.md +++ b/docs/apworld specification.md @@ -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, 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 (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. 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`. diff --git a/docs/world api.md b/docs/world api.md index 48e863fb2616..4e2354930445 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -491,9 +491,10 @@ 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 could be generated from json or something else. 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 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. item_name_to_id = {name: id for id, name in enumerate(mygame_items, base_id)} location_name_to_id = {name: id for @@ -770,6 +771,7 @@ 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. diff --git a/entrance_rando.py b/entrance_rando.py index a417767036ee..47664cb685bf 100644 --- a/entrance_rando.py +++ b/entrance_rando.py @@ -186,9 +186,20 @@ def __init__(self, world: World, entrance_lookup: EntranceLookup, coupled: bool) 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] @@ -226,7 +237,7 @@ def test_speculative_connection(self, source_exit: Entrance, target_entrance: En 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() + copied_state.sweep_for_advancements(self.world.get_locations()) # 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: @@ -402,7 +413,7 @@ def do_placement(source_exit: Entrance, target_entrance: Entrance) -> None: 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() + er_state.collection_state.sweep_for_advancements(world.get_locations()) if on_connect: change = on_connect(er_state, placed_exits, paired_entrances) if change: diff --git a/inno_setup.iss b/inno_setup.iss index c396224c5631..999070ad07a0 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -213,6 +213,11 @@ 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: ""; diff --git a/test/bases.py b/test/bases.py index dd93ca6452dd..19b19bea67ec 100644 --- a/test/bases.py +++ b/test/bases.py @@ -248,6 +248,7 @@ def fulfills_accessibility() -> bool: 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), diff --git a/test/general/test_ids.py b/test/general/test_ids.py index ad8aad11d15c..08b4d0aa494d 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -88,6 +88,7 @@ def test_postgen_datapackage(self): 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, diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index de432e369099..add6e5321e7f 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -46,6 +46,8 @@ def test_slot_data(self): 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 @@ -93,6 +95,7 @@ def test_explicit_indirect_conditions_spheres(self): 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. diff --git a/test/general/test_items.py b/test/general/test_items.py index 694e0db406ca..9c300cf94ed6 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -123,6 +123,7 @@ def setup_link_multiworld(world: Type[World], link_replace: bool) -> None: 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(): diff --git a/test/general/test_options.py b/test/general/test_options.py index e610e36794e8..6b08c8e9b048 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -1,7 +1,7 @@ import unittest from BaseClasses import PlandoOptions -from Options import Choice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts +from Options import Choice, TextChoice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts from Utils import restricted_dumps from worlds.AutoWorld import AutoWorldRegister @@ -16,6 +16,29 @@ def test_options_have_doc_string(self): 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(): diff --git a/test/hosting/webhost.py b/test/hosting/webhost.py index a8e70a50c20c..025044555129 100644 --- a/test/hosting/webhost.py +++ b/test/hosting/webhost.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Iterable, Optional, cast +from Utils import utcnow from WebHostLib import to_python if TYPE_CHECKING: @@ -133,7 +134,7 @@ def stop_room(app_client: "FlaskClient", room_id: str, timeout: Optional[float] = None, simulate_idle: bool = True) -> None: - from datetime import datetime, timedelta + from datetime import timedelta from time import sleep from pony.orm import db_session @@ -151,10 +152,11 @@ def stop_room(app_client: "FlaskClient", with db_session: room: Room = Room.get(id=room_uuid) + now = utcnow() if simulate_idle: - new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5) + new_last_activity = now - timedelta(seconds=room.timeout + 5) else: - new_last_activity = datetime.utcnow() - timedelta(days=3) + new_last_activity = now - timedelta(days=3) room.last_activity = new_last_activity address = f"localhost:{room.last_port}" if room.last_port > 0 else None if address: @@ -188,6 +190,7 @@ 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") diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py index 203af8b63a8b..d22013b4e078 100644 --- a/test/multiworld/test_multiworlds.py +++ b/test/multiworld/test_multiworlds.py @@ -61,6 +61,7 @@ def test_fills(self) -> None: 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") @@ -78,4 +79,5 @@ def test_two_player_single_game_fills(self) -> None: 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") diff --git a/test/webhost/test_host_room.py b/test/webhost/test_host_room.py index 4aa83e3b1c6c..0f43fea208b8 100644 --- a/test/webhost/test_host_room.py +++ b/test/webhost/test_host_room.py @@ -1,11 +1,22 @@ +import logging import os from uuid import UUID, uuid4, uuid5 from flask import url_for +from WebHostLib.customserver import set_up_logging, tear_down_logging from . import TestBase +def _cleanup_logger(room_id: UUID) -> None: + from Utils import user_path + tear_down_logging(room_id) + try: + os.unlink(user_path("logs", f"{room_id}.txt")) + except OSError: + pass + + class TestHostFakeRoom(TestBase): room_id: UUID log_filename: str @@ -39,7 +50,7 @@ def tearDown(self) -> None: try: os.unlink(self.log_filename) - except FileNotFoundError: + except OSError: pass def test_display_log_missing_full(self) -> None: @@ -191,3 +202,27 @@ def test_host_room_other_post(self) -> None: with db_session: commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore self.assertNotIn("/help", (command.commandtext for command in commands)) + + def test_logger_teardown(self) -> None: + """Verify that room loggers are removed from the global logging manager.""" + from WebHostLib.customserver import tear_down_logging + room_id = uuid4() + self.addCleanup(_cleanup_logger, room_id) + set_up_logging(room_id) + self.assertIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict) + tear_down_logging(room_id) + self.assertNotIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict) + + def test_handler_teardown(self) -> None: + """Verify that handlers for room loggers are closed by tear_down_logging.""" + from WebHostLib.customserver import tear_down_logging + room_id = uuid4() + self.addCleanup(_cleanup_logger, room_id) + logger = set_up_logging(room_id) + handlers = logger.handlers[:] + self.assertGreater(len(handlers), 0) + + tear_down_logging(room_id) + for handler in handlers: + if isinstance(handler, logging.FileHandler): + self.assertTrue(handler.stream is None or handler.stream.closed) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 327e386c05f1..04f0b61ff8b9 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -363,7 +363,7 @@ def __init__(self, multiworld: "MultiWorld", player: int): def __getattr__(self, item: str) -> Any: if item == "settings": - return self.__class__.settings + return getattr(self.__class__, item) raise AttributeError # overridable methods that get called by Main.py, sorted by execution order @@ -430,6 +430,23 @@ def post_fill(self) -> None: 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. diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index c062a17ea695..550e4878aabc 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1,74 +1,75 @@ # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. from collections import defaultdict +from BaseClasses import MultiWorld from .OverworldGlitchRules import overworld_glitch_connections from .UnderworldGlitchRules import underworld_glitch_connections from .Regions import mark_light_world_regions from .InvertedRegions import mark_dark_world_regions -def link_entrances(world, player): - connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now - connect_exit(world, 'Chris Houlihan Room Exit', 'Links House', player) # should always match link's house, except for plandos +def link_entrances(multiworld: MultiWorld, player: int): + connect_two_way(multiworld, 'Links House', 'Links House Exit', player) # unshuffled. For now + connect_exit(multiworld, 'Chris Houlihan Room Exit', 'Links House', player) # should always match link's house, except for plandos Dungeon_Exits = Dungeon_Exits_Base.copy() Cave_Exits = Cave_Exits_Base.copy() Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() - unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) + unbias_some_entrances(multiworld, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) # setup mandatory connections for exitname, regionname in mandatory_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) # if we do not shuffle, set default connections - if world.worlds[player].options.entrance_shuffle == 'vanilla': + if multiworld.worlds[player].options.entrance_shuffle == 'vanilla': for exitname, regionname in default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) for exitname, regionname in default_dungeon_connections: - connect_simple(world, exitname, regionname, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple': + connect_simple(multiworld, exitname, regionname, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_simple': for exitname, regionname in default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - simple_shuffle_dungeons(world, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_full': + simple_shuffle_dungeons(multiworld, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_full': for exitname, regionname in default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - skull_woods_shuffle(world, player) + skull_woods_shuffle(multiworld, player) dungeon_exits = list(Dungeon_Exits) lw_entrances = list(LW_Dungeon_Entrances) dw_entrances = list(DW_Dungeon_Entrances) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) else: dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) lw_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: dw_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')] - connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player) - connect_caves(world, lw_entrances, [], hyrule_castle_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player) + connect_caves(multiworld, lw_entrances, [], hyrule_castle_exits, player) else: - connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) - connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) - connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed': - crossed_shuffle_dungeons(world, player) - elif world.worlds[player].options.entrance_shuffle == 'simple': - simple_shuffle_dungeons(world, player) + connect_mandatory_exits(multiworld, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player) + connect_mandatory_exits(multiworld, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player) + connect_caves(multiworld, lw_entrances, dw_entrances, dungeon_exits, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_crossed': + crossed_shuffle_dungeons(multiworld, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'simple': + simple_shuffle_dungeons(multiworld, player) old_man_entrances = list(Old_Man_Entrances) caves = list(Cave_Exits) @@ -82,64 +83,64 @@ def link_entrances(world, player): # we shuffle all 2 entrance caves as pairs as a start # start with the ones that need to be directed two_door_caves = list(Two_Door_Caves_Directional) - world.random.shuffle(two_door_caves) - world.random.shuffle(caves) + multiworld.random.shuffle(two_door_caves) + multiworld.random.shuffle(caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) + connect_two_way(multiworld, entrance1, exit1, player) + connect_two_way(multiworld, entrance2, exit2, player) # now the remaining pairs two_door_caves = list(Two_Door_Caves) - world.random.shuffle(two_door_caves) + multiworld.random.shuffle(two_door_caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) + connect_two_way(multiworld, entrance1, exit1, player) + connect_two_way(multiworld, entrance2, exit2, player) # at this point only Light World death mountain entrances remain # place old man, has limited options remaining_entrances = ['Old Man Cave (West)', 'Old Man House (Bottom)', 'Death Mountain Return Cave (West)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave', 'Spiral Cave (Bottom)'] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() remaining_entrances.extend(old_man_entrances) - world.random.shuffle(remaining_entrances) + multiworld.random.shuffle(remaining_entrances) old_man_entrance = remaining_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) # add old man house to ensure it is always somewhere on light death mountain caves.extend(list(Old_Man_House)) caves.extend(list(three_exit_caves)) # connect rest - connect_caves(world, remaining_entrances, [], caves, player) + connect_caves(multiworld, remaining_entrances, [], caves, player) # scramble holes - scramble_holes(world, player) + scramble_holes(multiworld, player) # place blacksmith, has limited options - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) bomb_shop_doors.extend(blacksmith_doors) # place bomb shop, has limited options - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) single_doors.extend(bomb_shop_doors) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) # place remaining doors - connect_doors(world, single_doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'restricted': - simple_shuffle_dungeons(world, player) + connect_doors(multiworld, single_doors, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'restricted': + simple_shuffle_dungeons(multiworld, player) lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) dw_entrances = list(DW_Entrances + DW_Single_Cave_Doors) @@ -152,26 +153,26 @@ def link_entrances(world, player): door_targets = list(Single_Cave_Targets) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) # in restricted, the only mandatory exits are in dark world - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) + connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) lw_entrances.remove(old_man_exit) # place blacksmith, has limited options all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligable for placement) blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) if blacksmith_hut in lw_entrances: lw_entrances.remove(blacksmith_hut) if blacksmith_hut in dw_entrances: @@ -182,36 +183,36 @@ def link_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligable for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) if bomb_shop in lw_entrances: lw_entrances.remove(bomb_shop) if bomb_shop in dw_entrances: dw_entrances.remove(bomb_shop) # place the old man cave's entrance somewhere in the light world - world.random.shuffle(lw_entrances) + multiworld.random.shuffle(lw_entrances) old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # place Old Man House in Light World - connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #for multiple seeds + connect_caves(multiworld, lw_entrances, [], list(Old_Man_House), player) #for multiple seeds # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) + connect_caves(multiworld, lw_entrances, dw_entrances, caves, player) # scramble holes - scramble_holes(world, player) + scramble_holes(multiworld, player) doors = lw_entrances + dw_entrances # place remaining doors - connect_doors(world, doors, door_targets, player) + connect_doors(multiworld, doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'full': - skull_woods_shuffle(world, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'full': + skull_woods_shuffle(multiworld, player) lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances) dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) @@ -225,18 +226,18 @@ def link_entrances(world, player): old_man_house = list(Old_Man_House) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) else: - caves.append(tuple(world.random.sample( + caves.append(tuple(multiworld.random.sample( ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) lw_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: dw_entrances.append('Ganons Tower') caves.append('Ganons Tower Exit') @@ -244,45 +245,45 @@ def link_entrances(world, player): # we randomize which world requirements we fulfill first so we get better dungeon distribution #we also places the Old Man House at this time to make sure he can be connected to the desert one way - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) try: caves.remove(old_man_house[0]) except ValueError: pass else: # if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) + connect_caves(multiworld, lw_entrances, [], old_man_house, player) + connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player) else: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) + connect_mandatory_exits(multiworld, dw_entrances, caves, dw_must_exits, player) caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) try: caves.remove(old_man_house[0]) except ValueError: pass else: #if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) - if world.worlds[player].options.mode == 'standard': + connect_caves(multiworld, lw_entrances, [], old_man_house, player) + if multiworld.worlds[player].options.mode == 'standard': # rest of hyrule castle must be in light world - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) + connect_caves(multiworld, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) lw_entrances.remove(old_man_exit) # place blacksmith, has limited options all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligable for placement) blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) if blacksmith_hut in lw_entrances: lw_entrances.remove(blacksmith_hut) if blacksmith_hut in dw_entrances: @@ -293,9 +294,9 @@ def link_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligable for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) if bomb_shop in lw_entrances: lw_entrances.remove(bomb_shop) if bomb_shop in dw_entrances: @@ -303,21 +304,21 @@ def link_entrances(world, player): # place the old man cave's entrance somewhere in the light world old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) + connect_caves(multiworld, lw_entrances, dw_entrances, caves, player) # scramble holes - scramble_holes(world, player) + scramble_holes(multiworld, player) doors = lw_entrances + dw_entrances # place remaining doors - connect_doors(world, doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'crossed': - skull_woods_shuffle(world, player) + connect_doors(multiworld, doors, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'crossed': + skull_woods_shuffle(multiworld, player) entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors) must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit) @@ -329,43 +330,43 @@ def link_entrances(world, player): door_targets = list(Single_Cave_Targets) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) else: - caves.append(tuple(world.random.sample( + caves.append(tuple(multiworld.random.sample( ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: entrances.append('Ganons Tower') caves.append('Ganons Tower Exit') #place must-exit caves - connect_mandatory_exits(world, entrances, caves, must_exits, player) + connect_mandatory_exits(multiworld, entrances, caves, must_exits, player) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # rest of hyrule castle must be dealt with - connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) + connect_caves(multiworld, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) entrances.remove(old_man_exit) # place blacksmith, has limited options # cannot place it anywhere already taken (or that are otherwise not eligable for placement) blacksmith_doors = [door for door in blacksmith_doors if door in entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) entrances.remove(blacksmith_hut) bomb_shop_doors.extend(blacksmith_doors) @@ -373,28 +374,28 @@ def link_entrances(world, player): # cannot place it anywhere already taken (or that are otherwise not eligable for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) entrances.remove(bomb_shop) # place the old man cave's entrance somewhere - world.random.shuffle(entrances) + multiworld.random.shuffle(entrances) old_man_entrance = entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, entrances, [], caves, player) + connect_caves(multiworld, entrances, [], caves, player) # scramble holes - scramble_holes(world, player) + scramble_holes(multiworld, player) # place remaining doors - connect_doors(world, entrances, door_targets, player) + connect_doors(multiworld, entrances, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'insanity': + elif multiworld.worlds[player].options.entrance_shuffle == 'insanity': # beware ye who enter here entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] @@ -412,7 +413,7 @@ def link_entrances(world, player): blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors) door_targets = list(Single_Cave_Targets) - world.random.shuffle(doors) + multiworld.random.shuffle(doors) old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] @@ -429,13 +430,13 @@ def link_entrances(world, player): 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) + connect_exit(multiworld, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) + connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) else: hole_entrances.append('Hyrule Castle Secret Entrance Drop') hole_targets.append('Hyrule Castle Secret Entrance') @@ -443,10 +444,10 @@ def link_entrances(world, player): entrances.append('Hyrule Castle Secret Entrance Stairs') caves.append('Hyrule Castle Secret Entrance Exit') - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) + connect_two_way(multiworld, 'Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(multiworld, 'Pyramid Hole', 'Pyramid', player) else: entrances.append('Ganons Tower') caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) @@ -455,19 +456,19 @@ def link_entrances(world, player): entrances_must_exits.append('Pyramid Entrance') doors.extend(['Ganons Tower', 'Pyramid Entrance']) - world.random.shuffle(hole_entrances) - world.random.shuffle(hole_targets) - world.random.shuffle(entrances) + multiworld.random.shuffle(hole_entrances) + multiworld.random.shuffle(hole_targets) + multiworld.random.shuffle(entrances) # fill up holes for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) + connect_entrance(multiworld, hole, hole_targets.pop(), player) # hyrule castle handling - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) + connect_entrance(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_exit(multiworld, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) else: doors.append('Hyrule Castle Entrance (South)') @@ -476,7 +477,7 @@ def link_entrances(world, player): # now let's deal with mandatory reachable stuff def extract_reachable_exit(cavelist): - world.random.shuffle(cavelist) + multiworld.random.shuffle(cavelist) candidate = None for cave in cavelist: if isinstance(cave, tuple) and len(cave) > 1: @@ -496,8 +497,8 @@ def connect_reachable_exit(entrance, caves, doors): exit = cave[-1] cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) + connect_exit(multiworld, exit, entrance, player) + connect_entrance(multiworld, doors.pop(), exit, player) # rest of cave now is forced to be in this world caves.append(cave) @@ -508,26 +509,26 @@ def connect_reachable_exit(entrance, caves, doors): # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() entrances.remove(old_man_exit) - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) + connect_exit(multiworld, 'Old Man Cave Exit (East)', old_man_exit, player) + connect_entrance(multiworld, doors.pop(), 'Old Man Cave Exit (East)', player) caves.append('Old Man Cave Exit (West)') # place blacksmith, has limited options blacksmith_doors = [door for door in blacksmith_doors if door in doors] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) doors.remove(blacksmith_hut) # place dam and pyramid fairy, have limited options bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Big Bomb Shop', player) doors.remove(bomb_shop) # handle remaining caves @@ -536,40 +537,40 @@ def connect_reachable_exit(entrance, caves, doors): cave = (cave,) for exit in cave: - connect_exit(world, exit, entrances.pop(), player) - connect_entrance(world, doors.pop(), exit, player) + connect_exit(multiworld, exit, entrances.pop(), player) + connect_entrance(multiworld, doors.pop(), exit, player) # place remaining doors - connect_doors(world, doors, door_targets, player) + connect_doors(multiworld, doors, door_targets, player) else: raise NotImplementedError( - f'{world.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {world.get_player_name(player)}') + f'{multiworld.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {multiworld.get_player_name(player)}') - if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: - overworld_glitch_connections(world, player) + if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + overworld_glitch_connections(multiworld, player) # mandatory hybrid major glitches connections - if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: - underworld_glitch_connections(world, player) + if multiworld.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: + underworld_glitch_connections(multiworld, player) # check for swamp palace fix - if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': - world.worlds[player].swamp_patch_required = True + if multiworld.get_entrance('Dam', player).connected_region.name != 'Dam' or multiworld.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': + multiworld.worlds[player].swamp_patch_required = True # check for potion shop location - if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': - world.worlds[player].powder_patch_required = True + if multiworld.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': + multiworld.worlds[player].powder_patch_required = True # check for ganon location - if world.get_entrance('Pyramid Hole', player).connected_region.name != 'Pyramid': - world.worlds[player].ganon_at_pyramid = False + if multiworld.get_entrance('Pyramid Hole', player).connected_region.name != 'Pyramid': + multiworld.worlds[player].ganon_at_pyramid = False # check for Ganon's Tower location - if world.get_entrance('Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': - world.worlds[player].ganonstower_vanilla = False + if multiworld.get_entrance('Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': + multiworld.worlds[player].ganonstower_vanilla = False -def link_inverted_entrances(world, player): +def link_inverted_entrances(multiworld: MultiWorld, player: int): # Link's house shuffled freely, Houlihan set in mandatory_connections Dungeon_Exits = Inverted_Dungeon_Exits_Base.copy() @@ -577,28 +578,28 @@ def link_inverted_entrances(world, player): Old_Man_House = Old_Man_House_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy() - unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) + unbias_some_entrances(multiworld, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits) # setup mandatory connections for exitname, regionname in inverted_mandatory_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) # if we do not shuffle, set default connections - if world.worlds[player].options.entrance_shuffle == 'vanilla': + if multiworld.worlds[player].options.entrance_shuffle == 'vanilla': for exitname, regionname in inverted_default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) for exitname, regionname in inverted_default_dungeon_connections: - connect_simple(world, exitname, regionname, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple': + connect_simple(multiworld, exitname, regionname, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_simple': for exitname, regionname in inverted_default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - simple_shuffle_dungeons(world, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_full': + simple_shuffle_dungeons(multiworld, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_full': for exitname, regionname in inverted_default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - skull_woods_shuffle(world, player) + skull_woods_shuffle(multiworld, player) dungeon_exits = list(Dungeon_Exits) lw_entrances = list(Inverted_LW_Dungeon_Entrances) @@ -606,7 +607,7 @@ def link_inverted_entrances(world, player): dw_entrances = list(Inverted_DW_Dungeon_Entrances) # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1): + if multiworld.random.randint(0, 1): lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)') lw_entrances.append('Desert Palace Entrance (West)') else: @@ -616,8 +617,8 @@ def link_inverted_entrances(world, player): dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) lw_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: lw_entrances.append('Inverted Ganons Tower') @@ -627,14 +628,14 @@ def link_inverted_entrances(world, player): # shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit all_entrances_aga = lw_entrances + dw_entrances aga_doors = [i for i in all_entrances_aga] - world.random.shuffle(aga_doors) + multiworld.random.shuffle(aga_doors) aga_door = aga_doors.pop() if aga_door in hc_ledge_entrances: lw_entrances.remove(aga_door) hc_ledge_entrances.remove(aga_door) - world.random.shuffle(hc_ledge_entrances) + multiworld.random.shuffle(hc_ledge_entrances) hc_ledge_must_exit = hc_ledge_entrances.pop() lw_entrances.remove(hc_ledge_must_exit) lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit) @@ -643,16 +644,16 @@ def link_inverted_entrances(world, player): else: dw_entrances.remove(aga_door) - connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player) + connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player) dungeon_exits.remove('Inverted Agahnims Tower Exit') - connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) + connect_mandatory_exits(multiworld, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) - connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player) - elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed': - inverted_crossed_shuffle_dungeons(world, player) - elif world.worlds[player].options.entrance_shuffle == 'simple': - simple_shuffle_dungeons(world, player) + connect_caves(multiworld, lw_entrances, dw_entrances, dungeon_exits, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'dungeons_crossed': + inverted_crossed_shuffle_dungeons(multiworld, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'simple': + simple_shuffle_dungeons(multiworld, player) old_man_entrances = list(Inverted_Old_Man_Entrances) caves = list(Cave_Exits) @@ -666,28 +667,28 @@ def link_inverted_entrances(world, player): # we shuffle all 2 entrance caves as pairs as a start # start with the ones that need to be directed two_door_caves = list(Inverted_Two_Door_Caves_Directional) - world.random.shuffle(two_door_caves) - world.random.shuffle(caves) + multiworld.random.shuffle(two_door_caves) + multiworld.random.shuffle(caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) + connect_two_way(multiworld, entrance1, exit1, player) + connect_two_way(multiworld, entrance2, exit2, player) # now the remaining pairs two_door_caves = list(Inverted_Two_Door_Caves) - world.random.shuffle(two_door_caves) + multiworld.random.shuffle(two_door_caves) while two_door_caves: entrance1, entrance2 = two_door_caves.pop() exit1, exit2 = caves.pop() - connect_two_way(world, entrance1, exit1, player) - connect_two_way(world, entrance2, exit2, player) + connect_two_way(multiworld, entrance1, exit1, player) + connect_two_way(multiworld, entrance2, exit2, player) # place links house links_house_doors = [i for i in bomb_shop_doors + blacksmith_doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in bomb_shop_doors: bomb_shop_doors.remove(links_house) if links_house in blacksmith_doors: @@ -697,11 +698,11 @@ def link_inverted_entrances(world, player): # place dark sanc sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) bomb_shop_doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)', @@ -710,10 +711,10 @@ def link_inverted_entrances(world, player): # place old man, bumper cave bottom to DDM entrances not in east bottom - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) if old_man_exit == 'Spike Cave': bomb_shop_doors.remove('Spike Cave') bomb_shop_doors.extend(old_man_entrances) @@ -723,33 +724,33 @@ def link_inverted_entrances(world, player): caves.extend(list(three_exit_caves)) # connect rest - connect_caves(world, lw_dm_entrances, [], caves, player) + connect_caves(multiworld, lw_dm_entrances, [], caves, player) # scramble holes - scramble_inverted_holes(world, player) + scramble_inverted_holes(multiworld, player) # place blacksmith, has limited options blacksmith_doors = [door for door in blacksmith_doors[:]] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) bomb_shop_doors.extend(blacksmith_doors) # place bomb shop, has limited options bomb_shop_doors = [door for door in bomb_shop_doors[:]] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) single_doors.extend(bomb_shop_doors) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) # place remaining doors - connect_doors(world, single_doors, door_targets, player) + connect_doors(multiworld, single_doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'restricted': - simple_shuffle_dungeons(world, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'restricted': + simple_shuffle_dungeons(multiworld, player) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors) dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances) @@ -764,8 +765,8 @@ def link_inverted_entrances(world, player): # place links house links_house_doors = [i for i in lw_entrances + dw_entrances + lw_must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in lw_entrances: lw_entrances.remove(links_house) elif links_house in dw_entrances: @@ -775,32 +776,32 @@ def link_inverted_entrances(world, player): # place dark sanc sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) dw_entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) # place must exits - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in dw_entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) dw_entrances.remove(old_man_exit) # place blacksmith, has limited options all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligible for placement) blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) if blacksmith_hut in lw_entrances: lw_entrances.remove(blacksmith_hut) if blacksmith_hut in dw_entrances: @@ -811,30 +812,30 @@ def link_inverted_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligible for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) if bomb_shop in lw_entrances: lw_entrances.remove(bomb_shop) if bomb_shop in dw_entrances: dw_entrances.remove(bomb_shop) # place the old man cave's entrance somewhere in the dark world - world.random.shuffle(dw_entrances) + multiworld.random.shuffle(dw_entrances) old_man_entrance = dw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) + connect_caves(multiworld, lw_entrances, dw_entrances, caves, player) # scramble holes - scramble_inverted_holes(world, player) + scramble_inverted_holes(multiworld, player) doors = lw_entrances + dw_entrances # place remaining doors - connect_doors(world, doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'full': - skull_woods_shuffle(world, player) + connect_doors(multiworld, doors, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'full': + skull_woods_shuffle(multiworld, player) lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors) dw_entrances = list(Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors + Inverted_Old_Man_Entrances) @@ -847,7 +848,7 @@ def link_inverted_entrances(world, player): old_man_house = list(Old_Man_House) # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: lw_must_exits.append('Desert Palace Entrance (North)') lw_entrances.append('Desert Palace Entrance (West)') else: @@ -855,12 +856,12 @@ def link_inverted_entrances(world, player): lw_entrances.append('Desert Palace Entrance (North)') # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) lw_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: lw_entrances.append('Inverted Ganons Tower') @@ -870,14 +871,14 @@ def link_inverted_entrances(world, player): # shuffle aga door first. if it's on hc ledge, then one other hc ledge door has to be must_exit all_entrances_aga = lw_entrances + dw_entrances aga_doors = [i for i in all_entrances_aga] - world.random.shuffle(aga_doors) + multiworld.random.shuffle(aga_doors) aga_door = aga_doors.pop() if aga_door in hc_ledge_entrances: lw_entrances.remove(aga_door) hc_ledge_entrances.remove(aga_door) - world.random.shuffle(hc_ledge_entrances) + multiworld.random.shuffle(hc_ledge_entrances) hc_ledge_must_exit = hc_ledge_entrances.pop() lw_entrances.remove(hc_ledge_must_exit) lw_must_exits.append(hc_ledge_must_exit) @@ -886,14 +887,14 @@ def link_inverted_entrances(world, player): else: dw_entrances.remove(aga_door) - connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player) + connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player) caves.remove('Inverted Agahnims Tower Exit') # place links house links_house_doors = [i for i in lw_entrances + dw_entrances + lw_must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in lw_entrances: lw_entrances.remove(links_house) if links_house in dw_entrances: @@ -903,35 +904,35 @@ def link_inverted_entrances(world, player): # place dark sanc sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) dw_entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) # place old man house # no dw must exits in inverted, but we randomize whether cave is in light or dark world - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: caves += old_man_house - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) try: caves.remove(old_man_house[0]) except ValueError: pass else: # if the cave wasn't placed we get here - connect_caves(world, lw_entrances, [], old_man_house, player) + connect_caves(multiworld, lw_entrances, [], old_man_house, player) else: - connect_caves(world, dw_entrances, [], old_man_house, player) - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) + connect_caves(multiworld, dw_entrances, [], old_man_house, player) + connect_mandatory_exits(multiworld, lw_entrances, caves, lw_must_exits, player) # put all HC exits in LW in inverted full shuffle - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player) + connect_caves(multiworld, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in dw_entrances + lw_entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) if old_man_exit in dw_entrances: dw_entrances.remove(old_man_exit) old_man_world = 'dark' @@ -943,9 +944,9 @@ def link_inverted_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligible for placement) blacksmith_doors = [door for door in blacksmith_doors if door in all_entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) if blacksmith_hut in lw_entrances: lw_entrances.remove(blacksmith_hut) if blacksmith_hut in dw_entrances: @@ -956,36 +957,36 @@ def link_inverted_entrances(world, player): all_entrances = lw_entrances + dw_entrances # cannot place it anywhere already taken (or that are otherwise not eligible for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in all_entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) if bomb_shop in lw_entrances: lw_entrances.remove(bomb_shop) if bomb_shop in dw_entrances: dw_entrances.remove(bomb_shop) - # place the old man cave's entrance somewhere in the same world he'll exit from + # place the old man cave's entrance somewhere in the same multiworld he'll exit from if old_man_world == 'light': - world.random.shuffle(lw_entrances) + multiworld.random.shuffle(lw_entrances) old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) elif old_man_world == 'dark': - world.random.shuffle(dw_entrances) + multiworld.random.shuffle(dw_entrances) old_man_entrance = dw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) + connect_caves(multiworld, lw_entrances, dw_entrances, caves, player) # scramble holes - scramble_inverted_holes(world, player) + scramble_inverted_holes(multiworld, player) doors = lw_entrances + dw_entrances # place remaining doors - connect_doors(world, doors, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'crossed': - skull_woods_shuffle(world, player) + connect_doors(multiworld, doors, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'crossed': + skull_woods_shuffle(multiworld, player) entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors) must_exits = list(Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit) @@ -997,19 +998,19 @@ def link_inverted_entrances(world, player): door_targets = list(Inverted_Single_Cave_Targets) # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: must_exits.append('Desert Palace Entrance (North)') entrances.append('Desert Palace Entrance (West)') else: must_exits.append('Desert Palace Entrance (West)') entrances.append('Desert Palace Entrance (North)') - caves.append(tuple(world.random.sample( + caves.append(tuple(multiworld.random.sample( ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'], 3))) entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: entrances.append('Inverted Ganons Tower') @@ -1017,26 +1018,26 @@ def link_inverted_entrances(world, player): hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower'] # shuffle aga door. if it's on hc ledge, then one other hc ledge door has to be must_exit - aga_door = world.random.choice(list(entrances)) + aga_door = multiworld.random.choice(list(entrances)) if aga_door in hc_ledge_entrances: hc_ledge_entrances.remove(aga_door) - world.random.shuffle(hc_ledge_entrances) + multiworld.random.shuffle(hc_ledge_entrances) hc_ledge_must_exit = hc_ledge_entrances.pop() entrances.remove(hc_ledge_must_exit) must_exits.append(hc_ledge_must_exit) entrances.remove(aga_door) - connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player) + connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player) caves.remove('Inverted Agahnims Tower Exit') # place links house links_house_doors = [i for i in entrances + must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in entrances: entrances.remove(links_house) elif links_house in must_exits: @@ -1044,58 +1045,58 @@ def link_inverted_entrances(world, player): # place dark sanc sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) entrances.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) #place must-exit caves - connect_mandatory_exits(world, entrances, caves, must_exits, player) + connect_mandatory_exits(multiworld, entrances, caves, must_exits, player) # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [door for door in old_man_entrances if door in entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) + connect_two_way(multiworld, old_man_exit, 'Old Man Cave Exit (East)', player) entrances.remove(old_man_exit) # place blacksmith, has limited options # cannot place it anywhere already taken (or that are otherwise not eligible for placement) blacksmith_doors = [door for door in blacksmith_doors if door in entrances] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) entrances.remove(blacksmith_hut) # place bomb shop, has limited options # cannot place it anywhere already taken (or that are otherwise not eligible for placement) bomb_shop_doors = [door for door in bomb_shop_doors if door in entrances] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) entrances.remove(bomb_shop) # place the old man cave's entrance somewhere - world.random.shuffle(entrances) + multiworld.random.shuffle(entrances) old_man_entrance = entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) + connect_two_way(multiworld, old_man_entrance, 'Old Man Cave Exit (West)', player) # now scramble the rest - connect_caves(world, entrances, [], caves, player) + connect_caves(multiworld, entrances, [], caves, player) # scramble holes - scramble_inverted_holes(world, player) + scramble_inverted_holes(multiworld, player) # place remaining doors - connect_doors(world, entrances, door_targets, player) - elif world.worlds[player].options.entrance_shuffle == 'insanity': + connect_doors(multiworld, entrances, door_targets, player) + elif multiworld.worlds[player].options.entrance_shuffle == 'insanity': # beware ye who enter here entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)'] @@ -1106,7 +1107,7 @@ def link_inverted_entrances(world, player): Inverted_LW_Single_Cave_Doors + Inverted_DW_Single_Cave_Doors + ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1) == 0: + if multiworld.random.randint(0, 1) == 0: entrances_must_exits.append('Desert Palace Entrance (North)') entrances.append('Desert Palace Entrance (West)') else: @@ -1121,7 +1122,7 @@ def link_inverted_entrances(world, player): blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Inverted_Blacksmith_Multi_Cave_Doors) door_targets = list(Inverted_Single_Cave_Targets) - world.random.shuffle(doors) + multiworld.random.shuffle(doors) old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances) + ['Tower of Hera', 'Inverted Agahnims Tower'] @@ -1137,17 +1138,17 @@ def link_inverted_entrances(world, player): 'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)'] # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) + connect_doors(multiworld, ['Tavern North'], ['Tavern'], player) hole_entrances.append('Hyrule Castle Secret Entrance Drop') hole_targets.append('Hyrule Castle Secret Entrance') entrances.append('Hyrule Castle Secret Entrance Stairs') caves.append('Hyrule Castle Secret Entrance Exit') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + connect_two_way(multiworld, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(multiworld, 'Inverted Pyramid Hole', 'Pyramid', player) else: entrances.append('Inverted Ganons Tower') caves.extend(['Inverted Ganons Tower Exit', 'Pyramid Exit']) @@ -1155,13 +1156,13 @@ def link_inverted_entrances(world, player): hole_targets.append('Pyramid') doors.extend(['Inverted Ganons Tower', 'Inverted Pyramid Entrance']) - world.random.shuffle(hole_entrances) - world.random.shuffle(hole_targets) - world.random.shuffle(entrances) + multiworld.random.shuffle(hole_entrances) + multiworld.random.shuffle(hole_targets) + multiworld.random.shuffle(entrances) # fill up holes for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) + connect_entrance(multiworld, hole, hole_targets.pop(), player) doors.append('Hyrule Castle Entrance (South)') caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) @@ -1169,8 +1170,8 @@ def link_inverted_entrances(world, player): # place links house and dark sanc links_house_doors = [i for i in entrances + entrances_must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = world.random.choice(list(links_house_doors)) - connect_two_way(world, links_house, 'Inverted Links House Exit', player) + links_house = multiworld.random.choice(list(links_house_doors)) + connect_two_way(multiworld, links_house, 'Inverted Links House Exit', player) if links_house in entrances: entrances.remove(links_house) elif links_house in entrances_must_exits: @@ -1178,15 +1179,15 @@ def link_inverted_entrances(world, player): doors.remove(links_house) sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] - sanc_door = world.random.choice(sanc_doors) + sanc_door = multiworld.random.choice(sanc_doors) entrances.remove(sanc_door) doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player) - world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) + connect_entrance(multiworld, sanc_door, 'Inverted Dark Sanctuary', player) + multiworld.get_entrance('Inverted Dark Sanctuary Exit', player).connect(multiworld.get_entrance(sanc_door, player).parent_region) # now let's deal with mandatory reachable stuff def extract_reachable_exit(cavelist): - world.random.shuffle(cavelist) + multiworld.random.shuffle(cavelist) candidate = None for cave in cavelist: if isinstance(cave, tuple) and len(cave) > 1: @@ -1206,9 +1207,9 @@ def connect_reachable_exit(entrance, caves, doors): exit = cave[-1] cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) - # rest of cave now is forced to be in this world + connect_exit(multiworld, exit, entrance, player) + connect_entrance(multiworld, doors.pop(), exit, player) + # rest of cave now is forced to be in this multiworld caves.append(cave) # connect mandatory exits @@ -1218,26 +1219,26 @@ def connect_reachable_exit(entrance, caves, doors): # place old man, has limited options # exit has to come from specific set of doors, the entrance is free to move about old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances] - world.random.shuffle(old_man_entrances) + multiworld.random.shuffle(old_man_entrances) old_man_exit = old_man_entrances.pop() entrances.remove(old_man_exit) - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) + connect_exit(multiworld, 'Old Man Cave Exit (East)', old_man_exit, player) + connect_entrance(multiworld, doors.pop(), 'Old Man Cave Exit (East)', player) caves.append('Old Man Cave Exit (West)') # place blacksmith, has limited options blacksmith_doors = [door for door in blacksmith_doors if door in doors] - world.random.shuffle(blacksmith_doors) + multiworld.random.shuffle(blacksmith_doors) blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) + connect_entrance(multiworld, blacksmith_hut, 'Blacksmiths Hut', player) doors.remove(blacksmith_hut) # place dam and pyramid fairy, have limited options bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] - world.random.shuffle(bomb_shop_doors) + multiworld.random.shuffle(bomb_shop_doors) bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) + connect_entrance(multiworld, bomb_shop, 'Inverted Big Bomb Shop', player) doors.remove(bomb_shop) # handle remaining caves @@ -1246,49 +1247,49 @@ def connect_reachable_exit(entrance, caves, doors): cave = (cave,) for exit in cave: - connect_exit(world, exit, entrances.pop(), player) - connect_entrance(world, doors.pop(), exit, player) + connect_exit(multiworld, exit, entrances.pop(), player) + connect_entrance(multiworld, doors.pop(), exit, player) # place remaining doors - connect_doors(world, doors, door_targets, player) + connect_doors(multiworld, doors, door_targets, player) else: raise NotImplementedError('Shuffling not supported yet') - if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: - overworld_glitch_connections(world, player) + if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + overworld_glitch_connections(multiworld, player) # mandatory hybrid major glitches connections - if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: - underworld_glitch_connections(world, player) + if multiworld.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: + underworld_glitch_connections(multiworld, player) # patch swamp drain - if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': - world.worlds[player].swamp_patch_required = True + if multiworld.get_entrance('Dam', player).connected_region.name != 'Dam' or multiworld.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': + multiworld.worlds[player].swamp_patch_required = True # check for potion shop location - if world.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': - world.worlds[player].powder_patch_required = True + if multiworld.get_entrance('Potion Shop', player).connected_region.name != 'Potion Shop': + multiworld.worlds[player].powder_patch_required = True # check for ganon location - if world.get_entrance('Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': - world.worlds[player].ganon_at_pyramid = False + if multiworld.get_entrance('Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': + multiworld.worlds[player].ganon_at_pyramid = False # check for Ganon's Tower location - if world.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': - world.worlds[player].ganonstower_vanilla = False + if multiworld.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)': + multiworld.worlds[player].ganonstower_vanilla = False -def connect_simple(world, exitname, regionname, player): - world.get_entrance(exitname, player).connect(world.get_region(regionname, player)) +def connect_simple(multiworld: MultiWorld, exitname: str, regionname: str, player: int): + multiworld.get_entrance(exitname, player).connect(multiworld.get_region(regionname, player)) -def connect_entrance(world, entrancename: str, exitname: str, player: int): - entrance = world.get_entrance(entrancename, player) +def connect_entrance(multiworld: MultiWorld, entrancename: str, exitname: str, player: int): + entrance = multiworld.get_entrance(entrancename, player) # check if we got an entrance or a region to connect to try: - region = world.get_region(exitname, player) + region = multiworld.get_region(exitname, player) exit = None except KeyError: - exit = world.get_entrance(exitname, player) + exit = multiworld.get_entrance(exitname, player) region = exit.parent_region # if this was already connected somewhere, remove the backreference @@ -1299,24 +1300,24 @@ def connect_entrance(world, entrancename: str, exitname: str, player: int): addresses = door_addresses[entrance.name][0] entrance.connect(region, addresses, target) - world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) + multiworld.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) -def connect_exit(world, exitname, entrancename, player): - entrance = world.get_entrance(entrancename, player) - exit = world.get_entrance(exitname, player) +def connect_exit(multiworld: MultiWorld, exitname: str, entrancename: str, player: int): + entrance = multiworld.get_entrance(entrancename, player) + exit = multiworld.get_entrance(exitname, player) # if this was already connected somewhere, remove the backreference if exit.connected_region is not None: exit.connected_region.entrances.remove(exit) exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) - world.spoiler.set_entrance(entrance.name, exit.name, 'exit', player) + multiworld.spoiler.set_entrance(entrance.name, exit.name, 'exit', player) -def connect_two_way(world, entrancename, exitname, player): - entrance = world.get_entrance(entrancename, player) - exit = world.get_entrance(exitname, player) +def connect_two_way(multiworld: MultiWorld, entrancename: str, exitname: str, player: int): + entrance = multiworld.get_entrance(entrancename, player) + exit = multiworld.get_entrance(exitname, player) # if these were already connected somewhere, remove the backreference if entrance.connected_region is not None: @@ -1326,10 +1327,10 @@ def connect_two_way(world, entrancename, exitname, player): entrance.connect(exit.parent_region, door_addresses[entrance.name][0], exit_ids[exit.name][0]) exit.connect(entrance.parent_region, door_addresses[entrance.name][1], exit_ids[exit.name][1]) - world.spoiler.set_entrance(entrance.name, exit.name, 'both', player) + multiworld.spoiler.set_entrance(entrance.name, exit.name, 'both', player) -def scramble_holes(world, player): +def scramble_holes(multiworld: MultiWorld, player: int): hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), ('Bat Cave Cave', 'Bat Cave Drop'), ('North Fairy Cave', 'North Fairy Cave Drop'), @@ -1343,39 +1344,39 @@ def scramble_holes(world, player): ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - if not world.shuffle_ganon: - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(multiworld, 'Pyramid Hole', 'Pyramid', player) else: hole_targets.append(('Pyramid Exit', 'Pyramid')) - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # cannot move uncle cave - connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) + connect_two_way(multiworld, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) + connect_entrance(multiworld, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) else: hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.worlds[player].options.entrance_shuffle == 'crossed': + if multiworld.worlds[player].options.entrance_shuffle == 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - if world.shuffle_ganon: - world.random.shuffle(hole_targets) + if multiworld.shuffle_ganon: + multiworld.random.shuffle(hole_targets) exit, target = hole_targets.pop() - connect_two_way(world, 'Pyramid Entrance', exit, player) - connect_entrance(world, 'Pyramid Hole', target, player) - if world.worlds[player].options.entrance_shuffle != 'crossed': + connect_two_way(multiworld, 'Pyramid Entrance', exit, player) + connect_entrance(multiworld, 'Pyramid Hole', target, player) + if multiworld.worlds[player].options.entrance_shuffle != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - world.random.shuffle(hole_targets) + multiworld.random.shuffle(hole_targets) for entrance, drop in hole_entrances: exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) + connect_two_way(multiworld, entrance, exit, player) + connect_entrance(multiworld, drop, target, player) -def scramble_inverted_holes(world, player): +def scramble_inverted_holes(multiworld: MultiWorld, player: int): hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), ('Bat Cave Cave', 'Bat Cave Drop'), ('North Fairy Cave', 'North Fairy Cave Drop'), @@ -1389,9 +1390,9 @@ def scramble_inverted_holes(world, player): ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(multiworld, 'Inverted Pyramid Hole', 'Pyramid', player) else: hole_targets.append(('Pyramid Exit', 'Pyramid')) @@ -1400,58 +1401,59 @@ def scramble_inverted_holes(world, player): hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.worlds[player].options.entrance_shuffle == 'crossed': + if multiworld.worlds[player].options.entrance_shuffle == 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - if world.shuffle_ganon: - world.random.shuffle(hole_targets) + if multiworld.shuffle_ganon: + multiworld.random.shuffle(hole_targets) exit, target = hole_targets.pop() - connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) - connect_entrance(world, 'Inverted Pyramid Hole', target, player) - if world.worlds[player].options.entrance_shuffle != 'crossed': + connect_two_way(multiworld, 'Inverted Pyramid Entrance', exit, player) + connect_entrance(multiworld, 'Inverted Pyramid Hole', target, player) + if multiworld.worlds[player].options.entrance_shuffle != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - world.random.shuffle(hole_targets) + multiworld.random.shuffle(hole_targets) for entrance, drop in hole_entrances: exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) + connect_two_way(multiworld, entrance, exit, player) + connect_entrance(multiworld, drop, target, player) -def connect_random(world, exitlist, targetlist, player, two_way=False): +def connect_random(multiworld: MultiWorld, exitlist: list[str], targetlist: list[str], + player: int, two_way: bool = False): targetlist = list(targetlist) - world.random.shuffle(targetlist) + multiworld.random.shuffle(targetlist) for exit, target in zip(exitlist, targetlist): if two_way: - connect_two_way(world, exit, target, player) + connect_two_way(multiworld, exit, target, player) else: - connect_entrance(world, exit, target, player) + connect_entrance(multiworld, exit, target, player) -def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): +def connect_mandatory_exits(multiworld: MultiWorld, entrances: list[str], caves: list[tuple[str, str]], must_be_exits: list[str], player): # Keeps track of entrances that cannot be used to access each exit / cave - if world.worlds[player].options.mode == 'inverted': + if multiworld.worlds[player].options.mode == 'inverted': invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy() else: invalid_connections = Must_Exit_Invalid_Connections.copy() invalid_cave_connections = defaultdict(set) - if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + if multiworld.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: from . import OverworldGlitchRules - for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.worlds[player].options.mode == 'inverted'): + for entrance in OverworldGlitchRules.get_non_mandatory_exits(multiworld.worlds[player].options.mode == 'inverted'): invalid_connections[entrance] = set() if entrance in must_be_exits: must_be_exits.remove(entrance) entrances.append(entrance) """This works inplace""" - world.random.shuffle(entrances) - world.random.shuffle(caves) + multiworld.random.shuffle(entrances) + multiworld.random.shuffle(caves) # Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge - if world.worlds[player].options.mode == 'inverted': + if multiworld.worlds[player].options.mode == 'inverted': for entrance in invalid_connections: - if world.get_entrance(entrance, player).connected_region == world.get_region('Inverted Agahnims Tower', + if multiworld.get_entrance(entrance, player).connected_region == multiworld.get_region('Inverted Agahnims Tower', player): for exit in invalid_connections[entrance]: invalid_connections[exit] = invalid_connections[exit].union( @@ -1473,11 +1475,11 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): raise KeyError('No more caves left. Should not happen!') # all caves are sorted so that the last exit is always reachable - connect_two_way(world, exit, cave[-1], player) + connect_two_way(multiworld, exit, cave[-1], player) if len(cave) == 2: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in invalid_cave_connections[tuple(cave)]) entrances.remove(entrance) - connect_two_way(world, entrance, cave[0], player) + connect_two_way(multiworld, entrance, cave[0], player) if cave in used_caves: required_entrances -= 2 used_caves.remove(cave) @@ -1490,7 +1492,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit]) cave_entrances.append(entrance) entrances.remove(entrance) - connect_two_way(world, entrance, cave_exit, player) + connect_two_way(multiworld, entrance, cave_exit, player) if entrance not in invalid_connections: invalid_connections[exit] = set() if all(entrance in invalid_connections for entrance in cave_entrances): @@ -1504,7 +1506,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): else: required_entrances += len(cave)-1 caves.append(cave[0:-1]) - world.random.shuffle(caves) + multiworld.random.shuffle(caves) used_caves.append(cave[0:-1]) invalid_cave_connections[tuple(cave[0:-1])] = invalid_cave_connections[tuple(cave)].union(invalid_connections[exit]) caves.remove(cave) @@ -1514,19 +1516,20 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player): entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)]) invalid_cave_connections[tuple(cave)] = set() entrances.remove(entrance) - connect_two_way(world, entrance, cave_exit, player) + connect_two_way(multiworld, entrance, cave_exit, player) caves.remove(cave) -def connect_caves(world, lw_entrances, dw_entrances, caves, player): +def connect_caves(multiworld: MultiWorld, lw_entrances: list[str], dw_entrances: list[str], + caves: list[tuple[str, str]], player: int): """This works inplace""" - world.random.shuffle(lw_entrances) - world.random.shuffle(dw_entrances) - world.random.shuffle(caves) + multiworld.random.shuffle(lw_entrances) + multiworld.random.shuffle(dw_entrances) + multiworld.random.shuffle(caves) # connect highest exit count caves first, prevent issue where we have 2 or 3 exits accross worlds left to fill caves.sort(key=lambda cave: 1 if isinstance(cave, str) else len(cave), reverse=True) for cave in caves: - target = lw_entrances if world.random.randint(0, 1) else dw_entrances + target = lw_entrances if multiworld.random.randint(0, 1) else dw_entrances if isinstance(cave, str): cave = (cave,) @@ -1536,37 +1539,37 @@ def connect_caves(world, lw_entrances, dw_entrances, caves, player): target = lw_entrances if target is dw_entrances else dw_entrances for exit in cave: - connect_two_way(world, target.pop(), exit, player) + connect_two_way(multiworld, target.pop(), exit, player) caves.clear() # emulating old behaviour of popping caves from the list in-place -def connect_doors(world, doors, targets, player): +def connect_doors(multiworld: MultiWorld, doors: list[str], targets: list[str], player: int): """This works inplace""" - world.random.shuffle(doors) - world.random.shuffle(targets) + multiworld.random.shuffle(doors) + multiworld.random.shuffle(targets) placing = min(len(doors), len(targets)) for door, target in zip(doors, targets): - connect_entrance(world, door, target, player) + connect_entrance(multiworld, door, target, player) doors[:] = doors[placing:] targets[:] = targets[placing:] -def skull_woods_shuffle(world, player): - connect_random(world, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'], +def skull_woods_shuffle(multiworld: MultiWorld, player: int): + connect_random(multiworld, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'], ['Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)', 'Skull Woods Second Section (Drop)'], player) - connect_random(world, ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'], + connect_random(multiworld, ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'], ['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'], player, True) -def simple_shuffle_dungeons(world, player): - skull_woods_shuffle(world, player) +def simple_shuffle_dungeons(multiworld: MultiWorld, player: int): + skull_woods_shuffle(multiworld, player) dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace'] dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit'] - if world.worlds[player].options.mode != 'inverted': - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if multiworld.worlds[player].options.mode != 'inverted': + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') @@ -1575,17 +1578,17 @@ def simple_shuffle_dungeons(world, player): dungeon_exits.append('Inverted Agahnims Tower Exit') # shuffle up single entrance dungeons - connect_random(world, dungeon_entrances, dungeon_exits, player, True) + connect_random(multiworld, dungeon_entrances, dungeon_exits, player, True) # mix up 4 door dungeons multi_dungeons = ['Desert', 'Turtle Rock'] - if world.worlds[player].options.mode == 'open' or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon): + if multiworld.worlds[player].options.mode == 'open' or (multiworld.worlds[player].options.mode == 'inverted' and multiworld.shuffle_ganon): multi_dungeons.append('Hyrule Castle') - world.random.shuffle(multi_dungeons) + multiworld.random.shuffle(multi_dungeons) dp_target = multi_dungeons[0] tr_target = multi_dungeons[1] - if world.worlds[player].options.mode not in ['open', 'inverted'] or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon is False): + if multiworld.worlds[player].options.mode not in ['open', 'inverted'] or (multiworld.worlds[player].options.mode == 'inverted' and multiworld.shuffle_ganon is False): # place hyrule castle as intended hc_target = 'Hyrule Castle' else: @@ -1593,152 +1596,152 @@ def simple_shuffle_dungeons(world, player): # ToDo improve this? - if world.worlds[player].options.mode != 'inverted': + if multiworld.worlds[player].options.mode != 'inverted': if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Agahnims Tower Exit', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Agahnims Tower', 'Agahnims Tower Exit', player) elif hc_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Agahnims Tower Exit', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Agahnims Tower Exit', player) elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Agahnims Tower Exit', player) + connect_two_way(multiworld, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Agahnims Tower Exit', player) if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Agahnims Tower', 'Desert Palace Exit (North)', player) elif dp_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Agahnims Tower', 'Turtle Rock Isolated Ledge Exit', player) elif tr_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) else: if hc_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) elif hc_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Inverted Ganons Tower Exit', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Inverted Ganons Tower Exit', player) elif hc_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Inverted Ganons Tower Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Inverted Ganons Tower Exit', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Hyrule Castle Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Hyrule Castle Exit (East)', player) if dp_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Inverted Ganons Tower', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Desert Palace Exit (North)', player) elif dp_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Desert Palace Exit (North)', player) elif dp_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Desert Palace Exit (South)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Desert Palace Exit (South)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Desert Palace Exit (East)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Desert Palace Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Desert Palace Exit (North)', player) if tr_target == 'Hyrule Castle': - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Inverted Ganons Tower', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Turtle Rock Isolated Ledge Exit', player) elif tr_target == 'Desert': - connect_two_way(world, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) - connect_two_way(world, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Desert Palace Entrance (South)', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (North)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Desert Palace Entrance (East)', 'Turtle Rock Isolated Ledge Exit', player) elif tr_target == 'Turtle Rock': - connect_two_way(world, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) - connect_two_way(world, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) - connect_two_way(world, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) - connect_two_way(world, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) + connect_two_way(multiworld, 'Turtle Rock', 'Turtle Rock Exit (Front)', player) + connect_two_way(multiworld, 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Isolated Ledge Exit', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (West)', 'Turtle Rock Ledge Exit (West)', player) + connect_two_way(multiworld, 'Dark Death Mountain Ledge (East)', 'Turtle Rock Ledge Exit (East)', player) -def crossed_shuffle_dungeons(world, player: int): +def crossed_shuffle_dungeons(multiworld: MultiWorld, player: int): lw_entrances = LW_Dungeon_Entrances.copy() dw_entrances = DW_Dungeon_Entrances.copy() for exitname, regionname in default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - skull_woods_shuffle(world, player) + skull_woods_shuffle(multiworld, player) dungeon_exits = Dungeon_Exits_Base.copy() dungeon_entrances = lw_entrances+dw_entrances - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Ganons Tower', 'Ganons Tower Exit', player) else: dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') - if world.worlds[player].options.mode == 'standard': + if multiworld.worlds[player].options.mode == 'standard': # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) + connect_two_way(multiworld, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) else: dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) dungeon_entrances.append('Hyrule Castle Entrance (South)') - connect_mandatory_exits(world, dungeon_entrances, dungeon_exits, + connect_mandatory_exits(multiworld, dungeon_entrances, dungeon_exits, LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player) - if world.worlds[player].options.mode == 'standard': - connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) + if multiworld.worlds[player].options.mode == 'standard': + connect_caves(multiworld, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - connect_caves(world, dungeon_entrances, [], dungeon_exits, player) + connect_caves(multiworld, dungeon_entrances, [], dungeon_exits, player) assert not dungeon_exits , "make sure all exits are accounted for" -def inverted_crossed_shuffle_dungeons(world, player: int): +def inverted_crossed_shuffle_dungeons(multiworld: MultiWorld, player: int): lw_entrances = Inverted_LW_Dungeon_Entrances.copy() dw_entrances = Inverted_DW_Dungeon_Entrances.copy() lw_dungeon_entrances_must_exit = list(Inverted_LW_Dungeon_Entrances_Must_Exit) for exitname, regionname in inverted_default_connections: - connect_simple(world, exitname, regionname, player) + connect_simple(multiworld, exitname, regionname, player) - skull_woods_shuffle(world, player) + skull_woods_shuffle(multiworld, player) dungeon_exits = Inverted_Dungeon_Exits_Base.copy() dungeon_entrances = lw_entrances+dw_entrances # randomize which desert ledge door is a must-exit - if world.random.randint(0, 1): + if multiworld.random.randint(0, 1): lw_dungeon_entrances_must_exit.append('Desert Palace Entrance (North)') dungeon_entrances.append('Desert Palace Entrance (West)') else: @@ -1748,8 +1751,8 @@ def inverted_crossed_shuffle_dungeons(world, player: int): dungeon_exits.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) dungeon_entrances.append('Hyrule Castle Entrance (South)') - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) + if not multiworld.shuffle_ganon: + connect_two_way(multiworld, 'Inverted Ganons Tower', 'Inverted Ganons Tower Exit', player) hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)'] else: dungeon_entrances.append('Inverted Ganons Tower') @@ -1757,29 +1760,34 @@ def inverted_crossed_shuffle_dungeons(world, player: int): hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower'] # shuffle aga door first. If it's on HC ledge, remaining HC ledge door must be must-exit - world.random.shuffle(dungeon_entrances) + multiworld.random.shuffle(dungeon_entrances) aga_door = dungeon_entrances.pop() if aga_door in hc_ledge_entrances: hc_ledge_entrances.remove(aga_door) - world.random.shuffle(hc_ledge_entrances) + multiworld.random.shuffle(hc_ledge_entrances) hc_ledge_must_exit = hc_ledge_entrances.pop() dungeon_entrances.remove(hc_ledge_must_exit) lw_dungeon_entrances_must_exit.append(hc_ledge_must_exit) - connect_two_way(world, aga_door, 'Inverted Agahnims Tower Exit', player) + connect_two_way(multiworld, aga_door, 'Inverted Agahnims Tower Exit', player) dungeon_exits.remove('Inverted Agahnims Tower Exit') - connect_mandatory_exits(world, dungeon_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) + connect_mandatory_exits(multiworld, dungeon_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player) - connect_caves(world, dungeon_entrances, [], dungeon_exits, player) + connect_caves(multiworld, dungeon_entrances, [], dungeon_exits, player) assert not dungeon_exits, "make sure all exits are accounted for" -def unbias_some_entrances(world, Dungeon_Exits, Cave_Exits, Old_Man_House, Cave_Three_Exits): + +def unbias_some_entrances(multiworld: MultiWorld, + Dungeon_Exits: list[list[str] | str], + Cave_Exits: list[str], + Old_Man_House: list[str], + Cave_Three_Exits: list[tuple[str, str, str]]): def shuffle_lists_in_list(ls): for i, item in enumerate(ls): if isinstance(item, list): - ls[i] = world.random.sample(item, len(item)) + ls[i] = multiworld.random.sample(item, len(item)) def tuplize_lists_in_list(ls): for i, item in enumerate(ls): @@ -1793,7 +1801,7 @@ def tuplize_lists_in_list(ls): # paradox fixup if Cave_Three_Exits[1][0] == "Paradox Cave Exit (Bottom)": - i = world.random.randint(1, 2) + i = multiworld.random.randint(1, 2) Cave_Three_Exits[1][0] = Cave_Three_Exits[1][i] Cave_Three_Exits[1][i] = "Paradox Cave Exit (Bottom)" @@ -1822,18 +1830,18 @@ def tuplize_lists_in_list(ls): } -def plando_connect(world, player: int): - if world.worlds[player].options.plando_connections: - for connection in world.worlds[player].options.plando_connections: +def plando_connect(multiworld: MultiWorld, player: int): + if multiworld.worlds[player].options.plando_connections: + for connection in multiworld.worlds[player].options.plando_connections: func = lookup[connection.direction] try: - func(world, connection.entrance, connection.exit, player) + func(multiworld, connection.entrance, connection.exit, player) except Exception as e: raise Exception(f"Could not connect using {connection}") from e - if world.worlds[player].options.mode != 'inverted': - mark_light_world_regions(world, player) + if multiworld.worlds[player].options.mode != 'inverted': + mark_light_world_regions(multiworld, player) else: - mark_dark_world_regions(world, player) + mark_dark_world_regions(multiworld, player) LW_Dungeon_Entrances = ['Desert Palace Entrance (South)', diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index 63a2d499e2d4..47e12d313f66 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -1,15 +1,16 @@ 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(world, player): +def create_inverted_regions(multiworld: MultiWorld, player: int): - world.regions += [ - create_dw_region(world, player, 'Menu', None, + multiworld.regions += [ + create_dw_region(multiworld, player, 'Menu', None, ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']), - create_lw_region(world, player, 'Light World', + create_lw_region(multiworld, 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', @@ -35,184 +36,184 @@ def create_inverted_regions(world, player): '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(world, player, 'Bush Covered Lawn', None, + create_lw_region(multiworld, player, 'Bush Covered Lawn', None, ['Bush Covered House', 'Bush Covered Lawn Inner Bushes', 'Bush Covered Lawn Mirror Spot']), - create_lw_region(world, player, 'Bomb Hut Area', None, + create_lw_region(multiworld, player, 'Bomb Hut Area', None, ['Light World Bomb Hut', 'Bomb Hut Inner Bushes', 'Bomb Hut Mirror Spot']), - create_lw_region(world, player, 'Hyrule Castle Secret Entrance Area', None, + create_lw_region(multiworld, player, 'Hyrule Castle Secret Entrance Area', None, ['Hyrule Castle Secret Entrance Stairs', 'Secret Passage Inner Bushes']), - create_lw_region(world, player, 'Death Mountain Entrance', None, + create_lw_region(multiworld, player, 'Death Mountain Entrance', None, ['Old Man Cave (West)', 'Death Mountain Entrance Drop', 'Bumper Cave Entrance Mirror Spot']), - create_lw_region(world, player, 'Lake Hylia Central Island', None, + create_lw_region(multiworld, player, 'Lake Hylia Central Island', None, ['Capacity Upgrade', 'Lake Hylia Central Island Mirror Spot']), - create_cave_region(world, player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", + create_cave_region(multiworld, 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(world, player, 'Northeast Light World', None, + create_lw_region(multiworld, 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(world, player, 'Waterfall of Wishing Cave', None, + create_lw_region(multiworld, player, 'Waterfall of Wishing Cave', None, ['Waterfall of Wishing', 'Northeast Light World Return']), - create_lw_region(world, player, 'Potion Shop Area', None, + create_lw_region(multiworld, 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(world, player, 'Graveyard Cave Area', None, + create_lw_region(multiworld, player, 'Graveyard Cave Area', None, ['Graveyard Cave', 'Graveyard Cave Inner Bushes', 'Graveyard Cave Mirror Spot']), - 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', + 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', ['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', + 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(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'], + 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'], ['Inverted 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, + 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(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', + 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(world, player, 'Kakariko Well (top)', 'a drop\'s exit', + create_cave_region(multiworld, 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(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'], + 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(world, player, 'Lost Woods Hideout (bottom)', 'a drop\'s exit', None, + create_cave_region(multiworld, 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'], + create_cave_region(multiworld, 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_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', + 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', ['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(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, + 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(world, player, 'Maze Race Ledge', ['Maze Race'], + create_lw_region(multiworld, player, 'Maze Race Ledge', ['Maze Race'], ['Two Brothers House (West)', 'Maze Race Mirror Spot']), - create_cave_region(world, player, '50 Rupee Cave', 'a cave with some cash'), - create_lw_region(world, player, 'Desert Ledge', ['Desert Ledge'], + 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)', 'Desert Ledge Drop']), - create_lw_region(world, player, 'Desert Palace Stairs', None, + create_lw_region(multiworld, player, 'Desert Palace Stairs', None, ['Desert Palace Entrance (South)', 'Desert Palace Stairs Mirror Spot']), - create_lw_region(world, player, 'Desert Palace Lone Stairs', None, + create_lw_region(multiworld, 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, + create_lw_region(multiworld, player, 'Desert Palace Entrance (North) Spot', None, ['Desert Palace Entrance (North)', 'Desert Ledge Return Rocks', 'Desert Palace North Mirror Spot']), - create_dungeon_region(world, player, 'Desert Palace Main (Outer)', 'Desert Palace', ['Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest'], + create_dungeon_region(multiworld, 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(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', + 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', 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), - create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', + create_dungeon_region(multiworld, 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(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', + 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', ['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(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', + 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', 'Sewers - Secret Room - Right']), - 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'], + 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'], ['Old Man Cave Exit (East)', 'Old Man Cave Exit (West)']), - create_cave_region(world, player, 'Old Man House', 'a connector', None, + 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(world, player, 'Old Man House Back', 'a connector', None, + 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(world, player, 'Death Mountain', None, + 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 Mirror Spot']), - create_cave_region(world, player, 'Death Mountain Return Cave', 'a connector', None, + 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(world, player, 'Death Mountain Return Ledge', None, + create_lw_region(multiworld, player, 'Death Mountain Return Ledge', None, ['Death Mountain Return Ledge Drop', 'Death Mountain Return Cave (West)', 'Bumper Cave Ledge Mirror Spot']), - create_cave_region(world, player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], + 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(world, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), - create_cave_region(world, player, 'Spectacle Rock Cave (Peak)', 'a connector', None, + 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(world, player, 'East Death Mountain (Bottom)', None, + create_lw_region(multiworld, 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(world, player, 'Hookshot Fairy', 'fairies deep in a cave'), - create_cave_region(world, player, 'Paradox Cave Front', 'a connector', None, + 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(world, player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', + create_cave_region(multiworld, 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', @@ -220,273 +221,273 @@ def create_inverted_regions(world, player): 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right'], ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), - create_cave_region(world, player, 'Paradox Cave', 'a connector', None, + create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), - create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'), - create_lw_region(world, player, 'East Death Mountain (Top)', ['Floating Island'], + create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'), + create_lw_region(multiworld, 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(world, player, 'Spiral Cave Ledge', None, + create_lw_region(multiworld, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (West)']), - create_lw_region(world, player, 'Mimic Cave Ledge', None, + create_lw_region(multiworld, player, 'Mimic Cave Ledge', None, ['Mimic Cave', 'Mimic Cave Ledge Drop', 'Dark Death Mountain Ledge Mirror Spot (East)']), - create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], + create_cave_region(multiworld, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']), - create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), - create_lw_region(world, player, 'Fairy Ascension Plateau', None, + create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), + create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None, ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']), - create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']), - create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None, ['Fairy Ascension Cave Pots']), - create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None, ['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']), - create_lw_region(world, player, 'Fairy Ascension Ledge', None, + create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None, ['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)', 'Laser Bridge Mirror Spot']), - create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet', 'Spectacle Rock'], + create_lw_region(multiworld, 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(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], + create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)']), - 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_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_dw_region(world, player, 'East Dark World', ['Pyramid'], + create_dw_region(multiworld, 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(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), - create_dw_region(world, player, 'Northeast Dark World', None, + create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), + create_dw_region(multiworld, 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(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'], + 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'], ['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(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, + 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, ['East Dark World Pier', 'Dark Lake Hylia Ledge Pier', 'Ice Palace Missing Wall']), - create_dw_region(world, player, 'Dark Lake Hylia Central Island', None, + create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None, ['Dark Lake Hylia Shallows', 'Ice Palace', 'Dark Lake Hylia Central Island Teleporter']), - create_dw_region(world, player, 'Dark Lake Hylia Ledge', None, + create_dw_region(multiworld, 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(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', + 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', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), - create_dw_region(world, player, 'West Dark World', ['Frog', 'Flute Activation Spot'], + create_dw_region(multiworld, 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(world, player, 'Dark Grassy Lawn', None, + create_dw_region(multiworld, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']), - create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], + create_dw_region(multiworld, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']), - create_dw_region(world, player, 'Bumper Cave Entrance', None, + create_dw_region(multiworld, player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']), - 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', + 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', ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']), - 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, + 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, ['Inverted Dark Sanctuary Exit']), - create_cave_region(world, player, 'Bumper Cave', 'a connector', None, + create_cave_region(multiworld, player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), - create_dw_region(world, player, 'Skull Woods Forest', None, + create_dw_region(multiworld, 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(world, player, 'Skull Woods Forest (West)', None, + create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']), - create_dw_region(world, player, 'Dark Desert', None, + create_dw_region(multiworld, player, 'Dark Desert', None, ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'DD Flute']), - 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', + 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', ['Mire Shed - Left', 'Mire Shed - Right']), - create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'), - create_dw_region(world, player, 'Dark Death Mountain', None, + create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'), + create_dw_region(multiworld, 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(world, player, 'Dark Death Mountain Ledge', None, + create_dw_region(multiworld, player, 'Dark Death Mountain Ledge', None, ['Dark Death Mountain Ledge (East)', 'Dark Death Mountain Ledge (West)']), - create_dw_region(world, player, 'Turtle Rock (Top)', None, + create_dw_region(multiworld, player, 'Turtle Rock (Top)', None, ['Dark Death Mountain Teleporter (East)', 'Turtle Rock Drop']), - create_dw_region(world, player, 'Dark Death Mountain Isolated Ledge', None, + create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None, ['Turtle Rock Isolated Ledge Entrance']), - create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None, + create_dw_region(multiworld, 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(world, player, 'Superbunny Cave (Top)', 'a connector', + create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']), - create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']), - create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), - create_cave_region(world, player, 'Hookshot Cave', 'a connector', + create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), + create_cave_region(multiworld, 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(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', + create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', 'Hookshot Cave Bomb Wall (North)']), - create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None, + create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']), - create_cave_region(world, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), + create_cave_region(multiworld, player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']), - 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', + 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', 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']), - create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key', + create_dungeon_region(multiworld, 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(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', + 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', 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']), - create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', + create_dungeon_region(multiworld, 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(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', + create_dungeon_region(multiworld, 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(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', + 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', 'Ice Palace - Many Pots Pot Key', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), - 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', + 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', '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(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', + 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', 'Turtle Rock - Roller Room - Right'], ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']), - 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'], + 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(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', + 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(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', + 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', '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(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'], + 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'], ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']), - 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'], + 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'], ['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']), - 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', + 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', ['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(world, player, 'Ganons Tower (Tile Room)', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], + 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(world, player, 'Ganons Tower (Compass Room)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, 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(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, 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(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', + 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(world, player, 'Ganons Tower (Teleport Room)', 'Ganon\'s Tower', + create_dungeon_region(multiworld, 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(world, player, 'Ganons Tower (Bottom)', 'Ganon\'s Tower', + 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', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), - 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', + 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', 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']), - create_dungeon_region(world, player, 'Ganons Tower (Moldorm)', 'Ganon\'s Tower', None, ['Ganons Tower Moldorm Gap']), + create_dungeon_region(multiworld, 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 Drop']), # houlihan room exits here in inverted + 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 # to simplify flute connections - create_cave_region(world, player, 'The Sky', 'A Dark Sky', None, + create_cave_region(multiworld, player, 'The Sky', 'A Dark Sky', None, ['DDM Landing', 'NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing']), - create_lw_region(world, player, 'Desert Northern Cliffs'), - create_lw_region(world, player, 'Death Mountain Bunny Descent Area') + create_lw_region(multiworld, player, 'Desert Northern Cliffs'), + create_lw_region(multiworld, player, 'Death Mountain Bunny Descent Area') ] -def mark_dark_world_regions(world, player): +def mark_dark_world_regions(multiworld: MultiWorld, 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 world.get_regions(player) if region.type == LTTPRegionType.DarkWorld) + queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld) seen = set(queue) while queue: current = queue.popleft() @@ -499,7 +500,7 @@ def mark_dark_world_regions(world, player): seen.add(exit.connected_region) queue.append(exit.connected_region) - queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.LightWorld) + queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.LightWorld) seen = set(queue) while queue: current = queue.popleft() diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 6b0968f6e598..c7dc7a694848 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -1,8 +1,9 @@ from collections import namedtuple import logging -from BaseClasses import ItemClassification +from BaseClasses import ItemClassification, MultiWorld 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 @@ -14,6 +15,9 @@ 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. @@ -222,7 +226,7 @@ ) -def generate_itempool(world): +def generate_itempool(world: "ALTTPWorld"): player: int = world.player multiworld = world.multiworld @@ -531,7 +535,7 @@ def cut_item(items, item_to_cut, minimum_items): take_any_locations.sort() -def set_up_take_anys(multiworld, world, player): +def set_up_take_anys(multiworld: MultiWorld, world: "ALTTPWorld", player: int): # these are references, do not modify these lists in-place if world.options.mode == 'inverted': take_any_locs = take_any_locations_inverted @@ -585,15 +589,15 @@ def set_up_take_anys(multiworld, world, player): location.place_locked_item(item_factory("Boss Heart Container", world)) -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 +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 pool = [] placed_items = {} @@ -610,13 +614,13 @@ def place_item(loc, item): placed_items[loc] = item # provide boots to major glitch dependent seeds - if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots: + if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and multiworld.worlds[player].options.glitch_boots: precollected_items.append('Pegasus Boots') pool.remove('Pegasus Boots') pool.append('Rupees (20)') - want_progressives = world.worlds[player].options.progressive.want_progressives + want_progressives = multiworld.worlds[player].options.progressive.want_progressives - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressiveglove) else: pool.extend(diff.basicglove) @@ -640,27 +644,27 @@ def place_item(loc, item): thisbottle = None for _ in range(diff.bottle_count): if not diff.same_bottle or not thisbottle: - thisbottle = world.random.choice(diff.bottles) + thisbottle = multiworld.random.choice(diff.bottles) pool.append(thisbottle) - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressiveshield) else: pool.extend(diff.basicshield) - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressivearmor) else: pool.extend(diff.basicarmor) - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressivemagic) else: pool.extend(diff.basicmagic) - if want_progressives(world.random): + if want_progressives(multiworld.random): pool.extend(diff.progressivebow) - world.worlds[player].has_progressive_bows = True + multiworld.worlds[player].has_progressive_bows = True elif (swordless or logic == 'no_glitches'): swordless_bows = ['Bow', 'Silver Bow'] if difficulty == "easy": @@ -672,7 +676,7 @@ def place_item(loc, item): if swordless: pool.extend(diff.swordless) else: - progressive_swords = want_progressives(world.random) + progressive_swords = want_progressives(multiworld.random) pool.extend(diff.progressivesword if progressive_swords else diff.basicsword) extraitems = total_items_to_place - len(pool) - len(placed_items) @@ -688,29 +692,29 @@ def place_item(loc, item): additional_pieces_to_place = 0 if 'triforce_hunt' in goal: - 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)) + 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)) else: # available - treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value + treasure_hunt_total = multiworld.worlds[player].options.triforce_pieces_available.value - triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value)) + triforce_pieces = min(90, max(treasure_hunt_total, multiworld.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 = world.worlds[player].options.triforce_pieces_required.value + treasure_hunt_required = multiworld.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(world.random.sample(extra, extraitems)) + pool.extend(multiworld.random.sample(extra, extraitems)) break else: break @@ -729,25 +733,25 @@ def place_item(loc, item): else: break - if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal: + if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal: pool.extend(diff.universal_keys) if mode == 'standard': - if world.worlds[player].options.key_drop_shuffle: + if multiworld.worlds[player].options.key_drop_shuffle: key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop'] - key_location = world.random.choice(key_locations) + key_location = multiworld.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 = world.random.choice(key_locations) + key_location = multiworld.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 = world.random.choice(key_locations) + key_location = multiworld.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 = world.random.choice(key_locations) + key_location = multiworld.random.choice(key_locations) place_item(key_location, "Small Key (Universal)") pool = pool[:-3] diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index cbe6e9964232..1c173c619e3f 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -1,24 +1,24 @@ import typing -from BaseClasses import ItemClassification as IC +from BaseClasses import MultiWorld, ItemClassification as IC from worlds.AutoWorld import World -def GetBeemizerItem(world, player: int, item): +def GetBeemizerItem(multiworld: MultiWorld, player: int, item): item_name = item if isinstance(item, str) else item.name - if item_name not in trap_replaceable or player in world.groups: + if item_name not in trap_replaceable or player in multiworld.groups: return item # first roll - replaceable item should be replaced, within beemizer_total_chance - if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100): + if not multiworld.worlds[player].options.beemizer_total_chance or multiworld.random.random() > (multiworld.worlds[player].options.beemizer_total_chance / 100): return item # second roll - bee replacement should be trap, within beemizer_trap_chance - 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) + 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) else: - return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player) + return "Bee Trap" if isinstance(item, str) else multiworld.create_item("Bee Trap", player) def item_factory(items: typing.Union[str, typing.Iterable[str]], world: World): diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 519241d7f4a9..53bdbd6cba06 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -154,13 +154,13 @@ class OpenPyramid(Choice): alias_true = option_open alias_false = option_closed - def to_bool(self, world: MultiWorld, player: int) -> bool: + def to_bool(self, multiworld: MultiWorld, player: int) -> bool: if self.value == self.option_goal: - return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} + return multiworld.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} elif self.value == self.option_auto: - 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) + 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) elif self.value == self.option_open: return True else: diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py index aeff9cb88e9a..a243569cca57 100644 --- a/worlds/alttp/OverworldGlitchRules.py +++ b/worlds/alttp/OverworldGlitchRules.py @@ -2,6 +2,7 @@ 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 @@ -200,7 +201,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): +def get_mirror_offset_spots_lw(player: int): """ Mirror shenanigans placing a mirror portal with a broken camera """ @@ -218,54 +219,54 @@ def get_invalid_bunny_revival_dungeons(): yield 'Sanctuary' -def overworld_glitch_connections(world, player): +def overworld_glitch_connections(multiworld: MultiWorld, player: int): # Boots-accessible locations. - 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)) + 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)) # Glitched speed drops. - create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted')) + create_owg_connections(player, multiworld, get_glitched_speed_drops_dw(multiworld.worlds[player].options.mode == 'inverted')) # Mirror clip spots. - 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()) + 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()) else: - create_owg_connections(player, world, get_mirror_offset_spots_lw(player)) + create_owg_connections(player, multiworld, get_mirror_offset_spots_lw(player)) -def overworld_glitches_rules(world, player): +def overworld_glitches_rules(multiworld: MultiWorld, player: int): # Boots-accessible locations. - 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)) + 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)) # Glitched speed drops. - 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)) + 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)) # Dark Death Mountain Ledge Clip Spot also accessible with mirror. - 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)) + 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)) # Mirror clip spots. - 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)) + 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)) else: - 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)) + 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)) # Regions that require the boots and some other stuff. - 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)) + 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)) else: - add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player)) + add_alternate_rule(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', 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)) + 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)) # Zora's Ledge via waterwalk setup. - add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player)) + add_alternate_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player)) def add_alternate_rule(entrance, rule): @@ -273,22 +274,22 @@ def add_alternate_rule(entrance, rule): entrance.access_rule = lambda state: old_rule(state) or rule(state) -def create_no_logic_connections(player, world, connections): +def create_no_logic_connections(player: int, multiworld: MultiWorld, connections): for entrance, parent_region, target_region, *rule_override in connections: - parent = world.get_region(parent_region, player) - target = world.get_region(target_region, player) + parent = multiworld.get_region(parent_region, player) + target = multiworld.get_region(target_region, player) parent.connect(target, entrance) -def create_owg_connections(player, world, connections): +def create_owg_connections(player: int, multiworld: MultiWorld, connections): for entrance, parent_region, target_region, *rule_override in connections: - parent = world.get_region(parent_region, player) - target = world.get_region(target_region, player) + parent = multiworld.get_region(parent_region, player) + target = multiworld.get_region(target_region, player) parent.connect(target, entrance) -def set_owg_connection_rules(player, world, connections, default_rule): +def set_owg_connection_rules(player: int, multiworld: MultiWorld, connections, default_rule): for entrance, _, _, *rule_override in connections: - connection = world.get_entrance(entrance, player) + connection = multiworld.get_entrance(entrance, player) rule = rule_override[0] if len(rule_override) > 0 else default_rule connection.access_rule = rule diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index c2af7956373e..d3789f91baf0 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -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(world, player): +def create_regions(multiworld: MultiWorld, player: int): - 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', + 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', '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(world, player): '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(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", + 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", "Blind\'s Hideout - Left", "Blind\'s Hideout - Right", "Blind\'s Hideout - Far Left", "Blind\'s Hideout - Far Right"]), - 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', + 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', 'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), - 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', + 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', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy']), - 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'], + 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'], ['Desert Palace Pots (Outer)', 'Desert Palace Exit (West)', 'Desert Palace Exit (East)', 'Desert Palace East Wing']), - 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', + 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', 'Desert Palace - Boss', 'Desert Palace - Prize'], ['Desert Palace Exit (North)']), - create_dungeon_region(world, player, 'Eastern Palace', 'Eastern Palace', ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest', + create_dungeon_region(multiworld, 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(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', + 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', '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(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', + 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', 'Sewers - Secret Room - Right']), - 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', + 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', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', @@ -147,267 +147,267 @@ def create_regions(world, player): 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right'], ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), - create_cave_region(world, player, 'Paradox Cave', 'a connector', None, + create_cave_region(multiworld, player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), - create_cave_region(world, player, 'Light World Death Mountain Shop', 'a common shop'), - create_lw_region(world, player, 'East Death Mountain (Top)', None, + create_cave_region(multiworld, player, 'Light World Death Mountain Shop', 'a common shop'), + create_lw_region(multiworld, 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(world, player, 'Spiral Cave Ledge', None, ['Spiral Cave', 'Spiral Cave Ledge Drop']), - create_cave_region(world, player, 'Spiral Cave (Top)', 'a connector', ['Spiral Cave'], + 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'], ['Spiral Cave (top to bottom)', 'Spiral Cave Exit (Top)']), - create_cave_region(world, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), - create_lw_region(world, player, 'Fairy Ascension Plateau', None, + create_cave_region(multiworld, player, 'Spiral Cave (Bottom)', 'a connector', None, ['Spiral Cave Exit']), + create_lw_region(multiworld, player, 'Fairy Ascension Plateau', None, ['Fairy Ascension Drop', 'Fairy Ascension Cave (Bottom)']), - create_cave_region(world, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']), - create_cave_region(world, player, 'Fairy Ascension Cave (Drop)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Drop)', 'a connector', None, ['Fairy Ascension Cave Pots']), - create_cave_region(world, player, 'Fairy Ascension Cave (Top)', 'a connector', None, + create_cave_region(multiworld, player, 'Fairy Ascension Cave (Top)', 'a connector', None, ['Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Cave Drop']), - create_lw_region(world, player, 'Fairy Ascension Ledge', None, + create_lw_region(multiworld, player, 'Fairy Ascension Ledge', None, ['Fairy Ascension Ledge Drop', 'Fairy Ascension Cave (Top)']), - create_lw_region(world, player, 'Death Mountain (Top)', ['Ether Tablet'], + create_lw_region(multiworld, player, 'Death Mountain (Top)', ['Ether Tablet'], ['East Death Mountain (Top)', 'Tower of Hera', 'Death Mountain Drop']), - create_lw_region(world, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']), - create_dungeon_region(world, player, 'Tower of Hera (Bottom)', 'Tower of Hera', + create_lw_region(multiworld, player, 'Spectacle Rock', ['Spectacle Rock'], ['Spectacle Rock Drop']), + 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(world, player, 'Tower of Hera (Basement)', 'Tower of Hera', + create_dungeon_region(multiworld, 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', + 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_dw_region(world, player, 'East Dark World', ['Pyramid'], + create_dw_region(multiworld, 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(world, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), - create_dw_region(world, player, 'Northeast Dark World', None, + create_dw_region(multiworld, player, 'Catfish', ['Catfish'], ['Catfish Exit Rock']), + create_dw_region(multiworld, 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(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'], + 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'], ['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(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, + 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, ['Lake Hylia Island Mirror Spot', 'East Dark World Pier', 'Dark Lake Hylia Ledge']), - create_dw_region(world, player, 'Dark Lake Hylia Central Island', None, + create_dw_region(multiworld, player, 'Dark Lake Hylia Central Island', None, ['Ice Palace', 'Lake Hylia Central Island Mirror Spot']), - create_dw_region(world, player, 'Dark Lake Hylia Ledge', None, + create_dw_region(multiworld, 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(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', + 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', ['Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), - create_dw_region(world, player, 'West Dark World', ['Frog'], + create_dw_region(multiworld, 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(world, player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']), - create_dw_region(world, player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], + 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'], ['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']), - create_dw_region(world, player, 'Bumper Cave Entrance', None, + create_dw_region(multiworld, player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']), - 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', + 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', ['Pyramid Fairy - Left', 'Pyramid Fairy - Right']), - 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, + 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, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), - create_dw_region(world, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], + create_dw_region(multiworld, player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot']), - create_dw_region(world, player, 'Skull Woods Forest', None, + create_dw_region(multiworld, 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(world, player, 'Skull Woods Forest (West)', None, + create_dw_region(multiworld, player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']), - create_dw_region(world, player, 'Dark Desert', None, + create_dw_region(multiworld, 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(world, player, 'Mire Shed', 'a cave with two chests', + create_cave_region(multiworld, player, 'Mire Shed', 'a cave with two chests', ['Mire Shed - Left', 'Mire Shed - Right']), - create_cave_region(world, player, 'Dark Desert Hint', 'a storyteller'), - create_dw_region(world, player, 'Dark Death Mountain (West Bottom)', None, + create_cave_region(multiworld, player, 'Dark Desert Hint', 'a storyteller'), + create_dw_region(multiworld, player, 'Dark Death Mountain (West Bottom)', None, ['Spike Cave', 'Spectacle Rock Mirror Spot', 'Dark Death Mountain Fairy']), - create_dw_region(world, player, 'Dark Death Mountain (Top)', None, + create_dw_region(multiworld, 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(world, player, 'Dark Death Mountain Ledge', None, + create_dw_region(multiworld, 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(world, player, 'Dark Death Mountain Isolated Ledge', None, + create_dw_region(multiworld, player, 'Dark Death Mountain Isolated Ledge', None, ['Isolated Ledge Mirror Spot', 'Turtle Rock Isolated Ledge Entrance']), - create_dw_region(world, player, 'Dark Death Mountain (East Bottom)', None, + create_dw_region(multiworld, player, 'Dark Death Mountain (East Bottom)', None, ['Superbunny Cave (Bottom)', 'Cave Shop (Dark Death Mountain)', 'Fairy Ascension Mirror Spot']), - create_cave_region(world, player, 'Superbunny Cave (Top)', 'a connector', + create_cave_region(multiworld, player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']), - create_cave_region(world, player, 'Superbunny Cave (Bottom)', 'a connector', None, + create_cave_region(multiworld, player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']), - create_cave_region(world, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), - create_cave_region(world, player, 'Hookshot Cave', 'a connector', + create_cave_region(multiworld, player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), + create_cave_region(multiworld, 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(world, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', + create_cave_region(multiworld, player, 'Hookshot Cave (Upper)', 'a connector', None, ['Hookshot Cave Exit (North)', 'Hookshot Cave Bomb Wall (North)']), - create_dw_region(world, player, 'Death Mountain Floating Island (Dark World)', None, + create_dw_region(multiworld, player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']), - 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_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_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', + 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', 'Swamp Palace - Trench 1 Pot Key'], ['Swamp Palace (Center)']), - create_dungeon_region(world, player, 'Swamp Palace (Center)', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest', 'Swamp Palace - Hookshot Pot Key', + create_dungeon_region(multiworld, 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(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', + 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', 'Swamp Palace - Waterway Pot Key', 'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss', 'Swamp Palace - Prize']), - create_dungeon_region(world, player, 'Thieves Town (Entrance)', 'Thieves\' Town', ['Thieves\' Town - Big Key Chest', + create_dungeon_region(multiworld, 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(world, player, 'Thieves Town (Deep)', 'Thieves\' Town', ['Thieves\' Town - Attic', + create_dungeon_region(multiworld, 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(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', + 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', 'Ice Palace - Many Pots Pot Key', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room'], ['Ice Palace (East)', 'Ice Palace (Kholdstare)']), - 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', + 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', '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(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', + 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', 'Turtle Rock - Roller Room - Right'], ['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']), - 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', + 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', '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(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'], + 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'], ['Palace of Darkness Big Key Chest Staircase', 'Palace of Darkness (North)', 'Palace of Darkness Big Key Door']), - 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'], + 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'], ['Palace of Darkness Spike Statue Room Door', 'Palace of Darkness Maze Door']), - 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', + 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', '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(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', + 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', '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(world, player, 'Ganons Tower (Hookshot Room)', 'Ganon\'s Tower', ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', + create_dungeon_region(multiworld, 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(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', + 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', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'], ['Ganons Tower (Bottom) (West)']), - 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', + 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', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest']), - 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', + 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', 'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Mini Helmasaur Key Drop'], ['Ganons Tower Moldorm Door']), - 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') + 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') ] -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_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_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_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_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_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_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_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_region(world: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None, +def _create_region(multiworld: MultiWorld, player: int, name: str, type: LTTPRegionType, hint: str, locations=None, exits=None): from .SubClasses import ALttPLocation - ret = LTTPRegion(name, type, hint, player, world) + ret = LTTPRegion(name, type, hint, player, multiworld) if exits: for exit in exits: ret.create_exit(exit) @@ -422,10 +422,10 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy return ret -def mark_light_world_regions(world, player: int): +def mark_light_world_regions(multiworld: MultiWorld, 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 world.get_regions(player) if region.type == LTTPRegionType.LightWorld) + queue = collections.deque(region for region in multiworld.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(world, player: int): seen.add(exit.connected_region) queue.append(exit.connected_region) - queue = collections.deque(region for region in world.get_regions(player) if region.type == LTTPRegionType.DarkWorld) + queue = collections.deque(region for region in multiworld.get_regions(player) if region.type == LTTPRegionType.DarkWorld) seen = set(queue) while queue: current = queue.popleft() diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 2f62b3768238..8acff214f24b 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -19,7 +19,7 @@ import threading import concurrent.futures import bsdiff4 -from typing import Collection, Optional, List, SupportsIndex +from typing import Collection, Optional, List, SupportsIndex, TYPE_CHECKING 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,6 +39,9 @@ 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 @@ -792,13 +795,13 @@ def get_nonnative_item_sprite(code: int) -> int: # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886 -def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): - local_random = world.worlds[player].random - local_world = world.worlds[player] +def patch_rom(multiworld: MultiWorld, rom: LocalRom, player: int, enemized: bool): + local_random = multiworld.worlds[player].random + local_world = multiworld.worlds[player] # patch items - for location in world.get_locations(player): + for location in multiworld.get_locations(player): if location.address is None or location.shop_slot is not None: continue @@ -852,7 +855,7 @@ def patch_rom(world: 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 world.get_regions(player): + for region in multiworld.get_regions(player): for exit in region.exits: if exit.target is not None: if isinstance(exit.addresses, tuple): @@ -885,7 +888,7 @@ def patch_rom(world: 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 world.shuffle_ganon: # fix ganons tower exit point + elif room_id == 0x000c and multiworld.shuffle_ganon: # fix ganons tower exit point rom.write_int16(0x15DB5 + 2 * offset, 0x00A4) else: rom.write_int16(0x15DB5 + 2 * offset, link_y) @@ -905,9 +908,9 @@ def patch_rom(world: 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(world, rom, player) + patch_shuffled_dark_sanc(multiworld, rom, player) - write_custom_shops(rom, world, player) + write_custom_shops(rom, multiworld, player) def credits_digit(num): # top: $54 is 1, 55 2, etc , so 57=4, 5C=9 @@ -981,11 +984,11 @@ def credits_digit(num): if local_world.options.mode in ['open', 'inverted']: rom.write_byte(0x180032, 0x01) # open mode if local_world.options.mode == 'inverted': - set_inverted_mode(world, player, rom) + set_inverted_mode(multiworld, player, rom) elif local_world.options.mode == 'standard': rom.write_byte(0x180032, 0x00) # standard mode - uncle_location = world.get_location('Link\'s Uncle', player) + uncle_location = multiworld.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']: @@ -1280,7 +1283,7 @@ def chunk(l, n): # set up goals for treasure hunt rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required - - sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))) + sum(1 for item in multiworld.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) @@ -1309,7 +1312,7 @@ def chunk(l, n): 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(world, player) else 0x00) # pre-open Pyramid Hole + rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(multiworld, 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 @@ -1327,7 +1330,7 @@ def chunk(l, n): starting_max_bombs = 0 if local_world.options.bombless_start else 10 starting_max_arrows = 30 - startingstate = CollectionState(world) + startingstate = CollectionState(multiworld) if startingstate.has('Silver Bow', player): equip[0x340] = 1 @@ -1375,7 +1378,7 @@ def chunk(l, n): equip[0x37B] = 1 equip[0x36E] = 0x80 - for item in world.precollected_items[player]: + for item in multiworld.precollected_items[player]: if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)', 'Titans Mitts', 'Power Glove', 'Progressive Glove', @@ -1590,7 +1593,7 @@ def chunk(l, n): } def get_reveal_bytes(itemName): - locations = world.find_item_locations(itemName, player) + locations = multiworld.find_item_locations(itemName, player) if len(locations) < 1: return 0x0000 location = locations[0] @@ -1667,7 +1670,7 @@ def get_reveal_bytes(itemName): rom.write_byte(0x18004C, 0x01) # set correct flag for hera basement item - hera_basement = world.get_location('Tower of Hera - Basement Cage', player) + hera_basement = multiworld.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: @@ -1684,27 +1687,26 @@ def get_reveal_bytes(itemName): rom.write_byte(0xFEE41, 0x2A) # bombable exit if local_world.options.tile_shuffle: - tile_set = TileSet.get_random_tile_set(world.worlds[player].random) + tile_set = TileSet.get_random_tile_set(multiworld.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, world, player) + write_strings(rom, multiworld, player) # remote items flag, does not currently work rom.write_byte(0x18637C, 0) # set rom name # 21 bytes - from Utils import __version__ - rom.name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21] + rom.name = bytearray(f'AP{local_world.world_version.as_simple_string().replace(".", "")[0:3]}_{player}_{multiworld.seed:11}\0', 'utf8')[:21] rom.name.extend([0] * (21 - len(rom.name))) rom.write_bytes(0x7FC0, rom.name) # set player names - encoded_players = world.players + len(world.groups) + encoded_players = multiworld.players + len(multiworld.groups) for p in range(1, min(encoded_players, ROM_PLAYER_LIMIT) + 1): - rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p])) + rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(multiworld.player_name[p])) if encoded_players > ROM_PLAYER_LIMIT: rom.write_bytes(0x195FFC + ((ROM_PLAYER_LIMIT - 1) * 32), hud_format_text("Archipelago")) @@ -1723,9 +1725,9 @@ def get_reveal_bytes(itemName): return rom -def patch_race_rom(rom, world, player): +def patch_race_rom(rom: LocalRom, multiworld: MultiWorld, player: int): rom.write_bytes(0x180213, [0x01, 0x00]) # Tournament Seed - rom.encrypt(world, player) + rom.encrypt(multiworld, player) def get_price_data(price: int, price_type: int) -> List[int]: @@ -1738,8 +1740,8 @@ def get_price_data(price: int, price_type: int) -> List[int]: return int16_as_bytes(price) -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) +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) shop_data = bytearray() items_data = bytearray() @@ -1758,9 +1760,9 @@ def write_custom_shops(rom, world, player): slot = 0 if shop.type == ShopType.TakeAny else index if item is None: break - 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) + 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) 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 @@ -1773,11 +1775,11 @@ def write_custom_shops(rom, world, player): 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 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']]) + 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']]) else: item_code = item_table[item["item"]].item_code - if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow: + if item['item'] == 'Single Arrow' and item['player'] == 0 and multiworld.worlds[player].options.retro_bow: rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask) item_data = [shop_id, item_code] + price_data + \ @@ -1790,12 +1792,12 @@ def write_custom_shops(rom, world, player): items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) rom.write_bytes(0x184900, items_data) - if world.worlds[player].options.retro_bow: + if multiworld.worlds[player].options.retro_bow: retro_shop_slots.append(0xFF) rom.write_bytes(0x186540, retro_shop_slots) -def hud_format_text(text): +def hud_format_text(text: str): output = bytes() for char in text.lower(): if 'a' <= char <= 'z': @@ -1812,7 +1814,7 @@ def hud_format_text(text): output += b'\x7f\x00' return output[:32] -def apply_oof_sfx(rom, oof: str): +def apply_oof_sfx(rom: LocalRom, oof: str): with open(oof, 'rb') as stream: oof_bytes = bytearray(stream.read()) @@ -1862,9 +1864,10 @@ def apply_oof_sfx(rom, oof: str): rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08]) -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): +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): local_random = random if not world else world.worlds[player].random disable_music: bool = not music # enable instant item menu @@ -1948,7 +1951,7 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri rom.write_byte(0x180167, triforce_flag) if z3pr: - def buildAndRandomize(option_name, mode): + def buildAndRandomize(option_name: str, mode: str): options = { option_name: True } @@ -2012,7 +2015,7 @@ def next_color_generator(): rom.write_crc() -def restore_maseya_colors(rom, offsets_array): +def restore_maseya_colors(rom: LocalRom, offsets_array: list[list[int]]): if not rom.orig_buffer: return for offsetC in offsets_array: @@ -2020,7 +2023,7 @@ def restore_maseya_colors(rom, offsets_array): rom.write_bytes(address, rom.orig_buffer[address:address + 2]) -def set_color(rom, address, color, shade): +def set_color(rom: LocalRom, address: int, color: tuple[int, int, int], shade: int): 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) @@ -2028,7 +2031,7 @@ def set_color(rom, address, color, shade): rom.write_bytes(address, ((b << 10) | (g << 5) | (r << 0)).to_bytes(2, byteorder='little', signed=False)) -def default_ow_palettes(rom): +def default_ow_palettes(rom: LocalRom): if not rom.orig_buffer: return rom.write_bytes(0xDE604, rom.orig_buffer[0xDE604:0xDEBB4]) @@ -2037,7 +2040,7 @@ def default_ow_palettes(rom): rom.write_bytes(address, rom.orig_buffer[address:address + 2]) -def randomize_ow_palettes(rom, local_random): +def randomize_ow_palettes(rom: LocalRom, local_random: random.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)] @@ -2113,7 +2116,7 @@ def randomize_ow_palettes(rom, local_random): set_color(rom, address, color, shade) -def blackout_ow_palettes(rom): +def blackout_ow_palettes(rom: LocalRom): rom.write_bytes(0xDE604, [0] * 0xC4) for i in range(0xDE6C8, 0xDE86C, 70): rom.write_bytes(i, [0] * 64) @@ -2124,13 +2127,13 @@ def blackout_ow_palettes(rom): rom.write_bytes(address, [0, 0]) -def default_uw_palettes(rom): +def default_uw_palettes(rom: LocalRom): if not rom.orig_buffer: return rom.write_bytes(0xDD734, rom.orig_buffer[0xDD734:0xDE544]) -def randomize_uw_palettes(rom, local_random): +def randomize_uw_palettes(rom: LocalRom, local_random: random.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)] @@ -2177,7 +2180,7 @@ def randomize_uw_palettes(rom, local_random): set_color(rom, 0x0DD796 + (0xB4 * dungeon), floor3, 4) -def blackout_uw_palettes(rom): +def blackout_uw_palettes(rom: LocalRom): for i in range(0xDD734, 0xDE544, 180): rom.write_bytes(i, [0] * 38) rom.write_bytes(i + 44, [0] * 76) @@ -2188,25 +2191,25 @@ def get_hash_string(hash): return ", ".join([hash_alphabet[code & 0x1F] for code in hash]) -def write_string_to_rom(rom, target, string): +def write_string_to_rom(rom: LocalRom, target: str, string: str): address, maxbytes = text_addresses[target] rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes)) -def write_strings(rom, world, player): +def write_strings(rom: LocalRom, multiworld: MultiWorld, player: int): from . import ALTTPWorld - local_random = world.worlds[player].random - w: ALTTPWorld = world.worlds[player] + local_random = multiworld.worlds[player].random + w: ALTTPWorld = multiworld.worlds[player] tt = TextTable() tt.removeUnwantedText() # Let's keep this guy's text accurate to the shuffle setting. - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: + if multiworld.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 world.worlds[player].options.mode == 'inverted': + if multiworld.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): @@ -2218,45 +2221,45 @@ def hint_text(dest, ped_hint=False): hint = dest.hint_text if dest.player != player: if ped_hint: - hint += f" for {world.player_name[dest.player]}!" + hint += f" for {multiworld.player_name[dest.player]}!" elif isinstance(dest, (Region, Location)): - hint += f" in {world.player_name[dest.player]}'s world" + hint += f" in {multiworld.player_name[dest.player]}'s world" else: - hint += f" for {world.player_name[dest.player]}" + hint += f" for {multiworld.player_name[dest.player]}" return hint - if world.worlds[player].options.scams.gives_king_zora_hint: + if multiworld.worlds[player].options.scams.gives_king_zora_hint: # Zora hint - zora_location = world.get_location("King Zora", player) + zora_location = multiworld.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 world.worlds[player].options.scams.gives_bottle_merchant_hint: + if multiworld.worlds[player].options.scams.gives_bottle_merchant_hint: # Bottle Vendor hint - vendor_location = world.get_location("Bottle Merchant", player) + vendor_location = multiworld.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 world.worlds[player].options.hints: - if world.worlds[player].options.hints.value >= 2: - if world.worlds[player].options.hints == "full": + if multiworld.worlds[player].options.hints: + if multiworld.worlds[player].options.hints.value >= 2: + if multiworld.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(world.get_entrances(player)) + all_entrances = list(multiworld.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 world.shuffle_ganon: - if world.worlds[player].options.mode == 'inverted': + if multiworld.shuffle_ganon: + if multiworld.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 world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']: + if multiworld.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( @@ -2266,9 +2269,9 @@ def hint_text(dest, ped_hint=False): break # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. entrances_to_hint.update(InconvenientOtherEntrances) - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: hint_count = 0 - elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']: + elif multiworld.worlds[player].options.entrance_shuffle in ['simple', 'restricted']: hint_count = 2 else: hint_count = 4 @@ -2285,31 +2288,31 @@ def hint_text(dest, ped_hint=False): # Next we handle hints for randomly selected other entrances, # curating the selection intelligently based on shuffle. - if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']: + if multiworld.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']: entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(DungeonEntrances) - if world.worlds[player].options.mode == 'inverted': + if multiworld.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 world.worlds[player].options.entrance_shuffle == 'restricted': + elif multiworld.worlds[player].options.entrance_shuffle == 'restricted': entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(OtherEntrances) - if world.worlds[player].options.mode == 'inverted': + if multiworld.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 world.worlds[player].options.entrance_shuffle != 'insanity': + if multiworld.worlds[player].options.entrance_shuffle != 'insanity': entrances_to_hint.update(InsanityEntrances) - if world.shuffle_ganon: - if world.worlds[player].options.mode == 'inverted': + if multiworld.shuffle_ganon: + if multiworld.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 world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', + hint_count = 4 if multiworld.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: @@ -2324,77 +2327,77 @@ def hint_text(dest, ped_hint=False): # 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 world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.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 world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', + hint_count = 3 if multiworld.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(world.get_location('Swamp Palace - West Chest', player).item) - second_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) + 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) else: - 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) + 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) 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(world.get_location('Misery Mire - Compass Chest', player).item) - second_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) + 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) else: - 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) + 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) 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( - world.get_location(location, player).item) + '.' + multiworld.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( - world.get_location(location, player).item) + '.' + multiworld.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( - world.get_location(location, player).item) + '.' + multiworld.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( - world.get_location(location, player).item) + '.' + multiworld.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( - world.get_location(location, player).item) + '.' + multiworld.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( - world.get_location(location, player).item) + '.' + multiworld.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( - world.get_location(location, player).item) + '.' + multiworld.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint else: - this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.' + this_hint = location + ' contains ' + hint_text(multiworld.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 world.worlds[player].options.small_key_shuffle.hints_useful: + if multiworld.worlds[player].options.small_key_shuffle.hints_useful: items_to_hint |= item_name_groups["Small Keys"] - if world.worlds[player].options.big_key_shuffle.hints_useful: + if multiworld.worlds[player].options.big_key_shuffle.hints_useful: items_to_hint |= item_name_groups["Big Keys"] - if world.worlds[player].options.hints == "full": + if multiworld.worlds[player].options.hints == "full": hint_count = len(hint_locations) # fill all remaining hint locations with Item hints. else: - hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full', + hint_count = 5 if multiworld.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 = world.find_items_in_locations(items_to_hint, player, True) + locations = multiworld.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. @@ -2414,15 +2417,15 @@ def hint_text(dest, ped_hint=False): # We still need the older hints of course. Those are done here. - silverarrows = world.find_item_locations('Silver Bow', player, True) + silverarrows = multiworld.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 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) + 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) local_random.shuffle(prog_bow_locs) found_bow = False found_bow_alt = False @@ -2437,34 +2440,34 @@ def hint_text(dest, ped_hint=False): silverarrow_hint = (' %s?' % hint_text(bow_loc).replace('Ganon\'s', 'my')) tt[target] = 'Did you find the silver arrows%s' % silverarrow_hint - crystal5 = world.find_item('Crystal 5', player) - crystal6 = world.find_item('Crystal 6', player) + crystal5 = multiworld.find_item('Crystal 5', player) + crystal6 = multiworld.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 = world.find_item('Green Pendant', player) + greenpendant = multiworld.find_item('Green Pendant', player) tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text - if world.worlds[player].options.crystals_needed_for_gt == 1: + if multiworld.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 {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.' + tt['sign_ganons_tower'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_gt} crystals to enter.' - if world.worlds[player].options.goal == 'bosses': + if multiworld.worlds[player].options.goal == 'bosses': tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.' - elif world.worlds[player].options.goal == 'ganon_pedestal': + elif multiworld.worlds[player].options.goal == 'ganon_pedestal': tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.' - elif world.worlds[player].options.goal == "ganon": - if world.worlds[player].options.crystals_needed_for_ganon == 1: + elif multiworld.worlds[player].options.goal == "ganon": + if multiworld.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 {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \ + tt['sign_ganon'] = f'You need {multiworld.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \ f'have beaten Agahnim atop Ganons Tower' else: - if world.worlds[player].options.crystals_needed_for_ganon == 1: + if multiworld.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 {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.' + tt['sign_ganon'] = f'You need {multiworld.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)] @@ -2475,12 +2478,12 @@ def hint_text(dest, ped_hint=False): 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 world.precollected_items[player] if item.name == "Triforce Piece")) + sum(1 for item in multiworld.precollected_items[player] if item.name == "Triforce Piece")) - if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']: + if multiworld.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 world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1: + if multiworld.worlds[player].options.goal == 'triforce_hunt' and multiworld.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!' @@ -2494,7 +2497,7 @@ def hint_text(dest, ped_hint=False): "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 world.worlds[player].options.goal in ['pedestal']: + elif multiworld.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!' @@ -2503,44 +2506,44 @@ def hint_text(dest, ped_hint=False): 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 world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1: + if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.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 world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: + elif multiworld.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 world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1: + if multiworld.worlds[player].options.goal == 'ganon_triforce_hunt' and multiworld.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 world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']: + elif multiworld.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 = world.get_location('Master Sword Pedestal', player).item + pedestalitem = multiworld.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 = world.get_location('Ether Tablet', player).item + etheritem = multiworld.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 = world.get_location('Bombos Tablet', player).item + bombositem = multiworld.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 world.worlds[player].options.mode == 'inverted': + if multiworld.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 world.worlds[player].options.plando_texts: + for at, text, _ in multiworld.worlds[player].options.plando_texts: if at not in tt: raise Exception(f"No text target \"{at}\" found.") @@ -2551,22 +2554,22 @@ def hint_text(dest, ped_hint=False): credits = Credits() - sickkiditem = world.get_location('Sick Kid', player).item + sickkiditem = multiworld.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 = world.get_location('King Zora', player).item + zoraitem = multiworld.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 = world.get_location('Potion Shop', player).item + magicshopitem = multiworld.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 = world.get_location('Flute Spot', player).item + fluteboyitem = multiworld.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] @@ -2595,7 +2598,7 @@ def hint_text(dest, ped_hint=False): rom.write_bytes(0x76CC0, [byte for p in pointers for byte in [p & 0xFF, p >> 8 & 0xFF]]) -def set_inverted_mode(world, player, rom): +def set_inverted_mode(multiworld: MultiWorld, player: int, rom: LocalRom): 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) @@ -2613,12 +2616,12 @@ def set_inverted_mode(world, player, rom): 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 world.worlds[player].options.entrance_shuffle == 'vanilla': + if multiworld.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 world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.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) @@ -2676,7 +2679,7 @@ def set_inverted_mode(world, player, rom): 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 world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']: + if multiworld.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]) @@ -2739,7 +2742,7 @@ def set_inverted_mode(world, player, rom): 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 world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.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) @@ -2776,10 +2779,10 @@ def set_inverted_mode(world, player, rom): 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 world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.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 world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if multiworld.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) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 18e2965d8c5a..bce75a157be7 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -1,9 +1,9 @@ import collections import logging -from typing import Iterator, Set +from typing import Callable, Iterator, Set, TYPE_CHECKING from Options import ItemsAccessibility -from BaseClasses import MultiWorld +from BaseClasses import CollectionState, Item, Location, MultiWorld, Region from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items) @@ -21,112 +21,116 @@ has_misery_mire_medallion, has_sword, has_turtle_rock_medallion, has_triforce_pieces, can_use_bombs, can_bomb_or_bonk, can_activate_crystal_switch, can_kill_standard_start) +from .SubClasses import ALttPLocation from .UnderworldGlitchRules import underworld_glitches_rules +if TYPE_CHECKING: + from . import ALTTPWorld -def set_rules(world): + +def set_rules(world: "ALTTPWorld"): player = world.player - world = world.multiworld - if world.worlds[player].options.glitches_required == 'no_logic': - if player == next(player_id for player_id in world.get_game_players("A Link to the Past") - if world.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time + multiworld = world.multiworld + if world.options.glitches_required == 'no_logic': + if player == next(player_id for player_id in multiworld.get_game_players("A Link to the Past") + if multiworld.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time logging.info( 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') - if world.players == 1: - for exit in world.get_region('Menu', player).exits: + if multiworld.players == 1: + for exit in multiworld.get_region('Menu', player).exits: exit.hide_path = True return else: # Set access rules according to max glitches for multiworld progression. # Set accessibility to none, and shuffle assuming the no logic players can always win - world.worlds[player].options.accessibility.value = ItemsAccessibility.option_minimal - world.worlds[player].options.progression_balancing.value = 0 + world.options.accessibility.value = ItemsAccessibility.option_minimal + world.options.progression_balancing.value = 0 else: - world.completion_condition[player] = lambda state: state.has('Triforce', player) + multiworld.completion_condition[player] = lambda state: state.has('Triforce', player) - dungeon_boss_rules(world, player) - global_rules(world, player) + dungeon_boss_rules(multiworld, player) + global_rules(multiworld, player) - if world.worlds[player].options.mode != 'inverted': - default_rules(world, player) + if world.options.mode != 'inverted': + default_rules(multiworld, player) - if world.worlds[player].options.mode == 'open': - open_rules(world, player) - elif world.worlds[player].options.mode == 'standard': - standard_rules(world, player) - elif world.worlds[player].options.mode == 'inverted': - open_rules(world, player) - inverted_rules(world, player) + if world.options.mode == 'open': + open_rules(multiworld, player) + elif world.options.mode == 'standard': + standard_rules(multiworld, player) + elif world.options.mode == 'inverted': + open_rules(multiworld, player) + inverted_rules(multiworld, player) else: - raise NotImplementedError(f'World state {world.worlds[player].options.mode} is not implemented yet') + raise NotImplementedError(f'World state {world.options.mode} is not implemented yet') - if world.worlds[player].options.glitches_required == 'no_glitches': - no_glitches_rules(world, player) - forbid_bomb_jump_requirements(world, player) - elif world.worlds[player].options.glitches_required == 'overworld_glitches': + if world.options.glitches_required == 'no_glitches': + no_glitches_rules(multiworld, player) + forbid_bomb_jump_requirements(multiworld, player) + elif world.options.glitches_required == 'overworld_glitches': # Initially setting no_glitches_rules to set the baseline rules for some # entrances. The overworld_glitches_rules set is primarily additive. - no_glitches_rules(world, player) - fake_flipper_rules(world, player) - overworld_glitches_rules(world, player) - forbid_bomb_jump_requirements(world, player) - elif world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']: - no_glitches_rules(world, player) - fake_flipper_rules(world, player) - overworld_glitches_rules(world, player) - underworld_glitches_rules(world, player) - bomb_jump_requirements(world, player) - elif world.worlds[player].options.glitches_required == 'minor_glitches': - no_glitches_rules(world, player) - fake_flipper_rules(world, player) - forbid_bomb_jump_requirements(world, player) + no_glitches_rules(multiworld, player) + fake_flipper_rules(multiworld, player) + overworld_glitches_rules(multiworld, player) + forbid_bomb_jump_requirements(multiworld, player) + elif world.options.glitches_required in ['hybrid_major_glitches', 'no_logic']: + no_glitches_rules(multiworld, player) + fake_flipper_rules(multiworld, player) + overworld_glitches_rules(multiworld, player) + underworld_glitches_rules(multiworld, player) + bomb_jump_requirements(multiworld, player) + elif world.options.glitches_required == 'minor_glitches': + no_glitches_rules(multiworld, player) + fake_flipper_rules(multiworld, player) + forbid_bomb_jump_requirements(multiworld, player) else: - raise NotImplementedError(f'Not implemented yet: Logic - {world.worlds[player].options.glitches_required}') + raise NotImplementedError(f'Not implemented yet: Logic - {world.options.glitches_required}') - if world.worlds[player].options.goal == 'bosses': + if world.options.goal == 'bosses': # require all bosses to beat ganon - add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player)) - elif world.worlds[player].options.goal == 'ganon': + add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player)) + elif world.options.goal == 'ganon': # require aga2 to beat ganon - add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) + add_rule(multiworld.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) - if world.worlds[player].options.mode != 'inverted': - set_big_bomb_rules(world, player) - if world.worlds[player].options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}: - path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') + if world.options.mode != 'inverted': + set_big_bomb_rules(multiworld, player) + if world.options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}: + path_to_courtyard = mirrorless_path_to_castle_courtyard(multiworld, player) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or') else: - set_inverted_big_bomb_rules(world, player) + set_inverted_big_bomb_rules(multiworld, player) # if swamp and dam have not been moved we require mirror for swamp palace # however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself. - if not world.worlds[player].swamp_patch_required and world.worlds[player].options.glitches_required not in ['hybrid_major_glitches', 'no_logic']: - add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) + if not multiworld.worlds[player].swamp_patch_required and world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic']: + add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) # GT Entrance may be required for Turtle Rock for OWG and < 7 required - ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.worlds[player].options.mode == 'inverted' else 'Ganons Tower', player) - if (world.worlds[player].options.crystals_needed_for_gt == 7 - and not (world.worlds[player].options.glitches_required + ganons_tower = multiworld.get_entrance('Inverted Ganons Tower' if world.options.mode == 'inverted' else 'Ganons Tower', player) + if (world.options.crystals_needed_for_gt == 7 + and not (world.options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic'] - and world.worlds[player].options.mode != 'inverted')): + and world.options.mode != 'inverted')): set_rule(ganons_tower, lambda state: False) - set_trock_key_rules(world, player) + set_trock_key_rules(multiworld, player) - set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_gt, player)) - if world.worlds[player].options.mode != 'inverted' and world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: - add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') + set_rule(ganons_tower, lambda state: has_crystals(state, world.options.crystals_needed_for_gt.value, player)) + if world.options.mode != 'inverted' and world.options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + add_rule(multiworld.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') - set_bunny_rules(world, player, world.worlds[player].options.mode == 'inverted') + set_bunny_rules(multiworld, player, world.options.mode == 'inverted') -def mirrorless_path_to_castle_courtyard(world, player): +def mirrorless_path_to_castle_courtyard(multiworld: MultiWorld, player: int): # If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch. # Only considering the secret passage for now (in non-insanity shuffle). Basically, if it's Ganon you need the master sword. - start = world.get_entrance('Hyrule Castle Secret Entrance Drop', player) - target = world.get_region('Hyrule Castle Courtyard', player) + start = multiworld.get_entrance('Hyrule Castle Secret Entrance Drop', player) + target = multiworld.get_region('Hyrule Castle Courtyard', player) seen = {start.parent_region, start.connected_region} queue = collections.deque([(start.connected_region, [])]) while queue: @@ -139,37 +143,37 @@ def mirrorless_path_to_castle_courtyard(world, player): else: queue.append((entrance.connected_region, new_path)) - raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_name(player)})") + raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({multiworld.get_player_name(player)})") -def set_defeat_dungeon_boss_rule(location): +def set_defeat_dungeon_boss_rule(location: ALttPLocation): # Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) -def set_always_allow(spot, rule): +def set_always_allow(spot: Location, rule: Callable[[CollectionState, Item], bool]): spot.always_allow = rule -def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False): - if world.worlds[player].options.dark_room_logic == "lamp": +def add_lamp_requirement(multiworld: MultiWorld, spot, player: int, has_accessible_torch: bool = False): + if multiworld.worlds[player].options.dark_room_logic == "lamp": add_rule(spot, lambda state: state.has('Lamp', player)) - elif world.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well + elif multiworld.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well if has_accessible_torch: add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player)) else: add_rule(spot, lambda state: state.has('Lamp', player)) - elif world.worlds[player].options.dark_room_logic == "none": + elif multiworld.worlds[player].options.dark_room_logic == "none": pass else: - raise ValueError(f"Unknown Dark Room Logic: {world.worlds[player].options.dark_room_logic}") + raise ValueError(f"Unknown Dark Room Logic: {multiworld.worlds[player].options.dark_room_logic}") non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - { "Small Key (Universal)"} -def dungeon_boss_rules(world, player): +def dungeon_boss_rules(multiworld: MultiWorld, player: int): boss_locations = { 'Agahnim 1', 'Tower of Hera - Boss', @@ -190,7 +194,7 @@ def dungeon_boss_rules(world, player): 'Palace of Darkness - Prize', } for location in boss_locations: - set_defeat_dungeon_boss_rule(world.get_location(location, player)) + set_defeat_dungeon_boss_rule(multiworld.get_location(location, player)) def global_rules(multiworld: MultiWorld, player: int): @@ -616,330 +620,332 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player)) -def default_rules(world, player): +def default_rules(multiworld: MultiWorld, player: int): """Default world rules when world state is not inverted.""" # overworld requirements - set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Light World Bomb Hut', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Light Hype Fairy', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Mini Moldorm Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Ice Rod Cave', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) # Caution: If king's grave is releaxed at all to account for reaching it via a two way cave's exit in insanity mode, then the bomb shop logic will need to be updated (that would involve create a small ledge-like Region for it) - set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) - set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes - set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player)) - set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player)) - - set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement - set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) - set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point - set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle - set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up - set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block - set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) - set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) - - set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player))) - set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player))) - set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player)) - set_rule(world.get_entrance('Palace of Darkness', player), lambda state: state.has('Moon Pearl', player)) # kiki needs pearl - set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival - set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up? - set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull - set_rule(world.get_entrance('Skull Woods First Section Hole (North)', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush - set_rule(world.get_entrance('Skull Woods Second Section Hole', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush - set_rule(world.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Cave 45 Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bombos Tablet Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Lake Hylia Island Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player)) - set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) - - set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) - set_rule(world.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - - set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - - set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Spiral Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Fairy Ascension Mirror Spot', player), lambda state: state.has('Magic Mirror', player) and state.has('Moon Pearl', player)) # need to lift flowers - set_rule(world.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling - set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) - - set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid.to_bool(world, player)) - - if world.worlds[player].options.swordless: - swordless_rules(world, player) - - -def inverted_rules(world, player): + set_rule(multiworld.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) + set_rule(multiworld.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes + set_rule(multiworld.get_location('Flute Spot', player), lambda state: state.has('Shovel', player)) + set_rule(multiworld.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player)) + + set_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement + set_rule(multiworld.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player)) + set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point + set_rule(multiworld.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(multiworld.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up + set_rule(multiworld.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block + set_rule(multiworld.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player)) + + set_rule(multiworld.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player))) + set_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player))) + set_rule(multiworld.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Palace of Darkness', player), lambda state: state.has('Moon Pearl', player)) # kiki needs pearl + set_rule(multiworld.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival + set_rule(multiworld.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up? + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull + set_rule(multiworld.get_entrance('Skull Woods First Section Hole (North)', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush + set_rule(multiworld.get_entrance('Skull Woods Second Section Hole', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot lift bush + set_rule(multiworld.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Cave 45 Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bombos Tablet Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Dark World Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Lake Hylia Island Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) + set_rule(multiworld.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) + + set_rule(multiworld.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod + set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) + set_rule(multiworld.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + + set_rule(multiworld.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + + set_rule(multiworld.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Spiral Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Fairy Ascension Mirror Spot', player), lambda state: state.has('Magic Mirror', player) and state.has('Moon Pearl', player)) # need to lift flowers + set_rule(multiworld.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling + set_rule(multiworld.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) + + set_rule(multiworld.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or multiworld.worlds[player].options.open_pyramid.to_bool(multiworld, player)) + + if multiworld.worlds[player].options.swordless: + swordless_rules(multiworld, player) + + +def inverted_rules(multiworld: MultiWorld, player: int): # s&q regions. - set_rule(world.get_entrance('Castle Ledge S&Q', player), lambda state: state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Castle Ledge S&Q', player), lambda state: state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player)) # overworld requirements - set_rule(world.get_location('Maze Race', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Secret Passage Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book - set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player)) - - set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal - set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith - set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player)) - set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player)) - set_rule(world.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player)) - set_rule(world.get_location('Mushroom', player), lambda state: state.has('Moon Pearl', player)) # need pearl to pick up bushes - set_rule(world.get_entrance('Bush Covered Lawn Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bush Covered Lawn Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bush Covered Lawn Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bomb Hut Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) - set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy - set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point - set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up - set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block - set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer - set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer - - set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player))) - set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player))) - set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player)) - set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up? - set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers - set_rule(world.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player)) - set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) - set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) - set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) - - set_rule(world.get_entrance('Hype Cave', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Brewery', player), lambda state: can_use_bombs(state, player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: can_use_bombs(state, player)) - - - set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player)) - set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) - - set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player)) - - set_rule(world.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - - set_rule(world.get_entrance('East Death Mountain Mirror Spot (Bottom)', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (East)', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) + set_rule(multiworld.get_location('Maze Race', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Mini Moldorm Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Ice Rod Cave', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Light Hype Fairy', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Secret Passage Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book + set_rule(multiworld.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player)) + + set_rule(multiworld.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal + set_rule(multiworld.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith + set_rule(multiworld.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player)) + set_rule(multiworld.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player)) + set_rule(multiworld.get_location('Mushroom', player), lambda state: state.has('Moon Pearl', player)) # need pearl to pick up bushes + set_rule(multiworld.get_entrance('Bush Covered Lawn Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bush Covered Lawn Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bush Covered Lawn Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bomb Hut Inner Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Light World Bomb Hut', player), lambda state: state.has('Moon Pearl', player) and can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy + set_rule(multiworld.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point + set_rule(multiworld.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up + set_rule(multiworld.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block + set_rule(multiworld.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer + set_rule(multiworld.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer + + set_rule(multiworld.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player))) + set_rule(multiworld.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player))) + set_rule(multiworld.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player)) + set_rule(multiworld.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up? + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers + set_rule(multiworld.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player)) + set_rule(multiworld.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player)) + set_rule(multiworld.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player)) + set_rule(multiworld.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player)) + + set_rule(multiworld.get_entrance('Hype Cave', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Brewery', player), lambda state: can_use_bombs(state, player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: can_use_bombs(state, player)) + + + set_rule(multiworld.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player)) + set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!) + + set_rule(multiworld.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player)) + + set_rule(multiworld.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + + set_rule(multiworld.get_entrance('East Death Mountain Mirror Spot (Bottom)', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark Death Mountain Ledge Mirror Spot (East)', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!) # new inverted spots - set_rule(world.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) - set_rule(world.get_entrance('Mire Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Death Mountain Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('East Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('West Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('South Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Catfish Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Potion Shop Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Shopping Mall Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Desert Palace North Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Graveyard Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Bomb Hut Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) - set_rule(world.get_entrance('Skull Woods Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) + set_rule(multiworld.get_entrance('Mire Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Death Mountain Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('East Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('West Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('South Dark World Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Catfish Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Potion Shop Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Shopping Mall Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Desert Palace North Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Graveyard Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Bomb Hut Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) + set_rule(multiworld.get_entrance('Skull Woods Mirror Spot', player), lambda state: state.has('Magic Mirror', player)) # inverted flute spots - set_rule(world.get_entrance('DDM Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('NEDW Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('WDW Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('SDW Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('EDW Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('DLHL Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('DD Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('EDDM Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player)) - set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('DDM Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('NEDW Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('WDW Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('SDW Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('EDW Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('DLHL Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('DD Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('EDDM Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player)) + set_rule(multiworld.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player)) + + set_rule(multiworld.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or multiworld.worlds[player].options.open_pyramid) - set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid) + if multiworld.worlds[player].options.swordless: + swordless_rules(multiworld, player) - if world.worlds[player].options.swordless: - swordless_rules(world, player) -def no_glitches_rules(world, player): +def no_glitches_rules(multiworld: MultiWorld, player: int): """""" - if world.worlds[player].options.mode == 'inverted': - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player))) - set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player)) + if multiworld.worlds[player].options.mode == 'inverted': + set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player))) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player)) else: - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player)) - set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to - set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - - add_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override - add_conditional_lamps(world, player) - -def fake_flipper_rules(world, player): - if world.worlds[player].options.mode == 'inverted': - set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: True) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: True) - set_rule(world.get_entrance('East Dark World Pier', player), lambda state: True) + set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to + set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + + add_rule(multiworld.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has('Hookshot', player)) + set_rule(multiworld.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override + add_conditional_lamps(multiworld, player) + + +def fake_flipper_rules(multiworld: MultiWorld, player: int): + if multiworld.worlds[player].options.mode == 'inverted': + set_rule(multiworld.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Northeast Light World Warp', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: True) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: True) + set_rule(multiworld.get_entrance('East Dark World Pier', player), lambda state: True) #qirn jump - set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: True) + set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: True) else: - set_rule(world.get_entrance('Zoras River', player), lambda state: True) - set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: True) - set_rule(world.get_entrance('Hobo Bridge', player), lambda state: True) - set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) - set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) - set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Zoras River', player), lambda state: True) + set_rule(multiworld.get_entrance('Lake Hylia Central Island Pier', player), lambda state: True) + set_rule(multiworld.get_entrance('Hobo Bridge', player), lambda state: True) + set_rule(multiworld.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Moon Pearl', player)) #qirn jump - set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player)) + set_rule(multiworld.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player)) -def bomb_jump_requirements(multiworld, player): +def bomb_jump_requirements(multiworld: MultiWorld, player: int): DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'] for location in DMs_room_chests: add_rule(multiworld.get_location(location, player), lambda state: can_use_bombs(state, player), combine="or") @@ -947,7 +953,7 @@ def bomb_jump_requirements(multiworld, player): set_rule(multiworld.get_entrance('Skull Woods First Section Bomb Jump', player), lambda state: can_use_bombs(state, player)) -def forbid_bomb_jump_requirements(multiworld, player): +def forbid_bomb_jump_requirements(multiworld: MultiWorld, player: int): DMs_room_chests = ['Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'] for location in DMs_room_chests: add_rule(multiworld.get_location(location, player), lambda state: state.has('Hookshot', player)) @@ -972,14 +978,15 @@ def forbid_bomb_jump_requirements(multiworld, player): 'Turtle Rock', 'Dark Death Mountain Ledge (West)'] -def check_is_dark_world(region): + +def check_is_dark_world(region: Region): for entrance in region.entrances: if entrance.name in DW_Entrances: return True return False -def add_conditional_lamps(multiworld, player): +def add_conditional_lamps(multiworld: MultiWorld, player: int): # Light cones in standard depend on which world we actually are in, not which one the location would normally be # We add Lamp requirements only to those locations which lie in the dark world (or everything if open local_world = multiworld.worlds[player] @@ -1030,7 +1037,7 @@ def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=Fal add_lamp_requirement(multiworld, local_world.get_entrance("Throne Room"), player) -def open_rules(world, player): +def open_rules(multiworld: MultiWorld, player: int): def basement_key_rule(state): if location_item_name(state, 'Sewers - Key Rat Key Drop', player) == ("Small Key (Hyrule Castle)", player): @@ -1038,96 +1045,95 @@ def basement_key_rule(state): else: return state._lttp_has_key("Small Key (Hyrule Castle)", player, 3) - set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), + set_rule(multiworld.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 2)) - set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1)) + set_rule(multiworld.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: basement_key_rule(state) and can_kill_most_things(state, player, 1)) - set_rule(world.get_location('Sewers - Key Rat Key Drop', player), + set_rule(multiworld.get_location('Sewers - Key Rat Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_most_things(state, player, 1)) - set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), + set_rule(multiworld.get_location('Hyrule Castle - Big Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and can_kill_most_things(state, player, 1)) - set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) and state.has('Big Key (Hyrule Castle)', player) - and (world.worlds[player].options.enemy_health in ("easy", "default") + and (multiworld.worlds[player].options.enemy_health in ("easy", "default") or can_kill_most_things(state, player, 1))) -def swordless_rules(world, player): - set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) - set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain +def swordless_rules(multiworld: MultiWorld, player: int): + set_rule(multiworld.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2)) + set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain - set_rule(world.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) - set_rule(world.get_location('Ice Palace - Compass Chest', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) - set_rule(world.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) + set_rule(multiworld.get_location('Ice Palace - Jelly Key Drop', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) + set_rule(multiworld.get_location('Ice Palace - Compass Chest', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) + set_rule(multiworld.get_entrance('Ice Palace (Second Section)', player), lambda state: (state.has('Fire Rod', player) or state.has('Bombos', player)) and state._lttp_has_key('Small Key (Ice Palace)', player)) - set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop + set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop - if world.worlds[player].options.mode != 'inverted': - set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle - set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) + if multiworld.worlds[player].options.mode != 'inverted': + set_rule(multiworld.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) + set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) else: # only need ddm access for aga tower in inverted - set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - set_rule(world.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) + set_rule(multiworld.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!) + set_rule(multiworld.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!) -def add_connection(parent_name, target_name, entrance_name, world, player): - parent = world.get_region(parent_name, player) - target = world.get_region(target_name, player) +def add_connection(parent_name: str, target_name: str, entrance_name: str, multiworld: MultiWorld, player: int): + parent = multiworld.get_region(parent_name, player) + target = multiworld.get_region(target_name, player) parent.connect(target, entrance_name) +def standard_rules(multiworld: MultiWorld, player: int): + add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', multiworld, player) + multiworld.get_entrance('Uncle S&Q', player).hide_path = True + set_rule(multiworld.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player)) + set_rule(multiworld.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(multiworld.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(multiworld.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) + set_rule(multiworld.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) -def standard_rules(world, player): - add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player) - world.get_entrance('Uncle S&Q', player).hide_path = True - set_rule(world.get_entrance('Throne Room', player), lambda state: state.can_reach('Hyrule Castle - Zelda\'s Chest', 'Location', player)) - set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player)) - - if world.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal: - set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), + if multiworld.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal: + set_rule(multiworld.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1) and can_kill_standard_start(state, player, 2)) - set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), + set_rule(multiworld.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1) and can_kill_standard_start(state, player, 1)) - set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player), + set_rule(multiworld.get_location('Hyrule Castle - Map Guard Key Drop', player), lambda state: can_kill_standard_start(state, player, 1)) - set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), + set_rule(multiworld.get_location('Hyrule Castle - Big Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)) - set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2) and state.has('Big Key (Hyrule Castle)', player) - and (world.worlds[player].options.enemy_health in ("easy", "default") + and (multiworld.worlds[player].options.enemy_health in ("easy", "default") or can_kill_standard_start(state, player, 1))) - set_rule(world.get_location('Sewers - Key Rat Key Drop', player), + set_rule(multiworld.get_location('Sewers - Key Rat Key Drop', player), lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3) and can_kill_standard_start(state, player, 1)) else: - set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), + set_rule(multiworld.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.has('Big Key (Hyrule Castle)', player)) -def toss_junk_item(world, player): +def toss_junk_item(multiworld: MultiWorld, player: int): items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)', 'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap', 'Rupees (300)', 'Nothing'] for item in items: - big20 = next((i for i in world.itempool if i.name == item and i.player == player), None) + big20 = next((i for i in multiworld.itempool if i.name == item and i.player == player), None) if big20: - world.itempool.remove(big20) + multiworld.itempool.remove(big20) return raise Exception("Unable to find a junk item to toss to make room for a TR small key") -def set_trock_key_rules(multiworld, player): +def set_trock_key_rules(multiworld: MultiWorld, player: int): # First set all relevant locked doors to impassible. for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']: set_rule(multiworld.get_entrance(entrance, player), lambda state: False) @@ -1228,9 +1234,9 @@ def tr_big_key_chest_keys_needed(state): and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player))) -def set_big_bomb_rules(world, player): +def set_big_bomb_rules(multiworld: MultiWorld, player: int): # this is a mess - bombshop_entrance = world.get_region('Big Bomb Shop', player).entrances[0] + bombshop_entrance = multiworld.get_region('Big Bomb Shop', player).entrances[0] Normal_LW_entrances = ['Blinds Hideout', 'Bonk Fairy (Light)', 'Lake Hylia Fairy', @@ -1345,7 +1351,7 @@ def set_big_bomb_rules(world, player): 'Desert Palace Entrance (South)', 'Checkerboard Cave'] - set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player)) + set_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player)) #crossing peg bridge starting from the southern dark world def cross_peg_bridge(state): @@ -1372,96 +1378,96 @@ def basic_routes(state): #1. basic routes #2. Can reach Eastern dark world some other way, mirror, get bomb, return to mirror spot, walk to pyramid: Needs mirror # -> M or BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: basic_routes(state) or state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: basic_routes(state) or state.has('Magic Mirror', player)) elif bombshop_entrance.name in LW_walkable_entrances: #1. Mirror then basic routes # -> M and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and basic_routes(state)) elif bombshop_entrance.name in Northern_DW_entrances: #1. Mirror and basic routes #2. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (M and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': #1. Mirror and Lift rock and basic_routes #2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case) #3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl # -> (Mitts and CPB) or (((G or Flute) and M) and BR)) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state))) elif bombshop_entrance.name in Southern_DW_entrances: #1. Mirror and enter via gate: Need mirror and Aga1 #2. cross peg bridge: Need hammer and moon pearl # -> CPB or (M and A) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has('Magic Mirror', player) and state.has('Beat Agahnim 1', player))) elif bombshop_entrance.name in Isolated_DW_entrances: # 1. mirror then flute then basic routes # -> M and Flute and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and state.has('Activated Flute', player) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and state.has('Activated Flute', player) and basic_routes(state)) elif bombshop_entrance.name in Isolated_LW_entrances: # 1. flute then basic routes # Prexisting mirror spot is not permitted, because mirror might have been needed to reach these isolated locations. # -> Flute and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and basic_routes(state)) elif bombshop_entrance.name in West_LW_DM_entrances: # 1. flute then basic routes or mirror # Prexisting mirror spot is permitted, because flute can be used to reach west DM directly. # -> Flute and (M or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and (state.has('Magic Mirror', player) or basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and (state.has('Magic Mirror', player) or basic_routes(state))) elif bombshop_entrance.name in East_LW_DM_entrances: # 1. flute then basic routes or mirror and hookshot # Prexisting mirror spot is permitted, because flute can be used to reach west DM directly and then east DM via Hookshot # -> Flute and ((M and Hookshot) or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player)) or basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player)) or basic_routes(state))) elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)': # Same as East_LW_DM_entrances except navigation without BR requires Mitts # -> Flute and ((M and Hookshot and Mitts) or BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state))) elif bombshop_entrance.name in Castle_ledge_entrances: # 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror # 2. flute then basic routes # -> M or (Flute and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) or (state.has('Activated Flute', player) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) or (state.has('Activated Flute', player) and basic_routes(state))) elif bombshop_entrance.name in Desert_mirrorable_ledge_entrances: # Cases when you have mire access: Mirror to reach locations, return via mirror spot, move to center of desert, mirror anagin and: # 1. Have mire access, Mirror to reach locations, return via mirror spot, move to center of desert, mirror again and then basic routes # 2. flute then basic routes # -> (Mire access and M) or Flute) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: ((state.can_reach('Dark Desert', 'Region', player) and state.has('Magic Mirror', player)) or state.has('Activated Flute', player)) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: ((state.can_reach('Dark Desert', 'Region', player) and state.has('Magic Mirror', player)) or state.has('Activated Flute', player)) and basic_routes(state)) elif bombshop_entrance.name == 'Old Man Cave (West)': # 1. Lift rock then basic_routes # 2. flute then basic_routes # -> (Flute or G) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state)) elif bombshop_entrance.name == 'Graveyard Cave': # 1. flute then basic routes # 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge # -> (Flute or (M and P and West Dark World access) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)) elif bombshop_entrance.name in Mirror_from_SDW_entrances: # 1. flute then basic routes # 2. (has South dark world access) use existing mirror spot, mirror again off ledge # -> (Flute or (M and South Dark World access) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('South Dark World', 'Region', player) and state.has('Magic Mirror', player))) and basic_routes(state)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or (state.can_reach('South Dark World', 'Region', player) and state.has('Magic Mirror', player))) and basic_routes(state)) elif bombshop_entrance.name == 'Dark World Potion Shop': # 1. walk down by lifting rock: needs gloves and pearl` # 2. walk down by hammering peg: needs hammer and pearl # 3. mirror and basic routes # -> (P and (H or Gloves)) or (M and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state))) elif bombshop_entrance.name == 'Kings Grave': # same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts # (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot) # to account for insanity, must consider a way to escape without a cave for basic_routes # -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR) - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state))) elif bombshop_entrance.name == 'Waterfall of Wishing': # same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which # means you need an escape route of either Flippers or Flute - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and (basic_routes(state) or state.has('Magic Mirror', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and (basic_routes(state) or state.has('Magic Mirror', player))) -def set_inverted_big_bomb_rules(world, player): - bombshop_entrance = world.get_region('Inverted Big Bomb Shop', player).entrances[0] +def set_inverted_big_bomb_rules(multiworld: MultiWorld, player: int): + bombshop_entrance = multiworld.get_region('Inverted Big Bomb Shop', player).entrances[0] Normal_LW_entrances = ['Blinds Hideout', 'Bonk Fairy (Light)', 'Lake Hylia Fairy', @@ -1579,7 +1585,7 @@ def set_inverted_big_bomb_rules(world, player): 'Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)'] - set_rule(world.get_entrance('Pyramid Fairy', player), + set_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Inverted Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player)) # Key for below abbreviations: @@ -1593,64 +1599,64 @@ def set_inverted_big_bomb_rules(world, player): pass elif bombshop_entrance.name in Normal_LW_entrances: # Just walk to the castle and mirror. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player)) elif bombshop_entrance.name in Isolated_LW_entrances: # For these entrances, you cannot walk to the castle/pyramid and thus must use Mirror and then Flute. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and state.has('Magic Mirror', player)) elif bombshop_entrance.name in Northern_DW_entrances: # You can just fly with the Flute, you can take a long walk with Mitts and Hammer, # or you can leave a Mirror portal nearby and then walk to the castle to Mirror again. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in Southern_DW_entrances: # This is the same as north DW without the Mitts rock present. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in Isolated_DW_entrances: # There's just no way to escape these places with the bomb and no Flute. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player)) elif bombshop_entrance.name in LW_walkable_entrances: # You can fly with the flute, or leave a mirror portal and walk through the light world - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name in LW_bush_entrances: # These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)))) elif bombshop_entrance.name == 'Village of Outcasts Shop': # This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': # This is mostly the same as NDW but the Mirror path requires being able to lift a rock. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player))) elif bombshop_entrance.name == 'Old Man Cave (West)': # The three paths back are Mirror and DW walk, Mirror and Flute, or LW walk and then Mirror. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player))) elif bombshop_entrance.name == 'Dark World Potion Shop': # You either need to Flute to 5 or cross the rock/hammer choice pass to the south. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player)) elif bombshop_entrance.name == 'Kings Grave': # Either lift the rock and walk to the castle to Mirror or Mirror immediately and Flute. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player)) elif bombshop_entrance.name == 'Waterfall of Wishing': # You absolutely must be able to swim to return it from here. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player)) elif bombshop_entrance.name == 'Ice Palace': # You can swim to the dock or use the Flute to get off the island. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) or state.has('Activated Flute', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) or state.has('Activated Flute', player)) elif bombshop_entrance.name == 'Capacity Upgrade': # You must Mirror but then can use either Ice Palace return path. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Flippers', player) or state.has('Activated Flute', player)) and state.has('Magic Mirror', player)) elif bombshop_entrance.name == 'Two Brothers House (West)': # First you must Mirror. Then you can either Flute, cross the peg bridge, or use the Agah 1 portal to Mirror again. - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) and state.has('Magic Mirror', player)) + add_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) and state.has('Magic Mirror', player)) elif bombshop_entrance.name in LW_inaccessible_entrances: # You can't get to the pyramid from these entrances without bomb duping. raise Exception('No valid path to open Pyramid Fairy. (Could not route from %s)' % bombshop_entrance.name) elif bombshop_entrance.name == 'Pyramid Fairy': # Self locking. The shuffles don't put the bomb shop here, but doesn't lock anything important. - set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) + set_rule(multiworld.get_entrance('Pyramid Fairy', player), lambda state: False) else: raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) -def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): +def set_bunny_rules(multiworld: MultiWorld, player: int, inverted: bool): # regions for the exits of multi-entrance caves/drops that bunny cannot pass # Note spiral cave and two brothers house are passable in superbunny state for glitch logic with extra requirements. @@ -1690,7 +1696,7 @@ def is_link(region): def get_rule_to_add(region, location = None, connecting_entrance = None): # In OWG, a location can potentially be superbunny-mirror accessible or # bunny revival accessible. - if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']: + if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']: if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic return lambda state: state.has('Moon Pearl', player) if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch @@ -1730,7 +1736,7 @@ def get_rule_to_add(region, location = None, connecting_entrance = None): seen.add(new_region) if not is_link(new_region): # For glitch rulesets, establish superbunny and revival rules. - if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): + if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions(): possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player)) elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions() @@ -1753,29 +1759,29 @@ def get_rule_to_add(region, location = None, connecting_entrance = None): return options_to_access_rule(possible_options) # Add requirements for bunny-impassible caves if link is a bunny in them - for region in (world.get_region(name, player) for name in bunny_impassable_caves): + for region in (multiworld.get_region(name, player) for name in bunny_impassable_caves): if not is_bunny(region): continue rule = get_rule_to_add(region) for region_exit in region.exits: add_rule(region_exit, rule) - paradox_shop = world.get_region('Light World Death Mountain Shop', player) + paradox_shop = multiworld.get_region('Light World Death Mountain Shop', player) if is_bunny(paradox_shop): add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop)) # Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival - for entrance in world.get_entrances(player): + for entrance in multiworld.get_entrances(player): if is_bunny(entrance.connected_region): - if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] : + if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] : if entrance.connected_region.type == LTTPRegionType.Dungeon: if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance)) continue if entrance.connected_region.name == 'Turtle Rock (Entrance)': - add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance)) + add_rule(multiworld.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance)) for location in entrance.connected_region.locations: - if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): + if multiworld.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances(): continue if location.name in bunny_accessible_locations: continue diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 89e43a1a041a..182f25ddc288 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -7,7 +7,7 @@ from worlds.generic.Rules import add_rule -from BaseClasses import CollectionState +from BaseClasses import CollectionState, Item, MultiWorld from .SubClasses import ALttPLocation from .Items import item_name_groups @@ -159,7 +159,7 @@ class UpgradeShop(Shop): ShopType.TakeAny: TakeAny} -def push_shop_inventories(multiworld): +def push_shop_inventories(multiworld: 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): world.pushed_shop_inventories.set() -def create_shops(multiworld, player: int): +def create_shops(multiworld: 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 @@ class ShopData(NamedTuple): } -def set_up_shops(multiworld, player: int): +def set_up_shops(multiworld: 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 @@ def set_up_shops(multiworld, player: int): } -def get_price_modifier(item) -> float: +def get_price_modifier(item: 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) -> float: return 0.25 -def get_price(multiworld, item, player: int, price_type=None): +def get_price(multiworld: MultiWorld, item: 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: diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 25511f320d4d..1b606a23af3b 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -1,3 +1,4 @@ +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 @@ -5,27 +6,27 @@ # 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(world, player): - specrock = world.get_region('Spectacle Rock Cave (Bottom)', player) - mire = world.get_region('Misery Mire (West)', player) +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) 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 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) + 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) else: - 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)) + 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)) # 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, player): +def fake_pearl_state(state: CollectionState, player: int): if state.has('Moon Pearl', player): return state fake_state = state.copy() @@ -35,11 +36,11 @@ def fake_pearl_state(state, player): # 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(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 +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 - dungeon_entrance = [r for r in world.get_region(dungeon_region, player).entrances if r.name != clip.name][0] + dungeon_entrance = [r for r in multiworld.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': @@ -49,64 +50,64 @@ def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str 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(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) + add_rule(multiworld.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(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state)) + add_rule(multiworld.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(world, player): +def underworld_glitches_rules(multiworld: MultiWorld, player: int): # 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(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') + 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') # Kiki Skip - kikiskip = world.get_entrance('Kiki Skip', player) + kikiskip = multiworld.get_entrance('Kiki Skip', player) set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player)) - dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') + dungeon_reentry_rules(multiworld, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit') # Mire -> Hera -> Swamp # Using mire keys on other dungeon doors - mire = world.get_region('Misery Mire (West)', player) + mire = multiworld.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, 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') + 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') # 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 world.worlds[player].swamp_patch_required: - if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']: + if not multiworld.worlds[player].swamp_patch_required: + if multiworld.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 = world.worlds[player].options.mode == 'inverted' + inverted = multiworld.worlds[player].options.mode == 'inverted' hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \ - rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state) + rule_map.get(multiworld.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(world.get_entrance(('Ganons Tower' if not inverted else 'Inverted Ganons Tower'), player).connected_region.name, lambda state: False)(state) + rule_map.get(multiworld.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(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state)) + add_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player) or mirrorless_moat_rule(state)) else: - add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player)) + add_rule(multiworld.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 = world.get_entrance('Mire to Hera Clip', player) - mire_to_swamp = world.get_entrance('Hera to Swamp Clip', player) + mire_to_hera = multiworld.get_entrance('Mire to Hera Clip', player) + mire_to_swamp = multiworld.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(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') + 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') diff --git a/worlds/alttp/archipelago.json b/worlds/alttp/archipelago.json new file mode 100644 index 000000000000..19bbc0321354 --- /dev/null +++ b/worlds/alttp/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "A Link to the Past", + "minimum_ap_version": "0.6.6", + "world_version": "5.1.0", + "authors": ["Berserker"] +} diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py index b4e45fb57791..569a0348d29f 100644 --- a/worlds/dark_souls_3/Locations.py +++ b/worlds/dark_souls_3/Locations.py @@ -2025,13 +2025,13 @@ def __init__( 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, drop onto roof", "Red Tearstone Ring"), + DS3LocationData("LC: Red Tearstone Ring - chapel, balcony before drop", "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", "Hood of Prayer"), + DS3LocationData("LC: Hood of Prayer - ascent, chest at beginning", "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", diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 8c9716c03d5a..4ee972be9cb2 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -6,6 +6,7 @@ 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 @@ -1473,6 +1474,7 @@ def smooth_items(item_order: List[Union[DS3ItemData, DarkSouls3Item]]) -> None: 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] @@ -1480,12 +1482,12 @@ def smooth_items(item_order: List[Union[DS3ItemData, DarkSouls3Item]]) -> None: 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 - for location in onworld + offworld: - new_item = ds3_world._pop_item(location, converted_item_order) - location.item = new_item - new_item.location = location + 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) if ds3_world.options.smooth_upgrade_items: base_names = { @@ -1518,19 +1520,6 @@ def _shuffle(self, seq: Sequence) -> List: 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)) diff --git a/worlds/ff1/archipelago.json b/worlds/ff1/archipelago.json new file mode 100644 index 000000000000..e3cf746a3b32 --- /dev/null +++ b/worlds/ff1/archipelago.json @@ -0,0 +1,5 @@ +{ + "game": "Final Fantasy", + "world_version": "1.0.0", + "authors": ["Rosalie"] +} \ No newline at end of file diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index 69f59c739eda..625601d96b38 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -216,6 +216,28 @@ 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 diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 3b5a6e7e69cb..4b423d255a74 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1281,7 +1281,7 @@ LocationName.HadesCupTrophyParadoxCups, LocationName.MusicalOrichalcumPlus, ], - "HitlistCasual": { + "HitlistCasual": [ LocationName.FuturePete, LocationName.BetwixtandBetweenBondofFlame, LocationName.GrimReaper2, @@ -1299,7 +1299,7 @@ LocationName.MCP, LocationName.Lvl50, LocationName.Lvl99 - }, + ], "Cups": { LocationName.ProtectBeltPainandPanicCup, LocationName.SerenityGemPainandPanicCup, diff --git a/worlds/lingo/archipelago.json b/worlds/lingo/archipelago.json new file mode 100644 index 000000000000..d065ab5c0b32 --- /dev/null +++ b/worlds/lingo/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Lingo", + "authors": ["hatkirby"], + "minimum_ap_version": "0.6.3", + "world_version": "5.0.0" +} diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 4c41f3236f62..613eedc4b655 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -4470,6 +4470,10 @@ 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 @@ -4612,7 +4616,7 @@ enter_only: True orientation: east required_door: - door: Eights + door: First Eight progression: Progressive Number Hunt: panel_doors: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index f5eb3e069927..dff56b10d846 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 88a0cec2caad..d1b672db7d8c 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,7 +1,8 @@ import logging from typing import Any, ClassVar, TextIO -from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial +from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial, \ + PlandoOptions from Options import Accessibility from Utils import output_path from settings import FilePath, Group @@ -18,6 +19,7 @@ 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( @@ -151,6 +153,10 @@ 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 @@ -188,6 +194,11 @@ def generate_early(self) -> None: 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 @@ -279,6 +290,16 @@ def set_rules(self) -> None: 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 @@ -295,7 +316,7 @@ def connect_entrances(self) -> None: raise RuntimeError("Unable to generate valid portal output.") if self.options.shuffle_transitions: - shuffle_transitions(self) + shuffle_transitions(self, keep_entrance_logic) def write_spoiler_header(self, spoiler_handle: TextIO) -> None: if self.options.available_portals < 6: @@ -463,7 +484,7 @@ def stage_generate_output(cls, multiworld: MultiWorld, output_directory: str) -> "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) diff --git a/worlds/messenger/archipelago.json b/worlds/messenger/archipelago.json new file mode 100644 index 000000000000..86aefc42a84e --- /dev/null +++ b/worlds/messenger/archipelago.json @@ -0,0 +1,4 @@ +{ + "game": "The Messenger", + "authors": ["alwaysintreble"] +} \ No newline at end of file diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 3ef1df75cc13..02fd299a6c2f 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -28,6 +28,8 @@ 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") @@ -190,7 +192,7 @@ def after_mod_update_popup(answer: str) -> None: def launch(answer: str | None = None) -> None: """Launch the game.""" - nonlocal args + nonlocal args, prompt if prompt: prompt.dismiss() @@ -256,3 +258,5 @@ def launch(answer: str | None = None) -> 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() diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 2d5ee1b8a976..7f17232cfbf8 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING -from BaseClasses import CollectionState -from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items +from BaseClasses import CollectionState, CollectionRule +from worlds.generic.Rules import add_rule, allow_self_locking_items from .constants import NOTES, PHOBEKINS from .options import MessengerAccessibility diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index 2e438fdbfdc7..8beed4302719 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -1,8 +1,7 @@ from functools import cached_property from typing import TYPE_CHECKING -from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region -from entrance_rando import ERPlacementState +from BaseClasses import CollectionState, Item, ItemClassification, Location, Region from .regions import LOCATIONS, MEGA_SHARDS from .shop import FIGURINES, SHOP_ITEMS diff --git a/worlds/messenger/transitions.py b/worlds/messenger/transitions.py index c0ae64c5489e..39ad591bf2d7 100644 --- a/worlds/messenger/transitions.py +++ b/worlds/messenger/transitions.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from BaseClasses import Entrance, Region +from BaseClasses import Region, CollectionRule from entrance_rando import EntranceType, randomize_entrances from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS from .options import ShuffleTransitions, TransitionPlando @@ -26,7 +26,6 @@ def disconnect_entrance() -> 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") @@ -36,8 +35,9 @@ def disconnect_entrance() -> None: entrance = world.get_entrance(f"{parent} -> {child}") disconnect_entrance() -def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None: - def remove_dangling_exit(region: Region) -> None: + +def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando, keep_logic: bool = False) -> None: + def remove_dangling_exit(region: Region) -> CollectionRule: # find the disconnected exit and remove references to it for _exit in region.exits: if not _exit.connected_region: @@ -45,6 +45,7 @@ def remove_dangling_exit(region: Region) -> None: 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 @@ -65,30 +66,35 @@ def remove_dangling_entrance(region: Region) -> None: 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) - remove_dangling_exit(reg1) - + access_rule = remove_dangling_exit(reg1) + reg2 = world.get_region(plando_connection.exit) remove_dangling_entrance(reg2) # connect the regions - reg1.connect(reg2) + new_exit1 = reg1.connect(reg2) + if keep_logic: + new_exit1.access_rule = access_rule # 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): - remove_dangling_exit(reg2) + access_rule = remove_dangling_exit(reg2) remove_dangling_entrance(reg1) - reg2.connect(reg1) + new_exit2 = reg2.connect(reg1) + if keep_logic: + new_exit2.access_rule = access_rule -def shuffle_transitions(world: "MessengerWorld") -> None: +def shuffle_transitions(world: "MessengerWorld", keep_logic: bool = False) -> None: coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled plando = world.options.plando_connections if plando: - connect_plando(world, plando) + connect_plando(world, plando, keep_logic) result = randomize_entrances(world, coupled, {0: [0]}) diff --git a/worlds/messenger/universal_tracker.py b/worlds/messenger/universal_tracker.py new file mode 100644 index 000000000000..9d752031bf62 --- /dev/null +++ b/worlds/messenger/universal_tracker.py @@ -0,0 +1,41 @@ +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 diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py index 7944442b2a36..0334cc5abba9 100644 --- a/worlds/mlss/Client.py +++ b/worlds/mlss/Client.py @@ -1,11 +1,11 @@ -from typing import TYPE_CHECKING, Optional, Set, List, Dict +import asyncio 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,8 +41,6 @@ def __init__(self) -> None: 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")]) @@ -72,20 +70,15 @@ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: 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.seed_name is None: + if ctx.server_seed_name is None: return if not self.seed_verify: - seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")]) + seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.server_seed_name), "ROM")]) seed = seed[0].decode("UTF-8") - if seed not in ctx.seed_name: + if seed not in ctx.server_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, " diff --git a/worlds/mm2/client.py b/worlds/mm2/client.py index 96c477757dcd..725023c82066 100644 --- a/worlds/mm2/client.py +++ b/worlds/mm2/client.py @@ -140,8 +140,8 @@ def cmd_pool(self: "BizHawkClientCommandProcessor") -> None: def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None: - from worlds._bizhawk.context import BizHawkClientContext """Request a refill from EnergyLink.""" + from worlds._bizhawk.context import BizHawkClientContext if self.ctx.game != "Mega Man 2": logger.warning("This command can only be used when playing Mega Man 2.") return diff --git a/worlds/mm3/.apignore b/worlds/mm3/.apignore new file mode 100644 index 000000000000..4ae3da2695a9 --- /dev/null +++ b/worlds/mm3/.apignore @@ -0,0 +1 @@ +/src/* diff --git a/worlds/mm3/__init__.py b/worlds/mm3/__init__.py new file mode 100644 index 000000000000..5b349bc9c364 --- /dev/null +++ b/worlds/mm3/__init__.py @@ -0,0 +1,275 @@ +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] diff --git a/worlds/mm3/archipelago.json b/worlds/mm3/archipelago.json new file mode 100644 index 000000000000..ed5ecffc6cbe --- /dev/null +++ b/worlds/mm3/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Mega Man 3", + "authors": ["Silvris"], + "world_version": "0.1.7", + "minimum_ap_version": "0.6.4" +} diff --git a/worlds/mm3/client.py b/worlds/mm3/client.py new file mode 100644 index 000000000000..0e069043a7d9 --- /dev/null +++ b/worlds/mm3/client.py @@ -0,0 +1,783 @@ +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]}]) diff --git a/worlds/mm3/color.py b/worlds/mm3/color.py new file mode 100644 index 000000000000..094402643216 --- /dev/null +++ b/worlds/mm3/color.py @@ -0,0 +1,331 @@ +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])) diff --git a/worlds/mm3/data/mm3_basepatch.bsdiff4 b/worlds/mm3/data/mm3_basepatch.bsdiff4 new file mode 100644 index 000000000000..f80cb76d67cf Binary files /dev/null and b/worlds/mm3/data/mm3_basepatch.bsdiff4 differ diff --git a/worlds/mm3/docs/en_Mega Man 3.md b/worlds/mm3/docs/en_Mega Man 3.md new file mode 100644 index 000000000000..abb619858c48 --- /dev/null +++ b/worlds/mm3/docs/en_Mega Man 3.md @@ -0,0 +1,131 @@ +# 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 ` 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 \ No newline at end of file diff --git a/worlds/mm3/docs/setup_en.md b/worlds/mm3/docs/setup_en.md new file mode 100644 index 000000000000..07cae74a8a08 --- /dev/null +++ b/worlds/mm3/docs/setup_en.md @@ -0,0 +1,53 @@ +# 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. diff --git a/worlds/mm3/items.py b/worlds/mm3/items.py new file mode 100644 index 000000000000..40e6114fffc1 --- /dev/null +++ b/worlds/mm3/items.py @@ -0,0 +1,80 @@ +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} diff --git a/worlds/mm3/locations.py b/worlds/mm3/locations.py new file mode 100644 index 000000000000..2504236bda83 --- /dev/null +++ b/worlds/mm3/locations.py @@ -0,0 +1,312 @@ +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} diff --git a/worlds/mm3/names.py b/worlds/mm3/names.py new file mode 100644 index 000000000000..dfad7526766c --- /dev/null +++ b/worlds/mm3/names.py @@ -0,0 +1,221 @@ +# 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" diff --git a/worlds/mm3/options.py b/worlds/mm3/options.py new file mode 100644 index 000000000000..a1e9b24834f3 --- /dev/null +++ b/worlds/mm3/options.py @@ -0,0 +1,164 @@ +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 diff --git a/worlds/mm3/rom.py b/worlds/mm3/rom.py new file mode 100644 index 000000000000..8803f38cc545 --- /dev/null +++ b/worlds/mm3/rom.py @@ -0,0 +1,374 @@ +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) diff --git a/worlds/mm3/rules.py b/worlds/mm3/rules.py new file mode 100644 index 000000000000..b43908f42c82 --- /dev/null +++ b/worlds/mm3/rules.py @@ -0,0 +1,388 @@ +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)) diff --git a/worlds/mm3/src/__init__.py b/worlds/mm3/src/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/mm3/src/mm3_basepatch.asm b/worlds/mm3/src/mm3_basepatch.asm new file mode 100644 index 000000000000..16e0567ff5a3 --- /dev/null +++ b/worlds/mm3/src/mm3_basepatch.asm @@ -0,0 +1,781 @@ +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 == $3E + org
-$C000+($2000*)+!headersize ; org sets the position in the output file to write to (in norom, at least) + base
; 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 == $3F + org
-$E000+($2000*)+!headersize ; org sets the position in the output file to write to (in norom, at least) + base
; 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
>= $A000 + org
-$A000+($2000*)+!headersize + base
+ else + org
-$8000+($2000*)+!headersize + base
+ 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 :( diff --git a/worlds/mm3/src/patch_mm3base.py b/worlds/mm3/src/patch_mm3base.py new file mode 100644 index 000000000000..c64c83c3c028 --- /dev/null +++ b/worlds/mm3/src/patch_mm3base.py @@ -0,0 +1,8 @@ +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) diff --git a/worlds/mm3/test/__init__.py b/worlds/mm3/test/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/mm3/test/bases.py b/worlds/mm3/test/bases.py new file mode 100644 index 000000000000..38ea47ab2fb3 --- /dev/null +++ b/worlds/mm3/test/bases.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class MM3TestBase(WorldTestBase): + game = "Mega Man 3" diff --git a/worlds/mm3/test/test_weakness.py b/worlds/mm3/test/test_weakness.py new file mode 100644 index 000000000000..400eab1f4bc0 --- /dev/null +++ b/worlds/mm3/test/test_weakness.py @@ -0,0 +1,105 @@ +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" + } diff --git a/worlds/mm3/text.py b/worlds/mm3/text.py new file mode 100644 index 000000000000..337837244c40 --- /dev/null +++ b/worlds/mm3/text.py @@ -0,0 +1,63 @@ +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) diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 2a9f56750e8a..4c23a5c64f32 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -28,6 +28,7 @@ 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 = [ @@ -38,6 +39,7 @@ 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 diff --git a/worlds/musedash/MuseDashData.py b/worlds/musedash/MuseDashData.py index 4950c53e9667..86486a4929b0 100644 --- a/worlds/musedash/MuseDashData.py +++ b/worlds/musedash/MuseDashData.py @@ -625,7 +625,7 @@ "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", True, 5, 7, 9), + "Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", False, 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,4 +677,30 @@ "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), } diff --git a/worlds/musedash/archipelago.json b/worlds/musedash/archipelago.json index 9b22a9960523..dea7846b4f38 100644 --- a/worlds/musedash/archipelago.json +++ b/worlds/musedash/archipelago.json @@ -1,6 +1,6 @@ { "game": "Muse Dash", "authors": ["DeamonHunter"], - "world_version": "1.5.26", + "world_version": "1.5.29", "minimum_ap_version": "0.6.3" } \ No newline at end of file diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index db7be3d4ddc5..98bb44efc80b 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -272,7 +272,7 @@ def truncstr(txt, size): world_str = "" rom.write_bytes(rom.sym('WORLD_STRING_TXT'), makebytes(world_str, 12)) - time_str = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M") + " UTC" + time_str = datetime.datetime.now(datetime.timezone.utc).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) diff --git a/worlds/oot/docs/setup_de.md b/worlds/oot/docs/setup_de.md index f257ddc015f4..d456f6907939 100644 --- a/worlds/oot/docs/setup_de.md +++ b/worlds/oot/docs/setup_de.md @@ -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.3.1 und später werden unterstützt. Version 2.10 ist empfohlen. + - Version 2.10 und neuer 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,11 +19,6 @@ 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. diff --git a/worlds/oot/docs/setup_en.md b/worlds/oot/docs/setup_en.md index 31b7137bd8b1..a09752f0d840 100644 --- a/worlds/oot/docs/setup_en.md +++ b/worlds/oot/docs/setup_en.md @@ -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.3.1 and later are supported. Version 2.10 is recommended for stability. + - Version 2.10 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,11 +17,6 @@ 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 diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md index eb2e97384afa..8df50e05bd36 100644 --- a/worlds/oot/docs/setup_fr.md +++ b/worlds/oot/docs/setup_fr.md @@ -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.3.1 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de stabilité. + - Les versions 2.10 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,10 +18,6 @@ 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. diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 4f2c2ef95cb4..10abed539f70 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -123,6 +123,7 @@ 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 @@ -142,6 +143,7 @@ def __init__(self, multiworld, player): 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 = [] @@ -265,6 +267,7 @@ def create_regions(self) -> None: from .regions import create_regions all_regions = create_regions(self) + randomize_wild_encounters(self) # Categories with progression items always included categories = { LocationCategory.BADGE, @@ -494,7 +497,6 @@ def set_rules(self): 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. diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index 49ce147041ee..fd8d0ebc7d58 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -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: + 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: continue location_id += POKEDEX_OFFSET + national_dex_id diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py index 966d19205447..00eceff9a71e 100644 --- a/worlds/pokemon_emerald/opponents.py +++ b/worlds/pokemon_emerald/opponents.py @@ -63,7 +63,7 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None: if len(merged_blacklist) < NUM_REAL_SPECIES: break else: - raise RuntimeError("This should never happen") + merged_blacklist: Set[int] = set() candidates = [ species diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index 29929bd67237..9529be877ebe 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText, - PerGameCommonOptions, OptionGroup, StartInventory) + PerGameCommonOptions, OptionGroup, StartInventory, OptionList) from .data import data @@ -129,6 +129,17 @@ class Dexsanity(Toggle): display_name = "Dexsanity" +class DexsanityEncounterTypes(OptionList): + """ + Determines which Dexsanity encounter areas are in logic. + + Logic will only consider access to Pokemon at these encounter types, but they may still be found elsewhere. + """ + display_name = "Dexsanity Encounter Types" + valid_keys = {"Land", "Water", "Fishing"} + default = valid_keys.copy() + + class Trainersanity(Toggle): """ Defeating a trainer gives you an item. @@ -870,6 +881,7 @@ class PokemonEmeraldOptions(PerGameCommonOptions): npc_gifts: RandomizeNpcGifts berry_trees: RandomizeBerryTrees dexsanity: Dexsanity + dexsanity_encounter_types: DexsanityEncounterTypes trainersanity: Trainersanity item_pool_type: ItemPoolType diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index b39f8c2abf6e..73af6c465840 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -245,7 +245,7 @@ def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slo for r, sc in _encounter_subcategory_ranges[encounter_type].items() if i in r ) - subcategory_species = [] + subcategory_species: list[int] = [] for k in subcategory_range: if new_slots[k] not in subcategory_species: subcategory_species.append(new_slots[k]) @@ -264,6 +264,12 @@ def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slo def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: + encounter_table = { + "Land": EncounterType.LAND, + "Water": EncounterType.WATER, + "Fishing": EncounterType.FISHING, + } + enabled_encounters = {encounter_table[encounter_type] for encounter_type in world.options.dexsanity_encounter_types.value} if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla: return @@ -278,7 +284,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: RandomizeWildPokemon.option_match_base_stats_and_type, } - already_placed = set() + already_placed: set[int] = set() num_placeable_species = NUM_REAL_SPECIES - len(world.blacklisted_wilds) priority_species = [data.constants["SPECIES_WAILORD"], data.constants["SPECIES_RELICANTH"]] @@ -349,7 +355,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: if len(merged_blacklist) < NUM_REAL_SPECIES: break else: - raise RuntimeError("This should never happen") + merged_blacklist = set() candidates = [ species @@ -365,11 +371,13 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None: species_old_to_new_map[species_id] = new_species_id if world.options.dexsanity and encounter_type != EncounterType.ROCK_SMASH \ - and map_name not in OUT_OF_LOGIC_MAPS: + and map_name not in OUT_OF_LOGIC_MAPS and new_species_id not in world.blacklisted_wilds: already_placed.add(new_species_id) # Actually create the new list of slots and encounter table new_slots: List[int] = [] + if encounter_type in enabled_encounters: + world.allowed_dexsanity_species.update(table.slots) for species_id in table.slots: new_slots.append(species_old_to_new_map[species_id]) diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index 828eb20f7218..eeadb8bea21a 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -1548,7 +1548,7 @@ def get_location(location: str): for i in range(NUM_REAL_SPECIES): species = data.species[NATIONAL_ID_TO_SPECIES_ID[i + 1]] - if species.species_id in world.blacklisted_wilds: + if species.species_id in world.blacklisted_wilds or species.species_id not in world.allowed_dexsanity_species: continue set_rule( diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 7873ae54bbba..f52ff789eb6e 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -4,7 +4,10 @@ from .locations import RiskOfRainLocation, item_pickups, get_locations from .rules import set_rules from .ror2environments import environment_vanilla_table, environment_vanilla_orderedstages_table, \ - environment_sotv_orderedstages_table, environment_sotv_table, collapse_dict_list_vertical, shift_by_offset + environment_sotv_orderedstages_table, environment_sotv_table, environment_sost_orderedstages_table, \ + environment_sost_table, collapse_dict_list_vertical, shift_by_offset, environment_vanilla_variants_table, \ + environment_vanilla_variant_orderedstages_table, environment_sots_variants_table, \ + environment_sots_variants_orderedstages_table from BaseClasses import Item, ItemClassification, Tutorial from .options import ItemWeights, ROR2Options, ror2_option_groups @@ -46,7 +49,7 @@ class RiskOfRainWorld(World): } location_name_to_id = item_pickups - required_client_version = (0, 5, 0) + required_client_version = (0, 6, 4) web = RiskOfWeb() total_revivals: int @@ -62,7 +65,9 @@ def generate_early(self) -> None: scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=bool(self.options.dlc_sotv.value) + dlc_sotv=bool(self.options.dlc_sotv.value), + dlc_sots=bool(self.options.dlc_sots.value), + stage_variants=bool(self.options.stage_variants) ) ) self.total_revivals = int(self.options.total_revivals.value / 100 * @@ -71,6 +76,8 @@ def generate_early(self) -> None: self.total_revivals -= 1 if self.options.victory == "voidling" and not self.options.dlc_sotv: self.options.victory.value = self.options.victory.option_any + if self.options.victory == "falseson" and not self.options.dlc_sots: + self.options.victory.value = self.options.victory.option_any def create_regions(self) -> None: @@ -105,16 +112,39 @@ def create_items(self) -> None: # figure out all available ordered stages for each tier environment_available_orderedstages_table = environment_vanilla_orderedstages_table + environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) + # Vanilla Variants + if self.options.stage_variants: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_vanilla_variant_orderedstages_table) if self.options.dlc_sotv: environment_available_orderedstages_table = \ collapse_dict_list_vertical(environment_available_orderedstages_table, environment_sotv_orderedstages_table) + if self.options.dlc_sots: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sost_orderedstages_table) + if self.options.dlc_sots and self.options.stage_variants: + environment_available_orderedstages_table = \ + collapse_dict_list_vertical(environment_available_orderedstages_table, + environment_sots_variants_orderedstages_table) - environments_pool = shift_by_offset(environment_vanilla_table, environment_offset) - + if self.options.stage_variants: + environment_offset_table = shift_by_offset(environment_vanilla_variants_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} if self.options.dlc_sotv: environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset) environments_pool = {**environments_pool, **environment_offset_table} + if self.options.dlc_sots: + environment_offset_table = shift_by_offset(environment_sost_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} + # SOTS Variant Environments + if self.options.dlc_sots and self.options.stage_variants: + environment_offset_table = shift_by_offset(environment_sots_variants_table, environment_offset) + environments_pool = {**environments_pool, **environment_offset_table} + # percollect starting environment for stage 1 unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1) self.multiworld.push_precollected(self.create_item(unlock[0])) @@ -146,7 +176,9 @@ def create_items(self) -> None: scavengers=self.options.scavengers_per_stage.value, scanners=self.options.scanner_per_stage.value, altars=self.options.altars_per_stage.value, - dlc_sotv=bool(self.options.dlc_sotv.value) + dlc_sotv=bool(self.options.dlc_sotv.value), + dlc_sots=bool(self.options.dlc_sots.value), + stage_variants=bool(self.options.stage_variants) ) ) # Create junk items @@ -223,7 +255,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "chests_per_stage", "shrines_per_stage", "scavengers_per_stage", "scanner_per_stage", "altars_per_stage", "total_revivals", "start_with_revive", "final_stage_death", "death_link", "require_stages", - "progressive_stages", casing="camel") + "progressive_stages", "stage_variants", "show_seer_portals", casing="camel") return { **options_dict, "seed": "".join(self.random.choice(string.digits) for _ in range(16)), @@ -254,7 +286,7 @@ def create_events(self) -> None: event_loc.place_locked_item(RiskOfRainItem("Stage 5", ItemClassification.progression, None, self.player)) event_loc.show_in_spoiler = False event_region.locations.append(event_loc) - event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) + event_loc.access_rule = lambda state: state.has("Sky Meadow", self.player) or state.has("Helminth Hatchery", self.player) victory_region = self.multiworld.get_region("Victory", self.player) victory_event = RiskOfRainLocation(self.player, "Victory", None, victory_region) diff --git a/worlds/ror2/archipelago.json b/worlds/ror2/archipelago.json new file mode 100644 index 000000000000..78c54c1420c0 --- /dev/null +++ b/worlds/ror2/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Risk of Rain 2", + "minimum_ap_version": "0.6.4", + "world_version": "1.5.0", + "authors": ["Kindasneaki"] +} \ No newline at end of file diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md index 651c89a33923..2acd133e26f7 100644 --- a/worlds/ror2/docs/en_Risk of Rain 2.md +++ b/worlds/ror2/docs/en_Risk of Rain 2.md @@ -88,12 +88,21 @@ Explore Mode items are: * `Commencement` * `All the Hidden Realms` -Dlc_Sotv items +DLC Survivors of the Void (SOTV) items * `Siphoned Forest` * `Aphelian Sanctuary` * `Sulfur Pools` * `Void Locus` +DLC Seekers of the Storm (SOTS) items + +* `Shattered Abodes`, `Vicious Falls`, `Disturbed Impact` +* `Reformed Altar` +* `Treeborn Colony`, `Golden Dieback` +* `Prime Meridian` +* `Helminth Hatchery` + + When an explore item is granted, it will unlock that environment and will now be accessible! The game will still pick randomly which environment is next, but it will first check to see if they are available. If you have multiple of the next environments unlocked, it will weight the game to have a ***higher chance*** to go to one you diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index 6acf2654a8b2..cef0885970e1 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -23,6 +23,13 @@ all necessary dependencies as well. Click on the `Start modded` button in the top left in `r2modman` to start the game with the Archipelago mod installed. +### Troubleshooting + +* The mod doesn't show up in game! + * `r2modman` looks for the game at its default directory. If you have the game installed somewhere else, + you can update `r2modman` by going to `Settings > Change Risk of Rain 2 folder` + and selecting the correct directory. + ## Configuring your YAML File ### What is a YAML and why do I need one? You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn @@ -59,6 +66,7 @@ also optionally connect to the multiworld using the text client, which can be fo ### In-Game Commands These commands are to be used in-game by using ``Ctrl + Alt + ` `` and then typing the following: + - `archipelago_reconnect` Reconnect to AP. - `archipelago_connect [password]` example: "archipelago_connect archipelago.gg 38281 SlotName". - `archipelago_deathlink true/false` Toggle deathlink. - `archipelago_disconnect` Disconnect from AP. diff --git a/worlds/ror2/locations.py b/worlds/ror2/locations.py index 13077b3e149c..32972311520b 100644 --- a/worlds/ror2/locations.py +++ b/worlds/ror2/locations.py @@ -3,7 +3,8 @@ from .options import TotalLocations, ChestsPerEnvironment, ShrinesPerEnvironment, ScavengersPerEnvironment, \ ScannersPerEnvironment, AltarsPerEnvironment from .ror2environments import compress_dict_list_horizontal, environment_vanilla_orderedstages_table, \ - environment_sotv_orderedstages_table + environment_sotv_orderedstages_table, environment_sost_orderedstages_table, \ + environment_sots_variants_orderedstages_table, environment_vanilla_variant_orderedstages_table class RiskOfRainLocation(Location): @@ -57,13 +58,20 @@ def get_environment_locations(chests: int, shrines: int, scavengers: int, scanne return locations -def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool) \ +def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, altars: int, dlc_sotv: bool, + dlc_sots: bool, stage_variants: bool) \ -> Dict[str, int]: """Get a dictionary of locations for the orderedstage environments with the locations from the parameters.""" locations = {} orderedstages = compress_dict_list_horizontal(environment_vanilla_orderedstages_table) + if stage_variants: + orderedstages.update(compress_dict_list_horizontal(environment_vanilla_variant_orderedstages_table)) if dlc_sotv: orderedstages.update(compress_dict_list_horizontal(environment_sotv_orderedstages_table)) + if dlc_sots: + orderedstages.update(compress_dict_list_horizontal(environment_sost_orderedstages_table)) + if dlc_sots and stage_variants: + orderedstages.update(compress_dict_list_horizontal(environment_sots_variants_orderedstages_table)) # for every environment, generate the respective locations for environment_name, environment_index in orderedstages.items(): locations.update(get_environment_locations( @@ -86,4 +94,6 @@ def get_locations(chests: int, shrines: int, scavengers: int, scanners: int, alt scanners=ScannersPerEnvironment.range_end, altars=AltarsPerEnvironment.range_end, dlc_sotv=True, + dlc_sots=True, + stage_variants=True )) diff --git a/worlds/ror2/options.py b/worlds/ror2/options.py index 381c5942b07b..876a67b7fbc6 100644 --- a/worlds/ror2/options.py +++ b/worlds/ror2/options.py @@ -22,8 +22,9 @@ class Goal(Choice): class Victory(Choice): """ Mithrix: Defeat Mithrix in Commencement - Voidling: Defeat the Voidling in The Planetarium (DLC required! Will select any if not enabled.) + Voidling: Defeat the Voidling in The Planetarium (SOTV DLC required! Will select any if not enabled.) Limbo: Defeat the Scavenger in Hidden Realm: A Moment, Whole + Falseson: Defeat False son and gift an item to the altar in Prime Meridian (SOTS DLC required! Will select any if not enabled.) Any: Any victory in the game will count. See Final Stage Death for additional ways. """ display_name = "Victory Condition" @@ -31,6 +32,7 @@ class Victory(Choice): option_mithrix = 1 option_voidling = 2 option_limbo = 3 + option_falseson = 4 default = 0 @@ -138,18 +140,26 @@ class FinalStageDeath(Toggle): If not use the following to tell if final stage death will count: Victory: mithrix - only dying in Commencement will count. Victory: voidling - only dying in The Planetarium will count. - Victory: limbo - Obliterating yourself will count.""" + Victory: limbo - Obliterating yourself will count. + Victory: falseson - only dying in Prime Meridian will count.""" display_name = "Final Stage Death is Win" class DLC_SOTV(Toggle): """ - Enable if you are using SOTV DLC. + Enable if you are using Survivors of the Void DLC. Affects environment availability for Explore Mode. Adds Void Items into the item pool """ display_name = "Enable DLC - SOTV" +class DLC_SOTS(Toggle): + """ + Enable if you are using Seekers of the Storm DLC. + Affects environment availability for Explore Mode. + """ + display_name = "Enable DLC - SOTS" + class RequireStages(DefaultOnToggle): """Add Stage items to the pool to block access to the next set of environments.""" @@ -162,6 +172,23 @@ class ProgressiveStages(DefaultOnToggle): display_name = "Progressive Stages" +class StageVariants(Toggle): + """Enable if you want to include stage variants in the environment pool. + Stages included are: + - Distant Roost (2) + - Titanic Plains (2) + SOTS DLC Enabled: + - Vicious Falls + - Shattered Abodes + - Golden Dieback""" + display_name = "Include Stage Variants" + + +class ShowSeerPortals(DefaultOnToggle): + """Shows Seer Portals at the teleporter to allow choosing the next environment.""" + display_name = "Show Seer Portals" + + class GreenScrap(Range): """Weight of Green Scraps in the item pool. @@ -384,6 +411,8 @@ class ItemWeights(Choice): AltarsPerEnvironment, RequireStages, ProgressiveStages, + StageVariants, + ShowSeerPortals, ]), OptionGroup("Classic Mode Options", [ TotalLocations, @@ -427,8 +456,11 @@ class ROR2Options(PerGameCommonOptions): start_with_revive: StartWithRevive final_stage_death: FinalStageDeath dlc_sotv: DLC_SOTV + dlc_sots: DLC_SOTS require_stages: RequireStages progressive_stages: ProgressiveStages + stage_variants: StageVariants + show_seer_portals: ShowSeerPortals death_link: DeathLink item_pickup_step: ItemPickupStep shrine_use_step: ShrineUseStep diff --git a/worlds/ror2/regions.py b/worlds/ror2/regions.py index def29b47286b..780f66bcac90 100644 --- a/worlds/ror2/regions.py +++ b/worlds/ror2/regions.py @@ -18,13 +18,10 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: multiworld = ror2_world.multiworld # Default Locations non_dlc_regions: Dict[str, RoRRegionData] = { - "Menu": RoRRegionData(None, ["Distant Roost", "Distant Roost (2)", - "Titanic Plains", "Titanic Plains (2)", + "Menu": RoRRegionData(None, ["Distant Roost", "Titanic Plains", "Verdant Falls"]), "Distant Roost": RoRRegionData([], ["OrderedStage_1"]), - "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), "Titanic Plains": RoRRegionData([], ["OrderedStage_1"]), - "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]), "Verdant Falls": RoRRegionData([], ["OrderedStage_1"]), "Abandoned Aqueduct": RoRRegionData([], ["OrderedStage_2"]), "Wetland Aspect": RoRRegionData([], ["OrderedStage_2"]), @@ -35,12 +32,30 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: "Sundered Grove": RoRRegionData([], ["OrderedStage_4"]), "Sky Meadow": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]), } + non_dlc_variant_regions: Dict[str, RoRRegionData] = { + "Distant Roost (2)": RoRRegionData([], ["OrderedStage_1"]), + "Titanic Plains (2)": RoRRegionData([], ["OrderedStage_1"]), + } # SOTV Regions - dlc_regions: Dict[str, RoRRegionData] = { + dlc_sotv_regions: Dict[str, RoRRegionData] = { "Siphoned Forest": RoRRegionData([], ["OrderedStage_1"]), "Aphelian Sanctuary": RoRRegionData([], ["OrderedStage_2"]), "Sulfur Pools": RoRRegionData([], ["OrderedStage_3"]) } + + dlc_sost_regions: Dict[str, RoRRegionData] = { + "Shattered Abodes": RoRRegionData([], ["OrderedStage_1"]), + "Reformed Altar": RoRRegionData([], ["OrderedStage_2", "Treeborn Colony"]), + "Treeborn Colony": RoRRegionData([], ["OrderedStage_3", "Prime Meridian"]), + "Helminth Hatchery": RoRRegionData([], ["Hidden Realm: Bulwark's Ambry", "OrderedStage_5"]), + } + + dlc_sots_variant_regions: Dict[str, RoRRegionData] = { + "Viscous Falls": RoRRegionData([], ["OrderedStage_1"]), + "Disturbed Impact": RoRRegionData([], ["OrderedStage_1"]), + "Golden Dieback": RoRRegionData([], ["OrderedStage_3", "Prime Meridian"]), + } + other_regions: Dict[str, RoRRegionData] = { "Commencement": RoRRegionData(None, ["Victory", "Petrichor V"]), "OrderedStage_5": RoRRegionData(None, ["Hidden Realm: A Moment, Fractured", @@ -61,10 +76,15 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: "Hidden Realm: Bazaar Between Time": RoRRegionData(None, ["Void Fields"]), "Hidden Realm: Gilded Coast": RoRRegionData(None, None) } - dlc_other_regions: Dict[str, RoRRegionData] = { + dlc_sotv_other_regions: Dict[str, RoRRegionData] = { "The Planetarium": RoRRegionData(None, ["Victory", "Petrichor V"]), "Void Locus": RoRRegionData(None, ["The Planetarium"]) } + + dlc_sost_other_regions: Dict[str, RoRRegionData] = { + "Prime Meridian": RoRRegionData(None, ["Victory", "Petrichor V"]), + } + # Totals of each item chests = int(ror2_options.chests_per_stage) shrines = int(ror2_options.shrines_per_stage) @@ -72,8 +92,14 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: scanners = int(ror2_options.scanner_per_stage) newt = int(ror2_options.altars_per_stage) all_location_regions = {**non_dlc_regions} + if ror2_options.stage_variants: + all_location_regions.update(non_dlc_variant_regions) if ror2_options.dlc_sotv: - all_location_regions = {**non_dlc_regions, **dlc_regions} + all_location_regions.update(dlc_sotv_regions) + if ror2_options.dlc_sots: + all_location_regions.update(dlc_sost_regions) + if ror2_options.dlc_sots and ror2_options.stage_variants: + all_location_regions.update(dlc_sots_variant_regions) # Locations for key in all_location_regions: @@ -99,25 +125,52 @@ def create_explore_regions(ror2_world: "RiskOfRainWorld") -> None: all_location_regions[key].locations.append(f"{key}: Newt Altar {i + 1}") regions_pool: Dict = {**all_location_regions, **other_regions} - # DLC Locations + # Non DLC Variant Locations + if ror2_options.stage_variants: + non_dlc_regions["Menu"].region_exits.append("Distant Roost (2)") + non_dlc_regions["Menu"].region_exits.append("Titanic Plains (2)") + # SOTV DLC Locations if ror2_options.dlc_sotv: non_dlc_regions["Menu"].region_exits.append("Siphoned Forest") other_regions["OrderedStage_1"].region_exits.append("Aphelian Sanctuary") other_regions["OrderedStage_2"].region_exits.append("Sulfur Pools") other_regions["Void Fields"].region_exits.append("Void Locus") other_regions["Commencement"].region_exits.append("The Planetarium") - regions_pool: Dict = {**all_location_regions, **other_regions, **dlc_other_regions} + + # SOTS DLC Locations + if ror2_options.dlc_sots: + non_dlc_regions["Menu"].region_exits.append("Shattered Abodes") + other_regions["OrderedStage_1"].region_exits.append("Reformed Altar") + other_regions["OrderedStage_4"].region_exits.append("Helminth Hatchery") + + # SOTS Variant Locations + if ror2_options.dlc_sots and ror2_options.stage_variants: + non_dlc_regions["Menu"].region_exits.append("Viscous Falls") + non_dlc_regions["Menu"].region_exits.append("Disturbed Impact") + dlc_sost_regions["Reformed Altar"].region_exits.append("Golden Dieback") + + if ror2_options.dlc_sotv: + regions_pool.update(dlc_sotv_other_regions) + if ror2_options.dlc_sots: + regions_pool.update(dlc_sost_other_regions) # Check to see if Victory needs to be removed from regions if ror2_options.victory == "mithrix": other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) - dlc_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0) elif ror2_options.victory == "voidling": other_regions["Commencement"].region_exits.pop(0) other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0) elif ror2_options.victory == "limbo": other_regions["Commencement"].region_exits.pop(0) - dlc_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) + dlc_sost_other_regions["Prime Meridian"].region_exits.pop(0) + elif ror2_options.victory == "falseson": + other_regions["Commencement"].region_exits.pop(0) + other_regions["Hidden Realm: A Moment, Whole"].region_exits.pop(0) + dlc_sotv_other_regions["The Planetarium"].region_exits.pop(0) # Create all the regions for name, data in regions_pool.items(): diff --git a/worlds/ror2/ror2environments.py b/worlds/ror2/ror2environments.py index 61707b336241..40e63a35b13b 100644 --- a/worlds/ror2/ror2environments.py +++ b/worlds/ror2/ror2environments.py @@ -4,11 +4,14 @@ environment_vanilla_orderedstage_1_table: Dict[str, int] = { "Distant Roost": 7, # blackbeach - "Distant Roost (2)": 8, # blackbeach2 "Titanic Plains": 15, # golemplains - "Titanic Plains (2)": 16, # golemplains2 "Verdant Falls": 28, # lakes } +environment_vanilla_variant_orderedstage_1_table: Dict[str, int] = { + "Distant Roost (2)": 8, # blackbeach2 + "Titanic Plains (2)": 16, # golemplains2 +} + environment_vanilla_orderedstage_2_table: Dict[str, int] = { "Abandoned Aqueduct": 17, # goolake "Wetland Aspect": 12, # foggyswamp @@ -54,6 +57,34 @@ "The Planetarium": 45, # voidraid } +environment_sost_orderstage_1_table: Dict[str, int] = { + "Shattered Abodes": 54, # village + +} +environment_sost_variant_orderstage_1_table: Dict[str, int] = { + "Viscous Falls": 34, # lakesnight + "Disturbed Impact": 55, # villagenight +} + +environment_sost_orderstage_2_table: Dict[str, int] = { + "Reformed Altar": 36, # lemuriantemple +} + +environment_sost_orderstage_3_table: Dict[str, int] = { + "Treeborn Colony": 21, # habitat +} +environment_sost_variant_orderstage_3_table: Dict[str, int] = { + "Golden Dieback": 22, # habitatfall +} + +environment_sost_orderstage_5_table: Dict[str, int] = { + "Helminth Hatchery": 23, # helminthroost +} + +environment_sost_special_table: Dict[str, int] = { + "Prime Meridian": 40, # meridian +} + X = TypeVar("X") Y = TypeVar("Y") @@ -100,18 +131,32 @@ def collapse_dict_list_vertical(list_of_dict_1: List[Dict[X, Y]], *args: List[Di environment_vanilla_table = \ {**compress_dict_list_horizontal(environment_vanilla_orderedstages_table), **environment_vanilla_hidden_realm_table, **environment_vanilla_special_table} +# Vanilla Variants +environment_vanilla_variant_orderedstages_table = \ + [environment_vanilla_variant_orderedstage_1_table] +environment_vanilla_variants_table = \ + {**compress_dict_list_horizontal(environment_vanilla_variant_orderedstages_table)} +# SoTV environment_sotv_orderedstages_table = \ [environment_sotv_orderedstage_1_table, environment_sotv_orderedstage_2_table, environment_sotv_orderedstage_3_table] environment_sotv_table = \ {**compress_dict_list_horizontal(environment_sotv_orderedstages_table), **environment_sotv_special_table} - -environment_non_orderedstages_table = \ - {**environment_vanilla_hidden_realm_table, **environment_vanilla_special_table, **environment_sotv_special_table} -environment_orderedstages_table = \ - collapse_dict_list_vertical(environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table) -environment_all_table = {**environment_vanilla_table, **environment_sotv_table} +# SoST +environment_sost_orderedstages_table = \ + [environment_sost_orderstage_1_table, environment_sost_orderstage_2_table, + environment_sost_orderstage_3_table, {}, environment_sost_orderstage_5_table] # There is no new stage 4 in SoST +environment_sost_table = \ + {**compress_dict_list_horizontal(environment_sost_orderedstages_table), **environment_sost_special_table} +# SOTS Variants +environment_sots_variants_orderedstages_table = \ + [environment_sost_variant_orderstage_1_table, {}, environment_sost_variant_orderstage_3_table] +environment_sots_variants_table = \ + {**compress_dict_list_horizontal(environment_sots_variants_orderedstages_table)} + +environment_all_table = {**environment_vanilla_table, **environment_sotv_table, **environment_sost_table, + **environment_vanilla_variants_table, **environment_sots_variants_table} def shift_by_offset(dictionary: Dict[str, int], offset: int) -> Dict[str, int]: diff --git a/worlds/ror2/rules.py b/worlds/ror2/rules.py index f0ab9f28313f..d8d92ca27099 100644 --- a/worlds/ror2/rules.py +++ b/worlds/ror2/rules.py @@ -1,7 +1,9 @@ from worlds.generic.Rules import set_rule, add_rule from BaseClasses import MultiWorld from .locations import get_locations -from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table +from .ror2environments import environment_vanilla_orderedstages_table, environment_sotv_orderedstages_table, \ + environment_sost_orderedstages_table, environment_vanilla_variant_orderedstages_table, \ + environment_sots_variants_orderedstages_table from typing import Set, TYPE_CHECKING if TYPE_CHECKING: @@ -43,6 +45,24 @@ def has_location_access_rule(multiworld: MultiWorld, environment: str, player: i multiworld.get_location(location_name, player).access_rule = \ lambda state: state.has(environment, player) +def explore_environment_location_rules(table, multiworld, player, chests, shrines, newts, scavengers, scanners): + for i in range(len(table)): + for environment_name, _ in table[i].items(): + # Make sure to go through each location + if scavengers == 1: + has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger") + if scanners == 1: + has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner") + for chest in range(1, chests + 1): + has_location_access_rule(multiworld, environment_name, player, chest, "Chest") + for shrine in range(1, shrines + 1): + has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine") + if newts > 0: + for newt in range(1, newts + 1): + has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") + if i > 0: + has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) + def set_rules(ror2_world: "RiskOfRainWorld") -> None: player = ror2_world.player @@ -60,7 +80,9 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: scavengers=ror2_options.scavengers_per_stage.value, scanners=ror2_options.scanner_per_stage.value, altars=ror2_options.altars_per_stage.value, - dlc_sotv=bool(ror2_options.dlc_sotv.value) + dlc_sotv=bool(ror2_options.dlc_sotv.value), + dlc_sots=bool(ror2_options.dlc_sots.value), + stage_variants=bool(ror2_options.stage_variants) ) ) @@ -101,40 +123,25 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: newts = ror2_options.altars_per_stage.value scavengers = ror2_options.scavengers_per_stage.value scanners = ror2_options.scanner_per_stage.value - for i in range(len(environment_vanilla_orderedstages_table)): - for environment_name, _ in environment_vanilla_orderedstages_table[i].items(): - # Make sure to go through each location - if scavengers == 1: - has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger") - if scanners == 1: - has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner") - for chest in range(1, chests + 1): - has_location_access_rule(multiworld, environment_name, player, chest, "Chest") - for shrine in range(1, shrines + 1): - has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine") - if newts > 0: - for newt in range(1, newts + 1): - has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") - if i > 0: - has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) - + # Vanilla stages + explore_environment_location_rules(environment_vanilla_orderedstages_table, multiworld, player, chests, shrines, newts, + scavengers, scanners) + # Vanilla Variant stages + if ror2_options.stage_variants: + explore_environment_location_rules(environment_vanilla_variant_orderedstages_table, multiworld, player, chests, shrines, newts, + scavengers, scanners) + # SoTv stages if ror2_options.dlc_sotv: - for i in range(len(environment_sotv_orderedstages_table)): - for environment_name, _ in environment_sotv_orderedstages_table[i].items(): - # Make sure to go through each location - if scavengers == 1: - has_location_access_rule(multiworld, environment_name, player, scavengers, "Scavenger") - if scanners == 1: - has_location_access_rule(multiworld, environment_name, player, scanners, "Radio Scanner") - for chest in range(1, chests + 1): - has_location_access_rule(multiworld, environment_name, player, chest, "Chest") - for shrine in range(1, shrines + 1): - has_location_access_rule(multiworld, environment_name, player, shrine, "Shrine") - if newts > 0: - for newt in range(1, newts + 1): - has_location_access_rule(multiworld, environment_name, player, newt, "Newt Altar") - if i > 0: - has_stage_access_rule(multiworld, f"Stage {i}", i, environment_name, player) + explore_environment_location_rules(environment_sotv_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + # SoTS stages + if ror2_options.dlc_sots: + explore_environment_location_rules(environment_sost_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + if ror2_options.dlc_sots and ror2_options.stage_variants: + explore_environment_location_rules(environment_sots_variants_orderedstages_table, multiworld, player, chests, shrines, + newts, scavengers, scanners) + has_entrance_access_rule(multiworld, "Hidden Realm: A Moment, Fractured", "Hidden Realm: A Moment, Whole", player) has_stage_access_rule(multiworld, "Stage 1", 1, "Hidden Realm: Bazaar Between Time", player) @@ -147,6 +154,8 @@ def set_rules(ror2_world: "RiskOfRainWorld") -> None: has_entrance_access_rule(multiworld, "Stage 5", "Void Locus", player) if ror2_options.victory == "voidling": has_all_items(multiworld, {"Stage 5", "The Planetarium"}, "Commencement", player) + if ror2_options.dlc_sots: + has_entrance_access_rule(multiworld, "Stage 5", "Prime Meridian", player) # Win Condition multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/ror2/test/test_any_goal.py b/worlds/ror2/test/test_any_goal.py index 18d49944195d..7dbd549c9f5f 100644 --- a/worlds/ror2/test/test_any_goal.py +++ b/worlds/ror2/test/test_any_goal.py @@ -4,23 +4,33 @@ class DLCTest(RoR2TestBase): options = { "dlc_sotv": "true", - "victory": "any" + "victory": "any", + "dlc_sots": "true", } def test_commencement_victory(self) -> None: - self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) self.assertBeatable(False) self.collect_by_name("Commencement") self.assertBeatable(True) def test_planetarium_victory(self) -> None: - self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) self.assertBeatable(False) self.collect_by_name("The Planetarium") self.assertBeatable(True) def test_moment_whole_victory(self) -> None: - self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Victory"]) + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) self.assertBeatable(False) self.collect_by_name("Hidden Realm: A Moment, Whole") self.assertBeatable(True) + def test_false_son_victory(self) -> None: + self.collect_all_but(["Commencement", "The Planetarium", "Hidden Realm: A Moment, Whole", "Prime Meridian", + "Victory"]) + self.assertBeatable(False) + self.collect_by_name("Prime Meridian") + self.assertBeatable(True) diff --git a/worlds/ror2/test/test_falseson_goal.py b/worlds/ror2/test/test_falseson_goal.py new file mode 100644 index 000000000000..3cf815c94255 --- /dev/null +++ b/worlds/ror2/test/test_falseson_goal.py @@ -0,0 +1,17 @@ +from . import RoR2TestBase + + +class FalseSonGoalTest(RoR2TestBase): + options = { + "dlc_sots": "true", + "victory": "falseson", + "stage_variants": "true" + } + + def test_false_son(self) -> None: + self.collect_all_but(["Prime Meridian", "Victory"]) + self.assertFalse(self.can_reach_region("Prime Meridian")) + self.assertBeatable(False) + self.collect_by_name("Prime Meridian") + self.assertTrue(self.can_reach_region("Prime Meridian")) + self.assertBeatable(True) diff --git a/worlds/saving_princess/Client.py b/worlds/saving_princess/Client.py index 29a97bb667c0..195a6a57039e 100644 --- a/worlds/saving_princess/Client.py +++ b/worlds/saving_princess/Client.py @@ -1,4 +1,5 @@ import argparse +import ssl import zipfile from io import BytesIO @@ -8,12 +9,13 @@ import json import logging import os + +import certifi import requests -import secrets import shutil -import subprocess +import subprocess # nosec from tkinter import messagebox -from typing import Any, Dict, Set +from typing import Any, Dict, Set, List import urllib import urllib.parse @@ -90,7 +92,7 @@ def get_timestamp(date: str) -> float: def send_request(request_url: str) -> UrlResponse: """Fetches status code and json response from given url""" - response = requests.get(request_url) + response = requests.get(request_url, timeout=10) if response.status_code == 200: # success try: data = response.json() @@ -129,13 +131,16 @@ def update(target_asset: str, url: str) -> bool: if update_available and messagebox.askyesnocancel(f"New {target_asset}", "Would you like to install the new version now?"): # unzip and patch - with urllib.request.urlopen(release_url) as download: + if not release_url.lower().startswith("https"): + raise ValueError(f'Unexpected scheme for url "{release_url}".') + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + with urllib.request.urlopen(release_url, context=context) as download: # nosec with zipfile.ZipFile(BytesIO(download.read())) as zf: zf.extractall() patch_game() set_date(target_asset, newest_date) - except (ValueError, RuntimeError, urllib.error.HTTPError): - update_error = f"Failed to apply update." + except (ValueError, RuntimeError, urllib.error.HTTPError, urllib.error.URLError) as e: + update_error = f"Failed to apply update:\n{e}" messagebox.showerror("Failure", update_error) raise RuntimeError(update_error) return True @@ -158,8 +163,8 @@ def is_install_valid() -> bool: if not os.path.exists(file_name): return False with open(file_name, "rb") as clean: - current_hash = hashlib.md5(clean.read()).hexdigest() - if not secrets.compare_digest(current_hash, expected_hash): + current_hash = hashlib.md5(clean.read(), usedforsecurity=False).hexdigest() + if current_hash != expected_hash: return False return True @@ -189,12 +194,16 @@ def install() -> None: logging.info("Extracting files from cab archive.") if Utils.is_windows: - subprocess.run(["Extrac32", "/Y", "/E", "saving_princess.cab"]) + windows_path = os.environ["WINDIR"] + extractor_path = f"{windows_path}/System32/Extrac32" + subprocess.run([extractor_path, "/Y", "/E", "saving_princess.cab"]) #nosec else: - if shutil.which("wine") is not None: - subprocess.run(["wine", "Extrac32", "/Y", "/E", "saving_princess.cab"]) - elif shutil.which("7z") is not None: - subprocess.run(["7z", "e", "saving_princess.cab"]) + wine_path = shutil.which("wine") + p7zip_path = shutil.which("7z") + if wine_path is not None: + subprocess.run([wine_path, "Extrac32", "/Y", "/E", "saving_princess.cab"]) #nosec + elif p7zip_path is not None: + subprocess.run([p7zip_path, "e", "saving_princess.cab"]) #nosec else: error = "Could not find neither wine nor 7z.\n\nPlease install either the wine or the p7zip package." messagebox.showerror("Missing package!", f"Error: {error}") @@ -250,7 +259,10 @@ def launch(*args: str) -> Any: if SavingPrincessWorld.settings.launch_game: logging.info("Launching game.") try: - subprocess.Popen(f"{SavingPrincessWorld.settings.launch_command} {name} {password} {server}") + game: str = os.path.join(os.getcwd(), "Saving Princess v0_8.exe") + launch_command: List[str] = (SavingPrincessWorld.settings.launch_command_with_args + + [game, name, password, server]) + subprocess.Popen(launch_command) # nosec except FileNotFoundError: error = ("Could not run the game!\n\n" "Please check that launch_command in options.yaml or host.yaml is set up correctly.") diff --git a/worlds/saving_princess/__init__.py b/worlds/saving_princess/__init__.py index b4caf3828c5f..0c6208638abb 100644 --- a/worlds/saving_princess/__init__.py +++ b/worlds/saving_princess/__init__.py @@ -1,3 +1,4 @@ +import shutil from typing import ClassVar, Dict, Any, Type, List, Union import Utils @@ -20,6 +21,15 @@ def launch_client(*args: str): ) +def get_default_launch_command() -> List[str]: + """Returns platform-dependant default launch command for Saving Princess""" + if Utils.is_windows: + return [] + else: + wine_path = shutil.which("wine") + return [wine_path] if wine_path is not None else ["/usr/bin/wine"] + + class SavingPrincessSettings(Group): class GamePath(UserFilePath): """Path to the game executable from which files are extracted""" @@ -34,17 +44,17 @@ class InstallFolder(LocalFolderPath): class LaunchGame(Bool): """Set this to false to never autostart the game""" - class LaunchCommand(str): + class LaunchCommandWithArgs(List[str]): """ The console command that will be used to launch the game The command will be executed with the installation folder as the current directory + Additional items in the list will be passed in as arguments """ exe_path: GamePath = GamePath("Saving Princess.exe") install_folder: InstallFolder = InstallFolder("Saving Princess") launch_game: Union[LaunchGame, bool] = True - launch_command: LaunchCommand = LaunchCommand('"Saving Princess v0_8.exe"' if Utils.is_windows - else 'wine "Saving Princess v0_8.exe"') + launch_command_with_args: LaunchCommandWithArgs = LaunchCommandWithArgs(get_default_launch_command()) class SavingPrincessWeb(WebWorld): diff --git a/worlds/saving_princess/archipelago.json b/worlds/saving_princess/archipelago.json new file mode 100644 index 000000000000..d4f65dabff0f --- /dev/null +++ b/worlds/saving_princess/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Saving Princess", + "authors": [ "LeonarthCG" ], + "minimum_ap_version": "0.6.6", + "world_version": "1.0.0" +} diff --git a/worlds/shivers/archipelago.json b/worlds/shivers/archipelago.json new file mode 100644 index 000000000000..d9838d3841c1 --- /dev/null +++ b/worlds/shivers/archipelago.json @@ -0,0 +1,6 @@ +{ + "game": "Shivers", + "minimum_ap_version": "0.6.0", + "world_version": "2.7.5", + "authors": ["GodlFire", "Cynbel_Terreus"] +} \ No newline at end of file diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 3c2fb1a2da23..72aa113543b9 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -144,7 +144,7 @@ ), Mushroom.morel: ( Tag(ItemTag.FORAGE), - ForagingSource(seasons=(Season.spring, Season.fall), regions=(Region.secret_woods,)), + ForagingSource(seasons=(Season.spring,), regions=(Region.secret_woods,)), ), Mushroom.red: ( Tag(ItemTag.FORAGE), diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 78c9dcdb67b9..2d7a63245300 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -672,25 +672,29 @@ def write_spoiler_header(self, spoiler_handle: TextIO): # Remove parentheses for better readability spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n') - def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None: - if self.options.entrance_rando: - hint_data.update({self.player: {}}) - # all state seems to have efficient paths - all_state = self.multiworld.get_all_state(True) - all_state.update_reachable_regions(self.player) - paths = all_state.path - portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)}) - for location in self.multiworld.get_locations(self.player): - # skipping event locations - if not location.address: + @classmethod + def stage_extend_hint_information(cls, multiworld: MultiWorld, hint_data: dict[int, dict[int, str]]) -> None: + tunic_er_worlds: list[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC") + if world.options.entrance_rando] + if not tunic_er_worlds: + return + + hint_data.update({world.player: {} for world in tunic_er_worlds}) + all_state = multiworld.get_all_state() + paths = all_state.path + portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)}) + for world in tunic_er_worlds: + all_state.update_reachable_regions(world.player) + for region in world.get_regions(): + if region.name == "Menu": continue - path_to_loc = [] + path_to_region = [] previous_name = "placeholder" try: - name, connection = paths[location.parent_region] + name, connection = paths[region] except KeyError: # logic bug, proceed with warning since it takes a long time to update AP - warning(f"{location.name} is not logically accessible for {self.player_name}. " + warning(f"{region.name} is not logically accessible for {world.player_name}. " "Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs. " "If you are using Plando Items (excluding early locations), then this is likely the cause.") hint_text = "Inaccessible" @@ -703,11 +707,14 @@ def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None: # was getting some cases like Library Grave -> Library Grave -> other place if name in portal_names and name != previous_name: previous_name = name - path_to_loc.append(name) - hint_text = " -> ".join(reversed(path_to_loc)) + path_to_region.append(name) + hint_text = " -> ".join(reversed(path_to_region)) if hint_text: - hint_data[self.player][location.address] = hint_text + for location in region.get_locations(): + if location.address is None: + continue + hint_data[world.player][location.address] = hint_text def get_real_location(self, location: Location) -> tuple[str, int]: # if it's not in a group, it's not in an item link diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index a1b8b2fefdbc..60de72906dcb 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -29,6 +29,8 @@ def create_er_regions(world: "TunicWorld") -> dict[Portal, Portal]: world.used_shop_numbers = set() for region_name, region_data in world.er_regions.items(): + if region_name == "Zig Skip Exit": + continue if world.options.entrance_rando and region_name == "Zig Skip Exit": # need to check if there's a seed group for this first if world.options.entrance_rando.value not in EntranceRando.options.values(): @@ -773,11 +775,17 @@ def too_few_portals_for_direction_pairs(direction: int, offset: int) -> bool: # loop through our list of paired portals and make two-way connections def create_randomized_entrances(world: "TunicWorld", portal_pairs: dict[Portal, Portal], regions: dict[str, Region]) -> None: for portal1, portal2 in portal_pairs.items(): + # this portal is completely inaccessible, so let's not make this connection + if portal1.region == "Zig Skip Exit": + continue # connect to the outlet region if there is one, if not connect to the actual region regions[portal1.region].connect( connecting_region=regions[get_portal_outlet_region(portal2, world)], name=portal1.name) if not world.options.decoupled or not world.options.entrance_rando: + # this portal is completely inaccessible, so let's not make this connection + if portal2.region == "Zig Skip Exit": + continue regions[portal2.region].connect( connecting_region=regions[get_portal_outlet_region(portal1, world)], name=portal2.name)