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:
contents: read
pull-requests: write
env:
GH_REPO: ${{ github.repository }}
jobs:
labeler:

View File

@@ -937,13 +937,16 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo
count = block.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):
count = {"min": count, "max": count}
if "min" not in count:
count["min"] = 0
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
plando_blocks[player].append(new_block)

View File

@@ -11,6 +11,7 @@ Additional components can be added to worlds.LauncherComponents.components.
import argparse
import logging
import multiprocessing
import os
import shlex
import subprocess
import sys
@@ -41,13 +42,17 @@ def open_host_yaml():
if is_linux:
exe = which('sensible-editor') or which('gedit') or \
which('xdg-open') or which('gnome-open') or which('kde-open')
subprocess.Popen([exe, file])
elif is_macos:
exe = which("open")
subprocess.Popen([exe, file])
else:
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():
suffixes = []
@@ -92,7 +97,11 @@ def open_folder(folder_path):
return
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:
logging.warning(f"No file browser available to open {folder_path}")
@@ -104,14 +113,21 @@ def update_settings():
components.extend([
# Functions
Component("Open host.yaml", func=open_host_yaml),
Component("Open Patch", func=open_patch),
Component("Generate Template Options", func=generate_yamls),
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("Open host.yaml", func=open_host_yaml,
description="Open the host.yaml file to change settings for generation, games, and more."),
Component("Open Patch", func=open_patch,
description="Open a patch file, downloaded from the room page or provided by the host."),
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",
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files),
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4"),
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):
if in_terminal:
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
elif is_linux:
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)}")
locations = item.get("locations", [])
if not locations:
locations = item.get("location", ["Everywhere"])
locations = item.get("location", [])
if locations:
count = 1
else:
locations = ["Everywhere"]
if isinstance(locations, str):
locations = [locations]
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
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."
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
@@ -708,25 +713,30 @@ def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *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 = "") \
-> typing.Optional[str]:
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:
# prefer native dialog
from shutil import which
kdialog = which("kdialog")
if kdialog:
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")
if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
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
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 run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_linux:
# prefer native dialog
from shutil import which
kdialog = which("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 ".")
zenity = which("zenity")
if zenity:
z_filters = ("--directory",)
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
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 run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_kivy_running():
from kvui import MessageBox
MessageBox(title, text, error).open()
@@ -814,10 +818,10 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
from shutil import which
kdialog = which("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")
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:
import ctypes

View File

@@ -1,4 +1,4 @@
flask>=3.1.0
flask>=3.1.1
werkzeug>=3.1.3
pony>=0.7.19
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
elif handler:
data = zfile.open(file, "r").read()
patch = handler(BytesIO(data))
patch.read()
files[patch.player] = data
with zipfile.ZipFile(BytesIO(data)) as container:
player = json.loads(container.open("archipelago.json").read())["player"]
files[player] = data
# Spoiler
elif file.filename.endswith(".txt"):

View File

@@ -365,18 +365,14 @@ request_handlers = {
["PREFERRED_CORES"] = function (req)
local res = {}
local preferred_cores = client.getconfig().PreferredCores
local systems_enumerator = preferred_cores.Keys:GetEnumerator()
res["type"] = "PREFERRED_CORES_RESPONSE"
res["value"] = {}
res["value"]["NES"] = preferred_cores.NES
res["value"]["SNES"] = preferred_cores.SNES
res["value"]["GB"] = preferred_cores.GB
res["value"]["GBC"] = preferred_cores.GBC
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
while systems_enumerator:MoveNext() do
res["value"][systems_enumerator.Current] = preferred_cores[systems_enumerator.Current]
end
return res
end,

View File

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

View File

@@ -210,10 +210,14 @@ components: List[Component] = [
Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
# Core
Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True,
file_identifier=SuffixIdentifier('.archipelago', '.zip')),
Component('Generate', 'Generate', cli=True),
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld")),
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient),
file_identifier=SuffixIdentifier('.archipelago', '.zip'),
description="Host a generated multiworld on your computer."),
Component('Generate', 'Generate', cli=True,
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',
file_identifier=SuffixIdentifier('.apladx')),
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,
file_identifier=SuffixIdentifier())
file_identifier=SuffixIdentifier(),
description="Open the BizHawk client, to play games using the Bizhawk emulator.")
components.append(component)

View File

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

View File

@@ -2893,3 +2893,18 @@ dog_bite_ice_trap_fix = [
0x25291CB8, # ADDIU T1, T1, 0x1CB8
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):
"""
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"

View File

@@ -607,9 +607,10 @@ class CV64PatchExtensions(APPatchExtension):
rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200
rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper)
# Increase shimmy speed
# Shimmy speed increase hack
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
if options["fall_guard"]:

View File

@@ -211,7 +211,8 @@ class CVCotMWorld(World):
"ignore_cleansing": self.options.ignore_cleansing.value,
"skip_tutorials": self.options.skip_tutorials.value,
"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:
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]] = {
# 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,
"appearance": 0x01},
"Heart Vessel": {"type": 0xE4,
"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,
"appearance": 0x01},
"Max Aura": {"type": 0xE4,

View File

@@ -3,7 +3,7 @@
## Quick Links
- [Setup](/tutorial/Castlevania%20-%20Circle%20of%20the%20Moon/setup/en)
- [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)
- [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)

View File

@@ -22,7 +22,7 @@ clear it.
## 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).
## 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.
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).
2. Put the tracker pack into `packs/` in your PopTracker install.
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: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"),
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",
hidden=True), # Hidden fall
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",
"Twinkling Titanite", lizard=True),
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",
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"):
self._add_item_rule(
"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

View File

@@ -802,8 +802,10 @@ def connect_regions(world: World, level_list):
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[-1],
lambda state: (state.can_reach(LocationName.riverside_race_flag, "Location", world.player)))
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)))
world.multiworld.register_indirect_condition(world.get_location(LocationName.riverside_race_flag).parent_region,
connection)
# Cotton-Top Cove Connections
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,
lambda state: (state.has(ItemName.bowling_ball, world.player, 1)))
else:
connect(world, world.player, names, LocationName.mekanos_region, LocationName.sky_high_secret_region,
lambda state: (state.can_reach(LocationName.bleaks_house, "Location", world.player)))
connection = connect(world, world.player, names, LocationName.mekanos_region,
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_levels = [
@@ -946,3 +951,4 @@ def connect(world: World, player: int, used_names: typing.Dict[str, int], source
source_region.exits.append(connection)
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)
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:
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))
set_rule(world.get_entrance("Behind Rocks", player),
set_rule(world.get_entrance("Behind Rocks"),
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
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):

View File

@@ -69,7 +69,9 @@ class FactorioContext(CommonContext):
# updated by spinup server
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)
self.send_index: int = 0
self.rcon_client = None
@@ -82,6 +84,10 @@ class FactorioContext(CommonContext):
self.filter_item_sends: bool = filter_item_sends
self.multiplayer: bool = False # whether multiple different players have connected
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
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] "
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
def energy_link_status(self) -> str:
if not self.energy_link_increment:
@@ -311,7 +329,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
executable, "--create", savegame_name, "--preset", "archipelago"
))
factorio_process = subprocess.Popen((executable, "--start-server", savegame_name,
*(str(elem) for elem in server_args)),
*ctx.server_args),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
@@ -331,7 +349,7 @@ async def factorio_server_watcher(ctx: FactorioContext):
factorio_queue.task_done()
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)
if not ctx.server:
logger.info("Established bridge to Factorio Server. "
@@ -422,7 +440,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
executable, "--create", savegame_name
))
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,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
@@ -451,7 +469,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
"or a Factorio sharing data directories is already running. "
"Server could not start up.")
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:
raise Exception("No Archipelago mod was loaded. Aborting.")
await get_info(ctx, rcon_client)
@@ -474,9 +492,8 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
return False
async def main(args, filter_item_sends: bool, filter_bridge_chat_out: bool):
ctx = FactorioContext(args.connect, args.password, filter_item_sends, filter_bridge_chat_out)
async def main(make_context):
ctx = make_context()
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
@@ -509,38 +526,42 @@ class FactorioJSONtoTextParser(JSONtoTextParser):
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")
settings: FactorioSettings = get_settings().factorio_options
if os.path.samefile(settings.executable, sys.executable):
selected_executable = settings.executable
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
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():
def launch(*new_args: str):
import colorama
global executable, server_settings, server_args
global executable
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:
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_bridge_chat_out = bool(settings.bridge_chat_out)
@@ -554,14 +575,9 @@ def launch():
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
if server_settings and os.path.isfile(server_settings):
server_args = (
"--rcon-port", rcon_port,
"--rcon-password", rcon_password,
"--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))
asyncio.run(main(lambda: FactorioContext(
args.connect, args.password,
initial_filter_item_sends, initial_bridge_chat_out,
rcon_port, rcon_password, server_settings, rest
)))
colorama.deinit()

View File

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

View File

@@ -34,9 +34,9 @@ from .locations import (JakAndDaxterLocation,
cache_location_table,
orb_location_table)
from .regions import create_regions
from .rules import (enforce_multiplayer_limits,
enforce_singleplayer_limits,
verify_orb_trade_amounts,
from .rules import (enforce_mp_absolute_limits,
enforce_mp_friendly_limits,
enforce_sp_limits,
set_orb_trade_rule)
from .locs import (cell_locations as cells,
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.lava_tube_cell_count.value = self.power_cell_thresholds[2]
# Store this for remove function.
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds]
# 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.
# We would have done this earlier, but we needed to sort the power cell thresholds first. Don't worry, we'll
# come back to them.
enforce_friendly_options = self.settings.enforce_friendly_options
if enforce_friendly_options:
if self.multiworld.players > 1:
enforce_multiplayer_limits(self)
if self.multiworld.players == 1:
# For singleplayer games, always enforce/clamp the cell counts to valid values.
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:
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,
# 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.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.
if self.options.enable_orbsanity == options.EnableOrbsanity.option_per_level:
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 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)
- [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 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)
@@ -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
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
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
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
editing the `host.yaml`. In the Archipelago Launcher, click `Open host.yaml`, then search for `jakanddaxter_options`,
then search for `enforce_friendly_options`, then change this value from `true` to `false`. Disabling this allows for
more disruptive and challenging options, but it may cause seed generation to fail. **Use at your own risk!**
You can override **some**, not all, of those limits by editing the `host.yaml`. In the Archipelago Launcher, click
`Open host.yaml`, then search for `jakanddaxter_options`, then search for `enforce_friendly_options`, then change this
value from `true` to `false`. You can then generate a seed locally, and upload that to the Archipelago website to host
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
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.*
- [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.
@@ -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
- 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:
- 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.

View File

@@ -1,22 +1,78 @@
from dataclasses import dataclass
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
class StaticGetter:
def __init__(self, func):
self.fget = func
class readonly_classproperty:
"""This decorator is used for getting friendly or unfriendly range_end values for options like FireCanyonCellCount
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):
return self.fget(owner)
return self.getter(owner)
@StaticGetter
@readonly_classproperty
def determine_range_end(cls) -> int:
from . import JakAndDaxterWorld
enforce_friendly_options = JakAndDaxterWorld.settings.enforce_friendly_options
return cls.friendly_maximum if enforce_friendly_options else cls.absolute_maximum
from . import JakAndDaxterWorld # Avoid circular imports.
friendly = JakAndDaxterWorld.settings.enforce_friendly_options
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):
@@ -44,12 +100,13 @@ class EnableOrbsanity(Choice):
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."
There are 2000 orbs in the game, so your bundle size must be a factor of 2000.
Multiplayer Minimum: 10
Multiplayer Maximum: 200"""
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 = "Global Orbsanity Bundle Size"
option_1_orb = 1
option_2_orbs = 2
@@ -75,12 +132,33 @@ class GlobalOrbsanityBundleSize(Choice):
friendly_maximum = 200
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."
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"
option_1_orb = 1
option_2_orbs = 2
@@ -91,6 +169,18 @@ class PerLevelOrbsanityBundleSize(Choice):
friendly_minimum = 10
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):
"""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_mountain_pass = 87
option_cross_lava_tube = 89
option_defeat_dark_eco_plant = 6
# option_defeat_dark_eco_plant = 6
option_defeat_klaww = 86
option_defeat_gol_and_maia = 112
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:
multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player)
elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
# elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
# multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww:
multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player)

View File

@@ -1,3 +1,5 @@
import logging
import math
import typing
from BaseClasses import CollectionState
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)
def enforce_multiplayer_limits(world: "JakAndDaxterWorld"):
def clamp_cell_limits(world: "JakAndDaxterWorld") -> str:
options = world.options
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:
old_value = options.fire_canyon_cell_count.value
options.fire_canyon_cell_count.value = FireCanyonCellCount.friendly_maximum
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")
f"{FireCanyonCellCount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
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" "
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")
f"{MountainPassCellCount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
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" "
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")
f"{LavaTubeCellCount.friendly_maximum} (was {old_value}), "
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:
old_value = options.citizen_orb_trade_amount.value
options.citizen_orb_trade_amount.value = CitizenOrbTradeAmount.friendly_maximum
friendly_message += (f" "
f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
f"{CitizenOrbTradeAmount.friendly_maximum} (currently "
f"{options.citizen_orb_trade_amount.value}).\n")
f"{CitizenOrbTradeAmount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
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" "
f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
f"{OracleOrbTradeAmount.friendly_maximum} (currently "
f"{options.oracle_orb_trade_amount.value}).\n")
f"{OracleOrbTradeAmount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
friendly_message += clamp_cell_limits(world)
friendly_message += clamp_trade_total_limits(world)
if friendly_message != "":
raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n"
f"Please adjust the following Options for a multiplayer game. \n"
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 the seed generator's host.yaml to false. "
f"(Use at your own risk!)")
logging.warning(f"{world.player_name}: Your options have been modified to avoid disrupting the multiworld.\n"
f"{friendly_message}"
f"You can access more advanced options by setting 'enforce_friendly_options' in the seed "
f"generator's host.yaml to false and generating locally. (Use at your own risk!)")
def enforce_singleplayer_limits(world: "JakAndDaxterWorld"):
options = world.options
def enforce_mp_absolute_limits(world: "JakAndDaxterWorld"):
friendly_message = ""
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
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")
friendly_message += clamp_trade_total_limits(world)
if friendly_message != "":
raise OptionError(f"The options you have chosen may result in seed generation failures. \n"
f"Please adjust the following Options for a singleplayer game. \n"
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!)")
logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
f"{friendly_message}")
def verify_orb_trade_amounts(world: "JakAndDaxterWorld"):
def enforce_sp_limits(world: "JakAndDaxterWorld"):
friendly_message = ""
if world.total_trade_orbs > 2000:
raise OptionError(f"{world.player_name}: Required number of orbs for all trades ({world.total_trade_orbs}) "
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} "
f"or {world.options.oracle_orb_trade_amount.display_name}.")
friendly_message += clamp_cell_limits(world)
friendly_message += clamp_trade_total_limits(world)
if friendly_message != "":
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):
options = {
"enable_orbsanity": 2,
"global_orbsanity_bundle_size": 5,
"global_orbsanity_bundle_size": 10,
"citizen_orb_trade_amount": 0,
"oracle_orb_trade_amount": 0
}
def test_orb_items_are_filler(self):
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):
self.assertTrue(self.multiworld
@@ -22,15 +22,15 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
class TradesCostEverythingTest(JakAndDaxterTestBase):
options = {
"enable_orbsanity": 2,
"global_orbsanity_bundle_size": 5,
"global_orbsanity_bundle_size": 10,
"citizen_orb_trade_amount": 120,
"oracle_orb_trade_amount": 150
}
def test_orb_items_are_progression(self):
self.collect_all_but("")
self.assertIn("5 Precursor Orbs", self.multiworld.state.prog_items[self.player])
self.assertEqual(396, self.multiworld.state.prog_items[self.player]["5 Precursor Orbs"])
self.assertIn("10 Precursor Orbs", self.multiworld.state.prog_items[self.player])
self.assertEqual(198, self.multiworld.state.prog_items[self.player]["10 Precursor Orbs"])
def test_trades_are_accessible(self):
self.collect_all_but("")

View File

@@ -34,7 +34,7 @@ class KH2Context(CommonContext):
self.growthlevel = None
self.kh2connected = 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.location_name_to_data = {name: data for name, data, in all_locations.items()}
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.sending = []
self.slot_name = None
self.disconnect_from_server = False
# list used to keep track of locations+items player has. Used for disoneccting
self.kh2_seed_save_cache = {
"itemIndex": -1,
@@ -185,11 +187,20 @@ class KH2Context(CommonContext):
if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested)
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):
self.kh2connected = False
self.serverconneced = False
self.serverconnected = False
if self.kh2seedname is not None and self.auth is not None:
with open(self.kh2_seed_save_path_join, 'w') as f:
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):
self.kh2connected = False
self.serverconneced = False
self.serverconnected = False
self.locations_checked = []
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(self.kh2_seed_save_path_join, 'w') as f:
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):
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_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
for item in args['items']:
asyncio.create_task(self.give_item(item.item, item.location))
@@ -370,12 +390,14 @@ class KH2Context(CommonContext):
if not self.kh2:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
self.get_addresses()
#
except Exception as e:
if self.kh2connected:
self.kh2connected = False
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):
self.kh2_loc_name_to_id = loc_to_id
@@ -493,23 +515,38 @@ class KH2Context(CommonContext):
async def give_item(self, item, location):
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:
await asyncio.sleep(0.5)
itemname = self.lookup_id_to_item[item]
itemdata = self.item_name_to_data[itemname]
# itemcode = self.kh2_item_name_to_id[itemname]
if itemdata.ability:
if location in self.all_weapon_location_id:
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"}:
self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1
return
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]:
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]) < \
self.AbilityQuantityDict[itemname]:
if itemname in self.sora_ability_set:
@@ -528,18 +565,21 @@ class KH2Context(CommonContext):
if ability_slot in self.front_ability_slots:
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}:
# if memaddr is in a bitmask location in memory
if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]:
self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname)
# if itemdata in {magic}
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
# equipment is a list instead of dict because you can only have 1 currently
elif itemname in self.all_equipment:
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:
if itemname in self.keyblade_set:
self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname)
@@ -548,9 +588,11 @@ class KH2Context(CommonContext):
else:
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:
self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1
else:
# "normal" items. They have a unique byte reserved for how many they have
if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]:
self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1
else:
@@ -930,7 +972,7 @@ def finishedGame(ctx: KH2Context):
async def kh2_watcher(ctx: KH2Context):
while not ctx.exit_event.is_set():
try:
if ctx.kh2connected and ctx.serverconneced:
if ctx.kh2connected and ctx.serverconnected:
ctx.sending = []
await asyncio.create_task(ctx.checkWorldLocations())
await asyncio.create_task(ctx.checkLevels())
@@ -944,13 +986,19 @@ async def kh2_watcher(ctx: KH2Context):
if ctx.sending:
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced:
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")
elif not ctx.kh2connected and ctx.serverconnected:
logger.info("Game Connection lost. trying to reconnect.")
ctx.kh2 = None
while not ctx.kh2connected and ctx.serverconneced:
await asyncio.sleep(15)
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
ctx.get_addresses()
while not ctx.kh2connected and ctx.serverconnected:
try:
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
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:
if ctx.kh2connected:
ctx.kh2connected = False

View File

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

View File

@@ -277,9 +277,7 @@ class KH2World(World):
if self.options.FillerItemsLocal:
for item in filler_items:
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:
self.total_locations -= 6
@@ -400,6 +398,8 @@ class KH2World(World):
# plando goofy get bonuses
goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in
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:
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)
location.place_locked_item(random_ability)
self.donald_weapon_abilities.remove(random_ability)
# if option is turned off
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_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:
random_ability = self.random.choice(self.donald_get_bonus_abilities)
location.place_locked_item(random_ability)

View File

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

Binary file not shown.

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ def is_acceptable_pilgrimage_entrance(entrance_type: EntranceType, world: "Lingo
allowed_entrance_types = EntranceType.NORMAL
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:
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)
# 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:
# 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}")
else:
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:
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:
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
elif source_room == "Crossroads" and room_name == "Roof":
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"]:
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
# 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 'yaml'

View File

@@ -38,6 +38,7 @@ AP_JUNK = 0xD5
class OoTContainer(APPatch):
game: str = 'Ocarina of Time'
patch_file_ending = ".apz5"
def __init__(self, patch_data: bytes, base_path: str, output_directory: 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
- 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.
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
den obrigen Link gefunden werden.
- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert
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

View File

@@ -7,11 +7,11 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
## Required Software
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.3.1 and later are supported. Version 2.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.
- 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).
- An Ocarina of Time v1.0 ROM.
- A US Ocarina of Time v1.0 ROM.
## Configuring BizHawk

View File

@@ -7,12 +7,12 @@ Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windo
## Logiciel requis
- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.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.
- 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)
(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

View File

@@ -40,6 +40,8 @@ class RaftWorld(World):
options_dataclass = RaftOptions
options: RaftOptions
extraItemNamePool: list[str] | None = None
required_client_version = (0, 3, 4)
def create_items(self):
@@ -52,52 +54,52 @@ class RaftWorld(World):
pool = []
frequencyItems = []
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"]:
frequencyItems.append(raft_item)
else:
pool.append(raft_item)
extraItemNamePool = []
self.extraItemNamePool = []
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
dupeItemPool = item_table.copy()
# Remove frequencies if necessary
if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations
# 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
# instead add progressive-frequency as its own item a smaller amount of times to prevent
# flooding the duplicate item pool with them.
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
for _ in range(2):
# Progressives are not in item_pool, need to create faux item for duplicate item pool
# This can still be filtered out later by duplicate_items setting
dupeItemPool.append({ "name": "progressive-frequency", "progression": True }) # Progressive frequencies need to be included
# Always remove non-progressive Frequency items
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
# Remove progression or non-progression items if necessary
if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
dupeItemPool = list(dupeItemPool)
# Finally, add items as necessary
if len(dupeItemPool) > 0:
for item in dupeItemPool:
extraItemNamePool.append(item["name"])
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):
self.extraItemNamePool.append(createResourcePackName(i, packItem))
if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items
dupeItemPool = item_table.copy()
# Remove frequencies if necessary
if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations
# 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
# instead add progressive-frequency as its own item a smaller amount of times to prevent
# flooding the duplicate item pool with them.
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
for _ in range(2):
# Progressives are not in item_pool, need to create faux item for duplicate item pool
# This can still be filtered out later by duplicate_items setting
dupeItemPool.append({ "name": "progressive-frequency", "progression": True }) # Progressive frequencies need to be included
# Always remove non-progressive Frequency items
dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"])
# Remove progression or non-progression items if necessary
if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True)
elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only
dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False)
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):
for randomItem in self.random.choices(extraItemNamePool, k=extras):
raft_item = self.create_item_replaceAsNecessary(randomItem)
pool.append(raft_item)
assert self.extraItemNamePool, f"Don't know what extra items to create for {self.player_name}."
for randomItem in self.random.choices(self.extraItemNamePool, k=extras):
raft_item = self.create_item(randomItem)
pool.append(raft_item)
self.multiworld.itempool += pool
@@ -108,19 +110,35 @@ class RaftWorld(World):
if 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):
set_rules(self.multiworld, self.player)
def create_regions(self):
create_regions(self.multiworld, self.player)
def create_item_replaceAsNecessary(self, name: str) -> Item:
isFrequency = "Frequency" in name
shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive)
or (not isFrequency and self.options.progressive_items))
if shouldUseProgressive and name in progressive_table:
name = progressive_table[name]
return self.create_item(name)
def replace_item_name_as_necessary(self, name: str) -> str:
if name not in progressive_table:
return name
if "Frequency" in name:
if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive:
return progressive_table[name]
elif self.options.progressive_items:
return progressive_table[name]
return name
def create_item(self, name: str) -> Item:
item = lookup_name_to_item[name]

View File

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

View File

@@ -130,9 +130,7 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor
### Open the client
Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser. Do not
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.
Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser.
The client should automatically connect to SNI, the "SNES" status should change to green.

View File

@@ -1,9 +1,10 @@
import logging
import typing
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 worlds.AutoWorld import World, WebWorld
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.presets import sv_options_presets
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 .stardew_rule import True_, StardewRule, HasProgressionPercent
from .strings.ap_names.event_names import Event
@@ -124,18 +125,13 @@ class StardewValleyWorld(World):
self.content = create_content(self.options)
def create_regions(self):
def create_region(name: str, exits: Iterable[str]) -> Region:
region = Region(name, self.player, self.multiworld)
region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits]
return region
def create_region(name: str) -> Region:
return Region(name, self.player, self.multiworld)
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.modified_bundles = get_all_bundles(self.random,
self.logic,
self.content,
self.options)
self.modified_bundles = get_all_bundles(self.random, self.logic, self.content, self.options)
def add_location(name: str, code: Optional[int], region: str):
region: Region = world_regions[region]
@@ -308,6 +304,11 @@ class StardewValleyWorld(World):
def 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):
pass

View File

@@ -24,6 +24,9 @@ from ...strings.skill_names import Skill
from ...strings.tool_names import Tool, ToolMaterial
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):
@@ -67,6 +70,10 @@ class SVEContentPack(ContentPack):
content.game_items.pop(SVESeed.slime)
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(
ModNames.sve,
@@ -80,8 +87,9 @@ register_mod_content_pack(SVEContentPack(
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.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),),
SVEMeal.grampleton_orange_chicken: (
ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650,
shop_region=Region.saloon,
other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),),
ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),),
ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),),
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.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,),
other_requirements=(
CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),),
other_requirements=(CombatRequirement(Performance.galaxy),
ToolRequirement(Tool.axe, ToolMaterial.iron))),),
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),)),),
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,)),),
ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),),
ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,),
other_requirements=(
CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),),
other_requirements=(CombatRequirement(Performance.galaxy),
SkillRequirement(Skill.combat, 10),
YearRequirement(3),)),),
SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),),
# Fable Reef
@@ -207,7 +216,6 @@ register_mod_content_pack(SVEContentPack(
villagers_data.scarlett,
villagers_data.susan,
villagers_data.morris,
# The wizard leaves his tower on sunday, for like 1 hour... Good enough for entrance rando!
override(villagers_data.wizard, locations=(Region.wizard_tower, Region.forest), bachelor=True, mod_name=ModNames.sve),
override(villagers_data.wizard, 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})
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})
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})

View File

@@ -1129,8 +1129,8 @@ id,region,name,tags,mod_name
2204,Leo's Hut,Leo's Parrot,"GINGER_ISLAND,WALNUT_PURCHASE",
2205,Island South,Island West Turtle,"GINGER_ISLAND,WALNUT_PURCHASE",
2206,Island West,Island Farmhouse,"GINGER_ISLAND,WALNUT_PURCHASE",
2207,Island Farmhouse,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
2208,Island Farmhouse,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
2207,Island West,Island Mailbox,"GINGER_ISLAND,WALNUT_PURCHASE",
2208,Island West,Farm Obelisk,"GINGER_ISLAND,WALNUT_PURCHASE",
2209,Island North,Dig Site Bridge,"GINGER_ISLAND,WALNUT_PURCHASE",
2210,Island North,Island Trader,"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)
# 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()),
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) &
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),

View File

@@ -1,23 +1,23 @@
from typing import Tuple, Union
from typing import Tuple
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from ..options import EntranceRandomization
from ..stardew_rule import StardewRule, Reach, false_, true_
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,
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,
Region.vault, Region.bulletin_board, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
Region.elliott_house, Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent, Region.pierre_store,
Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house, Region.haley_house,
Region.sam_house, Region.jojamart, Region.fish_shop}
always_accessible_regions_with_non_progression_er = {*main_outside_area, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent,
Region.pierre_store, Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house,
Region.haley_house, 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,
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: main_outside_area,
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 ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem
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.wallet_item_names import Wallet

View File

@@ -1,15 +1,14 @@
from typing import Dict, List
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, \
JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance
from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \
AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion
deep_woods_regions = [
RegionData(Region.farm, [DeepWoodsEntrance.use_woods_obelisk]),
RegionData(DeepWoodsRegion.woods_obelisk_menu, [DeepWoodsEntrance.deep_woods_depth_1,
RegionData(Region.farm, (DeepWoodsEntrance.use_woods_obelisk,)),
RegionData(DeepWoodsRegion.woods_obelisk_menu, (DeepWoodsEntrance.deep_woods_depth_1,
DeepWoodsEntrance.deep_woods_depth_10,
DeepWoodsEntrance.deep_woods_depth_20,
DeepWoodsEntrance.deep_woods_depth_30,
@@ -19,9 +18,9 @@ deep_woods_regions = [
DeepWoodsEntrance.deep_woods_depth_70,
DeepWoodsEntrance.deep_woods_depth_80,
DeepWoodsEntrance.deep_woods_depth_90,
DeepWoodsEntrance.deep_woods_depth_100]),
RegionData(Region.secret_woods, [DeepWoodsEntrance.secret_woods_to_deep_woods]),
RegionData(DeepWoodsRegion.main_lichtung, [DeepWoodsEntrance.deep_woods_house]),
DeepWoodsEntrance.deep_woods_depth_100)),
RegionData(Region.secret_woods, (DeepWoodsEntrance.secret_woods_to_deep_woods,)),
RegionData(DeepWoodsRegion.main_lichtung, (DeepWoodsEntrance.deep_woods_house,)),
RegionData(DeepWoodsRegion.abandoned_home),
RegionData(DeepWoodsRegion.floor_10),
RegionData(DeepWoodsRegion.floor_20),
@@ -32,14 +31,13 @@ deep_woods_regions = [
RegionData(DeepWoodsRegion.floor_70),
RegionData(DeepWoodsRegion.floor_80),
RegionData(DeepWoodsRegion.floor_90),
RegionData(DeepWoodsRegion.floor_100)
RegionData(DeepWoodsRegion.floor_100),
]
deep_woods_entrances = [
ConnectionData(DeepWoodsEntrance.use_woods_obelisk, DeepWoodsRegion.woods_obelisk_menu),
ConnectionData(DeepWoodsEntrance.secret_woods_to_deep_woods, DeepWoodsRegion.main_lichtung),
ConnectionData(DeepWoodsEntrance.deep_woods_house, DeepWoodsRegion.abandoned_home,
flag=RandomizationFlag.NON_PROGRESSION),
ConnectionData(DeepWoodsEntrance.deep_woods_house, DeepWoodsRegion.abandoned_home, flag=RandomizationFlag.BUILDINGS),
ConnectionData(DeepWoodsEntrance.deep_woods_depth_1, DeepWoodsRegion.main_lichtung),
ConnectionData(DeepWoodsEntrance.deep_woods_depth_10, DeepWoodsRegion.floor_10),
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_80, DeepWoodsRegion.floor_80),
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 = [
RegionData(Region.forest, [EugeneEntrance.forest_to_garden]),
RegionData(EugeneRegion.eugene_garden, [EugeneEntrance.garden_to_bedroom]),
RegionData(EugeneRegion.eugene_bedroom)
RegionData(Region.forest, (EugeneEntrance.forest_to_garden,)),
RegionData(EugeneRegion.eugene_garden, (EugeneEntrance.garden_to_bedroom,)),
RegionData(EugeneRegion.eugene_bedroom),
]
eugene_entrances = [
ConnectionData(EugeneEntrance.forest_to_garden, EugeneRegion.eugene_garden,
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 = [
RegionData(Region.pierre_store, [MagicEntrance.store_to_altar]),
RegionData(MagicRegion.altar)
RegionData(Region.pierre_store, (MagicEntrance.store_to_altar,)),
RegionData(MagicRegion.altar),
]
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 = [
RegionData(Region.museum, [JasperEntrance.museum_to_bedroom]),
RegionData(JasperRegion.jasper_bedroom)
RegionData(Region.museum, (JasperEntrance.museum_to_bedroom,)),
RegionData(JasperRegion.jasper_bedroom),
]
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 = [
RegionData(Region.forest, [AlecEntrance.forest_to_petshop]),
RegionData(AlecRegion.pet_store, [AlecEntrance.petshop_to_bedroom]),
RegionData(AlecRegion.alec_bedroom)
RegionData(Region.forest, (AlecEntrance.forest_to_petshop,)),
RegionData(AlecRegion.pet_store, (AlecEntrance.petshop_to_bedroom,)),
RegionData(AlecRegion.alec_bedroom),
]
alec_entrances = [
ConnectionData(AlecEntrance.forest_to_petshop, AlecRegion.pet_store,
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 = [
RegionData(Region.secret_woods, [YobaEntrance.secret_woods_to_clearing]),
RegionData(YobaRegion.yoba_clearing)
RegionData(Region.secret_woods, (YobaEntrance.secret_woods_to_clearing,)),
RegionData(YobaRegion.yoba_clearing),
]
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 = [
RegionData(Region.forest, [JunaEntrance.forest_to_juna_cave]),
RegionData(JunaRegion.juna_cave)
RegionData(Region.forest, (JunaEntrance.forest_to_juna_cave,)),
RegionData(JunaRegion.juna_cave),
]
juna_entrances = [
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 = [
RegionData(Region.bus_stop, [AyeishaEntrance.bus_stop_to_mail_van]),
RegionData(AyeishaRegion.mail_van)
RegionData(Region.bus_stop, (AyeishaEntrance.bus_stop_to_mail_van,)),
RegionData(AyeishaRegion.mail_van),
]
ayeisha_entrances = [
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 = [
RegionData(Region.town, [RileyEntrance.town_to_riley]),
RegionData(RileyRegion.riley_house)
RegionData(Region.town, (RileyEntrance.town_to_riley,)),
RegionData(RileyRegion.riley_house),
]
riley_entrances = [
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 = [
RegionData(Region.backwoods, [SVEEntrance.backwoods_to_grove]),
RegionData(SVERegion.enchanted_grove, [SVEEntrance.grove_to_outpost_warp, SVEEntrance.grove_to_wizard_warp,
sve_main_land_regions = [
RegionData(Region.backwoods, (SVEEntrance.backwoods_to_grove,)),
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_spring_warp, SVEEntrance.grove_to_aurora_warp]),
RegionData(SVERegion.grove_farm_warp, [SVEEntrance.farm_warp_to_farm]),
RegionData(SVERegion.grove_aurora_warp, [SVEEntrance.aurora_warp_to_aurora]),
RegionData(SVERegion.grove_guild_warp, [SVEEntrance.guild_warp_to_guild]),
RegionData(SVERegion.grove_junimo_warp, [SVEEntrance.junimo_warp_to_junimo]),
RegionData(SVERegion.grove_spring_warp, [SVEEntrance.spring_warp_to_spring]),
RegionData(SVERegion.grove_outpost_warp, [SVEEntrance.outpost_warp_to_outpost]),
RegionData(SVERegion.grove_wizard_warp, [SVEEntrance.wizard_warp_to_wizard]),
RegionData(SVERegion.galmoran_outpost, [SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop,
SVEEntrance.use_isaac_shop]),
RegionData(SVERegion.badlands_entrance, [SVEEntrance.badlands_entrance_to_badlands]),
RegionData(SVERegion.crimson_badlands, [SVEEntrance.badlands_to_cave]),
SVEEntrance.grove_to_spring_warp, SVEEntrance.grove_to_aurora_warp)),
RegionData(SVERegion.grove_farm_warp, (SVEEntrance.farm_warp_to_farm,)),
RegionData(SVERegion.grove_aurora_warp, (SVEEntrance.aurora_warp_to_aurora,)),
RegionData(SVERegion.grove_guild_warp, (SVEEntrance.guild_warp_to_guild,)),
RegionData(SVERegion.grove_junimo_warp, (SVEEntrance.junimo_warp_to_junimo,)),
RegionData(SVERegion.grove_spring_warp, (SVEEntrance.spring_warp_to_spring,)),
RegionData(SVERegion.grove_outpost_warp, (SVEEntrance.outpost_warp_to_outpost,)),
RegionData(SVERegion.grove_wizard_warp, (SVEEntrance.wizard_warp_to_wizard,)),
RegionData(SVERegion.galmoran_outpost, (SVEEntrance.outpost_to_badlands_entrance, SVEEntrance.use_alesia_shop, SVEEntrance.use_isaac_shop)),
RegionData(SVERegion.badlands_entrance, (SVEEntrance.badlands_entrance_to_badlands,)),
RegionData(SVERegion.crimson_badlands, (SVEEntrance.badlands_to_cave,)),
RegionData(SVERegion.badlands_cave),
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_interior, [SVEEntrance.grandpa_interior_to_upstairs]),
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_interior, (SVEEntrance.grandpa_interior_to_upstairs,)),
RegionData(SVERegion.grandpas_shed_upstairs),
RegionData(Region.forest,
[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_fairhaven, SVEEntrance.forest_to_west, SVEEntrance.forest_to_lost_woods,
SVEEntrance.forest_to_bmv, SVEEntrance.forest_to_marnie_shed)),
RegionData(SVERegion.marnies_shed),
RegionData(SVERegion.fairhaven_farm),
RegionData(Region.town, [SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins,
SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot]),
RegionData(SVERegion.blue_moon_vineyard, [SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach]),
RegionData(Region.town, (SVEEntrance.town_to_bmv, SVEEntrance.town_to_jenkins, SVEEntrance.town_to_bridge, SVEEntrance.town_to_plot)),
RegionData(SVERegion.blue_moon_vineyard, (SVEEntrance.bmv_to_sophia, SVEEntrance.bmv_to_beach)),
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.unclaimed_plot, [SVEEntrance.plot_to_bridge]),
RegionData(SVERegion.unclaimed_plot, (SVEEntrance.plot_to_bridge,)),
RegionData(SVERegion.shearwater),
RegionData(Region.museum, [SVEEntrance.museum_to_gunther_bedroom]),
RegionData(Region.museum, (SVEEntrance.museum_to_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(Region.mountain, [SVEEntrance.mountain_to_guild_summit]),
RegionData(SVERegion.guild_summit, [SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines,
SVEEntrance.summit_to_highlands]),
RegionData(Region.railroad, [SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station]),
RegionData(SVERegion.grampleton_station, [SVEEntrance.grampleton_station_to_grampleton_suburbs]),
RegionData(SVERegion.grampleton_suburbs, [SVEEntrance.grampleton_suburbs_to_scarlett_house]),
RegionData(Region.mountain, (SVEEntrance.mountain_to_guild_summit,)),
# These entrances are removed from the mountain region when SVE is enabled
RegionData(Region.mountain, (Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_the_mines), flag=MergeFlag.REMOVE_EXITS),
RegionData(SVERegion.guild_summit, (SVEEntrance.guild_to_interior, SVEEntrance.guild_to_mines)),
RegionData(Region.railroad, (SVEEntrance.to_susan_house, SVEEntrance.enter_summit, SVEEntrance.railroad_to_grampleton_station)),
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(Region.wizard_basement, [SVEEntrance.wizard_to_fable_reef]),
RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild], is_ginger_island=True),
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.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(Region.secret_woods, [SVEEntrance.secret_woods_to_west]),
RegionData(Region.secret_woods, (SVEEntrance.secret_woods_to_west,)),
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.lost_woods, [SVEEntrance.lost_woods_to_junimo_woods]),
RegionData(SVERegion.junimo_woods, [SVEEntrance.use_purple_junimo]),
RegionData(SVERegion.lost_woods, (SVEEntrance.lost_woods_to_junimo_woods,)),
RegionData(SVERegion.junimo_woods, (SVEEntrance.use_purple_junimo,)),
RegionData(SVERegion.purple_junimo_shop),
RegionData(SVERegion.alesia_shop),
RegionData(SVERegion.isaac_shop),
RegionData(SVERegion.summit),
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.jenkins_to_cellar, SVERegion.jenkins_cellar, flag=RandomizationFlag.BUILDINGS),
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_shed_to_town, Region.town),
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.backwoods_to_grove, SVERegion.enchanted_grove, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
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.grove_to_spring_warp, SVERegion.grove_spring_warp),
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.badlands_entrance_to_badlands, SVERegion.crimson_badlands, 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.enter_summit, SVERegion.summit, flag=RandomizationFlag.BUILDINGS),
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_purple_junimo, SVERegion.purple_junimo_shop),
ConnectionData(SVEEntrance.use_alesia_shop, SVERegion.alesia_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.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs),
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.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.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 = [
RegionData(Region.witch_hut, [AlectoEntrance.witch_hut_to_witch_attic]),
RegionData(AlectoRegion.witch_attic)
RegionData(Region.witch_hut, (AlectoEntrance.witch_hut_to_witch_attic,)),
RegionData(AlectoRegion.witch_attic),
]
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 = [
RegionData(Region.forest, [LaceyEntrance.forest_to_hat_house]),
RegionData(LaceyRegion.hat_house)
RegionData(Region.forest, (LaceyEntrance.forest_to_hat_house,)),
RegionData(LaceyRegion.hat_house),
]
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 = [
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(Region.bus_stop, (BoardingHouseEntrance.bus_stop_to_boarding_house_plateau,)),
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_abandoned_mines_entrance]),
RegionData(BoardingHouseRegion.boarding_house_first, [BoardingHouseEntrance.boarding_house_first_to_boarding_house_second]),
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_second),
RegionData(BoardingHouseRegion.buffalo_ranch),
RegionData(BoardingHouseRegion.abandoned_mines_entrance, [BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a,
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_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_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_4, [BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5]),
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.abandoned_mines_entrance, (BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a,
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_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_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_4, (BoardingHouseEntrance.abandoned_mines_4_to_abandoned_mines_5,)),
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,
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.lost_valley_ruins, [BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1,
BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2]),
RegionData(BoardingHouseRegion.lost_valley_ruins, (BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1,
BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2)),
RegionData(BoardingHouseRegion.lost_valley_minecart),
RegionData(BoardingHouseRegion.lost_valley_house_1),
RegionData(BoardingHouseRegion.lost_valley_house_2)
RegionData(BoardingHouseRegion.lost_valley_house_2),
]
boarding_house_entrances = [
@@ -351,30 +352,29 @@ boarding_house_entrances = [
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.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]] = {
ModNames.sve: [
ConnectionData(Entrance.mountain_to_the_mines, Region.mines,
flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild,
flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA),
]
vanilla_connections_to_remove_by_content_pack: dict[str, tuple[str, ...]] = {
ModNames.sve: (
Entrance.mountain_to_the_mines,
Entrance.mountain_to_adventurer_guild,
)
}
ModDataList = {
ModNames.deepwoods: ModRegionData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances),
ModNames.eugene: ModRegionData(ModNames.eugene, eugene_regions, eugene_entrances),
ModNames.jasper: ModRegionData(ModNames.jasper, jasper_regions, jasper_entrances),
ModNames.alec: ModRegionData(ModNames.alec, alec_regions, alec_entrances),
ModNames.yoba: ModRegionData(ModNames.yoba, yoba_regions, yoba_entrances),
ModNames.juna: ModRegionData(ModNames.juna, juna_regions, juna_entrances),
ModNames.magic: ModRegionData(ModNames.magic, magic_regions, magic_entrances),
ModNames.ayeisha: ModRegionData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances),
ModNames.riley: ModRegionData(ModNames.riley, riley_regions, riley_entrances),
ModNames.sve: ModRegionData(ModNames.sve, stardew_valley_expanded_regions, mandatory_sve_connections),
ModNames.alecto: ModRegionData(ModNames.alecto, alecto_regions, alecto_entrances),
ModNames.lacey: ModRegionData(ModNames.lacey, lacey_regions, lacey_entrances),
ModNames.boarding_house: ModRegionData(ModNames.boarding_house, boarding_house_regions, boarding_house_entrances),
region_data_by_content_pack = {
ModNames.deepwoods: ModRegionsData(ModNames.deepwoods, deep_woods_regions, deep_woods_entrances),
ModNames.eugene: ModRegionsData(ModNames.eugene, eugene_regions, eugene_entrances),
ModNames.jasper: ModRegionsData(ModNames.jasper, jasper_regions, jasper_entrances),
ModNames.alec: ModRegionsData(ModNames.alec, alec_regions, alec_entrances),
ModNames.yoba: ModRegionsData(ModNames.yoba, yoba_regions, yoba_entrances),
ModNames.juna: ModRegionsData(ModNames.juna, juna_regions, juna_entrances),
ModNames.magic: ModRegionsData(ModNames.magic, magic_regions, magic_entrances),
ModNames.ayeisha: ModRegionsData(ModNames.ayeisha, ayeisha_regions, ayeisha_entrances),
ModNames.riley: ModRegionsData(ModNames.riley, riley_regions, riley_entrances),
ModNames.sve: ModRegionsData(ModNames.sve, sve_main_land_regions, sve_main_land_connections),
SVE_GINGER_ISLAND_PACK: ModRegionsData(SVE_GINGER_ISLAND_PACK, sve_ginger_island_regions, sve_ginger_island_connections),
ModNames.alecto: ModRegionsData(ModNames.alecto, alecto_regions, alecto_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_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.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.town_to_sewer, logic.wallet.has_rusty_key())
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 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.rule_explain import explain
@@ -79,3 +79,13 @@ class RuleAssertMixin(TestCase):
except KeyError as e:
raise AssertionError(f"Error while checking region {region_name}: {e}"
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 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.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
from worlds.AutoWorld import call_all
@@ -179,6 +179,11 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
state = self.multiworld.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 = {}

View File

@@ -1,17 +1,13 @@
import random
from typing import ClassVar
from BaseClasses import get_seed
from test.param import classvar_matrix
from ..TestGeneration import get_all_permanent_progression_items
from ..assertion import ModAssertMixin, WorldAssertMixin
from ..bases import SVTestCase, SVTestBase, solo_multiworld
from ..options.presets import allsanity_mods_6_x_x
from ..options.utils import fill_dataclass_with_default
from ... import options, Group, create_content
from ... import options, Group
from ...mods.mod_data import ModNames
from ...options.options import all_mods
from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions
class TestCanGenerateAllsanityWithMods(WorldAssertMixin, ModAssertMixin, SVTestCase):
@@ -117,39 +113,6 @@ class TestNoGingerIslandModItemGeneration(SVTestBase):
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):
"""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.

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('The lab', 'Lab: Coffee break', 1337066),
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 (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)) ),
@@ -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 (upper)', 'Lab: Download and chest room chest', 1337074),
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 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)),
@@ -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 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 (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): 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)', '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)', '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): 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)),
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)),
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 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 - 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('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

View File

@@ -88,12 +88,15 @@ class PreCalculatedWeights:
if options.risky_warps:
past_teleportation_gates.append("GateLakeSereneLeft")
present_teleportation_gates.append("GateDadsTower")
if not is_xarion_flooded:
present_teleportation_gates.append("GateXarion")
if not is_lab_flooded:
present_teleportation_gates.append("GateLabEntrance")
# Prevent going past the lazers without a way to the past
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):
all_gates: Tuple[str, ...] = present_teleportation_gates
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 (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 (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)', '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)')
@@ -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', '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)', '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 (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) )
@@ -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', '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', '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', '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"))

View File

@@ -42,6 +42,7 @@ class TimespinnerWorld(World):
topology_present = True
web = TimespinnerWebWorld()
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()}
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():
if portal1.scene_destination() == portal_sd:
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)
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
def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
for portal1, portal2 in portal_pairs.items():
if portal1.scene_destination() == portal_sd:
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
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(
connecting_region=regions["Overworld"])

View File

@@ -755,6 +755,53 @@ class TWWOptions(PerGameCommonOptions):
remove_music: RemoveMusic
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]:
"""
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 Options import Toggle
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.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"
class TWWContainer(APContainer, metaclass=AutoPatchRegister):
class TWWContainer(APPlayerContainer):
"""
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.
"""
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.
entrances = {

View File

@@ -117,7 +117,8 @@ def get_pool_core(world: "TWWWorld") -> tuple[list[str], list[str]]:
world.filler_pool = filler_pool
# 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.
nonexcluded_locations = [