mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 22:23:24 -07:00
Merge branch 'ArchipelagoMW:main' into new-options-api
This commit is contained in:
@@ -89,9 +89,6 @@ components: List[Component] = [
|
||||
Component('SNI Client', 'SNIClient',
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||
'.apsmw', '.apl2ac')),
|
||||
# BizHawk
|
||||
Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT,
|
||||
file_identifier=SuffixIdentifier()),
|
||||
Component('Links Awakening DX Client', 'LinksAwakeningClient',
|
||||
file_identifier=SuffixIdentifier('.apladx')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
|
||||
@@ -13,7 +13,6 @@ import typing
|
||||
|
||||
|
||||
BIZHAWK_SOCKET_PORT = 43055
|
||||
EXPECTED_SCRIPT_VERSION = 1
|
||||
|
||||
|
||||
class ConnectionStatus(enum.IntEnum):
|
||||
@@ -22,15 +21,6 @@ class ConnectionStatus(enum.IntEnum):
|
||||
CONNECTED = 3
|
||||
|
||||
|
||||
class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
connection_status: ConnectionStatus
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
|
||||
|
||||
class NotConnectedError(Exception):
|
||||
"""Raised when something tries to make a request to the connector script before a connection has been established"""
|
||||
pass
|
||||
@@ -51,6 +41,50 @@ class SyncError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
connection_status: ConnectionStatus
|
||||
_lock: asyncio.Lock
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def _send_message(self, message: str):
|
||||
async with self._lock:
|
||||
if self.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = self.streams
|
||||
writer.write(message.encode("utf-8") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
res = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if res == b"":
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
if self.connection_status == ConnectionStatus.TENTATIVE:
|
||||
self.connection_status = ConnectionStatus.CONNECTED
|
||||
|
||||
return res.decode("utf-8")
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
|
||||
|
||||
async def connect(ctx: BizHawkContext) -> bool:
|
||||
"""Attempts to establish a connection with the connector script. Returns True if successful."""
|
||||
try:
|
||||
@@ -72,74 +106,14 @@ def disconnect(ctx: BizHawkContext) -> None:
|
||||
|
||||
|
||||
async def get_script_version(ctx: BizHawkContext) -> int:
|
||||
if ctx.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = ctx.streams
|
||||
writer.write("VERSION".encode("ascii") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
version = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if version == b"":
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
return int(version.decode("ascii"))
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
return int(await ctx._send_message("VERSION"))
|
||||
|
||||
|
||||
async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
"""Sends a list of requests to the BizHawk connector and returns their responses.
|
||||
|
||||
It's likely you want to use the wrapper functions instead of this."""
|
||||
if ctx.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = ctx.streams
|
||||
writer.write(json.dumps(req_list).encode("utf-8") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
res = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if res == b"":
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
if ctx.connection_status == ConnectionStatus.TENTATIVE:
|
||||
ctx.connection_status = ConnectionStatus.CONNECTED
|
||||
|
||||
ret = json.loads(res.decode("utf-8"))
|
||||
for response in ret:
|
||||
if response["type"] == "ERROR":
|
||||
raise ConnectorError(response["err"])
|
||||
|
||||
return ret
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
return json.loads(await ctx._send_message(json.dumps(req_list)))
|
||||
|
||||
|
||||
async def ping(ctx: BizHawkContext) -> None:
|
||||
|
||||
@@ -16,12 +16,22 @@ else:
|
||||
BizHawkClientContext = object
|
||||
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient")
|
||||
|
||||
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier())
|
||||
components.append(component)
|
||||
|
||||
|
||||
class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
|
||||
|
||||
def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
|
||||
new_class = super().__new__(cls, name, bases, namespace)
|
||||
|
||||
# Register handler
|
||||
if "system" in namespace:
|
||||
systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"]))
|
||||
if systems not in AutoBizHawkClientRegister.game_handlers:
|
||||
@@ -30,6 +40,19 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
if "game" in namespace:
|
||||
AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class()
|
||||
|
||||
# Update launcher component's suffixes
|
||||
if "patch_suffix" in namespace:
|
||||
if namespace["patch_suffix"] is not None:
|
||||
existing_identifier: SuffixIdentifier = component.file_identifier
|
||||
new_suffixes = [*existing_identifier.suffixes]
|
||||
|
||||
if type(namespace["patch_suffix"]) is str:
|
||||
new_suffixes.append(namespace["patch_suffix"])
|
||||
else:
|
||||
new_suffixes.extend(namespace["patch_suffix"])
|
||||
|
||||
component.file_identifier = SuffixIdentifier(*new_suffixes)
|
||||
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
@@ -45,11 +68,14 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
|
||||
class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
system: ClassVar[Union[str, Tuple[str, ...]]]
|
||||
"""The system that the game this client is for runs on"""
|
||||
"""The system(s) that the game this client is for runs on"""
|
||||
|
||||
game: ClassVar[str]
|
||||
"""The game this client is for"""
|
||||
|
||||
patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]]
|
||||
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def validate_rom(self, ctx: BizHawkClientContext) -> bool:
|
||||
"""Should return whether the currently loaded ROM should be handled by this client. You might read the game name
|
||||
@@ -75,13 +101,3 @@ class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None:
|
||||
"""For handling packages from the server. Called from `BizHawkClientContext.on_package`."""
|
||||
pass
|
||||
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient")
|
||||
|
||||
|
||||
if not any(component.script_name == "BizHawkClient" for component in components):
|
||||
components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier()))
|
||||
|
||||
@@ -13,8 +13,8 @@ from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser,
|
||||
import Patch
|
||||
import Utils
|
||||
|
||||
from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \
|
||||
get_system, ping
|
||||
from . import BizHawkContext, ConnectionStatus, NotConnectedError, RequestFailedError, connect, disconnect, get_hash, \
|
||||
get_script_version, get_system, ping
|
||||
from .client import BizHawkClient, AutoBizHawkClientRegister
|
||||
|
||||
|
||||
@@ -133,6 +133,8 @@ async def _game_watcher(ctx: BizHawkClientContext):
|
||||
except RequestFailedError as exc:
|
||||
logger.info(f"Lost connection to BizHawk: {exc.args[0]}")
|
||||
continue
|
||||
except NotConnectedError:
|
||||
continue
|
||||
|
||||
# Get slot name and send `Connect`
|
||||
if ctx.server is not None and ctx.username is None:
|
||||
|
||||
@@ -42,360 +42,22 @@ and select EmuHawk.exe.
|
||||
An alternative BizHawk setup guide as well as various pieces of troubleshooting advice can be found
|
||||
[here](https://wiki.ootrandomizer.com/index.php?title=Bizhawk).
|
||||
|
||||
## Configuring your YAML file
|
||||
## Create a Config (.yaml) File
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
### What is a config file and why do I need one?
|
||||
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how it should
|
||||
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
|
||||
an experience customized for their taste, and different players in the same multiworld can all have different options.
|
||||
See the guide on setting up a basic YAML at the Archipelago setup
|
||||
guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
|
||||
|
||||
### Where do I get a YAML file?
|
||||
### Where do I get a config file?
|
||||
|
||||
A basic OoT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this
|
||||
tutorial, if you want to see a complete list, download Archipelago from
|
||||
the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in
|
||||
the "Players" folder.
|
||||
The Player Settings page on the website allows you to configure your personal settings and export a config file from
|
||||
them. Player settings page: [Ocarina of Time Player Settings Page](/games/Ocarina%20of%20Time/player-settings)
|
||||
|
||||
```yaml
|
||||
description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files
|
||||
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
name: YourName
|
||||
game:
|
||||
Ocarina of Time: 1
|
||||
requires:
|
||||
version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
|
||||
# Shared Options supported by all games:
|
||||
accessibility:
|
||||
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
|
||||
locations: 50 # Guarantees you will be able to access all locations, and therefore all items
|
||||
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
|
||||
progression_balancing: # A system to reduce BK, as in times during which you can't do anything, by moving your items into an earlier access sphere
|
||||
0: 0 # Choose a lower number if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
|
||||
25: 0
|
||||
50: 50 # Make it likely you have stuff to do.
|
||||
99: 0 # Get important items early, and stay at the front of the progression.
|
||||
Ocarina of Time:
|
||||
logic_rules: # Set the logic used for the generator.
|
||||
glitchless: 50
|
||||
glitched: 0
|
||||
no_logic: 0
|
||||
logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
|
||||
false: 50
|
||||
true: 0
|
||||
open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
|
||||
open: 50
|
||||
closed_deku: 0
|
||||
closed: 0
|
||||
open_kakariko: # Set the state of the Kakariko Village gate.
|
||||
open: 50
|
||||
zelda: 0
|
||||
closed: 0
|
||||
open_door_of_time: # Open the Door of Time by default, without the Song of Time.
|
||||
false: 0
|
||||
true: 50
|
||||
zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
|
||||
open: 0
|
||||
adult: 0
|
||||
closed: 50
|
||||
gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
|
||||
normal: 0
|
||||
fast: 50
|
||||
open: 0
|
||||
bridge: # Set the requirements for the Rainbow Bridge.
|
||||
open: 0
|
||||
vanilla: 0
|
||||
stones: 0
|
||||
medallions: 50
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
trials: # Set the number of required trials in Ganon's Castle.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 50 # minimum value
|
||||
6: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
starting_age: # Choose which age Link will start as.
|
||||
child: 50
|
||||
adult: 0
|
||||
triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
|
||||
false: 50
|
||||
true: 0
|
||||
triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
|
||||
# you can add additional values between minimum and maximum
|
||||
1: 0 # minimum value
|
||||
50: 0 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
20: 50
|
||||
bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
|
||||
false: 50
|
||||
true: 0
|
||||
bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
3: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_medallions: # Set the number of medallions required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
6: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
9: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
100: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
|
||||
remove: 0
|
||||
startwith: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_smallkeys: # Control where to shuffle dungeon small keys.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_hideoutkeys: # Control where to shuffle the Gerudo Fortress small keys.
|
||||
vanilla: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
|
||||
remove: 0
|
||||
vanilla: 0
|
||||
dungeon: 50
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
|
||||
remove: 50
|
||||
vanilla: 0
|
||||
dungeon: 0
|
||||
overworld: 0
|
||||
any_dungeon: 0
|
||||
keysanity: 0
|
||||
on_lacs: 0
|
||||
enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
|
||||
false: 50
|
||||
true: 0
|
||||
lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
|
||||
vanilla: 50
|
||||
stones: 0
|
||||
medallions: 0
|
||||
dungeons: 0
|
||||
tokens: 0
|
||||
lacs_stones: # Set the number of Spiritual Stones required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
3: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_medallions: # Set the number of medallions required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
6: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_rewards: # Set the number of dungeon rewards required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
9: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
|
||||
# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
100: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
shuffle_song_items: # Set where songs can appear.
|
||||
song: 50
|
||||
dungeon: 0
|
||||
any: 0
|
||||
shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
|
||||
0: 0
|
||||
1: 0
|
||||
2: 0
|
||||
3: 0
|
||||
4: 0
|
||||
random_value: 0
|
||||
off: 50
|
||||
tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
|
||||
off: 50
|
||||
dungeons: 0
|
||||
overworld: 0
|
||||
all: 0
|
||||
shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
|
||||
off: 50
|
||||
low: 0
|
||||
regular: 0
|
||||
random_prices: 0
|
||||
shuffle_cows: # Cows give items when Epona's Song is played.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
|
||||
false: 50
|
||||
true: 0
|
||||
shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
|
||||
false: 50
|
||||
true: 0
|
||||
skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
|
||||
false: 50
|
||||
true: 0
|
||||
no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
|
||||
false: 0
|
||||
true: 50
|
||||
no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
|
||||
false: 0
|
||||
true: 50
|
||||
no_epona_race: # Epona can always be summoned with Epona's Song.
|
||||
false: 0
|
||||
true: 50
|
||||
skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
|
||||
false: 0
|
||||
true: 50
|
||||
complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
|
||||
false: 50
|
||||
true: 0
|
||||
useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
|
||||
false: 0
|
||||
true: 50
|
||||
free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
|
||||
false: 50
|
||||
true: 0
|
||||
fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
|
||||
false: 50
|
||||
true: 0
|
||||
chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
|
||||
\# you can add additional values between minimum and maximum
|
||||
0: 0 # minimum value
|
||||
7: 50 # maximum value
|
||||
random: 0
|
||||
random-low: 0
|
||||
random-high: 0
|
||||
hints: # Gossip Stones can give hints about item locations.
|
||||
none: 0
|
||||
mask: 0
|
||||
agony: 0
|
||||
always: 50
|
||||
hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
|
||||
balanced: 50
|
||||
ddr: 0
|
||||
league: 0
|
||||
mw2: 0
|
||||
scrubs: 0
|
||||
strong: 0
|
||||
tournament: 0
|
||||
useless: 0
|
||||
very_strong: 0
|
||||
text_shuffle: # Randomizes text in the game for comedic effect.
|
||||
none: 50
|
||||
except_hints: 0
|
||||
complete: 0
|
||||
damage_multiplier: # Controls the amount of damage Link takes.
|
||||
half: 0
|
||||
normal: 50
|
||||
double: 0
|
||||
quadruple: 0
|
||||
ohko: 0
|
||||
no_collectible_hearts: # Hearts will not drop from enemies or objects.
|
||||
false: 50
|
||||
true: 0
|
||||
starting_tod: # Change the starting time of day.
|
||||
default: 50
|
||||
sunrise: 0
|
||||
morning: 0
|
||||
noon: 0
|
||||
afternoon: 0
|
||||
sunset: 0
|
||||
evening: 0
|
||||
midnight: 0
|
||||
witching_hour: 0
|
||||
start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
|
||||
false: 50
|
||||
true: 0
|
||||
start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
|
||||
false: 50
|
||||
true: 0
|
||||
item_pool_value: # Changes the number of items available in the game.
|
||||
plentiful: 0
|
||||
balanced: 50
|
||||
scarce: 0
|
||||
minimal: 0
|
||||
junk_ice_traps: # Adds ice traps to the item pool.
|
||||
off: 0
|
||||
normal: 50
|
||||
on: 0
|
||||
mayhem: 0
|
||||
onslaught: 0
|
||||
ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
|
||||
major_only: 50
|
||||
junk_only: 0
|
||||
anything: 0
|
||||
logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 50
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 0
|
||||
logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
|
||||
pocket_egg: 0
|
||||
pocket_cucco: 0
|
||||
cojiro: 0
|
||||
odd_mushroom: 0
|
||||
poachers_saw: 0
|
||||
broken_sword: 0
|
||||
prescription: 0
|
||||
eyeball_frog: 0
|
||||
eyedrops: 0
|
||||
claim_check: 50
|
||||
### Verifying your config file
|
||||
|
||||
```
|
||||
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
|
||||
validator page: [YAML Validation page](/mysterycheck)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
|
||||
@@ -1594,7 +1594,7 @@ def create_regions(self):
|
||||
connect(multiworld, player, "Menu", "Pallet Town", one_way=True)
|
||||
connect(multiworld, player, "Menu", "Pokedex", one_way=True)
|
||||
connect(multiworld, player, "Menu", "Evolution", one_way=True)
|
||||
connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state,
|
||||
connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state,
|
||||
state.multiworld.second_fossil_check_condition[player].value, player), one_way=True)
|
||||
connect(multiworld, player, "Pallet Town", "Route 1")
|
||||
connect(multiworld, player, "Route 1", "Viridian City")
|
||||
@@ -2269,23 +2269,28 @@ def create_regions(self):
|
||||
|
||||
event_locations = self.multiworld.get_filled_locations(player)
|
||||
|
||||
def adds_reachable_entrances(entrances_copy, item):
|
||||
def adds_reachable_entrances(entrances_copy, item, dead_end_cache):
|
||||
ret = dead_end_cache.get(item.name)
|
||||
if (ret != None):
|
||||
return ret
|
||||
|
||||
state_copy = state.copy()
|
||||
state_copy.collect(item, True)
|
||||
state.sweep_for_events(locations=event_locations)
|
||||
ret = len([entrance for entrance in entrances_copy if entrance in reachable_entrances or
|
||||
entrance.parent_region.can_reach(state_copy)]) > len(reachable_entrances)
|
||||
dead_end_cache[item.name] = ret
|
||||
return ret
|
||||
|
||||
def dead_end(entrances_copy, e):
|
||||
def dead_end(entrances_copy, e, dead_end_cache):
|
||||
region = e.parent_region
|
||||
check_warps = set()
|
||||
checked_regions = {region}
|
||||
check_warps.update(region.exits)
|
||||
check_warps.remove(e)
|
||||
for location in region.locations:
|
||||
if location.item and location.item.name in relevant_events and adds_reachable_entrances(entrances_copy,
|
||||
location.item):
|
||||
if location.item and location.item.name in relevant_events and \
|
||||
adds_reachable_entrances(entrances_copy, location.item, dead_end_cache):
|
||||
return False
|
||||
while check_warps:
|
||||
warp = check_warps.pop()
|
||||
@@ -2302,7 +2307,7 @@ def create_regions(self):
|
||||
check_warps.update(warp.connected_region.exits)
|
||||
for location in warp.connected_region.locations:
|
||||
if (location.item and location.item.name in relevant_events and
|
||||
adds_reachable_entrances(entrances_copy, location.item)):
|
||||
adds_reachable_entrances(entrances_copy, location.item, dead_end_cache)):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -2332,6 +2337,8 @@ def create_regions(self):
|
||||
if multiworld.door_shuffle[player] == "full" or len(entrances) != len(reachable_entrances):
|
||||
entrances.sort(key=lambda e: e.name not in entrance_only)
|
||||
|
||||
dead_end_cache = {}
|
||||
|
||||
# entrances list is empty while it's being sorted, must pass a copy to iterate through
|
||||
entrances_copy = entrances.copy()
|
||||
if multiworld.door_shuffle[player] == "decoupled":
|
||||
@@ -2342,10 +2349,10 @@ def create_regions(self):
|
||||
elif len(reachable_entrances) > (1 if multiworld.door_shuffle[player] == "insanity" else 8) and len(
|
||||
entrances) <= (starting_entrances - 3):
|
||||
entrances.sort(key=lambda e: 0 if e in reachable_entrances else 2 if
|
||||
dead_end(entrances_copy, e) else 1)
|
||||
dead_end(entrances_copy, e, dead_end_cache) else 1)
|
||||
else:
|
||||
entrances.sort(key=lambda e: 0 if e in reachable_entrances else 1 if
|
||||
dead_end(entrances_copy, e) else 2)
|
||||
dead_end(entrances_copy, e, dead_end_cache) else 2)
|
||||
if multiworld.door_shuffle[player] == "full":
|
||||
outdoor = outdoor_map(entrances[0].parent_region.name)
|
||||
if len(entrances) < 48 and not outdoor:
|
||||
|
||||
@@ -100,15 +100,15 @@ class StardewValleyWorld(World):
|
||||
return region
|
||||
|
||||
world_regions, self.randomized_entrances = create_regions(create_region, self.multiworld.random, self.options)
|
||||
self.multiworld.regions.extend(world_regions)
|
||||
|
||||
def add_location(name: str, code: Optional[int], region: str):
|
||||
region = self.multiworld.get_region(region, self.player)
|
||||
region = world_regions[region]
|
||||
location = StardewLocation(self.player, name, code, region)
|
||||
location.access_rule = lambda _: True
|
||||
region.locations.append(location)
|
||||
|
||||
create_locations(add_location, self.options, self.multiworld.random)
|
||||
self.multiworld.regions.extend(world_regions.values())
|
||||
|
||||
def create_items(self):
|
||||
self.precollect_starting_season()
|
||||
|
||||
@@ -429,7 +429,7 @@ def create_final_connections(world_options) -> List[ConnectionData]:
|
||||
|
||||
|
||||
def create_regions(region_factory: RegionFactory, random: Random, world_options) -> Tuple[
|
||||
Iterable[Region], Dict[str, str]]:
|
||||
Dict[str, Region], Dict[str, str]]:
|
||||
final_regions = create_final_regions(world_options)
|
||||
regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in
|
||||
final_regions}
|
||||
@@ -444,7 +444,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options)
|
||||
if connection.name in entrances:
|
||||
entrances[connection.name].connect(regions[connection.destination])
|
||||
|
||||
return regions.values(), randomized_data
|
||||
return regions, randomized_data
|
||||
|
||||
|
||||
def randomize_connections(random: Random, world_options, regions_by_name) -> Tuple[
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, Dict, List, Union, FrozenSet
|
||||
from typing import Iterable, Dict, List, Union, FrozenSet, Set
|
||||
|
||||
from BaseClasses import CollectionState, ItemClassification
|
||||
from .items import item_table
|
||||
@@ -14,13 +14,13 @@ class StardewRule:
|
||||
raise NotImplementedError
|
||||
|
||||
def __or__(self, other) -> StardewRule:
|
||||
if isinstance(other, Or):
|
||||
if type(other) is Or:
|
||||
return Or(self, *other.rules)
|
||||
|
||||
return Or(self, other)
|
||||
|
||||
def __and__(self, other) -> StardewRule:
|
||||
if isinstance(other, And):
|
||||
if type(other) is And:
|
||||
return And(other.rules.union({self}))
|
||||
|
||||
return And(self, other)
|
||||
@@ -80,28 +80,36 @@ class False_(StardewRule): # noqa
|
||||
return 999999999
|
||||
|
||||
|
||||
false_ = False_()
|
||||
true_ = True_()
|
||||
assert false_ is False_()
|
||||
assert true_ is True_()
|
||||
|
||||
|
||||
class Or(StardewRule):
|
||||
rules: FrozenSet[StardewRule]
|
||||
|
||||
def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
|
||||
rules_list = set()
|
||||
rules_list: Set[StardewRule]
|
||||
|
||||
if isinstance(rule, Iterable):
|
||||
rules_list.update(rule)
|
||||
rules_list = {*rule}
|
||||
else:
|
||||
rules_list.add(rule)
|
||||
rules_list = {rule}
|
||||
|
||||
if rules is not None:
|
||||
rules_list.update(rules)
|
||||
|
||||
assert rules_list, "Can't create a Or conditions without rules"
|
||||
|
||||
new_rules = set()
|
||||
for rule in rules_list:
|
||||
if isinstance(rule, Or):
|
||||
new_rules.update(rule.rules)
|
||||
else:
|
||||
new_rules.add(rule)
|
||||
rules_list = new_rules
|
||||
if any(type(rule) is Or for rule in rules_list):
|
||||
new_rules: Set[StardewRule] = set()
|
||||
for rule in rules_list:
|
||||
if type(rule) is Or:
|
||||
new_rules.update(rule.rules)
|
||||
else:
|
||||
new_rules.add(rule)
|
||||
rules_list = new_rules
|
||||
|
||||
self.rules = frozenset(rules_list)
|
||||
|
||||
@@ -112,11 +120,11 @@ class Or(StardewRule):
|
||||
return f"({' | '.join(repr(rule) for rule in self.rules)})"
|
||||
|
||||
def __or__(self, other):
|
||||
if isinstance(other, True_):
|
||||
if other is true_:
|
||||
return other
|
||||
if isinstance(other, False_):
|
||||
if other is false_:
|
||||
return self
|
||||
if isinstance(other, Or):
|
||||
if type(other) is Or:
|
||||
return Or(self.rules.union(other.rules))
|
||||
|
||||
return Or(self.rules.union({other}))
|
||||
@@ -131,17 +139,17 @@ class Or(StardewRule):
|
||||
return min(rule.get_difficulty() for rule in self.rules)
|
||||
|
||||
def simplify(self) -> StardewRule:
|
||||
if any(isinstance(rule, True_) for rule in self.rules):
|
||||
return True_()
|
||||
if true_ in self.rules:
|
||||
return true_
|
||||
|
||||
simplified_rules = {rule.simplify() for rule in self.rules}
|
||||
simplified_rules = {rule for rule in simplified_rules if rule is not False_()}
|
||||
simplified_rules = [simplified for simplified in {rule.simplify() for rule in self.rules}
|
||||
if simplified is not false_]
|
||||
|
||||
if not simplified_rules:
|
||||
return False_()
|
||||
return false_
|
||||
|
||||
if len(simplified_rules) == 1:
|
||||
return next(iter(simplified_rules))
|
||||
return simplified_rules[0]
|
||||
|
||||
return Or(simplified_rules)
|
||||
|
||||
@@ -150,25 +158,26 @@ class And(StardewRule):
|
||||
rules: FrozenSet[StardewRule]
|
||||
|
||||
def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
|
||||
rules_list = set()
|
||||
rules_list: Set[StardewRule]
|
||||
|
||||
if isinstance(rule, Iterable):
|
||||
rules_list.update(rule)
|
||||
rules_list = {*rule}
|
||||
else:
|
||||
rules_list.add(rule)
|
||||
rules_list = {rule}
|
||||
|
||||
if rules is not None:
|
||||
rules_list.update(rules)
|
||||
|
||||
if len(rules_list) < 1:
|
||||
rules_list.add(True_())
|
||||
|
||||
new_rules = set()
|
||||
for rule in rules_list:
|
||||
if isinstance(rule, And):
|
||||
new_rules.update(rule.rules)
|
||||
else:
|
||||
new_rules.add(rule)
|
||||
rules_list = new_rules
|
||||
if not rules_list:
|
||||
rules_list.add(true_)
|
||||
elif any(type(rule) is And for rule in rules_list):
|
||||
new_rules: Set[StardewRule] = set()
|
||||
for rule in rules_list:
|
||||
if type(rule) is And:
|
||||
new_rules.update(rule.rules)
|
||||
else:
|
||||
new_rules.add(rule)
|
||||
rules_list = new_rules
|
||||
|
||||
self.rules = frozenset(rules_list)
|
||||
|
||||
@@ -179,11 +188,11 @@ class And(StardewRule):
|
||||
return f"({' & '.join(repr(rule) for rule in self.rules)})"
|
||||
|
||||
def __and__(self, other):
|
||||
if isinstance(other, True_):
|
||||
if other is true_:
|
||||
return self
|
||||
if isinstance(other, False_):
|
||||
if other is false_:
|
||||
return other
|
||||
if isinstance(other, And):
|
||||
if type(other) is And:
|
||||
return And(self.rules.union(other.rules))
|
||||
|
||||
return And(self.rules.union({other}))
|
||||
@@ -198,17 +207,17 @@ class And(StardewRule):
|
||||
return max(rule.get_difficulty() for rule in self.rules)
|
||||
|
||||
def simplify(self) -> StardewRule:
|
||||
if any(isinstance(rule, False_) for rule in self.rules):
|
||||
return False_()
|
||||
if false_ in self.rules:
|
||||
return false_
|
||||
|
||||
simplified_rules = {rule.simplify() for rule in self.rules}
|
||||
simplified_rules = {rule for rule in simplified_rules if rule is not True_()}
|
||||
simplified_rules = [simplified for simplified in {rule.simplify() for rule in self.rules}
|
||||
if simplified is not true_]
|
||||
|
||||
if not simplified_rules:
|
||||
return True_()
|
||||
return true_
|
||||
|
||||
if len(simplified_rules) == 1:
|
||||
return next(iter(simplified_rules))
|
||||
return simplified_rules[0]
|
||||
|
||||
return And(simplified_rules)
|
||||
|
||||
@@ -218,11 +227,12 @@ class Count(StardewRule):
|
||||
rules: List[StardewRule]
|
||||
|
||||
def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule):
|
||||
rules_list = []
|
||||
rules_list: List[StardewRule]
|
||||
|
||||
if isinstance(rule, Iterable):
|
||||
rules_list.extend(rule)
|
||||
rules_list = [*rule]
|
||||
else:
|
||||
rules_list.append(rule)
|
||||
rules_list = [rule]
|
||||
|
||||
if rules is not None:
|
||||
rules_list.extend(rules)
|
||||
@@ -260,11 +270,12 @@ class TotalReceived(StardewRule):
|
||||
player: int
|
||||
|
||||
def __init__(self, count: int, items: Union[str, Iterable[str]], player: int):
|
||||
items_list = []
|
||||
items_list: List[str]
|
||||
|
||||
if isinstance(items, Iterable):
|
||||
items_list.extend(items)
|
||||
items_list = [*items]
|
||||
else:
|
||||
items_list.append(items)
|
||||
items_list = [items]
|
||||
|
||||
assert items_list, "Can't create a Total Received conditions without items"
|
||||
for item in items_list:
|
||||
|
||||
@@ -49,11 +49,9 @@ class TimespinnerWorld(World):
|
||||
|
||||
precalculated_weights: PreCalculatedWeights
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
self.precalculated_weights = PreCalculatedWeights(world, player)
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player)
|
||||
|
||||
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
|
||||
if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0:
|
||||
self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true
|
||||
|
||||
Reference in New Issue
Block a user