mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-17 08:03:30 -07:00
Compare commits
17 Commits
revert-491
...
plando-ite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c54a711c27 | ||
|
|
704cd97f21 | ||
|
|
47a0dd696f | ||
|
|
c64791e3a8 | ||
|
|
e82d50a3c5 | ||
|
|
0a7aa9e3e2 | ||
|
|
13ca134d12 | ||
|
|
8671e9a391 | ||
|
|
a7de89f45c | ||
|
|
e9f51e3302 | ||
|
|
5491f8c459 | ||
|
|
de71677208 | ||
|
|
653ee2b625 | ||
|
|
62694b1ce7 | ||
|
|
9c0ad2b825 | ||
|
|
88b529593f | ||
|
|
0351698ef7 |
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
267
FF1Client.py
267
FF1Client.py
@@ -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()
|
||||
60
Launcher.py
60
Launcher.py
@@ -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__':
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -160,6 +160,9 @@
|
||||
# Saving Princess
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# shapez
|
||||
/worlds/shapez/ @BlastSlimey
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire @korydondzila
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
3
setup.py
3
setup.py
@@ -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)
|
||||
|
||||
@@ -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
328
worlds/ff1/Client.py
Normal 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)])
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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]
|
||||
@@ -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
|
||||
2
worlds/stardew_valley/regions/__init__.py
Normal file
2
worlds/stardew_valley/regions/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .entrance_rando import prepare_mod_data
|
||||
from .regions import create_regions, RegionFactory
|
||||
73
worlds/stardew_valley/regions/entrance_rando.py
Normal file
73
worlds/stardew_valley/regions/entrance_rando.py
Normal 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
|
||||
94
worlds/stardew_valley/regions/model.py
Normal file
94
worlds/stardew_valley/regions/model.py
Normal 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]
|
||||
46
worlds/stardew_valley/regions/mods.py
Normal file
46
worlds/stardew_valley/regions/mods.py
Normal 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
|
||||
})
|
||||
61
worlds/stardew_valley/regions/regions.py
Normal file
61
worlds/stardew_valley/regions/regions.py
Normal 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}
|
||||
522
worlds/stardew_valley/regions/vanilla_data.py
Normal file
522
worlds/stardew_valley/regions/vanilla_data.py
Normal 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
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
167
worlds/stardew_valley/test/regions/TestEntranceRandomization.py
Normal file
167
worlds/stardew_valley/test/regions/TestEntranceRandomization.py
Normal 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
|
||||
88
worlds/stardew_valley/test/regions/TestRandomizationFlag.py
Normal file
88
worlds/stardew_valley/test/regions/TestRandomizationFlag.py
Normal 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)
|
||||
66
worlds/stardew_valley/test/regions/TestRegionConnections.py
Normal file
66
worlds/stardew_valley/test/regions/TestRegionConnections.py
Normal 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.")
|
||||
0
worlds/stardew_valley/test/regions/__init__.py
Normal file
0
worlds/stardew_valley/test/regions/__init__.py
Normal file
Reference in New Issue
Block a user