Compare commits

..

38 Commits

Author SHA1 Message Date
Exempt-Medic
86a6939f02 Also fix max count 2025-06-12 13:05:00 -04:00
Exempt-Medic
51254948aa Fix plando count value 2025-06-12 12:57:32 -04:00
JaredWeakStrike
52b11083fe KH2: Raise Exception for Misusing DonaldGoofyStatsanity Option (#4710)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-06-11 15:52:47 -04:00
BadMagic100
a8c87ce54b CI: Add GH_REPO environment variable to labeler (#5081) 2025-06-10 05:55:40 +02:00
JaredWeakStrike
ddb3240591 KH2: Give warning when client has cached locations (#5000)
* a

* disconnect when connect to wrong slot

* connection to the wrong seed fix

* seed_name is always none
2025-06-09 14:58:08 +02:00
qwint
f25ef639f2 Launcher: Fix Cli Components when installed to a directory with a space (#5091) 2025-06-09 00:43:23 +02:00
BlastSlimey
ab7d3ce4aa shapez: Remove preset unittests #5086 2025-06-06 00:05:53 +02:00
Jarno
50db922cef Timespinner: Fixed generation error because of timezone locking (#5084)
* Fixed generation error because of timezone locking

* Refactored logic + prevent excluding warps when unchained keys in on
2025-06-05 15:05:00 +02:00
Ehseezed
a2708edc37 Timespinner: Fix Castle Ramparts Region Connection #5082
Co-authored-by: ehseezed <Ehseezed@users.noreply.github.com>
2025-06-04 19:51:08 +02:00
Exempt-Medic
603a5005e2 DS3: Fix Non-Crow Itemlinking and Mark Aldrich Ruby and Twin Dragon Greatshield As Missable (#4510)
* Fix Branch (Not Crow)

* Oops

* Mark Aldrich Ruby as missable

* Expand comment

* Short circuit

* Mark Twin Dragon Greatshield as missable

* Add missable cause
2025-06-03 08:49:10 -04:00
Fabian Dill
b4f68bce76 Factorio: revamp args parsing and passing (#5036) 2025-06-03 13:49:44 +02:00
Scipio Wright
a76cec1539 TUNIC: Fix decoupled ER + ladder storage making invalid entrances #5075 2025-06-03 12:51:06 +02:00
black-sliver
694e6bcae3 Launcher/Utils: reset LD_LIBRARY_PATH for system EXEs (#5022) 2025-06-03 10:42:37 +00:00
black-sliver
b85b18cf5f SoE: remove outdated info from guide (#5064)
The client does not depend on Animation Frame anymore, so it can be backgrounded.
2025-06-02 16:39:42 +00:00
Mysteryem
04c707f874 DKC3: Add missing indirect conditions (#5073)
A couple of Entrance access rules were checking for being able to reach
a Location, but a Location first checks for being able to reach its
parent Region, so it needs to be registered that access to that parent
Region can give access to the Entrance.
2025-06-02 18:06:54 +02:00
Exempt-Medic
99142fd662 Plando Items: Fix count with empty locations/location #5040 2025-06-02 18:01:21 +02:00
Mysteryem
0c5cb17d96 DLCQuest: Add missing indirect conditions (#5074)
The `Behind Rocks` and `Pickaxe Hard Cave` Entrances require being able
to reach the `Cut Content` region, but no indirect conditions were being
registered for this region.

The `set_lfod_self_obtained_items_rules` function was also using a
`world` parameter that was actually expecting a `MultiWorld` instance,
so I have renamed it for clarity and updated the function to use
`world.get_entrance()` rather than `multiworld.get_entrance()`.

Much of the rest of the file passes `MultiWorld` instances to `world`
parameters, but fixing all of these is out of the scope of the changes
in this patch, so has not been included.
2025-06-02 17:56:11 +02:00
qwint
cabde313b5 WebHost: Use expected APPlayerContainer manifest location directly when ingesting them #4754 2025-06-02 17:53:57 +02:00
qwint
8f68bb342d Core and Various Worlds: define patch_file_ending to APPlayerContainer (#5058)
* move to playercontainer

* moves patch_file_ending handling to APPlayerContainer and updates the worlds using it to define their extensions

* give oot a patch_file_ending as well
2025-06-02 17:53:18 +02:00
Jérémie Bolduc
fab75d3a32 Stardew Valley: Fix Wizard Tower and Entrance Randomizer Softlocks (#4631)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-05-31 07:57:42 -04:00
massimilianodelliubaldini
d19bf98dc4 Jak and Daxter: Post-merge Polish (#5031)
- Cleans up a few missed references in the setup guide.
- Refactors Options class to use metaclass and decorators to enforce friendly limits on multiple levels.    
  - Templates generated from the website, even ones with `random` should not fail generation because the website will only allow values inside the friendly limits. 
  - _Uploaded_ yamls to the website with `random`, should also now respect friendly limits without the need for `random-range` shenanigans.
  - _Uploaded_ yamls to the website, or yamls that are used to generate locally, that have hard-defined values outside the friendly limits, will be clamped/dragged/massaged into those limits (with logged warnings).
- Removed an early completion goal that was playing havoc with fill. Not enough people seem to use this goal, so its loss will not be mourned.
2025-05-30 16:31:00 +02:00
sgrunt
b0f41c0360 Timespinner: Fix Connection Logic from Maw Cave Entrance to Maw (#4831)
Co-authored-by: sgrunt <sgrunt1987@gmail.com>
2025-05-28 20:40:24 -04:00
sgrunt
6ebd60feaa Timespinner: Fix Logic Error with Risky Warp to Emperor's Tower and Lab Access (#4784)
Co-authored-by: sgrunt <sgrunt1987@gmail.com>
2025-05-28 20:37:39 -04:00
Jonathan Tan
dd6007b309 TWW: Remove unnecessary items from slot data (#5045) 2025-05-29 00:27:03 +02:00
Ehseezed
fde203379d Timespinner: Fix Logic (#4803)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-05-28 15:04:57 -04:00
LiquidCat64
fcb3efee01 CVCotM: Add Nerf Roc Wing to Slot Data and HoD Max Ups to other_game_item_appearances (#5051) 2025-05-28 10:47:24 -04:00
black-sliver
19a21099ed Webhost: update Flask to 3.1.1 (#5052) 2025-05-27 16:21:43 +00:00
Jonathan Tan
20ca7e71c7 TWW: Update patch class (#5046) 2025-05-27 07:57:20 +02:00
ScootyPuffJr1
002202ff5f Update OOT Guides (#5041)
* Update OOT Guides

* Minor update per review
2025-05-26 07:25:39 +00:00
FlitPix
32487137e8 Core: Add descriptions to Components (#4849)
* Add descriptions to components

* Adhere to style guide

* Tweak BHC wording

* Trim Open Patch description

* Update text client description for consistency

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Remove newlines

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-05-25 17:17:30 -04:00
LiquidCat64
f327ab30a6 CV64: Allow Holding Z to Use the Regular Shimmy Speed (#4730)
* Add the shimmy modifier hack.

* Update the Increase Shimmy Speed option description.

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-05-25 05:20:25 -04:00
agilbert1412
e7545cbc28 SDV: Fixed Region for two Parrot Locations (#5042) 2025-05-24 17:59:55 -04:00
NewSoupVi
eba757d2cd Raft: Implement get_filler_item_name and refactor filler item code a bit (#4782)
* refactor filler item creation for Raft, implement get_filler_item_name

* wrong indent

* Update worlds/raft/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-05-24 23:02:27 +02:00
Star Rauchenberger
4119763e23 Lingo: Fix The Bearer's Pilgrimage Logic (#5005) 2025-05-24 09:35:06 -04:00
Jonathan Tan
e830a6d6f5 TWW: Only add Filler for Excluded Locations Which are Progress Locations (#4993)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-05-24 09:17:54 -04:00
Bryce Wilson
704cd97f21 BizHawkClient: Fix script to list all cores instead of explicit mapping (#5033) 2025-05-24 07:33:01 +02:00
agilbert1412
47a0dd696f Stardew Valley: Added moss to statue of blessings recipe (#5038) 2025-05-24 07:28:25 +02:00
Jérémie Bolduc
c64791e3a8 Stardew Valley: Replace current naive entrance rando with GER (#4624) 2025-05-24 07:15:41 +02:00
84 changed files with 2106 additions and 1621 deletions

View File

@@ -6,6 +6,8 @@ on:
permissions: permissions:
contents: read contents: read
pull-requests: write pull-requests: write
env:
GH_REPO: ${{ github.repository }}
jobs: jobs:
labeler: labeler:

View File

@@ -937,13 +937,16 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
count = block.count count = block.count
if not count: if not count:
count = len(new_block.items) count = (min(len(new_block.items), len(new_block.resolved_locations))
if new_block.resolved_locations else len(new_block.items))
if isinstance(count, int): if isinstance(count, int):
count = {"min": count, "max": count} count = {"min": count, "max": count}
if "min" not in count: if "min" not in count:
count["min"] = 0 count["min"] = 0
if "max" not in count: if "max" not in count:
count["max"] = len(new_block.items) count["max"] = (min(len(new_block.items), len(new_block.resolved_locations))
if new_block.resolved_locations else len(new_block.items))
new_block.count = count new_block.count = count
plando_blocks[player].append(new_block) plando_blocks[player].append(new_block)

View File

@@ -11,6 +11,7 @@ Additional components can be added to worlds.LauncherComponents.components.
import argparse import argparse
import logging import logging
import multiprocessing import multiprocessing
import os
import shlex import shlex
import subprocess import subprocess
import sys import sys
@@ -41,13 +42,17 @@ def open_host_yaml():
if is_linux: if is_linux:
exe = which('sensible-editor') or which('gedit') or \ exe = which('sensible-editor') or which('gedit') or \
which('xdg-open') or which('gnome-open') or which('kde-open') which('xdg-open') or which('gnome-open') or which('kde-open')
subprocess.Popen([exe, file])
elif is_macos: elif is_macos:
exe = which("open") exe = which("open")
subprocess.Popen([exe, file])
else: else:
webbrowser.open(file) webbrowser.open(file)
return
env = os.environ
if "LD_LIBRARY_PATH" in env:
env = env.copy()
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
subprocess.Popen([exe, file], env=env)
def open_patch(): def open_patch():
suffixes = [] suffixes = []
@@ -92,7 +97,11 @@ def open_folder(folder_path):
return return
if exe: if exe:
subprocess.Popen([exe, folder_path]) env = os.environ
if "LD_LIBRARY_PATH" in env:
env = env.copy()
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
subprocess.Popen([exe, folder_path], env=env)
else: else:
logging.warning(f"No file browser available to open {folder_path}") logging.warning(f"No file browser available to open {folder_path}")
@@ -104,14 +113,21 @@ def update_settings():
components.extend([ components.extend([
# Functions # Functions
Component("Open host.yaml", func=open_host_yaml), Component("Open host.yaml", func=open_host_yaml,
Component("Open Patch", func=open_patch), description="Open the host.yaml file to change settings for generation, games, and more."),
Component("Generate Template Options", func=generate_yamls), Component("Open Patch", func=open_patch,
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")), description="Open a patch file, downloaded from the room page or provided by the host."),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), Component("Generate Template Options", func=generate_yamls,
description="Generate template YAMLs for currently installed games."),
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/"),
description="Open archipelago.gg in your browser."),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2"),
description="Join the Discord server to play public multiworlds, report issues, or just chat!"),
Component("Unrated/18+ Discord Server", icon="discord", Component("Unrated/18+ Discord Server", icon="discord",
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4"),
Component("Browse Files", func=browse_files), description="Find unrated and 18+ games in the After Dark Discord server."),
Component("Browse Files", func=browse_files,
description="Open the Archipelago installation folder in your file browser."),
]) ])
@@ -180,7 +196,8 @@ def get_exe(component: str | Component) -> Sequence[str] | None:
def launch(exe, in_terminal=False): def launch(exe, in_terminal=False):
if in_terminal: if in_terminal:
if is_windows: if is_windows:
subprocess.Popen(['start', *exe], shell=True) # intentionally using a window title with a space so it gets quoted and treated as a title
subprocess.Popen(["start", "Running Archipelago", *exe], shell=True)
return return
elif is_linux: elif is_linux:
terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm') terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')

View File

@@ -1524,9 +1524,11 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
f"dictionary, not {type(items)}") f"dictionary, not {type(items)}")
locations = item.get("locations", []) locations = item.get("locations", [])
if not locations: if not locations:
locations = item.get("location", ["Everywhere"]) locations = item.get("location", [])
if locations: if locations:
count = 1 count = 1
else:
locations = ["Everywhere"]
if isinstance(locations, str): if isinstance(locations, str):
locations = [locations] locations = [locations]
if not isinstance(locations, list): if not isinstance(locations, list):

View File

@@ -226,7 +226,12 @@ def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
from shutil import which from shutil import which
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open")) open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
assert open_command, "Didn't find program for open_file! Please report this together with system details." assert open_command, "Didn't find program for open_file! Please report this together with system details."
subprocess.call([open_command, filename])
env = os.environ
if "LD_LIBRARY_PATH" in env:
env = env.copy()
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
subprocess.call([open_command, filename], env=env)
# from https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-4015118 with some changes # from https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-4015118 with some changes
@@ -708,25 +713,30 @@ def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args:
res.put(open_filename(*args)) res.put(open_filename(*args))
def _run_for_stdout(*args: str):
env = os.environ
if "LD_LIBRARY_PATH" in env:
env = env.copy()
del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH
return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
-> typing.Optional[str]: -> typing.Optional[str]:
logging.info(f"Opening file input dialog for {title}.") logging.info(f"Opening file input dialog for {title}.")
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_linux: if is_linux:
# prefer native dialog # prefer native dialog
from shutil import which from shutil import which
kdialog = which("kdialog") kdialog = which("kdialog")
if kdialog: if kdialog:
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes)) k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
return run(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters) return _run_for_stdout(kdialog, f"--title={title}", "--getopenfilename", suggest or ".", k_filters)
zenity = which("zenity") zenity = which("zenity")
if zenity: if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes) z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
selection = (f"--filename={suggest}",) if suggest else () selection = (f"--filename={suggest}",) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection) return _run_for_stdout(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
# fall back to tk # fall back to tk
try: try:
@@ -760,21 +770,18 @@ def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]: def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_linux: if is_linux:
# prefer native dialog # prefer native dialog
from shutil import which from shutil import which
kdialog = which("kdialog") kdialog = which("kdialog")
if kdialog: if kdialog:
return run(kdialog, f"--title={title}", "--getexistingdirectory", return _run_for_stdout(kdialog, f"--title={title}", "--getexistingdirectory",
os.path.abspath(suggest) if suggest else ".") os.path.abspath(suggest) if suggest else ".")
zenity = which("zenity") zenity = which("zenity")
if zenity: if zenity:
z_filters = ("--directory",) z_filters = ("--directory",)
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else () selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection) return _run_for_stdout(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
# fall back to tk # fall back to tk
try: try:
@@ -801,9 +808,6 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
def messagebox(title: str, text: str, error: bool = False) -> None: def messagebox(title: str, text: str, error: bool = False) -> None:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_kivy_running(): if is_kivy_running():
from kvui import MessageBox from kvui import MessageBox
MessageBox(title, text, error).open() MessageBox(title, text, error).open()
@@ -814,10 +818,10 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
from shutil import which from shutil import which
kdialog = which("kdialog") kdialog = which("kdialog")
if kdialog: if kdialog:
return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text) return _run_for_stdout(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
zenity = which("zenity") zenity = which("zenity")
if zenity: if zenity:
return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info") return _run_for_stdout(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
elif is_windows: elif is_windows:
import ctypes import ctypes

View File

@@ -1,4 +1,4 @@
flask>=3.1.0 flask>=3.1.1
werkzeug>=3.1.3 werkzeug>=3.1.3
pony>=0.7.19 pony>=0.7.19
waitress>=3.0.2 waitress>=3.0.2

View File

@@ -119,9 +119,9 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
# AP Container # AP Container
elif handler: elif handler:
data = zfile.open(file, "r").read() data = zfile.open(file, "r").read()
patch = handler(BytesIO(data)) with zipfile.ZipFile(BytesIO(data)) as container:
patch.read() player = json.loads(container.open("archipelago.json").read())["player"]
files[patch.player] = data files[player] = data
# Spoiler # Spoiler
elif file.filename.endswith(".txt"): elif file.filename.endswith(".txt"):

View File

@@ -365,18 +365,14 @@ request_handlers = {
["PREFERRED_CORES"] = function (req) ["PREFERRED_CORES"] = function (req)
local res = {} local res = {}
local preferred_cores = client.getconfig().PreferredCores local preferred_cores = client.getconfig().PreferredCores
local systems_enumerator = preferred_cores.Keys:GetEnumerator()
res["type"] = "PREFERRED_CORES_RESPONSE" res["type"] = "PREFERRED_CORES_RESPONSE"
res["value"] = {} res["value"] = {}
res["value"]["NES"] = preferred_cores.NES
res["value"]["SNES"] = preferred_cores.SNES while systems_enumerator:MoveNext() do
res["value"]["GB"] = preferred_cores.GB res["value"][systems_enumerator.Current] = preferred_cores[systems_enumerator.Current]
res["value"]["GBC"] = preferred_cores.GBC end
res["value"]["DGB"] = preferred_cores.DGB
res["value"]["SGB"] = preferred_cores.SGB
res["value"]["PCE"] = preferred_cores.PCE
res["value"]["PCECD"] = preferred_cores.PCECD
res["value"]["SGX"] = preferred_cores.SGX
return res return res
end, end,

View File

@@ -158,6 +158,7 @@ class APContainer:
class APPlayerContainer(APContainer): class APPlayerContainer(APContainer):
"""A zipfile containing at least archipelago.json meant for a player""" """A zipfile containing at least archipelago.json meant for a player"""
game: ClassVar[Optional[str]] = None game: ClassVar[Optional[str]] = None
patch_file_ending: str = ""
player: Optional[int] player: Optional[int]
player_name: str player_name: str
@@ -184,6 +185,7 @@ class APPlayerContainer(APContainer):
"player": self.player, "player": self.player,
"player_name": self.player_name, "player_name": self.player_name,
"game": self.game, "game": self.game,
"patch_file_ending": self.patch_file_ending,
}) })
return manifest return manifest
@@ -223,7 +225,6 @@ class APProcedurePatch(APAutoPatchInterface):
""" """
hash: Optional[str] # base checksum of source file hash: Optional[str] # base checksum of source file
source_data: bytes source_data: bytes
patch_file_ending: str = ""
files: Dict[str, bytes] files: Dict[str, bytes]
@classmethod @classmethod
@@ -245,7 +246,6 @@ class APProcedurePatch(APAutoPatchInterface):
manifest = super(APProcedurePatch, self).get_manifest() manifest = super(APProcedurePatch, self).get_manifest()
manifest["base_checksum"] = self.hash manifest["base_checksum"] = self.hash
manifest["result_file_ending"] = self.result_file_ending manifest["result_file_ending"] = self.result_file_ending
manifest["patch_file_ending"] = self.patch_file_ending
manifest["procedure"] = self.procedure manifest["procedure"] = self.procedure
if self.procedure == APDeltaPatch.procedure: if self.procedure == APDeltaPatch.procedure:
manifest["compatible_version"] = 5 manifest["compatible_version"] = 5

View File

@@ -210,10 +210,14 @@ components: List[Component] = [
Component('Launcher', 'Launcher', component_type=Type.HIDDEN), Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
# Core # Core
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True, Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
file_identifier=SuffixIdentifier('.archipelago', '.zip')), file_identifier=SuffixIdentifier('.archipelago', '.zip'),
Component('Generate', 'Generate', cli=True), description="Host a generated multiworld on your computer."),
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld")), Component('Generate', 'Generate', cli=True,
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient), description="Generate a multiworld with the YAMLs in the players folder."),
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld"),
description="Install an APWorld to play games not included with Archipelago by default."),
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient,
description="Connect to a multiworld using the text client."),
Component('Links Awakening DX Client', 'LinksAwakeningClient', Component('Links Awakening DX Client', 'LinksAwakeningClient',
file_identifier=SuffixIdentifier('.apladx')), file_identifier=SuffixIdentifier('.apladx')),
Component('LttP Adjuster', 'LttPAdjuster'), Component('LttP Adjuster', 'LttPAdjuster'),

View File

@@ -19,7 +19,8 @@ def launch_client(*args) -> None:
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
file_identifier=SuffixIdentifier()) file_identifier=SuffixIdentifier(),
description="Open the BizHawk client, to play games using the Bizhawk emulator.")
components.append(component) components.append(component)

View File

@@ -1,10 +1,9 @@
from dataclasses import dataclass from dataclasses import dataclass
import os import os
import io
from typing import TYPE_CHECKING, Dict, List, Optional, cast from typing import TYPE_CHECKING, Dict, List, Optional, cast
import zipfile import zipfile
from BaseClasses import Location from BaseClasses import Location
from worlds.Files import APContainer, AutoPatchRegister from worlds.Files import APPlayerContainer
from .Enum import CivVICheckType from .Enum import CivVICheckType
from .Locations import CivVILocation, CivVILocationData from .Locations import CivVILocation, CivVILocationData
@@ -26,22 +25,19 @@ class CivTreeItem:
ui_tree_row: int ui_tree_row: int
class CivVIContainer(APContainer, metaclass=AutoPatchRegister): class CivVIContainer(APPlayerContainer):
""" """
Responsible for generating the dynamic mod files for the Civ VI multiworld Responsible for generating the dynamic mod files for the Civ VI multiworld
""" """
game: Optional[str] = "Civilization VI" game: Optional[str] = "Civilization VI"
patch_file_ending = ".apcivvi" patch_file_ending = ".apcivvi"
def __init__(self, patch_data: Dict[str, str] | io.BytesIO, base_path: str = "", output_directory: str = "", def __init__(self, patch_data: Dict[str, str], base_path: str = "", output_directory: str = "",
player: Optional[int] = None, player_name: str = "", server: str = ""): player: Optional[int] = None, player_name: str = "", server: str = ""):
if isinstance(patch_data, io.BytesIO): self.patch_data = patch_data
super().__init__(patch_data, player, player_name, server) self.file_path = base_path
else: container_path = os.path.join(output_directory, base_path + ".apcivvi")
self.patch_data = patch_data super().__init__(container_path, player, player_name, server)
self.file_path = base_path
container_path = os.path.join(output_directory, base_path + ".apcivvi")
super().__init__(container_path, player, player_name, server)
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
for filename, yml in self.patch_data.items(): for filename, yml in self.patch_data.items():

View File

@@ -2893,3 +2893,18 @@ dog_bite_ice_trap_fix = [
0x25291CB8, # ADDIU T1, T1, 0x1CB8 0x25291CB8, # ADDIU T1, T1, 0x1CB8
0x01200008 # JR T1 0x01200008 # JR T1
] ]
shimmy_speed_modifier = [
# Increases the player's speed while shimmying as long as they are not holding down Z. If they are holding Z, it
# will be the normal speed, allowing it to still be used to set up any tricks that might require the normal speed
# (like Left Tower Skip).
0x3C088038, # LUI T0, 0x8038
0x91087D7E, # LBU T0, 0x7D7E (T0)
0x31090020, # ANDI T1, T0, 0x0020
0x3C0A800A, # LUI T2, 0x800A
0x240B005A, # ADDIU T3, R0, 0x005A
0x55200001, # BNEZL T1, [forward 0x01]
0x240B0032, # ADDIU T3, R0, 0x0032
0xA14B3641, # SB T3, 0x3641 (T2)
0x0800B7C3 # J 0x8002DF0C
]

View File

@@ -424,6 +424,7 @@ class PantherDash(Choice):
class IncreaseShimmySpeed(Toggle): class IncreaseShimmySpeed(Toggle):
""" """
Increases the speed at which characters shimmy left and right while hanging on ledges. Increases the speed at which characters shimmy left and right while hanging on ledges.
Hold Z to use the regular speed in case it's needed to do something.
""" """
display_name = "Increase Shimmy Speed" display_name = "Increase Shimmy Speed"

View File

@@ -607,9 +607,10 @@ class CV64PatchExtensions(APPatchExtension):
rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200 rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper) rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
# Increase shimmy speed # Shimmy speed increase hack
if options["increase_shimmy_speed"]: if options["increase_shimmy_speed"]:
rom_data.write_byte(0xA4241, 0x5A) rom_data.write_int32(0x97EB4, 0x803FE9F0)
rom_data.write_int32s(0xBFE9F0, patches.shimmy_speed_modifier)
# Disable landing fall damage # Disable landing fall damage
if options["fall_guard"]: if options["fall_guard"]:

View File

@@ -211,7 +211,8 @@ class CVCotMWorld(World):
"ignore_cleansing": self.options.ignore_cleansing.value, "ignore_cleansing": self.options.ignore_cleansing.value,
"skip_tutorials": self.options.skip_tutorials.value, "skip_tutorials": self.options.skip_tutorials.value,
"required_last_keys": self.required_last_keys, "required_last_keys": self.required_last_keys,
"completion_goal": self.options.completion_goal.value} "completion_goal": self.options.completion_goal.value,
"nerf_roc_wing": self.options.nerf_roc_wing.value}
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
return self.random.choice(FILLER_ITEM_NAMES) return self.random.choice(FILLER_ITEM_NAMES)

View File

@@ -48,11 +48,17 @@ class OtherGameAppearancesInfo(TypedDict):
other_game_item_appearances: Dict[str, Dict[str, OtherGameAppearancesInfo]] = { other_game_item_appearances: Dict[str, Dict[str, OtherGameAppearancesInfo]] = {
# NOTE: Symphony of the Night is currently an unsupported world not in main. # NOTE: Symphony of the Night and Harmony of Dissonance are custom worlds that are not core verified.
"Symphony of the Night": {"Life Vessel": {"type": 0xE4, "Symphony of the Night": {"Life Vessel": {"type": 0xE4,
"appearance": 0x01}, "appearance": 0x01},
"Heart Vessel": {"type": 0xE4, "Heart Vessel": {"type": 0xE4,
"appearance": 0x00}}, "appearance": 0x00}},
"Castlevania - Harmony of Dissonance": {"Life Max Up": {"type": 0xE4,
"appearance": 0x01},
"Heart Max Up": {"type": 0xE4,
"appearance": 0x00}},
"Timespinner": {"Max HP": {"type": 0xE4, "Timespinner": {"Max HP": {"type": 0xE4,
"appearance": 0x01}, "appearance": 0x01},
"Max Aura": {"type": 0xE4, "Max Aura": {"type": 0xE4,

View File

@@ -3,7 +3,7 @@
## Quick Links ## Quick Links
- [Setup](/tutorial/Castlevania%20-%20Circle%20of%20the%20Moon/setup/en) - [Setup](/tutorial/Castlevania%20-%20Circle%20of%20the%20Moon/setup/en)
- [Options Page](/games/Castlevania%20-%20Circle%20of%20the%20Moon/player-options) - [Options Page](/games/Castlevania%20-%20Circle%20of%20the%20Moon/player-options)
- [PopTracker Pack](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest) - [PopTracker Pack](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest)
- [Repo for the original, standalone CotMR](https://github.com/calm-palm/cotm-randomizer) - [Repo for the original, standalone CotMR](https://github.com/calm-palm/cotm-randomizer)
- [Web version of the above randomizer](https://rando.circleofthemoon.com/) - [Web version of the above randomizer](https://rando.circleofthemoon.com/)
- [A more in-depth guide to CotMR's nuances](https://docs.google.com/document/d/1uot4BD9XW7A--A8ecgoY8mLK_vSoQRpY5XCkzgas87c/view?usp=sharing) - [A more in-depth guide to CotMR's nuances](https://docs.google.com/document/d/1uot4BD9XW7A--A8ecgoY8mLK_vSoQRpY5XCkzgas87c/view?usp=sharing)

View File

@@ -22,7 +22,7 @@ clear it.
## Optional Software ## Optional Software
- [Castlevania: Circle of the Moon AP Tracker](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest), for use with - [Castlevania: Circle of the Moon AP Tracker](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest), for use with
[PopTracker](https://github.com/black-sliver/PopTracker/releases). [PopTracker](https://github.com/black-sliver/PopTracker/releases).
## Generating and Patching a Game ## Generating and Patching a Game
@@ -64,7 +64,7 @@ perfectly safe to make progress offline; everything will re-sync when you reconn
Castlevania: Circle of the Moon has a fully functional map tracker that supports auto-tracking. Castlevania: Circle of the Moon has a fully functional map tracker that supports auto-tracking.
1. Download [Castlevania: Circle of the Moon AP Tracker](https://github.com/sassyvania/Circle-of-the-Moon-Rando-AP-Map-Tracker-/releases/latest) and 1. Download [Castlevania: Circle of the Moon AP Tracker](https://github.com/BowserCrusher/Circle-of-the-Moon-AP-Tracker/releases/latest) and
[PopTracker](https://github.com/black-sliver/PopTracker/releases). [PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Put the tracker pack into `packs/` in your PopTracker install. 2. Put the tracker pack into `packs/` in your PopTracker install.
3. Open PopTracker, and load the Castlevania: Circle of the Moon pack. 3. Open PopTracker, and load the Castlevania: Circle of the Moon pack.

View File

@@ -884,7 +884,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
DS3LocationData("RS: Homeward Bone - balcony by Farron Keep", "Homeward Bone x2"), DS3LocationData("RS: Homeward Bone - balcony by Farron Keep", "Homeward Bone x2"),
DS3LocationData("RS: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"), DS3LocationData("RS: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"),
DS3LocationData("RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfire", DS3LocationData("RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfire",
"Twin Dragon Greatshield"), "Twin Dragon Greatshield", missable=True), # After Eclipse
DS3LocationData("RS: Sorcerer Hood - water beneath stronghold", "Sorcerer Hood", DS3LocationData("RS: Sorcerer Hood - water beneath stronghold", "Sorcerer Hood",
hidden=True), # Hidden fall hidden=True), # Hidden fall
DS3LocationData("RS: Sorcerer Robe - water beneath stronghold", "Sorcerer Robe", DS3LocationData("RS: Sorcerer Robe - water beneath stronghold", "Sorcerer Robe",
@@ -1887,7 +1887,7 @@ location_tables: Dict[str, List[DS3LocationData]] = {
DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #2", DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #2",
"Twinkling Titanite", lizard=True), "Twinkling Titanite", lizard=True),
DS3LocationData("AL: Aldrich's Ruby - dark cathedral, miniboss", "Aldrich's Ruby", DS3LocationData("AL: Aldrich's Ruby - dark cathedral, miniboss", "Aldrich's Ruby",
miniboss=True), # Deep Accursed drop miniboss=True, missable=True), # Deep Accursed drop, missable after defeating Aldrich
DS3LocationData("AL: Aldrich Faithful - water reserves, talk to McDonnel", "Aldrich Faithful", DS3LocationData("AL: Aldrich Faithful - water reserves, talk to McDonnel", "Aldrich Faithful",
hidden=True), # Behind illusory wall hidden=True), # Behind illusory wall

View File

@@ -705,7 +705,7 @@ class DarkSouls3World(World):
if self._is_location_available("US: Young White Branch - by white tree #2"): if self._is_location_available("US: Young White Branch - by white tree #2"):
self._add_item_rule( self._add_item_rule(
"US: Young White Branch - by white tree #2", "US: Young White Branch - by white tree #2",
lambda item: item.player == self.player and not item.data.unique lambda item: item.player != self.player or not item.data.unique
) )
# Make sure the Storm Ruler is available BEFORE Yhorm the Giant # Make sure the Storm Ruler is available BEFORE Yhorm the Giant

View File

@@ -802,8 +802,10 @@ def connect_regions(world: World, level_list):
for i in range(0, len(kremwood_forest_levels) - 1): for i in range(0, len(kremwood_forest_levels) - 1):
connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i]) connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[i])
connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1], connection = connect(world, world.player, names, LocationName.kremwood_forest_region, kremwood_forest_levels[-1],
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player))) lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player)))
world.multiworld.register_indirect_condition(world.get_location(LocationName.riverside_race_flag).parent_region,
connection)
# Cotton-Top Cove Connections # Cotton-Top Cove Connections
cotton_top_cove_levels = [ cotton_top_cove_levels = [
@@ -837,8 +839,11 @@ def connect_regions(world: World, level_list):
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region, connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
lambda state: (state.has(ItemName.bowling_ball, world.player, 1))) lambda state: (state.has(ItemName.bowling_ball, world.player, 1)))
else: else:
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region, connection = connect(world, world.player, names, LocationName.mekanos_region,
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player))) LocationName.sky_high_secret_region,
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player)))
world.multiworld.register_indirect_condition(world.get_location(LocationName.bleaks_house).parent_region,
connection)
# K3 Connections # K3 Connections
k3_levels = [ k3_levels = [
@@ -946,3 +951,4 @@ def connect(world: World, player: int, used_names: typing.Dict[str, int], source
source_region.exits.append(connection) source_region.exits.append(connection)
connection.connect(target_region) connection.connect(target_region)
return connection

View File

@@ -280,16 +280,19 @@ def set_boss_door_requirements_rules(player, world):
set_rule(world.get_entrance("Boss Door", player), has_3_swords) set_rule(world.get_entrance("Boss Door", player), has_3_swords)
def set_lfod_self_obtained_items_rules(world_options, player, world): def set_lfod_self_obtained_items_rules(world_options, player, multiworld):
if world_options.item_shuffle != Options.ItemShuffle.option_disabled: if world_options.item_shuffle != Options.ItemShuffle.option_disabled:
return return
set_rule(world.get_entrance("Vines", player), world = multiworld.worlds[player]
set_rule(world.get_entrance("Vines"),
lambda state: state.has("Incredibly Important Pack", player)) lambda state: state.has("Incredibly Important Pack", player))
set_rule(world.get_entrance("Behind Rocks", player), set_rule(world.get_entrance("Behind Rocks"),
lambda state: state.can_reach("Cut Content", 'region', player)) lambda state: state.can_reach("Cut Content", 'region', player))
set_rule(world.get_entrance("Pickaxe Hard Cave", player), multiworld.register_indirect_condition(world.get_region("Cut Content"), world.get_entrance("Behind Rocks"))
set_rule(world.get_entrance("Pickaxe Hard Cave"),
lambda state: state.can_reach("Cut Content", 'region', player) and lambda state: state.can_reach("Cut Content", 'region', player) and
state.has("Name Change Pack", player)) state.has("Name Change Pack", player))
multiworld.register_indirect_condition(world.get_region("Cut Content"), world.get_entrance("Pickaxe Hard Cave"))
def set_lfod_shuffled_items_rules(world_options, player, world): def set_lfod_shuffled_items_rules(world_options, player, world):

View File

@@ -69,7 +69,9 @@ class FactorioContext(CommonContext):
# updated by spinup server # updated by spinup server
mod_version: Version = Version(0, 0, 0) mod_version: Version = Version(0, 0, 0)
def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool): def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool,
rcon_port: int, rcon_password: str, server_settings_path: str | None,
factorio_server_args: tuple[str, ...]):
super(FactorioContext, self).__init__(server_address, password) super(FactorioContext, self).__init__(server_address, password)
self.send_index: int = 0 self.send_index: int = 0
self.rcon_client = None self.rcon_client = None
@@ -82,6 +84,10 @@ class FactorioContext(CommonContext):
self.filter_item_sends: bool = filter_item_sends self.filter_item_sends: bool = filter_item_sends
self.multiplayer: bool = False # whether multiple different players have connected self.multiplayer: bool = False # whether multiple different players have connected
self.bridge_chat_out: bool = bridge_chat_out self.bridge_chat_out: bool = bridge_chat_out
self.rcon_port: int = rcon_port
self.rcon_password: str = rcon_password
self.server_settings_path: str = server_settings_path
self.additional_factorio_server_args = factorio_server_args
@property @property
def energylink_key(self) -> str: def energylink_key(self) -> str:
@@ -126,6 +132,18 @@ class FactorioContext(CommonContext):
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] " self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}") f"{text}")
@property
def server_args(self) -> tuple[str, ...]:
if self.server_settings_path:
return (
"--rcon-port", str(self.rcon_port),
"--rcon-password", self.rcon_password,
"--server-settings", self.server_settings_path,
*self.additional_factorio_server_args)
else:
return ("--rcon-port", str(self.rcon_port), "--rcon-password", self.rcon_password,
*self.additional_factorio_server_args)
@property @property
def energy_link_status(self) -> str: def energy_link_status(self) -> str:
if not self.energy_link_increment: if not self.energy_link_increment:
@@ -311,7 +329,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
executable, "--create", savegame_name, "--preset", "archipelago" executable, "--create", savegame_name, "--preset", "archipelago"
)) ))
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name, factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
*(str(elem) for elem in server_args)), *ctx.server_args),
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
@@ -331,7 +349,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
factorio_queue.task_done() factorio_queue.task_done()
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password, ctx.rcon_client = factorio_rcon.RCONClient("localhost", ctx.rcon_port, ctx.rcon_password,
timeout=5) timeout=5)
if not ctx.server: if not ctx.server:
logger.info("Established bridge to Factorio Server. " logger.info("Established bridge to Factorio Server. "
@@ -422,7 +440,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
executable, "--create", savegame_name executable, "--create", savegame_name
)) ))
factorio_process = subprocess.Popen( factorio_process = subprocess.Popen(
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)), (executable, "--start-server", savegame_name, *ctx.server_args),
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
@@ -451,7 +469,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
"or a Factorio sharing data directories is already running. " "or a Factorio sharing data directories is already running. "
"Server could not start up.") "Server could not start up.")
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg: if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) rcon_client = factorio_rcon.RCONClient("localhost", ctx.rcon_port, ctx.rcon_password)
if ctx.mod_version == ctx.__class__.mod_version: if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.") raise Exception("No Archipelago mod was loaded. Aborting.")
await get_info(ctx, rcon_client) await get_info(ctx, rcon_client)
@@ -474,9 +492,8 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
return False return False
async def main(args, filter_item_sends: bool, filter_bridge_chat_out: bool): async def main(make_context):
ctx = FactorioContext(args.connect, args.password, filter_item_sends, filter_bridge_chat_out) ctx = make_context()
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled: if gui_enabled:
@@ -509,38 +526,42 @@ class FactorioJSONtoTextParser(JSONtoTextParser):
return self._handle_text(node) return self._handle_text(node)
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
args, rest = parser.parse_known_args()
rcon_port = args.rcon_port
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for x in range(32))
factorio_server_logger = logging.getLogger("FactorioServer") factorio_server_logger = logging.getLogger("FactorioServer")
settings: FactorioSettings = get_settings().factorio_options settings: FactorioSettings = get_settings().factorio_options
if os.path.samefile(settings.executable, sys.executable): if os.path.samefile(settings.executable, sys.executable):
selected_executable = settings.executable selected_executable = settings.executable
settings.executable = FactorioSettings.executable # reset to default settings.executable = FactorioSettings.executable # reset to default
raise Exception(f"FactorioClient was set to run itself {selected_executable}, aborting process bomb.") raise Exception(f"Factorio Client was set to run itself {selected_executable}, aborting process bomb.")
executable = settings.executable executable = settings.executable
server_settings = args.server_settings if args.server_settings \
else getattr(settings, "server_settings", None)
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
def launch(*new_args: str):
def launch():
import colorama import colorama
global executable, server_settings, server_args global executable
colorama.just_fix_windows_console() colorama.just_fix_windows_console()
# args handling
parser = get_base_parser(description="Optional arguments to Factorio Client follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
args, rest = parser.parse_known_args(args=new_args)
rcon_port = args.rcon_port
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for _ in range(32))
server_settings = args.server_settings if args.server_settings \
else getattr(settings, "server_settings", None)
if server_settings: if server_settings:
server_settings = os.path.abspath(server_settings) server_settings = os.path.abspath(server_settings)
if not os.path.isfile(server_settings):
raise FileNotFoundError(f"Could not find file {server_settings} for server_settings. Aborting.")
initial_filter_item_sends = bool(settings.filter_item_sends) initial_filter_item_sends = bool(settings.filter_item_sends)
initial_bridge_chat_out = bool(settings.bridge_chat_out) initial_bridge_chat_out = bool(settings.bridge_chat_out)
@@ -554,14 +575,9 @@ def launch():
else: else:
raise FileNotFoundError(f"Path {executable} is not an executable file.") raise FileNotFoundError(f"Path {executable} is not an executable file.")
if server_settings and os.path.isfile(server_settings): asyncio.run(main(lambda: FactorioContext(
server_args = ( args.connect, args.password,
"--rcon-port", rcon_port, initial_filter_item_sends, initial_bridge_chat_out,
"--rcon-password", rcon_password, rcon_port, rcon_password, server_settings, rest
"--server-settings", server_settings, )))
*rest)
else:
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
asyncio.run(main(args, initial_filter_item_sends, initial_bridge_chat_out))
colorama.deinit() colorama.deinit()

View File

@@ -67,6 +67,7 @@ class FactorioModFile(worlds.Files.APPlayerContainer):
game = "Factorio" game = "Factorio"
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]] writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
patch_file_ending = ".zip"
def __init__(self, *args: Any, **kwargs: Any): def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@@ -22,9 +22,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
from .settings import FactorioSettings from .settings import FactorioSettings
def launch_client(): def launch_client(*args: str):
from .Client import launch from .Client import launch
launch_component(launch, name="FactorioClient") launch_component(launch, name="Factorio Client", args=args)
components.append(Component("Factorio Client", func=launch_client, component_type=Type.CLIENT)) components.append(Component("Factorio Client", func=launch_client, component_type=Type.CLIENT))

View File

@@ -34,9 +34,9 @@ from .locations import (JakAndDaxterLocation,
cache_location_table, cache_location_table,
orb_location_table) orb_location_table)
from .regions import create_regions from .regions import create_regions
from .rules import (enforce_multiplayer_limits, from .rules import (enforce_mp_absolute_limits,
enforce_singleplayer_limits, enforce_mp_friendly_limits,
verify_orb_trade_amounts, enforce_sp_limits,
set_orb_trade_rule) set_orb_trade_rule)
from .locs import (cell_locations as cells, from .locs import (cell_locations as cells,
scout_locations as scouts, scout_locations as scouts,
@@ -258,18 +258,31 @@ class JakAndDaxterWorld(World):
self.options.mountain_pass_cell_count.value = self.power_cell_thresholds[1] self.options.mountain_pass_cell_count.value = self.power_cell_thresholds[1]
self.options.lava_tube_cell_count.value = self.power_cell_thresholds[2] self.options.lava_tube_cell_count.value = self.power_cell_thresholds[2]
# Store this for remove function. # We would have done this earlier, but we needed to sort the power cell thresholds first. Don't worry, we'll
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds] # come back to them.
# For the fairness of other players in a multiworld game, enforce some friendly limitations on our options,
# so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen.
# We would have done this earlier, but we needed to sort the power cell thresholds first.
enforce_friendly_options = self.settings.enforce_friendly_options enforce_friendly_options = self.settings.enforce_friendly_options
if enforce_friendly_options: if self.multiworld.players == 1:
if self.multiworld.players > 1: # For singleplayer games, always enforce/clamp the cell counts to valid values.
enforce_multiplayer_limits(self) enforce_sp_limits(self)
else:
if enforce_friendly_options:
# For multiplayer games, we have a host setting to make options fair/sane for other players.
# If this setting is enabled, enforce/clamp some friendly limitations on our options.
enforce_mp_friendly_limits(self)
else: else:
enforce_singleplayer_limits(self) # Even if the setting is disabled, some values must be clamped to avoid generation errors.
enforce_mp_absolute_limits(self)
# That's right, set the collection of thresholds again. Don't just clamp the values without updating this list!
self.power_cell_thresholds = [
self.options.fire_canyon_cell_count.value,
self.options.mountain_pass_cell_count.value,
self.options.lava_tube_cell_count.value,
100, # The 100 Power Cell Door.
]
# Now that the threshold list is finalized, store this for the remove function.
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds]
# Calculate the number of power cells needed for full region access, the number being replaced by traps, # Calculate the number of power cells needed for full region access, the number being replaced by traps,
# and the number of remaining filler. # and the number of remaining filler.
@@ -282,11 +295,6 @@ class JakAndDaxterWorld(World):
self.options.filler_power_cells_replaced_with_traps.value = self.total_trap_cells self.options.filler_power_cells_replaced_with_traps.value = self.total_trap_cells
self.total_filler_cells = non_prog_cells - self.total_trap_cells self.total_filler_cells = non_prog_cells - self.total_trap_cells
# Verify that we didn't overload the trade amounts with more orbs than exist in the world.
# This is easy to do by accident even in a singleplayer world.
self.total_trade_orbs = (9 * self.options.citizen_orb_trade_amount) + (6 * self.options.oracle_orb_trade_amount)
verify_orb_trade_amounts(self)
# Cache the orb bundle size and item name for quicker reference. # Cache the orb bundle size and item name for quicker reference.
if self.options.enable_orbsanity == options.EnableOrbsanity.option_per_level: if self.options.enable_orbsanity == options.EnableOrbsanity.option_per_level:
self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value

View File

@@ -18,7 +18,7 @@
- [What do Traps do?](#what-do-traps-do) - [What do Traps do?](#what-do-traps-do)
- [What kind of Traps are there?](#what-kind-of-traps-are-there) - [What kind of Traps are there?](#what-kind-of-traps-are-there)
- [I got soft-locked and cannot leave, how do I get out of here?](#i-got-soft-locked-and-cannot-leave-how-do-i-get-out-of-here) - [I got soft-locked and cannot leave, how do I get out of here?](#i-got-soft-locked-and-cannot-leave-how-do-i-get-out-of-here)
- [Why did I get an Option Error when generating a seed, and how do I fix it?](#why-did-i-get-an-option-error-when-generating-a-seed-and-how-do-i-fix-it) - [How do I generate seeds with 1 Orb Orbsanity and other extreme options?](#how-do-i-generate-seeds-with-1-orb-orbsanity-and-other-extreme-options)
- [How do I check my player options in-game?](#how-do-i-check-my-player-options-in-game) - [How do I check my player options in-game?](#how-do-i-check-my-player-options-in-game)
- [How does the HUD work?](#how-does-the-hud-work) - [How does the HUD work?](#how-does-the-hud-work)
- [I think I found a bug, where should I report it?](#i-think-i-found-a-bug-where-should-i-report-it) - [I think I found a bug, where should I report it?](#i-think-i-found-a-bug-where-should-i-report-it)
@@ -201,16 +201,19 @@ Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `W
Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back
to the nearest sage's hut to continue your journey. to the nearest sage's hut to continue your journey.
## Why did I get an Option Error when generating a seed and how do I fix it ## How do I generate seeds with 1 orb orbsanity and other extreme options?
Depending on your player YAML, Jak and Daxter can have a lot of items, which can sometimes be overwhelming or Depending on your player YAML, Jak and Daxter can have a lot of items, which can sometimes be overwhelming or
disruptive to multiworld games. There are also options that are mutually incompatible with each other, even in a solo disruptive to multiworld games. There are also options that are mutually incompatible with each other, even in a solo
game. To prevent the game from disrupting multiworlds, or generating an impossible solo seed, some options have game. To prevent the game from disrupting multiworlds, or generating an impossible solo seed, some options have
Singleplayer and Multiplayer Minimums and Maximums, collectively called "friendly limits." "friendly limits" that prevent you from choosing more extreme values.
If you're generating a solo game, or your multiworld host agrees to your request, you can override those limits by You can override **some**, not all, of those limits by editing the `host.yaml`. In the Archipelago Launcher, click
editing the `host.yaml`. In the Archipelago Launcher, click `Open host.yaml`, then search for `jakanddaxter_options`, `Open host.yaml`, then search for `jakanddaxter_options`, then search for `enforce_friendly_options`, then change this
then search for `enforce_friendly_options`, then change this value from `true` to `false`. Disabling this allows for value from `true` to `false`. You can then generate a seed locally, and upload that to the Archipelago website to host
more disruptive and challenging options, but it may cause seed generation to fail. **Use at your own risk!** for you (or host it yourself).
**Remember:** disabling this setting allows for more disruptive and challenging options, but it may cause seed
generation to fail. **Use at your own risk!**
## How do I check my player options in-game ## How do I check my player options in-game
When you connect your text client to the Archipelago Server, the server will tell the game what options were chosen When you connect your text client to the Archipelago Server, the server will tell the game what options were chosen

View File

@@ -4,7 +4,6 @@
- A legally purchased copy of *Jak And Daxter: The Precursor Legacy.* - A legally purchased copy of *Jak And Daxter: The Precursor Legacy.*
- [The OpenGOAL Launcher](https://opengoal.dev/) - [The OpenGOAL Launcher](https://opengoal.dev/)
- [The Jak and Daxter .APWORLD package](https://github.com/ArchipelaGOAL/Archipelago/releases)
At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux. At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux.
@@ -75,7 +74,7 @@ If you are in the middle of an async game, and you do not want to update the mod
### New Game ### New Game
- Run the Archipelago Launcher. - Run the Archipelago Launcher.
- From the right-most list, find and click `Jak and Daxter Client`. - From the client list, find and click `Jak and Daxter Client`.
- 3 new windows should appear: - 3 new windows should appear:
- The OpenGOAL compiler will launch and compile the game. They should take about 30 seconds to compile. - The OpenGOAL compiler will launch and compile the game. They should take about 30 seconds to compile.
- You should hear a musical cue to indicate the compilation was a success. If you do not, see the Troubleshooting section. - You should hear a musical cue to indicate the compilation was a success. If you do not, see the Troubleshooting section.

View File

@@ -1,22 +1,78 @@
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionCounter from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionCounter, \
AssembleOptions
from .items import trap_item_table from .items import trap_item_table
class StaticGetter: class readonly_classproperty:
def __init__(self, func): """This decorator is used for getting friendly or unfriendly range_end values for options like FireCanyonCellCount
self.fget = func and CitizenOrbTradeAmount. We only need to provide a getter as we will only be setting a single int to one of two
values."""
def __init__(self, getter):
self.getter = getter
def __get__(self, instance, owner): def __get__(self, instance, owner):
return self.fget(owner) return self.getter(owner)
@StaticGetter @readonly_classproperty
def determine_range_end(cls) -> int: def determine_range_end(cls) -> int:
from . import JakAndDaxterWorld from . import JakAndDaxterWorld # Avoid circular imports.
enforce_friendly_options = JakAndDaxterWorld.settings.enforce_friendly_options friendly = JakAndDaxterWorld.settings.enforce_friendly_options
return cls.friendly_maximum if enforce_friendly_options else cls.absolute_maximum return cls.friendly_maximum if friendly else cls.absolute_maximum
class classproperty:
"""This decorator (?) is used for getting and setting friendly or unfriendly option values for the Orbsanity
options."""
def __init__(self, getter, setter):
self.getter = getter
self.setter = setter
def __get__(self, obj, value):
return self.getter(obj)
def __set__(self, obj, value):
self.setter(obj, value)
class AllowedChoiceMeta(AssembleOptions):
"""This metaclass overrides AssembleOptions and provides inheriting classes a way to filter out "disallowed" values
by way of implementing get_disallowed_options. This function is used by Jak and Daxter to check host.yaml settings
without circular imports or breaking the settings API."""
_name_lookup: dict[int, str]
_options: dict[str, int]
def __new__(mcs, name, bases, attrs):
ret = super().__new__(mcs, name, bases, attrs)
ret._name_lookup = attrs["name_lookup"]
ret._options = attrs["options"]
return ret
def set_name_lookup(cls, value : dict[int, str]):
cls._name_lookup = value
def get_name_lookup(cls) -> dict[int, str]:
cls._name_lookup = {k: v for k, v in cls._name_lookup.items() if k not in cls.get_disallowed_options()}
return cls._name_lookup
def set_options(cls, value: dict[str, int]):
cls._options = value
def get_options(cls) -> dict[str, int]:
cls._options = {k: v for k, v in cls._options.items() if v not in cls.get_disallowed_options()}
return cls._options
def get_disallowed_options(cls):
return {}
name_lookup = classproperty(get_name_lookup, set_name_lookup)
options = classproperty(get_options, set_options)
class AllowedChoice(Choice, metaclass=AllowedChoiceMeta):
pass
class EnableMoveRandomizer(Toggle): class EnableMoveRandomizer(Toggle):
@@ -44,12 +100,13 @@ class EnableOrbsanity(Choice):
default = 0 default = 0
class GlobalOrbsanityBundleSize(Choice): class GlobalOrbsanityBundleSize(AllowedChoice):
"""The orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global." """The orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global."
There are 2000 orbs in the game, so your bundle size must be a factor of 2000. There are 2000 orbs in the game, so your bundle size must be a factor of 2000.
Multiplayer Minimum: 10 This value is restricted to safe minimum and maximum values to ensure valid singleplayer games and
Multiplayer Maximum: 200""" non-disruptive multiplayer games, but the host can remove this restriction by turning off enforce_friendly_options
in host.yaml."""
display_name = "Global Orbsanity Bundle Size" display_name = "Global Orbsanity Bundle Size"
option_1_orb = 1 option_1_orb = 1
option_2_orbs = 2 option_2_orbs = 2
@@ -75,12 +132,33 @@ class GlobalOrbsanityBundleSize(Choice):
friendly_maximum = 200 friendly_maximum = 200
default = 20 default = 20
@classmethod
def get_disallowed_options(cls) -> set[int]:
try:
from . import JakAndDaxterWorld
if JakAndDaxterWorld.settings.enforce_friendly_options:
return {cls.option_1_orb,
cls.option_2_orbs,
cls.option_4_orbs,
cls.option_5_orbs,
cls.option_8_orbs,
cls.option_250_orbs,
cls.option_400_orbs,
cls.option_500_orbs,
cls.option_1000_orbs,
cls.option_2000_orbs}
except ImportError:
pass
return set()
class PerLevelOrbsanityBundleSize(Choice):
class PerLevelOrbsanityBundleSize(AllowedChoice):
"""The orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level." """The orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level."
There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50. There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.
Multiplayer Minimum: 10""" This value is restricted to safe minimum and maximum values to ensure valid singleplayer games and
non-disruptive multiplayer games, but the host can remove this restriction by turning off enforce_friendly_options
in host.yaml."""
display_name = "Per Level Orbsanity Bundle Size" display_name = "Per Level Orbsanity Bundle Size"
option_1_orb = 1 option_1_orb = 1
option_2_orbs = 2 option_2_orbs = 2
@@ -91,6 +169,18 @@ class PerLevelOrbsanityBundleSize(Choice):
friendly_minimum = 10 friendly_minimum = 10
default = 25 default = 25
@classmethod
def get_disallowed_options(cls) -> set[int]:
try:
from . import JakAndDaxterWorld
if JakAndDaxterWorld.settings.enforce_friendly_options:
return {cls.option_1_orb,
cls.option_2_orbs,
cls.option_5_orbs}
except ImportError:
pass
return set()
class FireCanyonCellCount(Range): class FireCanyonCellCount(Range):
"""The number of power cells you need to cross Fire Canyon. This value is restricted to a safe maximum value to """The number of power cells you need to cross Fire Canyon. This value is restricted to a safe maximum value to
@@ -234,7 +324,7 @@ class CompletionCondition(Choice):
option_cross_fire_canyon = 69 option_cross_fire_canyon = 69
option_cross_mountain_pass = 87 option_cross_mountain_pass = 87
option_cross_lava_tube = 89 option_cross_lava_tube = 89
option_defeat_dark_eco_plant = 6 # option_defeat_dark_eco_plant = 6
option_defeat_klaww = 86 option_defeat_klaww = 86
option_defeat_gol_and_maia = 112 option_defeat_gol_and_maia = 112
option_open_100_cell_door = 116 option_open_100_cell_door = 116

View File

@@ -115,8 +115,8 @@ def create_regions(world: "JakAndDaxterWorld"):
elif options.jak_completion_condition == CompletionCondition.option_cross_lava_tube: elif options.jak_completion_condition == CompletionCondition.option_cross_lava_tube:
multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player) multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player)
elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant: # elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player) # multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww: elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww:
multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player) multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player)

View File

@@ -1,3 +1,5 @@
import logging
import math
import typing import typing
from BaseClasses import CollectionState from BaseClasses import CollectionState
from Options import OptionError from Options import OptionError
@@ -131,100 +133,138 @@ def can_fight(state: CollectionState, player: int) -> bool:
return state.has_any(("Jump Dive", "Jump Kick", "Punch", "Kick"), player) return state.has_any(("Jump Dive", "Jump Kick", "Punch", "Kick"), player)
def enforce_multiplayer_limits(world: "JakAndDaxterWorld"): def clamp_cell_limits(world: "JakAndDaxterWorld") -> str:
options = world.options options = world.options
friendly_message = "" friendly_message = ""
if (options.enable_orbsanity == EnableOrbsanity.option_global
and (options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum
or options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.friendly_maximum)):
friendly_message += (f" "
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
f"{GlobalOrbsanityBundleSize.friendly_minimum} and no greater than "
f"{GlobalOrbsanityBundleSize.friendly_maximum} (currently "
f"{options.global_orbsanity_bundle_size.value}).\n")
if (options.enable_orbsanity == EnableOrbsanity.option_per_level
and options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum):
friendly_message += (f" "
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (currently "
f"{options.level_orbsanity_bundle_size.value}).\n")
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum: if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
old_value = options.fire_canyon_cell_count.value
options.fire_canyon_cell_count.value = FireCanyonCellCount.friendly_maximum
friendly_message += (f" " friendly_message += (f" "
f"{options.fire_canyon_cell_count.display_name} must be no greater than " f"{options.fire_canyon_cell_count.display_name} must be no greater than "
f"{FireCanyonCellCount.friendly_maximum} (currently " f"{FireCanyonCellCount.friendly_maximum} (was {old_value}), "
f"{options.fire_canyon_cell_count.value}).\n") f"changed option to appropriate value.\n")
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum: if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
old_value = options.mountain_pass_cell_count.value
options.mountain_pass_cell_count.value = MountainPassCellCount.friendly_maximum
friendly_message += (f" " friendly_message += (f" "
f"{options.mountain_pass_cell_count.display_name} must be no greater than " f"{options.mountain_pass_cell_count.display_name} must be no greater than "
f"{MountainPassCellCount.friendly_maximum} (currently " f"{MountainPassCellCount.friendly_maximum} (was {old_value}), "
f"{options.mountain_pass_cell_count.value}).\n") f"changed option to appropriate value.\n")
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum: if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
old_value = options.lava_tube_cell_count.value
options.lava_tube_cell_count.value = LavaTubeCellCount.friendly_maximum
friendly_message += (f" " friendly_message += (f" "
f"{options.lava_tube_cell_count.display_name} must be no greater than " f"{options.lava_tube_cell_count.display_name} must be no greater than "
f"{LavaTubeCellCount.friendly_maximum} (currently " f"{LavaTubeCellCount.friendly_maximum} (was {old_value}), "
f"{options.lava_tube_cell_count.value}).\n") f"changed option to appropriate value.\n")
return friendly_message
def clamp_trade_total_limits(world: "JakAndDaxterWorld"):
"""Check if we need to recalculate the 2 trade orb options so the total fits under 2000. If so let's keep them
proportional relative to each other. Then we'll recalculate total_trade_orbs. Remember this situation is
only possible if both values are greater than 0, otherwise the absolute maximums would keep them under 2000."""
options = world.options
friendly_message = ""
world.total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
if world.total_trade_orbs > 2000:
old_total = world.total_trade_orbs
old_citizen_value = options.citizen_orb_trade_amount.value
old_oracle_value = options.oracle_orb_trade_amount.value
coefficient = old_oracle_value / old_citizen_value
options.citizen_orb_trade_amount.value = math.floor(2000 / (9 + (6 * coefficient)))
options.oracle_orb_trade_amount.value = math.floor(coefficient * options.citizen_orb_trade_amount.value)
world.total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
friendly_message += (f" "
f"Required number of orbs ({old_total}) must be no greater than total orbs in the game "
f"(2000). Reduced the value of {world.options.citizen_orb_trade_amount.display_name} "
f"from {old_citizen_value} to {options.citizen_orb_trade_amount.value} and "
f"{world.options.oracle_orb_trade_amount.display_name} from {old_oracle_value} to "
f"{options.oracle_orb_trade_amount.value}.\n")
return friendly_message
def enforce_mp_friendly_limits(world: "JakAndDaxterWorld"):
options = world.options
friendly_message = ""
if options.enable_orbsanity == EnableOrbsanity.option_global:
if options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum:
old_value = options.global_orbsanity_bundle_size.value
options.global_orbsanity_bundle_size.value = GlobalOrbsanityBundleSize.friendly_minimum
friendly_message += (f" "
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
f"{GlobalOrbsanityBundleSize.friendly_minimum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.friendly_maximum:
old_value = options.global_orbsanity_bundle_size.value
options.global_orbsanity_bundle_size.value = GlobalOrbsanityBundleSize.friendly_maximum
friendly_message += (f" "
f"{options.global_orbsanity_bundle_size.display_name} must be no greater than "
f"{GlobalOrbsanityBundleSize.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
if options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum:
old_value = options.level_orbsanity_bundle_size.value
options.level_orbsanity_bundle_size.value = PerLevelOrbsanityBundleSize.friendly_minimum
friendly_message += (f" "
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum: if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum:
old_value = options.citizen_orb_trade_amount.value
options.citizen_orb_trade_amount.value = CitizenOrbTradeAmount.friendly_maximum
friendly_message += (f" " friendly_message += (f" "
f"{options.citizen_orb_trade_amount.display_name} must be no greater than " f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
f"{CitizenOrbTradeAmount.friendly_maximum} (currently " f"{CitizenOrbTradeAmount.friendly_maximum} (was {old_value}), "
f"{options.citizen_orb_trade_amount.value}).\n") f"changed option to appropriate value.\n")
if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum: if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum:
old_value = options.oracle_orb_trade_amount.value
options.oracle_orb_trade_amount.value = OracleOrbTradeAmount.friendly_maximum
friendly_message += (f" " friendly_message += (f" "
f"{options.oracle_orb_trade_amount.display_name} must be no greater than " f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
f"{OracleOrbTradeAmount.friendly_maximum} (currently " f"{OracleOrbTradeAmount.friendly_maximum} (was {old_value}), "
f"{options.oracle_orb_trade_amount.value}).\n") f"changed option to appropriate value.\n")
friendly_message += clamp_cell_limits(world)
friendly_message += clamp_trade_total_limits(world)
if friendly_message != "": if friendly_message != "":
raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n" logging.warning(f"{world.player_name}: Your options have been modified to avoid disrupting the multiworld.\n"
f"Please adjust the following Options for a multiplayer game. \n" f"{friendly_message}"
f"{friendly_message}" f"You can access more advanced options by setting 'enforce_friendly_options' in the seed "
f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n" f"generator's host.yaml to false and generating locally. (Use at your own risk!)")
f"Or set 'enforce_friendly_options' in the seed generator's host.yaml to false. "
f"(Use at your own risk!)")
def enforce_singleplayer_limits(world: "JakAndDaxterWorld"): def enforce_mp_absolute_limits(world: "JakAndDaxterWorld"):
options = world.options
friendly_message = "" friendly_message = ""
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum: friendly_message += clamp_trade_total_limits(world)
friendly_message += (f" "
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
f"{FireCanyonCellCount.friendly_maximum} (currently "
f"{options.fire_canyon_cell_count.value}).\n")
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
friendly_message += (f" "
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
f"{MountainPassCellCount.friendly_maximum} (currently "
f"{options.mountain_pass_cell_count.value}).\n")
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
friendly_message += (f" "
f"{options.lava_tube_cell_count.display_name} must be no greater than "
f"{LavaTubeCellCount.friendly_maximum} (currently "
f"{options.lava_tube_cell_count.value}).\n")
if friendly_message != "": if friendly_message != "":
raise OptionError(f"The options you have chosen may result in seed generation failures. \n" logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
f"Please adjust the following Options for a singleplayer game. \n" f"{friendly_message}")
f"{friendly_message}"
f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n"
f"Or set 'enforce_friendly_options' in your host.yaml to false. "
f"(Use at your own risk!)")
def verify_orb_trade_amounts(world: "JakAndDaxterWorld"): def enforce_sp_limits(world: "JakAndDaxterWorld"):
friendly_message = ""
if world.total_trade_orbs > 2000: friendly_message += clamp_cell_limits(world)
raise OptionError(f"{world.player_name}: Required number of orbs for all trades ({world.total_trade_orbs}) " friendly_message += clamp_trade_total_limits(world)
f"is more than all the orbs in the game (2000). Reduce the value of either "
f"{world.options.citizen_orb_trade_amount.display_name} " if friendly_message != "":
f"or {world.options.oracle_orb_trade_amount.display_name}.") logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
f"{friendly_message}")

View File

@@ -4,14 +4,14 @@ from .bases import JakAndDaxterTestBase
class TradesCostNothingTest(JakAndDaxterTestBase): class TradesCostNothingTest(JakAndDaxterTestBase):
options = { options = {
"enable_orbsanity": 2, "enable_orbsanity": 2,
"global_orbsanity_bundle_size": 5, "global_orbsanity_bundle_size": 10,
"citizen_orb_trade_amount": 0, "citizen_orb_trade_amount": 0,
"oracle_orb_trade_amount": 0 "oracle_orb_trade_amount": 0
} }
def test_orb_items_are_filler(self): def test_orb_items_are_filler(self):
self.collect_all_but("") self.collect_all_but("")
self.assertNotIn("5 Precursor Orbs", self.multiworld.state.prog_items) self.assertNotIn("10 Precursor Orbs", self.multiworld.state.prog_items)
def test_trades_are_accessible(self): def test_trades_are_accessible(self):
self.assertTrue(self.multiworld self.assertTrue(self.multiworld
@@ -22,15 +22,15 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
class TradesCostEverythingTest(JakAndDaxterTestBase): class TradesCostEverythingTest(JakAndDaxterTestBase):
options = { options = {
"enable_orbsanity": 2, "enable_orbsanity": 2,
"global_orbsanity_bundle_size": 5, "global_orbsanity_bundle_size": 10,
"citizen_orb_trade_amount": 120, "citizen_orb_trade_amount": 120,
"oracle_orb_trade_amount": 150 "oracle_orb_trade_amount": 150
} }
def test_orb_items_are_progression(self): def test_orb_items_are_progression(self):
self.collect_all_but("") self.collect_all_but("")
self.assertIn("5 Precursor Orbs", self.multiworld.state.prog_items[self.player]) self.assertIn("10 Precursor Orbs", self.multiworld.state.prog_items[self.player])
self.assertEqual(396, self.multiworld.state.prog_items[self.player]["5 Precursor Orbs"]) self.assertEqual(198, self.multiworld.state.prog_items[self.player]["10 Precursor Orbs"])
def test_trades_are_accessible(self): def test_trades_are_accessible(self):
self.collect_all_but("") self.collect_all_but("")

View File

@@ -34,7 +34,7 @@ class KH2Context(CommonContext):
self.growthlevel = None self.growthlevel = None
self.kh2connected = False self.kh2connected = False
self.kh2_finished_game = False self.kh2_finished_game = False
self.serverconneced = False self.serverconnected = False
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
self.location_name_to_data = {name: data for name, data, in all_locations.items()} self.location_name_to_data = {name: data for name, data, in all_locations.items()}
self.kh2_data_package = {} self.kh2_data_package = {}
@@ -47,6 +47,8 @@ class KH2Context(CommonContext):
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()} self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
self.sending = [] self.sending = []
self.slot_name = None
self.disconnect_from_server = False
# list used to keep track of locations+items player has. Used for disoneccting # list used to keep track of locations+items player has. Used for disoneccting
self.kh2_seed_save_cache = { self.kh2_seed_save_cache = {
"itemIndex": -1, "itemIndex": -1,
@@ -185,11 +187,20 @@ class KH2Context(CommonContext):
if password_requested and not self.password: if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested) await super(KH2Context, self).server_auth(password_requested)
await self.get_username() await self.get_username()
await self.send_connect() # if slot name != first time login or previous name
# and seed name is none or saved seed name
if not self.slot_name and not self.kh2seedname:
await self.send_connect()
elif self.slot_name == self.auth and self.kh2seedname:
await self.send_connect()
else:
logger.info(f"You are trying to connect with data still cached in the client. Close client or connect to the correct slot: {self.slot_name}")
self.serverconnected = False
self.disconnect_from_server = True
async def connection_closed(self): async def connection_closed(self):
self.kh2connected = False self.kh2connected = False
self.serverconneced = False self.serverconnected = False
if self.kh2seedname is not None and self.auth is not None: if self.kh2seedname is not None and self.auth is not None:
with open(self.kh2_seed_save_path_join, 'w') as f: with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4)) f.write(json.dumps(self.kh2_seed_save, indent=4))
@@ -197,7 +208,8 @@ class KH2Context(CommonContext):
async def disconnect(self, allow_autoreconnect: bool = False): async def disconnect(self, allow_autoreconnect: bool = False):
self.kh2connected = False self.kh2connected = False
self.serverconneced = False self.serverconnected = False
self.locations_checked = []
if self.kh2seedname not in {None} and self.auth not in {None}: if self.kh2seedname not in {None} and self.auth not in {None}:
with open(self.kh2_seed_save_path_join, 'w') as f: with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4)) f.write(json.dumps(self.kh2_seed_save, indent=4))
@@ -239,7 +251,15 @@ class KH2Context(CommonContext):
def on_package(self, cmd: str, args: dict): def on_package(self, cmd: str, args: dict):
if cmd == "RoomInfo": if cmd == "RoomInfo":
self.kh2seedname = args['seed_name'] if not self.kh2seedname:
self.kh2seedname = args['seed_name']
elif self.kh2seedname != args['seed_name']:
self.disconnect_from_server = True
self.serverconnected = False
self.kh2connected = False
logger.info("Connection to the wrong seed, connect to the correct seed or close the client.")
return
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json" self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path) self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path)
@@ -338,7 +358,7 @@ class KH2Context(CommonContext):
}, },
}, },
} }
if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconneced: if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconnected:
self.kh2_seed_save_cache["itemIndex"] = start_index self.kh2_seed_save_cache["itemIndex"] = start_index
for item in args['items']: for item in args['items']:
asyncio.create_task(self.give_item(item.item, item.location)) asyncio.create_task(self.give_item(item.item, item.location))
@@ -370,12 +390,14 @@ class KH2Context(CommonContext):
if not self.kh2: if not self.kh2:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
self.get_addresses() self.get_addresses()
#
except Exception as e: except Exception as e:
if self.kh2connected: if self.kh2connected:
self.kh2connected = False self.kh2connected = False
logger.info("Game is not open.") logger.info("Game is not open.")
self.serverconneced = True
self.serverconnected = True
self.slot_name = self.auth
def data_package_kh2_cache(self, loc_to_id, item_to_id): def data_package_kh2_cache(self, loc_to_id, item_to_id):
self.kh2_loc_name_to_id = loc_to_id self.kh2_loc_name_to_id = loc_to_id
@@ -493,23 +515,38 @@ class KH2Context(CommonContext):
async def give_item(self, item, location): async def give_item(self, item, location):
try: try:
# todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites # sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
#sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts
while not self.lookup_id_to_item: while not self.lookup_id_to_item:
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
itemname = self.lookup_id_to_item[item] itemname = self.lookup_id_to_item[item]
itemdata = self.item_name_to_data[itemname] itemdata = self.item_name_to_data[itemname]
# itemcode = self.kh2_item_name_to_id[itemname]
if itemdata.ability: if itemdata.ability:
if location in self.all_weapon_location_id: if location in self.all_weapon_location_id:
return return
# growth have reserved ability slots because of how the goa handles them
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1 self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1
return return
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]: if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = [] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = []
# appending the slot that the ability should be in # appending the slot that the ability should be in
# abilities have a limit amount of slots.
# we start from the back going down to not mess with stuff.
# Front of Invo
# Sora: Save+24F0+0x54 : 0x2546
# Donald: Save+2604+0x54 : 0x2658
# Goofy: Save+2718+0x54 : 0x276C
# Back of Invo. Sora has 6 ability slots that are reserved
# Sora: Save+24F0+0x54+0x92 : 0x25D8
# Donald: Save+2604+0x54+0x9C : 0x26F4
# Goofy: Save+2718+0x54+0x9C : 0x2808
# seed has 2 scans in sora's abilities
# recieved second scan
# if len(seed_save(Scan:[ability slot 52]) < (2)amount of that ability they should have from slot data
# ability_slot = back of inventory that isnt taken
# add ability_slot to seed_save(Scan[]) so now its Scan:[ability slot 52,50]
# decrease back of inventory since its ability_slot is already taken
if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \
self.AbilityQuantityDict[itemname]: self.AbilityQuantityDict[itemname]:
if itemname in self.sora_ability_set: if itemname in self.sora_ability_set:
@@ -528,18 +565,21 @@ class KH2Context(CommonContext):
if ability_slot in self.front_ability_slots: if ability_slot in self.front_ability_slots:
self.front_ability_slots.remove(ability_slot) self.front_ability_slots.remove(ability_slot)
# if itemdata in {bitmask} all the forms,summons and a few other things are bitmasks
elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}:
# if memaddr is in a bitmask location in memory # if memaddr is in a bitmask location in memory
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname) self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname)
# if itemdata in {magic}
elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}: elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
# if memaddr is in magic addresses
self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1 self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1
# equipment is a list instead of dict because you can only have 1 currently
elif itemname in self.all_equipment: elif itemname in self.all_equipment:
self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname) self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname)
# weapons are done differently since you can only have one and has to check it differently
elif itemname in self.all_weapons: elif itemname in self.all_weapons:
if itemname in self.keyblade_set: if itemname in self.keyblade_set:
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname) self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname)
@@ -548,9 +588,11 @@ class KH2Context(CommonContext):
else: else:
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname) self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname)
# TODO: this can just be removed and put into the else below it
elif itemname in self.stat_increase_set: elif itemname in self.stat_increase_set:
self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1 self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1
else: else:
# "normal" items. They have a unique byte reserved for how many they have
if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]: if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]:
self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1 self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1
else: else:
@@ -930,7 +972,7 @@ def finishedGame(ctx: KH2Context):
async def kh2_watcher(ctx: KH2Context): async def kh2_watcher(ctx: KH2Context):
while not ctx.exit_event.is_set(): while not ctx.exit_event.is_set():
try: try:
if ctx.kh2connected and ctx.serverconneced: if ctx.kh2connected and ctx.serverconnected:
ctx.sending = [] ctx.sending = []
await asyncio.create_task(ctx.checkWorldLocations()) await asyncio.create_task(ctx.checkWorldLocations())
await asyncio.create_task(ctx.checkLevels()) await asyncio.create_task(ctx.checkLevels())
@@ -944,13 +986,19 @@ async def kh2_watcher(ctx: KH2Context):
if ctx.sending: if ctx.sending:
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
await ctx.send_msgs(message) await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced: elif not ctx.kh2connected and ctx.serverconnected:
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") logger.info("Game Connection lost. trying to reconnect.")
ctx.kh2 = None ctx.kh2 = None
while not ctx.kh2connected and ctx.serverconneced: while not ctx.kh2connected and ctx.serverconnected:
await asyncio.sleep(15) try:
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
ctx.get_addresses() ctx.get_addresses()
logger.info("Game Connection Established.")
except Exception as e:
await asyncio.sleep(5)
if ctx.disconnect_from_server:
ctx.disconnect_from_server = False
await ctx.disconnect()
except Exception as e: except Exception as e:
if ctx.kh2connected: if ctx.kh2connected:
ctx.kh2connected = False ctx.kh2connected = False

View File

@@ -13,6 +13,7 @@ from worlds.Files import APPlayerContainer
class KH2Container(APPlayerContainer): class KH2Container(APPlayerContainer):
game: str = 'Kingdom Hearts 2' game: str = 'Kingdom Hearts 2'
patch_file_ending = ".zip"
def __init__(self, patch_data: dict, base_path: str, output_directory: str, def __init__(self, patch_data: dict, base_path: str, output_directory: str,
player=None, player_name: str = "", server: str = ""): player=None, player_name: str = "", server: str = ""):

View File

@@ -277,9 +277,7 @@ class KH2World(World):
if self.options.FillerItemsLocal: if self.options.FillerItemsLocal:
for item in filler_items: for item in filler_items:
self.options.local_items.value.add(item) self.options.local_items.value.add(item)
# By imitating remote this doesn't have to be plandoded filler anymore
# for location in {LocationName.JunkMedal, LocationName.JunkMedal}:
# self.plando_locations[location] = random_stt_item
if not self.options.SummonLevelLocationToggle: if not self.options.SummonLevelLocationToggle:
self.total_locations -= 6 self.total_locations -= 6
@@ -400,6 +398,8 @@ class KH2World(World):
# plando goofy get bonuses # plando goofy get bonuses
goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"] Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"]
if len(goofy_get_bonus_location_pool) > len(self.goofy_get_bonus_abilities):
raise Exception(f"Too little abilities to fill goofy get bonus locations for player {self.player_name}.")
for location in goofy_get_bonus_location_pool: for location in goofy_get_bonus_location_pool:
self.random.choice(self.goofy_get_bonus_abilities) self.random.choice(self.goofy_get_bonus_abilities)
random_ability = self.random.choice(self.goofy_get_bonus_abilities) random_ability = self.random.choice(self.goofy_get_bonus_abilities)
@@ -416,11 +416,12 @@ class KH2World(World):
random_ability = self.random.choice(self.donald_weapon_abilities) random_ability = self.random.choice(self.donald_weapon_abilities)
location.place_locked_item(random_ability) location.place_locked_item(random_ability)
self.donald_weapon_abilities.remove(random_ability) self.donald_weapon_abilities.remove(random_ability)
# if option is turned off
if not self.options.DonaldGoofyStatsanity: if not self.options.DonaldGoofyStatsanity:
# plando goofy get bonuses
donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"] Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"]
if len(donald_get_bonus_location_pool) > len(self.donald_get_bonus_abilities):
raise Exception(f"Too little abilities to fill donald get bonus locations for player {self.player_name}.")
for location in donald_get_bonus_location_pool: for location in donald_get_bonus_location_pool:
random_ability = self.random.choice(self.donald_get_bonus_abilities) random_ability = self.random.choice(self.donald_get_bonus_abilities)
location.place_locked_item(random_ability) location.place_locked_item(random_ability)

View File

@@ -4956,10 +4956,16 @@
Outside The Initiated: Outside The Initiated:
room: Art Gallery room: Art Gallery
door: Exit door: Exit
The Bearer (East): True The Bearer (East):
The Bearer (North): True static_painting: True
The Bearer (South): True The Bearer (North):
The Bearer (West): True static_painting: True
The Bearer (South):
static_painting: True
The Bearer (West):
- static_painting: True
- room: The Bearer (West)
door: Side Area Shortcut
Roof: True Roof: True
panels: panels:
Achievement: Achievement:
@@ -5053,7 +5059,8 @@
- MIDDLE - MIDDLE
The Bearer (East): The Bearer (East):
entrances: entrances:
Cross Tower (East): True Cross Tower (East):
static_painting: True
Bearer Side Area: Bearer Side Area:
door: Side Area Access door: Side Area Access
Roof: True Roof: True
@@ -5084,7 +5091,8 @@
panel: SPACE panel: SPACE
The Bearer (North): The Bearer (North):
entrances: entrances:
Cross Tower (East): True Cross Tower (North):
static_painting: True
Roof: True Roof: True
panels: panels:
SILENT (1): SILENT (1):
@@ -5128,7 +5136,8 @@
panel: POTS panel: POTS
The Bearer (South): The Bearer (South):
entrances: entrances:
Cross Tower (North): True Cross Tower (South):
static_painting: True
Bearer Side Area: Bearer Side Area:
door: Side Area Shortcut door: Side Area Shortcut
Roof: True Roof: True
@@ -5162,7 +5171,10 @@
panel: SILENT (1) panel: SILENT (1)
The Bearer (West): The Bearer (West):
entrances: entrances:
Cross Tower (West): True Cross Tower (West):
static_painting: True
The Bearer:
door: Side Area Shortcut
Bearer Side Area: Bearer Side Area:
door: Side Area Shortcut door: Side Area Shortcut
Roof: True Roof: True
@@ -5235,6 +5247,7 @@
The Bearer: The Bearer:
room: The Bearer room: The Bearer
door: East Entrance door: East Entrance
static_painting: True
Roof: True Roof: True
panels: panels:
WINTER: WINTER:
@@ -5250,6 +5263,7 @@
The Bearer (East): The Bearer (East):
room: The Bearer (East) room: The Bearer (East)
door: North Entrance door: North Entrance
static_painting: True
Roof: True Roof: True
panels: panels:
NORTH: NORTH:
@@ -5270,6 +5284,7 @@
The Bearer (North): The Bearer (North):
room: The Bearer (North) room: The Bearer (North)
door: South Entrance door: South Entrance
static_painting: True
panels: panels:
FIRE: FIRE:
id: Cross Room/Panel_fire_fire id: Cross Room/Panel_fire_fire
@@ -5284,6 +5299,7 @@
Bearer Side Area: Bearer Side Area:
room: Bearer Side Area room: Bearer Side Area
door: West Entrance door: West Entrance
static_painting: True
Roof: True Roof: True
panels: panels:
DIAMONDS: DIAMONDS:
@@ -7108,6 +7124,8 @@
entrances: entrances:
Orange Tower Third Floor: Orange Tower Third Floor:
warp: True warp: True
Art Gallery (First Floor):
warp: True
Art Gallery (Second Floor): Art Gallery (Second Floor):
warp: True warp: True
Art Gallery (Third Floor): Art Gallery (Third Floor):
@@ -7125,22 +7143,6 @@
required_door: required_door:
room: Number Hunt room: Number Hunt
door: Eights door: Eights
EON:
id: Painting Room/Panel_eon_one
colors: yellow
tag: midyellow
TRUSTWORTHY:
id: Painting Room/Panel_to_two
colors: red
tag: midred
FREE:
id: Painting Room/Panel_free_three
colors: purple
tag: midpurp
OUR:
id: Painting Room/Panel_our_four
colors: blue
tag: midblue
ORDER: ORDER:
id: Painting Room/Panel_order_onepathmanyturns id: Painting Room/Panel_order_onepathmanyturns
tag: forbid tag: forbid
@@ -7159,15 +7161,8 @@
- scenery_painting_2c - scenery_painting_2c
skip_location: True skip_location: True
panels: panels:
- EON - room: Art Gallery (First Floor)
First Floor Puzzles: panel: EON
skip_item: True
location_name: Art Gallery - First Floor Puzzles
panels:
- EON
- TRUSTWORTHY
- FREE
- OUR
Third Floor: Third Floor:
painting_id: painting_id:
- scenery_painting_3b - scenery_painting_3b
@@ -7227,11 +7222,42 @@
- Third Floor - Third Floor
- Fourth Floor - Fourth Floor
- Fifth Floor - Fifth Floor
Art Gallery (First Floor):
entrances:
Art Gallery:
static_painting: True
panels:
EON:
id: Painting Room/Panel_eon_one
colors: yellow
tag: midyellow
TRUSTWORTHY:
id: Painting Room/Panel_to_two
colors: red
tag: midred
FREE:
id: Painting Room/Panel_free_three
colors: purple
tag: midpurp
OUR:
id: Painting Room/Panel_our_four
colors: blue
tag: midblue
doors:
Puzzles:
skip_item: True
location_name: Art Gallery - First Floor Puzzles
panels:
- EON
- TRUSTWORTHY
- FREE
- OUR
Art Gallery (Second Floor): Art Gallery (Second Floor):
entrances: entrances:
Art Gallery: Art Gallery:
room: Art Gallery room: Art Gallery
door: Second Floor door: Second Floor
static_painting: True
panels: panels:
HOUSE: HOUSE:
id: Painting Room/Panel_house_neighborhood id: Painting Room/Panel_house_neighborhood
@@ -7263,6 +7289,7 @@
Art Gallery: Art Gallery:
room: Art Gallery room: Art Gallery
door: Third Floor door: Third Floor
static_painting: True
panels: panels:
AN: AN:
id: Painting Room/Panel_an_many id: Painting Room/Panel_an_many
@@ -7294,6 +7321,7 @@
Art Gallery: Art Gallery:
room: Art Gallery room: Art Gallery
door: Fourth Floor door: Fourth Floor
static_painting: True
panels: panels:
URNS: URNS:
id: Painting Room/Panel_urns_turns id: Painting Room/Panel_urns_turns

Binary file not shown.

View File

@@ -727,11 +727,12 @@ panels:
WANDER: 444975 WANDER: 444975
Art Gallery: Art Gallery:
EIGHT: 444976 EIGHT: 444976
ORDER: 444981
Art Gallery (First Floor):
EON: 444977 EON: 444977
TRUSTWORTHY: 444978 TRUSTWORTHY: 444978
FREE: 444979 FREE: 444979
OUR: 444980 OUR: 444980
ORDER: 444981
Art Gallery (Second Floor): Art Gallery (Second Floor):
HOUSE: 444982 HOUSE: 444982
PATH: 444983 PATH: 444983
@@ -1382,8 +1383,6 @@ doors:
Art Gallery: Art Gallery:
Second Floor: Second Floor:
item: 444558 item: 444558
First Floor Puzzles:
location: 445256
Third Floor: Third Floor:
item: 444559 item: 444559
Fourth Floor: Fourth Floor:
@@ -1393,6 +1392,9 @@ doors:
Exit: Exit:
item: 444562 item: 444562
location: 444981 location: 444981
Art Gallery (First Floor):
Puzzles:
location: 445256
Art Gallery (Second Floor): Art Gallery (Second Floor):
Puzzles: Puzzles:
location: 445257 location: 445257

View File

@@ -23,6 +23,7 @@ class EntranceType(Flag):
SUNWARP = auto() SUNWARP = auto()
WARP = auto() WARP = auto()
CROSSROADS_ROOF_ACCESS = auto() CROSSROADS_ROOF_ACCESS = auto()
STATIC_PAINTING = auto()
class RoomEntrance(NamedTuple): class RoomEntrance(NamedTuple):

View File

@@ -30,7 +30,7 @@ def is_acceptable_pilgrimage_entrance(entrance_type: EntranceType, world: "Lingo
allowed_entrance_types = EntranceType.NORMAL allowed_entrance_types = EntranceType.NORMAL
if world.options.pilgrimage_allows_paintings: if world.options.pilgrimage_allows_paintings:
allowed_entrance_types |= EntranceType.PAINTING allowed_entrance_types |= EntranceType.PAINTING | EntranceType.STATIC_PAINTING
if world.options.pilgrimage_allows_roof_access: if world.options.pilgrimage_allows_roof_access:
allowed_entrance_types |= EntranceType.CROSSROADS_ROOF_ACCESS allowed_entrance_types |= EntranceType.CROSSROADS_ROOF_ACCESS
@@ -105,7 +105,8 @@ def create_regions(world: "LingoWorld") -> None:
regions[pilgrimage_region_name] = Region(pilgrimage_region_name, world.player, world.multiworld) regions[pilgrimage_region_name] = Region(pilgrimage_region_name, world.player, world.multiworld)
# Connect all created regions now that they exist. # Connect all created regions now that they exist.
allowed_entrance_types = EntranceType.NORMAL | EntranceType.WARP | EntranceType.CROSSROADS_ROOF_ACCESS allowed_entrance_types = EntranceType.NORMAL | EntranceType.WARP | EntranceType.CROSSROADS_ROOF_ACCESS | \
EntranceType.STATIC_PAINTING
if not painting_shuffle: if not painting_shuffle:
# Don't use the vanilla painting connections if we are shuffling paintings. # Don't use the vanilla painting connections if we are shuffling paintings.
@@ -156,11 +157,11 @@ def create_regions(world: "LingoWorld") -> None:
regions[from_room].connect(regions[to_room], f"Pilgrimage Part {i+1}") regions[from_room].connect(regions[to_room], f"Pilgrimage Part {i+1}")
else: else:
connect_entrance(regions, regions["Starting Room"], regions["Pilgrim Antechamber"], "Sun Painting", connect_entrance(regions, regions["Starting Room"], regions["Pilgrim Antechamber"], "Sun Painting",
RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world) RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.STATIC_PAINTING, False, world)
if early_color_hallways: if early_color_hallways:
connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways", connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways",
None, EntranceType.PAINTING, False, world) None, EntranceType.STATIC_PAINTING, False, world)
if painting_shuffle: if painting_shuffle:
for warp_enter, warp_exit in world.player_logic.painting_mapping.items(): for warp_enter, warp_exit in world.player_logic.painting_mapping.items():

View File

@@ -138,6 +138,8 @@ def process_single_entrance(source_room: str, room_name: str, door_obj) -> RoomE
entrance_type = EntranceType.WARP entrance_type = EntranceType.WARP
elif source_room == "Crossroads" and room_name == "Roof": elif source_room == "Crossroads" and room_name == "Roof":
entrance_type = EntranceType.CROSSROADS_ROOF_ACCESS entrance_type = EntranceType.CROSSROADS_ROOF_ACCESS
elif "static_painting" in door_obj and door_obj["static_painting"]:
entrance_type = EntranceType.STATIC_PAINTING
if "painting" in door_obj and door_obj["painting"]: if "painting" in door_obj and door_obj["painting"]:
PAINTING_EXIT_ROOMS.add(room_name) PAINTING_EXIT_ROOMS.add(room_name)

View File

@@ -2,7 +2,7 @@
# the file are consistent. It also checks that the panel and door IDs mentioned # the file are consistent. It also checks that the panel and door IDs mentioned
# all exist in the map file. # all exist in the map file.
# #
# Usage: validate_config.rb [config file] [map file] # Usage: validate_config.rb [config file] [ids path] [map file]
require 'set' require 'set'
require 'yaml' require 'yaml'

View File

@@ -38,6 +38,7 @@ AP_JUNK = 0xD5
class OoTContainer(APPatch): class OoTContainer(APPatch):
game: str = 'Ocarina of Time' game: str = 'Ocarina of Time'
patch_file_ending = ".apz5"
def __init__(self, patch_data: bytes, base_path: str, output_directory: str, def __init__(self, patch_data: bytes, base_path: str, output_directory: str,
player = None, player_name: str = "", server: str = ""): player = None, player_name: str = "", server: str = ""):

View File

@@ -7,13 +7,13 @@ Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
## Benötigte Software ## Benötigte Software
- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.3.1 und später werden unterstützt. Version 2.9 ist empfohlen. - Version 2.3.1 und später werden unterstützt. Version 2.10 ist empfohlen.
- Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden. - Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden.
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über - Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
den obrigen Link gefunden werden. den obrigen Link gefunden werden.
- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert - Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert
werden kann. werden kann.
- Eine `Ocarina of Time v1.0 US(?) ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!) - Eine `Ocarina of Time v1.0 US ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!)
## Konfigurieren von BizHawk ## Konfigurieren von BizHawk

View File

@@ -7,11 +7,11 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
## Required Software ## Required Software
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability. - Version 2.3.1 and later are supported. Version 2.10 is recommended for stability.
- Detailed installation instructions for BizHawk can be found at the above link. - Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link. - Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases). - The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- An Ocarina of Time v1.0 ROM. - A US Ocarina of Time v1.0 ROM.
## Configuring BizHawk ## Configuring BizHawk

View File

@@ -7,12 +7,12 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
## Logiciel requis ## Logiciel requis
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour des raisons de stabilité. - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.10 est recommandée pour des raisons de stabilité.
- Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus. - Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus.
- Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus. - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus.
- Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases) - Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases)
(sélectionnez « Ocarina of Time Client » lors de l'installation). (sélectionnez « Ocarina of Time Client » lors de l'installation).
- Une ROM Ocarina of Time v1.0. - Un fichier ROM v1.0 US d'Ocarina of Time.
## Configuration de BizHawk ## Configuration de BizHawk

View File

@@ -40,6 +40,8 @@ class RaftWorld(World):
options_dataclass = RaftOptions options_dataclass = RaftOptions
options: RaftOptions options: RaftOptions
extraItemNamePool: list[str] | None = None
required_client_version = (0, 3, 4) required_client_version = (0, 3, 4)
def create_items(self): def create_items(self):
@@ -52,52 +54,52 @@ class RaftWorld(World):
pool = [] pool = []
frequencyItems = [] frequencyItems = []
for item in item_table: for item in item_table:
raft_item = self.create_item_replaceAsNecessary(item["name"]) raft_item = self.create_item(self.replace_item_name_as_necessary(item["name"]))
if isFillingFrequencies and "Frequency" in item["name"]: if isFillingFrequencies and "Frequency" in item["name"]:
frequencyItems.append(raft_item) frequencyItems.append(raft_item)
else: else:
pool.append(raft_item) pool.append(raft_item)
extraItemNamePool = [] self.extraItemNamePool = []
extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot
if extras > 0:
if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs
for packItem in resourcePackItems:
for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1):
extraItemNamePool.append(createResourcePackName(i, packItem))
if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs
dupeItemPool = item_table.copy() for packItem in resourcePackItems:
# Remove frequencies if necessary for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1):
if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations self.extraItemNamePool.append(createResourcePackName(i, packItem))
# If we let frequencies stay in with progressive-frequencies, the progressive-frequency item
# will be included 7 times. This is a massive flood of progressive-frequency items, so we if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items
# instead add progressive-frequency as its own item a smaller amount of times to prevent dupeItemPool = item_table.copy()
# flooding the duplicate item pool with them. # Remove frequencies if necessary
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive: if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations
for _ in range(2): # If we let frequencies stay in with progressive-frequencies, the progressive-frequency item
# Progressives are not in item_pool, need to create faux item for duplicate item pool # will be included 7 times. This is a massive flood of progressive-frequency items, so we
# This can still be filtered out later by duplicate_items setting # instead add progressive-frequency as its own item a smaller amount of times to prevent
dupeItemPool.append({ "name": "progressive-frequency", "progression": True }) # Progressive frequencies need to be included # flooding the duplicate item pool with them.
# Always remove non-progressive Frequency items if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"]) for _ in range(2):
# Progressives are not in item_pool, need to create faux item for duplicate item pool
# Remove progression or non-progression items if necessary # This can still be filtered out later by duplicate_items setting
if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only dupeItemPool.append({ "name": "progressive-frequency", "progression": True }) # Progressive frequencies need to be included
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True) # Always remove non-progressive Frequency items
elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
# Remove progression or non-progression items if necessary
dupeItemPool = list(dupeItemPool) if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only
# Finally, add items as necessary dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
if len(dupeItemPool) > 0: elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only
for item in dupeItemPool: dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
extraItemNamePool.append(item["name"])
dupeItemPool = list(dupeItemPool)
# Finally, add items as necessary
for item in dupeItemPool:
self.extraItemNamePool.append(self.replace_item_name_as_necessary(item))
if (len(extraItemNamePool) > 0): assert self.extraItemNamePool, f"Don't know what extra items to create for {self.player_name}."
for randomItem in self.random.choices(extraItemNamePool, k=extras):
raft_item = self.create_item_replaceAsNecessary(randomItem) for randomItem in self.random.choices(self.extraItemNamePool, k=extras):
pool.append(raft_item) raft_item = self.create_item(randomItem)
pool.append(raft_item)
self.multiworld.itempool += pool self.multiworld.itempool += pool
@@ -108,19 +110,35 @@ class RaftWorld(World):
if frequencyItems: if frequencyItems:
self.place_frequencyItems(frequencyItems) self.place_frequencyItems(frequencyItems)
def get_filler_item_name(self) -> str:
# A normal Raft world will have an extraItemNamePool defined after create_items.
if self.extraItemNamePool:
return self.random.choice(self.extraItemNamePool)
# If this is a "fake" world, e.g. item links with link replacement: Resource packs are always be safe to create
minRPSpecified = self.options.minimum_resource_pack_amount.value
maxRPSpecified = self.options.maximum_resource_pack_amount.value
minimumResourcePackAmount = min(minRPSpecified, maxRPSpecified)
maximumResourcePackAmount = max(minRPSpecified, maxRPSpecified)
resource_amount = self.random.randint(minimumResourcePackAmount, maximumResourcePackAmount)
resource_type = self.random.choice(resourcePackItems)
return createResourcePackName(resource_amount, resource_type)
def set_rules(self): def set_rules(self):
set_rules(self.multiworld, self.player) set_rules(self.multiworld, self.player)
def create_regions(self): def create_regions(self):
create_regions(self.multiworld, self.player) create_regions(self.multiworld, self.player)
def create_item_replaceAsNecessary(self, name: str) -> Item: def replace_item_name_as_necessary(self, name: str) -> str:
isFrequency = "Frequency" in name if name not in progressive_table:
shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive) return name
or (not isFrequency and self.options.progressive_items)) if "Frequency" in name:
if shouldUseProgressive and name in progressive_table: if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
name = progressive_table[name] return progressive_table[name]
return self.create_item(name) elif self.options.progressive_items:
return progressive_table[name]
return name
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
item = lookup_name_to_item[name] item = lookup_name_to_item[name]

View File

@@ -92,17 +92,7 @@ class TestGlobalOptionsImport(TestCase):
f"{max_levels_and_upgrades} instead.") f"{max_levels_and_upgrades} instead.")
class TestMinimum(ShapezTestBase): # The following unittests are intended to test all code paths of the generator
options = options_presets["Minimum checks"]
class TestMaximum(ShapezTestBase):
options = options_presets["Maximum checks"]
class TestRestrictive(ShapezTestBase):
options = options_presets["Restrictive start"]
class TestAllRelevantOptions1(ShapezTestBase): class TestAllRelevantOptions1(ShapezTestBase):
options = { options = {

View File

@@ -130,9 +130,7 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor
### Open the client ### Open the client
Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser. Do not Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser.
switch tabs, open it in a new window if you want to use the browser while playing. Do not minimize the window with the
client.
The client should automatically connect to SNI, the "SNES" status should change to green. The client should automatically connect to SNI, the "SNES" status should change to green.

View File

@@ -1,9 +1,10 @@
import logging import logging
import typing import typing
from random import Random from random import Random
from typing import Dict, Any, Iterable, Optional, List, TextIO from typing import Dict, Any, Optional, List, TextIO
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState import entrance_rando
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from Options import PerGameCommonOptions from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from .bundles.bundle_room import BundleRoom from .bundles.bundle_room import BundleRoom
@@ -21,7 +22,7 @@ from .options.forced_options import force_change_options_if_incompatible
from .options.option_groups import sv_option_groups from .options.option_groups import sv_option_groups
from .options.presets import sv_options_presets from .options.presets import sv_options_presets
from .options.worlds_group import apply_most_restrictive_options from .options.worlds_group import apply_most_restrictive_options
from .regions import create_regions from .regions import create_regions, prepare_mod_data
from .rules import set_rules from .rules import set_rules
from .stardew_rule import True_, StardewRule, HasProgressionPercent from .stardew_rule import True_, StardewRule, HasProgressionPercent
from .strings.ap_names.event_names import Event from .strings.ap_names.event_names import Event
@@ -124,18 +125,13 @@ class StardewValleyWorld(World):
self.content = create_content(self.options) self.content = create_content(self.options)
def create_regions(self): def create_regions(self):
def create_region(name: str, exits: Iterable[str]) -> Region: def create_region(name: str) -> Region:
region = Region(name, self.player, self.multiworld) return Region(name, self.player, self.multiworld)
region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits]
return region
world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options, self.content) world_regions = create_regions(create_region, self.options, self.content)
self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys()) self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys())
self.modified_bundles = get_all_bundles(self.random, self.modified_bundles = get_all_bundles(self.random, self.logic, self.content, self.options)
self.logic,
self.content,
self.options)
def add_location(name: str, code: Optional[int], region: str): def add_location(name: str, code: Optional[int], region: str):
region: Region = world_regions[region] region: Region = world_regions[region]
@@ -308,6 +304,11 @@ class StardewValleyWorld(World):
def set_rules(self): def set_rules(self):
set_rules(self) set_rules(self)
def connect_entrances(self) -> None:
no_target_groups = {0: [0]}
placement = entrance_rando.randomize_entrances(self, coupled=True, target_group_lookup=no_target_groups)
self.randomized_entrances = prepare_mod_data(placement)
def generate_basic(self): def generate_basic(self):
pass pass

View File

@@ -24,6 +24,9 @@ from ...strings.skill_names import Skill
from ...strings.tool_names import Tool, ToolMaterial from ...strings.tool_names import Tool, ToolMaterial
from ...strings.villager_names import ModNPC from ...strings.villager_names import ModNPC
# Used to adapt content not yet moved to content packs to easily detect when SVE and Ginger Island are both enabled.
SVE_GINGER_ISLAND_PACK = ModNames.sve + "+" + ginger_island_content_pack.name
class SVEContentPack(ContentPack): class SVEContentPack(ContentPack):
@@ -67,6 +70,10 @@ class SVEContentPack(ContentPack):
content.game_items.pop(SVESeed.slime) content.game_items.pop(SVESeed.slime)
content.game_items.pop(SVEFruit.slime_berry) content.game_items.pop(SVEFruit.slime_berry)
def finalize_hook(self, content: StardewContent):
if ginger_island_content_pack.name in content.registered_packs:
content.registered_packs.add(SVE_GINGER_ISLAND_PACK)
register_mod_content_pack(SVEContentPack( register_mod_content_pack(SVEContentPack(
ModNames.sve, ModNames.sve,
@@ -80,8 +87,9 @@ register_mod_content_pack(SVEContentPack(
ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),), ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),),
ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),), ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),),
ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),), ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),),
SVEMeal.grampleton_orange_chicken: ( SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650,
ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),), shop_region=Region.saloon,
other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),), ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),),
ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),), ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),),
SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),), SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),),
@@ -118,8 +126,8 @@ register_mod_content_pack(SVEContentPack(
ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),), ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),),
ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,), ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,),
other_requirements=( other_requirements=(CombatRequirement(Performance.galaxy),
CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),), ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),),
ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),), ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),),
SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),), SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),),
@@ -139,8 +147,9 @@ register_mod_content_pack(SVEContentPack(
SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),), SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),),
ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,), ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,),
other_requirements=( other_requirements=(CombatRequirement(Performance.galaxy),
CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),), SkillRequirement(Skill.combat, 10),
YearRequirement(3),)),),
SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),), SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),),
# Fable Reef # Fable Reef
@@ -207,7 +216,6 @@ register_mod_content_pack(SVEContentPack(
villagers_data.scarlett, villagers_data.scarlett,
villagers_data.susan, villagers_data.susan,
villagers_data.morris, villagers_data.morris,
# The wizard leaves his tower on sunday, for like 1 hour... Good enough for entrance rando! override(villagers_data.wizard, bachelor=True, mod_name=ModNames.sve),
override(villagers_data.wizard, locations=(Region.wizard_tower, Region.forest), bachelor=True, mod_name=ModNames.sve),
) )
)) ))

View File

@@ -305,7 +305,7 @@ hopper = ap_recipe(Craftable.hopper, {Material.hardwood: 10, MetalBar.iridium: 1
cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 3, {Material.wood: 15, Material.fiber: 10, Material.coal: 3}) cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 3, {Material.wood: 15, Material.fiber: 10, Material.coal: 3})
tent_kit = skill_recipe(Craftable.tent_kit, Skill.foraging, 8, {Material.hardwood: 10, Material.fiber: 25, ArtisanGood.cloth: 1}) tent_kit = skill_recipe(Craftable.tent_kit, Skill.foraging, 8, {Material.hardwood: 10, Material.fiber: 25, ArtisanGood.cloth: 1})
statue_of_blessings = mastery_recipe(Statue.blessings, Skill.farming, {Material.sap: 999, Material.fiber: 999, Material.stone: 999}) statue_of_blessings = mastery_recipe(Statue.blessings, Skill.farming, {Material.sap: 999, Material.fiber: 999, Material.stone: 999, Material.moss: 333})
statue_of_dwarf_king = mastery_recipe(Statue.dwarf_king, Skill.mining, {MetalBar.iridium: 20}) statue_of_dwarf_king = mastery_recipe(Statue.dwarf_king, Skill.mining, {MetalBar.iridium: 20})
heavy_furnace = mastery_recipe(Machine.heavy_furnace, Skill.mining, {Machine.furnace: 2, MetalBar.iron: 3, Material.stone: 50}) heavy_furnace = mastery_recipe(Machine.heavy_furnace, Skill.mining, {Machine.furnace: 2, MetalBar.iron: 3, Material.stone: 50})
mystic_tree_seed = mastery_recipe(TreeSeed.mystic, Skill.foraging, {TreeSeed.acorn: 5, TreeSeed.maple: 5, TreeSeed.pine: 5, TreeSeed.mahogany: 5}) mystic_tree_seed = mastery_recipe(TreeSeed.mystic, Skill.foraging, {TreeSeed.acorn: 5, TreeSeed.maple: 5, TreeSeed.pine: 5, TreeSeed.mahogany: 5})

View File

@@ -1129,8 +1129,8 @@ id,region,name,tags,mod_name
2204,Leo's Hut,Leo's Parrot,"GINGER_ISLAND,WALNUT_PURCHASE", 2204,Leo's Hut,Leo's Parrot,"GINGER_ISLAND,WALNUT_PURCHASE",
2205,Island South,Island West Turtle,"GINGER_ISLAND,WALNUT_PURCHASE", 2205,Island South,Island West Turtle,"GINGER_ISLAND,WALNUT_PURCHASE",
2206,Island West,Island Farmhouse,"GINGER_ISLAND,WALNUT_PURCHASE", 2206,Island West,Island Farmhouse,"GINGER_ISLAND,WALNUT_PURCHASE",
2207,Island Farmhouse,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE", 2207,Island West,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
2208,Island Farmhouse,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE", 2208,Island West,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
2209,Island North,Dig Site Bridge,"GINGER_ISLAND,WALNUT_PURCHASE", 2209,Island North,Dig Site Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
2210,Island North,Island Trader,"GINGER_ISLAND,WALNUT_PURCHASE", 2210,Island North,Island Trader,"GINGER_ISLAND,WALNUT_PURCHASE",
2211,Volcano Entrance,Volcano Bridge,"GINGER_ISLAND,WALNUT_PURCHASE", 2211,Volcano Entrance,Volcano Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
1 id region name tags mod_name
1129 2204 Leo's Hut Leo's Parrot GINGER_ISLAND,WALNUT_PURCHASE
1130 2205 Island South Island West Turtle GINGER_ISLAND,WALNUT_PURCHASE
1131 2206 Island West Island Farmhouse GINGER_ISLAND,WALNUT_PURCHASE
1132 2207 Island Farmhouse Island West Island Mailbox GINGER_ISLAND,WALNUT_PURCHASE
1133 2208 Island Farmhouse Island West Farm Obelisk GINGER_ISLAND,WALNUT_PURCHASE
1134 2209 Island North Dig Site Bridge GINGER_ISLAND,WALNUT_PURCHASE
1135 2210 Island North Island Trader GINGER_ISLAND,WALNUT_PURCHASE
1136 2211 Volcano Entrance Volcano Bridge GINGER_ISLAND,WALNUT_PURCHASE

View File

@@ -89,7 +89,7 @@ class QuestLogic(BaseLogic):
Quest.goblin_problem: self.logic.region.can_reach(Region.witch_swamp) Quest.goblin_problem: self.logic.region.can_reach(Region.witch_swamp)
# Void mayo can be fished at 5% chance in the witch swamp while the quest is active. It drops a lot after the quest. # Void mayo can be fished at 5% chance in the witch swamp while the quest is active. It drops a lot after the quest.
& (self.logic.has(ArtisanGood.void_mayonnaise) | self.logic.fishing.can_fish()), & (self.logic.has(ArtisanGood.void_mayonnaise) | self.logic.fishing.can_fish()),
Quest.magic_ink: self.logic.relationship.can_meet(NPC.wizard), Quest.magic_ink: self.logic.region.can_reach(Region.witch_hut) & self.logic.relationship.can_meet(NPC.wizard),
Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) & Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) &
self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) & self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) &
self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy), self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy),

View File

@@ -1,23 +1,23 @@
from typing import Tuple, Union from typing import Tuple
from Utils import cache_self1 from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from ..options import EntranceRandomization from ..options import EntranceRandomization
from ..stardew_rule import StardewRule, Reach, false_, true_ from ..stardew_rule import StardewRule, Reach, false_, true_
from ..strings.region_names import Region from ..strings.region_names import Region
main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest, main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest,
Region.bus_stop, Region.backwoods, Region.bus_tunnel, Region.tunnel_entrance} Region.bus_stop, Region.backwoods, Region.bus_tunnel, Region.tunnel_entrance}
always_accessible_regions_without_er = {*main_outside_area, Region.community_center, Region.pantry, Region.crafts_room, Region.fish_tank, Region.boiler_room, always_accessible_regions_with_non_progression_er = {*main_outside_area, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
Region.vault, Region.bulletin_board, Region.mines, Region.hospital, Region.carpenter, Region.alex_house, Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent,
Region.elliott_house, Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent, Region.pierre_store, Region.pierre_store, Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house,
Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house, Region.haley_house, Region.haley_house, Region.sam_house, Region.jojamart, Region.fish_shop}
Region.sam_house, Region.jojamart, Region.fish_shop} always_accessible_regions_without_er = {*always_accessible_regions_with_non_progression_er, Region.community_center, Region.pantry, Region.crafts_room,
Region.fish_tank, Region.boiler_room, Region.vault, Region.bulletin_board}
always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er, always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er,
EntranceRandomization.option_pelican_town: always_accessible_regions_without_er, EntranceRandomization.option_pelican_town: always_accessible_regions_without_er,
EntranceRandomization.option_non_progression: always_accessible_regions_without_er, EntranceRandomization.option_non_progression: always_accessible_regions_with_non_progression_er,
EntranceRandomization.option_buildings_without_house: main_outside_area, EntranceRandomization.option_buildings_without_house: main_outside_area,
EntranceRandomization.option_buildings: main_outside_area, EntranceRandomization.option_buildings: main_outside_area,
EntranceRandomization.option_chaos: always_accessible_regions_without_er} EntranceRandomization.option_chaos: always_accessible_regions_without_er}

View File

@@ -1,8 +1,7 @@
from ..mod_regions import SVERegion
from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.base_logic import BaseLogicMixin, BaseLogic
from ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem from ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem
from ...strings.quest_names import Quest, ModQuest from ...strings.quest_names import Quest, ModQuest
from ...strings.region_names import Region from ...strings.region_names import Region, SVERegion
from ...strings.tool_names import Tool, ToolMaterial from ...strings.tool_names import Tool, ToolMaterial
from ...strings.wallet_item_names import Wallet from ...strings.wallet_item_names import Wallet

View File

@@ -1,15 +1,14 @@
from typing import Dict, List
from .mod_data import ModNames from .mod_data import ModNames
from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData from ..content.mods.sve import SVE_GINGER_ISLAND_PACK
from ..regions.model import RegionData, ConnectionData, MergeFlag, RandomizationFlag, ModRegionsData
from ..strings.entrance_names import Entrance, DeepWoodsEntrance, EugeneEntrance, LaceyEntrance, BoardingHouseEntrance, \ from ..strings.entrance_names import Entrance, DeepWoodsEntrance, EugeneEntrance, LaceyEntrance, BoardingHouseEntrance, \
JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance
from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \ from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \
AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion
deep_woods_regions = [ deep_woods_regions = [
RegionData(Region.farm, [DeepWoodsEntrance.use_woods_obelisk]), RegionData(Region.farm, (DeepWoodsEntrance.use_woods_obelisk,)),
RegionData(DeepWoodsRegion.woods_obelisk_menu, [DeepWoodsEntrance.deep_woods_depth_1, RegionData(DeepWoodsRegion.woods_obelisk_menu, (DeepWoodsEntrance.deep_woods_depth_1,
DeepWoodsEntrance.deep_woods_depth_10, DeepWoodsEntrance.deep_woods_depth_10,
DeepWoodsEntrance.deep_woods_depth_20, DeepWoodsEntrance.deep_woods_depth_20,
DeepWoodsEntrance.deep_woods_depth_30, DeepWoodsEntrance.deep_woods_depth_30,
@@ -19,9 +18,9 @@ deep_woods_regions = [
DeepWoodsEntrance.deep_woods_depth_70, DeepWoodsEntrance.deep_woods_depth_70,
DeepWoodsEntrance.deep_woods_depth_80, DeepWoodsEntrance.deep_woods_depth_80,
DeepWoodsEntrance.deep_woods_depth_90, DeepWoodsEntrance.deep_woods_depth_90,
DeepWoodsEntrance.deep_woods_depth_100]), DeepWoodsEntrance.deep_woods_depth_100)),
RegionData(Region.secret_woods, [DeepWoodsEntrance.secret_woods_to_deep_woods]), RegionData(Region.secret_woods, (DeepWoodsEntrance.secret_woods_to_deep_woods,)),
RegionData(DeepWoodsRegion.main_lichtung, [DeepWoodsEntrance.deep_woods_house]), RegionData(DeepWoodsRegion.main_lichtung, (DeepWoodsEntrance.deep_woods_house,)),
RegionData(DeepWoodsRegion.abandoned_home), RegionData(DeepWoodsRegion.abandoned_home),
RegionData(DeepWoodsRegion.floor_10), RegionData(DeepWoodsRegion.floor_10),
RegionData(DeepWoodsRegion.floor_20), RegionData(DeepWoodsRegion.floor_20),
@@ -32,14 +31,13 @@ deep_woods_regions = [
RegionData(DeepWoodsRegion.floor_70), RegionData(DeepWoodsRegion.floor_70),
RegionData(DeepWoodsRegion.floor_80), RegionData(DeepWoodsRegion.floor_80),
RegionData(DeepWoodsRegion.floor_90), RegionData(DeepWoodsRegion.floor_90),
RegionData(DeepWoodsRegion.floor_100) RegionData(DeepWoodsRegion.floor_100),
] ]
deep_woods_entrances = [ deep_woods_entrances = [
ConnectionData(DeepWoodsEntrance.use_woods_obelisk, DeepWoodsRegion.woods_obelisk_menu), ConnectionData(DeepWoodsEntrance.use_woods_obelisk, DeepWoodsRegion.woods_obelisk_menu),
ConnectionData(DeepWoodsEntrance.secret_woods_to_deep_woods, DeepWoodsRegion.main_lichtung), ConnectionData(DeepWoodsEntrance.secret_woods_to_deep_woods, DeepWoodsRegion.main_lichtung),
ConnectionData(DeepWoodsEntrance.deep_woods_house, DeepWoodsRegion.abandoned_home, ConnectionData(DeepWoodsEntrance.deep_woods_house, DeepWoodsRegion.abandoned_home, flag=RandomizationFlag.BUILDINGS),
flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(DeepWoodsEntrance.deep_woods_depth_1, DeepWoodsRegion.main_lichtung), ConnectionData(DeepWoodsEntrance.deep_woods_depth_1, DeepWoodsRegion.main_lichtung),
ConnectionData(DeepWoodsEntrance.deep_woods_depth_10, DeepWoodsRegion.floor_10), ConnectionData(DeepWoodsEntrance.deep_woods_depth_10, DeepWoodsRegion.floor_10),
ConnectionData(DeepWoodsEntrance.deep_woods_depth_20, DeepWoodsRegion.floor_20), ConnectionData(DeepWoodsEntrance.deep_woods_depth_20, DeepWoodsRegion.floor_20),
@@ -50,165 +48,166 @@ deep_woods_entrances = [
ConnectionData(DeepWoodsEntrance.deep_woods_depth_70, DeepWoodsRegion.floor_70), ConnectionData(DeepWoodsEntrance.deep_woods_depth_70, DeepWoodsRegion.floor_70),
ConnectionData(DeepWoodsEntrance.deep_woods_depth_80, DeepWoodsRegion.floor_80), ConnectionData(DeepWoodsEntrance.deep_woods_depth_80, DeepWoodsRegion.floor_80),
ConnectionData(DeepWoodsEntrance.deep_woods_depth_90, DeepWoodsRegion.floor_90), ConnectionData(DeepWoodsEntrance.deep_woods_depth_90, DeepWoodsRegion.floor_90),
ConnectionData(DeepWoodsEntrance.deep_woods_depth_100, DeepWoodsRegion.floor_100) ConnectionData(DeepWoodsEntrance.deep_woods_depth_100, DeepWoodsRegion.floor_100),
] ]
eugene_regions = [ eugene_regions = [
RegionData(Region.forest, [EugeneEntrance.forest_to_garden]), RegionData(Region.forest, (EugeneEntrance.forest_to_garden,)),
RegionData(EugeneRegion.eugene_garden, [EugeneEntrance.garden_to_bedroom]), RegionData(EugeneRegion.eugene_garden, (EugeneEntrance.garden_to_bedroom,)),
RegionData(EugeneRegion.eugene_bedroom) RegionData(EugeneRegion.eugene_bedroom),
] ]
eugene_entrances = [ eugene_entrances = [
ConnectionData(EugeneEntrance.forest_to_garden, EugeneRegion.eugene_garden, ConnectionData(EugeneEntrance.forest_to_garden, EugeneRegion.eugene_garden,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(EugeneEntrance.garden_to_bedroom, EugeneRegion.eugene_bedroom, flag=RandomizationFlag.BUILDINGS) ConnectionData(EugeneEntrance.garden_to_bedroom, EugeneRegion.eugene_bedroom, flag=RandomizationFlag.BUILDINGS),
] ]
magic_regions = [ magic_regions = [
RegionData(Region.pierre_store, [MagicEntrance.store_to_altar]), RegionData(Region.pierre_store, (MagicEntrance.store_to_altar,)),
RegionData(MagicRegion.altar) RegionData(MagicRegion.altar),
] ]
magic_entrances = [ magic_entrances = [
ConnectionData(MagicEntrance.store_to_altar, MagicRegion.altar, flag=RandomizationFlag.NOT_RANDOMIZED) ConnectionData(MagicEntrance.store_to_altar, MagicRegion.altar, flag=RandomizationFlag.NOT_RANDOMIZED),
] ]
jasper_regions = [ jasper_regions = [
RegionData(Region.museum, [JasperEntrance.museum_to_bedroom]), RegionData(Region.museum, (JasperEntrance.museum_to_bedroom,)),
RegionData(JasperRegion.jasper_bedroom) RegionData(JasperRegion.jasper_bedroom),
] ]
jasper_entrances = [ jasper_entrances = [
ConnectionData(JasperEntrance.museum_to_bedroom, JasperRegion.jasper_bedroom, flag=RandomizationFlag.BUILDINGS) ConnectionData(JasperEntrance.museum_to_bedroom, JasperRegion.jasper_bedroom, flag=RandomizationFlag.BUILDINGS),
] ]
alec_regions = [ alec_regions = [
RegionData(Region.forest, [AlecEntrance.forest_to_petshop]), RegionData(Region.forest, (AlecEntrance.forest_to_petshop,)),
RegionData(AlecRegion.pet_store, [AlecEntrance.petshop_to_bedroom]), RegionData(AlecRegion.pet_store, (AlecEntrance.petshop_to_bedroom,)),
RegionData(AlecRegion.alec_bedroom) RegionData(AlecRegion.alec_bedroom),
] ]
alec_entrances = [ alec_entrances = [
ConnectionData(AlecEntrance.forest_to_petshop, AlecRegion.pet_store, ConnectionData(AlecEntrance.forest_to_petshop, AlecRegion.pet_store,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(AlecEntrance.petshop_to_bedroom, AlecRegion.alec_bedroom, flag=RandomizationFlag.BUILDINGS) ConnectionData(AlecEntrance.petshop_to_bedroom, AlecRegion.alec_bedroom, flag=RandomizationFlag.BUILDINGS),
] ]
yoba_regions = [ yoba_regions = [
RegionData(Region.secret_woods, [YobaEntrance.secret_woods_to_clearing]), RegionData(Region.secret_woods, (YobaEntrance.secret_woods_to_clearing,)),
RegionData(YobaRegion.yoba_clearing) RegionData(YobaRegion.yoba_clearing),
] ]
yoba_entrances = [ yoba_entrances = [
ConnectionData(YobaEntrance.secret_woods_to_clearing, YobaRegion.yoba_clearing, flag=RandomizationFlag.BUILDINGS) ConnectionData(YobaEntrance.secret_woods_to_clearing, YobaRegion.yoba_clearing, flag=RandomizationFlag.BUILDINGS),
] ]
juna_regions = [ juna_regions = [
RegionData(Region.forest, [JunaEntrance.forest_to_juna_cave]), RegionData(Region.forest, (JunaEntrance.forest_to_juna_cave,)),
RegionData(JunaRegion.juna_cave) RegionData(JunaRegion.juna_cave),
] ]
juna_entrances = [ juna_entrances = [
ConnectionData(JunaEntrance.forest_to_juna_cave, JunaRegion.juna_cave, ConnectionData(JunaEntrance.forest_to_juna_cave, JunaRegion.juna_cave,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA) flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
] ]
ayeisha_regions = [ ayeisha_regions = [
RegionData(Region.bus_stop, [AyeishaEntrance.bus_stop_to_mail_van]), RegionData(Region.bus_stop, (AyeishaEntrance.bus_stop_to_mail_van,)),
RegionData(AyeishaRegion.mail_van) RegionData(AyeishaRegion.mail_van),
] ]
ayeisha_entrances = [ ayeisha_entrances = [
ConnectionData(AyeishaEntrance.bus_stop_to_mail_van, AyeishaRegion.mail_van, ConnectionData(AyeishaEntrance.bus_stop_to_mail_van, AyeishaRegion.mail_van,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA) flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
] ]
riley_regions = [ riley_regions = [
RegionData(Region.town, [RileyEntrance.town_to_riley]), RegionData(Region.town, (RileyEntrance.town_to_riley,)),
RegionData(RileyRegion.riley_house) RegionData(RileyRegion.riley_house),
] ]
riley_entrances = [ riley_entrances = [
ConnectionData(RileyEntrance.town_to_riley, RileyRegion.riley_house, ConnectionData(RileyEntrance.town_to_riley, RileyRegion.riley_house,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA) flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
] ]
stardew_valley_expanded_regions = [ sve_main_land_regions = [
RegionData(Region.backwoods, [SVEEntrance.backwoods_to_grove]), RegionData(Region.backwoods, (SVEEntrance.backwoods_to_grove,)),
RegionData(SVERegion.enchanted_grove, [SVEEntrance.grove_to_outpost_warp, SVEEntrance.grove_to_wizard_warp, RegionData(SVERegion.enchanted_grove, (SVEEntrance.grove_to_outpost_warp, SVEEntrance.grove_to_wizard_warp,
SVEEntrance.grove_to_farm_warp, SVEEntrance.grove_to_guild_warp, SVEEntrance.grove_to_junimo_warp, SVEEntrance.grove_to_farm_warp, SVEEntrance.grove_to_guild_warp, SVEEntrance.grove_to_junimo_warp,
SVEEntrance.grove_to_spring_warp, SVEEntrance.grove_to_aurora_warp]), SVEEntrance.grove_to_spring_warp, SVEEntrance.grove_to_aurora_warp)),
RegionData(SVERegion.grove_farm_warp, [SVEEntrance.farm_warp_to_farm]), RegionData(SVERegion.grove_farm_warp, (SVEEntrance.farm_warp_to_farm,)),
RegionData(SVERegion.grove_aurora_warp, [SVEEntrance.aurora_warp_to_aurora]), RegionData(SVERegion.grove_aurora_warp, (SVEEntrance.aurora_warp_to_aurora,)),
RegionData(SVERegion.grove_guild_warp, [SVEEntrance.guild_warp_to_guild]), RegionData(SVERegion.grove_guild_warp, (SVEEntrance.guild_warp_to_guild,)),
RegionData(SVERegion.grove_junimo_warp, [SVEEntrance.junimo_warp_to_junimo]), RegionData(SVERegion.grove_junimo_warp, (SVEEntrance.junimo_warp_to_junimo,)),
RegionData(SVERegion.grove_spring_warp, [SVEEntrance.spring_warp_to_spring]), RegionData(SVERegion.grove_spring_warp, (SVEEntrance.spring_warp_to_spring,)),
RegionData(SVERegion.grove_outpost_warp, [SVEEntrance.outpost_warp_to_outpost]), RegionData(SVERegion.grove_outpost_warp, (SVEEntrance.outpost_warp_to_outpost,)),
RegionData(SVERegion.grove_wizard_warp, [SVEEntrance.wizard_warp_to_wizard]), RegionData(SVERegion.grove_wizard_warp, (SVEEntrance.wizard_warp_to_wizard,)),
RegionData(SVERegion.galmoran_outpost, [SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop, RegionData(SVERegion.galmoran_outpost, (SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop, SVEEntrance.use_isaac_shop)),
SVEEntrance.use_isaac_shop]), RegionData(SVERegion.badlands_entrance, (SVEEntrance.badlands_entrance_to_badlands,)),
RegionData(SVERegion.badlands_entrance, [SVEEntrance.badlands_entrance_to_badlands]), RegionData(SVERegion.crimson_badlands, (SVEEntrance.badlands_to_cave,)),
RegionData(SVERegion.crimson_badlands, [SVEEntrance.badlands_to_cave]),
RegionData(SVERegion.badlands_cave), RegionData(SVERegion.badlands_cave),
RegionData(Region.bus_stop, [SVEEntrance.bus_stop_to_shed]), RegionData(Region.bus_stop, (SVEEntrance.bus_stop_to_shed,)),
RegionData(SVERegion.grandpas_shed, [SVEEntrance.grandpa_shed_to_interior, SVEEntrance.grandpa_shed_to_town]), RegionData(SVERegion.grandpas_shed, (SVEEntrance.grandpa_shed_to_interior, SVEEntrance.grandpa_shed_to_town)),
RegionData(SVERegion.grandpas_shed_interior, [SVEEntrance.grandpa_interior_to_upstairs]), RegionData(SVERegion.grandpas_shed_interior, (SVEEntrance.grandpa_interior_to_upstairs,)),
RegionData(SVERegion.grandpas_shed_upstairs), RegionData(SVERegion.grandpas_shed_upstairs),
RegionData(Region.forest, RegionData(Region.forest,
[SVEEntrance.forest_to_fairhaven, SVEEntrance.forest_to_west, SVEEntrance.forest_to_lost_woods, (SVEEntrance.forest_to_fairhaven, SVEEntrance.forest_to_west, SVEEntrance.forest_to_lost_woods,
SVEEntrance.forest_to_bmv, SVEEntrance.forest_to_marnie_shed]), SVEEntrance.forest_to_bmv, SVEEntrance.forest_to_marnie_shed)),
RegionData(SVERegion.marnies_shed), RegionData(SVERegion.marnies_shed),
RegionData(SVERegion.fairhaven_farm), RegionData(SVERegion.fairhaven_farm),
RegionData(Region.town, [SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins, RegionData(Region.town, (SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins, SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot)),
SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot]), RegionData(SVERegion.blue_moon_vineyard, (SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach)),
RegionData(SVERegion.blue_moon_vineyard, [SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach]),
RegionData(SVERegion.sophias_house), RegionData(SVERegion.sophias_house),
RegionData(SVERegion.jenkins_residence, [SVEEntrance.jenkins_to_cellar]), RegionData(SVERegion.jenkins_residence, (SVEEntrance.jenkins_to_cellar,)),
RegionData(SVERegion.jenkins_cellar), RegionData(SVERegion.jenkins_cellar),
RegionData(SVERegion.unclaimed_plot, [SVEEntrance.plot_to_bridge]), RegionData(SVERegion.unclaimed_plot, (SVEEntrance.plot_to_bridge,)),
RegionData(SVERegion.shearwater), RegionData(SVERegion.shearwater),
RegionData(Region.museum, [SVEEntrance.museum_to_gunther_bedroom]), RegionData(Region.museum, (SVEEntrance.museum_to_gunther_bedroom,)),
RegionData(SVERegion.gunther_bedroom), RegionData(SVERegion.gunther_bedroom),
RegionData(Region.fish_shop, [SVEEntrance.fish_shop_to_willy_bedroom]), RegionData(Region.fish_shop, (SVEEntrance.fish_shop_to_willy_bedroom,)),
RegionData(SVERegion.willy_bedroom), RegionData(SVERegion.willy_bedroom),
RegionData(Region.mountain, [SVEEntrance.mountain_to_guild_summit]), RegionData(Region.mountain, (SVEEntrance.mountain_to_guild_summit,)),
RegionData(SVERegion.guild_summit, [SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines, # These entrances are removed from the mountain region when SVE is enabled
SVEEntrance.summit_to_highlands]), RegionData(Region.mountain, (Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_the_mines), flag=MergeFlag.REMOVE_EXITS),
RegionData(Region.railroad, [SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station]), RegionData(SVERegion.guild_summit, (SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines)),
RegionData(SVERegion.grampleton_station, [SVEEntrance.grampleton_station_to_grampleton_suburbs]), RegionData(Region.railroad, (SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station)),
RegionData(SVERegion.grampleton_suburbs, [SVEEntrance.grampleton_suburbs_to_scarlett_house]), RegionData(SVERegion.grampleton_station, (SVEEntrance.grampleton_station_to_grampleton_suburbs,)),
RegionData(SVERegion.grampleton_suburbs, (SVEEntrance.grampleton_suburbs_to_scarlett_house,)),
RegionData(SVERegion.scarlett_house), RegionData(SVERegion.scarlett_house),
RegionData(Region.wizard_basement, [SVEEntrance.wizard_to_fable_reef]), RegionData(SVERegion.forest_west, (SVEEntrance.forest_west_to_spring, SVEEntrance.west_to_aurora, SVEEntrance.use_bear_shop,)),
RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild], is_ginger_island=True), RegionData(SVERegion.aurora_vineyard, (SVEEntrance.to_aurora_basement,)),
RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True),
RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True),
RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True),
RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond], is_ginger_island=True),
RegionData(SVERegion.highlands_pond, is_ginger_island=True),
RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True),
RegionData(SVERegion.dwarf_prison, is_ginger_island=True),
RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True),
RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands], is_ginger_island=True),
RegionData(SVERegion.forest_west, [SVEEntrance.forest_west_to_spring, SVEEntrance.west_to_aurora,
SVEEntrance.use_bear_shop]),
RegionData(SVERegion.aurora_vineyard, [SVEEntrance.to_aurora_basement]),
RegionData(SVERegion.aurora_vineyard_basement), RegionData(SVERegion.aurora_vineyard_basement),
RegionData(Region.secret_woods, [SVEEntrance.secret_woods_to_west]), RegionData(Region.secret_woods, (SVEEntrance.secret_woods_to_west,)),
RegionData(SVERegion.bear_shop), RegionData(SVERegion.bear_shop),
RegionData(SVERegion.sprite_spring, [SVEEntrance.sprite_spring_to_cave]), RegionData(SVERegion.sprite_spring, (SVEEntrance.sprite_spring_to_cave,)),
RegionData(SVERegion.sprite_spring_cave), RegionData(SVERegion.sprite_spring_cave),
RegionData(SVERegion.lost_woods, [SVEEntrance.lost_woods_to_junimo_woods]), RegionData(SVERegion.lost_woods, (SVEEntrance.lost_woods_to_junimo_woods,)),
RegionData(SVERegion.junimo_woods, [SVEEntrance.use_purple_junimo]), RegionData(SVERegion.junimo_woods, (SVEEntrance.use_purple_junimo,)),
RegionData(SVERegion.purple_junimo_shop), RegionData(SVERegion.purple_junimo_shop),
RegionData(SVERegion.alesia_shop), RegionData(SVERegion.alesia_shop),
RegionData(SVERegion.isaac_shop), RegionData(SVERegion.isaac_shop),
RegionData(SVERegion.summit), RegionData(SVERegion.summit),
RegionData(SVERegion.susans_house), RegionData(SVERegion.susans_house),
RegionData(Region.mountain, [Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_the_mines], ModificationFlag.MODIFIED)
] ]
mandatory_sve_connections = [ sve_ginger_island_regions = [
RegionData(Region.wizard_basement, (SVEEntrance.wizard_to_fable_reef,)),
RegionData(SVERegion.fable_reef, (SVEEntrance.fable_reef_to_guild,)),
RegionData(SVERegion.first_slash_guild, (SVEEntrance.first_slash_guild_to_hallway,)),
RegionData(SVERegion.first_slash_hallway, (SVEEntrance.first_slash_hallway_to_room,)),
RegionData(SVERegion.first_slash_spare_room),
RegionData(SVERegion.guild_summit, (SVEEntrance.summit_to_highlands,)),
RegionData(SVERegion.highlands_outside, (SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond), ),
RegionData(SVERegion.highlands_pond),
RegionData(SVERegion.highlands_cavern, (SVEEntrance.to_dwarf_prison,)),
RegionData(SVERegion.dwarf_prison),
RegionData(SVERegion.lances_house, (SVEEntrance.lance_to_ladder,)),
RegionData(SVERegion.lances_ladder, (SVEEntrance.lance_ladder_to_highlands,)),
]
sve_main_land_connections = [
ConnectionData(SVEEntrance.town_to_jenkins, SVERegion.jenkins_residence, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(SVEEntrance.town_to_jenkins, SVERegion.jenkins_residence, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(SVEEntrance.jenkins_to_cellar, SVERegion.jenkins_cellar, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.jenkins_to_cellar, SVERegion.jenkins_cellar, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.forest_to_bmv, SVERegion.blue_moon_vineyard), ConnectionData(SVEEntrance.forest_to_bmv, SVERegion.blue_moon_vineyard),
@@ -223,7 +222,7 @@ mandatory_sve_connections = [
ConnectionData(SVEEntrance.grandpa_interior_to_upstairs, SVERegion.grandpas_shed_upstairs, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.grandpa_interior_to_upstairs, SVERegion.grandpas_shed_upstairs, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.grandpa_shed_to_town, Region.town), ConnectionData(SVEEntrance.grandpa_shed_to_town, Region.town),
ConnectionData(SVEEntrance.bmv_to_sophia, SVERegion.sophias_house, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(SVEEntrance.bmv_to_sophia, SVERegion.sophias_house, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(SVEEntrance.summit_to_highlands, SVERegion.highlands_outside, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(SVEEntrance.summit_to_highlands, SVERegion.highlands_outside),
ConnectionData(SVEEntrance.guild_to_interior, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.guild_to_interior, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.backwoods_to_grove, SVERegion.enchanted_grove, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(SVEEntrance.backwoods_to_grove, SVERegion.enchanted_grove, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(SVEEntrance.grove_to_outpost_warp, SVERegion.grove_outpost_warp), ConnectionData(SVEEntrance.grove_to_outpost_warp, SVERegion.grove_outpost_warp),
@@ -242,8 +241,6 @@ mandatory_sve_connections = [
ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop), ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop),
ConnectionData(SVEEntrance.grove_to_spring_warp, SVERegion.grove_spring_warp), ConnectionData(SVEEntrance.grove_to_spring_warp, SVERegion.grove_spring_warp),
ConnectionData(SVEEntrance.spring_warp_to_spring, SVERegion.sprite_spring, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.spring_warp_to_spring, SVERegion.sprite_spring, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.wizard_to_fable_reef, SVERegion.fable_reef, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.fable_reef_to_guild, SVERegion.first_slash_guild, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.outpost_to_badlands_entrance, SVERegion.badlands_entrance, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.outpost_to_badlands_entrance, SVERegion.badlands_entrance, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.badlands_entrance_to_badlands, SVERegion.crimson_badlands, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.badlands_entrance_to_badlands, SVERegion.crimson_badlands, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.badlands_to_cave, SVERegion.badlands_cave, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.badlands_to_cave, SVERegion.badlands_cave, flag=RandomizationFlag.BUILDINGS),
@@ -259,71 +256,75 @@ mandatory_sve_connections = [
ConnectionData(SVEEntrance.to_susan_house, SVERegion.susans_house, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.to_susan_house, SVERegion.susans_house, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.enter_summit, SVERegion.summit, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.enter_summit, SVERegion.summit, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.forest_to_fairhaven, SVERegion.fairhaven_farm, flag=RandomizationFlag.NON_PROGRESSION), ConnectionData(SVEEntrance.forest_to_fairhaven, SVERegion.fairhaven_farm, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(SVEEntrance.highlands_to_lance, SVERegion.lances_house, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.lance_to_ladder, SVERegion.lances_ladder, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.lance_ladder_to_highlands, SVERegion.highlands_outside, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.highlands_to_cave, SVERegion.highlands_cavern, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.use_bear_shop, SVERegion.bear_shop), ConnectionData(SVEEntrance.use_bear_shop, SVERegion.bear_shop),
ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop), ConnectionData(SVEEntrance.use_purple_junimo, SVERegion.purple_junimo_shop),
ConnectionData(SVEEntrance.use_alesia_shop, SVERegion.alesia_shop), ConnectionData(SVEEntrance.use_alesia_shop, SVERegion.alesia_shop),
ConnectionData(SVEEntrance.use_isaac_shop, SVERegion.isaac_shop), ConnectionData(SVEEntrance.use_isaac_shop, SVERegion.isaac_shop),
ConnectionData(SVEEntrance.to_dwarf_prison, SVERegion.dwarf_prison, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.railroad_to_grampleton_station, SVERegion.grampleton_station), ConnectionData(SVEEntrance.railroad_to_grampleton_station, SVERegion.grampleton_station),
ConnectionData(SVEEntrance.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs), ConnectionData(SVEEntrance.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs),
ConnectionData(SVEEntrance.grampleton_suburbs_to_scarlett_house, SVERegion.scarlett_house, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.grampleton_suburbs_to_scarlett_house, SVERegion.scarlett_house, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.first_slash_guild_to_hallway, SVERegion.first_slash_hallway, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond), ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond),
] ]
sve_ginger_island_connections = [
ConnectionData(SVEEntrance.wizard_to_fable_reef, SVERegion.fable_reef, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.fable_reef_to_guild, SVERegion.first_slash_guild, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.highlands_to_lance, SVERegion.lances_house, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.lance_to_ladder, SVERegion.lances_ladder),
ConnectionData(SVEEntrance.lance_ladder_to_highlands, SVERegion.highlands_outside, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.highlands_to_cave, SVERegion.highlands_cavern, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.to_dwarf_prison, SVERegion.dwarf_prison, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.first_slash_guild_to_hallway, SVERegion.first_slash_hallway, flag=RandomizationFlag.BUILDINGS),
ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room, flag=RandomizationFlag.BUILDINGS),
]
alecto_regions = [ alecto_regions = [
RegionData(Region.witch_hut, [AlectoEntrance.witch_hut_to_witch_attic]), RegionData(Region.witch_hut, (AlectoEntrance.witch_hut_to_witch_attic,)),
RegionData(AlectoRegion.witch_attic) RegionData(AlectoRegion.witch_attic),
] ]
alecto_entrances = [ alecto_entrances = [
ConnectionData(AlectoEntrance.witch_hut_to_witch_attic, AlectoRegion.witch_attic, flag=RandomizationFlag.BUILDINGS) ConnectionData(AlectoEntrance.witch_hut_to_witch_attic, AlectoRegion.witch_attic, flag=RandomizationFlag.BUILDINGS),
] ]
lacey_regions = [ lacey_regions = [
RegionData(Region.forest, [LaceyEntrance.forest_to_hat_house]), RegionData(Region.forest, (LaceyEntrance.forest_to_hat_house,)),
RegionData(LaceyRegion.hat_house) RegionData(LaceyRegion.hat_house),
] ]
lacey_entrances = [ lacey_entrances = [
ConnectionData(LaceyEntrance.forest_to_hat_house, LaceyRegion.hat_house, flag=RandomizationFlag.BUILDINGS) ConnectionData(LaceyEntrance.forest_to_hat_house, LaceyRegion.hat_house, flag=RandomizationFlag.BUILDINGS),
] ]
boarding_house_regions = [ boarding_house_regions = [
RegionData(Region.bus_stop, [BoardingHouseEntrance.bus_stop_to_boarding_house_plateau]), RegionData(Region.bus_stop, (BoardingHouseEntrance.bus_stop_to_boarding_house_plateau,)),
RegionData(BoardingHouseRegion.boarding_house_plateau, [BoardingHouseEntrance.boarding_house_plateau_to_boarding_house_first, RegionData(BoardingHouseRegion.boarding_house_plateau, (BoardingHouseEntrance.boarding_house_plateau_to_boarding_house_first,
BoardingHouseEntrance.boarding_house_plateau_to_buffalo_ranch, BoardingHouseEntrance.boarding_house_plateau_to_buffalo_ranch,
BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance]), BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance)),
RegionData(BoardingHouseRegion.boarding_house_first, [BoardingHouseEntrance.boarding_house_first_to_boarding_house_second]), RegionData(BoardingHouseRegion.boarding_house_first, (BoardingHouseEntrance.boarding_house_first_to_boarding_house_second,)),
RegionData(BoardingHouseRegion.boarding_house_second), RegionData(BoardingHouseRegion.boarding_house_second),
RegionData(BoardingHouseRegion.buffalo_ranch), RegionData(BoardingHouseRegion.buffalo_ranch),
RegionData(BoardingHouseRegion.abandoned_mines_entrance, [BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a, RegionData(BoardingHouseRegion.abandoned_mines_entrance, (BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a,
BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley]), BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley)),
RegionData(BoardingHouseRegion.abandoned_mines_1a, [BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b]), RegionData(BoardingHouseRegion.abandoned_mines_1a, (BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b,)),
RegionData(BoardingHouseRegion.abandoned_mines_1b, [BoardingHouseEntrance.abandoned_mines_1b_to_abandoned_mines_2a]), RegionData(BoardingHouseRegion.abandoned_mines_1b, (BoardingHouseEntrance.abandoned_mines_1b_to_abandoned_mines_2a,)),
RegionData(BoardingHouseRegion.abandoned_mines_2a, [BoardingHouseEntrance.abandoned_mines_2a_to_abandoned_mines_2b]), RegionData(BoardingHouseRegion.abandoned_mines_2a, (BoardingHouseEntrance.abandoned_mines_2a_to_abandoned_mines_2b,)),
RegionData(BoardingHouseRegion.abandoned_mines_2b, [BoardingHouseEntrance.abandoned_mines_2b_to_abandoned_mines_3]), RegionData(BoardingHouseRegion.abandoned_mines_2b, (BoardingHouseEntrance.abandoned_mines_2b_to_abandoned_mines_3,)),
RegionData(BoardingHouseRegion.abandoned_mines_3, [BoardingHouseEntrance.abandoned_mines_3_to_abandoned_mines_4]), RegionData(BoardingHouseRegion.abandoned_mines_3, (BoardingHouseEntrance.abandoned_mines_3_to_abandoned_mines_4,)),
RegionData(BoardingHouseRegion.abandoned_mines_4, [BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5]), RegionData(BoardingHouseRegion.abandoned_mines_4, (BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5,)),
RegionData(BoardingHouseRegion.abandoned_mines_5, [BoardingHouseEntrance.abandoned_mines_5_to_the_lost_valley]), RegionData(BoardingHouseRegion.abandoned_mines_5, (BoardingHouseEntrance.abandoned_mines_5_to_the_lost_valley,)),
RegionData(BoardingHouseRegion.the_lost_valley, [BoardingHouseEntrance.the_lost_valley_to_gregory_tent, RegionData(BoardingHouseRegion.the_lost_valley, (BoardingHouseEntrance.the_lost_valley_to_gregory_tent,
BoardingHouseEntrance.lost_valley_to_lost_valley_minecart, BoardingHouseEntrance.lost_valley_to_lost_valley_minecart,
BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins]), BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins)),
RegionData(BoardingHouseRegion.gregory_tent), RegionData(BoardingHouseRegion.gregory_tent),
RegionData(BoardingHouseRegion.lost_valley_ruins, [BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, RegionData(BoardingHouseRegion.lost_valley_ruins, (BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1,
BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2]), BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2)),
RegionData(BoardingHouseRegion.lost_valley_minecart), RegionData(BoardingHouseRegion.lost_valley_minecart),
RegionData(BoardingHouseRegion.lost_valley_house_1), RegionData(BoardingHouseRegion.lost_valley_house_1),
RegionData(BoardingHouseRegion.lost_valley_house_2) RegionData(BoardingHouseRegion.lost_valley_house_2),
] ]
boarding_house_entrances = [ boarding_house_entrances = [
@@ -351,30 +352,29 @@ boarding_house_entrances = [
ConnectionData(BoardingHouseEntrance.lost_valley_to_lost_valley_minecart, BoardingHouseRegion.lost_valley_minecart), ConnectionData(BoardingHouseEntrance.lost_valley_to_lost_valley_minecart, BoardingHouseRegion.lost_valley_minecart),
ConnectionData(BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, BoardingHouseRegion.lost_valley_ruins, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, BoardingHouseRegion.lost_valley_ruins, flag=RandomizationFlag.BUILDINGS),
ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, BoardingHouseRegion.lost_valley_house_1, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, BoardingHouseRegion.lost_valley_house_1, flag=RandomizationFlag.BUILDINGS),
ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2, BoardingHouseRegion.lost_valley_house_2, flag=RandomizationFlag.BUILDINGS) ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2, BoardingHouseRegion.lost_valley_house_2, flag=RandomizationFlag.BUILDINGS),
] ]
vanilla_connections_to_remove_by_mod: Dict[str, List[ConnectionData]] = { vanilla_connections_to_remove_by_content_pack: dict[str, tuple[str, ...]] = {
ModNames.sve: [ ModNames.sve: (
ConnectionData(Entrance.mountain_to_the_mines, Region.mines, Entrance.mountain_to_the_mines,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), Entrance.mountain_to_adventurer_guild,
ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, )
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
]
} }
ModDataList = { region_data_by_content_pack = {
ModNames.deepwoods: ModRegionData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances), ModNames.deepwoods: ModRegionsData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances),
ModNames.eugene: ModRegionData(ModNames.eugene, eugene_regions, eugene_entrances), ModNames.eugene: ModRegionsData(ModNames.eugene, eugene_regions, eugene_entrances),
ModNames.jasper: ModRegionData(ModNames.jasper, jasper_regions, jasper_entrances), ModNames.jasper: ModRegionsData(ModNames.jasper, jasper_regions, jasper_entrances),
ModNames.alec: ModRegionData(ModNames.alec, alec_regions, alec_entrances), ModNames.alec: ModRegionsData(ModNames.alec, alec_regions, alec_entrances),
ModNames.yoba: ModRegionData(ModNames.yoba, yoba_regions, yoba_entrances), ModNames.yoba: ModRegionsData(ModNames.yoba, yoba_regions, yoba_entrances),
ModNames.juna: ModRegionData(ModNames.juna, juna_regions, juna_entrances), ModNames.juna: ModRegionsData(ModNames.juna, juna_regions, juna_entrances),
ModNames.magic: ModRegionData(ModNames.magic, magic_regions, magic_entrances), ModNames.magic: ModRegionsData(ModNames.magic, magic_regions, magic_entrances),
ModNames.ayeisha: ModRegionData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances), ModNames.ayeisha: ModRegionsData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances),
ModNames.riley: ModRegionData(ModNames.riley, riley_regions, riley_entrances), ModNames.riley: ModRegionsData(ModNames.riley, riley_regions, riley_entrances),
ModNames.sve: ModRegionData(ModNames.sve, stardew_valley_expanded_regions, mandatory_sve_connections), ModNames.sve: ModRegionsData(ModNames.sve, sve_main_land_regions, sve_main_land_connections),
ModNames.alecto: ModRegionData(ModNames.alecto, alecto_regions, alecto_entrances), SVE_GINGER_ISLAND_PACK: ModRegionsData(SVE_GINGER_ISLAND_PACK, sve_ginger_island_regions, sve_ginger_island_connections),
ModNames.lacey: ModRegionData(ModNames.lacey, lacey_regions, lacey_entrances), ModNames.alecto: ModRegionsData(ModNames.alecto, alecto_regions, alecto_entrances),
ModNames.boarding_house: ModRegionData(ModNames.boarding_house, boarding_house_regions, boarding_house_entrances), ModNames.lacey: ModRegionsData(ModNames.lacey, lacey_regions, lacey_entrances),
ModNames.boarding_house: ModRegionsData(ModNames.boarding_house, boarding_house_regions, boarding_house_entrances),
} }

View File

@@ -1,67 +0,0 @@
from copy import deepcopy
from dataclasses import dataclass, field
from enum import IntFlag
from typing import Optional, List, Set
connector_keyword = " to "
class ModificationFlag(IntFlag):
NOT_MODIFIED = 0
MODIFIED = 1
class RandomizationFlag(IntFlag):
NOT_RANDOMIZED = 0b0
PELICAN_TOWN = 0b00011111
NON_PROGRESSION = 0b00011110
BUILDINGS = 0b00011100
EVERYTHING = 0b00011000
GINGER_ISLAND = 0b00100000
LEAD_TO_OPEN_AREA = 0b01000000
MASTERIES = 0b10000000
@dataclass(frozen=True)
class RegionData:
name: str
exits: List[str] = field(default_factory=list)
flag: ModificationFlag = ModificationFlag.NOT_MODIFIED
is_ginger_island: bool = False
def get_merged_with(self, exits: List[str]):
merged_exits = []
merged_exits.extend(self.exits)
if exits is not None:
merged_exits.extend(exits)
merged_exits = sorted(set(merged_exits))
return RegionData(self.name, merged_exits, is_ginger_island=self.is_ginger_island)
def get_without_exits(self, exits_to_remove: Set[str]):
exits = [exit_ for exit_ in self.exits if exit_ not in exits_to_remove]
return RegionData(self.name, exits, is_ginger_island=self.is_ginger_island)
def get_clone(self):
return deepcopy(self)
@dataclass(frozen=True)
class ConnectionData:
name: str
destination: str
origin: Optional[str] = None
reverse: Optional[str] = None
flag: RandomizationFlag = RandomizationFlag.NOT_RANDOMIZED
def __post_init__(self):
if connector_keyword in self.name:
origin, destination = self.name.split(connector_keyword)
if self.reverse is None:
super().__setattr__("reverse", f"{destination}{connector_keyword}{origin}")
@dataclass(frozen=True)
class ModRegionData:
mod_name: str
regions: List[RegionData]
connections: List[ConnectionData]

View File

@@ -1,775 +0,0 @@
from random import Random
from typing import Iterable, Dict, Protocol, List, Tuple, Set
from BaseClasses import Region, Entrance
from .content import content_packs, StardewContent
from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod
from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions
from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag
from .strings.entrance_names import Entrance, LogicEntrance
from .strings.region_names import Region as RegionName, LogicRegion
class RegionFactory(Protocol):
def __call__(self, name: str, regions: Iterable[str]) -> Region:
raise NotImplementedError
vanilla_regions = [
RegionData(RegionName.menu, [Entrance.to_stardew_valley]),
RegionData(RegionName.stardew_valley, [Entrance.to_farmhouse]),
RegionData(RegionName.farm_house,
[Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce]),
RegionData(RegionName.cellar),
RegionData(RegionName.farm,
[Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse,
Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops,
LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping,
LogicEntrance.fishing, ]),
RegionData(RegionName.backwoods, [Entrance.backwoods_to_mountain]),
RegionData(RegionName.bus_stop,
[Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance]),
RegionData(RegionName.forest,
[Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch,
Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant,
LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby,
LogicEntrance.attend_festival_of_ice]),
RegionData(LogicRegion.forest_waterfall),
RegionData(RegionName.farm_cave),
RegionData(RegionName.greenhouse,
[LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse,
LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse]),
RegionData(RegionName.mountain,
[Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop,
Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild,
Entrance.mountain_to_town, Entrance.mountain_to_maru_room,
Entrance.mountain_to_leo_treehouse]),
RegionData(RegionName.leo_treehouse, is_ginger_island=True),
RegionData(RegionName.maru_room),
RegionData(RegionName.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]),
RegionData(RegionName.bus_tunnel),
RegionData(RegionName.town,
[Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store,
Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house,
Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart,
Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair,
LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star]),
RegionData(RegionName.beach,
[Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.attend_luau,
LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]),
RegionData(RegionName.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]),
RegionData(RegionName.ranch),
RegionData(RegionName.leah_house),
RegionData(RegionName.mastery_cave),
RegionData(RegionName.sewer, [Entrance.enter_mutant_bug_lair]),
RegionData(RegionName.mutant_bug_lair),
RegionData(RegionName.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]),
RegionData(RegionName.wizard_basement),
RegionData(RegionName.tent),
RegionData(RegionName.carpenter, [Entrance.enter_sebastian_room]),
RegionData(RegionName.sebastian_room),
RegionData(RegionName.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]),
RegionData(RegionName.adventurer_guild_bedroom),
RegionData(RegionName.community_center,
[Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank,
Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault]),
RegionData(RegionName.crafts_room),
RegionData(RegionName.pantry),
RegionData(RegionName.fish_tank),
RegionData(RegionName.boiler_room),
RegionData(RegionName.bulletin_board),
RegionData(RegionName.vault),
RegionData(RegionName.hospital, [Entrance.enter_harvey_room]),
RegionData(RegionName.harvey_room),
RegionData(RegionName.pierre_store, [Entrance.enter_sunroom]),
RegionData(RegionName.sunroom),
RegionData(RegionName.saloon, [Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart]),
RegionData(RegionName.jotpk_world_1, [Entrance.reach_jotpk_world_2]),
RegionData(RegionName.jotpk_world_2, [Entrance.reach_jotpk_world_3]),
RegionData(RegionName.jotpk_world_3),
RegionData(RegionName.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
RegionData(RegionName.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
RegionData(RegionName.junimo_kart_3, [Entrance.reach_junimo_kart_4]),
RegionData(RegionName.junimo_kart_4),
RegionData(RegionName.alex_house),
RegionData(RegionName.trailer),
RegionData(RegionName.mayor_house),
RegionData(RegionName.sam_house),
RegionData(RegionName.haley_house),
RegionData(RegionName.blacksmith, [LogicEntrance.blacksmith_copper]),
RegionData(RegionName.museum),
RegionData(RegionName.jojamart, [Entrance.enter_abandoned_jojamart]),
RegionData(RegionName.abandoned_jojamart, [Entrance.enter_movie_theater]),
RegionData(RegionName.movie_ticket_stand),
RegionData(RegionName.movie_theater),
RegionData(RegionName.fish_shop, [Entrance.fish_shop_to_boat_tunnel]),
RegionData(RegionName.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True),
RegionData(RegionName.elliott_house),
RegionData(RegionName.tide_pools),
RegionData(RegionName.bathhouse_entrance, [Entrance.enter_locker_room]),
RegionData(RegionName.locker_room, [Entrance.enter_public_bath]),
RegionData(RegionName.public_bath),
RegionData(RegionName.witch_warp_cave, [Entrance.enter_witch_swamp]),
RegionData(RegionName.witch_swamp, [Entrance.enter_witch_hut]),
RegionData(RegionName.witch_hut, [Entrance.witch_warp_to_wizard_basement]),
RegionData(RegionName.quarry, [Entrance.enter_quarry_mine_entrance]),
RegionData(RegionName.quarry_mine_entrance, [Entrance.enter_quarry_mine]),
RegionData(RegionName.quarry_mine),
RegionData(RegionName.secret_woods),
RegionData(RegionName.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]),
RegionData(RegionName.oasis, [Entrance.enter_casino]),
RegionData(RegionName.casino),
RegionData(RegionName.skull_cavern_entrance, [Entrance.enter_skull_cavern]),
RegionData(RegionName.skull_cavern, [Entrance.mine_to_skull_cavern_floor_25]),
RegionData(RegionName.skull_cavern_25, [Entrance.mine_to_skull_cavern_floor_50]),
RegionData(RegionName.skull_cavern_50, [Entrance.mine_to_skull_cavern_floor_75]),
RegionData(RegionName.skull_cavern_75, [Entrance.mine_to_skull_cavern_floor_100]),
RegionData(RegionName.skull_cavern_100, [Entrance.mine_to_skull_cavern_floor_125]),
RegionData(RegionName.skull_cavern_125, [Entrance.mine_to_skull_cavern_floor_150]),
RegionData(RegionName.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]),
RegionData(RegionName.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]),
RegionData(RegionName.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]),
RegionData(RegionName.dangerous_skull_cavern, is_ginger_island=True),
RegionData(RegionName.island_south,
[Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast,
Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site,
Entrance.parrot_express_docks_to_jungle],
is_ginger_island=True),
RegionData(RegionName.island_resort, is_ginger_island=True),
RegionData(RegionName.island_west,
[Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave,
Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks,
Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island,
LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island,
LogicEntrance.grow_indoor_crops_on_island],
is_ginger_island=True),
RegionData(RegionName.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True),
RegionData(RegionName.island_shrine, is_ginger_island=True),
RegionData(RegionName.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True),
RegionData(RegionName.island_north,
[Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano,
Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks],
is_ginger_island=True),
RegionData(RegionName.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True),
RegionData(RegionName.volcano_secret_beach, is_ginger_island=True),
RegionData(RegionName.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True),
RegionData(RegionName.volcano_dwarf_shop, is_ginger_island=True),
RegionData(RegionName.volcano_floor_10, is_ginger_island=True),
RegionData(RegionName.island_trader, is_ginger_island=True),
RegionData(RegionName.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True),
RegionData(RegionName.gourmand_frog_cave, is_ginger_island=True),
RegionData(RegionName.colored_crystals_cave, is_ginger_island=True),
RegionData(RegionName.shipwreck, is_ginger_island=True),
RegionData(RegionName.qi_walnut_room, is_ginger_island=True),
RegionData(RegionName.leo_hut, is_ginger_island=True),
RegionData(RegionName.pirate_cove, is_ginger_island=True),
RegionData(RegionName.field_office, is_ginger_island=True),
RegionData(RegionName.dig_site,
[Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano,
Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle],
is_ginger_island=True),
RegionData(RegionName.professor_snail_cave, is_ginger_island=True),
RegionData(RegionName.coop),
RegionData(RegionName.barn),
RegionData(RegionName.shed),
RegionData(RegionName.slime_hutch),
RegionData(RegionName.mines, [LogicEntrance.talk_to_mines_dwarf,
Entrance.dig_to_mines_floor_5]),
RegionData(RegionName.mines_floor_5, [Entrance.dig_to_mines_floor_10]),
RegionData(RegionName.mines_floor_10, [Entrance.dig_to_mines_floor_15]),
RegionData(RegionName.mines_floor_15, [Entrance.dig_to_mines_floor_20]),
RegionData(RegionName.mines_floor_20, [Entrance.dig_to_mines_floor_25]),
RegionData(RegionName.mines_floor_25, [Entrance.dig_to_mines_floor_30]),
RegionData(RegionName.mines_floor_30, [Entrance.dig_to_mines_floor_35]),
RegionData(RegionName.mines_floor_35, [Entrance.dig_to_mines_floor_40]),
RegionData(RegionName.mines_floor_40, [Entrance.dig_to_mines_floor_45]),
RegionData(RegionName.mines_floor_45, [Entrance.dig_to_mines_floor_50]),
RegionData(RegionName.mines_floor_50, [Entrance.dig_to_mines_floor_55]),
RegionData(RegionName.mines_floor_55, [Entrance.dig_to_mines_floor_60]),
RegionData(RegionName.mines_floor_60, [Entrance.dig_to_mines_floor_65]),
RegionData(RegionName.mines_floor_65, [Entrance.dig_to_mines_floor_70]),
RegionData(RegionName.mines_floor_70, [Entrance.dig_to_mines_floor_75]),
RegionData(RegionName.mines_floor_75, [Entrance.dig_to_mines_floor_80]),
RegionData(RegionName.mines_floor_80, [Entrance.dig_to_mines_floor_85]),
RegionData(RegionName.mines_floor_85, [Entrance.dig_to_mines_floor_90]),
RegionData(RegionName.mines_floor_90, [Entrance.dig_to_mines_floor_95]),
RegionData(RegionName.mines_floor_95, [Entrance.dig_to_mines_floor_100]),
RegionData(RegionName.mines_floor_100, [Entrance.dig_to_mines_floor_105]),
RegionData(RegionName.mines_floor_105, [Entrance.dig_to_mines_floor_110]),
RegionData(RegionName.mines_floor_110, [Entrance.dig_to_mines_floor_115]),
RegionData(RegionName.mines_floor_115, [Entrance.dig_to_mines_floor_120]),
RegionData(RegionName.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]),
RegionData(RegionName.dangerous_mines_20, is_ginger_island=True),
RegionData(RegionName.dangerous_mines_60, is_ginger_island=True),
RegionData(RegionName.dangerous_mines_100, is_ginger_island=True),
RegionData(LogicRegion.mines_dwarf_shop),
RegionData(LogicRegion.blacksmith_copper, [LogicEntrance.blacksmith_iron]),
RegionData(LogicRegion.blacksmith_iron, [LogicEntrance.blacksmith_gold]),
RegionData(LogicRegion.blacksmith_gold, [LogicEntrance.blacksmith_iridium]),
RegionData(LogicRegion.blacksmith_iridium),
RegionData(LogicRegion.kitchen),
RegionData(LogicRegion.queen_of_sauce),
RegionData(LogicRegion.fishing),
RegionData(LogicRegion.spring_farming),
RegionData(LogicRegion.summer_farming, [LogicEntrance.grow_summer_fall_crops_in_summer]),
RegionData(LogicRegion.fall_farming, [LogicEntrance.grow_summer_fall_crops_in_fall]),
RegionData(LogicRegion.winter_farming),
RegionData(LogicRegion.summer_or_fall_farming),
RegionData(LogicRegion.indoor_farming),
RegionData(LogicRegion.shipping),
RegionData(LogicRegion.traveling_cart, [LogicEntrance.buy_from_traveling_merchant_sunday,
LogicEntrance.buy_from_traveling_merchant_monday,
LogicEntrance.buy_from_traveling_merchant_tuesday,
LogicEntrance.buy_from_traveling_merchant_wednesday,
LogicEntrance.buy_from_traveling_merchant_thursday,
LogicEntrance.buy_from_traveling_merchant_friday,
LogicEntrance.buy_from_traveling_merchant_saturday]),
RegionData(LogicRegion.traveling_cart_sunday),
RegionData(LogicRegion.traveling_cart_monday),
RegionData(LogicRegion.traveling_cart_tuesday),
RegionData(LogicRegion.traveling_cart_wednesday),
RegionData(LogicRegion.traveling_cart_thursday),
RegionData(LogicRegion.traveling_cart_friday),
RegionData(LogicRegion.traveling_cart_saturday),
RegionData(LogicRegion.raccoon_daddy, [LogicEntrance.buy_from_raccoon]),
RegionData(LogicRegion.raccoon_shop),
RegionData(LogicRegion.egg_festival),
RegionData(LogicRegion.desert_festival),
RegionData(LogicRegion.flower_dance),
RegionData(LogicRegion.luau),
RegionData(LogicRegion.trout_derby),
RegionData(LogicRegion.moonlight_jellies),
RegionData(LogicRegion.fair),
RegionData(LogicRegion.spirit_eve),
RegionData(LogicRegion.festival_of_ice),
RegionData(LogicRegion.night_market),
RegionData(LogicRegion.winter_star),
RegionData(LogicRegion.squidfest),
RegionData(LogicRegion.bookseller_1, [LogicEntrance.buy_year1_books]),
RegionData(LogicRegion.bookseller_2, [LogicEntrance.buy_year3_books]),
RegionData(LogicRegion.bookseller_3),
]
# Exists and where they lead
vanilla_connections = [
ConnectionData(Entrance.to_stardew_valley, RegionName.stardew_valley),
ConnectionData(Entrance.to_farmhouse, RegionName.farm_house),
ConnectionData(Entrance.farmhouse_to_farm, RegionName.farm),
ConnectionData(Entrance.downstairs_to_cellar, RegionName.cellar),
ConnectionData(Entrance.farm_to_backwoods, RegionName.backwoods),
ConnectionData(Entrance.farm_to_bus_stop, RegionName.bus_stop),
ConnectionData(Entrance.farm_to_forest, RegionName.forest),
ConnectionData(Entrance.farm_to_farmcave, RegionName.farm_cave, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.enter_greenhouse, RegionName.greenhouse),
ConnectionData(Entrance.enter_coop, RegionName.coop),
ConnectionData(Entrance.enter_barn, RegionName.barn),
ConnectionData(Entrance.enter_shed, RegionName.shed),
ConnectionData(Entrance.enter_slime_hutch, RegionName.slime_hutch),
ConnectionData(Entrance.use_desert_obelisk, RegionName.desert),
ConnectionData(Entrance.use_island_obelisk, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.use_farm_obelisk, RegionName.farm),
ConnectionData(Entrance.backwoods_to_mountain, RegionName.mountain),
ConnectionData(Entrance.bus_stop_to_town, RegionName.town),
ConnectionData(Entrance.bus_stop_to_tunnel_entrance, RegionName.tunnel_entrance),
ConnectionData(Entrance.tunnel_entrance_to_bus_tunnel, RegionName.bus_tunnel, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.take_bus_to_desert, RegionName.desert),
ConnectionData(Entrance.forest_to_town, RegionName.town),
ConnectionData(Entrance.forest_to_wizard_tower, RegionName.wizard_tower,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.forest_to_marnie_ranch, RegionName.ranch,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.forest_to_leah_cottage, RegionName.leah_house,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_secret_woods, RegionName.secret_woods),
ConnectionData(Entrance.forest_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.forest_to_mastery_cave, RegionName.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES),
ConnectionData(Entrance.town_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_mutant_bug_lair, RegionName.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_railroad, RegionName.railroad),
ConnectionData(Entrance.mountain_to_tent, RegionName.tent,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.mountain_to_leo_treehouse, RegionName.leo_treehouse,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.mountain_to_carpenter_shop, RegionName.carpenter,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.mountain_to_maru_room, RegionName.maru_room,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_sebastian_room, RegionName.sebastian_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_adventurer_guild, RegionName.adventurer_guild,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.adventurer_guild_to_bedroom, RegionName.adventurer_guild_bedroom),
ConnectionData(Entrance.enter_quarry, RegionName.quarry),
ConnectionData(Entrance.enter_quarry_mine_entrance, RegionName.quarry_mine_entrance,
flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_quarry_mine, RegionName.quarry_mine),
ConnectionData(Entrance.mountain_to_town, RegionName.town),
ConnectionData(Entrance.town_to_community_center, RegionName.community_center,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.access_crafts_room, RegionName.crafts_room),
ConnectionData(Entrance.access_pantry, RegionName.pantry),
ConnectionData(Entrance.access_fish_tank, RegionName.fish_tank),
ConnectionData(Entrance.access_boiler_room, RegionName.boiler_room),
ConnectionData(Entrance.access_bulletin_board, RegionName.bulletin_board),
ConnectionData(Entrance.access_vault, RegionName.vault),
ConnectionData(Entrance.town_to_hospital, RegionName.hospital,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_harvey_room, RegionName.harvey_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.town_to_pierre_general_store, RegionName.pierre_store,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_sunroom, RegionName.sunroom, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.town_to_clint_blacksmith, RegionName.blacksmith,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_saloon, RegionName.saloon,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.play_journey_of_the_prairie_king, RegionName.jotpk_world_1),
ConnectionData(Entrance.reach_jotpk_world_2, RegionName.jotpk_world_2),
ConnectionData(Entrance.reach_jotpk_world_3, RegionName.jotpk_world_3),
ConnectionData(Entrance.play_junimo_kart, RegionName.junimo_kart_1),
ConnectionData(Entrance.reach_junimo_kart_2, RegionName.junimo_kart_2),
ConnectionData(Entrance.reach_junimo_kart_3, RegionName.junimo_kart_3),
ConnectionData(Entrance.reach_junimo_kart_4, RegionName.junimo_kart_4),
ConnectionData(Entrance.town_to_sam_house, RegionName.sam_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_haley_house, RegionName.haley_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_mayor_manor, RegionName.mayor_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_alex_house, RegionName.alex_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_trailer, RegionName.trailer,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_museum, RegionName.museum,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_jojamart, RegionName.jojamart,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.purchase_movie_ticket, RegionName.movie_ticket_stand),
ConnectionData(Entrance.enter_abandoned_jojamart, RegionName.abandoned_jojamart),
ConnectionData(Entrance.enter_movie_theater, RegionName.movie_theater),
ConnectionData(Entrance.town_to_beach, RegionName.beach),
ConnectionData(Entrance.enter_elliott_house, RegionName.elliott_house,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.beach_to_willy_fish_shop, RegionName.fish_shop,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.fish_shop_to_boat_tunnel, RegionName.boat_tunnel,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.boat_to_ginger_island, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_tide_pools, RegionName.tide_pools),
ConnectionData(Entrance.mountain_to_the_mines, RegionName.mines,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.dig_to_mines_floor_5, RegionName.mines_floor_5),
ConnectionData(Entrance.dig_to_mines_floor_10, RegionName.mines_floor_10),
ConnectionData(Entrance.dig_to_mines_floor_15, RegionName.mines_floor_15),
ConnectionData(Entrance.dig_to_mines_floor_20, RegionName.mines_floor_20),
ConnectionData(Entrance.dig_to_mines_floor_25, RegionName.mines_floor_25),
ConnectionData(Entrance.dig_to_mines_floor_30, RegionName.mines_floor_30),
ConnectionData(Entrance.dig_to_mines_floor_35, RegionName.mines_floor_35),
ConnectionData(Entrance.dig_to_mines_floor_40, RegionName.mines_floor_40),
ConnectionData(Entrance.dig_to_mines_floor_45, RegionName.mines_floor_45),
ConnectionData(Entrance.dig_to_mines_floor_50, RegionName.mines_floor_50),
ConnectionData(Entrance.dig_to_mines_floor_55, RegionName.mines_floor_55),
ConnectionData(Entrance.dig_to_mines_floor_60, RegionName.mines_floor_60),
ConnectionData(Entrance.dig_to_mines_floor_65, RegionName.mines_floor_65),
ConnectionData(Entrance.dig_to_mines_floor_70, RegionName.mines_floor_70),
ConnectionData(Entrance.dig_to_mines_floor_75, RegionName.mines_floor_75),
ConnectionData(Entrance.dig_to_mines_floor_80, RegionName.mines_floor_80),
ConnectionData(Entrance.dig_to_mines_floor_85, RegionName.mines_floor_85),
ConnectionData(Entrance.dig_to_mines_floor_90, RegionName.mines_floor_90),
ConnectionData(Entrance.dig_to_mines_floor_95, RegionName.mines_floor_95),
ConnectionData(Entrance.dig_to_mines_floor_100, RegionName.mines_floor_100),
ConnectionData(Entrance.dig_to_mines_floor_105, RegionName.mines_floor_105),
ConnectionData(Entrance.dig_to_mines_floor_110, RegionName.mines_floor_110),
ConnectionData(Entrance.dig_to_mines_floor_115, RegionName.mines_floor_115),
ConnectionData(Entrance.dig_to_mines_floor_120, RegionName.mines_floor_120),
ConnectionData(Entrance.dig_to_dangerous_mines_20, RegionName.dangerous_mines_20, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_to_dangerous_mines_60, RegionName.dangerous_mines_60, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_to_dangerous_mines_100, RegionName.dangerous_mines_100, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_skull_cavern_entrance, RegionName.skull_cavern_entrance,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_oasis, RegionName.oasis,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_casino, RegionName.casino, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_skull_cavern, RegionName.skull_cavern),
ConnectionData(Entrance.mine_to_skull_cavern_floor_25, RegionName.skull_cavern_25),
ConnectionData(Entrance.mine_to_skull_cavern_floor_50, RegionName.skull_cavern_50),
ConnectionData(Entrance.mine_to_skull_cavern_floor_75, RegionName.skull_cavern_75),
ConnectionData(Entrance.mine_to_skull_cavern_floor_100, RegionName.skull_cavern_100),
ConnectionData(Entrance.mine_to_skull_cavern_floor_125, RegionName.skull_cavern_125),
ConnectionData(Entrance.mine_to_skull_cavern_floor_150, RegionName.skull_cavern_150),
ConnectionData(Entrance.mine_to_skull_cavern_floor_175, RegionName.skull_cavern_175),
ConnectionData(Entrance.mine_to_skull_cavern_floor_200, RegionName.skull_cavern_200),
ConnectionData(Entrance.enter_dangerous_skull_cavern, RegionName.dangerous_skull_cavern, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.enter_witch_warp_cave, RegionName.witch_warp_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_swamp, RegionName.witch_swamp, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_hut, RegionName.witch_hut, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.witch_warp_to_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_bathhouse_entrance, RegionName.bathhouse_entrance,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_locker_room, RegionName.locker_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_public_bath, RegionName.public_bath, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_south_to_west, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_north, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_east, RegionName.island_east, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_south_to_southeast, RegionName.island_south_east,
flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.use_island_resort, RegionName.island_resort, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_islandfarmhouse, RegionName.island_farmhouse,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_gourmand_cave, RegionName.gourmand_frog_cave,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_crystals_cave, RegionName.colored_crystals_cave,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_shipwreck, RegionName.shipwreck,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_west_to_qi_walnut_room, RegionName.qi_walnut_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_east_to_leo_hut, RegionName.leo_hut,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_east_to_island_shrine, RegionName.island_shrine,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_southeast_to_pirate_cove, RegionName.pirate_cove,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_north_to_field_office, RegionName.field_office,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_north_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.dig_site_to_professor_snail_cave, RegionName.professor_snail_cave,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.island_north_to_volcano, RegionName.volcano,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.volcano_to_secret_beach, RegionName.volcano_secret_beach,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.talk_to_island_trader, RegionName.island_trader, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.climb_to_volcano_5, RegionName.volcano_floor_5, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.talk_to_volcano_dwarf, RegionName.volcano_dwarf_shop, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.climb_to_volcano_10, RegionName.volcano_floor_10, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_docks, RegionName.island_south, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_jungle, RegionName.island_west, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_volcano_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_dig_site, RegionName.dig_site, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_dig_site_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_docks_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(Entrance.parrot_express_jungle_to_volcano, RegionName.island_north, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop),
ConnectionData(LogicEntrance.buy_from_traveling_merchant, LogicRegion.traveling_cart),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_sunday, LogicRegion.traveling_cart_sunday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_monday, LogicRegion.traveling_cart_monday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_tuesday, LogicRegion.traveling_cart_tuesday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_wednesday, LogicRegion.traveling_cart_wednesday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_thursday, LogicRegion.traveling_cart_thursday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_friday, LogicRegion.traveling_cart_friday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_saturday, LogicRegion.traveling_cart_saturday),
ConnectionData(LogicEntrance.complete_raccoon_requests, LogicRegion.raccoon_daddy),
ConnectionData(LogicEntrance.fish_in_waterfall, LogicRegion.forest_waterfall),
ConnectionData(LogicEntrance.buy_from_raccoon, LogicRegion.raccoon_shop),
ConnectionData(LogicEntrance.farmhouse_cooking, LogicRegion.kitchen),
ConnectionData(LogicEntrance.watch_queen_of_sauce, LogicRegion.queen_of_sauce),
ConnectionData(LogicEntrance.grow_spring_crops, LogicRegion.spring_farming),
ConnectionData(LogicEntrance.grow_summer_crops, LogicRegion.summer_farming),
ConnectionData(LogicEntrance.grow_fall_crops, LogicRegion.fall_farming),
ConnectionData(LogicEntrance.grow_winter_crops, LogicRegion.winter_farming),
ConnectionData(LogicEntrance.grow_spring_crops_in_greenhouse, LogicRegion.spring_farming),
ConnectionData(LogicEntrance.grow_summer_crops_in_greenhouse, LogicRegion.summer_farming),
ConnectionData(LogicEntrance.grow_fall_crops_in_greenhouse, LogicRegion.fall_farming),
ConnectionData(LogicEntrance.grow_winter_crops_in_greenhouse, LogicRegion.winter_farming),
ConnectionData(LogicEntrance.grow_indoor_crops_in_greenhouse, LogicRegion.indoor_farming),
ConnectionData(LogicEntrance.grow_spring_crops_on_island, LogicRegion.spring_farming, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(LogicEntrance.grow_summer_crops_on_island, LogicRegion.summer_farming, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(LogicEntrance.grow_fall_crops_on_island, LogicRegion.fall_farming, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(LogicEntrance.grow_winter_crops_on_island, LogicRegion.winter_farming, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(LogicEntrance.grow_indoor_crops_on_island, LogicRegion.indoor_farming, flag=RandomizationFlag.GINGER_ISLAND),
ConnectionData(LogicEntrance.grow_summer_fall_crops_in_summer, LogicRegion.summer_or_fall_farming),
ConnectionData(LogicEntrance.grow_summer_fall_crops_in_fall, LogicRegion.summer_or_fall_farming),
ConnectionData(LogicEntrance.shipping, LogicRegion.shipping),
ConnectionData(LogicEntrance.blacksmith_copper, LogicRegion.blacksmith_copper),
ConnectionData(LogicEntrance.blacksmith_iron, LogicRegion.blacksmith_iron),
ConnectionData(LogicEntrance.blacksmith_gold, LogicRegion.blacksmith_gold),
ConnectionData(LogicEntrance.blacksmith_iridium, LogicRegion.blacksmith_iridium),
ConnectionData(LogicEntrance.fishing, LogicRegion.fishing),
ConnectionData(LogicEntrance.island_cooking, LogicRegion.kitchen),
ConnectionData(LogicEntrance.attend_egg_festival, LogicRegion.egg_festival),
ConnectionData(LogicEntrance.attend_desert_festival, LogicRegion.desert_festival),
ConnectionData(LogicEntrance.attend_flower_dance, LogicRegion.flower_dance),
ConnectionData(LogicEntrance.attend_luau, LogicRegion.luau),
ConnectionData(LogicEntrance.attend_trout_derby, LogicRegion.trout_derby),
ConnectionData(LogicEntrance.attend_moonlight_jellies, LogicRegion.moonlight_jellies),
ConnectionData(LogicEntrance.attend_fair, LogicRegion.fair),
ConnectionData(LogicEntrance.attend_spirit_eve, LogicRegion.spirit_eve),
ConnectionData(LogicEntrance.attend_festival_of_ice, LogicRegion.festival_of_ice),
ConnectionData(LogicEntrance.attend_night_market, LogicRegion.night_market),
ConnectionData(LogicEntrance.attend_winter_star, LogicRegion.winter_star),
ConnectionData(LogicEntrance.attend_squidfest, LogicRegion.squidfest),
ConnectionData(LogicEntrance.buy_experience_books, LogicRegion.bookseller_1),
ConnectionData(LogicEntrance.buy_year1_books, LogicRegion.bookseller_2),
ConnectionData(LogicEntrance.buy_year3_books, LogicRegion.bookseller_3),
]
def create_final_regions(world_options) -> List[RegionData]:
final_regions = []
final_regions.extend(vanilla_regions)
if world_options.mods is None:
return final_regions
for mod in sorted(world_options.mods.value):
if mod not in ModDataList:
continue
for mod_region in ModDataList[mod].regions:
existing_region = next(
(region for region in final_regions if region.name == mod_region.name), None)
if existing_region:
final_regions.remove(existing_region)
if ModificationFlag.MODIFIED in mod_region.flag:
mod_region = modify_vanilla_regions(existing_region, mod_region)
final_regions.append(existing_region.get_merged_with(mod_region.exits))
continue
final_regions.append(mod_region.get_clone())
return final_regions
def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, ConnectionData], Dict[str, RegionData]]:
regions_data: Dict[str, RegionData] = {region.name: region for region in create_final_regions(world_options)}
connections = {connection.name: connection for connection in vanilla_connections}
connections = modify_connections_for_mods(connections, sorted(world_options.mods.value))
include_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_false
return remove_ginger_island_regions_and_connections(regions_data, connections, include_island)
def remove_ginger_island_regions_and_connections(regions_by_name: Dict[str, RegionData], connections: Dict[str, ConnectionData], include_island: bool):
if include_island:
return connections, regions_by_name
removed_connections = set()
for connection_name in tuple(connections):
connection = connections[connection_name]
if connection.flag & RandomizationFlag.GINGER_ISLAND:
connections.pop(connection_name)
removed_connections.add(connection_name)
for region_name in tuple(regions_by_name):
region = regions_by_name[region_name]
if region.is_ginger_island:
regions_by_name.pop(region_name)
else:
regions_by_name[region_name] = region.get_without_exits(removed_connections)
return connections, regions_by_name
def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods: Iterable) -> Dict[str, ConnectionData]:
for mod in mods:
if mod not in ModDataList:
continue
if mod in vanilla_connections_to_remove_by_mod:
for connection_data in vanilla_connections_to_remove_by_mod[mod]:
connections.pop(connection_data.name)
connections.update({connection.name: connection for connection in ModDataList[mod].connections})
return connections
def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionData) -> RegionData:
updated_region = existing_region
region_exits = updated_region.exits
modified_exits = modified_region.exits
for exits in modified_exits:
region_exits.remove(exits)
return updated_region
def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions, content: StardewContent) \
-> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]:
entrances_data, regions_data = create_final_connections_and_regions(world_options)
regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data}
entrances_by_name: Dict[str: Entrance] = {
entrance.name: entrance
for region in regions_by_name.values()
for entrance in region.exits
if entrance.name in entrances_data
}
connections, randomized_data = randomize_connections(random, world_options, content, regions_data, entrances_data)
for connection in connections:
if connection.name in entrances_by_name:
entrances_by_name[connection.name].connect(regions_by_name[connection.destination])
return regions_by_name, entrances_by_name, randomized_data
def randomize_connections(random: Random, world_options: StardewValleyOptions, content: StardewContent, regions_by_name: Dict[str, RegionData],
connections_by_name: Dict[str, ConnectionData]) -> Tuple[List[ConnectionData], Dict[str, str]]:
connections_to_randomize: List[ConnectionData] = []
if world_options.entrance_randomization == EntranceRandomization.option_pelican_town:
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
RandomizationFlag.PELICAN_TOWN in connections_by_name[connection].flag]
elif world_options.entrance_randomization == EntranceRandomization.option_non_progression:
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
RandomizationFlag.NON_PROGRESSION in connections_by_name[connection].flag]
elif world_options.entrance_randomization == EntranceRandomization.option_buildings or world_options.entrance_randomization == EntranceRandomization.option_buildings_without_house:
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
RandomizationFlag.BUILDINGS in connections_by_name[connection].flag]
elif world_options.entrance_randomization == EntranceRandomization.option_chaos:
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
RandomizationFlag.BUILDINGS in connections_by_name[connection].flag]
connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content)
# On Chaos, we just add the connections to randomize, unshuffled, and the client does it every day
randomized_data_for_mod = {}
for connection in connections_to_randomize:
randomized_data_for_mod[connection.name] = connection.name
randomized_data_for_mod[connection.reverse] = connection.reverse
return list(connections_by_name.values()), randomized_data_for_mod
connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content)
random.shuffle(connections_to_randomize)
destination_pool = list(connections_to_randomize)
random.shuffle(destination_pool)
randomized_connections = randomize_chosen_connections(connections_to_randomize, destination_pool)
add_non_randomized_connections(list(connections_by_name.values()), connections_to_randomize, randomized_connections)
swap_connections_until_valid(regions_by_name, connections_by_name, randomized_connections, connections_to_randomize, random)
randomized_connections_for_generation = create_connections_for_generation(randomized_connections)
randomized_data_for_mod = create_data_for_mod(randomized_connections, connections_to_randomize)
return randomized_connections_for_generation, randomized_data_for_mod
def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], content: StardewContent) -> List[ConnectionData]:
# FIXME remove when regions are handled in content packs
if content_packs.ginger_island_content_pack.name not in content.registered_packs:
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag]
if not content.features.skill_progression.are_masteries_shuffled:
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag]
return connections_to_randomize
def randomize_chosen_connections(connections_to_randomize: List[ConnectionData],
destination_pool: List[ConnectionData]) -> Dict[ConnectionData, ConnectionData]:
randomized_connections = {}
for connection in connections_to_randomize:
destination = destination_pool.pop()
randomized_connections[connection] = destination
return randomized_connections
def create_connections_for_generation(randomized_connections: Dict[ConnectionData, ConnectionData]) -> List[ConnectionData]:
connections = []
for connection in randomized_connections:
destination = randomized_connections[connection]
connections.append(ConnectionData(connection.name, destination.destination, destination.reverse))
return connections
def create_data_for_mod(randomized_connections: Dict[ConnectionData, ConnectionData],
connections_to_randomize: List[ConnectionData]) -> Dict[str, str]:
randomized_data_for_mod = {}
for connection in randomized_connections:
if connection not in connections_to_randomize:
continue
destination = randomized_connections[connection]
add_to_mod_data(connection, destination, randomized_data_for_mod)
return randomized_data_for_mod
def add_to_mod_data(connection: ConnectionData, destination: ConnectionData, randomized_data_for_mod: Dict[str, str]):
randomized_data_for_mod[connection.name] = destination.name
randomized_data_for_mod[destination.reverse] = connection.reverse
def add_non_randomized_connections(all_connections: List[ConnectionData], connections_to_randomize: List[ConnectionData],
randomized_connections: Dict[ConnectionData, ConnectionData]):
for connection in all_connections:
if connection in connections_to_randomize:
continue
randomized_connections[connection] = connection
def swap_connections_until_valid(regions_by_name, connections_by_name: Dict[str, ConnectionData], randomized_connections: Dict[ConnectionData, ConnectionData],
connections_to_randomize: List[ConnectionData], random: Random):
while True:
reachable_regions, unreachable_regions = find_reachable_regions(regions_by_name, connections_by_name, randomized_connections)
if not unreachable_regions:
return randomized_connections
swap_one_random_connection(regions_by_name, connections_by_name, randomized_connections, reachable_regions,
unreachable_regions, connections_to_randomize, random)
def region_should_be_reachable(region_name: str, connections_in_slot: Iterable[ConnectionData]) -> bool:
if region_name == RegionName.menu:
return True
for connection in connections_in_slot:
if region_name == connection.destination:
return True
return False
def find_reachable_regions(regions_by_name, connections_by_name,
randomized_connections: Dict[ConnectionData, ConnectionData]):
reachable_regions = {RegionName.menu}
unreachable_regions = {region for region in regions_by_name.keys()}
# unreachable_regions = {region for region in regions_by_name.keys() if region_should_be_reachable(region, connections_by_name.values())}
unreachable_regions.remove(RegionName.menu)
exits_to_explore = list(regions_by_name[RegionName.menu].exits)
while exits_to_explore:
exit_name = exits_to_explore.pop()
# if exit_name not in connections_by_name:
# continue
exit_connection = connections_by_name[exit_name]
replaced_connection = randomized_connections[exit_connection]
target_region_name = replaced_connection.destination
if target_region_name in reachable_regions:
continue
target_region = regions_by_name[target_region_name]
reachable_regions.add(target_region_name)
unreachable_regions.remove(target_region_name)
exits_to_explore.extend(target_region.exits)
return reachable_regions, unreachable_regions
def swap_one_random_connection(regions_by_name, connections_by_name, randomized_connections: Dict[ConnectionData, ConnectionData],
reachable_regions: Set[str], unreachable_regions: Set[str],
connections_to_randomize: List[ConnectionData], random: Random):
randomized_connections_already_shuffled = {connection: randomized_connections[connection]
for connection in randomized_connections
if connection != randomized_connections[connection]}
unreachable_regions_names_leading_somewhere = [region for region in sorted(unreachable_regions) if len(regions_by_name[region].exits) > 0]
unreachable_regions_leading_somewhere = [regions_by_name[region_name] for region_name in unreachable_regions_names_leading_somewhere]
unreachable_regions_exits_names = [exit_name for region in unreachable_regions_leading_somewhere for exit_name in region.exits]
unreachable_connections = [connections_by_name[exit_name] for exit_name in unreachable_regions_exits_names]
unreachable_connections_that_can_be_randomized = [connection for connection in unreachable_connections if connection in connections_to_randomize]
chosen_unreachable_entrance = random.choice(unreachable_connections_that_can_be_randomized)
chosen_reachable_entrance = None
while chosen_reachable_entrance is None or chosen_reachable_entrance not in randomized_connections_already_shuffled:
chosen_reachable_region_name = random.choice(sorted(reachable_regions))
chosen_reachable_region = regions_by_name[chosen_reachable_region_name]
if not any(chosen_reachable_region.exits):
continue
chosen_reachable_entrance_name = random.choice(chosen_reachable_region.exits)
chosen_reachable_entrance = connections_by_name[chosen_reachable_entrance_name]
swap_two_connections(chosen_reachable_entrance, chosen_unreachable_entrance, randomized_connections)
def swap_two_connections(entrance_1, entrance_2, randomized_connections):
reachable_destination = randomized_connections[entrance_1]
unreachable_destination = randomized_connections[entrance_2]
randomized_connections[entrance_1] = unreachable_destination
randomized_connections[entrance_2] = reachable_destination

View File

@@ -0,0 +1,2 @@
from .entrance_rando import prepare_mod_data
from .regions import create_regions, RegionFactory

View File

@@ -0,0 +1,73 @@
from BaseClasses import Region
from entrance_rando import ERPlacementState
from .model import ConnectionData, RandomizationFlag, reverse_connection_name, RegionData
from ..content import StardewContent
from ..options import EntranceRandomization
def create_player_randomization_flag(entrance_randomization_choice: EntranceRandomization, content: StardewContent):
"""Return the flag that a connection is expected to have to be randomized. Only the bit corresponding to the player randomization choice will be enabled.
Other bits for content exclusion might also be enabled, tho the preferred solution to exclude content should be to not create those regions at alls, when possible.
"""
flag = RandomizationFlag.NOT_RANDOMIZED
if entrance_randomization_choice.value == EntranceRandomization.option_disabled:
return flag
if entrance_randomization_choice == EntranceRandomization.option_pelican_town:
flag |= RandomizationFlag.BIT_PELICAN_TOWN
elif entrance_randomization_choice == EntranceRandomization.option_non_progression:
flag |= RandomizationFlag.BIT_NON_PROGRESSION
elif entrance_randomization_choice in (
EntranceRandomization.option_buildings,
EntranceRandomization.option_buildings_without_house,
EntranceRandomization.option_chaos
):
flag |= RandomizationFlag.BIT_BUILDINGS
if not content.features.skill_progression.are_masteries_shuffled:
flag |= RandomizationFlag.EXCLUDE_MASTERIES
return flag
def connect_regions(region_data_by_name: dict[str, RegionData], connection_data_by_name: dict[str, ConnectionData], regions_by_name: dict[str, Region],
player_randomization_flag: RandomizationFlag) -> None:
for region_name, region_data in region_data_by_name.items():
origin_region = regions_by_name[region_name]
for exit_name in region_data.exits:
connection_data = connection_data_by_name[exit_name]
destination_region = regions_by_name[connection_data.destination]
if connection_data.is_eligible_for_randomization(player_randomization_flag):
create_entrance_rando_target(origin_region, destination_region, connection_data)
else:
origin_region.connect(destination_region, connection_data.name)
def create_entrance_rando_target(origin: Region, destination: Region, connection_data: ConnectionData) -> None:
"""We need our own function to create the GER targets, because the Stardew Mod have very specific expectations for the name of the entrances.
We need to know exactly which entrances to swap in both directions."""
origin.create_exit(connection_data.name)
destination.create_er_target(connection_data.reverse)
def prepare_mod_data(placements: ERPlacementState) -> dict[str, str]:
"""Take the placements from GER and prepare the data for the mod.
The mod require a dictionary detailing which connections need to be swapped. It acts as if the connections are decoupled, so both directions are required.
For instance, GER will provide placements like (Town to Community Center, Hospital to Town), meaning that the door of the Community Center will instead lead
to the Hospital, and that the exit of the Hospital will lead to the Town by the Community Center door. The StardewAP mod need to know both swaps, being the
original destination of the "Town to Community Center" connection is to be replaced by the original destination of "Town to Hospital", and the original
destination of "Hospital to Town" is to be replaced by the original destination of "Community Center to Town".
"""
swapped_connections = {}
for entrance, exit_ in placements.pairings:
swapped_connections[entrance] = reverse_connection_name(exit_)
swapped_connections[exit_] = reverse_connection_name(entrance)
return swapped_connections

View File

@@ -0,0 +1,94 @@
from __future__ import annotations
from collections.abc import Container
from dataclasses import dataclass, field
from enum import IntFlag
connector_keyword = " to "
def reverse_connection_name(name: str) -> str | None:
try:
origin, destination = name.split(connector_keyword)
except ValueError:
return None
return f"{destination}{connector_keyword}{origin}"
class MergeFlag(IntFlag):
ADD_EXITS = 0
REMOVE_EXITS = 1
class RandomizationFlag(IntFlag):
NOT_RANDOMIZED = 0
# Randomization options
# The first 4 bits are used to mark if an entrance is eligible for randomization according to the entrance randomization options.
BIT_PELICAN_TOWN = 1 # 0b0001
BIT_NON_PROGRESSION = 1 << 1 # 0b0010
BIT_BUILDINGS = 1 << 2 # 0b0100
BIT_EVERYTHING = 1 << 3 # 0b1000
# Content flag for entrances exclusions
# The next 2 bits are used to mark if an entrance is to be excluded from randomization according to the content options.
# Those bits must be removed from an entrance flags when then entrance must be excluded.
__UNUSED = 1 << 4 # 0b010000
EXCLUDE_MASTERIES = 1 << 5 # 0b100000
# Entrance groups
# The last bit is used to add additional qualifiers on entrances to group them
# Those bits should be added when an entrance need additional qualifiers.
LEAD_TO_OPEN_AREA = 1 << 6
# Tags to apply on connections
EVERYTHING = EXCLUDE_MASTERIES | BIT_EVERYTHING
BUILDINGS = EVERYTHING | BIT_BUILDINGS
NON_PROGRESSION = BUILDINGS | BIT_NON_PROGRESSION
PELICAN_TOWN = NON_PROGRESSION | BIT_PELICAN_TOWN
@dataclass(frozen=True)
class RegionData:
name: str
exits: tuple[str, ...] = field(default_factory=tuple)
flag: MergeFlag = MergeFlag.ADD_EXITS
def __post_init__(self):
assert not isinstance(self.exits, str), "Exits must be a tuple of strings, you probably forgot a trailing comma."
def merge_with(self, other: RegionData) -> RegionData:
assert self.name == other.name, "Regions must have the same name to be merged"
if other.flag == MergeFlag.REMOVE_EXITS:
return self.get_without_exits(other.exits)
merged_exits = self.exits + other.exits
assert len(merged_exits) == len(set(merged_exits)), "Two regions getting merged have duplicated exists..."
return RegionData(self.name, merged_exits)
def get_without_exits(self, exits_to_remove: Container[str]) -> RegionData:
exits = tuple(exit_ for exit_ in self.exits if exit_ not in exits_to_remove)
return RegionData(self.name, exits)
@dataclass(frozen=True)
class ConnectionData:
name: str
destination: str
flag: RandomizationFlag = RandomizationFlag.NOT_RANDOMIZED
@property
def reverse(self) -> str | None:
return reverse_connection_name(self.name)
def is_eligible_for_randomization(self, chosen_randomization_flag: RandomizationFlag) -> bool:
return chosen_randomization_flag and chosen_randomization_flag in self.flag
@dataclass(frozen=True)
class ModRegionsData:
mod_name: str
regions: list[RegionData]
connections: list[ConnectionData]

View File

@@ -0,0 +1,46 @@
from collections.abc import Iterable
from .model import ConnectionData, RegionData, ModRegionsData
from ..mods.region_data import region_data_by_content_pack, vanilla_connections_to_remove_by_content_pack
def modify_regions_for_mods(current_regions_by_name: dict[str, RegionData], active_content_packs: Iterable[str]) -> None:
for content_pack in active_content_packs:
try:
region_data = region_data_by_content_pack[content_pack]
except KeyError:
continue
merge_mod_regions(current_regions_by_name, region_data)
def merge_mod_regions(current_regions_by_name: dict[str, RegionData], mod_region_data: ModRegionsData) -> None:
for new_region in mod_region_data.regions:
region_name = new_region.name
try:
current_region = current_regions_by_name[region_name]
except KeyError:
current_regions_by_name[region_name] = new_region
continue
current_regions_by_name[region_name] = current_region.merge_with(new_region)
def modify_connections_for_mods(connections: dict[str, ConnectionData], active_mods: Iterable[str]) -> None:
for active_mod in active_mods:
try:
region_data = region_data_by_content_pack[active_mod]
except KeyError:
continue
try:
vanilla_connections_to_remove = vanilla_connections_to_remove_by_content_pack[active_mod]
for connection_name in vanilla_connections_to_remove:
connections.pop(connection_name)
except KeyError:
pass
connections.update({
connection.name: connection
for connection in region_data.connections
})

View File

@@ -0,0 +1,61 @@
from typing import Protocol
from BaseClasses import Region
from . import vanilla_data, mods
from .entrance_rando import create_player_randomization_flag, connect_regions
from .model import ConnectionData, RegionData
from ..content import StardewContent
from ..content.vanilla.ginger_island import ginger_island_content_pack
from ..options import StardewValleyOptions
class RegionFactory(Protocol):
def __call__(self, name: str) -> Region:
raise NotImplementedError
def create_regions(region_factory: RegionFactory, world_options: StardewValleyOptions, content: StardewContent) -> dict[str, Region]:
connection_data_by_name, region_data_by_name = create_connections_and_regions(content.registered_packs)
regions_by_name: dict[str: Region] = {
region_name: region_factory(region_name)
for region_name in region_data_by_name
}
randomization_flag = create_player_randomization_flag(world_options.entrance_randomization, content)
connect_regions(region_data_by_name, connection_data_by_name, regions_by_name, randomization_flag)
return regions_by_name
def create_connections_and_regions(active_content_packs: set[str]) -> tuple[dict[str, ConnectionData], dict[str, RegionData]]:
regions_by_name = create_all_regions(active_content_packs)
connections_by_name = create_all_connections(active_content_packs)
return connections_by_name, regions_by_name
def create_all_regions(active_content_packs: set[str]) -> dict[str, RegionData]:
current_regions_by_name = create_vanilla_regions(active_content_packs)
mods.modify_regions_for_mods(current_regions_by_name, sorted(active_content_packs))
return current_regions_by_name
def create_vanilla_regions(active_content_packs: set[str]) -> dict[str, RegionData]:
if ginger_island_content_pack.name in active_content_packs:
return {**vanilla_data.regions_with_ginger_island_by_name}
else:
return {**vanilla_data.regions_without_ginger_island_by_name}
def create_all_connections(active_content_packs: set[str]) -> dict[str, ConnectionData]:
connections = create_vanilla_connections(active_content_packs)
mods.modify_connections_for_mods(connections, sorted(active_content_packs))
return connections
def create_vanilla_connections(active_content_packs: set[str]) -> dict[str, ConnectionData]:
if ginger_island_content_pack.name in active_content_packs:
return {**vanilla_data.connections_with_ginger_island_by_name}
else:
return {**vanilla_data.connections_without_ginger_island_by_name}

View File

@@ -0,0 +1,522 @@
from collections.abc import Mapping
from types import MappingProxyType
from .model import ConnectionData, RandomizationFlag, RegionData
from ..strings.entrance_names import LogicEntrance, Entrance
from ..strings.region_names import LogicRegion, Region as RegionName
vanilla_regions: tuple[RegionData, ...] = (
RegionData(RegionName.menu, (Entrance.to_stardew_valley,)),
RegionData(RegionName.stardew_valley, (Entrance.to_farmhouse,)),
RegionData(RegionName.farm_house,
(Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce)),
RegionData(RegionName.cellar),
RegionData(RegionName.farm,
(Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse,
Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops,
LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping,
LogicEntrance.fishing,)),
RegionData(RegionName.backwoods, (Entrance.backwoods_to_mountain,)),
RegionData(RegionName.bus_stop,
(Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance)),
RegionData(RegionName.forest,
(Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch,
Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant,
LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby,
LogicEntrance.attend_festival_of_ice)),
RegionData(LogicRegion.forest_waterfall),
RegionData(RegionName.farm_cave),
RegionData(RegionName.greenhouse,
(LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse,
LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse)),
RegionData(RegionName.mountain,
(Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop,
Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild,
Entrance.mountain_to_town, Entrance.mountain_to_maru_room)),
RegionData(RegionName.maru_room),
RegionData(RegionName.tunnel_entrance, (Entrance.tunnel_entrance_to_bus_tunnel,)),
RegionData(RegionName.bus_tunnel),
RegionData(RegionName.town,
(Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store,
Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house,
Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart,
Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair,
LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star)),
RegionData(RegionName.beach,
(Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.attend_luau,
LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest)),
RegionData(RegionName.railroad, (Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave)),
RegionData(RegionName.ranch),
RegionData(RegionName.leah_house),
RegionData(RegionName.mastery_cave),
RegionData(RegionName.sewer, (Entrance.enter_mutant_bug_lair,)),
RegionData(RegionName.mutant_bug_lair),
RegionData(RegionName.wizard_tower, (Entrance.enter_wizard_basement, Entrance.use_desert_obelisk)),
RegionData(RegionName.wizard_basement),
RegionData(RegionName.tent),
RegionData(RegionName.carpenter, (Entrance.enter_sebastian_room,)),
RegionData(RegionName.sebastian_room),
RegionData(RegionName.adventurer_guild, (Entrance.adventurer_guild_to_bedroom,)),
RegionData(RegionName.adventurer_guild_bedroom),
RegionData(RegionName.community_center,
(Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank,
Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault)),
RegionData(RegionName.crafts_room),
RegionData(RegionName.pantry),
RegionData(RegionName.fish_tank),
RegionData(RegionName.boiler_room),
RegionData(RegionName.bulletin_board),
RegionData(RegionName.vault),
RegionData(RegionName.hospital, (Entrance.enter_harvey_room,)),
RegionData(RegionName.harvey_room),
RegionData(RegionName.pierre_store, (Entrance.enter_sunroom,)),
RegionData(RegionName.sunroom),
RegionData(RegionName.saloon, (Entrance.play_journey_of_the_prairie_king, Entrance.play_junimo_kart)),
RegionData(RegionName.jotpk_world_1, (Entrance.reach_jotpk_world_2,)),
RegionData(RegionName.jotpk_world_2, (Entrance.reach_jotpk_world_3,)),
RegionData(RegionName.jotpk_world_3),
RegionData(RegionName.junimo_kart_1, (Entrance.reach_junimo_kart_2,)),
RegionData(RegionName.junimo_kart_2, (Entrance.reach_junimo_kart_3,)),
RegionData(RegionName.junimo_kart_3, (Entrance.reach_junimo_kart_4,)),
RegionData(RegionName.junimo_kart_4),
RegionData(RegionName.alex_house),
RegionData(RegionName.trailer),
RegionData(RegionName.mayor_house),
RegionData(RegionName.sam_house),
RegionData(RegionName.haley_house),
RegionData(RegionName.blacksmith, (LogicEntrance.blacksmith_copper,)),
RegionData(RegionName.museum),
RegionData(RegionName.jojamart, (Entrance.enter_abandoned_jojamart,)),
RegionData(RegionName.abandoned_jojamart, (Entrance.enter_movie_theater,)),
RegionData(RegionName.movie_ticket_stand),
RegionData(RegionName.movie_theater),
RegionData(RegionName.fish_shop),
RegionData(RegionName.elliott_house),
RegionData(RegionName.tide_pools),
RegionData(RegionName.bathhouse_entrance, (Entrance.enter_locker_room,)),
RegionData(RegionName.locker_room, (Entrance.enter_public_bath,)),
RegionData(RegionName.public_bath),
RegionData(RegionName.witch_warp_cave, (Entrance.enter_witch_swamp,)),
RegionData(RegionName.witch_swamp, (Entrance.enter_witch_hut,)),
RegionData(RegionName.witch_hut, (Entrance.witch_warp_to_wizard_basement,)),
RegionData(RegionName.quarry, (Entrance.enter_quarry_mine_entrance,)),
RegionData(RegionName.quarry_mine_entrance, (Entrance.enter_quarry_mine,)),
RegionData(RegionName.quarry_mine),
RegionData(RegionName.secret_woods),
RegionData(RegionName.desert, (Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival)),
RegionData(RegionName.oasis, (Entrance.enter_casino,)),
RegionData(RegionName.casino),
RegionData(RegionName.skull_cavern_entrance, (Entrance.enter_skull_cavern,)),
RegionData(RegionName.skull_cavern, (Entrance.mine_to_skull_cavern_floor_25,)),
RegionData(RegionName.skull_cavern_25, (Entrance.mine_to_skull_cavern_floor_50,)),
RegionData(RegionName.skull_cavern_50, (Entrance.mine_to_skull_cavern_floor_75,)),
RegionData(RegionName.skull_cavern_75, (Entrance.mine_to_skull_cavern_floor_100,)),
RegionData(RegionName.skull_cavern_100, (Entrance.mine_to_skull_cavern_floor_125,)),
RegionData(RegionName.skull_cavern_125, (Entrance.mine_to_skull_cavern_floor_150,)),
RegionData(RegionName.skull_cavern_150, (Entrance.mine_to_skull_cavern_floor_175,)),
RegionData(RegionName.skull_cavern_175, (Entrance.mine_to_skull_cavern_floor_200,)),
RegionData(RegionName.skull_cavern_200),
RegionData(RegionName.coop),
RegionData(RegionName.barn),
RegionData(RegionName.shed),
RegionData(RegionName.slime_hutch),
RegionData(RegionName.mines, (LogicEntrance.talk_to_mines_dwarf, Entrance.dig_to_mines_floor_5)),
RegionData(RegionName.mines_floor_5, (Entrance.dig_to_mines_floor_10,)),
RegionData(RegionName.mines_floor_10, (Entrance.dig_to_mines_floor_15,)),
RegionData(RegionName.mines_floor_15, (Entrance.dig_to_mines_floor_20,)),
RegionData(RegionName.mines_floor_20, (Entrance.dig_to_mines_floor_25,)),
RegionData(RegionName.mines_floor_25, (Entrance.dig_to_mines_floor_30,)),
RegionData(RegionName.mines_floor_30, (Entrance.dig_to_mines_floor_35,)),
RegionData(RegionName.mines_floor_35, (Entrance.dig_to_mines_floor_40,)),
RegionData(RegionName.mines_floor_40, (Entrance.dig_to_mines_floor_45,)),
RegionData(RegionName.mines_floor_45, (Entrance.dig_to_mines_floor_50,)),
RegionData(RegionName.mines_floor_50, (Entrance.dig_to_mines_floor_55,)),
RegionData(RegionName.mines_floor_55, (Entrance.dig_to_mines_floor_60,)),
RegionData(RegionName.mines_floor_60, (Entrance.dig_to_mines_floor_65,)),
RegionData(RegionName.mines_floor_65, (Entrance.dig_to_mines_floor_70,)),
RegionData(RegionName.mines_floor_70, (Entrance.dig_to_mines_floor_75,)),
RegionData(RegionName.mines_floor_75, (Entrance.dig_to_mines_floor_80,)),
RegionData(RegionName.mines_floor_80, (Entrance.dig_to_mines_floor_85,)),
RegionData(RegionName.mines_floor_85, (Entrance.dig_to_mines_floor_90,)),
RegionData(RegionName.mines_floor_90, (Entrance.dig_to_mines_floor_95,)),
RegionData(RegionName.mines_floor_95, (Entrance.dig_to_mines_floor_100,)),
RegionData(RegionName.mines_floor_100, (Entrance.dig_to_mines_floor_105,)),
RegionData(RegionName.mines_floor_105, (Entrance.dig_to_mines_floor_110,)),
RegionData(RegionName.mines_floor_110, (Entrance.dig_to_mines_floor_115,)),
RegionData(RegionName.mines_floor_115, (Entrance.dig_to_mines_floor_120,)),
RegionData(RegionName.mines_floor_120),
RegionData(LogicRegion.mines_dwarf_shop),
RegionData(LogicRegion.blacksmith_copper, (LogicEntrance.blacksmith_iron,)),
RegionData(LogicRegion.blacksmith_iron, (LogicEntrance.blacksmith_gold,)),
RegionData(LogicRegion.blacksmith_gold, (LogicEntrance.blacksmith_iridium,)),
RegionData(LogicRegion.blacksmith_iridium),
RegionData(LogicRegion.kitchen),
RegionData(LogicRegion.queen_of_sauce),
RegionData(LogicRegion.fishing),
RegionData(LogicRegion.spring_farming),
RegionData(LogicRegion.summer_farming, (LogicEntrance.grow_summer_fall_crops_in_summer,)),
RegionData(LogicRegion.fall_farming, (LogicEntrance.grow_summer_fall_crops_in_fall,)),
RegionData(LogicRegion.winter_farming),
RegionData(LogicRegion.summer_or_fall_farming),
RegionData(LogicRegion.indoor_farming),
RegionData(LogicRegion.shipping),
RegionData(LogicRegion.traveling_cart, (LogicEntrance.buy_from_traveling_merchant_sunday,
LogicEntrance.buy_from_traveling_merchant_monday,
LogicEntrance.buy_from_traveling_merchant_tuesday,
LogicEntrance.buy_from_traveling_merchant_wednesday,
LogicEntrance.buy_from_traveling_merchant_thursday,
LogicEntrance.buy_from_traveling_merchant_friday,
LogicEntrance.buy_from_traveling_merchant_saturday)),
RegionData(LogicRegion.traveling_cart_sunday),
RegionData(LogicRegion.traveling_cart_monday),
RegionData(LogicRegion.traveling_cart_tuesday),
RegionData(LogicRegion.traveling_cart_wednesday),
RegionData(LogicRegion.traveling_cart_thursday),
RegionData(LogicRegion.traveling_cart_friday),
RegionData(LogicRegion.traveling_cart_saturday),
RegionData(LogicRegion.raccoon_daddy, (LogicEntrance.buy_from_raccoon,)),
RegionData(LogicRegion.raccoon_shop),
RegionData(LogicRegion.egg_festival),
RegionData(LogicRegion.desert_festival),
RegionData(LogicRegion.flower_dance),
RegionData(LogicRegion.luau),
RegionData(LogicRegion.trout_derby),
RegionData(LogicRegion.moonlight_jellies),
RegionData(LogicRegion.fair),
RegionData(LogicRegion.spirit_eve),
RegionData(LogicRegion.festival_of_ice),
RegionData(LogicRegion.night_market),
RegionData(LogicRegion.winter_star),
RegionData(LogicRegion.squidfest),
RegionData(LogicRegion.bookseller_1, (LogicEntrance.buy_year1_books,)),
RegionData(LogicRegion.bookseller_2, (LogicEntrance.buy_year3_books,)),
RegionData(LogicRegion.bookseller_3),
)
ginger_island_regions = (
# This overrides the regions from vanilla... When regions are moved to content packs, overriding existing entrances should no longer be necessary.
RegionData(RegionName.mountain,
(Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop,
Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild,
Entrance.mountain_to_town, Entrance.mountain_to_maru_room, Entrance.mountain_to_leo_treehouse)),
RegionData(RegionName.wizard_tower, (Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk,)),
RegionData(RegionName.fish_shop, (Entrance.fish_shop_to_boat_tunnel,)),
RegionData(RegionName.mines_floor_120, (Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100)),
RegionData(RegionName.skull_cavern_200, (Entrance.enter_dangerous_skull_cavern,)),
RegionData(RegionName.leo_treehouse),
RegionData(RegionName.boat_tunnel, (Entrance.boat_to_ginger_island,)),
RegionData(RegionName.dangerous_skull_cavern),
RegionData(RegionName.island_south,
(Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast,
Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site,
Entrance.parrot_express_docks_to_jungle), ),
RegionData(RegionName.island_resort),
RegionData(RegionName.island_west,
(Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave,
Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks,
Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island,
LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island,
LogicEntrance.grow_indoor_crops_on_island), ),
RegionData(RegionName.island_east, (Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine)),
RegionData(RegionName.island_shrine),
RegionData(RegionName.island_south_east, (Entrance.island_southeast_to_pirate_cove,)),
RegionData(RegionName.island_north,
(Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano,
Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks), ),
RegionData(RegionName.volcano, (Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach)),
RegionData(RegionName.volcano_secret_beach),
RegionData(RegionName.volcano_floor_5, (Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10)),
RegionData(RegionName.volcano_dwarf_shop),
RegionData(RegionName.volcano_floor_10),
RegionData(RegionName.island_trader),
RegionData(RegionName.island_farmhouse, (LogicEntrance.island_cooking,)),
RegionData(RegionName.gourmand_frog_cave),
RegionData(RegionName.colored_crystals_cave),
RegionData(RegionName.shipwreck),
RegionData(RegionName.qi_walnut_room),
RegionData(RegionName.leo_hut),
RegionData(RegionName.pirate_cove),
RegionData(RegionName.field_office),
RegionData(RegionName.dig_site,
(Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano,
Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle), ),
RegionData(RegionName.professor_snail_cave),
RegionData(RegionName.dangerous_mines_20),
RegionData(RegionName.dangerous_mines_60),
RegionData(RegionName.dangerous_mines_100),
)
# Exists and where they lead
vanilla_connections: tuple[ConnectionData, ...] = (
ConnectionData(Entrance.to_stardew_valley, RegionName.stardew_valley),
ConnectionData(Entrance.to_farmhouse, RegionName.farm_house),
ConnectionData(Entrance.farmhouse_to_farm, RegionName.farm),
ConnectionData(Entrance.downstairs_to_cellar, RegionName.cellar),
ConnectionData(Entrance.farm_to_backwoods, RegionName.backwoods),
ConnectionData(Entrance.farm_to_bus_stop, RegionName.bus_stop),
ConnectionData(Entrance.farm_to_forest, RegionName.forest),
ConnectionData(Entrance.farm_to_farmcave, RegionName.farm_cave, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.enter_greenhouse, RegionName.greenhouse),
ConnectionData(Entrance.enter_coop, RegionName.coop),
ConnectionData(Entrance.enter_barn, RegionName.barn),
ConnectionData(Entrance.enter_shed, RegionName.shed),
ConnectionData(Entrance.enter_slime_hutch, RegionName.slime_hutch),
ConnectionData(Entrance.use_desert_obelisk, RegionName.desert),
ConnectionData(Entrance.backwoods_to_mountain, RegionName.mountain),
ConnectionData(Entrance.bus_stop_to_town, RegionName.town),
ConnectionData(Entrance.bus_stop_to_tunnel_entrance, RegionName.tunnel_entrance),
ConnectionData(Entrance.tunnel_entrance_to_bus_tunnel, RegionName.bus_tunnel, flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(Entrance.take_bus_to_desert, RegionName.desert),
ConnectionData(Entrance.forest_to_town, RegionName.town),
ConnectionData(Entrance.forest_to_wizard_tower, RegionName.wizard_tower,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.forest_to_marnie_ranch, RegionName.ranch,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.forest_to_leah_cottage, RegionName.leah_house,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_secret_woods, RegionName.secret_woods),
ConnectionData(Entrance.forest_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
# We remove the bit for masteries, because the mastery cave is to be excluded from the randomization if masteries are not shuffled.
ConnectionData(Entrance.forest_to_mastery_cave, RegionName.mastery_cave, flag=RandomizationFlag.BUILDINGS ^ RandomizationFlag.EXCLUDE_MASTERIES),
ConnectionData(Entrance.town_to_sewer, RegionName.sewer, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_mutant_bug_lair, RegionName.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_railroad, RegionName.railroad),
ConnectionData(Entrance.mountain_to_tent, RegionName.tent,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.mountain_to_carpenter_shop, RegionName.carpenter,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.mountain_to_maru_room, RegionName.maru_room,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_sebastian_room, RegionName.sebastian_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.mountain_to_adventurer_guild, RegionName.adventurer_guild,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.adventurer_guild_to_bedroom, RegionName.adventurer_guild_bedroom),
ConnectionData(Entrance.enter_quarry, RegionName.quarry),
ConnectionData(Entrance.enter_quarry_mine_entrance, RegionName.quarry_mine_entrance,
flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_quarry_mine, RegionName.quarry_mine),
ConnectionData(Entrance.mountain_to_town, RegionName.town),
ConnectionData(Entrance.town_to_community_center, RegionName.community_center,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.access_crafts_room, RegionName.crafts_room),
ConnectionData(Entrance.access_pantry, RegionName.pantry),
ConnectionData(Entrance.access_fish_tank, RegionName.fish_tank),
ConnectionData(Entrance.access_boiler_room, RegionName.boiler_room),
ConnectionData(Entrance.access_bulletin_board, RegionName.bulletin_board),
ConnectionData(Entrance.access_vault, RegionName.vault),
ConnectionData(Entrance.town_to_hospital, RegionName.hospital,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_harvey_room, RegionName.harvey_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.town_to_pierre_general_store, RegionName.pierre_store,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_sunroom, RegionName.sunroom, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.town_to_clint_blacksmith, RegionName.blacksmith,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_saloon, RegionName.saloon,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.play_journey_of_the_prairie_king, RegionName.jotpk_world_1),
ConnectionData(Entrance.reach_jotpk_world_2, RegionName.jotpk_world_2),
ConnectionData(Entrance.reach_jotpk_world_3, RegionName.jotpk_world_3),
ConnectionData(Entrance.play_junimo_kart, RegionName.junimo_kart_1),
ConnectionData(Entrance.reach_junimo_kart_2, RegionName.junimo_kart_2),
ConnectionData(Entrance.reach_junimo_kart_3, RegionName.junimo_kart_3),
ConnectionData(Entrance.reach_junimo_kart_4, RegionName.junimo_kart_4),
ConnectionData(Entrance.town_to_sam_house, RegionName.sam_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_haley_house, RegionName.haley_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_mayor_manor, RegionName.mayor_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_alex_house, RegionName.alex_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_trailer, RegionName.trailer,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_museum, RegionName.museum,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_jojamart, RegionName.jojamart,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.purchase_movie_ticket, RegionName.movie_ticket_stand),
ConnectionData(Entrance.enter_abandoned_jojamart, RegionName.abandoned_jojamart),
ConnectionData(Entrance.enter_movie_theater, RegionName.movie_theater),
ConnectionData(Entrance.town_to_beach, RegionName.beach),
ConnectionData(Entrance.enter_elliott_house, RegionName.elliott_house,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.beach_to_willy_fish_shop, RegionName.fish_shop,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_tide_pools, RegionName.tide_pools),
ConnectionData(Entrance.mountain_to_the_mines, RegionName.mines,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.dig_to_mines_floor_5, RegionName.mines_floor_5),
ConnectionData(Entrance.dig_to_mines_floor_10, RegionName.mines_floor_10),
ConnectionData(Entrance.dig_to_mines_floor_15, RegionName.mines_floor_15),
ConnectionData(Entrance.dig_to_mines_floor_20, RegionName.mines_floor_20),
ConnectionData(Entrance.dig_to_mines_floor_25, RegionName.mines_floor_25),
ConnectionData(Entrance.dig_to_mines_floor_30, RegionName.mines_floor_30),
ConnectionData(Entrance.dig_to_mines_floor_35, RegionName.mines_floor_35),
ConnectionData(Entrance.dig_to_mines_floor_40, RegionName.mines_floor_40),
ConnectionData(Entrance.dig_to_mines_floor_45, RegionName.mines_floor_45),
ConnectionData(Entrance.dig_to_mines_floor_50, RegionName.mines_floor_50),
ConnectionData(Entrance.dig_to_mines_floor_55, RegionName.mines_floor_55),
ConnectionData(Entrance.dig_to_mines_floor_60, RegionName.mines_floor_60),
ConnectionData(Entrance.dig_to_mines_floor_65, RegionName.mines_floor_65),
ConnectionData(Entrance.dig_to_mines_floor_70, RegionName.mines_floor_70),
ConnectionData(Entrance.dig_to_mines_floor_75, RegionName.mines_floor_75),
ConnectionData(Entrance.dig_to_mines_floor_80, RegionName.mines_floor_80),
ConnectionData(Entrance.dig_to_mines_floor_85, RegionName.mines_floor_85),
ConnectionData(Entrance.dig_to_mines_floor_90, RegionName.mines_floor_90),
ConnectionData(Entrance.dig_to_mines_floor_95, RegionName.mines_floor_95),
ConnectionData(Entrance.dig_to_mines_floor_100, RegionName.mines_floor_100),
ConnectionData(Entrance.dig_to_mines_floor_105, RegionName.mines_floor_105),
ConnectionData(Entrance.dig_to_mines_floor_110, RegionName.mines_floor_110),
ConnectionData(Entrance.dig_to_mines_floor_115, RegionName.mines_floor_115),
ConnectionData(Entrance.dig_to_mines_floor_120, RegionName.mines_floor_120),
ConnectionData(Entrance.enter_skull_cavern_entrance, RegionName.skull_cavern_entrance,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_oasis, RegionName.oasis,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_casino, RegionName.casino, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_skull_cavern, RegionName.skull_cavern),
ConnectionData(Entrance.mine_to_skull_cavern_floor_25, RegionName.skull_cavern_25),
ConnectionData(Entrance.mine_to_skull_cavern_floor_50, RegionName.skull_cavern_50),
ConnectionData(Entrance.mine_to_skull_cavern_floor_75, RegionName.skull_cavern_75),
ConnectionData(Entrance.mine_to_skull_cavern_floor_100, RegionName.skull_cavern_100),
ConnectionData(Entrance.mine_to_skull_cavern_floor_125, RegionName.skull_cavern_125),
ConnectionData(Entrance.mine_to_skull_cavern_floor_150, RegionName.skull_cavern_150),
ConnectionData(Entrance.mine_to_skull_cavern_floor_175, RegionName.skull_cavern_175),
ConnectionData(Entrance.mine_to_skull_cavern_floor_200, RegionName.skull_cavern_200),
ConnectionData(Entrance.enter_witch_warp_cave, RegionName.witch_warp_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_swamp, RegionName.witch_swamp, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_witch_hut, RegionName.witch_hut, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.witch_warp_to_wizard_basement, RegionName.wizard_basement, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_bathhouse_entrance, RegionName.bathhouse_entrance,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.enter_locker_room, RegionName.locker_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.enter_public_bath, RegionName.public_bath, flag=RandomizationFlag.BUILDINGS),
ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop),
ConnectionData(LogicEntrance.buy_from_traveling_merchant, LogicRegion.traveling_cart),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_sunday, LogicRegion.traveling_cart_sunday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_monday, LogicRegion.traveling_cart_monday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_tuesday, LogicRegion.traveling_cart_tuesday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_wednesday, LogicRegion.traveling_cart_wednesday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_thursday, LogicRegion.traveling_cart_thursday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_friday, LogicRegion.traveling_cart_friday),
ConnectionData(LogicEntrance.buy_from_traveling_merchant_saturday, LogicRegion.traveling_cart_saturday),
ConnectionData(LogicEntrance.complete_raccoon_requests, LogicRegion.raccoon_daddy),
ConnectionData(LogicEntrance.fish_in_waterfall, LogicRegion.forest_waterfall),
ConnectionData(LogicEntrance.buy_from_raccoon, LogicRegion.raccoon_shop),
ConnectionData(LogicEntrance.farmhouse_cooking, LogicRegion.kitchen),
ConnectionData(LogicEntrance.watch_queen_of_sauce, LogicRegion.queen_of_sauce),
ConnectionData(LogicEntrance.grow_spring_crops, LogicRegion.spring_farming),
ConnectionData(LogicEntrance.grow_summer_crops, LogicRegion.summer_farming),
ConnectionData(LogicEntrance.grow_fall_crops, LogicRegion.fall_farming),
ConnectionData(LogicEntrance.grow_winter_crops, LogicRegion.winter_farming),
ConnectionData(LogicEntrance.grow_spring_crops_in_greenhouse, LogicRegion.spring_farming),
ConnectionData(LogicEntrance.grow_summer_crops_in_greenhouse, LogicRegion.summer_farming),
ConnectionData(LogicEntrance.grow_fall_crops_in_greenhouse, LogicRegion.fall_farming),
ConnectionData(LogicEntrance.grow_winter_crops_in_greenhouse, LogicRegion.winter_farming),
ConnectionData(LogicEntrance.grow_indoor_crops_in_greenhouse, LogicRegion.indoor_farming),
ConnectionData(LogicEntrance.grow_summer_fall_crops_in_summer, LogicRegion.summer_or_fall_farming),
ConnectionData(LogicEntrance.grow_summer_fall_crops_in_fall, LogicRegion.summer_or_fall_farming),
ConnectionData(LogicEntrance.shipping, LogicRegion.shipping),
ConnectionData(LogicEntrance.blacksmith_copper, LogicRegion.blacksmith_copper),
ConnectionData(LogicEntrance.blacksmith_iron, LogicRegion.blacksmith_iron),
ConnectionData(LogicEntrance.blacksmith_gold, LogicRegion.blacksmith_gold),
ConnectionData(LogicEntrance.blacksmith_iridium, LogicRegion.blacksmith_iridium),
ConnectionData(LogicEntrance.fishing, LogicRegion.fishing),
ConnectionData(LogicEntrance.attend_egg_festival, LogicRegion.egg_festival),
ConnectionData(LogicEntrance.attend_desert_festival, LogicRegion.desert_festival),
ConnectionData(LogicEntrance.attend_flower_dance, LogicRegion.flower_dance),
ConnectionData(LogicEntrance.attend_luau, LogicRegion.luau),
ConnectionData(LogicEntrance.attend_trout_derby, LogicRegion.trout_derby),
ConnectionData(LogicEntrance.attend_moonlight_jellies, LogicRegion.moonlight_jellies),
ConnectionData(LogicEntrance.attend_fair, LogicRegion.fair),
ConnectionData(LogicEntrance.attend_spirit_eve, LogicRegion.spirit_eve),
ConnectionData(LogicEntrance.attend_festival_of_ice, LogicRegion.festival_of_ice),
ConnectionData(LogicEntrance.attend_night_market, LogicRegion.night_market),
ConnectionData(LogicEntrance.attend_winter_star, LogicRegion.winter_star),
ConnectionData(LogicEntrance.attend_squidfest, LogicRegion.squidfest),
ConnectionData(LogicEntrance.buy_experience_books, LogicRegion.bookseller_1),
ConnectionData(LogicEntrance.buy_year1_books, LogicRegion.bookseller_2),
ConnectionData(LogicEntrance.buy_year3_books, LogicRegion.bookseller_3),
)
ginger_island_connections = (
ConnectionData(Entrance.use_island_obelisk, RegionName.island_south),
ConnectionData(Entrance.use_farm_obelisk, RegionName.farm),
ConnectionData(Entrance.mountain_to_leo_treehouse, RegionName.leo_treehouse, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.fish_shop_to_boat_tunnel, RegionName.boat_tunnel, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.boat_to_ginger_island, RegionName.island_south),
ConnectionData(Entrance.enter_dangerous_skull_cavern, RegionName.dangerous_skull_cavern),
ConnectionData(Entrance.dig_to_dangerous_mines_20, RegionName.dangerous_mines_20),
ConnectionData(Entrance.dig_to_dangerous_mines_60, RegionName.dangerous_mines_60),
ConnectionData(Entrance.dig_to_dangerous_mines_100, RegionName.dangerous_mines_100),
ConnectionData(Entrance.island_south_to_west, RegionName.island_west),
ConnectionData(Entrance.island_south_to_north, RegionName.island_north),
ConnectionData(Entrance.island_south_to_east, RegionName.island_east),
ConnectionData(Entrance.island_south_to_southeast, RegionName.island_south_east),
ConnectionData(Entrance.use_island_resort, RegionName.island_resort),
ConnectionData(Entrance.island_west_to_islandfarmhouse, RegionName.island_farmhouse, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_west_to_gourmand_cave, RegionName.gourmand_frog_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_west_to_crystals_cave, RegionName.colored_crystals_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_west_to_shipwreck, RegionName.shipwreck, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_west_to_qi_walnut_room, RegionName.qi_walnut_room, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_east_to_leo_hut, RegionName.leo_hut, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_east_to_island_shrine, RegionName.island_shrine, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_southeast_to_pirate_cove, RegionName.pirate_cove, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_north_to_field_office, RegionName.field_office, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_north_to_dig_site, RegionName.dig_site),
ConnectionData(Entrance.dig_site_to_professor_snail_cave, RegionName.professor_snail_cave, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.island_north_to_volcano, RegionName.volcano, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.volcano_to_secret_beach, RegionName.volcano_secret_beach, flag=RandomizationFlag.BUILDINGS),
ConnectionData(Entrance.talk_to_island_trader, RegionName.island_trader),
ConnectionData(Entrance.climb_to_volcano_5, RegionName.volcano_floor_5),
ConnectionData(Entrance.talk_to_volcano_dwarf, RegionName.volcano_dwarf_shop),
ConnectionData(Entrance.climb_to_volcano_10, RegionName.volcano_floor_10),
ConnectionData(Entrance.parrot_express_jungle_to_docks, RegionName.island_south),
ConnectionData(Entrance.parrot_express_dig_site_to_docks, RegionName.island_south),
ConnectionData(Entrance.parrot_express_volcano_to_docks, RegionName.island_south),
ConnectionData(Entrance.parrot_express_volcano_to_jungle, RegionName.island_west),
ConnectionData(Entrance.parrot_express_docks_to_jungle, RegionName.island_west),
ConnectionData(Entrance.parrot_express_dig_site_to_jungle, RegionName.island_west),
ConnectionData(Entrance.parrot_express_docks_to_dig_site, RegionName.dig_site),
ConnectionData(Entrance.parrot_express_volcano_to_dig_site, RegionName.dig_site),
ConnectionData(Entrance.parrot_express_jungle_to_dig_site, RegionName.dig_site),
ConnectionData(Entrance.parrot_express_dig_site_to_volcano, RegionName.island_north),
ConnectionData(Entrance.parrot_express_docks_to_volcano, RegionName.island_north),
ConnectionData(Entrance.parrot_express_jungle_to_volcano, RegionName.island_north),
ConnectionData(LogicEntrance.grow_spring_crops_on_island, LogicRegion.spring_farming),
ConnectionData(LogicEntrance.grow_summer_crops_on_island, LogicRegion.summer_farming),
ConnectionData(LogicEntrance.grow_fall_crops_on_island, LogicRegion.fall_farming),
ConnectionData(LogicEntrance.grow_winter_crops_on_island, LogicRegion.winter_farming),
ConnectionData(LogicEntrance.grow_indoor_crops_on_island, LogicRegion.indoor_farming),
ConnectionData(LogicEntrance.island_cooking, LogicRegion.kitchen),
)
connections_without_ginger_island_by_name: Mapping[str, ConnectionData] = MappingProxyType({
connection.name: connection
for connection in vanilla_connections
})
regions_without_ginger_island_by_name: Mapping[str, RegionData] = MappingProxyType({
region.name: region
for region in vanilla_regions
})
connections_with_ginger_island_by_name: Mapping[str, ConnectionData] = MappingProxyType({
connection.name: connection
for connection in vanilla_connections + ginger_island_connections
})
regions_with_ginger_island_by_name: Mapping[str, RegionData] = MappingProxyType({
region.name: region
for region in vanilla_regions + ginger_island_regions
})

View File

@@ -195,6 +195,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S
set_entrance_rule(multiworld, player, Entrance.enter_tide_pools, logic.received("Beach Bridge") | (logic.mod.magic.can_blink())) set_entrance_rule(multiworld, player, Entrance.enter_tide_pools, logic.received("Beach Bridge") | (logic.mod.magic.can_blink()))
set_entrance_rule(multiworld, player, Entrance.enter_quarry, logic.received("Bridge Repair") | (logic.mod.magic.can_blink())) set_entrance_rule(multiworld, player, Entrance.enter_quarry, logic.received("Bridge Repair") | (logic.mod.magic.can_blink()))
set_entrance_rule(multiworld, player, Entrance.enter_secret_woods, logic.tool.has_tool(Tool.axe, "Iron") | (logic.mod.magic.can_blink())) set_entrance_rule(multiworld, player, Entrance.enter_secret_woods, logic.tool.has_tool(Tool.axe, "Iron") | (logic.mod.magic.can_blink()))
set_entrance_rule(multiworld, player, Entrance.forest_to_wizard_tower, logic.region.can_reach(Region.community_center))
set_entrance_rule(multiworld, player, Entrance.forest_to_sewer, logic.wallet.has_rusty_key()) set_entrance_rule(multiworld, player, Entrance.forest_to_sewer, logic.wallet.has_rusty_key())
set_entrance_rule(multiworld, player, Entrance.town_to_sewer, logic.wallet.has_rusty_key()) set_entrance_rule(multiworld, player, Entrance.town_to_sewer, logic.wallet.has_rusty_key())
set_entrance_rule(multiworld, player, Entrance.enter_abandoned_jojamart, logic.has_abandoned_jojamart()) set_entrance_rule(multiworld, player, Entrance.enter_abandoned_jojamart, logic.has_abandoned_jojamart())

View File

@@ -1,173 +0,0 @@
import random
import unittest
from typing import Set
from BaseClasses import get_seed
from .bases import SVTestCase
from .options.utils import fill_dataclass_with_default
from .. import create_content
from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression
from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions
from ..strings.entrance_names import Entrance as EntranceName
from ..strings.region_names import Region as RegionName
connections_by_name = {connection.name for connection in vanilla_connections}
regions_by_name = {region.name for region in vanilla_regions}
class TestRegions(unittest.TestCase):
def test_region_exits_lead_somewhere(self):
for region in vanilla_regions:
with self.subTest(region=region):
for exit in region.exits:
self.assertIn(exit, connections_by_name,
f"{region.name} is leading to {exit} but it does not exist.")
def test_connection_lead_somewhere(self):
for connection in vanilla_connections:
with self.subTest(connection=connection):
self.assertIn(connection.destination, regions_by_name,
f"{connection.name} is leading to {connection.destination} but it does not exist.")
def explore_connections_tree_up_to_blockers(blocked_entrances: Set[str], connections_by_name, regions_by_name):
explored_entrances = set()
explored_regions = set()
entrances_to_explore = set()
current_node_name = "Menu"
current_node = regions_by_name[current_node_name]
entrances_to_explore.update(current_node.exits)
while entrances_to_explore:
current_entrance_name = entrances_to_explore.pop()
current_entrance = connections_by_name[current_entrance_name]
current_node_name = current_entrance.destination
explored_entrances.add(current_entrance_name)
explored_regions.add(current_node_name)
if current_entrance_name in blocked_entrances:
continue
current_node = regions_by_name[current_node_name]
entrances_to_explore.update({entrance for entrance in current_node.exits if entrance not in explored_entrances})
return explored_regions
class TestEntranceRando(SVTestCase):
def test_entrance_randomization(self):
for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
(EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
(EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
(EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
sv_options = fill_dataclass_with_default({
EntranceRandomization.internal_name: option,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
})
content = create_content(sv_options)
seed = get_seed()
rand = random.Random(seed)
with self.subTest(flag=flag, msg=f"Seed: {seed}"):
entrances, regions = create_final_connections_and_regions(sv_options)
_, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances)
for connection in vanilla_connections:
if flag in connection.flag:
connection_in_randomized = connection.name in randomized_connections
reverse_in_randomized = connection.reverse in randomized_connections
self.assertTrue(connection_in_randomized, f"Connection {connection.name} should be randomized but it is not in the output.")
self.assertTrue(reverse_in_randomized, f"Connection {connection.reverse} should be randomized but it is not in the output.")
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
f"Connections are duplicated in randomization.")
def test_entrance_randomization_without_island(self):
for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
(EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
(EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
(EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
sv_options = fill_dataclass_with_default({
EntranceRandomization.internal_name: option,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
})
content = create_content(sv_options)
seed = get_seed()
rand = random.Random(seed)
with self.subTest(option=option, flag=flag, seed=seed):
entrances, regions = create_final_connections_and_regions(sv_options)
_, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances)
for connection in vanilla_connections:
if flag in connection.flag:
if RandomizationFlag.GINGER_ISLAND in connection.flag:
self.assertNotIn(connection.name, randomized_connections,
f"Connection {connection.name} should not be randomized but it is in the output.")
self.assertNotIn(connection.reverse, randomized_connections,
f"Connection {connection.reverse} should not be randomized but it is in the output.")
else:
self.assertIn(connection.name, randomized_connections,
f"Connection {connection.name} should be randomized but it is not in the output.")
self.assertIn(connection.reverse, randomized_connections,
f"Connection {connection.reverse} should be randomized but it is not in the output.")
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
f"Connections are duplicated in randomization.")
def test_cannot_put_island_access_on_island(self):
sv_options = fill_dataclass_with_default({
EntranceRandomization.internal_name: EntranceRandomization.option_buildings,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
})
content = create_content(sv_options)
for i in range(0, 100 if self.skip_long_tests else 10000):
seed = get_seed()
rand = random.Random(seed)
with self.subTest(msg=f"Seed: {seed}"):
entrances, regions = create_final_connections_and_regions(sv_options)
randomized_connections, randomized_data = randomize_connections(rand, sv_options, content, regions, entrances)
connections_by_name = {connection.name: connection for connection in randomized_connections}
blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island}
required_regions = {RegionName.wizard_tower, RegionName.boat_tunnel}
self.assert_can_reach_any_region_before_blockers(required_regions, blocked_entrances, connections_by_name, regions)
def assert_can_reach_any_region_before_blockers(self, required_regions, blocked_entrances, connections_by_name, regions_by_name):
explored_regions = explore_connections_tree_up_to_blockers(blocked_entrances, connections_by_name, regions_by_name)
self.assertTrue(any(region in explored_regions for region in required_regions))
class TestEntranceClassifications(SVTestCase):
def test_non_progression_are_all_accessible_with_empty_inventory(self):
for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
(EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION)]:
world_options = {
EntranceRandomization.internal_name: option
}
with self.solo_world_sub_test(world_options=world_options, flag=flag) as (multiworld, sv_world):
ap_entrances = {entrance.name: entrance for entrance in multiworld.get_entrances()}
for randomized_entrance in sv_world.randomized_entrances:
if randomized_entrance in ap_entrances:
ap_entrance_origin = ap_entrances[randomized_entrance]
self.assertTrue(ap_entrance_origin.access_rule(multiworld.state))
if sv_world.randomized_entrances[randomized_entrance] in ap_entrances:
ap_entrance_destination = multiworld.get_entrance(sv_world.randomized_entrances[randomized_entrance], 1)
self.assertTrue(ap_entrance_destination.access_rule(multiworld.state))
def test_no_ginger_island_entrances_when_excluded(self):
world_options = {
EntranceRandomization.internal_name: EntranceRandomization.option_disabled,
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true
}
with self.solo_world_sub_test(world_options=world_options) as (multiworld, _):
ap_entrances = {entrance.name: entrance for entrance in multiworld.get_entrances()}
entrance_data_by_name = {entrance.name: entrance for entrance in vanilla_connections}
for entrance_name in ap_entrances:
entrance_data = entrance_data_by_name[entrance_name]
with self.subTest(f"{entrance_name}: {entrance_data.flag}"):
self.assertFalse(entrance_data.flag & RandomizationFlag.GINGER_ISLAND)

View File

@@ -1,7 +1,7 @@
from typing import List from typing import List
from unittest import TestCase from unittest import TestCase
from BaseClasses import CollectionState, Location, Region from BaseClasses import CollectionState, Location, Region, Entrance
from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach
from ...stardew_rule.rule_explain import explain from ...stardew_rule.rule_explain import explain
@@ -79,3 +79,13 @@ class RuleAssertMixin(TestCase):
except KeyError as e: except KeyError as e:
raise AssertionError(f"Error while checking region {region_name}: {e}" raise AssertionError(f"Error while checking region {region_name}: {e}"
f"\nExplanation: {expl}") f"\nExplanation: {expl}")
def assert_can_reach_entrance(self, entrance: Entrance | str, state: CollectionState) -> None:
entrance_name = entrance.name if isinstance(entrance, Entrance) else entrance
expl = explain(Reach(entrance_name, "Entrance", 1), state)
try:
can_reach = state.can_reach_entrance(entrance_name, 1)
self.assertTrue(can_reach, expl)
except KeyError as e:
raise AssertionError(f"Error while checking entrance {entrance_name}: {e}"
f"\nExplanation: {expl}")

View File

@@ -7,7 +7,7 @@ import unittest
from contextlib import contextmanager from contextlib import contextmanager
from typing import Optional, Dict, Union, Any, List, Iterable from typing import Optional, Dict, Union, Any, List, Iterable
from BaseClasses import get_seed, MultiWorld, Location, Item, CollectionState from BaseClasses import get_seed, MultiWorld, Location, Item, CollectionState, Entrance
from test.bases import WorldTestBase from test.bases import WorldTestBase
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
from worlds.AutoWorld import call_all from worlds.AutoWorld import call_all
@@ -179,6 +179,11 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
state = self.multiworld.state state = self.multiworld.state
super().assert_cannot_reach_location(location, state) super().assert_cannot_reach_location(location, state)
def assert_can_reach_entrance(self, entrance: Entrance | str, state: CollectionState | None = None) -> None:
if state is None:
state = self.multiworld.state
super().assert_can_reach_entrance(entrance, state)
pre_generated_worlds = {} pre_generated_worlds = {}

View File

@@ -1,17 +1,13 @@
import random
from typing import ClassVar from typing import ClassVar
from BaseClasses import get_seed
from test.param import classvar_matrix from test.param import classvar_matrix
from ..TestGeneration import get_all_permanent_progression_items from ..TestGeneration import get_all_permanent_progression_items
from ..assertion import ModAssertMixin, WorldAssertMixin from ..assertion import ModAssertMixin, WorldAssertMixin
from ..bases import SVTestCase, SVTestBase, solo_multiworld from ..bases import SVTestCase, SVTestBase, solo_multiworld
from ..options.presets import allsanity_mods_6_x_x from ..options.presets import allsanity_mods_6_x_x
from ..options.utils import fill_dataclass_with_default from ... import options, Group
from ... import options, Group, create_content
from ...mods.mod_data import ModNames from ...mods.mod_data import ModNames
from ...options.options import all_mods from ...options.options import all_mods
from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions
class TestCanGenerateAllsanityWithMods(WorldAssertMixin, ModAssertMixin, SVTestCase): class TestCanGenerateAllsanityWithMods(WorldAssertMixin, ModAssertMixin, SVTestCase):
@@ -117,39 +113,6 @@ class TestNoGingerIslandModItemGeneration(SVTestBase):
self.assertIn(progression_item.name, all_created_items) self.assertIn(progression_item.name, all_created_items)
class TestModEntranceRando(SVTestCase):
def test_mod_entrance_randomization(self):
for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
(options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
(options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS),
(options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
sv_options = fill_dataclass_with_default({
options.EntranceRandomization.internal_name: option,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false,
options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries,
options.Mods.internal_name: frozenset(options.Mods.valid_keys)
})
content = create_content(sv_options)
seed = get_seed()
rand = random.Random(seed)
with self.subTest(option=option, flag=flag, seed=seed):
final_connections, final_regions = create_final_connections_and_regions(sv_options)
_, randomized_connections = randomize_connections(rand, sv_options, content, final_regions, final_connections)
for connection_name in final_connections:
connection = final_connections[connection_name]
if flag in connection.flag:
connection_in_randomized = connection_name in randomized_connections
reverse_in_randomized = connection.reverse in randomized_connections
self.assertTrue(connection_in_randomized, f"Connection {connection_name} should be randomized but it is not in the output")
self.assertTrue(reverse_in_randomized, f"Connection {connection.reverse} should be randomized but it is not in the output.")
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
f"Connections are duplicated in randomization.")
class TestVanillaLogicAlternativeWhenQuestsAreNotRandomized(WorldAssertMixin, SVTestBase): class TestVanillaLogicAlternativeWhenQuestsAreNotRandomized(WorldAssertMixin, SVTestBase):
"""We often forget to add an alternative rule that works when quests are not randomized. When this happens, some """We often forget to add an alternative rule that works when quests are not randomized. When this happens, some
Location are not reachable because they depend on items that are only added to the pool when quests are randomized. Location are not reachable because they depend on items that are only added to the pool when quests are randomized.

View File

@@ -0,0 +1,36 @@
from ..bases import SVTestBase
from ... import options
from ...regions.model import RandomizationFlag
from ...regions.regions import create_all_connections
class EntranceRandomizationAssertMixin:
def assert_non_progression_are_all_accessible_with_empty_inventory(self: SVTestBase):
all_connections = create_all_connections(self.world.content.registered_packs)
non_progression_connections = [connection for connection in all_connections.values() if RandomizationFlag.BIT_NON_PROGRESSION in connection.flag]
for non_progression_connections in non_progression_connections:
with self.subTest(connection=non_progression_connections):
self.assert_can_reach_entrance(non_progression_connections.name)
# This test does not actually need to generate with entrance randomization. Entrances rules are the same regardless of the randomization.
class TestVanillaEntranceClassifications(EntranceRandomizationAssertMixin, SVTestBase):
options = {
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false,
options.Mods: frozenset()
}
def test_non_progression_are_all_accessible_with_empty_inventory(self):
self.assert_non_progression_are_all_accessible_with_empty_inventory()
class TestModdedEntranceClassifications(EntranceRandomizationAssertMixin, SVTestBase):
options = {
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false,
options.Mods: frozenset(options.Mods.valid_keys)
}
def test_non_progression_are_all_accessible_with_empty_inventory(self):
self.assert_non_progression_are_all_accessible_with_empty_inventory()

View File

@@ -0,0 +1,167 @@
from collections import deque
from collections.abc import Collection
from unittest.mock import patch, Mock
from BaseClasses import get_seed, MultiWorld, Entrance
from ..assertion import WorldAssertMixin
from ..bases import SVTestCase, solo_multiworld
from ... import options
from ...mods.mod_data import ModNames
from ...options import EntranceRandomization, ExcludeGingerIsland, SkillProgression
from ...options.options import all_mods
from ...regions.entrance_rando import create_entrance_rando_target, prepare_mod_data, connect_regions
from ...regions.model import RegionData, ConnectionData, RandomizationFlag
from ...strings.entrance_names import Entrance as EntranceName
from ...strings.region_names import Region as RegionName
class TestEntranceRando(SVTestCase):
def test_given_connection_matching_randomization_when_connect_regions_then_make_connection_entrance_rando_target(self):
region_data_by_name = {
"Region1": RegionData("Region1", ("randomized_connection", "not_randomized")),
"Region2": RegionData("Region2"),
"Region3": RegionData("Region3"),
}
connection_data_by_name = {
"randomized_connection": ConnectionData("randomized_connection", "Region2", flag=RandomizationFlag.PELICAN_TOWN),
"not_randomized": ConnectionData("not_randomized", "Region2", flag=RandomizationFlag.BUILDINGS),
}
regions_by_name = {
"Region1": Mock(),
"Region2": Mock(),
"Region3": Mock(),
}
player_randomization_flag = RandomizationFlag.BIT_PELICAN_TOWN
with patch("worlds.stardew_valley.regions.entrance_rando.create_entrance_rando_target") as mock_create_entrance_rando_target:
connect_regions(region_data_by_name, connection_data_by_name, regions_by_name, player_randomization_flag)
expected_origin, expected_destination = regions_by_name["Region1"], regions_by_name["Region2"]
expected_connection = connection_data_by_name["randomized_connection"]
mock_create_entrance_rando_target.assert_called_once_with(expected_origin, expected_destination, expected_connection)
def test_when_create_entrance_rando_target_then_create_exit_and_er_target(self):
origin = Mock()
destination = Mock()
connection_data = ConnectionData("origin to destination", "destination")
create_entrance_rando_target(origin, destination, connection_data)
origin.create_exit.assert_called_once_with("origin to destination")
destination.create_er_target.assert_called_once_with("destination to origin")
def test_when_prepare_mod_data_then_swapped_connections_contains_both_directions(self):
placements = Mock(pairings=[("A to B", "C to A"), ("C to D", "A to C")])
swapped_connections = prepare_mod_data(placements)
self.assertEqual({"A to B": "A to C", "C to A": "B to A", "C to D": "C to A", "A to C": "D to C"}, swapped_connections)
class TestEntranceRandoCreatesValidWorlds(WorldAssertMixin, SVTestCase):
# The following tests validate that ER still generates winnable and logically-sane games with given mods.
# Mods that do not interact with entrances are skipped
# Not all ER settings are tested, because 'buildings' is, essentially, a superset of all others
def test_ginger_island_excluded_buildings(self):
world_options = {
options.EntranceRandomization: options.EntranceRandomization.option_buildings,
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true
}
with solo_multiworld(world_options) as (multi_world, _):
self.assert_basic_checks(multi_world)
def test_deepwoods_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.deepwoods, options.EntranceRandomization.option_buildings)
def test_juna_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.juna, options.EntranceRandomization.option_buildings)
def test_jasper_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.jasper, options.EntranceRandomization.option_buildings)
def test_alec_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.alec, options.EntranceRandomization.option_buildings)
def test_yoba_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.yoba, options.EntranceRandomization.option_buildings)
def test_eugene_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.eugene, options.EntranceRandomization.option_buildings)
def test_ayeisha_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.ayeisha, options.EntranceRandomization.option_buildings)
def test_riley_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.riley, options.EntranceRandomization.option_buildings)
def test_sve_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.sve, options.EntranceRandomization.option_buildings)
def test_alecto_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.alecto, options.EntranceRandomization.option_buildings)
def test_lacey_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.lacey, options.EntranceRandomization.option_buildings)
def test_boarding_house_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(ModNames.boarding_house, options.EntranceRandomization.option_buildings)
def test_all_mods_entrance_randomization_buildings(self):
self.perform_basic_checks_on_mod_with_er(all_mods, options.EntranceRandomization.option_buildings)
def perform_basic_checks_on_mod_with_er(self, mods: str | set[str], er_option: int) -> None:
if isinstance(mods, str):
mods = {mods}
world_options = {
options.EntranceRandomization: er_option,
options.Mods: frozenset(mods),
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false
}
with solo_multiworld(world_options) as (multi_world, _):
self.assert_basic_checks(multi_world)
# GER should have this covered, but it's good to have a backup
class TestGingerIslandEntranceRando(SVTestCase):
def test_cannot_put_island_access_on_island(self):
test_options = {
options.EntranceRandomization: EntranceRandomization.option_buildings,
options.ExcludeGingerIsland: ExcludeGingerIsland.option_false,
options.SkillProgression: SkillProgression.option_progressive_with_masteries,
}
blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island}
required_regions = {RegionName.wizard_tower, RegionName.boat_tunnel}
for i in range(0, 10 if self.skip_long_tests else 1000):
seed = get_seed()
with self.solo_world_sub_test(f"Seed: {seed}", world_options=test_options, world_caching=False, seed=seed) as (multiworld, world):
self.assert_can_reach_any_region_before_blockers(required_regions, blocked_entrances, multiworld)
def assert_can_reach_any_region_before_blockers(self, required_regions: Collection[str], blocked_entrances: Collection[str], multiworld: MultiWorld):
explored_regions = explore_regions_up_to_blockers(blocked_entrances, multiworld)
self.assertTrue(any(region in explored_regions for region in required_regions))
def explore_regions_up_to_blockers(blocked_entrances: Collection[str], multiworld: MultiWorld) -> set[str]:
explored_regions: set[str] = set()
regions_by_name = multiworld.regions.region_cache[1]
regions_to_explore = deque([regions_by_name["Menu"]])
while regions_to_explore:
region = regions_to_explore.pop()
if region.name in explored_regions:
continue
explored_regions.add(region.name)
for exit_ in region.exits:
exit_: Entrance
if exit_.name in blocked_entrances:
continue
regions_to_explore.append(exit_.connected_region)
return explored_regions

View File

@@ -0,0 +1,88 @@
import unittest
from ..options.utils import fill_dataclass_with_default
from ... import create_content, options
from ...regions.entrance_rando import create_player_randomization_flag
from ...regions.model import RandomizationFlag, ConnectionData
class TestConnectionData(unittest.TestCase):
def test_given_entrances_not_randomized_when_is_eligible_for_randomization_then_not_eligible(self):
player_flag = RandomizationFlag.NOT_RANDOMIZED
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.PELICAN_TOWN)
is_eligible = connection.is_eligible_for_randomization(player_flag)
self.assertFalse(is_eligible)
def test_given_pelican_town_connection_when_is_eligible_for_pelican_town_randomization_then_eligible(self):
player_flag = RandomizationFlag.BIT_PELICAN_TOWN
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.PELICAN_TOWN)
is_eligible = connection.is_eligible_for_randomization(player_flag)
self.assertTrue(is_eligible)
def test_given_pelican_town_connection_when_is_eligible_for_buildings_randomization_then_eligible(self):
player_flag = RandomizationFlag.BIT_BUILDINGS
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.PELICAN_TOWN)
is_eligible = connection.is_eligible_for_randomization(player_flag)
self.assertTrue(is_eligible)
def test_given_non_progression_connection_when_is_eligible_for_pelican_town_randomization_then_not_eligible(self):
player_flag = RandomizationFlag.BIT_PELICAN_TOWN
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.NON_PROGRESSION)
is_eligible = connection.is_eligible_for_randomization(player_flag)
self.assertFalse(is_eligible)
def test_given_non_progression_masteries_connection_when_is_eligible_for_non_progression_randomization_then_eligible(self):
player_flag = RandomizationFlag.BIT_NON_PROGRESSION
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.NON_PROGRESSION ^ RandomizationFlag.EXCLUDE_MASTERIES)
is_eligible = connection.is_eligible_for_randomization(player_flag)
self.assertTrue(is_eligible)
def test_given_non_progression_masteries_connection_when_is_eligible_for_non_progression_without_masteries_randomization_then_not_eligible(self):
player_flag = RandomizationFlag.BIT_NON_PROGRESSION | RandomizationFlag.EXCLUDE_MASTERIES
connection = ConnectionData("Go to Somewhere", "Somewhere", RandomizationFlag.NON_PROGRESSION ^ RandomizationFlag.EXCLUDE_MASTERIES)
is_eligible = connection.is_eligible_for_randomization(player_flag)
self.assertFalse(is_eligible)
class TestRandomizationFlag(unittest.TestCase):
def test_given_entrance_randomization_choice_when_create_player_randomization_flag_then_only_relevant_bit_is_enabled(self):
for entrance_randomization_choice, expected_bit in (
(options.EntranceRandomization.option_disabled, RandomizationFlag.NOT_RANDOMIZED),
(options.EntranceRandomization.option_pelican_town, RandomizationFlag.BIT_PELICAN_TOWN),
(options.EntranceRandomization.option_non_progression, RandomizationFlag.BIT_NON_PROGRESSION),
(options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BIT_BUILDINGS),
(options.EntranceRandomization.option_buildings, RandomizationFlag.BIT_BUILDINGS),
(options.EntranceRandomization.option_chaos, RandomizationFlag.BIT_BUILDINGS),
):
player_options = fill_dataclass_with_default({options.EntranceRandomization: entrance_randomization_choice})
content = create_content(player_options)
flag = create_player_randomization_flag(player_options.entrance_randomization, content)
self.assertEqual(flag, expected_bit)
def test_given_masteries_not_randomized_when_create_player_randomization_flag_then_exclude_masteries_bit_enabled(self):
for entrance_randomization_choice in set(options.EntranceRandomization.options.values()) ^ {options.EntranceRandomization.option_disabled}:
player_options = fill_dataclass_with_default({
options.EntranceRandomization: entrance_randomization_choice,
options.SkillProgression: options.SkillProgression.option_progressive
})
content = create_content(player_options)
flag = create_player_randomization_flag(player_options.entrance_randomization, content)
self.assertIn(RandomizationFlag.EXCLUDE_MASTERIES, flag)

View File

@@ -0,0 +1,66 @@
import unittest
from ..options.utils import fill_dataclass_with_default
from ... import options
from ...content import create_content
from ...mods.region_data import region_data_by_content_pack
from ...regions import vanilla_data
from ...regions.model import MergeFlag
from ...regions.regions import create_all_regions, create_all_connections
class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase):
def test_region_exits_lead_somewhere(self):
for region in vanilla_data.regions_with_ginger_island_by_name.values():
with self.subTest(region=region):
for exit_ in region.exits:
self.assertIn(exit_, vanilla_data.connections_with_ginger_island_by_name,
f"{region.name} is leading to {exit_} but it does not exist.")
def test_connection_lead_somewhere(self):
for connection in vanilla_data.connections_with_ginger_island_by_name.values():
with self.subTest(connection=connection):
self.assertIn(connection.destination, vanilla_data.regions_with_ginger_island_by_name,
f"{connection.name} is leading to {connection.destination} but it does not exist.")
class TestVanillaRegionsConnectionsWithoutGingerIsland(unittest.TestCase):
def test_region_exits_lead_somewhere(self):
for region in vanilla_data.regions_without_ginger_island_by_name.values():
with self.subTest(region=region):
for exit_ in region.exits:
self.assertIn(exit_, vanilla_data.connections_without_ginger_island_by_name,
f"{region.name} is leading to {exit_} but it does not exist.")
def test_connection_lead_somewhere(self):
for connection in vanilla_data.connections_without_ginger_island_by_name.values():
with self.subTest(connection=connection):
self.assertIn(connection.destination, vanilla_data.regions_without_ginger_island_by_name,
f"{connection.name} is leading to {connection.destination} but it does not exist.")
class TestModsConnections(unittest.TestCase):
options = {
options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false,
options.Mods: frozenset(options.Mods.valid_keys)
}
content = create_content(fill_dataclass_with_default(options))
all_regions_by_name = create_all_regions(content.registered_packs)
all_connections_by_name = create_all_connections(content.registered_packs)
def test_region_exits_lead_somewhere(self):
for mod_region_data in region_data_by_content_pack.values():
for region in mod_region_data.regions:
if MergeFlag.REMOVE_EXITS in region.flag:
continue
with self.subTest(mod=mod_region_data.mod_name, region=region.name):
for exit_ in region.exits:
self.assertIn(exit_, self.all_connections_by_name, f"{region.name} is leading to {exit_} but it does not exist.")
def test_connection_lead_somewhere(self):
for mod_region_data in region_data_by_content_pack.values():
for connection in mod_region_data.connections:
with self.subTest(mod=mod_region_data.mod_name, connection=connection.name):
self.assertIn(connection.destination, self.all_regions_by_name,
f"{connection.name} is leading to {connection.destination} but it does not exist.")

View File

@@ -92,7 +92,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: state.has('Water Mask', player) if flooded.flood_lab else (logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state))), LocationData('Military Fortress (hangar)', 'Military Fortress: Pedestal', 1337065, lambda state: state.has('Water Mask', player) if flooded.flood_lab else (logic.has_doublejump_of_npc(state) or logic.has_forwarddash_doublejump(state))),
LocationData('The lab', 'Lab: Coffee break', 1337066), LocationData('The lab', 'Lab: Coffee break', 1337066),
LocationData('The lab', 'Lab: Lower trash right', 1337067, logic.has_doublejump), LocationData('The lab', 'Lab: Lower trash right', 1337067, logic.has_doublejump),
LocationData('The lab', 'Lab: Lower trash left', 1337068, lambda state: logic.has_doublejump_of_npc(state) if options.lock_key_amadeus else logic.has_upwarddash ), LocationData('The lab', 'Lab: Lower trash left', 1337068, lambda state: logic.has_doublejump_of_npc(state) if options.lock_key_amadeus else logic.has_upwarddash(state) ),
LocationData('The lab', 'Lab: Below lab entrance', 1337069, logic.has_doublejump), LocationData('The lab', 'Lab: Below lab entrance', 1337069, logic.has_doublejump),
LocationData('The lab (power off)', 'Lab: Trash jump room', 1337070, lambda state: not options.lock_key_amadeus or logic.has_doublejump_of_npc(state) ), LocationData('The lab (power off)', 'Lab: Trash jump room', 1337070, lambda state: not options.lock_key_amadeus or logic.has_doublejump_of_npc(state) ),
LocationData('The lab (power off)', 'Lab: Dynamo Works', 1337071, lambda state: not options.lock_key_amadeus or (state.has_all(('Lab Access Research', 'Lab Access Dynamo'), player)) ), LocationData('The lab (power off)', 'Lab: Dynamo Works', 1337071, lambda state: not options.lock_key_amadeus or (state.has_all(('Lab Access Research', 'Lab Access Dynamo'), player)) ),
@@ -100,7 +100,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
LocationData('The lab (power off)', 'Lab: Experiment #13', 1337073, lambda state: not options.lock_key_amadeus or state.has('Lab Access Experiment', player) ), LocationData('The lab (power off)', 'Lab: Experiment #13', 1337073, lambda state: not options.lock_key_amadeus or state.has('Lab Access Experiment', player) ),
LocationData('The lab (upper)', 'Lab: Download and chest room chest', 1337074), LocationData('The lab (upper)', 'Lab: Download and chest room chest', 1337074),
LocationData('The lab (upper)', 'Lab: Lab secret', 1337075, logic.can_break_walls), LocationData('The lab (upper)', 'Lab: Lab secret', 1337075, logic.can_break_walls),
LocationData('The lab (power off)', 'Lab: Spider Hell', 1337076, lambda state: logic.has_keycard_A and not options.lock_key_amadeus or state.has('Lab Access Research', player)), LocationData('The lab (power off)', 'Lab: Spider Hell', 1337076, lambda state: logic.has_keycard_A(state) and not options.lock_key_amadeus or state.has('Lab Access Research', player)),
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard bottom chest', 1337077), LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard bottom chest', 1337077),
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard floor secret', 1337078, lambda state: logic.has_upwarddash(state) and logic.can_break_walls(state)), LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard floor secret', 1337078, lambda state: logic.has_upwarddash(state) and logic.can_break_walls(state)),
LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard upper chest', 1337079, lambda state: logic.has_upwarddash(state)), LocationData('Emperors tower', 'Emperor\'s Tower: Courtyard upper chest', 1337079, lambda state: logic.has_upwarddash(state)),
@@ -150,10 +150,10 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 3', 1337118, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Jackpot room chest 4', 1337119, lambda state: flooded.flood_maw or logic.has_forwarddash_doublejump(state)),
LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Pedestal', 1337120, lambda state: not flooded.flood_maw or state.has('Water Mask', player)),
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: state.has('Water Mask', player) if flooded.flood_maw else logic.has_doublejump(state)), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Last chance before Maw', 1337121, lambda state: flooded.flood_maw or logic.has_doublejump(state)),
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Plasma Crystal', 1337173, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player)),
LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player) and (not flooded.flood_maw or state.has('Water Mask', player))), LocationData('Caves of Banishment (Maw)', 'Killed Maw', EventId, lambda state: state.has('Gas Mask', player)),
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) and (not flooded.flood_maw or state.has('Water Mask', player))), LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Mineshaft', 1337122, lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player)),
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Wyvern room', 1337123), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Wyvern room', 1337123),
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room above water chest', 1337124), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room above water chest', 1337124),
LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater left chest', 1337125, lambda state: state.has('Water Mask', player)), LocationData('Caves of Banishment (Sirens)', 'Caves of Banishment (Sirens): Siren room underwater left chest', 1337125, lambda state: state.has('Water Mask', player)),
@@ -251,7 +251,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
LocationData('Royal towers (upper)', 'Royal Towers: Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195), LocationData('Royal towers (upper)', 'Royal Towers: Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195),
LocationData('Royal towers (upper)', 'Royal Towers: Journal - Aelana Boss (Stained Letter)', 1337196), LocationData('Royal towers (upper)', 'Royal Towers: Journal - Aelana Boss (Stained Letter)', 1337196),
LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: flooded.flood_courtyard or logic.has_doublejump_of_npc(state)), LocationData('Royal towers', 'Royal Towers: Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: flooded.flood_courtyard or logic.has_doublejump_of_npc(state)),
LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198, lambda state: not flooded.flood_maw or state.has('Water Mask', player)) LocationData('Caves of Banishment (Maw)', 'Caves of Banishment (Maw): Journal - Lower Left Caves (Naivety)', 1337198)
) )
# 1337199 - 1337232 Reserved for future use # 1337199 - 1337232 Reserved for future use

View File

@@ -88,12 +88,15 @@ class PreCalculatedWeights:
if options.risky_warps: if options.risky_warps:
past_teleportation_gates.append("GateLakeSereneLeft") past_teleportation_gates.append("GateLakeSereneLeft")
present_teleportation_gates.append("GateDadsTower")
if not is_xarion_flooded: if not is_xarion_flooded:
present_teleportation_gates.append("GateXarion") present_teleportation_gates.append("GateXarion")
if not is_lab_flooded: # Prevent going past the lazers without a way to the past
present_teleportation_gates.append("GateLabEntrance") if options.unchained_keys or options.prism_break or not options.pyramid_start:
present_teleportation_gates.append("GateDadsTower")
if not is_lab_flooded:
present_teleportation_gates.append("GateLabEntrance")
# Prevent getting stuck in the past without a way back to the future
if options.inverted or (options.pyramid_start and not options.back_to_the_future): if options.inverted or (options.pyramid_start and not options.back_to_the_future):
all_gates: Tuple[str, ...] = present_teleportation_gates all_gates: Tuple[str, ...] = present_teleportation_gates
else: else:

View File

@@ -115,7 +115,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, options: Timesp
connect(world, player, 'The lab', 'The lab (power off)', lambda state: options.lock_key_amadeus or logic.has_doublejump_of_npc(state)) connect(world, player, 'The lab', 'The lab (power off)', lambda state: options.lock_key_amadeus or logic.has_doublejump_of_npc(state))
connect(world, player, 'The lab (power off)', 'The lab', lambda state: not flooded.flood_lab or state.has('Water Mask', player)) connect(world, player, 'The lab (power off)', 'The lab', lambda state: not flooded.flood_lab or state.has('Water Mask', player))
connect(world, player, 'The lab (power off)', 'The lab (upper)', lambda state: logic.has_forwarddash_doublejump(state) and ((not options.lock_key_amadeus) or state.has('Lab Access Genza', player))) connect(world, player, 'The lab (power off)', 'The lab (upper)', lambda state: logic.has_forwarddash_doublejump(state) and ((not options.lock_key_amadeus) or state.has('Lab Access Genza', player)))
connect(world, player, 'The lab (upper)', 'The lab (power off)') connect(world, player, 'The lab (upper)', 'The lab (power off)', lambda state: options.lock_key_amadeus and state.has('Lab Access Genza', player))
connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump) connect(world, player, 'The lab (upper)', 'Emperors tower', logic.has_forwarddash_doublejump)
connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) connect(world, player, 'The lab (upper)', 'Ancient Pyramid (entrance)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player))
connect(world, player, 'Emperors tower', 'The lab (upper)') connect(world, player, 'Emperors tower', 'The lab (upper)')
@@ -141,7 +141,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, options: Timesp
connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves') connect(world, player, 'Lower Lake Serene', 'Left Side forest Caves')
connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)) connect(world, player, 'Lower Lake Serene', 'Caves of Banishment (upper)', lambda state: flooded.flood_lake_serene or logic.has_doublejump(state))
connect(world, player, 'Caves of Banishment (upper)', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player)) connect(world, player, 'Caves of Banishment (upper)', 'Lower Lake Serene', lambda state: not flooded.flood_lake_serene or state.has('Water Mask', player))
connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: logic.has_doublejump(state) or state.has_any({'Gas Mask', 'Talaria Attachment'} or logic.has_teleport(state), player)) connect(world, player, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: not flooded.flood_maw or state.has('Water Mask', player))
connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Caves of Banishment (upper)', 'Space time continuum', logic.has_teleport)
connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player)) connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: logic.has_doublejump(state) if not flooded.flood_maw else state.has('Water Mask', player))
connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) ) connect(world, player, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has_any({'Gas Mask', 'Talaria Attachment'}, player) )
@@ -178,7 +178,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, options: Timesp
connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft")) connect(world, player, 'Space time continuum', 'Upper Lake Serene', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneLeft"))
connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight")) connect(world, player, 'Space time continuum', 'Left Side forest Caves', lambda state: logic.can_teleport_to(state, "Past", "GateLakeSereneRight"))
connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast")) connect(world, player, 'Space time continuum', 'Refugee Camp', lambda state: logic.can_teleport_to(state, "Past", "GateAccessToPast"))
connect(world, player, 'Space time continuum', 'Castle Ramparts', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts")) connect(world, player, 'Space time continuum', 'Forest', lambda state: logic.can_teleport_to(state, "Past", "GateCastleRamparts"))
connect(world, player, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep")) connect(world, player, 'Space time continuum', 'Castle Keep', lambda state: logic.can_teleport_to(state, "Past", "GateCastleKeep"))
connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers"))
connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw"))

View File

@@ -42,6 +42,7 @@ class TimespinnerWorld(World):
topology_present = True topology_present = True
web = TimespinnerWebWorld() web = TimespinnerWebWorld()
required_client_version = (0, 4, 2) required_client_version = (0, 4, 2)
ut_can_gen_without_yaml = True
item_name_to_id = {name: data.code for name, data in item_table.items()} item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)} location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)}

View File

@@ -56,18 +56,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
for portal1, portal2 in portal_pairs.items(): for portal1, portal2 in portal_pairs.items():
if portal1.scene_destination() == portal_sd: if portal1.scene_destination() == portal_sd:
return portal1.name, get_portal_outlet_region(portal2, world) return portal1.name, get_portal_outlet_region(portal2, world)
if portal2.scene_destination() == portal_sd: if portal2.scene_destination() == portal_sd and not (options.decoupled and options.entrance_rando):
return portal2.name, get_portal_outlet_region(portal1, world) return portal2.name, get_portal_outlet_region(portal1, world)
raise Exception("No matches found in get_portal_info") raise Exception(f"No matches found in get_portal_info for {portal_sd}")
# input scene destination tag, returns paired portal's name and region # input scene destination tag, returns paired portal's name and region
def get_paired_portal(portal_sd: str) -> Tuple[str, str]: def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
for portal1, portal2 in portal_pairs.items(): for portal1, portal2 in portal_pairs.items():
if portal1.scene_destination() == portal_sd: if portal1.scene_destination() == portal_sd:
return portal2.name, portal2.region return portal2.name, portal2.region
if portal2.scene_destination() == portal_sd: if portal2.scene_destination() == portal_sd and not (options.decoupled and options.entrance_rando):
return portal1.name, portal1.region return portal1.name, portal1.region
raise Exception("no matches found in get_paired_portal") raise Exception(f"No matches found in get_paired_portal for {portal_sd}")
regions["Menu"].connect( regions["Menu"].connect(
connecting_region=regions["Overworld"]) connecting_region=regions["Overworld"])

View File

@@ -755,6 +755,53 @@ class TWWOptions(PerGameCommonOptions):
remove_music: RemoveMusic remove_music: RemoveMusic
death_link: DeathLink death_link: DeathLink
def get_slot_data_dict(self) -> dict[str, Any]:
"""
Returns a dictionary of option name to value to be placed in
the slot data network package.
:return: Dictionary of option name to value for the slot data.
"""
return self.as_dict(
"progression_dungeons",
"progression_tingle_chests",
"progression_dungeon_secrets",
"progression_puzzle_secret_caves",
"progression_combat_secret_caves",
"progression_savage_labyrinth",
"progression_great_fairies",
"progression_short_sidequests",
"progression_long_sidequests",
"progression_spoils_trading",
"progression_minigames",
"progression_battlesquid",
"progression_free_gifts",
"progression_mail",
"progression_platforms_rafts",
"progression_submarines",
"progression_eye_reef_chests",
"progression_big_octos_gunboats",
"progression_triforce_charts",
"progression_treasure_charts",
"progression_expensive_purchases",
"progression_island_puzzles",
"progression_misc",
"sword_mode",
"required_bosses",
"logic_obscurity",
"logic_precision",
"enable_tuner_logic",
"randomize_dungeon_entrances",
"randomize_secret_cave_entrances",
"randomize_miniboss_entrances",
"randomize_boss_entrances",
"randomize_secret_cave_inner_entrances",
"randomize_fairy_fountain_entrances",
"swift_sail",
"skip_rematch_bosses",
"remove_music",
)
def get_output_dict(self) -> dict[str, Any]: def get_output_dict(self) -> dict[str, Any]:
""" """
Returns a dictionary of option name to value to be placed in Returns a dictionary of option name to value to be placed in

View File

@@ -11,7 +11,7 @@ from BaseClasses import ItemClassification as IC
from BaseClasses import MultiWorld, Region, Tutorial from BaseClasses import MultiWorld, Region, Tutorial
from Options import Toggle from Options import Toggle
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from worlds.Files import APContainer, AutoPatchRegister from worlds.Files import APPlayerContainer
from worlds.generic.Rules import add_item_rule from worlds.generic.Rules import add_item_rule
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, icon_paths, launch_subprocess from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, icon_paths, launch_subprocess
@@ -51,7 +51,7 @@ components.append(
icon_paths["The Wind Waker"] = "ap:worlds.tww/assets/icon.png" icon_paths["The Wind Waker"] = "ap:worlds.tww/assets/icon.png"
class TWWContainer(APContainer, metaclass=AutoPatchRegister): class TWWContainer(APPlayerContainer):
""" """
This class defines the container file for The Wind Waker. This class defines the container file for The Wind Waker.
""" """
@@ -586,7 +586,7 @@ class TWWWorld(World):
:return: A dictionary to be sent to the client when it connects to the server. :return: A dictionary to be sent to the client when it connects to the server.
""" """
slot_data = self.options.as_dict(*self.options_dataclass.type_hints) slot_data = self.options.get_slot_data_dict()
# Add entrances to `slot_data`. This is the same data that is written to the .aptww file. # Add entrances to `slot_data`. This is the same data that is written to the .aptww file.
entrances = { entrances = {

View File

@@ -117,7 +117,8 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
world.filler_pool = filler_pool world.filler_pool = filler_pool
# Add filler items to place into excluded locations. # Add filler items to place into excluded locations.
pool.extend([world.get_filler_item_name() for _ in world.options.exclude_locations]) excluded_locations = world.progress_locations.intersection(world.options.exclude_locations)
pool.extend([world.get_filler_item_name() for _ in excluded_locations])
# The remaining of items left to place should be the same as the number of non-excluded locations in the world. # The remaining of items left to place should be the same as the number of non-excluded locations in the world.
nonexcluded_locations = [ nonexcluded_locations = [