From 663b50b33e6ccd49f18b1ebff728978789c75b01 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 19 May 2024 15:17:55 +0200 Subject: [PATCH 01/12] WebHost: fix AutoLauncher restarting rooms due to race condition (#3333) --- WebHostLib/autolauncher.py | 2 +- WebHostLib/customserver.py | 24 +++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 78fff6c50991..e70a31bc4166 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -80,7 +80,7 @@ def keep_running(): room.last_activity >= datetime.utcnow() - timedelta(days=3)) for room in rooms: # we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled. - if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout): + if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5): hosters[room.id.int % len(hosters)].start_room(room.id) except AlreadyRunningException: diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 04b4b6a0a02a..f12069678182 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -21,7 +21,6 @@ from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert from Utils import restricted_loads, cache_argsless -from .locker import Locker from .models import Command, GameDataPackage, Room, db @@ -246,25 +245,24 @@ async def start_room(room_id): ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) await ctx.shutdown_task - # ensure auto launch is on the same page in regard to room activity. - with db_session: - room: Room = Room.get(id=ctx.room_id) - room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60) - except (KeyboardInterrupt, SystemExit): - with db_session: - room = Room.get(id=room_id) - # ensure the Room does not spin up again on its own, minute of safety buffer - room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout) + pass except Exception: with db_session: room = Room.get(id=room_id) room.last_port = -1 - # ensure the Room does not spin up again on its own, minute of safety buffer - room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout) raise finally: - rooms_shutting_down.put(room_id) + try: + 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) + logging.info(f"Shutting down room {room_id} on {name}.") + finally: + await asyncio.sleep(5) + rooms_shutting_down.put(room_id) class Starter(threading.Thread): def run(self): From cf34f125d67abfa6c07799f2ca3c69dd0a843fc3 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 19 May 2024 15:32:11 +0200 Subject: [PATCH 02/12] CustomServer: don't mutate static server data (#3334) when switching to multiple rooms per process, we ended up modifying the static server data because that's how _load works and the data is now shared between multiple rooms. --- WebHostLib/customserver.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index f12069678182..ac88ddd3e0c0 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -73,6 +73,7 @@ def __init__(self, static_server_data: dict, logger: logging.Logger): def _load_game_data(self): for key, value in self.static_server_data.items(): + # NOTE: attributes are mutable and shared, so they will have to be copied before being modified setattr(self, key, value) self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names) @@ -100,18 +101,37 @@ def load(self, room_id: int): multidata = self.decompress(room.seed.multidata) game_data_packages = {} + + static_gamespackage = self.gamespackage # this is shared across all rooms + static_item_name_groups = self.item_name_groups + static_location_name_groups = self.location_name_groups + self.gamespackage = {} # this may be modified by _load + self.item_name_groups = {} + self.location_name_groups = {} + for game in list(multidata.get("datapackage", {})): game_data = multidata["datapackage"][game] if "checksum" in game_data: - if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]: - # non-custom. remove from multidata + if static_gamespackage.get(game, {}).get("checksum") == game_data["checksum"]: + # non-custom. remove from multidata and use static data # games package could be dropped from static data once all rooms embed data package del multidata["datapackage"][game] else: row = GameDataPackage.get(checksum=game_data["checksum"]) if row: # None if rolled on >= 0.3.9 but uploaded to <= 0.3.8. multidata should be complete game_data_packages[game] = Utils.restricted_loads(row.data) - + continue + else: + self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}") + self.gamespackage[game] = static_gamespackage.get(game, {}) + self.item_name_groups[game] = static_item_name_groups.get(game, {}) + self.location_name_groups[game] = static_location_name_groups.get(game, {}) + + if not game_data_packages: + # all static -> use the static dicts directly + self.gamespackage = static_gamespackage + self.item_name_groups = static_item_name_groups + self.location_name_groups = static_location_name_groups return self._load(multidata, game_data_packages, True) @db_session From d3f4ee499478ea6c262e64c3fb57e98ba98b7695 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 19 May 2024 16:31:35 +0200 Subject: [PATCH 03/12] WebHost: re-introduce per-Room Locker (#3337) --- WebHostLib/customserver.py | 96 ++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index ac88ddd3e0c0..ff1b19309431 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -21,6 +21,7 @@ from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert from Utils import restricted_loads, cache_argsless +from .locker import Locker from .models import Command, GameDataPackage, Room, db @@ -231,58 +232,61 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, loop = asyncio.get_event_loop() async def start_room(room_id): - try: - logger = set_up_logging(room_id) - ctx = WebHostContext(static_server_data, logger) - ctx.load(room_id) - ctx.init_save() + with Locker(f"RoomLocker {room_id}"): try: - ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) - - await ctx.server - except OSError: # likely port in use - ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context) - - await ctx.server - port = 0 - for wssocket in ctx.server.ws_server.sockets: - socketname = wssocket.getsockname() - if wssocket.family == socket.AF_INET6: - # Prefer IPv4, as most users seem to not have working ipv6 support - if not port: + logger = set_up_logging(room_id) + ctx = WebHostContext(static_server_data, logger) + ctx.load(room_id) + ctx.init_save() + try: + ctx.server = websockets.serve( + functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) + + await ctx.server + except OSError: # likely port in use + ctx.server = websockets.serve( + functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context) + + await ctx.server + port = 0 + for wssocket in ctx.server.ws_server.sockets: + socketname = wssocket.getsockname() + if wssocket.family == socket.AF_INET6: + # Prefer IPv4, as most users seem to not have working ipv6 support + if not port: + port = socketname[1] + elif wssocket.family == socket.AF_INET: port = socketname[1] - elif wssocket.family == socket.AF_INET: - port = socketname[1] - if port: - ctx.logger.info(f'Hosting game at {host}:{port}') + if port: + ctx.logger.info(f'Hosting game at {host}:{port}') + with db_session: + room = Room.get(id=ctx.room_id) + room.last_port = port + else: + ctx.logger.exception("Could not determine port. Likely hosting failure.") with db_session: - room = Room.get(id=ctx.room_id) - room.last_port = port - else: - ctx.logger.exception("Could not determine port. Likely hosting failure.") - with db_session: - ctx.auto_shutdown = Room.get(id=room_id).timeout - ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) - await ctx.shutdown_task + ctx.auto_shutdown = Room.get(id=room_id).timeout + ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) + await ctx.shutdown_task - except (KeyboardInterrupt, SystemExit): - pass - except Exception: - with db_session: - room = Room.get(id=room_id) - room.last_port = -1 - raise - finally: - try: - with (db_session): - # ensure the Room does not spin up again on its own, minute of safety buffer + except (KeyboardInterrupt, SystemExit): + pass + except Exception: + with db_session: room = Room.get(id=room_id) - room.last_activity = datetime.datetime.utcnow() - \ - datetime.timedelta(minutes=1, seconds=room.timeout) - logging.info(f"Shutting down room {room_id} on {name}.") + room.last_port = -1 + raise finally: - await asyncio.sleep(5) - rooms_shutting_down.put(room_id) + try: + 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) + logging.info(f"Shutting down room {room_id} on {name}.") + finally: + await asyncio.sleep(5) + rooms_shutting_down.put(room_id) class Starter(threading.Thread): def run(self): From e97eddcdaf35920adee42055895a9ec1519f4895 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 19 May 2024 18:25:56 +0200 Subject: [PATCH 04/12] WebHost: move atexit saving to end of room hosting function (#3339) --- MultiServer.py | 7 ++++--- WebHostLib/customserver.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 194f0a67fd6a..e95e44dd7d5c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -508,7 +508,7 @@ def init_save(self, enabled: bool = True): self.logger.exception(e) self._start_async_saving() - def _start_async_saving(self): + def _start_async_saving(self, atexit_save: bool = True): if not self.auto_saver_thread: def save_regularly(): # time.time() is platform dependent, so using the expensive datetime method instead @@ -532,8 +532,9 @@ def get_datetime_second(): self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True) self.auto_saver_thread.start() - import atexit - atexit.register(self._save, True) # make sure we save on exit too + if atexit_save: + import atexit + atexit.register(self._save, True) # make sure we save on exit too def get_save(self) -> dict: self.recheck_hints() diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index ff1b19309431..8cff86dcf041 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -142,7 +142,7 @@ def init_save(self, enabled: bool = True): 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._start_async_saving() + self._start_async_saving(atexit_save=False) threading.Thread(target=self.listen_to_db_commands, daemon=True).start() @db_session @@ -278,6 +278,7 @@ async def start_room(room_id): raise finally: try: + ctx._save() with (db_session): # ensure the Room does not spin up again on its own, minute of safety buffer room = Room.get(id=room_id) From 2801e212969dbc69afa71f96e5ff6e3e3efa4d7d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 19 May 2024 20:21:46 +0200 Subject: [PATCH 05/12] WebHost: fixup WebHostLib/options.py (#3332) * WebHost: fixup WebHostLib/options.py * Update WebHostLib/options.py * Update WebHostLib/options.py * fix visibility flag handling --- WebHostLib/generate.py | 56 ++++++++++++++++++++----------------- WebHostLib/options.py | 63 +++++++++++++++--------------------------- 2 files changed, 52 insertions(+), 67 deletions(-) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index ee1ce591ee84..a78560cb0bd3 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -70,35 +70,39 @@ def generate(race=False): flash(options) else: meta = get_meta(request.form, race) - results, gen_options = roll_options(options, set(meta["plando_options"])) - - if any(type(result) == str for result in results.values()): - return render_template("checkResult.html", results=results) - elif len(gen_options) > app.config["MAX_ROLL"]: - flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " - f"If you have a larger group, please generate it yourself and upload it.") - elif len(gen_options) >= app.config["JOB_THRESHOLD"]: - gen = Generation( - options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), - # convert to json compatible - meta=json.dumps(meta), - state=STATE_QUEUED, - owner=session["_id"]) - commit() + return start_generation(options, meta) - return redirect(url_for("wait_seed", seed=gen.id)) - else: - try: - seed_id = gen_game({name: vars(options) for name, options in gen_options.items()}, - meta=meta, owner=session["_id"].int) - except BaseException as e: - from .autolauncher import handle_generation_failure - handle_generation_failure(e) - return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e))) + return render_template("generate.html", race=race, version=__version__) - return redirect(url_for("view_seed", seed=seed_id)) - return render_template("generate.html", race=race, version=__version__) +def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]): + results, gen_options = roll_options(options, set(meta["plando_options"])) + + if any(type(result) == str for result in results.values()): + return render_template("checkResult.html", results=results) + elif len(gen_options) > app.config["MAX_ROLL"]: + flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " + f"If you have a larger group, please generate it yourself and upload it.") + elif len(gen_options) >= app.config["JOB_THRESHOLD"]: + gen = Generation( + options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), + # convert to json compatible + meta=json.dumps(meta), + state=STATE_QUEUED, + owner=session["_id"]) + commit() + + return redirect(url_for("wait_seed", seed=gen.id)) + else: + try: + seed_id = gen_game({name: vars(options) for name, options in gen_options.items()}, + meta=meta, owner=session["_id"].int) + except BaseException as e: + from .autolauncher import handle_generation_failure + handle_generation_failure(e) + return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e))) + + return redirect(url_for("view_seed", seed=seed_id)) def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None): diff --git a/WebHostLib/options.py b/WebHostLib/options.py index e631d31b036b..f52f0f3d9f91 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -1,33 +1,33 @@ import collections.abc +import json import os +from textwrap import dedent +from typing import Dict, Union + import yaml -import requests -import json -import flask +from flask import redirect, render_template, request, Response import Options -from Options import Visibility -from flask import redirect, render_template, request, Response -from worlds.AutoWorld import AutoWorldRegister from Utils import local_path -from textwrap import dedent +from worlds.AutoWorld import AutoWorldRegister from . import app, cache -def create(): +def create() -> None: target_folder = local_path("WebHostLib", "static", "generated") yaml_folder = os.path.join(target_folder, "configs") Options.generate_yaml_templates(yaml_folder) -def get_world_theme(game_name: str): +def get_world_theme(game_name: str) -> str: if game_name in AutoWorldRegister.world_types: return AutoWorldRegister.world_types[game_name].web.theme return 'grass' -def render_options_page(template: str, world_name: str, is_complex: bool = False): +def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]: + visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui world = AutoWorldRegister.world_types[world_name] if world.hidden or world.web.options_page is False: return redirect("games") @@ -39,13 +39,8 @@ def render_options_page(template: str, world_name: str, is_complex: bool = False grouped_options = {group: {} for group in ordered_groups} for option_name, option in world.options_dataclass.type_hints.items(): # Exclude settings from options pages if their visibility is disabled - if not is_complex and option.visibility < Visibility.simple_ui: - continue - - if is_complex and option.visibility < Visibility.complex_ui: - continue - - grouped_options[option_groups.get(option, "Game Options")][option_name] = option + if visibility_flag in option.visibility: + grouped_options[option_groups.get(option, "Game Options")][option_name] = option return render_template( template, @@ -58,26 +53,12 @@ def render_options_page(template: str, world_name: str, is_complex: bool = False ) -def generate_game(player_name: str, formatted_options: dict): - payload = { - "race": 0, - "hint_cost": 10, - "forfeit_mode": "auto", - "remaining_mode": "disabled", - "collect_mode": "goal", - "weights": { - player_name: formatted_options, - }, - } - r = requests.post("https://archipelago.gg/api/generate", json=payload) - if 200 <= r.status_code <= 299: - response_data = r.json() - return redirect(response_data["url"]) - else: - return r.text - - -def send_yaml(player_name: str, formatted_options: dict): +def generate_game(options: Dict[str, Union[dict, str]]) -> Union[Response, str]: + from .generate import start_generation + return start_generation(options, {"plando_options": ["items", "connections", "texts", "bosses"]}) + + +def send_yaml(player_name: str, formatted_options: dict) -> Response: response = Response(yaml.dump(formatted_options, sort_keys=False)) response.headers["Content-Type"] = "text/yaml" response.headers["Content-Disposition"] = f"attachment; filename={player_name}.yaml" @@ -85,7 +66,7 @@ def send_yaml(player_name: str, formatted_options: dict): @app.template_filter("dedent") -def filter_dedent(text: str): +def filter_dedent(text: str) -> str: return dedent(text).strip("\n ") @@ -111,7 +92,7 @@ def default(self, obj): return json.JSONEncoder.default(self, obj) json_data = json.dumps(presets, cls=SetEncoder) - response = flask.Response(json_data) + response = Response(json_data) response.headers["Content-Type"] = "application/json" return response @@ -169,7 +150,7 @@ def generate_weighted_yaml(game: str): } if intent_generate: - return generate_game(player_name, formatted_options) + return generate_game({player_name: formatted_options}) else: return send_yaml(player_name, formatted_options) @@ -243,7 +224,7 @@ def generate_yaml(game: str): } if intent_generate: - return generate_game(player_name, formatted_options) + return generate_game({player_name: formatted_options}) else: return send_yaml(player_name, formatted_options) From 8e9a050889aa0e568abc62191c6470223ed4a640 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sun, 19 May 2024 11:36:47 -0700 Subject: [PATCH 06/12] Zillion: "item counts" OptionGroup (#3338) --- worlds/zillion/__init__.py | 4 +++- worlds/zillion/options.py | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 62623edc0803..cce120d7e3f4 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -14,7 +14,7 @@ from .gen_data import GenData from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion -from .options import ZillionOptions, validate +from .options import ZillionOptions, validate, z_option_groups from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id @@ -62,6 +62,8 @@ class ZillionWebWorld(WebWorld): ["beauxq"] )] + option_groups = z_option_groups + class ZillionWorld(World): """ diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 0b94e3d63513..d75dd1a1c22c 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -3,7 +3,7 @@ from typing import ClassVar, Dict, Tuple from typing_extensions import TypeGuard # remove when Python >= 3.10 -from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice +from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Toggle from zilliandomizer.options import ( Options as ZzOptions, char_to_gun, char_to_jump, ID, @@ -279,6 +279,14 @@ class ZillionOptions(PerGameCommonOptions): room_gen: ZillionRoomGen +z_option_groups = [ + OptionGroup("item counts", [ + ZillionIDCardCount, ZillionBreadCount, ZillionOpaOpaCount, ZillionZillionCount, + ZillionFloppyDiskCount, ZillionScopeCount, ZillionRedIDCardCount + ]) +] + + def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: tr: ZzItemCounts = { ID.card: ic["ID Card"], From 019dfb82422d7f87be0cf207d312c288f2a73538 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 19 May 2024 20:40:08 +0200 Subject: [PATCH 07/12] CustomServer: re-add missing Archipelago to data package (#3341) --- WebHostLib/customserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 8cff86dcf041..bc9f74bacee7 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -106,7 +106,7 @@ def load(self, room_id: int): static_gamespackage = self.gamespackage # this is shared across all rooms static_item_name_groups = self.item_name_groups static_location_name_groups = self.location_name_groups - self.gamespackage = {} # this may be modified by _load + self.gamespackage = {"Archipelago": static_gamespackage["Archipelago"]} # this may be modified by _load self.item_name_groups = {} self.location_name_groups = {} From e9781094101af349e13558cfd27bb594ee116da9 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 19 May 2024 20:40:36 +0200 Subject: [PATCH 08/12] WebHost: properly stop worker threads (#3340) * WebHost: properly stop worker threads * Less jank * Forgot the try-catch around the while true --- WebHost.py | 10 +++++++++- WebHostLib/autolauncher.py | 28 ++++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/WebHost.py b/WebHost.py index 8ccf6b68c2ee..9b5edd322f91 100644 --- a/WebHost.py +++ b/WebHost.py @@ -117,7 +117,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO) from WebHostLib.lttpsprites import update_sprites_lttp - from WebHostLib.autolauncher import autohost, autogen + from WebHostLib.autolauncher import autohost, autogen, stop from WebHostLib.options import create as create_options_files try: @@ -138,3 +138,11 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] else: from waitress import serve serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"]) + else: + from time import sleep + try: + while True: + sleep(1) # wait for process to be killed + except (SystemExit, KeyboardInterrupt): + pass + stop() # stop worker threads diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index e70a31bc4166..08a1309ebc73 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -3,16 +3,26 @@ import json import logging import multiprocessing -import time import typing -from uuid import UUID from datetime import timedelta, datetime +from threading import Event, Thread +from uuid import UUID from pony.orm import db_session, select, commit from Utils import restricted_loads from .locker import Locker, AlreadyRunningException +_stop_event = Event() + + +def stop(): + """Stops previously launched threads""" + global _stop_event + stop_event = _stop_event + _stop_event = Event() # new event for new threads + stop_event.set() + def handle_generation_success(seed_id): logging.info(f"Generation finished for seed {seed_id}") @@ -63,6 +73,7 @@ def cleanup(): def autohost(config: dict): def keep_running(): + stop_event = _stop_event try: with Locker("autohost"): cleanup() @@ -72,8 +83,7 @@ def keep_running(): hosters.append(hoster) hoster.start() - while 1: - time.sleep(0.1) + while not stop_event.wait(0.1): with db_session: rooms = select( room for room in Room if @@ -86,12 +96,12 @@ def keep_running(): except AlreadyRunningException: logging.info("Autohost reports as already running, not starting another.") - import threading - threading.Thread(target=keep_running, name="AP_Autohost").start() + Thread(target=keep_running, name="AP_Autohost").start() def autogen(config: dict): def keep_running(): + stop_event = _stop_event try: with Locker("autogen"): @@ -112,8 +122,7 @@ def keep_running(): commit() select(generation for generation in Generation if generation.state == STATE_ERROR).delete() - while 1: - time.sleep(0.1) + while not stop_event.wait(0.1): with db_session: # for update locks the database row(s) during transaction, preventing writes from elsewhere to_start = select( @@ -124,8 +133,7 @@ def keep_running(): except AlreadyRunningException: logging.info("Autogen reports as already running, not starting another.") - import threading - threading.Thread(target=keep_running, name="AP_Autogen").start() + Thread(target=keep_running, name="AP_Autogen").start() multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {} From 14321d6ba20e8a1ec8f63f7b122320b11c782dcb Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 19 May 2024 20:41:18 +0200 Subject: [PATCH 09/12] Factorio: update factorio-rcon (#3198) 2.1.1 didn't work with py3.8, 2.1.2 fixes that --- worlds/factorio/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt index c8a60369dab6..8d684401663b 100644 --- a/worlds/factorio/requirements.txt +++ b/worlds/factorio/requirements.txt @@ -1,2 +1 @@ -factorio-rcon-py>=2.1.1; python_version >= '3.9' -factorio-rcon-py==2.0.1; python_version <= '3.8' +factorio-rcon-py>=2.1.2 From e0b68896349f217ab7216f86e4b5c1c47036b5a1 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 19 May 2024 16:18:41 -0400 Subject: [PATCH 10/12] ALTTP: Second attempt to fix Swamp Palace boss logic (#3315) --- worlds/alttp/Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index e0d812e6d27d..eac810610b48 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -399,8 +399,8 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5)) if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']: forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player) - set_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) - set_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) + add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) + add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)) if multiworld.pot_shuffle[player]: # key can (and probably will) be moved behind bombable wall set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player)) From 12cde88f95803da6638f748da234086a1c1dded6 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sun, 19 May 2024 18:56:24 -0400 Subject: [PATCH 11/12] Lingo: Fixed edge case sunwarp shuffle accessibility issue (#3228) * Lingo: Fixed edge case sunwarp shuffle accessibility issue * Minor readability update --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/lingo/player_logic.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 19583bc8228b..b6941f37eed1 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -2,7 +2,7 @@ from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING from Options import OptionError -from .datatypes import Door, DoorType, RoomAndDoor, RoomAndPanel +from .datatypes import Door, DoorType, Painting, RoomAndDoor, RoomAndPanel from .items import ALL_ITEM_TABLE, ItemType from .locations import ALL_LOCATION_TABLE, LocationClassification from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition @@ -361,13 +361,29 @@ def randomize_paintings(self, world: "LingoWorld") -> bool: if door_shuffle == ShuffleDoors.option_none: required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors] - req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() - if not painting.exit_only and not painting.disable and not painting.req_blocked and - not painting.req_blocked_when_no_doors and painting.room not in required_painting_rooms] - else: - req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() - if not painting.exit_only and not painting.disable and not painting.req_blocked and - painting.room not in required_painting_rooms] + + def is_req_enterable(painting_id: str, painting: Painting) -> bool: + if painting.exit_only or painting.disable or painting.req_blocked\ + or painting.room in required_painting_rooms: + return False + + if world.options.shuffle_doors == ShuffleDoors.option_none: + if painting.req_blocked_when_no_doors: + return False + + # Special case for the paintings in Color Hunt and Champion's Rest. These are req blocked when not on + # doors mode, and when sunwarps are disabled or sunwarp shuffle is on and the Color Hunt sunwarp is not + # an exit. This is because these two rooms would then be inaccessible without roof access, and we can't + # hide the Owl Hallway entrance behind roof access. + if painting.room in ["Color Hunt", "Champion's Rest"]: + if world.options.sunwarp_access == SunwarpAccess.option_disabled\ + or (world.options.shuffle_sunwarps and "Color Hunt" not in self.sunwarp_exits): + return False + + return True + + req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() + if is_req_enterable(painting_id, painting)] req_exits += [painting_id for painting_id, painting in PAINTINGS.items() if painting.exit_only and painting.required] req_entrances = world.random.sample(req_enterable, len(req_exits)) From 754fc11c1b009f3ed707a80ac13fae95bb98397a Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 19 May 2024 19:01:24 -0400 Subject: [PATCH 12/12] TUNIC: ER Refactor for better plando connections, fewer shops improvement (#3075) * Fixed shop changes * Update option description * Apply suggestions from Vi's review (thank you) * Fix for plando connections on a full scene * Plando connections should work better now for complicated paths * Even more good plando connections yes * Starting to move the info over * Fixing up formatting a bit * Remove unneeded item info * Put in updated_reachable_regions, to replace add_dependent_regions * Updated to match ladder shuffle * More stuff I guess * It functions! * It mostly works with plando now, some slight issues still * Fixed minor logic bug * Fixed world leakage * Change exception message * Make exception message better for troubleshooting failed connections * Merged with main * technically a logic fix but it would never matter cause no start shuffle * Add a couple more alias item groups cause yeah * Rename beneath the vault front -> beneath the vault main * Flip lantern access rule to the region * Add missing connection to traversal reqs * Move start_inventory_from_pool to the top so that it's next to start_inventory * Reword the fixed shop description slightly * Refactor per ixrec's comments * Greatly reduced an overcomplicated block because Vi is cool and smart and also cool * Rewrite traversal reqs thing per Vi's comments --- worlds/tunic/er_data.py | 1171 +++++++++++++++++++++++++----------- worlds/tunic/er_rules.py | 23 +- worlds/tunic/er_scripts.py | 336 +++++------ worlds/tunic/items.py | 2 + worlds/tunic/locations.py | 2 +- worlds/tunic/options.py | 8 +- 6 files changed, 972 insertions(+), 570 deletions(-) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index d850a06dfa78..f49e7dff3e58 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1,4 +1,4 @@ -from typing import Dict, NamedTuple, List, Tuple +from typing import Dict, NamedTuple, List from enum import IntEnum @@ -432,6 +432,9 @@ def destination_scene(self) -> str: # the vanilla connection destination="ziggurat2020_2", tag="_"), Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance", destination="ziggurat2020_FTRoom", tag="_"), + # only if fixed shop is on, removed otherwise + Portal(name="Ziggurat Lower Falling Entrance", region="Zig Skip Exit", + destination="ziggurat2020_1", tag="_zig2_skip"), Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit", destination="ziggurat2020_3", tag="_"), @@ -514,13 +517,13 @@ def destination_scene(self) -> str: # the vanilla connection class RegionInfo(NamedTuple): game_scene: str # the name of the scene in the actual game dead_end: int = 0 # if a region has only one exit - hint: int = 0 # what kind of hint text you should have class DeadEnd(IntEnum): free = 0 # not a dead end all_cats = 1 # dead end in every logic category restricted = 2 # dead end only in restricted + special = 3 # special handling for secret gathering place and zig skip exit # there's no dead ends that are only in unrestricted @@ -567,7 +570,7 @@ class DeadEnd(IntEnum): "Furnace Fuse": RegionInfo("Furnace"), # top of the furnace "Furnace Ladder Area": RegionInfo("Furnace"), # the two portals accessible by the ladder "Furnace Walking Path": RegionInfo("Furnace"), # dark tomb to west garden - "Secret Gathering Place": RegionInfo("Waterfall", dead_end=DeadEnd.all_cats), + "Secret Gathering Place": RegionInfo("Waterfall", dead_end=DeadEnd.special), "Changing Room": RegionInfo("Changing Room", dead_end=DeadEnd.all_cats), "Patrol Cave": RegionInfo("PatrolCave", dead_end=DeadEnd.all_cats), "Ruined Shop": RegionInfo("Ruined Shop", dead_end=DeadEnd.all_cats), @@ -650,7 +653,7 @@ class DeadEnd(IntEnum): "Fortress Courtyard": RegionInfo("Fortress Courtyard"), "Fortress Courtyard Upper": RegionInfo("Fortress Courtyard"), "Beneath the Vault Ladder Exit": RegionInfo("Fortress Basement"), - "Beneath the Vault Front": RegionInfo("Fortress Basement"), # the vanilla entry point + "Beneath the Vault Main": RegionInfo("Fortress Basement"), # the vanilla entry point "Beneath the Vault Back": RegionInfo("Fortress Basement"), # the vanilla exit point "Eastern Vault Fortress": RegionInfo("Fortress Main"), "Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"), @@ -687,6 +690,7 @@ class DeadEnd(IntEnum): "Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"), "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side + "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special), # the exit from zig skip, for use with fixed shop on "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"), "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"), @@ -723,357 +727,812 @@ class DeadEnd(IntEnum): } -# the key is the region you have, the value is the regions you get for having that region -# this is mostly so we don't have to do something overly complex to get this information -# really want to get rid of this, but waiting on item plando being workable with ER -dependent_regions_restricted: Dict[Tuple[str, ...], List[str]] = { - ("Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", - "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", - "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", "East Overworld", "Upper Overworld", - "Overworld after Temple Rafters", "Overworld Quarry Entry", "Overworld above Patrol Cave", - "Overworld at Patrol Cave", "Overworld to West Garden Upper", "Overworld Well Ladder", "Overworld Beach", - "Overworld to Atoll Upper", "Overworld above Quarry Entrance", "Overworld after Envoy", "Overworld Tunnel Turret"): - ["Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", - "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", - "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", - "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", "East Overworld", - "Upper Overworld", "Overworld after Temple Rafters", "Overworld Quarry Entry", "Overworld above Patrol Cave", - "Overworld at Patrol Cave", "Overworld to West Garden Upper", "Overworld Well Ladder", "Overworld Beach", - "Overworld to Atoll Upper", "Overworld Temple Door", "Overworld above Quarry Entrance", - "Overworld after Envoy", "Overworld Tunnel Turret"], - ("Hourglass Cave",): - ["Hourglass Cave", "Hourglass Cave Tower"], - ("Old House Front",): - ["Old House Front", "Old House Back"], - ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): - ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], - ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], - ("Forest Belltower Upper",): - ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], - ("Forest Belltower Main",): - ["Forest Belltower Main", "Forest Belltower Lower"], - ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"): - ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"], - ("Guard House 1 East", "Guard House 1 West"): - ["Guard House 1 East", "Guard House 1 West"], - ("Guard House 2 Upper", "Guard House 2 Lower"): - ["Guard House 2 Upper", "Guard House 2 Lower"], - ("Forest Grave Path Main", "Forest Grave Path Upper"): - ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], - ("Forest Grave Path by Grave", "Forest Hero's Grave"): - ["Forest Grave Path by Grave", "Forest Hero's Grave"], - ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"): - ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"], - ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"): - ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"], - ("Well Boss",): - ["Dark Tomb Checkpoint", "Well Boss"], - ("West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region"): - ["West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region"], - ("West Garden Portal", "West Garden Portal Item"): ["West Garden Portal", "West Garden Portal Item"], - ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"): - ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"], - ("Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"): - ["Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"], - ("Frog's Domain", "Frog's Domain Entry"): - ["Frog's Domain", "Frog's Domain Back", "Frog's Domain Entry"], - ("Library Exterior Ladder Region", "Library Exterior Tree Region"): - ["Library Exterior Ladder Region", "Library Exterior Tree Region"], - ("Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"): - ["Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"], - ("Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"): - ["Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"], - ("Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"): - ["Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"], - ("Fortress Courtyard Upper",): - ["Fortress Courtyard Upper", "Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard"], - ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Beneath the Vault Entry"): - ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Beneath the Vault Entry"], - ("Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"): - ["Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"], - ("Fortress East Shortcut Upper",): - ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], - ("Eastern Vault Fortress",): - ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], - ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"): - ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"], - ("Fortress Arena", "Fortress Arena Portal"): - ["Fortress Arena", "Fortress Arena Portal"], - ("Lower Mountain", "Lower Mountain Stairs"): - ["Lower Mountain", "Lower Mountain Stairs"], - ("Monastery Front",): - ["Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"], - ("Monastery Back", "Monastery Hero's Grave Region"): - ["Monastery Back", "Monastery Hero's Grave Region"], - ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Even Lower Quarry"): - ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Lower Quarry Zig Door", "Even Lower Quarry"], - ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", - "Lower Quarry Zig Door", "Even Lower Quarry"], - ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): - ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], - ("Rooted Ziggurat Middle Top",): - ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], - ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): - ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], - ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): - ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], - ("Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp Ledge under Cathedral Door"): - ["Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", - "Swamp Ledge under Cathedral Door"], - ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region"): - ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region"], - ("Cathedral Gauntlet Checkpoint",): - ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Cathedral Gauntlet Exit",): - ["Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", - "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"): - ["Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", - "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"] -} - - -dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = { - ("Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", - "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", - "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Ruined Passage Door", "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", - "East Overworld", "Upper Overworld", "Overworld after Temple Rafters", "Overworld Quarry Entry", - "Overworld above Patrol Cave", "Overworld at Patrol Cave", "Overworld to West Garden Upper", - "Overworld Well Ladder", "Overworld Beach", "Overworld to Atoll Upper", "Overworld above Quarry Entrance", - "Overworld after Envoy", "Overworld Tunnel Turret"): - ["Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", - "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", - "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", - "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", "East Overworld", - "Upper Overworld", "Overworld after Temple Rafters", "Overworld Quarry Entry", "Overworld above Patrol Cave", - "Overworld at Patrol Cave", "Overworld to West Garden Upper", "Overworld Well Ladder", "Overworld Beach", - "Overworld to Atoll Upper", "Overworld above Quarry Entrance", "Overworld after Envoy", - "Overworld Tunnel Turret"], - # can laurels through the gate - ("Old House Front", "Old House Back"): - ["Old House Front", "Old House Back"], - ("Hourglass Cave",): - ["Hourglass Cave", "Hourglass Cave Tower"], - ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): - ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], - ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], - ("Forest Belltower Upper",): - ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], - ("Forest Belltower Main",): - ["Forest Belltower Main", "Forest Belltower Lower"], - ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"): - ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"], - ("Guard House 1 East", "Guard House 1 West"): - ["Guard House 1 East", "Guard House 1 West"], - ("Guard House 2 Upper", "Guard House 2 Lower"): - ["Guard House 2 Upper", "Guard House 2 Lower"], - ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): - ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], - ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"): - ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"], - ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"): - ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"], - ("Dark Tomb Checkpoint", "Well Boss"): - ["Dark Tomb Checkpoint", "Well Boss"], - ("West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region", - "West Garden Portal", "West Garden Portal Item"): - ["West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region", - "West Garden Portal", "West Garden Portal Item"], - ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"): - ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"], - ("Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"): - ["Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"], - ("Frog's Domain", "Frog's Domain Entry"): - ["Frog's Domain", "Frog's Domain Back", "Frog's Domain Entry"], - ("Library Exterior Ladder Region", "Library Exterior Tree Region"): - ["Library Exterior Ladder Region", "Library Exterior Tree Region"], - ("Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"): - ["Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"], - ("Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"): - ["Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"], - ("Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"): - ["Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"], - ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper", "Beneath the Vault Entry"): - ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper", "Beneath the Vault Entry"], - ("Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"): - ["Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"], - ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): - ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], - ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): - ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], - ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"): - ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"], - ("Fortress Grave Path Upper",): - ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", - "Fortress Hero's Grave Region"], - ("Fortress Arena", "Fortress Arena Portal"): - ["Fortress Arena", "Fortress Arena Portal"], - ("Lower Mountain", "Lower Mountain Stairs"): - ["Lower Mountain", "Lower Mountain Stairs"], - ("Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"): - ["Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"], - ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Even Lower Quarry"): - ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Lower Quarry Zig Door", "Even Lower Quarry"], - ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", - "Lower Quarry Zig Door", "Even Lower Quarry"], - ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): - ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], - ("Rooted Ziggurat Middle Top",): - ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], - ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): - ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], - ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): - ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], - ("Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", - "Swamp Ledge under Cathedral Door"): - ["Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", - "Swamp Ledge under Cathedral Door"], - ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region"): - ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region", "Swamp Front", "Swamp Mid", - "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", - "Swamp Ledge under Cathedral Door"], - ("Cathedral Gauntlet Checkpoint",): - ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Cathedral Gauntlet Exit",): - ["Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", - "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"): - ["Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", - "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"] -} - +traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { + "Overworld": { + "Overworld Beach": + [], + "Overworld to Atoll Upper": + [["Hyperdash"]], + "Overworld Belltower": + [["Hyperdash"], ["UR"]], + "Overworld Swamp Upper Entry": + [["Hyperdash"], ["UR"]], + "Overworld Swamp Lower Entry": + [], + "Overworld Special Shop Entry": + [["Hyperdash"], ["UR"]], + "Overworld Well Ladder": + [], + "Overworld Ruined Passage Door": + [], + "After Ruined Passage": + [], + "Above Ruined Passage": + [], + "East Overworld": + [], + "Overworld above Patrol Cave": + [], + "Overworld above Quarry Entrance": + [], + "Overworld after Envoy": + [], + "Overworld Quarry Entry": + [["NMG"]], + "Overworld Tunnel Turret": + [["NMG"], ["Hyperdash"]], + "Overworld Temple Door": + [["NMG"], ["Forest Belltower Upper", "Overworld Belltower"]], + "Overworld Southeast Cross Door": + [], + "Overworld Fountain Cross Door": + [], + "Overworld Town Portal": + [], + "Overworld Spawn Portal": + [], + "Overworld Well to Furnace Rail": + [["UR"]], + "Overworld Old House Door": + [], + }, + "East Overworld": { + "Above Ruined Passage": + [], + "After Ruined Passage": + [["NMG"]], + "Overworld": + [], + "Overworld at Patrol Cave": + [], + "Overworld above Patrol Cave": + [], + "Overworld Special Shop Entry": + [["Hyperdash"], ["UR"]] + }, + "Overworld Special Shop Entry": { + "East Overworld": + [["Hyperdash"]] + }, + "Overworld Belltower": { + "Overworld Belltower at Bell": + [], + "Overworld": + [], + "Overworld to West Garden Upper": + [], + }, + "Overworld to West Garden Upper": { + "Overworld Belltower": + [], + }, + "Overworld Swamp Upper Entry": { + "Overworld": + [], + }, + "Overworld Swamp Lower Entry": { + "Overworld": + [], + }, + "Overworld Beach": { + "Overworld": + [], + "Overworld West Garden Laurels Entry": + [["Hyperdash"]], + "Overworld to Atoll Upper": + [], + "Overworld Tunnel Turret": + [], + }, + "Overworld West Garden Laurels Entry": { + "Overworld Beach": + [["Hyperdash"]], + }, + "Overworld to Atoll Upper": { + "Overworld": + [], + "Overworld Beach": + [], + }, + "Overworld Tunnel Turret": { + "Overworld": + [], + "Overworld Beach": + [], + }, + "Overworld Well Ladder": { + "Overworld": + [], + }, + "Overworld at Patrol Cave": { + "East Overworld": + [["Hyperdash"]], + "Overworld above Patrol Cave": + [], + }, + "Overworld above Patrol Cave": { + "Overworld": + [], + "East Overworld": + [], + "Upper Overworld": + [], + "Overworld at Patrol Cave": + [], + "Overworld Belltower at Bell": + [["NMG"]], + }, + "Upper Overworld": { + "Overworld above Patrol Cave": + [], + "Overworld above Quarry Entrance": + [], + "Overworld after Temple Rafters": + [], + }, + "Overworld after Temple Rafters": { + "Upper Overworld": + [], + }, + "Overworld above Quarry Entrance": { + "Overworld": + [], + "Upper Overworld": + [], + }, + "Overworld Quarry Entry": { + "Overworld after Envoy": + [], + "Overworld": + [["NMG"]], + }, + "Overworld after Envoy": { + "Overworld": + [], + "Overworld Quarry Entry": + [], + }, + "After Ruined Passage": { + "Overworld": + [], + "Above Ruined Passage": + [], + "East Overworld": + [["NMG"]], + }, + "Above Ruined Passage": { + "Overworld": + [], + "After Ruined Passage": + [], + "East Overworld": + [], + }, + "Overworld Ruined Passage Door": { + "Overworld": + [["Hyperdash", "NMG"]], + }, + "Overworld Town Portal": { + "Overworld": + [], + }, + "Overworld Spawn Portal": { + "Overworld": + [], + }, + "Old House Front": { + "Old House Back": + [], + }, + "Old House Back": { + "Old House Front": + [["Hyperdash", "NMG"]], + }, + "Furnace Fuse": { + "Furnace Ladder Area": + [["Hyperdash"]], + }, + "Furnace Ladder Area": { + "Furnace Fuse": + [["Hyperdash"], ["UR"]], + "Furnace Walking Path": + [["Hyperdash"], ["UR"]], + }, + "Furnace Walking Path": { + "Furnace Ladder Area": + [["Hyperdash"]], + }, + "Sealed Temple": { + "Sealed Temple Rafters": + [], + }, + "Sealed Temple Rafters": { + "Sealed Temple": + [["Hyperdash"]], + }, + "Hourglass Cave": { + "Hourglass Cave Tower": + [], + }, + "Forest Belltower Upper": { + "Forest Belltower Main": + [], + }, + "Forest Belltower Main": { + "Forest Belltower Lower": + [], + }, + "East Forest": { + "East Forest Dance Fox Spot": + [["Hyperdash"], ["NMG"]], + "East Forest Portal": + [], + "Lower Forest": + [], + }, + "East Forest Dance Fox Spot": { + "East Forest": + [["Hyperdash"], ["NMG"]], + }, + "East Forest Portal": { + "East Forest": + [], + }, + "Lower Forest": { + "East Forest": + [], + }, + "Guard House 1 East": { + "Guard House 1 West": + [], + }, + "Guard House 1 West": { + "Guard House 1 East": + [["Hyperdash"], ["UR"]], + }, + "Guard House 2 Upper": { + "Guard House 2 Lower": + [], + }, + "Guard House 2 Lower": { + "Guard House 2 Upper": + [], + }, + "Forest Grave Path Main": { + "Forest Grave Path Upper": + [["Hyperdash"], ["UR"]], + "Forest Grave Path by Grave": + [], + }, + "Forest Grave Path Upper": { + "Forest Grave Path Main": + [["Hyperdash"], ["NMG"]], + }, + "Forest Grave Path by Grave": { + "Forest Hero's Grave": + [], + "Forest Grave Path Main": + [["NMG"]], + }, + "Forest Hero's Grave": { + "Forest Grave Path by Grave": + [], + }, + "Beneath the Well Ladder Exit": { + "Beneath the Well Front": + [], + }, + "Beneath the Well Front": { + "Beneath the Well Ladder Exit": + [], + "Beneath the Well Main": + [], + }, + "Beneath the Well Main": { + "Beneath the Well Front": + [], + "Beneath the Well Back": + [], + }, + "Beneath the Well Back": { + "Beneath the Well Main": + [], + }, + "Well Boss": { + "Dark Tomb Checkpoint": + [], + }, + "Dark Tomb Checkpoint": { + "Well Boss": + [["Hyperdash", "NMG"]], + }, + "Dark Tomb Entry Point": { + "Dark Tomb Upper": + [], + }, + "Dark Tomb Upper": { + "Dark Tomb Entry Point": + [], + "Dark Tomb Main": + [], + }, + "Dark Tomb Main": { + "Dark Tomb Upper": + [], + "Dark Tomb Dark Exit": + [], + }, + "Dark Tomb Dark Exit": { + "Dark Tomb Main": + [], + }, + "West Garden": { + "West Garden Laurels Exit Region": + [["Hyperdash"], ["UR"]], + "West Garden after Boss": + [], + "West Garden Hero's Grave Region": + [], + "West Garden Portal Item": + [["NMG"]], + }, + "West Garden Laurels Exit Region": { + "West Garden": + [["Hyperdash"]], + }, + "West Garden after Boss": { + "West Garden": + [["Hyperdash"]], + }, + "West Garden Portal Item": { + "West Garden": + [["NMG"]], + "West Garden Portal": + [["Hyperdash", "West Garden"]], + }, + "West Garden Portal": { + "West Garden Portal Item": + [["Hyperdash"]], + }, + "West Garden Hero's Grave Region": { + "West Garden": + [], + }, + "Ruined Atoll": { + "Ruined Atoll Lower Entry Area": + [["Hyperdash"], ["UR"]], + "Ruined Atoll Ladder Tops": + [], + "Ruined Atoll Frog Mouth": + [], + "Ruined Atoll Frog Eye": + [], + "Ruined Atoll Portal": + [], + "Ruined Atoll Statue": + [], + }, + "Ruined Atoll Lower Entry Area": { + "Ruined Atoll": + [], + }, + "Ruined Atoll Ladder Tops": { + "Ruined Atoll": + [], + }, + "Ruined Atoll Frog Mouth": { + "Ruined Atoll": + [], + }, + "Ruined Atoll Frog Eye": { + "Ruined Atoll": + [], + }, + "Ruined Atoll Portal": { + "Ruined Atoll": + [], + }, + "Ruined Atoll Statue": { + "Ruined Atoll": + [], + }, + "Frog Stairs Eye Exit": { + "Frog Stairs Upper": + [], + }, + "Frog Stairs Upper": { + "Frog Stairs Eye Exit": + [], + "Frog Stairs Lower": + [], + }, + "Frog Stairs Lower": { + "Frog Stairs Upper": + [], + "Frog Stairs to Frog's Domain": + [], + }, + "Frog Stairs to Frog's Domain": { + "Frog Stairs Lower": + [], + }, + "Frog's Domain Entry": { + "Frog's Domain": + [], + }, + "Frog's Domain": { + "Frog's Domain Entry": + [], + "Frog's Domain Back": + [], + }, + "Library Exterior Ladder Region": { + "Library Exterior Tree Region": + [], + }, + "Library Exterior Tree Region": { + "Library Exterior Ladder Region": + [], + }, + "Library Hall Bookshelf": { + "Library Hall": + [], + }, + "Library Hall": { + "Library Hall Bookshelf": + [], + "Library Hero's Grave Region": + [], + }, + "Library Hero's Grave Region": { + "Library Hall": + [], + }, + "Library Hall to Rotunda": { + "Library Hall": + [], + }, + "Library Rotunda to Hall": { + "Library Rotunda": + [], + }, + "Library Rotunda": { + "Library Rotunda to Hall": + [], + "Library Rotunda to Lab": + [], + }, + "Library Rotunda to Lab": { + "Library Rotunda": + [], + }, -dependent_regions_ur: Dict[Tuple[str, ...], List[str]] = { - # can use ladder storage to get to the well rail - ("Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", - "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", - "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Ruined Passage Door", "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", - "East Overworld", "Upper Overworld", "Overworld after Temple Rafters", "Overworld Quarry Entry", - "Overworld above Patrol Cave", "Overworld at Patrol Cave", "Overworld to West Garden Upper", - "Overworld Well Ladder", "Overworld Beach", "Overworld to Atoll Upper", "Overworld above Quarry Entrance", - "Overworld after Envoy", "Overworld Tunnel Turret"): - ["Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", - "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", - "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Ruined Passage Door", "Overworld Swamp Lower Entry", "After Ruined Passage", - "Above Ruined Passage", "East Overworld", "Upper Overworld", "Overworld after Temple Rafters", - "Overworld Quarry Entry", "Overworld above Patrol Cave", "Overworld at Patrol Cave", - "Overworld to West Garden Upper", "Overworld Well Ladder", "Overworld Beach", "Overworld to Atoll Upper", - "Overworld above Quarry Entrance", "Overworld after Envoy", "Overworld Tunnel Turret"], - # can laurels through the gate - ("Old House Front", "Old House Back"): - ["Old House Front", "Old House Back"], - ("Hourglass Cave",): - ["Hourglass Cave", "Hourglass Cave Tower"], - ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): - ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], - ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], - ("Forest Belltower Upper",): - ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], - ("Forest Belltower Main",): - ["Forest Belltower Main", "Forest Belltower Lower"], - ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"): - ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"], - ("Guard House 1 East", "Guard House 1 West"): - ["Guard House 1 East", "Guard House 1 West"], - ("Guard House 2 Upper", "Guard House 2 Lower"): - ["Guard House 2 Upper", "Guard House 2 Lower"], - # can use laurels, ice grapple, or ladder storage to traverse - ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): - ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], - ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"): - ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"], - ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"): - ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"], - ("Dark Tomb Checkpoint", "Well Boss"): - ["Dark Tomb Checkpoint", "Well Boss"], - # can ice grapple from portal area to the rest, and vice versa - ("West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region", - "West Garden Portal", "West Garden Portal Item"): - ["West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region", - "West Garden Portal", "West Garden Portal Item"], - ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"): - ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"], - ("Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"): - ["Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"], - ("Frog's Domain", "Frog's Domain Entry"): - ["Frog's Domain", "Frog's Domain Back", "Frog's Domain Entry"], - ("Library Exterior Ladder Region", "Library Exterior Tree Region"): - ["Library Exterior Ladder Region", "Library Exterior Tree Region"], - ("Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"): - ["Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"], - ("Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"): - ["Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"], - ("Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"): - ["Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"], - # can use ice grapple or ladder storage to get from any ladder to upper - ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper", "Beneath the Vault Entry"): - ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper", "Beneath the Vault Entry"], - ("Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"): - ["Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"], - # can ice grapple up - ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): - ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], - ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): - ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], - ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"): - ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"], - # can ice grapple down - ("Fortress Grave Path Upper",): - ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", - "Fortress Hero's Grave Region"], - ("Fortress Arena", "Fortress Arena Portal"): - ["Fortress Arena", "Fortress Arena Portal"], - ("Lower Mountain", "Lower Mountain Stairs"): - ["Lower Mountain", "Lower Mountain Stairs"], - ("Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"): - ["Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"], - # can use ladder storage at any of the Quarry ladders to get to Monastery Rope - ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Monastery Rope", "Even Lower Quarry"): - ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Monastery Rope", "Lower Quarry Zig Door", "Even Lower Quarry"], - ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): - ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], - ("Rooted Ziggurat Middle Top",): - ["Rooted Ziggurat Middle Top", "Rooted Ziggurat Middle Bottom"], - ("Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"): - ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], - ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): - ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], - ("Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", - "Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region", "Swamp Ledge under Cathedral Door"): - ["Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", - "Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region", - "Swamp Ledge under Cathedral Door"], - ("Cathedral Gauntlet Checkpoint",): - ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Cathedral Gauntlet Exit",): - ["Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", - "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"): - ["Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", - "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"] + "Library Lab Lower": { + "Library Lab": + [], + }, + "Library Lab": { + "Library Lab Lower": + [["Hyperdash"]], + "Library Portal": + [], + "Library Lab to Librarian": + [], + }, + "Library Portal": { + "Library Lab": + [], + }, + "Library Lab to Librarian": { + "Library Lab": + [], + }, + "Fortress Exterior from East Forest": { + "Fortress Exterior from Overworld": + [], + "Fortress Courtyard Upper": + [["UR"]], + "Fortress Exterior near cave": + [["UR"]], + "Fortress Courtyard": + [["UR"]], + }, + "Fortress Exterior from Overworld": { + "Fortress Exterior from East Forest": + [["Hyperdash"]], + "Fortress Exterior near cave": + [], + "Fortress Courtyard": + [["Hyperdash"], ["NMG"]], + }, + "Fortress Exterior near cave": { + "Fortress Exterior from Overworld": + [["Hyperdash"], ["UR"]], + "Fortress Courtyard": + [["UR"]], + "Fortress Courtyard Upper": + [["UR"]], + "Beneath the Vault Entry": + [], + }, + "Beneath the Vault Entry": { + "Fortress Exterior near cave": + [], + }, + "Fortress Courtyard": { + "Fortress Courtyard Upper": + [["NMG"]], + "Fortress Exterior from Overworld": + [["Hyperdash"]], + }, + "Fortress Courtyard Upper": { + "Fortress Courtyard": + [], + }, + "Beneath the Vault Ladder Exit": { + "Beneath the Vault Main": + [], + }, + "Beneath the Vault Main": { + "Beneath the Vault Ladder Exit": + [], + "Beneath the Vault Back": + [], + }, + "Beneath the Vault Back": { + "Beneath the Vault Main": + [], + "Beneath the Vault Ladder Exit": + [], + }, + "Fortress East Shortcut Lower": { + "Fortress East Shortcut Upper": + [["NMG"]], + }, + "Fortress East Shortcut Upper": { + "Fortress East Shortcut Lower": + [], + }, + "Eastern Vault Fortress": { + "Eastern Vault Fortress Gold Door": + [["NMG"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], + }, + "Eastern Vault Fortress Gold Door": { + "Eastern Vault Fortress": + [["NMG"]], + }, + "Fortress Grave Path": { + "Fortress Hero's Grave Region": + [], + "Fortress Grave Path Dusty Entrance Region": + [["Hyperdash"]], + }, + "Fortress Grave Path Upper": { + "Fortress Grave Path": + [["NMG"]], + }, + "Fortress Grave Path Dusty Entrance Region": { + "Fortress Grave Path": + [["Hyperdash"]], + }, + "Fortress Hero's Grave Region": { + "Fortress Grave Path": + [], + }, + "Fortress Arena": { + "Fortress Arena Portal": + [["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], + }, + "Fortress Arena Portal": { + "Fortress Arena": + [], + }, + "Lower Mountain": { + "Lower Mountain Stairs": + [], + }, + "Lower Mountain Stairs": { + "Lower Mountain": + [], + }, + "Monastery Back": { + "Monastery Front": + [["Hyperdash", "NMG"]], + "Monastery Hero's Grave Region": + [], + }, + "Monastery Hero's Grave Region": { + "Monastery Back": + [], + }, + "Monastery Front": { + "Monastery Back": + [], + }, + "Quarry Entry": { + "Quarry Portal": + [["Quarry Connector"]], + "Quarry": + [], + }, + "Quarry Portal": { + "Quarry Entry": + [], + }, + "Quarry Monastery Entry": { + "Quarry": + [], + "Quarry Back": + [["Hyperdash"]], + "Monastery Rope": + [["UR"]], + }, + "Quarry Back": { + "Quarry": + [], + "Quarry Monastery Entry": + [["Hyperdash"]], + }, + "Quarry": { + "Lower Quarry": + [], + "Quarry Entry": + [], + "Quarry Back": + [], + "Quarry Monastery Entry": + [], + "Lower Quarry Zig Door": + [["NMG"]], + }, + "Lower Quarry": { + "Even Lower Quarry": + [], + }, + "Even Lower Quarry": { + "Lower Quarry": + [], + "Lower Quarry Zig Door": + [["Quarry", "Quarry Connector"], ["NMG"]], + }, + "Monastery Rope": { + "Quarry Back": + [], + }, + "Rooted Ziggurat Upper Entry": { + "Rooted Ziggurat Upper Front": + [], + }, + "Rooted Ziggurat Upper Front": { + "Rooted Ziggurat Upper Back": + [], + }, + "Rooted Ziggurat Upper Back": { + "Rooted Ziggurat Upper Front": + [["Hyperdash"]], + }, + "Rooted Ziggurat Middle Top": { + "Rooted Ziggurat Middle Bottom": + [], + }, + "Rooted Ziggurat Lower Front": { + "Rooted Ziggurat Lower Back": + [], + }, + "Rooted Ziggurat Lower Back": { + "Rooted Ziggurat Lower Front": + [["Hyperdash"], ["UR"]], + "Rooted Ziggurat Portal Room Entrance": + [], + }, + "Zig Skip Exit": { + "Rooted Ziggurat Lower Front": + [], + }, + "Rooted Ziggurat Portal Room Entrance": { + "Rooted Ziggurat Lower Back": + [], + }, + "Rooted Ziggurat Portal Room Exit": { + "Rooted Ziggurat Portal": + [], + }, + "Rooted Ziggurat Portal": { + "Rooted Ziggurat Portal Room Exit": + [["Rooted Ziggurat Lower Back"]], + }, + "Swamp Front": { + "Swamp Mid": + [], + }, + "Swamp Mid": { + "Swamp Front": + [], + "Swamp to Cathedral Main Entrance Region": + [["Hyperdash"], ["NMG"]], + "Swamp Ledge under Cathedral Door": + [], + "Back of Swamp": + [["UR"]], + }, + "Swamp Ledge under Cathedral Door": { + "Swamp Mid": + [], + "Swamp to Cathedral Treasure Room": + [], + }, + "Swamp to Cathedral Treasure Room": { + "Swamp Ledge under Cathedral Door": + [], + }, + "Swamp to Cathedral Main Entrance Region": { + "Swamp Mid": + [["NMG"]], + }, + "Back of Swamp": { + "Back of Swamp Laurels Area": + [["Hyperdash"], ["UR"]], + "Swamp Hero's Grave Region": + [], + }, + "Back of Swamp Laurels Area": { + "Back of Swamp": + [["Hyperdash"]], + "Swamp Mid": + [["NMG", "Hyperdash"]], + }, + "Swamp Hero's Grave Region": { + "Back of Swamp": + [], + }, + "Cathedral Gauntlet Checkpoint": { + "Cathedral Gauntlet": + [], + }, + "Cathedral Gauntlet": { + "Cathedral Gauntlet Exit": + [["Hyperdash"]], + }, + "Cathedral Gauntlet Exit": { + "Cathedral Gauntlet": + [["Hyperdash"]], + }, + "Far Shore": { + "Far Shore to Spawn Region": + [["Hyperdash"]], + "Far Shore to East Forest Region": + [["Hyperdash"]], + "Far Shore to Quarry Region": + [["Quarry Connector", "Quarry"]], + "Far Shore to Library Region": + [["Library Lab"]], + "Far Shore to West Garden Region": + [["West Garden"]], + "Far Shore to Fortress Region": + [["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], + }, + "Far Shore to Spawn Region": { + "Far Shore": + [["Hyperdash"]], + }, + "Far Shore to East Forest Region": { + "Far Shore": + [["Hyperdash"]], + }, + "Far Shore to Quarry Region": { + "Far Shore": + [], + }, + "Far Shore to Library Region": { + "Far Shore": + [], + }, + "Far Shore to West Garden Region": { + "Far Shore": + [], + }, + "Far Shore to Fortress Region": { + "Far Shore": + [], + }, } diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 6352d96bf407..08eb73a3b010 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -268,7 +268,8 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Overworld Well Ladder"], rule=lambda state: has_ladder("Ladders in Well", state, player, options)) regions["Overworld Well Ladder"].connect( - connecting_region=regions["Overworld"]) + connecting_region=regions["Overworld"], + rule=lambda state: has_ladder("Ladders in Well", state, player, options)) # nmg: can ice grapple through the door regions["Overworld"].connect( @@ -706,17 +707,18 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Fortress Exterior from Overworld"]) regions["Beneath the Vault Ladder Exit"].connect( - connecting_region=regions["Beneath the Vault Front"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) - regions["Beneath the Vault Front"].connect( + connecting_region=regions["Beneath the Vault Main"], + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options) + and has_lantern(state, player, options)) + regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Ladder Exit"], rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) - regions["Beneath the Vault Front"].connect( - connecting_region=regions["Beneath the Vault Back"], - rule=lambda state: has_lantern(state, player, options)) + regions["Beneath the Vault Main"].connect( + connecting_region=regions["Beneath the Vault Back"]) regions["Beneath the Vault Back"].connect( - connecting_region=regions["Beneath the Vault Front"]) + connecting_region=regions["Beneath the Vault Main"], + rule=lambda state: has_lantern(state, player, options)) regions["Fortress East Shortcut Upper"].connect( connecting_region=regions["Fortress East Shortcut Lower"]) @@ -870,6 +872,9 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Rooted Ziggurat Portal Room Entrance"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"]) + regions["Zig Skip Exit"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"]) + regions["Rooted Ziggurat Portal"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Exit"], rule=lambda state: state.has("Activate Ziggurat Fuse", player)) @@ -1453,8 +1458,6 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) # Beneath the Vault set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) - set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), - lambda state: has_lantern(state, player, options)) # Quarry set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 323ccf421764..3abdfecce233 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,12 +1,12 @@ from typing import Dict, List, Set, TYPE_CHECKING from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table -from .er_data import Portal, tunic_er_regions, portal_mapping, \ - dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur +from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd from .er_rules import set_er_region_rules from .options import EntranceRando from worlds.generic import PlandoConnection from random import Random +from copy import deepcopy if TYPE_CHECKING: from . import TunicWorld @@ -95,7 +95,8 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: def vanilla_portals() -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} - portal_map = portal_mapping.copy() + # we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here + portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"] while portal_map: portal1 = portal_map[0] @@ -130,9 +131,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: dead_ends: List[Portal] = [] two_plus: List[Portal] = [] player_name = world.multiworld.get_player_name(world.player) + portal_map = portal_mapping.copy() logic_rules = world.options.logic_rules.value fixed_shop = world.options.fixed_shop laurels_location = world.options.laurels_location + traversal_reqs = deepcopy(traversal_requirements) + has_laurels = True + waterfall_plando = False # if it's not one of the EntranceRando options, it's a custom seed if world.options.entrance_rando.value not in EntranceRando.options: @@ -140,38 +145,53 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: logic_rules = seed_group["logic_rules"] fixed_shop = seed_group["fixed_shop"] laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False - + + # marking that you don't immediately have laurels + if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"): + has_laurels = False + shop_scenes: Set[str] = set() shop_count = 6 if fixed_shop: - shop_count = 1 + shop_count = 0 shop_scenes.add("Overworld Redux") - - if not logic_rules: - dependent_regions = dependent_regions_restricted - elif logic_rules == 1: - dependent_regions = dependent_regions_nmg else: - dependent_regions = dependent_regions_ur + # if fixed shop is off, remove this portal + for portal in portal_map: + if portal.region == "Zig Skip Exit": + portal_map.remove(portal) + break # create separate lists for dead ends and non-dead ends - if logic_rules: - for portal in portal_mapping: - if tunic_er_regions[portal.region].dead_end == 1: - dead_ends.append(portal) - else: + for portal in portal_map: + dead_end_status = tunic_er_regions[portal.region].dead_end + if dead_end_status == DeadEnd.free: + two_plus.append(portal) + elif dead_end_status == DeadEnd.all_cats: + dead_ends.append(portal) + elif dead_end_status == DeadEnd.restricted: + if logic_rules: two_plus.append(portal) - else: - for portal in portal_mapping: - if tunic_er_regions[portal.region].dead_end: - dead_ends.append(portal) else: - two_plus.append(portal) + dead_ends.append(portal) + # these two get special handling + elif dead_end_status == DeadEnd.special: + if portal.region == "Secret Gathering Place": + if laurels_location == "10_fairies": + two_plus.append(portal) + else: + dead_ends.append(portal) + if portal.region == "Zig Skip Exit": + if fixed_shop: + two_plus.append(portal) + else: + dead_ends.append(portal) connected_regions: Set[str] = set() # make better start region stuff when/if implementing random start start_region = "Overworld" - connected_regions.update(add_dependent_regions(start_region, logic_rules)) + connected_regions.add(start_region) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) if world.options.entrance_rando.value in EntranceRando.options: plando_connections = world.multiworld.plando_connections[world.player] @@ -205,11 +225,17 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: non_dead_end_regions.add(region_name) elif region_info.dead_end == 2 and logic_rules: non_dead_end_regions.add(region_name) + elif region_info.dead_end == 3: + if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \ + or (region_name == "Zig Skip Exit" and fixed_shop): + non_dead_end_regions.add(region_name) if plando_connections: for connection in plando_connections: p_entrance = connection.entrance p_exit = connection.exit + portal1_dead_end = True + portal2_dead_end = True portal1 = None portal2 = None @@ -218,8 +244,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: for portal in two_plus: if p_entrance == portal.name: portal1 = portal + portal1_dead_end = False if p_exit == portal.name: portal2 = portal + portal2_dead_end = False # search dead_ends individually since we can't really remove items from two_plus during the loop if portal1: @@ -233,7 +261,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: else: raise Exception(f"{player_name} paired a dead end to a dead end in their " "plando connections.") - + for portal in dead_ends: if p_entrance == portal.name: portal1 = portal @@ -246,7 +274,6 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: if portal2: two_plus.remove(portal2) else: - # check if portal2 is a dead end for portal in dead_ends: if p_exit == portal.name: portal2 = portal @@ -256,6 +283,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") shop_count -= 1 + # need to maintain an even number of portals total if shop_count < 0: shop_count += 2 for p in portal_mapping: @@ -269,48 +297,36 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: f"plando connections in {player_name}'s YAML.") dead_ends.remove(portal2) - portal_pairs[portal1] = portal2 + # update the traversal chart to say you can get from portal1's region to portal2's and vice versa + if not portal1_dead_end and not portal2_dead_end: + traversal_reqs.setdefault(portal1.region, dict())[portal2.region] = [] + traversal_reqs.setdefault(portal2.region, dict())[portal1.region] = [] - # update dependent regions based on the plando'd connections, to ensure the portals connect well, logically - for origins, destinations in dependent_regions.items(): - if portal1.region in origins: - if portal2.region in non_dead_end_regions: - destinations.append(portal2.region) - if portal2.region in origins: - if portal1.region in non_dead_end_regions: - destinations.append(portal1.region) + if portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit": + if portal1_dead_end or portal2_dead_end or \ + portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place": + if world.options.entrance_rando.value not in EntranceRando.options: + raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " + "end to a dead end in their plando connections.") + else: + raise Exception(f"{player_name} paired a dead end to a dead end in their " + "plando connections.") + + if portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place": + # need to make sure you didn't pair this to a dead end or zig skip + if portal1_dead_end or portal2_dead_end or \ + portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit": + if world.options.entrance_rando.value not in EntranceRando.options: + raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " + "end to a dead end in their plando connections.") + else: + raise Exception(f"{player_name} paired a dead end to a dead end in their " + "plando connections.") + waterfall_plando = True + portal_pairs[portal1] = portal2 # if we have plando connections, our connected regions may change somewhat - while True: - test1 = len(connected_regions) - for region in connected_regions.copy(): - connected_regions.update(add_dependent_regions(region, logic_rules)) - test2 = len(connected_regions) - if test1 == test2: - break - - # need to plando fairy cave, or it could end up laurels locked - # fix this later to be random after adding some item logic to dependent regions - if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"): - portal1 = None - portal2 = None - for portal in two_plus: - if portal.scene_destination() == "Overworld Redux, Waterfall_": - portal1 = portal - break - for portal in dead_ends: - if portal.scene_destination() == "Waterfall, Overworld Redux_": - portal2 = portal - break - if not portal1: - raise Exception(f"Failed to do Laurels Location at 10 Fairies option. " - f"Did {player_name} plando connection the Secret Gathering Place Entrance?") - if not portal2: - raise Exception(f"Failed to do Laurels Location at 10 Fairies option. " - f"Did {player_name} plando connection the Secret Gathering Place Exit?") - portal_pairs[portal1] = portal2 - two_plus.remove(portal1) - dead_ends.remove(portal2) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"): portal1 = None @@ -339,47 +355,54 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: previous_conn_num = 0 fail_count = 0 while len(connected_regions) < len(non_dead_end_regions): - # if the connected regions length stays unchanged for too long, it's stuck in a loop - # should, hopefully, only ever occur if someone plandos connections poorly + # if this is universal tracker, just break immediately and move on if hasattr(world.multiworld, "re_gen_passthrough"): break + # if the connected regions length stays unchanged for too long, it's stuck in a loop + # should, hopefully, only ever occur if someone plandos connections poorly if previous_conn_num == len(connected_regions): fail_count += 1 if fail_count >= 500: - raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for loops.") + raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for errors. " + "Unconnected regions:", non_dead_end_regions - connected_regions) else: fail_count = 0 previous_conn_num = len(connected_regions) - # find a portal in an inaccessible region + # find a portal in a connected region if check_success == 0: for portal in two_plus: if portal.region in connected_regions: - # if there's risk of self-locking, start over - if gate_before_switch(portal, two_plus): - random_object.shuffle(two_plus) - break portal1 = portal two_plus.remove(portal) check_success = 1 break - # then we find a portal in a connected region + # then we find a portal in an inaccessible region if check_success == 1: for portal in two_plus: if portal.region not in connected_regions: - # if there's risk of self-locking, shuffle and try again - if gate_before_switch(portal, two_plus): - random_object.shuffle(two_plus) - break + # if secret gathering place happens to get paired really late, you can end up running out + if not has_laurels and len(two_plus) < 80: + # if you plando'd secret gathering place with laurels at 10 fairies, you're the reason for this + if waterfall_plando: + cr = connected_regions.copy() + cr.add(portal.region) + if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_rules): + continue + elif portal.region != "Secret Gathering Place": + continue portal2 = portal + connected_regions.add(portal.region) two_plus.remove(portal) check_success = 2 break # once we have both portals, connect them and add the new region(s) to connected_regions if check_success == 2: - connected_regions.update(add_dependent_regions(portal2.region, logic_rules)) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) + if "Secret Gathering Place" in connected_regions: + has_laurels = True portal_pairs[portal1] = portal2 check_success = 0 random_object.shuffle(two_plus) @@ -411,7 +434,6 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: portal1 = two_plus.pop() portal2 = dead_ends.pop() portal_pairs[portal1] = portal2 - # then randomly connect the remaining portals to each other # every region is accessible, so gate_before_switch is not necessary while len(two_plus) > 1: @@ -438,126 +460,42 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic region2.connect(connecting_region=region1, name=portal2.name) -# loop through the static connections, return regions you can reach from this region -# todo: refactor to take region_name and dependent_regions -def add_dependent_regions(region_name: str, logic_rules: int) -> Set[str]: - region_set = set() - if not logic_rules: - regions_to_add = dependent_regions_restricted - elif logic_rules == 1: - regions_to_add = dependent_regions_nmg - else: - regions_to_add = dependent_regions_ur - for origin_regions, destination_regions in regions_to_add.items(): - if region_name in origin_regions: - # if you matched something in the first set, you get the regions in its paired set - region_set.update(destination_regions) - return region_set - # if you didn't match anything in the first sets, just gives you the region - region_set = {region_name} - return region_set - - -# we're checking if an event-locked portal is being placed before the regions where its key(s) is/are -# doing this ensures the keys will not be locked behind the event-locked portal -def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool: - # the western belltower cannot be locked since you can access it with laurels - # so we only need to make sure the forest belltower isn't locked - if check_portal.scene_destination() == "Overworld Redux, Temple_main": - i = 0 - for portal in two_plus: - if portal.region == "Forest Belltower Upper": - i += 1 - break - if i == 1: - return True +def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]], + has_laurels: bool, logic: int) -> Set[str]: + # starting count, so we can run it again if this changes + region_count = len(connected_regions) + for origin, destinations in traversal_reqs.items(): + if origin not in connected_regions: + continue + # check if we can traverse to any of the destinations + for destination, req_lists in destinations.items(): + if destination in connected_regions: + continue + met_traversal_reqs = False + if len(req_lists) == 0: + met_traversal_reqs = True + # loop through each set of possible requirements, with a fancy for else loop + for reqs in req_lists: + for req in reqs: + if req == "Hyperdash": + if not has_laurels: + break + elif req == "NMG": + if not logic: + break + elif req == "UR": + if logic < 2: + break + elif req not in connected_regions: + break + else: + met_traversal_reqs = True + break + if met_traversal_reqs: + connected_regions.add(destination) - # fortress big gold door needs 2 scenes and one of the two upper portals of the courtyard - elif check_portal.scene_destination() == "Fortress Main, Fortress Arena_": - i = j = k = 0 - for portal in two_plus: - if portal.region == "Fortress Courtyard Upper": - i += 1 - if portal.scene() == "Fortress Basement": - j += 1 - if portal.region == "Eastern Vault Fortress": - k += 1 - if i == 2 or j == 2 or k == 5: - return True - - # fortress teleporter needs only the left fuses - elif check_portal.scene_destination() in {"Fortress Arena, Transit_teleporter_spidertank", - "Transit, Fortress Arena_teleporter_spidertank"}: - i = j = k = 0 - for portal in two_plus: - if portal.scene() == "Fortress Courtyard": - i += 1 - if portal.scene() == "Fortress Basement": - j += 1 - if portal.region == "Eastern Vault Fortress": - k += 1 - if i == 8 or j == 2 or k == 5: - return True - - # Cathedral door needs Overworld and the front of Swamp - # Overworld is currently guaranteed, so no need to check it - elif check_portal.scene_destination() == "Swamp Redux 2, Cathedral Redux_main": - i = 0 - for portal in two_plus: - if portal.region in {"Swamp Front", "Swamp to Cathedral Treasure Room", - "Swamp to Cathedral Main Entrance Region"}: - i += 1 - if i == 4: - return True - - # Zig portal room exit needs Zig 3 to be accessible to hit the fuse - elif check_portal.scene_destination() == "ziggurat2020_FTRoom, ziggurat2020_3_": - i = 0 - for portal in two_plus: - if portal.scene() == "ziggurat2020_3": - i += 1 - if i == 2: - return True - - # Quarry teleporter needs you to hit the Darkwoods fuse - # Since it's physically in Quarry, we don't need to check for it - elif check_portal.scene_destination() in {"Quarry Redux, Transit_teleporter_quarry teleporter", - "Quarry Redux, ziggurat2020_0_"}: - i = 0 - for portal in two_plus: - if portal.scene() == "Darkwoods Tunnel": - i += 1 - if i == 2: - return True - - # Same as above, but Quarry isn't guaranteed here - elif check_portal.scene_destination() == "Transit, Quarry Redux_teleporter_quarry teleporter": - i = j = 0 - for portal in two_plus: - if portal.scene() == "Darkwoods Tunnel": - i += 1 - if portal.scene() == "Quarry Redux": - j += 1 - if i == 2 or j == 7: - return True - - # Need Library fuse to use this teleporter - elif check_portal.scene_destination() == "Transit, Library Lab_teleporter_library teleporter": - i = 0 - for portal in two_plus: - if portal.scene() == "Library Lab": - i += 1 - if i == 3: - return True - - # Need West Garden fuse to use this teleporter - elif check_portal.scene_destination() == "Transit, Archipelagos Redux_teleporter_archipelagos_teleporter": - i = 0 - for portal in two_plus: - if portal.scene() == "Archipelagos Redux": - i += 1 - if i == 6: - return True + # if the length of connected_regions changed, we got new regions, so we want to check those new origins + if region_count != len(connected_regions): + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic) - # false means you're good to place the portal - return False + return connected_regions diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 6efdeaa3eabb..f470ea540d76 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -237,6 +237,8 @@ def get_item_group(item_name: str) -> str: "Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't "Ladders to Bell": {"Ladders to West Bell"}, "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell + "Ladders in Atoll": {"Ladders in South Atoll"}, + "Ladders in Ruined Atoll": {"Ladders in South Atoll"}, } item_name_groups.update(extra_groups) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index fdf662167953..2d87140fe50f 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -86,7 +86,7 @@ class TunicLocationData(NamedTuple): "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"), "Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), - "Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"), + "Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Main"), "Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"), diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 9af0a0409c01..605bb065fda8 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -118,7 +118,8 @@ class EntranceRando(TextChoice): class FixedShop(Toggle): - """Forces the Windmill entrance to lead to a shop, and places only one other shop in the pool. + """Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool. + Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances. Has no effect if Entrance Rando is not enabled.""" internal_name = "fixed_shop" display_name = "Fewer Shops in Entrance Rando" @@ -126,8 +127,7 @@ class FixedShop(Toggle): class LaurelsLocation(Choice): """Force the Hero's Laurels to be placed at a location in your world. - For if you want to avoid or specify early or late Laurels. - If you use the 10 Fairies option in Entrance Rando, Secret Gathering Place will be at its vanilla entrance.""" + For if you want to avoid or specify early or late Laurels.""" internal_name = "laurels_location" display_name = "Laurels Location" option_anywhere = 0 @@ -147,6 +147,7 @@ class ShuffleLadders(Toggle): @dataclass class TunicOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool sword_progression: SwordProgression start_with_sword: StartWithSword keys_behind_bosses: KeysBehindBosses @@ -162,4 +163,3 @@ class TunicOptions(PerGameCommonOptions): lanternless: Lanternless maskless: Maskless laurels_location: LaurelsLocation - start_inventory_from_pool: StartInventoryPool