Compare commits

...

17 Commits

Author SHA1 Message Date
Exempt-Medic
c54a711c27 Update Options.py 2025-05-24 07:46:31 -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
Aaron Wagener
e82d50a3c5 The Messenger: more generous portal validation (#5011)
* The Messenger: more generous portal validation

* remove the while and just go for 20 attempts. hopefully that's enough
2025-05-24 00:13:34 +02:00
qwint
0a7aa9e3e2 Launcher: skip launcher gui when opening webhost list with no game handlers (#4888)
* calc relevant components before opening the launcher app so it can be skipped for text client only uri launches

* generically passthrough the url arg

* Apply suggestions from code review

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* flip if not else

* Update Launcher.py

* pluralize

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2025-05-24 00:02:50 +02:00
NewSoupVi
13ca134d12 Core: Fix a playthrough crash when a world uses "placement based logic" (#3915)
* Fix playthrough

* oops

* oops 2

* I don't like this

* that should do it

* Update BaseClasses.py

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* Update BaseClasses.py

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-05-23 23:47:21 +02:00
Jérémie Bolduc
8671e9a391 Stardew Valley: Make animal catalog logically year 2 (#5032) 2025-05-23 19:52:47 +00:00
BlastSlimey
a7de89f45c shapez: Add game to README and CODEOWNERS (#5034)
* Aktualisieren von README.md

* Aktualisieren von CODEOWNERS
2025-05-23 19:41:27 +00:00
black-sliver
e9f51e3302 Linux: avoid adding cwd to LD_LIBRARY_PATH (#5029)
When LD_LIBRARY_PATH is not set, the old code would also add
the current working directory to LD_LIBRARY_PATH, which is bad.
2025-05-23 19:26:37 +00:00
Aaron Wagener
5491f8c459 Core: Make get_all_state Sweeping Optional (#4828) 2025-05-22 22:28:56 -04:00
Fabian Dill
de71677208 Core: only raise min_client_version for new gens (#4896) 2025-05-22 21:30:30 +02:00
Nicholas Saylor
653ee2b625 Docs: Update Snippets to Modern Type Hints (#4987) 2025-05-22 15:00:30 -04:00
qwint
62694b1ce7 Launcher: Fix on File Drop Error Message (#5026) 2025-05-22 11:37:23 -04:00
Rosalie
9c0ad2b825 FF1: Bizhawk Client and APWorld Support (#4448)
Co-authored-by: beauxq <beauxq@yahoo.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-05-22 11:35:38 -04:00
qwint
88b529593f CommonClient: Add docs for Attributes (#5003)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-05-22 11:08:15 -04:00
agilbert1412
0351698ef7 SDV: Fixed Import bases (#5025) 2025-05-22 11:07:57 -04:00
47 changed files with 1849 additions and 2096 deletions

View File

@@ -439,7 +439,7 @@ class MultiWorld():
return self.regions.location_cache[player][location_name]
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False,
collect_pre_fill_items: bool = True) -> CollectionState:
collect_pre_fill_items: bool = True, perform_sweep: bool = True) -> CollectionState:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
return cached.copy()
@@ -453,7 +453,8 @@ class MultiWorld():
subworld = self.worlds[player]
for item in subworld.get_pre_fill_items():
subworld.collect(ret, item)
ret.sweep_for_advancements()
if perform_sweep:
ret.sweep_for_advancements()
if use_cache:
self._all_state = ret
@@ -558,7 +559,9 @@ class MultiWorld():
else:
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
def can_beat_game(self,
starting_state: Optional[CollectionState] = None,
locations: Optional[Iterable[Location]] = None) -> bool:
if starting_state:
if self.has_beaten_game(starting_state):
return True
@@ -567,7 +570,9 @@ class MultiWorld():
state = CollectionState(self)
if self.has_beaten_game(state):
return True
prog_locations = {location for location in self.get_locations() if location.item
base_locations = self.get_locations() if locations is None else locations
prog_locations = {location for location in base_locations if location.item
and location.item.advancement and location not in state.locations_checked}
while prog_locations:
@@ -1602,21 +1607,19 @@ class Spoiler:
# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later: Dict[Location, Item] = {}
required_locations = {location for sphere in collection_spheres for location in sphere}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete: Set[Location] = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
# we remove the location from required_locations to sweep from, and check if the game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
location.item.player)
old_item = location.item
location.item = None
if multiworld.can_beat_game(state_cache[num]):
required_locations.remove(location)
if multiworld.can_beat_game(state_cache[num], required_locations):
to_delete.add(location)
restore_later[location] = old_item
else:
# still required, got to keep it around
location.item = old_item
required_locations.add(location)
# cull entries in spheres for spoiler walkthrough at end
sphere -= to_delete
@@ -1633,7 +1636,7 @@ class Spoiler:
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
precollected_items.remove(item)
multiworld.state.remove(item)
if not multiworld.can_beat_game():
if not multiworld.can_beat_game(multiworld.state, required_locations):
# Add the item back into `precollected_items` and collect it into `multiworld.state`.
multiworld.push_precollected(item)
else:
@@ -1675,9 +1678,6 @@ class Spoiler:
self.create_paths(state, collection_spheres)
# repair the multiworld again
for location, item in restore_later.items():
location.item = item
for item in removed_precollected:
multiworld.push_precollected(item)

View File

@@ -266,38 +266,71 @@ class CommonContext:
last_death_link: float = time.time() # last send/received death link on AP layer
# remaining type info
slot_info: typing.Dict[int, NetworkSlot]
server_address: typing.Optional[str]
password: typing.Optional[str]
hint_cost: typing.Optional[int]
hint_points: typing.Optional[int]
player_names: typing.Dict[int, str]
slot_info: dict[int, NetworkSlot]
"""Slot Info from the server for the current connection"""
server_address: str | None
"""Autoconnect address provided by the ctx constructor"""
password: str | None
"""Password used for Connecting, expected by server_auth"""
hint_cost: int | None
"""Current Hint Cost per Hint from the server"""
hint_points: int | None
"""Current avaliable Hint Points from the server"""
player_names: dict[int, str]
"""Current lookup of slot number to player display name from server (includes aliases)"""
finished_game: bool
"""
Bool to signal that status should be updated to Goal after reconnecting
to be used to ensure that a StatusUpdate packet does not get lost when disconnected
"""
ready: bool
team: typing.Optional[int]
slot: typing.Optional[int]
auth: typing.Optional[str]
seed_name: typing.Optional[str]
"""Bool to keep track of state for the /ready command"""
team: int | None
"""Team number of currently connected slot"""
slot: int | None
"""Slot number of currently connected slot"""
auth: str | None
"""Name used in Connect packet"""
seed_name: str | None
"""Seed name that will be validated on opening a socket if present"""
# locations
locations_checked: typing.Set[int] # local state
locations_scouted: typing.Set[int]
items_received: typing.List[NetworkItem]
missing_locations: typing.Set[int] # server state
checked_locations: typing.Set[int] # server state
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
locations_info: typing.Dict[int, NetworkItem]
locations_checked: set[int]
"""
Local container of location ids checked to signal that LocationChecks should be resent after reconnecting
to be used to ensure that a LocationChecks packet does not get lost when disconnected
"""
locations_scouted: set[int]
"""
Local container of location ids scouted to signal that LocationScouts should be resent after reconnecting
to be used to ensure that a LocationScouts packet does not get lost when disconnected
"""
items_received: list[NetworkItem]
"""List of NetworkItems recieved from the server"""
missing_locations: set[int]
"""Container of Locations that are unchecked per server state"""
checked_locations: set[int]
"""Container of Locations that are checked per server state"""
server_locations: set[int]
"""Container of Locations that exist per server state; a combination between missing and checked locations"""
locations_info: dict[int, NetworkItem]
"""Dict of location id: NetworkItem info from LocationScouts request"""
# data storage
stored_data: typing.Dict[str, typing.Any]
stored_data_notification_keys: typing.Set[str]
stored_data: dict[str, typing.Any]
"""
Data Storage values by key that were retrieved from the server
any keys subscribed to with SetNotify will be kept up to date
"""
stored_data_notification_keys: set[str]
"""Current container of watched Data Storage keys, managed by ctx.set_notify"""
# internals
# current message box through kvui
_messagebox: typing.Optional["kvui.MessageBox"] = None
# message box reporting a loss of connection
"""Current message box through kvui"""
_messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None
"""Message box reporting a loss of connection"""
def __init__(self, server_address: typing.Optional[str] = None, password: typing.Optional[str] = None) -> None:
# server state

View File

@@ -1,267 +0,0 @@
import asyncio
import copy
import json
import time
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_ff1.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_ff1.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_ff1.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
class FF1CommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_nes(self):
"""Check NES Connection State"""
if isinstance(self.ctx, FF1Context):
logger.info(f"NES Status: {self.ctx.nes_status}")
def _cmd_toggle_msgs(self):
"""Toggle displaying messages in EmuHawk"""
global DISPLAY_MSGS
DISPLAY_MSGS = not DISPLAY_MSGS
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
class FF1Context(CommonContext):
command_processor = FF1CommandProcessor
game = 'Final Fantasy'
items_handling = 0b111 # full remote
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.nes_streams: (StreamReader, StreamWriter) = None
self.nes_sync_task = None
self.messages = {}
self.locations_array = None
self.nes_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FF1Context, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to NES to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[time.time(), msg_id] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
async_start(parse_locations(self.locations_array, self, True))
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
def on_print_json(self, args: dict):
if self.ui:
self.ui.print_json(copy.deepcopy(args["data"]))
else:
text = self.jsontotextparser(copy.deepcopy(args["data"]))
logger.info(text)
relevant = args.get("type", None) in {"Hint", "ItemSend"}
if relevant:
item = args["item"]
# goes to this world
if self.slot_concerns_self(args["receiving"]):
relevant = True
# found in this world
elif self.slot_concerns_self(item.player):
relevant = True
# not related
else:
relevant = False
if relevant:
item = args["item"]
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
self._set_message(msg, item.item)
def run_gui(self):
from kvui import GameManager
class FF1Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Final Fantasy 1 Client"
self.ui = FF1Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: FF1Context):
current_time = time.time()
return json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10}
}
)
async def parse_locations(locations_array: List[int], ctx: FF1Context, force: bool):
if locations_array == ctx.locations_array and not force:
return
else:
# print("New values")
ctx.locations_array = locations_array
locations_checked = []
if len(locations_array) > 0xFE and locations_array[0xFE] & 0x02 != 0 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
for location in ctx.missing_locations:
# index will be - 0x100 or 0x200
index = location
if location < 0x200:
# Location is a chest
index -= 0x100
flag = 0x04
else:
# Location is an NPC
index -= 0x200
flag = 0x02
# print(f"Location: {ctx.location_names[location]}")
# print(f"Index: {str(hex(index))}")
# print(f"value: {locations_array[index] & flag != 0}")
if locations_array[index] & flag != 0:
locations_checked.append(location)
if locations_checked:
# print([ctx.location_names[location] for location in locations_checked])
await ctx.send_msgs([
{"cmd": "LocationChecks",
"locations": locations_checked}
])
async def nes_sync_task(ctx: FF1Context):
logger.info("Starting nes connector. Use /nes for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.nes_streams:
(reader, writer) = ctx.nes_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
# print(data_decoded)
if ctx.game is not None and 'locations' in data_decoded:
# Not just a keep alive ping, parse
async_start(parse_locations(data_decoded['locations'], ctx, False))
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
"the ROM using the same link but adding your slot name")
if ctx.awaiting_rom:
await ctx.server_auth(False)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.nes_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.nes_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.nes_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.nes_streams = None
if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to NES")
ctx.nes_status = CONNECTION_CONNECTED_STATUS
else:
ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.nes_status = error_status
logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
else:
try:
logger.debug("Attempting to connect to NES")
ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
ctx.nes_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.nes_status = CONNECTION_REFUSED_STATUS
continue
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
Utils.init_logging("FF1Client")
options = Utils.get_options()
DISPLAY_MSGS = options["ffr_options"]["display_msgs"]
async def main(args):
ctx = FF1Context(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.nes_sync_task:
await ctx.nes_sync_task
import colorama
parser = get_base_parser()
args = parser.parse_args()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -115,34 +115,30 @@ components.extend([
])
def handle_uri(path: str, launch_args: tuple[str, ...]) -> None:
def handle_uri(path: str) -> tuple[list[Component], Component]:
url = urllib.parse.urlparse(path)
queries = urllib.parse.parse_qs(url.query)
launch_args = (path, *launch_args)
client_component = []
client_components = []
text_client_component = None
game = queries["game"][0]
for component in components:
if component.supports_uri and component.game_name == game:
client_component.append(component)
client_components.append(component)
elif component.display_name == "Text Client":
text_client_component = component
return client_components, text_client_component
if not client_component:
run_component(text_client_component, *launch_args)
return
else:
from kvui import ButtonsPrompt
component_options = {
text_client_component.display_name: text_client_component,
**{component.display_name: component for component in client_component}
}
popup = ButtonsPrompt("Connect to Multiworld",
"Select client to open and connect with.",
lambda component_name: run_component(component_options[component_name], *launch_args),
*component_options.keys())
popup.open()
def build_uri_popup(component_list: list[Component], launch_args: tuple[str, ...]) -> None:
from kvui import ButtonsPrompt
component_options = {
component.display_name: component for component in component_list
}
popup = ButtonsPrompt("Connect to Multiworld",
"Select client to open and connect with.",
lambda component_name: run_component(component_options[component_name], *launch_args),
*component_options.keys())
popup.open()
def identify(path: None | str) -> tuple[None | str, None | Component]:
@@ -212,7 +208,7 @@ def create_shortcut(button: Any, component: Component) -> None:
refresh_components: Callable[[], None] | None = None
def run_gui(path: str, args: Any) -> None:
def run_gui(launch_components: list[Component], args: Any) -> None:
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
from kivy.properties import ObjectProperty
from kivy.core.window import Window
@@ -245,12 +241,12 @@ def run_gui(path: str, args: Any) -> None:
cards: list[LauncherCard]
current_filter: Sequence[str | Type] | None
def __init__(self, ctx=None, path=None, args=None):
def __init__(self, ctx=None, components=None, args=None):
self.title = self.base_title + " " + Utils.__version__
self.ctx = ctx
self.icon = r"data/icon.png"
self.favorites = []
self.launch_uri = path
self.launch_components = components
self.launch_args = args
self.cards = []
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
@@ -372,9 +368,9 @@ def run_gui(path: str, args: Any) -> None:
return self.top_screen
def on_start(self):
if self.launch_uri:
handle_uri(self.launch_uri, self.launch_args)
self.launch_uri = None
if self.launch_components:
build_uri_popup(self.launch_components, self.launch_args)
self.launch_components = None
self.launch_args = None
@staticmethod
@@ -392,7 +388,7 @@ def run_gui(path: str, args: Any) -> None:
if file and component:
run_component(component, file)
else:
logging.warning(f"unable to identify component for {file}")
logging.warning(f"unable to identify component for {filename}")
def _on_keyboard(self, window: Window, key: int, scancode: int, codepoint: str, modifier: list[str]):
# Activate search as soon as we start typing, no matter if we are focused on the search box or not.
@@ -415,7 +411,7 @@ def run_gui(path: str, args: Any) -> None:
for filter in self.current_filter))
super().on_stop()
Launcher(path=path, args=args).run()
Launcher(components=launch_components, args=args).run()
# avoiding Launcher reference leak
# and don't try to do something with widgets after window closed
@@ -442,7 +438,15 @@ def main(args: argparse.Namespace | dict | None = None):
path = args.get("Patch|Game|Component|url", None)
if path is not None:
if not path.startswith("archipelago://"):
if path.startswith("archipelago://"):
args["args"] = (path, *args.get("args", ()))
# add the url arg to the passthrough args
components, text_client_component = handle_uri(path)
if not components:
args["component"] = text_client_component
else:
args['launch_components'] = [text_client_component, *components]
else:
file, component = identify(path)
if file:
args['file'] = file
@@ -458,7 +462,7 @@ def main(args: argparse.Namespace | dict | None = None):
elif "component" in args:
run_component(args["component"], *args["args"])
elif not args["update_settings"]:
run_gui(path, args.get("args", ()))
run_gui(args.get("launch_components", None), args.get("args", ()))
if __name__ == '__main__':

View File

@@ -458,8 +458,12 @@ class Context:
self.generator_version = Version(*decoded_obj["version"])
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
self.minimum_client_versions = {}
if self.generator_version < Version(0, 6, 2):
min_version = Version(0, 1, 6)
else:
min_version = min_client_version
for player, version in clients_ver.items():
self.minimum_client_versions[player] = max(Version(*version), min_client_version)
self.minimum_client_versions[player] = max(Version(*version), min_version)
self.slot_info = decoded_obj["slot_info"]
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}

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

@@ -82,6 +82,7 @@ Currently, the following games are supported:
* The Legend of Zelda: The Wind Waker
* Jak and Daxter: The Precursor Legacy
* Super Mario Land 2: 6 Golden Coins
* shapez
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -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

@@ -1,462 +0,0 @@
local socket = require("socket")
local json = require('json')
local math = require('math')
require("common")
local STATE_OK = "Ok"
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
local STATE_UNINITIALIZED = "Uninitialized"
local ITEM_INDEX = 0x03
local WEAPON_INDEX = 0x07
local ARMOR_INDEX = 0x0B
local goldLookup = {
[0x16C] = 10,
[0x16D] = 20,
[0x16E] = 25,
[0x16F] = 30,
[0x170] = 55,
[0x171] = 70,
[0x172] = 85,
[0x173] = 110,
[0x174] = 135,
[0x175] = 155,
[0x176] = 160,
[0x177] = 180,
[0x178] = 240,
[0x179] = 255,
[0x17A] = 260,
[0x17B] = 295,
[0x17C] = 300,
[0x17D] = 315,
[0x17E] = 330,
[0x17F] = 350,
[0x180] = 385,
[0x181] = 400,
[0x182] = 450,
[0x183] = 500,
[0x184] = 530,
[0x185] = 575,
[0x186] = 620,
[0x187] = 680,
[0x188] = 750,
[0x189] = 795,
[0x18A] = 880,
[0x18B] = 1020,
[0x18C] = 1250,
[0x18D] = 1455,
[0x18E] = 1520,
[0x18F] = 1760,
[0x190] = 1975,
[0x191] = 2000,
[0x192] = 2750,
[0x193] = 3400,
[0x194] = 4150,
[0x195] = 5000,
[0x196] = 5450,
[0x197] = 6400,
[0x198] = 6720,
[0x199] = 7340,
[0x19A] = 7690,
[0x19B] = 7900,
[0x19C] = 8135,
[0x19D] = 9000,
[0x19E] = 9300,
[0x19F] = 9500,
[0x1A0] = 9900,
[0x1A1] = 10000,
[0x1A2] = 12350,
[0x1A3] = 13000,
[0x1A4] = 13450,
[0x1A5] = 14050,
[0x1A6] = 14720,
[0x1A7] = 15000,
[0x1A8] = 17490,
[0x1A9] = 18010,
[0x1AA] = 19990,
[0x1AB] = 20000,
[0x1AC] = 20010,
[0x1AD] = 26000,
[0x1AE] = 45000,
[0x1AF] = 65000
}
local extensionConsumableLookup = {
[432] = 0x3C,
[436] = 0x3C,
[440] = 0x3C,
[433] = 0x3D,
[437] = 0x3D,
[441] = 0x3D,
[434] = 0x3E,
[438] = 0x3E,
[442] = 0x3E,
[435] = 0x3F,
[439] = 0x3F,
[443] = 0x3F
}
local noOverworldItemsLookup = {
[499] = 0x2B,
[500] = 0x12,
}
local consumableStacks = nil
local prevstate = ""
local curstate = STATE_UNINITIALIZED
local ff1Socket = nil
local frame = 0
local isNesHawk = false
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
local function defineMemoryFunctions()
local memDomain = {}
local domains = memory.getmemorydomainlist()
if domains[1] == "System Bus" then
--NesHawk
isNesHawk = true
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
elseif domains[1] == "WRAM" then
--QuickNES
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
memDomain["saveram"] = function() memory.usememorydomain("WRAM") end
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
end
return memDomain
end
local memDomain = defineMemoryFunctions()
local function StateOKForMainLoop()
memDomain.saveram()
local A = u8(0x102) -- Party Made
local B = u8(0x0FC)
local C = u8(0x0A3)
return A ~= 0x00 and not (A== 0xF2 and B == 0xF2 and C == 0xF2)
end
function generateLocationChecked()
memDomain.saveram()
data = uRange(0x01FF, 0x101)
data[0] = nil
return data
end
function setConsumableStacks()
memDomain.rom()
consumableStacks = {}
-- In order shards, tent, cabin, house, heal, pure, soft, ext1, ext2, ext3, ex4
consumableStacks[0x35] = 1
consumableStacks[0x36] = u8(0x47400) + 1
consumableStacks[0x37] = u8(0x47401) + 1
consumableStacks[0x38] = u8(0x47402) + 1
consumableStacks[0x39] = u8(0x47403) + 1
consumableStacks[0x3A] = u8(0x47404) + 1
consumableStacks[0x3B] = u8(0x47405) + 1
consumableStacks[0x3C] = u8(0x47406) + 1
consumableStacks[0x3D] = u8(0x47407) + 1
consumableStacks[0x3E] = u8(0x47408) + 1
consumableStacks[0x3F] = u8(0x47409) + 1
end
function getEmptyWeaponSlots()
memDomain.saveram()
ret = {}
count = 1
slot1 = uRange(0x118, 0x4)
slot2 = uRange(0x158, 0x4)
slot3 = uRange(0x198, 0x4)
slot4 = uRange(0x1D8, 0x4)
for i,v in pairs(slot1) do
if v == 0 then
ret[count] = 0x118 + i
count = count + 1
end
end
for i,v in pairs(slot2) do
if v == 0 then
ret[count] = 0x158 + i
count = count + 1
end
end
for i,v in pairs(slot3) do
if v == 0 then
ret[count] = 0x198 + i
count = count + 1
end
end
for i,v in pairs(slot4) do
if v == 0 then
ret[count] = 0x1D8 + i
count = count + 1
end
end
return ret
end
function getEmptyArmorSlots()
memDomain.saveram()
ret = {}
count = 1
slot1 = uRange(0x11C, 0x4)
slot2 = uRange(0x15C, 0x4)
slot3 = uRange(0x19C, 0x4)
slot4 = uRange(0x1DC, 0x4)
for i,v in pairs(slot1) do
if v == 0 then
ret[count] = 0x11C + i
count = count + 1
end
end
for i,v in pairs(slot2) do
if v == 0 then
ret[count] = 0x15C + i
count = count + 1
end
end
for i,v in pairs(slot3) do
if v == 0 then
ret[count] = 0x19C + i
count = count + 1
end
end
for i,v in pairs(slot4) do
if v == 0 then
ret[count] = 0x1DC + i
count = count + 1
end
end
return ret
end
local function slice (tbl, s, e)
local pos, new = 1, {}
for i = s + 1, e do
new[pos] = tbl[i]
pos = pos + 1
end
return new
end
function processBlock(block)
local msgBlock = block['messages']
if msgBlock ~= nil then
for i, v in pairs(msgBlock) do
if itemMessages[i] == nil then
local msg = {TTL=450, message=v, color=0xFFFF0000}
itemMessages[i] = msg
end
end
end
local itemsBlock = block["items"]
memDomain.saveram()
isInGame = u8(0x102)
if itemsBlock ~= nil and isInGame ~= 0x00 then
if consumableStacks == nil then
setConsumableStacks()
end
memDomain.saveram()
-- print('ITEMBLOCK: ')
-- print(itemsBlock)
itemIndex = u8(ITEM_INDEX)
-- print('ITEMINDEX: '..itemIndex)
for i, v in pairs(slice(itemsBlock, itemIndex, #itemsBlock)) do
-- Minus the offset and add to the correct domain
local memoryLocation = v
if v >= 0x100 and v <= 0x114 then
-- This is a key item
memoryLocation = memoryLocation - 0x0E0
wU8(memoryLocation, 0x01)
elseif v >= 0x1E0 and v <= 0x1F2 then
-- This is a movement item
-- Minus Offset (0x100) - movement offset (0xE0)
memoryLocation = memoryLocation - 0x1E0
-- Canal is a flipped bit
if memoryLocation == 0x0C then
wU8(memoryLocation, 0x00)
else
wU8(memoryLocation, 0x01)
end
elseif v >= 0x1F3 and v <= 0x1F4 then
-- NoOverworld special items
memoryLocation = noOverworldItemsLookup[v]
wU8(memoryLocation, 0x01)
elseif v >= 0x16C and v <= 0x1AF then
-- This is a gold item
amountToAdd = goldLookup[v]
biggest = u8(0x01E)
medium = u8(0x01D)
smallest = u8(0x01C)
currentValue = 0x10000 * biggest + 0x100 * medium + smallest
newValue = currentValue + amountToAdd
newBiggest = math.floor(newValue / 0x10000)
newMedium = math.floor(math.fmod(newValue, 0x10000) / 0x100)
newSmallest = math.floor(math.fmod(newValue, 0x100))
wU8(0x01E, newBiggest)
wU8(0x01D, newMedium)
wU8(0x01C, newSmallest)
elseif v >= 0x115 and v <= 0x11B then
-- This is a regular consumable OR a shard
-- Minus Offset (0x100) + item offset (0x20)
memoryLocation = memoryLocation - 0x0E0
currentValue = u8(memoryLocation)
amountToAdd = consumableStacks[memoryLocation]
if currentValue < 99 then
wU8(memoryLocation, currentValue + amountToAdd)
end
elseif v >= 0x1B0 and v <= 0x1BB then
-- This is an extension consumable
memoryLocation = extensionConsumableLookup[v]
currentValue = u8(memoryLocation)
amountToAdd = consumableStacks[memoryLocation]
if currentValue < 99 then
value = currentValue + amountToAdd
if value > 99 then
value = 99
end
wU8(memoryLocation, value)
end
end
end
if #itemsBlock > itemIndex then
wU8(ITEM_INDEX, #itemsBlock)
end
memDomain.saveram()
weaponIndex = u8(WEAPON_INDEX)
emptyWeaponSlots = getEmptyWeaponSlots()
lastUsedWeaponIndex = weaponIndex
-- print('WEAPON_INDEX: '.. weaponIndex)
memDomain.saveram()
for i, v in pairs(slice(itemsBlock, weaponIndex, #itemsBlock)) do
if v >= 0x11C and v <= 0x143 then
-- Minus the offset and add to the correct domain
local itemValue = v - 0x11B
if #emptyWeaponSlots > 0 then
slot = table.remove(emptyWeaponSlots, 1)
wU8(slot, itemValue)
lastUsedWeaponIndex = weaponIndex + i
else
break
end
end
end
if lastUsedWeaponIndex ~= weaponIndex then
wU8(WEAPON_INDEX, lastUsedWeaponIndex)
end
memDomain.saveram()
armorIndex = u8(ARMOR_INDEX)
emptyArmorSlots = getEmptyArmorSlots()
lastUsedArmorIndex = armorIndex
-- print('ARMOR_INDEX: '.. armorIndex)
memDomain.saveram()
for i, v in pairs(slice(itemsBlock, armorIndex, #itemsBlock)) do
if v >= 0x144 and v <= 0x16B then
-- Minus the offset and add to the correct domain
local itemValue = v - 0x143
if #emptyArmorSlots > 0 then
slot = table.remove(emptyArmorSlots, 1)
wU8(slot, itemValue)
lastUsedArmorIndex = armorIndex + i
else
break
end
end
end
if lastUsedArmorIndex ~= armorIndex then
wU8(ARMOR_INDEX, lastUsedArmorIndex)
end
end
end
function receive()
l, e = ff1Socket:receive()
if e == 'closed' then
if curstate == STATE_OK then
print("Connection closed")
end
curstate = STATE_UNINITIALIZED
return
elseif e == 'timeout' then
print("timeout")
return
elseif e ~= nil then
print(e)
curstate = STATE_UNINITIALIZED
return
end
processBlock(json.decode(l))
-- Determine Message to send back
memDomain.rom()
local playerName = uRange(0x7BCBF, 0x41)
playerName[0] = nil
local retTable = {}
retTable["playerName"] = playerName
if StateOKForMainLoop() then
retTable["locations"] = generateLocationChecked()
end
msg = json.encode(retTable).."\n"
local ret, error = ff1Socket:send(msg)
if ret == nil then
print(error)
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
curstate = STATE_TENTATIVELY_CONNECTED
elseif curstate == STATE_TENTATIVELY_CONNECTED then
print("Connected!")
itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"}
curstate = STATE_OK
end
end
function main()
if not checkBizHawkVersion() then
return
end
server, error = socket.bind('localhost', 52980)
while true do
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
frame = frame + 1
drawMessages()
if not (curstate == prevstate) then
-- console.log("Current state: "..curstate)
prevstate = curstate
end
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 60 == 0) then
gui.drawEllipse(248, 9, 6, 6, "Black", "Blue")
receive()
else
gui.drawEllipse(248, 9, 6, 6, "Black", "Green")
end
elseif (curstate == STATE_UNINITIALIZED) then
gui.drawEllipse(248, 9, 6, 6, "Black", "White")
if (frame % 60 == 0) then
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
drawText(5, 8, "Waiting for client", 0xFFFF0000)
drawText(5, 32, "Please start FF1Client.exe", 0xFFFF0000)
-- Advance so the messages are drawn
emu.frameadvance()
server:settimeout(2)
print("Attempting to connect")
local client, timeout = server:accept()
if timeout == nil then
-- print('Initial Connection Made')
curstate = STATE_INITIAL_CONNECTION_MADE
ff1Socket = client
ff1Socket:settimeout(0)
end
end
end
emu.frameadvance()
end
end
main()

View File

@@ -160,6 +160,9 @@
# Saving Princess
/worlds/saving_princess/ @LeonarthCG
# shapez
/worlds/shapez/ @BlastSlimey
# Shivers
/worlds/shivers/ @GodlFire @korydondzila

View File

@@ -231,11 +231,11 @@ Sent to clients after a client requested this message be sent to them, more info
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
| text | str | A descriptive message of the problem at hand. |
| Name | Type | Notes |
| ---- |-------------| ----- |
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
| original_cmd | str \| None | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
| text | str | A descriptive message of the problem at hand. |
##### PacketProblemType
`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future.
@@ -551,14 +551,14 @@ In JSON this may look like:
Message nodes sent along with [PrintJSON](#PrintJSON) packet to be reconstructed into a legible message. The nodes are intended to be read in the order they are listed in the packet.
```python
from typing import TypedDict, Optional
from typing import TypedDict
class JSONMessagePart(TypedDict):
type: Optional[str]
text: Optional[str]
color: Optional[str] # only available if type is a color
flags: Optional[int] # only available if type is an item_id or item_name
player: Optional[int] # only available if type is either item or location
hint_status: Optional[HintStatus] # only available if type is hint_status
type: str | None
text: str | None
color: str | None # only available if type is a color
flags: int | None # only available if type is an item_id or item_name
player: int | None # only available if type is either item or location
hint_status: HintStatus | None # only available if type is hint_status
```
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.

View File

@@ -333,7 +333,7 @@ within the world.
### TextChoice
Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any
user defined string as a valid option, so will either need to be validated by adding a validation step to the option
class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified
class or within world, if necessary. Value for this class is `str | int` so if you need the value at a specified
point, `self.options.my_option.current_key` will always return a string.
### PlandoBosses

View File

@@ -102,17 +102,16 @@ In worlds, this should only be used for the top level to avoid issues when upgra
### Bool
Since `bool` can not be subclassed, use the `settings.Bool` helper in a `typing.Union` to get a comment in host.yaml.
Since `bool` can not be subclassed, use the `settings.Bool` helper in a union to get a comment in host.yaml.
```python
import settings
import typing
class MySettings(settings.Group):
class MyBool(settings.Bool):
"""Doc string"""
my_value: typing.Union[MyBool, bool] = True
my_value: MyBool | bool = True
```
### UserFilePath
@@ -134,15 +133,15 @@ Checks the file against [md5s](#md5s) by default.
Resolves to an executable (varying file extension based on platform)
#### description: Optional\[str\]
#### description: str | None
Human-readable name to use in file browser
#### copy_to: Optional\[str\]
#### copy_to: str | None
Instead of storing the path, copy the file.
#### md5s: List[Union[str, bytes]]
#### md5s: list[str | bytes]
Provide md5 hashes as hex digests or raw bytes for automatic validation.

View File

@@ -86,6 +86,7 @@ Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\*.exe"
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
Type: files; Name: "{app}\data\lua\connector_ff1.lua"
Type: filesandordirs; Name: "{app}\SNI\lua*"
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
#include "installdelete.iss"

View File

@@ -64,7 +64,6 @@ non_apworlds: set[str] = {
"ArchipIDLE",
"Archipelago",
"Clique",
"Final Fantasy",
"Lufia II Ancient Cave",
"Meritous",
"Ocarina of Time",
@@ -482,7 +481,7 @@ tmp="${{exe#*/}}"
if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then
exe="{default_exe.parent}/$exe"
fi
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib"
export LD_LIBRARY_PATH="${{LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}}$APPDIR/{default_exe.parent}/lib"
$APPDIR/$exe "$@"
""")
launcher_filename.chmod(0o755)

View File

@@ -224,8 +224,6 @@ components: List[Component] = [
Component('OoT Client', 'OoTClient',
file_identifier=SuffixIdentifier('.apz5')),
Component('OoT Adjuster', 'OoTAdjuster'),
# FF1
Component('FF1 Client', 'FF1Client'),
# TLoZ
Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')),
# ChecksFinder

328
worlds/ff1/Client.py Normal file
View File

@@ -0,0 +1,328 @@
import logging
from collections import deque
from typing import TYPE_CHECKING
from NetUtils import ClientStatus
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
base_id = 7000
logger = logging.getLogger("Client")
rom_name_location = 0x07FFE3
locations_array_start = 0x200
locations_array_length = 0x100
items_obtained = 0x03
gp_location_low = 0x1C
gp_location_middle = 0x1D
gp_location_high = 0x1E
weapons_arrays_starts = [0x118, 0x158, 0x198, 0x1D8]
armors_arrays_starts = [0x11C, 0x15C, 0x19C, 0x1DC]
status_a_location = 0x102
status_b_location = 0x0FC
status_c_location = 0x0A3
key_items = ["Lute", "Crown", "Crystal", "Herb", "Key", "Tnt", "Adamant", "Slab", "Ruby", "Rod",
"Floater", "Chime", "Tail", "Cube", "Bottle", "Oxyale", "EarthOrb", "FireOrb", "WaterOrb", "AirOrb"]
consumables = ["Shard", "Tent", "Cabin", "House", "Heal", "Pure", "Soft"]
weapons = ["WoodenNunchucks", "SmallKnife", "WoodenRod", "Rapier", "IronHammer", "ShortSword", "HandAxe", "Scimitar",
"IronNunchucks", "LargeKnife", "IronStaff", "Sabre", "LongSword", "GreatAxe", "Falchon", "SilverKnife",
"SilverSword", "SilverHammer", "SilverAxe", "FlameSword", "IceSword", "DragonSword", "GiantSword",
"SunSword", "CoralSword", "WereSword", "RuneSword", "PowerRod", "LightAxe", "HealRod", "MageRod", "Defense",
"WizardRod", "Vorpal", "CatClaw", "ThorHammer", "BaneSword", "Katana", "Xcalber", "Masamune"]
armor = ["Cloth", "WoodenArmor", "ChainArmor", "IronArmor", "SteelArmor", "SilverArmor", "FlameArmor", "IceArmor",
"OpalArmor", "DragonArmor", "Copper", "Silver", "Gold", "Opal", "WhiteShirt", "BlackShirt", "WoodenShield",
"IronShield", "SilverShield", "FlameShield", "IceShield", "OpalShield", "AegisShield", "Buckler", "ProCape",
"Cap", "WoodenHelm", "IronHelm", "SilverHelm", "OpalHelm", "HealHelm", "Ribbon", "Gloves", "CopperGauntlets",
"IronGauntlets", "SilverGauntlets", "ZeusGauntlets", "PowerGauntlets", "OpalGauntlets", "ProRing"]
gold_items = ["Gold10", "Gold20", "Gold25", "Gold30", "Gold55", "Gold70", "Gold85", "Gold110", "Gold135", "Gold155",
"Gold160", "Gold180", "Gold240", "Gold255", "Gold260", "Gold295", "Gold300", "Gold315", "Gold330",
"Gold350", "Gold385", "Gold400", "Gold450", "Gold500", "Gold530", "Gold575", "Gold620", "Gold680",
"Gold750", "Gold795", "Gold880", "Gold1020", "Gold1250", "Gold1455", "Gold1520", "Gold1760", "Gold1975",
"Gold2000", "Gold2750", "Gold3400", "Gold4150", "Gold5000", "Gold5450", "Gold6400", "Gold6720",
"Gold7340", "Gold7690", "Gold7900", "Gold8135", "Gold9000", "Gold9300", "Gold9500", "Gold9900",
"Gold10000", "Gold12350", "Gold13000", "Gold13450", "Gold14050", "Gold14720", "Gold15000", "Gold17490",
"Gold18010", "Gold19990", "Gold20000", "Gold20010", "Gold26000", "Gold45000", "Gold65000"]
extended_consumables = ["FullCure", "Phoenix", "Blast", "Smoke",
"Refresh", "Flare", "Black", "Guard",
"Quick", "HighPotion", "Wizard", "Cloak"]
ext_consumables_lookup = {"FullCure": "Ext1", "Phoenix": "Ext2", "Blast": "Ext3", "Smoke": "Ext4",
"Refresh": "Ext1", "Flare": "Ext2", "Black": "Ext3", "Guard": "Ext4",
"Quick": "Ext1", "HighPotion": "Ext2", "Wizard": "Ext3", "Cloak": "Ext4"}
ext_consumables_locations = {"Ext1": 0x3C, "Ext2": 0x3D, "Ext3": 0x3E, "Ext4": 0x3F}
movement_items = ["Ship", "Bridge", "Canal", "Canoe"]
no_overworld_items = ["Sigil", "Mark"]
class FF1Client(BizHawkClient):
game = "Final Fantasy"
system = "NES"
weapons_queue: deque[int]
armor_queue: deque[int]
consumable_stack_amounts: dict[str, int] | None
def __init__(self) -> None:
self.wram = "RAM"
self.sram = "WRAM"
self.rom = "PRG ROM"
self.consumable_stack_amounts = None
self.weapons_queue = deque()
self.armor_queue = deque()
self.guard_character = 0x00
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
try:
# Check ROM name/patch version
rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(rom_name_location, 0x0D, self.rom)]))[0])
rom_name = rom_name.decode("ascii")
if rom_name != "FINAL FANTASY":
return False # Not a Final Fantasy 1 ROM
except bizhawk.RequestFailedError:
return False # Not able to get a response, say no for now
ctx.game = self.game
ctx.items_handling = 0b111
ctx.want_slot_data = True
# Resetting these in case of switching ROMs
self.consumable_stack_amounts = None
self.weapons_queue = deque()
self.armor_queue = deque()
return True
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
if ctx.server is None:
return
if ctx.slot is None:
return
try:
self.guard_character = await self.read_sram_value(ctx, status_a_location)
# If the first character's name starts with a 0 value, we're at the title screen/character creation.
# In that case, don't allow any read/writes.
# We do this by setting the guard to 1 because that's neither a valid character nor the initial value.
if self.guard_character == 0:
self.guard_character = 0x01
if self.consumable_stack_amounts is None:
self.consumable_stack_amounts = {}
self.consumable_stack_amounts["Shard"] = 1
other_consumable_amounts = await self.read_rom(ctx, 0x47400, 10)
self.consumable_stack_amounts["Tent"] = other_consumable_amounts[0] + 1
self.consumable_stack_amounts["Cabin"] = other_consumable_amounts[1] + 1
self.consumable_stack_amounts["House"] = other_consumable_amounts[2] + 1
self.consumable_stack_amounts["Heal"] = other_consumable_amounts[3] + 1
self.consumable_stack_amounts["Pure"] = other_consumable_amounts[4] + 1
self.consumable_stack_amounts["Soft"] = other_consumable_amounts[5] + 1
self.consumable_stack_amounts["Ext1"] = other_consumable_amounts[6] + 1
self.consumable_stack_amounts["Ext2"] = other_consumable_amounts[7] + 1
self.consumable_stack_amounts["Ext3"] = other_consumable_amounts[8] + 1
self.consumable_stack_amounts["Ext4"] = other_consumable_amounts[9] + 1
await self.location_check(ctx)
await self.received_items_check(ctx)
await self.process_weapons_queue(ctx)
await self.process_armor_queue(ctx)
except bizhawk.RequestFailedError:
# The connector didn't respond. Exit handler and return to main loop to reconnect
pass
async def location_check(self, ctx: "BizHawkClientContext"):
locations_data = await self.read_sram_values_guarded(ctx, locations_array_start, locations_array_length)
if locations_data is None:
return
locations_checked = []
if len(locations_data) > 0xFE and locations_data[0xFE] & 0x02 != 0 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL}
])
ctx.finished_game = True
for location in ctx.missing_locations:
# index will be - 0x100 or 0x200
index = location
if location < 0x200:
# Location is a chest
index -= 0x100
flag = 0x04
else:
# Location is an NPC
index -= 0x200
flag = 0x02
if locations_data[index] & flag != 0:
locations_checked.append(location)
found_locations = await ctx.check_locations(locations_checked)
for location in found_locations:
ctx.locations_checked.add(location)
location_name = ctx.location_names.lookup_in_game(location)
logger.info(
f'New Check: {location_name} ({len(ctx.locations_checked)}/'
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
async def received_items_check(self, ctx: "BizHawkClientContext") -> None:
assert self.consumable_stack_amounts, "shouldn't call this function without reading consumable_stack_amounts"
write_list: list[tuple[int, list[int], str]] = []
items_received_count = await self.read_sram_value_guarded(ctx, items_obtained)
if items_received_count is None:
return
if items_received_count < len(ctx.items_received):
current_item = ctx.items_received[items_received_count]
current_item_id = current_item.item
current_item_name = ctx.item_names.lookup_in_game(current_item_id, ctx.game)
if current_item_name in key_items:
location = current_item_id - 0xE0
write_list.append((location, [1], self.sram))
elif current_item_name in movement_items:
location = current_item_id - 0x1E0
if current_item_name != "Canal":
write_list.append((location, [1], self.sram))
else:
write_list.append((location, [0], self.sram))
elif current_item_name in no_overworld_items:
if current_item_name == "Sigil":
location = 0x28
else:
location = 0x12
write_list.append((location, [1], self.sram))
elif current_item_name in gold_items:
gold_amount = int(current_item_name[4:])
current_gold_value = await self.read_sram_values_guarded(ctx, gp_location_low, 3)
if current_gold_value is None:
return
current_gold = int.from_bytes(current_gold_value, "little")
new_gold = min(gold_amount + current_gold, 999999)
lower_byte = new_gold % (2 ** 8)
middle_byte = (new_gold // (2 ** 8)) % (2 ** 8)
upper_byte = new_gold // (2 ** 16)
write_list.append((gp_location_low, [lower_byte], self.sram))
write_list.append((gp_location_middle, [middle_byte], self.sram))
write_list.append((gp_location_high, [upper_byte], self.sram))
elif current_item_name in consumables:
location = current_item_id - 0xE0
current_value = await self.read_sram_value_guarded(ctx, location)
if current_value is None:
return
amount_to_add = self.consumable_stack_amounts[current_item_name]
new_value = min(current_value + amount_to_add, 99)
write_list.append((location, [new_value], self.sram))
elif current_item_name in extended_consumables:
ext_name = ext_consumables_lookup[current_item_name]
location = ext_consumables_locations[ext_name]
current_value = await self.read_sram_value_guarded(ctx, location)
if current_value is None:
return
amount_to_add = self.consumable_stack_amounts[ext_name]
new_value = min(current_value + amount_to_add, 99)
write_list.append((location, [new_value], self.sram))
elif current_item_name in weapons:
self.weapons_queue.appendleft(current_item_id - 0x11B)
elif current_item_name in armor:
self.armor_queue.appendleft(current_item_id - 0x143)
write_list.append((items_obtained, [items_received_count + 1], self.sram))
write_successful = await self.write_sram_values_guarded(ctx, write_list)
if write_successful:
await bizhawk.display_message(ctx.bizhawk_ctx, f"Received {current_item_name}")
async def process_weapons_queue(self, ctx: "BizHawkClientContext"):
empty_slots = deque()
char1_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[0], 4)
char2_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[1], 4)
char3_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[2], 4)
char4_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[3], 4)
if char1_slots is None or char2_slots is None or char3_slots is None or char4_slots is None:
return
for i, slot in enumerate(char1_slots):
if slot == 0:
empty_slots.appendleft(weapons_arrays_starts[0] + i)
for i, slot in enumerate(char2_slots):
if slot == 0:
empty_slots.appendleft(weapons_arrays_starts[1] + i)
for i, slot in enumerate(char3_slots):
if slot == 0:
empty_slots.appendleft(weapons_arrays_starts[2] + i)
for i, slot in enumerate(char4_slots):
if slot == 0:
empty_slots.appendleft(weapons_arrays_starts[3] + i)
while len(empty_slots) > 0 and len(self.weapons_queue) > 0:
current_slot = empty_slots.pop()
current_weapon = self.weapons_queue.pop()
await self.write_sram_guarded(ctx, current_slot, current_weapon)
async def process_armor_queue(self, ctx: "BizHawkClientContext"):
empty_slots = deque()
char1_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[0], 4)
char2_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[1], 4)
char3_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[2], 4)
char4_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[3], 4)
if char1_slots is None or char2_slots is None or char3_slots is None or char4_slots is None:
return
for i, slot in enumerate(char1_slots):
if slot == 0:
empty_slots.appendleft(armors_arrays_starts[0] + i)
for i, slot in enumerate(char2_slots):
if slot == 0:
empty_slots.appendleft(armors_arrays_starts[1] + i)
for i, slot in enumerate(char3_slots):
if slot == 0:
empty_slots.appendleft(armors_arrays_starts[2] + i)
for i, slot in enumerate(char4_slots):
if slot == 0:
empty_slots.appendleft(armors_arrays_starts[3] + i)
while len(empty_slots) > 0 and len(self.armor_queue) > 0:
current_slot = empty_slots.pop()
current_armor = self.armor_queue.pop()
await self.write_sram_guarded(ctx, current_slot, current_armor)
async def read_sram_value(self, ctx: "BizHawkClientContext", location: int):
value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.sram)]))[0])
return int.from_bytes(value, "little")
async def read_sram_values_guarded(self, ctx: "BizHawkClientContext", location: int, size: int):
value = await bizhawk.guarded_read(ctx.bizhawk_ctx,
[(location, size, self.sram)],
[(status_a_location, [self.guard_character], self.sram)])
if value is None:
return None
return value[0]
async def read_sram_value_guarded(self, ctx: "BizHawkClientContext", location: int):
value = await bizhawk.guarded_read(ctx.bizhawk_ctx,
[(location, 1, self.sram)],
[(status_a_location, [self.guard_character], self.sram)])
if value is None:
return None
return int.from_bytes(value[0], "little")
async def read_rom(self, ctx: "BizHawkClientContext", location: int, size: int):
return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.rom)]))[0]
async def write_sram_guarded(self, ctx: "BizHawkClientContext", location: int, value: int):
return await bizhawk.guarded_write(ctx.bizhawk_ctx,
[(location, [value], self.sram)],
[(status_a_location, [self.guard_character], self.sram)])
async def write_sram_values_guarded(self, ctx: "BizHawkClientContext", write_list):
return await bizhawk.guarded_write(ctx.bizhawk_ctx,
write_list,
[(status_a_location, [self.guard_character], self.sram)])

View File

@@ -1,5 +1,5 @@
import json
from pathlib import Path
import pkgutil
from typing import Dict, Set, NamedTuple, List
from BaseClasses import Item, ItemClassification
@@ -37,15 +37,13 @@ class FF1Items:
_item_table_lookup: Dict[str, ItemData] = {}
def _populate_item_table_from_data(self):
base_path = Path(__file__).parent
file_path = (base_path / "data/items.json").resolve()
with open(file_path) as file:
items = json.load(file)
# Hardcode progression and categories for now
self._item_table = [ItemData(name, code, "FF1Item", ItemClassification.progression if name in
FF1_PROGRESSION_LIST else ItemClassification.useful if name in FF1_USEFUL_LIST else
ItemClassification.filler) for name, code in items.items()]
self._item_table_lookup = {item.name: item for item in self._item_table}
file = pkgutil.get_data(__name__, "data/items.json").decode("utf-8")
items = json.loads(file)
# Hardcode progression and categories for now
self._item_table = [ItemData(name, code, "FF1Item", ItemClassification.progression if name in
FF1_PROGRESSION_LIST else ItemClassification.useful if name in FF1_USEFUL_LIST else
ItemClassification.filler) for name, code in items.items()]
self._item_table_lookup = {item.name: item for item in self._item_table}
def _get_item_table(self) -> List[ItemData]:
if not self._item_table or not self._item_table_lookup:

View File

@@ -1,5 +1,5 @@
import json
from pathlib import Path
import pkgutil
from typing import Dict, NamedTuple, List, Optional
from BaseClasses import Region, Location, MultiWorld
@@ -18,13 +18,11 @@ class FF1Locations:
_location_table_lookup: Dict[str, LocationData] = {}
def _populate_item_table_from_data(self):
base_path = Path(__file__).parent
file_path = (base_path / "data/locations.json").resolve()
with open(file_path) as file:
locations = json.load(file)
# Hardcode progression and categories for now
self._location_table = [LocationData(name, code) for name, code in locations.items()]
self._location_table_lookup = {item.name: item for item in self._location_table}
file = pkgutil.get_data(__name__, "data/locations.json")
locations = json.loads(file)
# Hardcode progression and categories for now
self._location_table = [LocationData(name, code) for name, code in locations.items()]
self._location_table_lookup = {item.name: item for item in self._location_table}
def _get_location_table(self) -> List[LocationData]:
if not self._location_table or not self._location_table_lookup:

View File

@@ -7,6 +7,7 @@ from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST,
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
from .Options import FF1Options
from ..AutoWorld import World, WebWorld
from .Client import FF1Client
class FF1Settings(settings.Group):

View File

@@ -22,11 +22,6 @@ All items can appear in other players worlds, including consumables, shards, wea
## What does another world's item look like in Final Fantasy
All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the
emulator will display what was found external to the in-game text box.
All local and remote items appear the same. Final Fantasy will say that you received an item, then the client log will
display what was found external to the in-game text box.
## Unique Local Commands
The following commands are only available when using the FF1Client for the Final Fantasy Randomizer.
- `/nes` Shows the current status of the NES connection.
- `/toggle_msgs` Toggle displaying messages in EmuHawk

View File

@@ -2,10 +2,10 @@
## Required Software
- The FF1Client
- Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended
- [BizHawk at TASVideos](https://tasvideos.org/BizHawk)
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prerequisite installer first, which can also be found at the above link.
- The built-in BizHawk client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
- Your legally obtained Final Fantasy (USA Edition) ROM file, probably named `Final Fantasy (USA).nes`. Neither
Archipelago.gg nor the Final Fantasy Randomizer Community can supply you with this.
@@ -13,7 +13,7 @@
1. Download and install the latest version of Archipelago.
1. On Windows, download Setup.Archipelago.<HighestVersion\>.exe and run it
2. Assign EmuHawk version 2.3.1 or higher as your default program for launching `.nes` files.
2. Assign EmuHawk as your default program for launching `.nes` files.
1. Extract your BizHawk folder to your Desktop, or somewhere you will remember. Below are optional additional steps
for loading ROMs more conveniently
1. Right-click on a ROM file and select **Open with...**
@@ -46,7 +46,7 @@ please refer to the [game agnostic setup guide](/tutorial/Archipelago/setup/en).
Once the Archipelago server has been hosted:
1. Navigate to your Archipelago install folder and run `ArchipelagoFF1Client.exe`
1. Navigate to your Archipelago install folder and run `ArchipelagoBizhawkClient.exe`
2. Notice the `/connect command` on the server hosting page (It should look like `/connect archipelago.gg:*****`
where ***** are numbers)
3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should
@@ -54,16 +54,11 @@ Once the Archipelago server has been hosted:
### Running Your Game and Connecting to the Client Program
1. Open EmuHawk 2.3.1 or higher and load your ROM OR click your ROM file if it is already associated with the
1. Open EmuHawk and load your ROM OR click your ROM file if it is already associated with the
extension `*.nes`
2. Navigate to where you installed Archipelago, then to `data/lua`, and drag+drop the `connector_ff1.lua` script onto
the main EmuHawk window.
1. You could instead open the Lua Console manually, click `Script``Open Script`, and navigate to
`connector_ff1.lua` with the file picker.
2. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception
close your emulator entirely, restart it and re-run these steps
3. If it says `Must use a version of BizHawk 2.3.1 or higher`, double-check your BizHawk version by clicking **
Help** -> **About**
2. Navigate to where you installed Archipelago, then to `data/lua`, and drag+drop the `connector_bizhawk_generic.lua`
script onto the main EmuHawk window. You can also instead open the Lua Console manually, click `Script``Open Script`,
and navigate to `connector_bizhawk_generic.lua` with the file picker.
## Play the game

View File

@@ -281,7 +281,7 @@ class MessengerWorld(World):
disconnect_entrances(self)
add_closed_portal_reqs(self)
# i need portal shuffle to happen after rules exist so i can validate it
attempts = 5
attempts = 20
if self.options.shuffle_portals:
self.portal_mapping = []
self.spoiler_portal_mapping = {}

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

View File

@@ -3,7 +3,7 @@ from ...data import villagers_data, fish_data
from ...data.building import Building
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource
from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, YearRequirement
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
from ...strings.artisan_good_names import ArtisanGood
from ...strings.book_names import Book
@@ -209,7 +209,7 @@ pelican_town = ContentPack(
# Books
Book.animal_catalogue: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
ShopSource(money_price=5000, shop_region=Region.ranch),),
ShopSource(money_price=5000, shop_region=Region.ranch, other_requirements=(YearRequirement(2),)),),
Book.book_of_mysteries: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
MysteryBoxSource(amount=38),), # After 38 boxes, there are 49.99% chances player received the book.

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

@@ -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

@@ -1,7 +1,7 @@
import unittest
from typing import ClassVar
from . import SVTestBase
from .bases import SVTestBase
from .. import options
from ..locations import LocationTags, location_table
from ..mods.mod_data import ModNames

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.")