diff --git a/BaseClasses.py b/BaseClasses.py
index a5de1689a7..0d4f34e514 100644
--- a/BaseClasses.py
+++ b/BaseClasses.py
@@ -194,7 +194,9 @@ class MultiWorld():
self.player_types[new_id] = NetUtils.SlotType.group
world_type = AutoWorld.AutoWorldRegister.world_types[game]
self.worlds[new_id] = world_type.create_group(self, new_id, players)
- self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
+ self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id])
+ self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id])
+ self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id])
self.player_name[new_id] = name
new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
@@ -720,7 +722,7 @@ class CollectionState():
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
- assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
+ assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
@@ -946,6 +948,7 @@ class Entrance:
self.player = player
def can_reach(self, state: CollectionState) -> bool:
+ assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
@@ -1166,7 +1169,7 @@ class Location:
def can_reach(self, state: CollectionState) -> bool:
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
- assert self.parent_region, "Can't reach location without region"
+ assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region"
return self.parent_region.can_reach(state) and self.access_rule(state)
def place_locked_item(self, item: Item):
diff --git a/CommonClient.py b/CommonClient.py
index 6bdd8fc819..1aedd518b4 100644
--- a/CommonClient.py
+++ b/CommonClient.py
@@ -45,10 +45,21 @@ def get_ssl_context():
class ClientCommandProcessor(CommandProcessor):
+ """
+ The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called
+ when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit".
+
+ The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first
+ space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw
+ and method("one", "two", "three") without.
+
+ In addition all docstrings for command methods will be displayed to the user on launch and when using "/help"
+ """
def __init__(self, ctx: CommonContext):
self.ctx = ctx
def output(self, text: str):
+ """Helper function to abstract logging to the CommonClient UI"""
logger.info(text)
def _cmd_exit(self) -> bool:
@@ -164,13 +175,14 @@ class ClientCommandProcessor(CommandProcessor):
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def default(self, raw: str):
+ """The default message parser to be used when parsing any messages that do not match a command"""
raw = self.ctx.on_user_say(raw)
if raw:
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
class CommonContext:
- # Should be adjusted as needed in subclasses
+ # The following attributes are used to Connect and should be adjusted as needed in subclasses
tags: typing.Set[str] = {"AP"}
game: typing.Optional[str] = None
items_handling: typing.Optional[int] = None
@@ -429,7 +441,10 @@ class CommonContext:
self.auth = await self.console_input()
async def send_connect(self, **kwargs: typing.Any) -> None:
- """ send `Connect` packet to log in to server """
+ """
+ Send a `Connect` packet to log in to the server,
+ additional keyword args can override any value in the connection packet
+ """
payload = {
'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
@@ -459,6 +474,7 @@ class CommonContext:
return False
def slot_concerns_self(self, slot) -> bool:
+ """Helper function to abstract player groups, should be used instead of checking slot == self.slot directly."""
if slot == self.slot:
return True
if slot in self.slot_info:
@@ -466,6 +482,7 @@ class CommonContext:
return False
def is_echoed_chat(self, print_json_packet: dict) -> bool:
+ """Helper function for filtering out messages sent by self."""
return print_json_packet.get("type", "") == "Chat" \
and print_json_packet.get("team", None) == self.team \
and print_json_packet.get("slot", None) == self.slot
@@ -497,13 +514,14 @@ class CommonContext:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text
-
+
def on_ui_command(self, text: str) -> None:
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
The command processor is still called; this is just intended for command echoing."""
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
def update_permissions(self, permissions: typing.Dict[str, int]):
+ """Internal method to parse and save server permissions from RoomInfo"""
for permission_name, permission_flag in permissions.items():
try:
flag = Permission(permission_flag)
@@ -613,6 +631,7 @@ class CommonContext:
logger.info(f"DeathLink: Received from {data['source']}")
async def send_death(self, death_text: str = ""):
+ """Helper function to send a deathlink using death_text as the unique death cause string."""
if self.server and self.server.socket:
logger.info("DeathLink: Sending death to your friends...")
self.last_death_link = time.time()
@@ -626,6 +645,7 @@ class CommonContext:
}])
async def update_death_link(self, death_link: bool):
+ """Helper function to set Death Link connection tag on/off and update the connection if already connected."""
old_tags = self.tags.copy()
if death_link:
self.tags.add("DeathLink")
@@ -635,7 +655,7 @@ class CommonContext:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
- """Displays an error messagebox"""
+ """Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework"""
if not self.ui:
return None
title = title or "Error"
@@ -987,6 +1007,7 @@ async def console_loop(ctx: CommonContext):
def get_base_parser(description: typing.Optional[str] = None):
+ """Base argument parser to be reused for components subclassing off of CommonClient"""
import argparse
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
@@ -1037,6 +1058,7 @@ def run_as_textclient(*args):
parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args)
+ # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
if args.url:
url = urllib.parse.urlparse(args.url)
if url.scheme == "archipelago":
@@ -1048,6 +1070,7 @@ def run_as_textclient(*args):
else:
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
+ # use colorama to display colored text highlighting on windows
colorama.init()
asyncio.run(main(args))
diff --git a/WargrooveClient.py b/WargrooveClient.py
index 39da044d65..f9971f7a6c 100644
--- a/WargrooveClient.py
+++ b/WargrooveClient.py
@@ -267,9 +267,7 @@ class WargrooveContext(CommonContext):
def build(self):
container = super().build()
- panel = TabbedPanelItem(text="Wargroove")
- panel.content = self.build_tracker()
- self.tabs.add_widget(panel)
+ self.add_client_tab("Wargroove", self.build_tracker())
return container
def build_tracker(self) -> TrackerLayout:
diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html
index 5a53320408..947cf28372 100644
--- a/WebHostLib/templates/genericTracker.html
+++ b/WebHostLib/templates/genericTracker.html
@@ -99,14 +99,18 @@
{% if hint.finding_player == player %}
{{ player_names_with_alias[(team, hint.finding_player)] }}
{% else %}
- {{ player_names_with_alias[(team, hint.finding_player)] }}
+
+ {{ player_names_with_alias[(team, hint.finding_player)] }}
+
{% endif %}
{% if hint.receiving_player == player %}
{{ player_names_with_alias[(team, hint.receiving_player)] }}
{% else %}
- {{ player_names_with_alias[(team, hint.receiving_player)] }}
+
+ {{ player_names_with_alias[(team, hint.receiving_player)] }}
+
{% endif %}
|
{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} |
diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html
index 71a0f6747b..4e3747f4f9 100644
--- a/WebHostLib/templates/userContent.html
+++ b/WebHostLib/templates/userContent.html
@@ -1,5 +1,21 @@
{% extends 'tablepage.html' %}
+{%- macro games(slots) -%}
+ {%- set gameList = [] -%}
+ {%- set maxGamesToShow = 10 -%}
+
+ {%- for slot in (slots|list|sort(attribute="player_id"))[:maxGamesToShow] -%}
+ {% set player = "#" + slot["player_id"]|string + " " + slot["player_name"] + " : " + slot["game"] -%}
+ {% set _ = gameList.append(player) -%}
+ {%- endfor -%}
+
+ {%- if slots|length > maxGamesToShow -%}
+ {% set _ = gameList.append("... and " + (slots|length - maxGamesToShow)|string + " more") -%}
+ {%- endif -%}
+
+ {{ gameList|join('\n') }}
+{%- endmacro -%}
+
{% block head %}
{{ super() }}
User Content
@@ -33,10 +49,12 @@
| {{ room.seed.id|suuid }} |
{{ room.id|suuid }} |
- {{ room.seed.slots|length }} |
+
+ {{ room.seed.slots|length }}
+ |
{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }} |
{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }} |
- Delete next maintenance. |
+ Delete next maintenance. |
{% endfor %}
@@ -60,10 +78,15 @@
{% for seed in seeds %}
| {{ seed.id|suuid }} |
- {% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
+ |
+ {% if seed.multidata %}
+ {{ seed.slots|length }}
+ {% else %}
+ 1
+ {% endif %}
|
{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }} |
- Delete next maintenance. |
+ Delete next maintenance. |
{% endfor %}
diff --git a/kvui.py b/kvui.py
index 65cf52c7a4..536dce1220 100644
--- a/kvui.py
+++ b/kvui.py
@@ -536,9 +536,8 @@ class GameManager(App):
# show Archipelago tab if other logging is present
self.tabs.add_widget(panel)
- hint_panel = TabbedPanelItem(text="Hints")
- self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser)
- self.tabs.add_widget(hint_panel)
+ hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
+ self.log_panels["Hints"] = hint_panel.content
if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago"
@@ -572,6 +571,14 @@ class GameManager(App):
return self.container
+ def add_client_tab(self, title: str, content: Widget) -> Widget:
+ """Adds a new tab to the client window with a given title, and provides a given Widget as its content.
+ Returns the new tab widget, with the provided content being placed on the tab as content."""
+ new_tab = TabbedPanelItem(text=title)
+ new_tab.content = content
+ self.tabs.add_widget(new_tab)
+ return new_tab
+
def update_texts(self, dt):
if hasattr(self.tabs.content.children[0], "fix_heights"):
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 19ec9a14a8..f7dae2b927 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -342,7 +342,7 @@ class World(metaclass=AutoWorldRegister):
# overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_",
- # in that case the MultiWorld object is passed as an argument, and it gets called once for the entire multiworld.
+ # in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld.
# An example of this can be found in alttp as stage_pre_fill
@classmethod
diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py
index 74f2954b98..3627f385c2 100644
--- a/worlds/_bizhawk/__init__.py
+++ b/worlds/_bizhawk/__init__.py
@@ -223,8 +223,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None:
raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
-async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]],
- guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]:
+async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]],
+ guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]:
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
value.
@@ -266,7 +266,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[
return ret
-async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
+async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
"""Reads data at 1 or more addresses.
Items in `read_list` should be organized `(address, size, domain)` where
@@ -278,8 +278,8 @@ async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int
return await guarded_read(ctx, read_list, [])
-async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]],
- guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool:
+async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]],
+ guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool:
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
Items in `write_list` should be organized `(address, value, domain)` where
@@ -316,7 +316,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl
return True
-async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None:
+async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None:
"""Writes data to 1 or more addresses.
Items in write_list should be organized `(address, value, domain)` where
diff --git a/worlds/ahit/Client.py b/worlds/ahit/Client.py
index 2cd67e4682..cbb5f2a13d 100644
--- a/worlds/ahit/Client.py
+++ b/worlds/ahit/Client.py
@@ -4,7 +4,7 @@ import websockets
import functools
from copy import deepcopy
from typing import List, Any, Iterable
-from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem
+from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer
from MultiServer import Endpoint
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
@@ -101,12 +101,35 @@ class AHITContext(CommonContext):
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
- self.connected_msg = encode([args])
+ json = args
+ # This data is not needed and causes the game to freeze for long periods of time in large asyncs.
+ if "slot_info" in json.keys():
+ json["slot_info"] = {}
+ if "players" in json.keys():
+ me: NetworkPlayer
+ for n in json["players"]:
+ if n.slot == json["slot"] and n.team == json["team"]:
+ me = n
+ break
+
+ # Only put our player info in there as we actually need it
+ json["players"] = [me]
+ if DEBUG:
+ print(json)
+ self.connected_msg = encode([json])
if self.awaiting_info:
self.server_msgs.append(self.room_info)
self.update_items()
self.awaiting_info = False
+ elif cmd == "RoomUpdate":
+ # Same story as above
+ json = args
+ if "players" in json.keys():
+ json["players"] = []
+
+ self.server_msgs.append(encode(json))
+
elif cmd == "ReceivedItems":
if args["index"] == 0:
self.full_inventory.clear()
@@ -166,6 +189,17 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
await ctx.disconnect_proxy()
break
+ if ctx.auth:
+ name = msg.get("name", "")
+ if name != "" and name != ctx.auth:
+ logger.info("Aborting proxy connection: player name mismatch from save file")
+ logger.info(f"Expected: {ctx.auth}, got: {name}")
+ text = encode([{"cmd": "PrintJSON",
+ "data": [{"text": "Connection aborted - player name mismatch"}]}])
+ await ctx.send_msgs_proxy(text)
+ await ctx.disconnect_proxy()
+ break
+
if ctx.connected_msg and ctx.is_connected():
await ctx.send_msgs_proxy(ctx.connected_msg)
ctx.update_items()
diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py
index ef74cadcaa..ce339c7c19 100644
--- a/worlds/ahit/DeathWishLocations.py
+++ b/worlds/ahit/DeathWishLocations.py
@@ -152,10 +152,10 @@ def create_dw_regions(world: "HatInTimeWorld"):
for name in annoying_dws:
world.excluded_dws.append(name)
- if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses:
+ if not world.options.DWEnableBonus and world.options.DWAutoCompleteBonuses:
for name in death_wishes:
world.excluded_bonuses.append(name)
- elif world.options.DWExcludeAnnoyingBonuses:
+ if world.options.DWExcludeAnnoyingBonuses and not world.options.DWAutoCompleteBonuses:
for name in annoying_bonuses:
world.excluded_bonuses.append(name)
diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py
index dd5e88abbc..14cf13ec34 100644
--- a/worlds/ahit/__init__.py
+++ b/worlds/ahit/__init__.py
@@ -253,7 +253,8 @@ class HatInTimeWorld(World):
else:
item_name = loc.item.name
- shop_item_names.setdefault(str(loc.address), item_name)
+ shop_item_names.setdefault(str(loc.address),
+ f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})")
slot_data["ShopItemNames"] = shop_item_names
diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py
index fe261dc94d..ffe9efd2de 100644
--- a/worlds/bumpstik/__init__.py
+++ b/worlds/bumpstik/__init__.py
@@ -125,6 +125,6 @@ class BumpStikWorld(World):
lambda state: state.has("Hazard Bumper", self.player, 25)
self.multiworld.completion_condition[self.player] = \
- lambda state: state.has("Booster Bumper", self.player, 5) and \
- state.has("Treasure Bumper", self.player, 32)
+ lambda state: state.has_all_counts({"Booster Bumper": 5, "Treasure Bumper": 32, "Hazard Bumper": 25}, \
+ self.player)
diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md
index ed90289a8b..9755cce1c6 100644
--- a/worlds/dark_souls_3/docs/setup_en.md
+++ b/worlds/dark_souls_3/docs/setup_en.md
@@ -3,7 +3,7 @@
## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
-- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
+- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest)
## Optional Software
@@ -11,8 +11,9 @@
## Setting Up
-First, download the client from the link above. It doesn't need to go into any particular directory;
-it'll automatically locate _Dark Souls III_ in your Steam installation folder.
+First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go
+into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam
+installation folder.
Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This
is the latest version, so you don't need to do any downpatching! However, if you've already
@@ -35,8 +36,9 @@ randomized item and (optionally) enemy locations. You only need to do this once
To run _Dark Souls III_ in Archipelago mode:
-1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the
- DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn.
+1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain
+ scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu
+ screen.
2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that
you can use to interact with the Archipelago server.
@@ -52,4 +54,21 @@ To run _Dark Souls III_ in Archipelago mode:
### Where do I get a config file?
The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to
-configure your personal options and export them into a config file.
+configure your personal options and export them into a config file. The [AP client archive] also
+includes an options template.
+
+[AP client archive]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest
+
+### Does this work with Proton?
+
+The *Dark Souls III* Archipelago randomizer supports running on Linux under Proton. There are a few
+things to keep in mind:
+
+* Because `DS3Randomizer.exe` relies on the .NET runtime, you'll need to install
+ the [.NET Runtime] under **plain [WINE]**, then run `DS3Randomizer.exe` under
+ plain WINE as well. It won't work as a Proton app!
+
+* To run the game itself, just run `launchmod_darksouls3.bat` under Proton.
+
+[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
+[WINE]: https://www.winehq.org/
diff --git a/worlds/doom_1993/Locations.py b/worlds/doom_1993/Locations.py
index 2cbb9b9d15..90a6916cd7 100644
--- a/worlds/doom_1993/Locations.py
+++ b/worlds/doom_1993/Locations.py
@@ -2214,13 +2214,13 @@ location_table: Dict[int, LocationDict] = {
'map': 2,
'index': 217,
'doom_type': 2006,
- 'region': "Perfect Hatred (E4M2) Blue"},
+ 'region': "Perfect Hatred (E4M2) Upper"},
351367: {'name': 'Perfect Hatred (E4M2) - Exit',
'episode': 4,
'map': 2,
'index': -1,
'doom_type': -1,
- 'region': "Perfect Hatred (E4M2) Blue"},
+ 'region': "Perfect Hatred (E4M2) Upper"},
351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability',
'episode': 4,
'map': 3,
diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py
index f013bdceaf..c32f7b4701 100644
--- a/worlds/doom_1993/Regions.py
+++ b/worlds/doom_1993/Regions.py
@@ -502,13 +502,12 @@ regions:List[RegionDict] = [
"episode":4,
"connections":[
{"target":"Perfect Hatred (E4M2) Blue","pro":False},
- {"target":"Perfect Hatred (E4M2) Yellow","pro":False}]},
+ {"target":"Perfect Hatred (E4M2) Yellow","pro":False},
+ {"target":"Perfect Hatred (E4M2) Upper","pro":True}]},
{"name":"Perfect Hatred (E4M2) Blue",
"connects_to_hub":False,
"episode":4,
- "connections":[
- {"target":"Perfect Hatred (E4M2) Main","pro":False},
- {"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
+ "connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]},
{"name":"Perfect Hatred (E4M2) Yellow",
"connects_to_hub":False,
"episode":4,
@@ -518,7 +517,13 @@ regions:List[RegionDict] = [
{"name":"Perfect Hatred (E4M2) Cave",
"connects_to_hub":False,
"episode":4,
- "connections":[]},
+ "connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]},
+ {"name":"Perfect Hatred (E4M2) Upper",
+ "connects_to_hub":False,
+ "episode":4,
+ "connections":[
+ {"target":"Perfect Hatred (E4M2) Cave","pro":False},
+ {"target":"Perfect Hatred (E4M2) Main","pro":False}]},
# Sever the Wicked (E4M3)
{"name":"Sever the Wicked (E4M3) Main",
diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py
index 4faeb4a27d..89b09ff9f2 100644
--- a/worlds/doom_1993/Rules.py
+++ b/worlds/doom_1993/Rules.py
@@ -403,9 +403,8 @@ def set_episode4_rules(player, multiworld, pro):
state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state:
- state.has("Shotgun", player, 1) or
- state.has("Chaingun", player, 1) or
- state.has("Hell Beneath (E4M1) - Blue skull key", player, 1))
+ (state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or
+ state.has("Chaingun", player, 1)))
# Perfect Hatred (E4M2)
set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state:
diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md
index 161b1e465b..1980e81cbc 100644
--- a/worlds/generic/docs/plando_en.md
+++ b/worlds/generic/docs/plando_en.md
@@ -22,9 +22,9 @@ enabled (opt-in).
* You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
```yaml
- requires:
- version: current.version.number
- plando: bosses, items, texts, connections
+requires:
+ version: current.version.number
+ plando: bosses, items, texts, connections
```
## Item Plando
@@ -74,77 +74,77 @@ A list of all available items and locations can be found in the [website's datap
### Examples
```yaml
-plando_items:
- # example block 1 - Timespinner
- - item:
- Empire Orb: 1
- Radiant Orb: 1
- location: Starter Chest 1
- from_pool: true
- world: true
- percentage: 50
-
- # example block 2 - Ocarina of Time
- - items:
- Kokiri Sword: 1
- Biggoron Sword: 1
- Bow: 1
- Magic Meter: 1
- Progressive Strength Upgrade: 3
- Progressive Hookshot: 2
- locations:
- - Deku Tree Slingshot Chest
- - Dodongos Cavern Bomb Bag Chest
- - Jabu Jabus Belly Boomerang Chest
- - Bottom of the Well Lens of Truth Chest
- - Forest Temple Bow Chest
- - Fire Temple Megaton Hammer Chest
- - Water Temple Longshot Chest
- - Shadow Temple Hover Boots Chest
- - Spirit Temple Silver Gauntlets Chest
- world: false
-
- # example block 3 - Slay the Spire
- - items:
- Boss Relic: 3
- locations:
- - Boss Relic 1
- - Boss Relic 2
- - Boss Relic 3
-
- # example block 4 - Factorio
- - items:
- progressive-electric-energy-distribution: 2
- electric-energy-accumulators: 1
- progressive-turret: 2
- locations:
- - military
- - gun-turret
- - logistic-science-pack
- - steel-processing
- percentage: 80
- force: true
-
-# example block 5 - Secret of Evermore
- - items:
- Levitate: 1
- Revealer: 1
- Energize: 1
- locations:
- - Master Sword Pedestal
- - Boss Relic 1
- world: true
- count: 2
-
-# example block 6 - A Link to the Past
- - items:
- Progressive Sword: 4
- world:
- - BobsSlaytheSpire
- - BobsRogueLegacy
- count:
- min: 1
- max: 4
+ plando_items:
+ # example block 1 - Timespinner
+ - item:
+ Empire Orb: 1
+ Radiant Orb: 1
+ location: Starter Chest 1
+ from_pool: true
+ world: true
+ percentage: 50
+
+ # example block 2 - Ocarina of Time
+ - items:
+ Kokiri Sword: 1
+ Biggoron Sword: 1
+ Bow: 1
+ Magic Meter: 1
+ Progressive Strength Upgrade: 3
+ Progressive Hookshot: 2
+ locations:
+ - Deku Tree Slingshot Chest
+ - Dodongos Cavern Bomb Bag Chest
+ - Jabu Jabus Belly Boomerang Chest
+ - Bottom of the Well Lens of Truth Chest
+ - Forest Temple Bow Chest
+ - Fire Temple Megaton Hammer Chest
+ - Water Temple Longshot Chest
+ - Shadow Temple Hover Boots Chest
+ - Spirit Temple Silver Gauntlets Chest
+ world: false
+
+ # example block 3 - Slay the Spire
+ - items:
+ Boss Relic: 3
+ locations:
+ - Boss Relic 1
+ - Boss Relic 2
+ - Boss Relic 3
+
+ # example block 4 - Factorio
+ - items:
+ progressive-electric-energy-distribution: 2
+ electric-energy-accumulators: 1
+ progressive-turret: 2
+ locations:
+ - military
+ - gun-turret
+ - logistic-science-pack
+ - steel-processing
+ percentage: 80
+ force: true
+
+ # example block 5 - Secret of Evermore
+ - items:
+ Levitate: 1
+ Revealer: 1
+ Energize: 1
+ locations:
+ - Master Sword Pedestal
+ - Boss Relic 1
+ world: true
+ count: 2
+
+ # example block 6 - A Link to the Past
+ - items:
+ Progressive Sword: 4
+ world:
+ - BobsSlaytheSpire
+ - BobsRogueLegacy
+ count:
+ min: 1
+ max: 4
```
1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another
player's Starter Chest 1 and removes the chosen item from the item pool.
@@ -221,25 +221,25 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections).
### Examples
```yaml
-plando_connections:
- # example block 1 - A Link to the Past
- - entrance: Cave Shop (Lake Hylia)
- exit: Cave 45
- direction: entrance
- - entrance: Cave 45
- exit: Cave Shop (Lake Hylia)
- direction: entrance
- - entrance: Agahnims Tower
- exit: Old Man Cave Exit (West)
- direction: exit
-
- # example block 2 - Minecraft
- - entrance: Overworld Structure 1
- exit: Nether Fortress
- direction: both
- - entrance: Overworld Structure 2
- exit: Village
- direction: both
+ plando_connections:
+ # example block 1 - A Link to the Past
+ - entrance: Cave Shop (Lake Hylia)
+ exit: Cave 45
+ direction: entrance
+ - entrance: Cave 45
+ exit: Cave Shop (Lake Hylia)
+ direction: entrance
+ - entrance: Agahnims Tower
+ exit: Old Man Cave Exit (West)
+ direction: exit
+
+ # example block 2 - Minecraft
+ - entrance: Overworld Structure 1
+ exit: Nether Fortress
+ direction: both
+ - entrance: Overworld Structure 2
+ exit: Village
+ direction: both
```
1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and
diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py
index 860243ee95..9ec77e6bf0 100644
--- a/worlds/hk/__init__.py
+++ b/worlds/hk/__init__.py
@@ -21,6 +21,16 @@ from .Charms import names as charm_names
from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState
from worlds.AutoWorld import World, LogicMixin, WebWorld
+from settings import Group, Bool
+
+
+class HollowKnightSettings(Group):
+ class DisableMapModSpoilers(Bool):
+ """Disallows the APMapMod from showing spoiler placements."""
+
+ disable_spoilers: typing.Union[DisableMapModSpoilers, bool] = False
+
+
path_of_pain_locations = {
"Soul_Totem-Path_of_Pain_Below_Thornskip",
"Lore_Tablet-Path_of_Pain_Entrance",
@@ -124,14 +134,25 @@ shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
class HKWeb(WebWorld):
- tutorials = [Tutorial(
+ setup_en = Tutorial(
"Mod Setup and Use Guide",
"A guide to playing Hollow Knight with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["Ijwu"]
- )]
+ )
+
+ setup_pt_br = Tutorial(
+ setup_en.tutorial_name,
+ setup_en.description,
+ "Português Brasileiro",
+ "setup_pt_br.md",
+ "setup/pt_br",
+ ["JoaoVictor-FA"]
+ )
+
+ tutorials = [setup_en, setup_pt_br]
bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title="
@@ -145,6 +166,7 @@ class HKWorld(World):
game: str = "Hollow Knight"
options_dataclass = HKOptions
options: HKOptions
+ settings: typing.ClassVar[HollowKnightSettings]
web = HKWeb()
@@ -512,26 +534,16 @@ class HKWorld(World):
for option_name in hollow_knight_options:
option = getattr(self.options, option_name)
try:
+ # exclude more complex types - we only care about int, bool, enum for player options; the client
+ # can get them back to the necessary type.
optionvalue = int(option.value)
- except TypeError:
- pass # C# side is currently typed as dict[str, int], drop what doesn't fit
- else:
options[option_name] = optionvalue
+ except TypeError:
+ pass
# 32 bit int
slot_data["seed"] = self.random.randint(-2147483647, 2147483646)
- # Backwards compatibility for shop cost data (HKAP < 0.1.0)
- if not self.options.CostSanity:
- for shop, terms in shop_cost_types.items():
- unit = cost_terms[next(iter(terms))].option
- if unit == "Geo":
- continue
- slot_data[f"{unit}_costs"] = {
- loc.name: next(iter(loc.costs.values()))
- for loc in self.created_multi_locations[shop]
- }
-
# HKAP 0.1.0 and later cost data.
location_costs = {}
for region in self.multiworld.get_regions(self.player):
@@ -544,6 +556,8 @@ class HKWorld(World):
slot_data["grub_count"] = self.grub_count
+ slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race
+
return slot_data
def create_item(self, name: str) -> HKItem:
diff --git a/worlds/hk/docs/setup_pt_br.md b/worlds/hk/docs/setup_pt_br.md
new file mode 100644
index 0000000000..9ae1ea89d5
--- /dev/null
+++ b/worlds/hk/docs/setup_pt_br.md
@@ -0,0 +1,52 @@
+# Guia de configuração para Hollow Knight no Archipelago
+
+## Programas obrigatórios
+* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/).
+* Uma cópia legal de Hollow Knight.
+ * Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
+ * Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
+
+## Instalando o mod Archipelago Mod usando Lumafly
+1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight.
+2. Clique em "Install (instalar)" perto da opção "Archipelago" mod.
+ * Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo.
+3. Abra o jogo, tudo preparado!
+
+### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação
+1. Encontre a pasta manualmente.
+ * Xbox Game Pass:
+ 1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda.
+ 2. Clique nos 3 pontos depois clique gerenciar.
+ 3. Vá nos arquivos e selecione procurar.
+ 4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie.
+ * Steam:
+ 1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está.
+ . Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço.
+ * Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
+ * Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight`
+ * Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app`
+2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou.
+
+## Configurando seu arquivo YAML
+### O que é um YAML e por que eu preciso de um?
+Um arquivo YAML é a forma que você informa suas configurações do jogador para o Archipelago.
+Olhe o [guia de configuração básica de um multiworld](/tutorial/Archipelago/setup/en) aqui no site do Archipelago para aprender mais.
+
+### Onde eu consigo o YAML?
+Você pode usar a [página de configurações do jogador para Hollow Knight](/games/Hollow%20Knight/player-options) aqui no site do Archipelago
+para gerar o YAML usando a interface gráfica.
+
+### Entrando numa partida de Archipelago no Hollow Knight
+1. Começe o jogo depois de instalar todos os mods necessários.
+2. Crie um **novo jogo salvo.**
+3. Selecione o modo de jogo **Archipelago** do menu de seleção.
+4. Coloque as configurações corretas do seu servidor Archipelago.
+5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens.
+6. O jogo vai te colocar imediatamente numa partida randomizada.
+ * Se você está esperando uma contagem então espere ele cair antes de apertar começar.
+ * Ou clique em começar e pause o jogo enquanto estiver nele.
+
+## Dicas e outros comandos
+Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no
+[Guia de comandos](/tutorial/Archipelago/commands/en). Você pode usar o cliente de texto do Archipelago para isso,
+que está incluido na ultima versão do [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
diff --git a/worlds/kh1/Client.py b/worlds/kh1/Client.py
index acfd5dba38..33fba85f6c 100644
--- a/worlds/kh1/Client.py
+++ b/worlds/kh1/Client.py
@@ -31,6 +31,9 @@ def check_stdin() -> None:
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
class KH1ClientCommandProcessor(ClientCommandProcessor):
+ def __init__(self, ctx):
+ super().__init__(ctx)
+
def _cmd_deathlink(self):
"""Toggles Deathlink"""
global death_link
@@ -40,6 +43,40 @@ class KH1ClientCommandProcessor(ClientCommandProcessor):
else:
death_link = True
self.output(f"Death Link turned on")
+
+ def _cmd_goal(self):
+ """Prints goal setting"""
+ if "goal" in self.ctx.slot_data.keys():
+ self.output(str(self.ctx.slot_data["goal"]))
+ else:
+ self.output("Unknown")
+
+ def _cmd_eotw_unlock(self):
+ """Prints End of the World Unlock setting"""
+ if "required_reports_door" in self.ctx.slot_data.keys():
+ if self.ctx.slot_data["required_reports_door"] > 13:
+ self.output("Item")
+ else:
+ self.output(str(self.ctx.slot_data["required_reports_eotw"]) + " reports")
+ else:
+ self.output("Unknown")
+
+ def _cmd_door_unlock(self):
+ """Prints Final Rest Door Unlock setting"""
+ if "door" in self.ctx.slot_data.keys():
+ if self.ctx.slot_data["door"] == "reports":
+ self.output(str(self.ctx.slot_data["required_reports_door"]) + " reports")
+ else:
+ self.output(str(self.ctx.slot_data["door"]))
+ else:
+ self.output("Unknown")
+
+ def _cmd_advanced_logic(self):
+ """Prints advanced logic setting"""
+ if "advanced_logic" in self.ctx.slot_data.keys():
+ self.output(str(self.ctx.slot_data["advanced_logic"]))
+ else:
+ self.output("Unknown")
class KH1Context(CommonContext):
command_processor: int = KH1ClientCommandProcessor
@@ -51,6 +88,8 @@ class KH1Context(CommonContext):
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
+ self.hinted_synth_location_ids = False
+ self.slot_data = {}
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM")
@@ -104,6 +143,7 @@ class KH1Context(CommonContext):
f.close()
#Handle Slot Data
+ self.slot_data = args['slot_data']
for key in list(args['slot_data'].keys()):
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f:
f.write(str(args['slot_data'][key]))
@@ -217,11 +257,13 @@ async def game_watcher(ctx: KH1Context):
if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10:
await ctx.send_death(death_text = "Sora was defeated!")
if file.find("insynthshop") > -1:
- await ctx.send_msgs([{
- "cmd": "LocationScouts",
- "locations": [2656401,2656402,2656403,2656404,2656405,2656406],
- "create_as_hint": 2
- }])
+ if not ctx.hinted_synth_location_ids:
+ await ctx.send_msgs([{
+ "cmd": "LocationScouts",
+ "locations": [2656401,2656402,2656403,2656404,2656405,2656406],
+ "create_as_hint": 2
+ }])
+ ctx.hinted_synth_location_ids = True
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py
index 9f4784f749..1f9358a4f5 100644
--- a/worlds/ladx/Items.py
+++ b/worlds/ladx/Items.py
@@ -83,8 +83,8 @@ class ItemName:
RUPEES_200 = "200 Rupees"
RUPEES_500 = "500 Rupees"
SEASHELL = "Seashell"
- MESSAGE = "Master Stalfos' Message"
- GEL = "Gel"
+ MESSAGE = "Nothing"
+ GEL = "Zol Attack"
BOOMERANG = "Boomerang"
HEART_PIECE = "Heart Piece"
BOWWOW = "BowWow"
diff --git a/worlds/ladx/LADXR/patches/goldenLeaf.py b/worlds/ladx/LADXR/patches/goldenLeaf.py
index 87cefae0f6..b35c722a43 100644
--- a/worlds/ladx/LADXR/patches/goldenLeaf.py
+++ b/worlds/ladx/LADXR/patches/goldenLeaf.py
@@ -29,6 +29,7 @@ def fixGoldenLeaf(rom):
rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard
rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard
rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves
- rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path
+ rom.patch(0x06, 0x00B6, ASM("ld a, $FF"), ASM("ld a, $06"))
+ rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores 6 in the leaf counter if we opened the path (instead of FF, so that nothing breaks if we get more for some reason)
# 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message.
# rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler
diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py
index 79f1fe470f..2846b40e67 100644
--- a/worlds/ladx/__init__.py
+++ b/worlds/ladx/__init__.py
@@ -216,7 +216,7 @@ class LinksAwakeningWorld(World):
for _ in range(count):
if item_name in exclude:
exclude.remove(item_name) # this is destructive. create unique list above
- self.multiworld.itempool.append(self.create_item("Master Stalfos' Message"))
+ self.multiworld.itempool.append(self.create_item("Nothing"))
else:
item = self.create_item(item_name)
@@ -513,6 +513,9 @@ class LinksAwakeningWorld(World):
state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name]
return change
+ def get_filler_item_name(self) -> str:
+ return "Nothing"
+
def fill_slot_data(self):
slot_data = {}
diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md
index 8248a4755d..a68ee5ba4c 100644
--- a/worlds/messenger/docs/en_The Messenger.md
+++ b/worlds/messenger/docs/en_The Messenger.md
@@ -39,7 +39,9 @@ You can find items wherever items can be picked up in the original game. This in
When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a
group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint
-for it. The groups you can use for The Messenger are:
+for it.
+
+The groups you can use for The Messenger are:
* Notes - This covers the music notes
* Keys - An alternative name for the music notes
* Crest - The Sun and Moon Crests
@@ -50,26 +52,26 @@ for it. The groups you can use for The Messenger are:
* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu
* This can cause issues if used at specific times. If used in any of these known problematic areas, immediately
-quit to title and reload the save. The currently known areas include:
+ quit to title and reload the save. The currently known areas include:
* During Boss fights
* After Courage Note collection (Corrupted Future chase)
* After reaching ninja village a teleport option is added to the menu to reach it quickly
* Toggle Windmill Shuriken button is added to option menu once the item is received
* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed
-when the player fulfills the necessary conditions.
+ when the player fulfills the necessary conditions.
* After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be
-used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
-be entered in game.
+ used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
+ be entered in game.
## Known issues
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
* If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
-to Searing Crags and re-enter to get it to play correctly.
+ to Searing Crags and re-enter to get it to play correctly.
* Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left
-and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock
+ and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock
* Text entry menus don't accept controller input
* In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the
-chest will not work.
+ chest will not work.
## What do I do if I have a problem?
diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md
index c1770e7474..64b706c264 100644
--- a/worlds/messenger/docs/setup_en.md
+++ b/worlds/messenger/docs/setup_en.md
@@ -41,14 +41,27 @@ These steps can also be followed to launch the game and check for mod updates af
## Joining a MultiWorld Game
+### Automatic Connection on archipelago.gg
+
+1. Go to the room page of the MultiWorld you are going to join.
+2. Click on your slot name on the left side.
+3. Click the "The Messenger" button in the prompt.
+4. Follow the remaining prompts. This process will check that you have the mod installed and will also check for updates
+ before launching The Messenger. If you are using the Steam version of The Messenger you may also get a prompt from
+ Steam asking if the game should be launched with arguments. These arguments are the URI which the mod uses to
+ connect.
+5. Start a new save. You will already be connected in The Messenger and do not need to go through the menus.
+
+### Manual Connection
+
1. Launch the game
2. Navigate to `Options > Archipelago Options`
3. Enter connection info using the relevant option buttons
* **The game is limited to alphanumerical characters, `.`, and `-`.**
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
-website.
+ website.
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
-directory. When using this, all connection information must be entered in the file.
+ directory. When using this, all connection information must be entered in the file.
4. Select the `Connect to Archipelago` button
5. Navigate to save file selection
6. Start a new game
diff --git a/worlds/oot/Cosmetics.py b/worlds/oot/Cosmetics.py
index f40f8a1ebb..4a748c60aa 100644
--- a/worlds/oot/Cosmetics.py
+++ b/worlds/oot/Cosmetics.py
@@ -1,9 +1,9 @@
from .Utils import data_path, __version__
from .Colors import *
import logging
-import worlds.oot.Music as music
-import worlds.oot.Sounds as sfx
-import worlds.oot.IconManip as icon
+from . import Music as music
+from . import Sounds as sfx
+from . import IconManip as icon
from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict
import json
@@ -105,7 +105,7 @@ def patch_tunic_colors(rom, ootworld, symbols):
# handle random
if tunic_option == 'Random Choice':
- tunic_option = random.choice(tunic_color_list)
+ tunic_option = ootworld.random.choice(tunic_color_list)
# handle completely random
if tunic_option == 'Completely Random':
color = generate_random_color()
@@ -156,9 +156,9 @@ def patch_navi_colors(rom, ootworld, symbols):
# choose a random choice for the whole group
if navi_option_inner == 'Random Choice':
- navi_option_inner = random.choice(navi_color_list)
+ navi_option_inner = ootworld.random.choice(navi_color_list)
if navi_option_outer == 'Random Choice':
- navi_option_outer = random.choice(navi_color_list)
+ navi_option_outer = ootworld.random.choice(navi_color_list)
if navi_option_outer == 'Match Inner':
navi_option_outer = navi_option_inner
@@ -233,9 +233,9 @@ def patch_sword_trails(rom, ootworld, symbols):
# handle random choice
if option_inner == 'Random Choice':
- option_inner = random.choice(sword_trail_color_list)
+ option_inner = ootworld.random.choice(sword_trail_color_list)
if option_outer == 'Random Choice':
- option_outer = random.choice(sword_trail_color_list)
+ option_outer = ootworld.random.choice(sword_trail_color_list)
if option_outer == 'Match Inner':
option_outer = option_inner
@@ -326,9 +326,9 @@ def patch_trails(rom, ootworld, trails):
# handle random choice
if option_inner == 'Random Choice':
- option_inner = random.choice(trail_color_list)
+ option_inner = ootworld.random.choice(trail_color_list)
if option_outer == 'Random Choice':
- option_outer = random.choice(trail_color_list)
+ option_outer = ootworld.random.choice(trail_color_list)
if option_outer == 'Match Inner':
option_outer = option_inner
@@ -393,7 +393,7 @@ def patch_gauntlet_colors(rom, ootworld, symbols):
# handle random
if gauntlet_option == 'Random Choice':
- gauntlet_option = random.choice(gauntlet_color_list)
+ gauntlet_option = ootworld.random.choice(gauntlet_color_list)
# handle completely random
if gauntlet_option == 'Completely Random':
color = generate_random_color()
@@ -424,10 +424,10 @@ def patch_shield_frame_colors(rom, ootworld, symbols):
# handle random
if shield_frame_option == 'Random Choice':
- shield_frame_option = random.choice(shield_frame_color_list)
+ shield_frame_option = ootworld.random.choice(shield_frame_color_list)
# handle completely random
if shield_frame_option == 'Completely Random':
- color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
+ color = [ootworld.random.getrandbits(8), ootworld.random.getrandbits(8), ootworld.random.getrandbits(8)]
# grab the color from the list
elif shield_frame_option in shield_frame_colors:
color = list(shield_frame_colors[shield_frame_option])
@@ -458,7 +458,7 @@ def patch_heart_colors(rom, ootworld, symbols):
# handle random
if heart_option == 'Random Choice':
- heart_option = random.choice(heart_color_list)
+ heart_option = ootworld.random.choice(heart_color_list)
# handle completely random
if heart_option == 'Completely Random':
color = generate_random_color()
@@ -495,7 +495,7 @@ def patch_magic_colors(rom, ootworld, symbols):
magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting])
if magic_option == 'Random Choice':
- magic_option = random.choice(magic_color_list)
+ magic_option = ootworld.random.choice(magic_color_list)
if magic_option == 'Completely Random':
color = generate_random_color()
@@ -559,7 +559,7 @@ def patch_button_colors(rom, ootworld, symbols):
# handle random
if button_option == 'Random Choice':
- button_option = random.choice(list(button_colors.keys()))
+ button_option = ootworld.random.choice(list(button_colors.keys()))
# handle completely random
if button_option == 'Completely Random':
fixed_font_color = [10, 10, 10]
@@ -618,11 +618,11 @@ def patch_sfx(rom, ootworld, symbols):
rom.write_int16(loc, sound_id)
else:
if selection == 'random-choice':
- selection = random.choice(sfx.get_hook_pool(hook)).value.keyword
+ selection = ootworld.random.choice(sfx.get_hook_pool(hook)).value.keyword
elif selection == 'random-ear-safe':
- selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
+ selection = ootworld.random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
elif selection == 'completely-random':
- selection = random.choice(sfx.standard).value.keyword
+ selection = ootworld.random.choice(sfx.standard).value.keyword
sound_id = sound_dict[selection]
for loc in hook.value.locations:
rom.write_int16(loc, sound_id)
@@ -644,7 +644,7 @@ def patch_instrument(rom, ootworld, symbols):
choice = ootworld.sfx_ocarina
if choice == 'random-choice':
- choice = random.choice(list(instruments.keys()))
+ choice = ootworld.random.choice(list(instruments.keys()))
rom.write_byte(0x00B53C7B, instruments[choice])
rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods
@@ -769,7 +769,6 @@ patch_sets[0x1F073FD9] = {
def patch_cosmetics(ootworld, rom):
# Use the world's slot seed for cosmetics
- random.seed(ootworld.multiworld.per_slot_randoms[ootworld.player].random())
# try to detect the cosmetic patch data format
versioned_patch_set = None
diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py
index 6c4b6428f5..8b041f045d 100644
--- a/worlds/oot/Entrance.py
+++ b/worlds/oot/Entrance.py
@@ -3,9 +3,9 @@ from BaseClasses import Entrance
class OOTEntrance(Entrance):
game: str = 'Ocarina of Time'
- def __init__(self, player, world, name='', parent=None):
+ def __init__(self, player, multiworld, name='', parent=None):
super(OOTEntrance, self).__init__(player, name, parent)
- self.multiworld = world
+ self.multiworld = multiworld
self.access_rules = []
self.reverse = None
self.replaces = None
diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py
index cda442ffb1..66c5df804c 100644
--- a/worlds/oot/EntranceShuffle.py
+++ b/worlds/oot/EntranceShuffle.py
@@ -440,16 +440,16 @@ class EntranceShuffleError(Exception):
def shuffle_random_entrances(ootworld):
- world = ootworld.multiworld
+ multiworld = ootworld.multiworld
player = ootworld.player
# Gather locations to keep reachable for validation
all_state = ootworld.get_state_with_complete_itempool()
all_state.sweep_for_advancements(locations=ootworld.get_locations())
- locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
+ locations_to_ensure_reachable = {loc for loc in multiworld.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
# Set entrance data for all entrances
- set_all_entrances_data(world, player)
+ set_all_entrances_data(multiworld, player)
# Determine entrance pools based on settings
one_way_entrance_pools = {}
@@ -547,10 +547,10 @@ def shuffle_random_entrances(ootworld):
none_state = CollectionState(ootworld.multiworld)
# Plando entrances
- if world.plando_connections[player]:
+ if ootworld.options.plando_connections:
rollbacks = []
all_targets = {**one_way_target_entrance_pools, **target_entrance_pools}
- for conn in world.plando_connections[player]:
+ for conn in ootworld.options.plando_connections:
try:
entrance = ootworld.get_entrance(conn.entrance)
exit = ootworld.get_entrance(conn.exit)
@@ -628,7 +628,7 @@ def shuffle_random_entrances(ootworld):
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
# Game is beatable
new_all_state = ootworld.get_state_with_complete_itempool()
- if not world.has_beaten_game(new_all_state, player):
+ if not multiworld.has_beaten_game(new_all_state, player):
raise EntranceShuffleError('Cannot beat game')
# Validate world
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
@@ -675,7 +675,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools):
avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools))
- ootworld.multiworld.random.shuffle(avail_pool)
+ ootworld.random.shuffle(avail_pool)
for entrance in avail_pool:
if entrance.replaces:
@@ -725,11 +725,11 @@ def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances,
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
- ootworld.multiworld.random.shuffle(entrances)
+ ootworld.random.shuffle(entrances)
for entrance in entrances:
if entrance.connected_region != None:
continue
- ootworld.multiworld.random.shuffle(target_entrances)
+ ootworld.random.shuffle(target_entrances)
# Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems.
# success rate over randomization
if pool_type in {'InteriorSoft', 'MixedSoft'}:
@@ -785,7 +785,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran
# TODO: improve this function
def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig):
- world = ootworld.multiworld
+ multiworld = ootworld.multiworld
player = ootworld.player
all_state = all_state_orig.copy()
@@ -828,8 +828,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
- potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
- potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
+ potion_front = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
+ potion_back = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back):
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
elif (potion_front and not potion_back) or (not potion_front and potion_back):
@@ -840,8 +840,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
if ootworld.shuffle_cows:
- impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
- impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
+ impas_front = get_entrance_replacing(multiworld.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
+ impas_back = get_entrance_replacing(multiworld.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back):
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
elif (impas_front and not impas_back) or (not impas_front and impas_back):
@@ -861,25 +861,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)):
raise EntranceShuffleError('Time passing is not guaranteed as both ages')
- if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
+ if ootworld.starting_age == 'child' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
raise EntranceShuffleError('Path to ToT as adult not guaranteed')
- if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
+ if ootworld.starting_age == 'adult' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
raise EntranceShuffleError('Path to ToT as child not guaranteed')
if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
# Ensure big poe shop is always reachable as adult
- if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
+ if multiworld.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult')
if ootworld.shopsanity == 'off':
# Ensure that Goron and Zora shops are accessible as adult
- if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
+ if multiworld.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Goron City Shop not accessible as adult')
- if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
+ if multiworld.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult')
if ootworld.open_forest == 'closed':
# Ensure that Kokiri Shop is reachable as child with no items
- if world.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
+ if multiworld.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]:
raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest')
diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py
index b0f20858e7..28a5d37a51 100644
--- a/worlds/oot/HintList.py
+++ b/worlds/oot/HintList.py
@@ -1,5 +1,3 @@
-import random
-
from BaseClasses import LocationProgressType
from .Items import OOTItem
@@ -28,7 +26,7 @@ class Hint(object):
text = ""
type = []
- def __init__(self, name, text, type, choice=None):
+ def __init__(self, name, text, type, rand, choice=None):
self.name = name
self.type = [type] if not isinstance(type, list) else type
@@ -36,31 +34,31 @@ class Hint(object):
self.text = text
else:
if choice == None:
- self.text = random.choice(text)
+ self.text = rand.choice(text)
else:
self.text = text[choice]
-def getHint(item, clearer_hint=False):
+def getHint(item, rand, clearer_hint=False):
if item in hintTable:
textOptions, clearText, hintType = hintTable[item]
if clearer_hint:
if clearText == None:
- return Hint(item, textOptions, hintType, 0)
- return Hint(item, clearText, hintType)
+ return Hint(item, textOptions, hintType, rand, 0)
+ return Hint(item, clearText, hintType, rand)
else:
- return Hint(item, textOptions, hintType)
+ return Hint(item, textOptions, hintType, rand)
elif isinstance(item, str):
- return Hint(item, item, 'generic')
+ return Hint(item, item, 'generic', rand)
else: # is an Item
- return Hint(item.name, item.hint_text, 'item')
+ return Hint(item.name, item.hint_text, 'item', rand)
def getHintGroup(group, world):
ret = []
for name in hintTable:
- hint = getHint(name, world.clearer_hints)
+ hint = getHint(name, world.random, world.clearer_hints)
if hint.name in world.always_hints and group == 'always':
hint.type = 'always'
@@ -95,7 +93,7 @@ def getHintGroup(group, world):
def getRequiredHints(world):
ret = []
for name in hintTable:
- hint = getHint(name)
+ hint = getHint(name, world.random)
if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world):
ret.append(hint)
return ret
@@ -1689,7 +1687,7 @@ def hintExclusions(world, clear_cache=False):
location_hints = []
for name in hintTable:
- hint = getHint(name, world.clearer_hints)
+ hint = getHint(name, world.random, world.clearer_hints)
if any(item in hint.type for item in
['always',
'dual_always',
diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py
index e63e135e50..c01241d048 100644
--- a/worlds/oot/Hints.py
+++ b/worlds/oot/Hints.py
@@ -136,13 +136,13 @@ def getItemGenericName(item):
def isRestrictedDungeonItem(dungeon, item):
if not isinstance(item, OOTItem):
return False
- if (item.map or item.compass) and dungeon.multiworld.shuffle_mapcompass == 'dungeon':
+ if (item.map or item.compass) and dungeon.world.options.shuffle_mapcompass == 'dungeon':
return item in dungeon.dungeon_items
- if item.type == 'SmallKey' and dungeon.multiworld.shuffle_smallkeys == 'dungeon':
+ if item.type == 'SmallKey' and dungeon.world.options.shuffle_smallkeys == 'dungeon':
return item in dungeon.small_keys
- if item.type == 'BossKey' and dungeon.multiworld.shuffle_bosskeys == 'dungeon':
+ if item.type == 'BossKey' and dungeon.world.options.shuffle_bosskeys == 'dungeon':
return item in dungeon.boss_key
- if item.type == 'GanonBossKey' and dungeon.multiworld.shuffle_ganon_bosskey == 'dungeon':
+ if item.type == 'GanonBossKey' and dungeon.world.options.shuffle_ganon_bosskey == 'dungeon':
return item in dungeon.boss_key
return False
@@ -261,8 +261,8 @@ hintPrefixes = [
'',
]
-def getSimpleHintNoPrefix(item):
- hint = getHint(item.name, True).text
+def getSimpleHintNoPrefix(item, rand):
+ hint = getHint(item.name, rand, True).text
for prefix in hintPrefixes:
if hint.startswith(prefix):
@@ -417,9 +417,9 @@ class HintArea(Enum):
# Formats the hint text for this area with proper grammar.
# Dungeons are hinted differently depending on the clearer_hints setting.
- def text(self, clearer_hints, preposition=False, world=None):
+ def text(self, rand, clearer_hints, preposition=False, world=None):
if self.is_dungeon:
- text = getHint(self.dungeon_name, clearer_hints).text
+ text = getHint(self.dungeon_name, rand, clearer_hints).text
else:
text = str(self)
prefix, suffix = text.replace('#', '').split(' ', 1)
@@ -489,7 +489,7 @@ def get_woth_hint(world, checked):
if getattr(location.parent_region, "dungeon", None):
world.woth_dungeon += 1
- location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
+ location_text = getHint(location.parent_region.dungeon.name, world.random, world.clearer_hints).text
else:
location_text = get_hint_area(location)
@@ -570,9 +570,9 @@ def get_good_item_hint(world, checked):
location = world.hint_rng.choice(locations)
checked[location.player].add(location.name)
- item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+ item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
if getattr(location.parent_region, "dungeon", None):
- location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
+ location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), location)
else:
@@ -613,10 +613,10 @@ def get_specific_item_hint(world, checked):
location = world.hint_rng.choice(locations)
checked[location.player].add(location.name)
- item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+ item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
if getattr(location.parent_region, "dungeon", None):
- location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
+ location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text
if world.hint_dist_user.get('vague_named_items', False):
return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
else:
@@ -648,9 +648,9 @@ def get_random_location_hint(world, checked):
checked[location.player].add(location.name)
dungeon = location.parent_region.dungeon
- item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+ item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
if dungeon:
- location_text = getHint(dungeon.name, world.clearer_hints).text
+ location_text = getHint(dungeon.name, world.hint_rng, world.clearer_hints).text
return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), location)
else:
@@ -675,7 +675,7 @@ def get_specific_hint(world, checked, type):
location_text = hint.text
if '#' not in location_text:
location_text = '#%s#' % location_text
- item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+ item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), location)
@@ -724,9 +724,9 @@ def get_entrance_hint(world, checked):
connected_region = entrance.connected_region
if connected_region.dungeon:
- region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text
+ region_text = getHint(connected_region.dungeon.name, world.hint_rng, world.clearer_hints).text
else:
- region_text = getHint(connected_region.name, world.clearer_hints).text
+ region_text = getHint(connected_region.name, world.hint_rng, world.clearer_hints).text
if '#' not in region_text:
region_text = '#%s#' % region_text
@@ -882,10 +882,10 @@ def buildWorldGossipHints(world, checkedLocations=None):
if location.name in world.hint_text_overrides:
location_text = world.hint_text_overrides[location.name]
else:
- location_text = getHint(location.name, world.clearer_hints).text
+ location_text = getHint(location.name, world.hint_rng, world.clearer_hints).text
if '#' not in location_text:
location_text = '#%s#' % location_text
- item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+ item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text
add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True)
logging.getLogger('').debug('Placed always hint for %s.', location.name)
@@ -1003,16 +1003,16 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
('Goron Ruby', 'Red'),
('Zora Sapphire', 'Blue'),
]
- child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04'
+ child_text += getHint('Spiritual Stone Text Start', world.hint_rng, world.clearer_hints).text + '\x04'
for (reward, color) in bossRewardsSpiritualStones:
child_text += buildBossString(reward, color, world)
- child_text += getHint('Child Altar Text End', world.clearer_hints).text
+ child_text += getHint('Child Altar Text End', world.hint_rng, world.clearer_hints).text
child_text += '\x0B'
update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20)
# text that appears at altar as an adult.
adult_text = '\x08'
- adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04'
+ adult_text += getHint('Adult Altar Text Start', world.hint_rng, world.clearer_hints).text + '\x04'
if include_rewards:
bossRewardsMedallions = [
('Light Medallion', 'Light Blue'),
@@ -1029,7 +1029,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
adult_text += '\x04'
adult_text += buildGanonBossKeyString(world)
else:
- adult_text += getHint('Adult Altar Text End', world.clearer_hints).text
+ adult_text += getHint('Adult Altar Text End', world.hint_rng, world.clearer_hints).text
adult_text += '\x0B'
update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20)
@@ -1044,7 +1044,7 @@ def buildBossString(reward, color, world):
text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='')
else:
location = world.hinted_dungeon_reward_locations[reward]
- location_text = HintArea.at(location).text(world.clearer_hints, preposition=True)
+ location_text = HintArea.at(location).text(world.hint_rng, world.clearer_hints, preposition=True)
text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='')
return str(text) + '\x04'
@@ -1054,7 +1054,7 @@ def buildBridgeReqsString(world):
if world.bridge == 'open':
string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells."
else:
- item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text
+ item_req_string = getHint('bridge_' + world.bridge, world.hint_rng, world.clearer_hints).text
if world.bridge == 'medallions':
item_req_string = str(world.bridge_medallions) + ' ' + item_req_string
elif world.bridge == 'stones':
@@ -1077,7 +1077,7 @@ def buildGanonBossKeyString(world):
string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#."
else:
if world.shuffle_ganon_bosskey == 'on_lacs':
- item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text
+ item_req_string = getHint('lacs_' + world.lacs_condition, world.hint_rng, world.clearer_hints).text
if world.lacs_condition == 'medallions':
item_req_string = str(world.lacs_medallions) + ' ' + item_req_string
elif world.lacs_condition == 'stones':
@@ -1092,7 +1092,7 @@ def buildGanonBossKeyString(world):
item_req_string = '#%s#' % item_req_string
bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string
elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']:
- item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
+ item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text
if world.shuffle_ganon_bosskey == 'medallions':
item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string
elif world.shuffle_ganon_bosskey == 'stones':
@@ -1107,7 +1107,7 @@ def buildGanonBossKeyString(world):
item_req_string = '#%s#' % item_req_string
bk_location_string = "automatically granted once %s are retrieved" % item_req_string
else:
- bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
+ bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text
string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string
return str(GossipText(string, ['Yellow'], prefix=''))
@@ -1142,16 +1142,16 @@ def buildMiscItemHints(world, messages):
if location.player != world.player:
player_text = world.multiworld.get_player_name(location.player) + "'s "
if location.game == 'Ocarina of Time':
- area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None)
+ area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.hint_rng, world.clearer_hints, world=None)
else:
area = location.name
text = data['default_item_text'].format(area=rom_safe_text(player_text + area))
elif 'default_item_fallback' in data:
text = data['default_item_fallback']
else:
- text = getHint('Validation Line', world.clearer_hints).text
+ text = getHint('Validation Line', world.hint_rng, world.clearer_hints).text
location = world.get_location('Ganons Tower Boss Key Chest')
- text += f"#{getHint(getItemGenericName(location.item), world.clearer_hints).text}#"
+ text += f"#{getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text}#"
for find, replace in data.get('replace', {}).items():
text = text.replace(find, replace)
@@ -1165,7 +1165,7 @@ def buildMiscLocationHints(world, messages):
if hint_type in world.misc_hints:
location = world.get_location(data['item_location'])
item = location.item
- item_text = getHint(getItemGenericName(item), world.clearer_hints).text
+ item_text = getHint(getItemGenericName(item), world.hint_rng, world.clearer_hints).text
if item.player != world.player:
item_text += f' for {world.multiworld.get_player_name(item.player)}'
text = data['location_text'].format(item=rom_safe_text(item_text))
diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py
index 6ca6bc9268..805d1fc72d 100644
--- a/worlds/oot/ItemPool.py
+++ b/worlds/oot/ItemPool.py
@@ -295,16 +295,14 @@ random = None
def get_junk_pool(ootworld):
junk_pool[:] = list(junk_pool_base)
- if ootworld.junk_ice_traps == 'on':
+ if ootworld.options.junk_ice_traps == 'on':
junk_pool.append(('Ice Trap', 10))
- elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']:
+ elif ootworld.options.junk_ice_traps in ['mayhem', 'onslaught']:
junk_pool[:] = [('Ice Trap', 1)]
return junk_pool
-def get_junk_item(count=1, pool=None, plando_pool=None):
- global random
-
+def get_junk_item(rand, count=1, pool=None, plando_pool=None):
if count < 1:
raise ValueError("get_junk_item argument 'count' must be greater than 0.")
@@ -323,17 +321,17 @@ def get_junk_item(count=1, pool=None, plando_pool=None):
raise RuntimeError("Not enough junk is available in the item pool to replace removed items.")
else:
junk_items, junk_weights = zip(*junk_pool)
- return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count))
+ return_pool.extend(rand.choices(junk_items, weights=junk_weights, k=count))
return return_pool
-def replace_max_item(items, item, max):
+def replace_max_item(items, item, max, rand):
count = 0
for i,val in enumerate(items):
if val == item:
if count >= max:
- items[i] = get_junk_item()[0]
+ items[i] = get_junk_item(rand)[0]
count += 1
@@ -375,7 +373,7 @@ def get_pool_core(world):
pending_junk_pool.append('Kokiri Sword')
if world.shuffle_ocarinas:
pending_junk_pool.append('Ocarina')
- if world.shuffle_beans and world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0):
+ if world.shuffle_beans and world.options.start_inventory.value.get('Magic Bean Pack', 0):
pending_junk_pool.append('Magic Bean Pack')
if (world.gerudo_fortress != "open"
and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']):
@@ -450,7 +448,7 @@ def get_pool_core(world):
else:
item = deku_scrubs_items[location.vanilla_item]
if isinstance(item, list):
- item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
+ item = world.random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
shuffle_item = True
# Kokiri Sword
@@ -489,7 +487,7 @@ def get_pool_core(world):
# Cows
elif location.vanilla_item == 'Milk':
if world.shuffle_cows:
- item = get_junk_item()[0]
+ item = get_junk_item(world.random)[0]
shuffle_item = world.shuffle_cows
if not shuffle_item:
location.show_in_spoiler = False
@@ -508,13 +506,13 @@ def get_pool_core(world):
item = 'Rutos Letter'
ruto_bottles -= 1
else:
- item = random.choice(normal_bottles)
+ item = world.random.choice(normal_bottles)
shuffle_item = True
# Magic Beans
elif location.vanilla_item == 'Buy Magic Bean':
if world.shuffle_beans:
- item = 'Magic Bean Pack' if not world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0) else get_junk_item()[0]
+ item = 'Magic Bean Pack' if not world.options.start_inventory.value.get('Magic Bean Pack', 0) else get_junk_item(world.random)[0]
shuffle_item = world.shuffle_beans
if not shuffle_item:
location.show_in_spoiler = False
@@ -528,7 +526,7 @@ def get_pool_core(world):
# Adult Trade Item
elif location.vanilla_item == 'Pocket Egg':
potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items
- item = random.choice(sorted(potential_trade_items))
+ item = world.random.choice(sorted(potential_trade_items))
world.selected_adult_trade_item = item
shuffle_item = True
@@ -541,7 +539,7 @@ def get_pool_core(world):
shuffle_item = False
location.show_in_spoiler = False
if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings:
- item = get_junk_item()[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
+ item = get_junk_item(world.random)[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
# Freestanding Rupees and Hearts
elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']:
@@ -618,7 +616,7 @@ def get_pool_core(world):
elif dungeon.name in world.key_rings and not dungeon.small_keys:
item = dungeon.item_name("Small Key Ring")
elif dungeon.name in world.key_rings:
- item = get_junk_item()[0]
+ item = get_junk_item(world.random)[0]
shuffle_item = True
# Any other item in a dungeon.
elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]:
@@ -630,7 +628,7 @@ def get_pool_core(world):
if shuffle_setting in ['remove', 'startwith']:
world.multiworld.push_precollected(dungeon_collection[-1])
world.remove_from_start_inventory.append(dungeon_collection[-1].name)
- item = get_junk_item()[0]
+ item = get_junk_item(world.random)[0]
shuffle_item = True
elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']:
dungeon_collection[-1].priority = True
@@ -658,9 +656,9 @@ def get_pool_core(world):
shop_non_item_count = len(world.shop_prices)
shop_item_count = shop_slots_count - shop_non_item_count
- pool.extend(random.sample(remain_shop_items, shop_item_count))
+ pool.extend(world.random.sample(remain_shop_items, shop_item_count))
if shop_non_item_count:
- pool.extend(get_junk_item(shop_non_item_count))
+ pool.extend(get_junk_item(world.random, shop_non_item_count))
# Extra rupees for shopsanity.
if world.shopsanity not in ['off', '0']:
@@ -706,19 +704,19 @@ def get_pool_core(world):
if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']:
placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)'
- pool.extend(get_junk_item())
+ pool.extend(get_junk_item(world.random))
else:
placed_items['Gift from Sages'] = IGNORE_LOCATION
world.get_location('Gift from Sages').show_in_spoiler = False
if world.junk_ice_traps == 'off':
- replace_max_item(pool, 'Ice Trap', 0)
+ replace_max_item(pool, 'Ice Trap', 0, world.random)
elif world.junk_ice_traps == 'onslaught':
for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']:
- replace_max_item(pool, item, 0)
+ replace_max_item(pool, item, 0, world.random)
for item, maximum in item_difficulty_max[world.item_pool_value].items():
- replace_max_item(pool, item, maximum)
+ replace_max_item(pool, item, maximum, world.random)
# world.distribution.alter_pool(world, pool)
@@ -748,7 +746,7 @@ def get_pool_core(world):
pending_item = pending_junk_pool.pop()
if not junk_candidates:
raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1))
- junk_item = random.choice(junk_candidates)
+ junk_item = world.random.choice(junk_candidates)
junk_candidates.remove(junk_item)
pool.remove(junk_item)
pool.append(pending_item)
diff --git a/worlds/oot/Messages.py b/worlds/oot/Messages.py
index 25c2a9934d..5059c01f3c 100644
--- a/worlds/oot/Messages.py
+++ b/worlds/oot/Messages.py
@@ -1,6 +1,5 @@
# text details: https://wiki.cloudmodding.com/oot/Text_Format
-import random
from .HintList import misc_item_hint_table, misc_location_hint_table
from .TextBox import line_wrap
from .Utils import find_last
@@ -969,7 +968,7 @@ def repack_messages(rom, messages, permutation=None, always_allow_skip=True, spe
rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
# shuffles the messages in the game, making sure to keep various message types in their own group
-def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
+def shuffle_messages(messages, rand, except_hints=True, always_allow_skip=True):
permutation = [i for i, _ in enumerate(messages)]
@@ -1002,7 +1001,7 @@ def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
def shuffle_group(group):
group_permutation = [i for i, _ in enumerate(group)]
- random.shuffle(group_permutation)
+ rand.shuffle(group_permutation)
for index_from, index_to in enumerate(group_permutation):
permutation[group[index_to].index] = group[index_from].index
diff --git a/worlds/oot/Music.py b/worlds/oot/Music.py
index 6ed1ab54ae..1bb3b65aac 100644
--- a/worlds/oot/Music.py
+++ b/worlds/oot/Music.py
@@ -1,6 +1,5 @@
#Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer
-import random
import os
from .Utils import compare_version, data_path
@@ -175,7 +174,7 @@ def process_sequences(rom, sequences, target_sequences, disabled_source_sequence
return sequences, target_sequences
-def shuffle_music(sequences, target_sequences, music_mapping, log):
+def shuffle_music(sequences, target_sequences, music_mapping, log, rand):
sequence_dict = {}
sequence_ids = []
@@ -191,7 +190,7 @@ def shuffle_music(sequences, target_sequences, music_mapping, log):
# Shuffle the sequences
if len(sequences) < len(target_sequences):
raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).")
- random.shuffle(sequence_ids)
+ rand.shuffle(sequence_ids)
sequences = []
for target_sequence in target_sequences:
@@ -328,7 +327,7 @@ def rebuild_sequences(rom, sequences):
rom.write_byte(base, j.instrument_set)
-def shuffle_pointers_table(rom, ids, music_mapping, log):
+def shuffle_pointers_table(rom, ids, music_mapping, log, rand):
# Read in all the Music data
bgm_data = {}
bgm_ids = []
@@ -341,7 +340,7 @@ def shuffle_pointers_table(rom, ids, music_mapping, log):
bgm_ids.append(bgm[0])
# shuffle data
- random.shuffle(bgm_ids)
+ rand.shuffle(bgm_ids)
# Write Music data back in random ordering
for bgm in ids:
@@ -424,13 +423,13 @@ def randomize_music(rom, ootworld, music_mapping):
# process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids)
# if ootworld.background_music == 'random_custom_only':
# sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()]
- # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log)
+ # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log, ootworld.random)
# if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped:
# process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare')
# if ootworld.fanfares == 'random_custom_only':
# fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()]
- # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log)
+ # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log, ootworld.random)
# if disabled_source_sequences:
# log = disable_music(rom, disabled_source_sequences.values(), log)
@@ -438,10 +437,10 @@ def randomize_music(rom, ootworld, music_mapping):
# rebuild_sequences(rom, sequences + fanfare_sequences)
# else:
if ootworld.background_music == 'randomized' or bgm_mapped:
- log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log)
+ log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log, ootworld.random)
if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped:
- log = shuffle_pointers_table(rom, ff_ids, music_mapping, log)
+ log = shuffle_pointers_table(rom, ff_ids, music_mapping, log, ootworld.random)
# end_else
if disabled_target_sequences:
log = disable_music(rom, disabled_target_sequences.values(), log)
diff --git a/worlds/oot/N64Patch.py b/worlds/oot/N64Patch.py
index 5af3279e80..3013a94a8e 100644
--- a/worlds/oot/N64Patch.py
+++ b/worlds/oot/N64Patch.py
@@ -1,5 +1,4 @@
import struct
-import random
import io
import array
import zlib
@@ -88,7 +87,7 @@ def write_block_section(start, key_skip, in_data, patch_data, is_continue):
# xor_range is the range the XOR key will read from. This range is not
# too important, but I tried to choose from a section that didn't really
# have big gaps of 0s which we want to avoid.
-def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
+def create_patch_file(rom, rand, xor_range=(0x00B8AD30, 0x00F029A0)):
dma_start, dma_end = rom.get_dma_table_range()
# add header
@@ -100,7 +99,7 @@ def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
# get random xor key. This range is chosen because it generally
# doesn't have many sections of 0s
- xor_address = random.Random().randint(*xor_range)
+ xor_address = rand.randint(*xor_range)
patch_data.append_int32(xor_address)
new_buffer = copy.copy(rom.original.buffer)
diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py
index daf072adb5..613c5d01b3 100644
--- a/worlds/oot/Options.py
+++ b/worlds/oot/Options.py
@@ -1,6 +1,8 @@
import typing
import random
-from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections
+from dataclasses import dataclass
+from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \
+ PerGameCommonOptions, OptionGroup
from .EntranceShuffle import entrance_shuffle_table
from .LogicTricks import normalized_name_tricks
from .ColorSFXOptions import *
@@ -1281,21 +1283,166 @@ class LogicTricks(OptionList):
valid_keys_casefold = True
-# All options assembled into a single dict
-oot_options: typing.Dict[str, type(Option)] = {
- "plando_connections": OoTPlandoConnections,
- "logic_rules": Logic,
- "logic_no_night_tokens_without_suns_song": NightTokens,
- **open_options,
- **world_options,
- **bridge_options,
- **dungeon_items_options,
- **shuffle_options,
- **timesavers_options,
- **misc_options,
- **itempool_options,
- **cosmetic_options,
- **sfx_options,
- "logic_tricks": LogicTricks,
- "death_link": DeathLink,
-}
+@dataclass
+class OoTOptions(PerGameCommonOptions):
+ plando_connections: OoTPlandoConnections
+ death_link: DeathLink
+ logic_rules: Logic
+ logic_no_night_tokens_without_suns_song: NightTokens
+ logic_tricks: LogicTricks
+ open_forest: Forest
+ open_kakariko: Gate
+ open_door_of_time: DoorOfTime
+ zora_fountain: Fountain
+ gerudo_fortress: Fortress
+ bridge: Bridge
+ trials: Trials
+ starting_age: StartingAge
+ shuffle_interior_entrances: InteriorEntrances
+ shuffle_grotto_entrances: GrottoEntrances
+ shuffle_dungeon_entrances: DungeonEntrances
+ shuffle_overworld_entrances: OverworldEntrances
+ owl_drops: OwlDrops
+ warp_songs: WarpSongs
+ spawn_positions: SpawnPositions
+ shuffle_bosses: BossEntrances
+ # mix_entrance_pools: MixEntrancePools
+ # decouple_entrances: DecoupleEntrances
+ triforce_hunt: TriforceHunt
+ triforce_goal: TriforceGoal
+ extra_triforce_percentage: ExtraTriforces
+ bombchus_in_logic: LogicalChus
+ dungeon_shortcuts: DungeonShortcuts
+ dungeon_shortcuts_list: DungeonShortcutsList
+ mq_dungeons_mode: MQDungeons
+ mq_dungeons_list: MQDungeonList
+ mq_dungeons_count: MQDungeonCount
+ # empty_dungeons_mode: EmptyDungeons
+ # empty_dungeons_list: EmptyDungeonList
+ # empty_dungeon_count: EmptyDungeonCount
+ bridge_stones: BridgeStones
+ bridge_medallions: BridgeMedallions
+ bridge_rewards: BridgeRewards
+ bridge_tokens: BridgeTokens
+ bridge_hearts: BridgeHearts
+ shuffle_mapcompass: ShuffleMapCompass
+ shuffle_smallkeys: ShuffleKeys
+ shuffle_hideoutkeys: ShuffleGerudoKeys
+ shuffle_bosskeys: ShuffleBossKeys
+ enhance_map_compass: EnhanceMC
+ shuffle_ganon_bosskey: ShuffleGanonBK
+ ganon_bosskey_medallions: GanonBKMedallions
+ ganon_bosskey_stones: GanonBKStones
+ ganon_bosskey_rewards: GanonBKRewards
+ ganon_bosskey_tokens: GanonBKTokens
+ ganon_bosskey_hearts: GanonBKHearts
+ key_rings: KeyRings
+ key_rings_list: KeyRingList
+ shuffle_song_items: SongShuffle
+ shopsanity: ShopShuffle
+ shop_slots: ShopSlots
+ shopsanity_prices: ShopPrices
+ tokensanity: TokenShuffle
+ shuffle_scrubs: ScrubShuffle
+ shuffle_child_trade: ShuffleChildTrade
+ shuffle_freestanding_items: ShuffleFreestanding
+ shuffle_pots: ShufflePots
+ shuffle_crates: ShuffleCrates
+ shuffle_cows: ShuffleCows
+ shuffle_beehives: ShuffleBeehives
+ shuffle_kokiri_sword: ShuffleSword
+ shuffle_ocarinas: ShuffleOcarinas
+ shuffle_gerudo_card: ShuffleCard
+ shuffle_beans: ShuffleBeans
+ shuffle_medigoron_carpet_salesman: ShuffleMedigoronCarpet
+ shuffle_frog_song_rupees: ShuffleFrogRupees
+ no_escape_sequence: SkipEscape
+ no_guard_stealth: SkipStealth
+ no_epona_race: SkipEponaRace
+ skip_some_minigame_phases: SkipMinigamePhases
+ complete_mask_quest: CompleteMaskQuest
+ useful_cutscenes: UsefulCutscenes
+ fast_chests: FastChests
+ free_scarecrow: FreeScarecrow
+ fast_bunny_hood: FastBunny
+ plant_beans: PlantBeans
+ chicken_count: ChickenCount
+ big_poe_count: BigPoeCount
+ fae_torch_count: FAETorchCount
+ correct_chest_appearances: CorrectChestAppearance
+ minor_items_as_major_chest: MinorInMajor
+ invisible_chests: InvisibleChests
+ correct_potcrate_appearances: CorrectPotCrateAppearance
+ hints: Hints
+ misc_hints: MiscHints
+ hint_dist: HintDistribution
+ text_shuffle: TextShuffle
+ damage_multiplier: DamageMultiplier
+ deadly_bonks: DeadlyBonks
+ no_collectible_hearts: HeroMode
+ starting_tod: StartingToD
+ blue_fire_arrows: BlueFireArrows
+ fix_broken_drops: FixBrokenDrops
+ start_with_consumables: ConsumableStart
+ start_with_rupees: RupeeStart
+ item_pool_value: ItemPoolValue
+ junk_ice_traps: IceTraps
+ ice_trap_appearance: IceTrapVisual
+ adult_trade_start: AdultTradeStart
+ default_targeting: Targeting
+ display_dpad: DisplayDpad
+ dpad_dungeon_menu: DpadDungeonMenu
+ correct_model_colors: CorrectColors
+ background_music: BackgroundMusic
+ fanfares: Fanfares
+ ocarina_fanfares: OcarinaFanfares
+ kokiri_color: kokiri_color
+ goron_color: goron_color
+ zora_color: zora_color
+ silver_gauntlets_color: silver_gauntlets_color
+ golden_gauntlets_color: golden_gauntlets_color
+ mirror_shield_frame_color: mirror_shield_frame_color
+ navi_color_default_inner: navi_color_default_inner
+ navi_color_default_outer: navi_color_default_outer
+ navi_color_enemy_inner: navi_color_enemy_inner
+ navi_color_enemy_outer: navi_color_enemy_outer
+ navi_color_npc_inner: navi_color_npc_inner
+ navi_color_npc_outer: navi_color_npc_outer
+ navi_color_prop_inner: navi_color_prop_inner
+ navi_color_prop_outer: navi_color_prop_outer
+ sword_trail_duration: SwordTrailDuration
+ sword_trail_color_inner: sword_trail_color_inner
+ sword_trail_color_outer: sword_trail_color_outer
+ bombchu_trail_color_inner: bombchu_trail_color_inner
+ bombchu_trail_color_outer: bombchu_trail_color_outer
+ boomerang_trail_color_inner: boomerang_trail_color_inner
+ boomerang_trail_color_outer: boomerang_trail_color_outer
+ heart_color: heart_color
+ magic_color: magic_color
+ a_button_color: a_button_color
+ b_button_color: b_button_color
+ c_button_color: c_button_color
+ start_button_color: start_button_color
+ sfx_navi_overworld: sfx_navi_overworld
+ sfx_navi_enemy: sfx_navi_enemy
+ sfx_low_hp: sfx_low_hp
+ sfx_menu_cursor: sfx_menu_cursor
+ sfx_menu_select: sfx_menu_select
+ sfx_nightfall: sfx_nightfall
+ sfx_horse_neigh: sfx_horse_neigh
+ sfx_hover_boots: sfx_hover_boots
+ sfx_ocarina: SfxOcarina
+
+
+oot_option_groups: typing.List[OptionGroup] = [
+ OptionGroup("Open", [option for option in open_options.values()]),
+ OptionGroup("World", [*[option for option in world_options.values()],
+ *[option for option in bridge_options.values()]]),
+ OptionGroup("Shuffle", [option for option in shuffle_options.values()]),
+ OptionGroup("Dungeon Items", [option for option in dungeon_items_options.values()]),
+ OptionGroup("Timesavers", [option for option in timesavers_options.values()]),
+ OptionGroup("Misc", [option for option in misc_options.values()]),
+ OptionGroup("Item Pool", [option for option in itempool_options.values()]),
+ OptionGroup("Cosmetics", [option for option in cosmetic_options.values()]),
+ OptionGroup("SFX", [option for option in sfx_options.values()])
+]
diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py
index 2219d7bb95..561d7c3f7b 100644
--- a/worlds/oot/Patches.py
+++ b/worlds/oot/Patches.py
@@ -208,8 +208,8 @@ def patch_rom(world, rom):
# Fix Ice Cavern Alcove Camera
if not world.dungeon_mq['Ice Cavern']:
- rom.write_byte(0x2BECA25,0x01);
- rom.write_byte(0x2BECA2D,0x01);
+ rom.write_byte(0x2BECA25,0x01)
+ rom.write_byte(0x2BECA2D,0x01)
# Fix GS rewards to be static
rom.write_int32(0xEA3934, 0)
@@ -944,7 +944,7 @@ def patch_rom(world, rom):
scene_table = 0x00B71440
for scene in range(0x00, 0x65):
- scene_start = rom.read_int32(scene_table + (scene * 0x14));
+ scene_start = rom.read_int32(scene_table + (scene * 0x14))
add_scene_exits(scene_start)
return exit_table
@@ -1632,10 +1632,10 @@ def patch_rom(world, rom):
reward_text = None
elif getattr(location.item, 'looks_like_item', None) is not None:
jabu_item = location.item.looks_like_item
- reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), True).text)
+ reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), world.hint_rng, True).text)
else:
jabu_item = location.item
- reward_text = getHint(getItemGenericName(location.item), True).text
+ reward_text = getHint(getItemGenericName(location.item), world.hint_rng, True).text
# Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu
if reward_text is None:
@@ -1687,7 +1687,7 @@ def patch_rom(world, rom):
# Sets hooks for gossip stone changes
- symbol = rom.sym("GOSSIP_HINT_CONDITION");
+ symbol = rom.sym("GOSSIP_HINT_CONDITION")
if world.hints == 'none':
rom.write_int32(symbol, 0)
@@ -2264,9 +2264,9 @@ def patch_rom(world, rom):
# text shuffle
if world.text_shuffle == 'except_hints':
- permutation = shuffle_messages(messages, except_hints=True)
+ permutation = shuffle_messages(messages, world.random, except_hints=True)
elif world.text_shuffle == 'complete':
- permutation = shuffle_messages(messages, except_hints=False)
+ permutation = shuffle_messages(messages, world.random, except_hints=False)
# update warp song preview text boxes
update_warp_song_text(messages, world)
@@ -2358,7 +2358,7 @@ def patch_rom(world, rom):
# Write numeric seed truncated to 32 bits for rng seeding
# Overwritten with new seed every time a new rng value is generated
- rng_seed = world.multiworld.per_slot_randoms[world.player].getrandbits(32)
+ rng_seed = world.random.getrandbits(32)
rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed)
# Static initial seed value for one-time random actions like the Hylian Shield discount
rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed)
@@ -2560,7 +2560,7 @@ def scene_get_actors(rom, actor_func, scene_data, scene, alternate=None, process
room_count = rom.read_byte(scene_data + 1)
room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF)
for _ in range(0, room_count):
- room_data = rom.read_int32(room_list);
+ room_data = rom.read_int32(room_list)
if not room_data in processed_rooms:
actors.update(room_get_actors(rom, actor_func, room_data, scene))
@@ -2591,7 +2591,7 @@ def get_actor_list(rom, actor_func):
actors = {}
scene_table = 0x00B71440
for scene in range(0x00, 0x65):
- scene_data = rom.read_int32(scene_table + (scene * 0x14));
+ scene_data = rom.read_int32(scene_table + (scene * 0x14))
actors.update(scene_get_actors(rom, actor_func, scene_data, scene))
return actors
@@ -2605,7 +2605,7 @@ def get_override_itemid(override_table, scene, type, flags):
def remove_entrance_blockers(rom):
def remove_entrance_blockers_do(rom, actor_id, actor, scene):
if actor_id == 0x014E and scene == 97:
- actor_var = rom.read_int16(actor + 14);
+ actor_var = rom.read_int16(actor + 14)
if actor_var == 0xFF01:
rom.write_int16(actor + 14, 0x0700)
get_actor_list(rom, remove_entrance_blockers_do)
@@ -2789,7 +2789,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1])
else:
if item_display.game == "Ocarina of Time":
- shop_item_name = getSimpleHintNoPrefix(item_display)
+ shop_item_name = getSimpleHintNoPrefix(item_display, world.random)
else:
shop_item_name = item_display.name
diff --git a/worlds/oot/RuleParser.py b/worlds/oot/RuleParser.py
index 0791ad5d1a..e5390474b7 100644
--- a/worlds/oot/RuleParser.py
+++ b/worlds/oot/RuleParser.py
@@ -53,7 +53,7 @@ def isliteral(expr):
class Rule_AST_Transformer(ast.NodeTransformer):
def __init__(self, world, player):
- self.multiworld = world
+ self.world = world
self.player = player
self.events = set()
# map Region -> rule ast string -> item name
@@ -86,9 +86,9 @@ class Rule_AST_Transformer(ast.NodeTransformer):
ctx=ast.Load()),
args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)],
keywords=[])
- elif node.id in self.multiworld.__dict__:
+ elif node.id in self.world.__dict__:
# Settings are constant
- return ast.parse('%r' % self.multiworld.__dict__[node.id], mode='eval').body
+ return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body
elif node.id in State.__dict__:
return self.make_call(node, node.id, [], [])
elif node.id in self.kwarg_defaults or node.id in allowed_globals:
@@ -137,7 +137,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
if isinstance(count, ast.Name):
# Must be a settings constant
- count = ast.parse('%r' % self.multiworld.__dict__[count.id], mode='eval').body
+ count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body
if iname in escaped_items:
iname = escaped_items[iname]
@@ -182,7 +182,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
new_args = []
for child in node.args:
if isinstance(child, ast.Name):
- if child.id in self.multiworld.__dict__:
+ if child.id in self.world.__dict__:
# child = ast.Attribute(
# value=ast.Attribute(
# value=ast.Name(id='state', ctx=ast.Load()),
@@ -190,7 +190,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
# ctx=ast.Load()),
# attr=child.id,
# ctx=ast.Load())
- child = ast.Constant(getattr(self.multiworld, child.id))
+ child = ast.Constant(getattr(self.world, child.id))
elif child.id in rule_aliases:
child = self.visit(child)
elif child.id in escaped_items:
@@ -242,7 +242,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
# Fast check for json can_use
if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq)
and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name)
- and node.left.id not in self.multiworld.__dict__ and node.comparators[0].id not in self.multiworld.__dict__):
+ and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__):
return ast.NameConstant(node.left.id == node.comparators[0].id)
node.left = escape_or_string(node.left)
@@ -378,7 +378,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
# Requires the target regions have been defined in the world.
def create_delayed_rules(self):
for region_name, node, subrule_name in self.delayed_rules:
- region = self.multiworld.multiworld.get_region(region_name, self.player)
+ region = self.world.multiworld.get_region(region_name, self.player)
event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True)
event.show_in_spoiler = False
@@ -395,7 +395,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
set_rule(event, access_rule)
region.locations.append(event)
- self.multiworld.make_event_item(subrule_name, event)
+ self.world.make_event_item(subrule_name, event)
# Safeguard in case this is called multiple times per world
self.delayed_rules.clear()
@@ -448,7 +448,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
## Handlers for compile-time optimizations (former State functions)
def at_day(self, node):
- if self.multiworld.ensure_tod_access:
+ if self.world.ensure_tod_access:
# tod has DAY or (tod == NONE and (ss or find a path from a provider))
# parsing is better than constructing this expression by hand
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
@@ -456,7 +456,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
return ast.NameConstant(True)
def at_dampe_time(self, node):
- if self.multiworld.ensure_tod_access:
+ if self.world.ensure_tod_access:
# tod has DAMPE or (tod == NONE and (find a path from a provider))
# parsing is better than constructing this expression by hand
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
@@ -464,10 +464,10 @@ class Rule_AST_Transformer(ast.NodeTransformer):
return ast.NameConstant(True)
def at_night(self, node):
- if self.current_spot.type == 'GS Token' and self.multiworld.logic_no_night_tokens_without_suns_song:
+ if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song:
# Using visit here to resolve 'can_play' rule
return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body)
- if self.multiworld.ensure_tod_access:
+ if self.world.ensure_tod_access:
# tod has DAMPE or (tod == NONE and (ss or find a path from a provider))
# parsing is better than constructing this expression by hand
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
@@ -501,7 +501,7 @@ class Rule_AST_Transformer(ast.NodeTransformer):
return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body
def current_spot_starting_age_access(self, node):
- return self.current_spot_child_access(node) if self.multiworld.starting_age == 'child' else self.current_spot_adult_access(node)
+ return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node)
def has_bottle(self, node):
return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body
diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py
index 36563a3f9f..00f4aeb4b7 100644
--- a/worlds/oot/Rules.py
+++ b/worlds/oot/Rules.py
@@ -10,7 +10,7 @@ from .LocationList import dungeon_song_locations
from BaseClasses import CollectionState, MultiWorld
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
-from ..AutoWorld import LogicMixin
+from worlds.AutoWorld import LogicMixin
class OOTLogic(LogicMixin):
@@ -132,17 +132,17 @@ class OOTLogic(LogicMixin):
def set_rules(ootworld):
logger = logging.getLogger('')
- world = ootworld.multiworld
+ multiworld = ootworld.multiworld
player = ootworld.player
if ootworld.logic_rules != 'no_logic':
if ootworld.triforce_hunt:
- world.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal)
+ multiworld.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal)
else:
- world.completion_condition[player] = lambda state: state.has('Triforce', player)
+ multiworld.completion_condition[player] = lambda state: state.has('Triforce', player)
# ganon can only carry triforce
- world.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce'
+ multiworld.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce'
# is_child = ootworld.parser.parse_rule('is_child')
guarantee_hint = ootworld.parser.parse_rule('guarantee_hint')
@@ -156,22 +156,22 @@ def set_rules(ootworld):
if (ootworld.dungeon_mq['Forest Temple'] and ootworld.shuffle_bosskeys == 'dungeon'
and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off'):
# First room chest needs to be a small key. Make sure the boss key isn't placed here.
- location = world.get_location('Forest Temple MQ First Room Chest', player)
+ location = multiworld.get_location('Forest Temple MQ First Room Chest', player)
forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items:
# Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else.
# This is required if map/compass included, or any_dungeon shuffle.
- location = world.get_location('Sheik in Ice Cavern', player)
+ location = multiworld.get_location('Sheik in Ice Cavern', player)
add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song'))
if ootworld.shuffle_child_trade == 'skip_child_zelda':
# Song from Impa must be local
- location = world.get_location('Song from Impa', player)
+ location = multiworld.get_location('Song from Impa', player)
add_item_rule(location, lambda item: item.player == player)
for name in ootworld.always_hints:
- add_rule(world.get_location(name, player), guarantee_hint)
+ add_rule(multiworld.get_location(name, player), guarantee_hint)
# TODO: re-add hints once they are working
# if location.type == 'HintStone' and ootworld.hints == 'mask':
diff --git a/worlds/oot/TextBox.py b/worlds/oot/TextBox.py
index a9db479962..e502d73904 100644
--- a/worlds/oot/TextBox.py
+++ b/worlds/oot/TextBox.py
@@ -1,4 +1,4 @@
-import worlds.oot.Messages as Messages
+from . import Messages
# Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the
# characters on a line reach this value.
diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py
index 94587a41a0..b93f60b2a0 100644
--- a/worlds/oot/__init__.py
+++ b/worlds/oot/__init__.py
@@ -20,7 +20,7 @@ from .ItemPool import generate_itempool, get_junk_item, get_junk_pool
from .Regions import OOTRegion, TimeOfDay
from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
from .RuleParser import Rule_AST_Transformer
-from .Options import oot_options
+from .Options import OoTOptions, oot_option_groups
from .Utils import data_path, read_json
from .LocationList import business_scrubs, set_drop_location_names, dungeon_song_locations
from .DungeonList import dungeon_table, create_dungeons
@@ -30,12 +30,12 @@ from .Patches import OoTContainer, patch_rom
from .N64Patch import create_patch_file
from .Cosmetics import patch_cosmetics
-from Utils import get_options
+from settings import get_settings
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections
from Fill import fill_restrictive, fast_fill, FillError
from worlds.generic.Rules import exclusion_rules, add_item_rule
-from ..AutoWorld import World, AutoLogicRegister, WebWorld
+from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
i_o_limiter = threading.Semaphore(2)
@@ -128,6 +128,7 @@ class OOTWeb(WebWorld):
)
tutorials = [setup, setup_es, setup_fr, setup_de]
+ option_groups = oot_option_groups
class OOTWorld(World):
@@ -137,7 +138,8 @@ class OOTWorld(World):
to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
"""
game: str = "Ocarina of Time"
- option_definitions: dict = oot_options
+ options_dataclass = OoTOptions
+ options: OoTOptions
settings: typing.ClassVar[OOTSettings]
topology_present: bool = True
item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if
@@ -195,15 +197,15 @@ class OOTWorld(World):
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
- rom = Rom(file=get_options()['oot_options']['rom_file'])
+ rom = Rom(file=get_settings()['oot_options']['rom_file'])
# Option parsing, handling incompatible options, building useful-item table
def generate_early(self):
self.parser = Rule_AST_Transformer(self, self.player)
- for (option_name, option) in oot_options.items():
- result = getattr(self.multiworld, option_name)[self.player]
+ for option_name in self.options_dataclass.type_hints:
+ result = getattr(self.options, option_name)
if isinstance(result, Range):
option_value = int(result)
elif isinstance(result, Toggle):
@@ -223,8 +225,8 @@ class OOTWorld(World):
self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
self.starting_items = Counter()
self.songs_as_items = False
- self.file_hash = [self.multiworld.random.randint(0, 31) for i in range(5)]
- self.connect_name = ''.join(self.multiworld.random.choices(printable, k=16))
+ self.file_hash = [self.random.randint(0, 31) for i in range(5)]
+ self.connect_name = ''.join(self.random.choices(printable, k=16))
self.collectible_flag_addresses = {}
# Incompatible option handling
@@ -283,7 +285,7 @@ class OOTWorld(World):
local_types.append('BossKey')
if self.shuffle_ganon_bosskey != 'keysanity':
local_types.append('GanonBossKey')
- self.multiworld.local_items[self.player].value |= set(name for name, data in item_table.items() if data[0] in local_types)
+ self.options.local_items.value |= set(name for name, data in item_table.items() if data[0] in local_types)
# If any songs are itemlinked, set songs_as_items
for group in self.multiworld.groups.values():
@@ -297,7 +299,7 @@ class OOTWorld(World):
# Determine skipped trials in GT
# This needs to be done before the logic rules in GT are parsed
trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
- chosen_trials = self.multiworld.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip
+ chosen_trials = self.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip
self.skipped_trials = {trial: (trial not in chosen_trials) for trial in trial_list}
# Determine tricks in logic
@@ -311,8 +313,8 @@ class OOTWorld(World):
# No Logic forces all tricks on, prog balancing off and beatable-only
elif self.logic_rules == 'no_logic':
- self.multiworld.progression_balancing[self.player].value = False
- self.multiworld.accessibility[self.player].value = Accessibility.option_minimal
+ self.options.progression_balancing.value = False
+ self.options.accessibility.value = Accessibility.option_minimal
for trick in normalized_name_tricks.values():
setattr(self, trick['name'], True)
@@ -333,8 +335,8 @@ class OOTWorld(World):
# Set internal names used by the OoT generator
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
- self.trials_random = self.multiworld.trials[self.player].randomized
- self.mq_dungeons_random = self.multiworld.mq_dungeons_count[self.player].randomized
+ self.trials_random = self.options.trials.randomized
+ self.mq_dungeons_random = self.options.mq_dungeons_count.randomized
self.easier_fire_arrow_entry = self.fae_torch_count < 24
if self.misc_hints:
@@ -393,8 +395,8 @@ class OOTWorld(World):
elif self.key_rings == 'choose':
self.key_rings = self.key_rings_list
elif self.key_rings == 'random_dungeons':
- self.key_rings = self.multiworld.random.sample(keyring_dungeons,
- self.multiworld.random.randint(0, len(keyring_dungeons)))
+ self.key_rings = self.random.sample(keyring_dungeons,
+ self.random.randint(0, len(keyring_dungeons)))
# Determine which dungeons are MQ. Not compatible with glitched logic.
mq_dungeons = set()
@@ -405,7 +407,7 @@ class OOTWorld(World):
elif self.mq_dungeons_mode == 'specific':
mq_dungeons = self.mq_dungeons_specific
elif self.mq_dungeons_mode == 'count':
- mq_dungeons = self.multiworld.random.sample(all_dungeons, self.mq_dungeons_count)
+ mq_dungeons = self.random.sample(all_dungeons, self.mq_dungeons_count)
else:
self.mq_dungeons_mode = 'count'
self.mq_dungeons_count = 0
@@ -425,8 +427,8 @@ class OOTWorld(World):
elif self.dungeon_shortcuts_choice == 'all':
self.dungeon_shortcuts = set(shortcut_dungeons)
elif self.dungeon_shortcuts_choice == 'random':
- self.dungeon_shortcuts = self.multiworld.random.sample(shortcut_dungeons,
- self.multiworld.random.randint(0, len(shortcut_dungeons)))
+ self.dungeon_shortcuts = self.random.sample(shortcut_dungeons,
+ self.random.randint(0, len(shortcut_dungeons)))
# == 'choice', leave as previous
else:
self.dungeon_shortcuts = set()
@@ -576,7 +578,7 @@ class OOTWorld(World):
new_exit = OOTEntrance(self.player, self.multiworld, '%s -> %s' % (new_region.name, exit), new_region)
new_exit.vanilla_connected_region = exit
new_exit.rule_string = rule
- if self.multiworld.logic_rules != 'none':
+ if self.options.logic_rules != 'no_logic':
self.parser.parse_spot_rule(new_exit)
if new_exit.never:
logger.debug('Dropping unreachable exit: %s', new_exit.name)
@@ -607,7 +609,7 @@ class OOTWorld(World):
elif self.shuffle_scrubs == 'random':
# this is a random value between 0-99
# average value is ~33 rupees
- price = int(self.multiworld.random.betavariate(1, 2) * 99)
+ price = int(self.random.betavariate(1, 2) * 99)
# Set price in the dictionary as well as the location.
self.scrub_prices[scrub_item] = price
@@ -624,7 +626,7 @@ class OOTWorld(World):
self.shop_prices = {}
for region in self.regions:
if self.shopsanity == 'random':
- shop_item_count = self.multiworld.random.randint(0, 4)
+ shop_item_count = self.random.randint(0, 4)
else:
shop_item_count = int(self.shopsanity)
@@ -632,17 +634,17 @@ class OOTWorld(World):
if location.type == 'Shop':
if location.name[-1:] in shop_item_indexes[:shop_item_count]:
if self.shopsanity_prices == 'normal':
- self.shop_prices[location.name] = int(self.multiworld.random.betavariate(1.5, 2) * 60) * 5
+ self.shop_prices[location.name] = int(self.random.betavariate(1.5, 2) * 60) * 5
elif self.shopsanity_prices == 'affordable':
self.shop_prices[location.name] = 10
elif self.shopsanity_prices == 'starting_wallet':
- self.shop_prices[location.name] = self.multiworld.random.randrange(0,100,5)
+ self.shop_prices[location.name] = self.random.randrange(0,100,5)
elif self.shopsanity_prices == 'adults_wallet':
- self.shop_prices[location.name] = self.multiworld.random.randrange(0,201,5)
+ self.shop_prices[location.name] = self.random.randrange(0,201,5)
elif self.shopsanity_prices == 'giants_wallet':
- self.shop_prices[location.name] = self.multiworld.random.randrange(0,501,5)
+ self.shop_prices[location.name] = self.random.randrange(0,501,5)
elif self.shopsanity_prices == 'tycoons_wallet':
- self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5)
+ self.shop_prices[location.name] = self.random.randrange(0,1000,5)
# Fill boss prizes
@@ -667,8 +669,8 @@ class OOTWorld(World):
while bossCount:
bossCount -= 1
- self.multiworld.random.shuffle(prizepool)
- self.multiworld.random.shuffle(prize_locs)
+ self.random.shuffle(prizepool)
+ self.random.shuffle(prize_locs)
item = prizepool.pop()
loc = prize_locs.pop()
loc.place_locked_item(item)
@@ -778,7 +780,7 @@ class OOTWorld(World):
# Call the junk fill and get a replacement
if item in self.itempool:
self.itempool.remove(item)
- self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
+ self.itempool.append(self.create_item(*get_junk_item(self.random, pool=junk_pool)))
if self.start_with_consumables:
self.starting_items['Deku Sticks'] = 30
self.starting_items['Deku Nuts'] = 40
@@ -881,7 +883,7 @@ class OOTWorld(World):
# Prefill shops, songs, and dungeon items
items = self.get_pre_fill_items()
locations = list(self.multiworld.get_unfilled_locations(self.player))
- self.multiworld.random.shuffle(locations)
+ self.random.shuffle(locations)
# Set up initial state
state = CollectionState(self.multiworld)
@@ -910,7 +912,7 @@ class OOTWorld(World):
if isinstance(locations, list):
for item in stage_items:
self.pre_fill_items.remove(item)
- self.multiworld.random.shuffle(locations)
+ self.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items,
single_player_placement=True, lock=True, allow_excluded=True)
else:
@@ -923,7 +925,7 @@ class OOTWorld(World):
if isinstance(locations, list):
for item in dungeon_items:
self.pre_fill_items.remove(item)
- self.multiworld.random.shuffle(locations)
+ self.random.shuffle(locations)
fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items,
single_player_placement=True, lock=True, allow_excluded=True)
@@ -964,7 +966,7 @@ class OOTWorld(World):
while tries:
try:
- self.multiworld.random.shuffle(song_locations)
+ self.random.shuffle(song_locations)
fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:],
single_player_placement=True, lock=True, allow_excluded=True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
@@ -996,7 +998,7 @@ class OOTWorld(World):
'Buy Goron Tunic': 1,
'Buy Zora Tunic': 1,
}.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement
- self.multiworld.random.shuffle(shop_locations)
+ self.random.shuffle(shop_locations)
self.pre_fill_items = [] # all prefill should be done
fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog,
single_player_placement=True, lock=True, allow_excluded=True)
@@ -1028,7 +1030,7 @@ class OOTWorld(World):
ganon_junk_fill = min(1, ganon_junk_fill)
gc = next(filter(lambda dungeon: dungeon.name == 'Ganons Castle', self.dungeons))
locations = [loc.name for region in gc.regions for loc in region.locations if loc.item is None]
- junk_fill_locations = self.multiworld.random.sample(locations, round(len(locations) * ganon_junk_fill))
+ junk_fill_locations = self.random.sample(locations, round(len(locations) * ganon_junk_fill))
exclusion_rules(self.multiworld, self.player, junk_fill_locations)
# Locations which are not sendable must be converted to events
@@ -1074,13 +1076,13 @@ class OOTWorld(World):
trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap]
self.trap_appearances = {}
for loc_id in trap_location_ids:
- self.trap_appearances[loc_id] = self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.fake_items).name)
+ self.trap_appearances[loc_id] = self.create_item(self.random.choice(self.fake_items).name)
# Seed hint RNG, used for ganon text lines also
- self.hint_rng = self.multiworld.per_slot_randoms[self.player]
+ self.hint_rng = self.random
outfile_name = self.multiworld.get_out_file_name_base(self.player)
- rom = Rom(file=get_options()['oot_options']['rom_file'])
+ rom = Rom(file=get_settings()['oot_options']['rom_file'])
try:
if self.hints != 'none':
buildWorldGossipHints(self)
@@ -1092,7 +1094,7 @@ class OOTWorld(World):
finally:
self.collectible_flags_available.set()
rom.update_header()
- patch_data = create_patch_file(rom)
+ patch_data = create_patch_file(rom, self.random)
rom.restore()
apz5 = OoTContainer(patch_data, outfile_name, output_directory,
@@ -1399,7 +1401,7 @@ class OOTWorld(World):
return all_state
def get_filler_item_name(self) -> str:
- return get_junk_item(count=1, pool=get_junk_pool(self))[0]
+ return get_junk_item(self.random, count=1, pool=get_junk_pool(self))[0]
def valid_dungeon_item_location(world: OOTWorld, option: str, dungeon: str, loc: OOTLocation) -> bool:
diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py
index 520cd8e8b0..81e017eddb 100644
--- a/worlds/osrs/Options.py
+++ b/worlds/osrs/Options.py
@@ -63,6 +63,7 @@ class MaxCombatLevel(Range):
The highest combat level of monster to possibly be assigned as a task.
If set to 0, no combat tasks will be generated.
"""
+ display_name = "Max Required Enemy Combat Level"
range_start = 0
range_end = 1520
default = 50
@@ -74,6 +75,7 @@ class MaxCombatTasks(Range):
If set to 0, no combat tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Combat Task Count"
range_start = 0
range_end = MAX_COMBAT_TASKS
default = MAX_COMBAT_TASKS
@@ -85,6 +87,7 @@ class CombatTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Combat Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -95,6 +98,7 @@ class MaxPrayerLevel(Range):
The highest Prayer requirement of any task generated.
If set to 0, no Prayer tasks will be generated.
"""
+ display_name = "Max Required Prayer Level"
range_start = 0
range_end = 99
default = 50
@@ -106,6 +110,7 @@ class MaxPrayerTasks(Range):
If set to 0, no Prayer tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Prayer Task Count"
range_start = 0
range_end = MAX_PRAYER_TASKS
default = MAX_PRAYER_TASKS
@@ -117,6 +122,7 @@ class PrayerTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Prayer Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -127,6 +133,7 @@ class MaxMagicLevel(Range):
The highest Magic requirement of any task generated.
If set to 0, no Magic tasks will be generated.
"""
+ display_name = "Max Required Magic Level"
range_start = 0
range_end = 99
default = 50
@@ -138,6 +145,7 @@ class MaxMagicTasks(Range):
If set to 0, no Magic tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Magic Task Count"
range_start = 0
range_end = MAX_MAGIC_TASKS
default = MAX_MAGIC_TASKS
@@ -149,6 +157,7 @@ class MagicTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Magic Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -159,6 +168,7 @@ class MaxRunecraftLevel(Range):
The highest Runecraft requirement of any task generated.
If set to 0, no Runecraft tasks will be generated.
"""
+ display_name = "Max Required Runecraft Level"
range_start = 0
range_end = 99
default = 50
@@ -170,6 +180,7 @@ class MaxRunecraftTasks(Range):
If set to 0, no Runecraft tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Runecraft Task Count"
range_start = 0
range_end = MAX_RUNECRAFT_TASKS
default = MAX_RUNECRAFT_TASKS
@@ -181,6 +192,7 @@ class RunecraftTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Runecraft Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -191,6 +203,7 @@ class MaxCraftingLevel(Range):
The highest Crafting requirement of any task generated.
If set to 0, no Crafting tasks will be generated.
"""
+ display_name = "Max Required Crafting Level"
range_start = 0
range_end = 99
default = 50
@@ -202,6 +215,7 @@ class MaxCraftingTasks(Range):
If set to 0, no Crafting tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Crafting Task Count"
range_start = 0
range_end = MAX_CRAFTING_TASKS
default = MAX_CRAFTING_TASKS
@@ -213,6 +227,7 @@ class CraftingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Crafting Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -223,6 +238,7 @@ class MaxMiningLevel(Range):
The highest Mining requirement of any task generated.
If set to 0, no Mining tasks will be generated.
"""
+ display_name = "Max Required Mining Level"
range_start = 0
range_end = 99
default = 50
@@ -234,6 +250,7 @@ class MaxMiningTasks(Range):
If set to 0, no Mining tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Mining Task Count"
range_start = 0
range_end = MAX_MINING_TASKS
default = MAX_MINING_TASKS
@@ -245,6 +262,7 @@ class MiningTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Mining Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -255,6 +273,7 @@ class MaxSmithingLevel(Range):
The highest Smithing requirement of any task generated.
If set to 0, no Smithing tasks will be generated.
"""
+ display_name = "Max Required Smithing Level"
range_start = 0
range_end = 99
default = 50
@@ -266,6 +285,7 @@ class MaxSmithingTasks(Range):
If set to 0, no Smithing tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Smithing Task Count"
range_start = 0
range_end = MAX_SMITHING_TASKS
default = MAX_SMITHING_TASKS
@@ -277,6 +297,7 @@ class SmithingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Smithing Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -287,6 +308,7 @@ class MaxFishingLevel(Range):
The highest Fishing requirement of any task generated.
If set to 0, no Fishing tasks will be generated.
"""
+ display_name = "Max Required Fishing Level"
range_start = 0
range_end = 99
default = 50
@@ -298,6 +320,7 @@ class MaxFishingTasks(Range):
If set to 0, no Fishing tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Fishing Task Count"
range_start = 0
range_end = MAX_FISHING_TASKS
default = MAX_FISHING_TASKS
@@ -309,6 +332,7 @@ class FishingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Fishing Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -319,6 +343,7 @@ class MaxCookingLevel(Range):
The highest Cooking requirement of any task generated.
If set to 0, no Cooking tasks will be generated.
"""
+ display_name = "Max Required Cooking Level"
range_start = 0
range_end = 99
default = 50
@@ -330,6 +355,7 @@ class MaxCookingTasks(Range):
If set to 0, no Cooking tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Cooking Task Count"
range_start = 0
range_end = MAX_COOKING_TASKS
default = MAX_COOKING_TASKS
@@ -341,6 +367,7 @@ class CookingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Cooking Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -351,6 +378,7 @@ class MaxFiremakingLevel(Range):
The highest Firemaking requirement of any task generated.
If set to 0, no Firemaking tasks will be generated.
"""
+ display_name = "Max Required Firemaking Level"
range_start = 0
range_end = 99
default = 50
@@ -362,6 +390,7 @@ class MaxFiremakingTasks(Range):
If set to 0, no Firemaking tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Firemaking Task Count"
range_start = 0
range_end = MAX_FIREMAKING_TASKS
default = MAX_FIREMAKING_TASKS
@@ -373,6 +402,7 @@ class FiremakingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Firemaking Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -383,6 +413,7 @@ class MaxWoodcuttingLevel(Range):
The highest Woodcutting requirement of any task generated.
If set to 0, no Woodcutting tasks will be generated.
"""
+ display_name = "Max Required Woodcutting Level"
range_start = 0
range_end = 99
default = 50
@@ -394,6 +425,7 @@ class MaxWoodcuttingTasks(Range):
If set to 0, no Woodcutting tasks will be generated.
This only determines the maximum possible, fewer than the maximum could be assigned.
"""
+ display_name = "Max Woodcutting Task Count"
range_start = 0
range_end = MAX_WOODCUTTING_TASKS
default = MAX_WOODCUTTING_TASKS
@@ -405,6 +437,7 @@ class WoodcuttingTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "Woodcutting Task Weight"
range_start = 0
range_end = 99
default = 50
@@ -416,6 +449,7 @@ class MinimumGeneralTasks(Range):
General progression tasks will be used to fill out any holes caused by having fewer possible tasks than needed, so
there is no maximum.
"""
+ display_name = "Minimum General Task Count"
range_start = 0
range_end = NON_QUEST_LOCATION_COUNT
default = 10
@@ -427,6 +461,7 @@ class GeneralTaskWeight(Range):
Weights of all Task Types will be compared against each other, a task with 50 weight
is twice as likely to appear as one with 25.
"""
+ display_name = "General Task Weight"
range_start = 0
range_end = 99
default = 50
diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py
index 49aa166608..9ed55f218d 100644
--- a/worlds/osrs/__init__.py
+++ b/worlds/osrs/__init__.py
@@ -90,16 +90,18 @@ class OSRSWorld(World):
rnd = self.random
starting_area = self.options.starting_area
+
+ #UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT
+ if not hasattr(self.multiworld, "generation_is_fake"):
+ if starting_area.value == StartingArea.option_any_bank:
+ self.starting_area_item = rnd.choice(starting_area_dict)
+ elif starting_area.value < StartingArea.option_chunksanity:
+ self.starting_area_item = starting_area_dict[starting_area.value]
+ else:
+ self.starting_area_item = rnd.choice(chunksanity_starting_chunks)
- if starting_area.value == StartingArea.option_any_bank:
- self.starting_area_item = rnd.choice(starting_area_dict)
- elif starting_area.value < StartingArea.option_chunksanity:
- self.starting_area_item = starting_area_dict[starting_area.value]
- else:
- self.starting_area_item = rnd.choice(chunksanity_starting_chunks)
-
- # Set Starting Chunk
- self.multiworld.push_precollected(self.create_item(self.starting_area_item))
+ # Set Starting Chunk
+ self.multiworld.push_precollected(self.create_item(self.starting_area_item))
"""
This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client.
@@ -109,8 +111,23 @@ class OSRSWorld(World):
def fill_slot_data(self):
data = self.options.as_dict("brutal_grinds")
data["data_csv_tag"] = data_csv_tag
+ data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv
return data
+ def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None:
+ if "starting_area" in slot_data:
+ self.starting_area_item = slot_data["starting_area"]
+ menu_region = self.multiworld.get_region("Menu",self.player)
+ menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot
+ if self.starting_area_item in chunksanity_special_region_names:
+ starting_area_region = chunksanity_special_region_names[self.starting_area_item]
+ else:
+ starting_area_region = self.starting_area_item[6:] # len("Area: ")
+ starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
+ starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
+ starting_entrance.connect(self.region_name_to_data[starting_area_region])
+
+
def create_regions(self) -> None:
"""
called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done
@@ -128,13 +145,14 @@ class OSRSWorld(World):
# Removes the word "Area: " from the item name to get the region it applies to.
# I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse
- if self.starting_area_item in chunksanity_special_region_names:
- starting_area_region = chunksanity_special_region_names[self.starting_area_item]
- else:
- starting_area_region = self.starting_area_item[6:] # len("Area: ")
- starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
- starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
- starting_entrance.connect(self.region_name_to_data[starting_area_region])
+ if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it
+ if self.starting_area_item in chunksanity_special_region_names:
+ starting_area_region = chunksanity_special_region_names[self.starting_area_item]
+ else:
+ starting_area_region = self.starting_area_item[6:] # len("Area: ")
+ starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
+ starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
+ starting_entrance.connect(self.region_name_to_data[starting_area_region])
# Create entrances between regions
for region_row in region_rows:
diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py
index fe62e61624..51c55b437d 100644
--- a/worlds/sc2/ClientGui.py
+++ b/worlds/sc2/ClientGui.py
@@ -111,13 +111,10 @@ class SC2Manager(GameManager):
def build(self):
container = super().build()
- panel = TabbedPanelItem(text="Starcraft 2 Launcher")
- panel.content = CampaignScroll()
+ panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll())
self.campaign_panel = MultiCampaignLayout()
panel.content.add_widget(self.campaign_panel)
- self.tabs.add_widget(panel)
-
Clock.schedule_interval(self.build_mission_table, 0.5)
return container
diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py
index 8277d0e7e1..ee1f34d75b 100644
--- a/worlds/sc2/Items.py
+++ b/worlds/sc2/Items.py
@@ -1274,16 +1274,16 @@ item_table = {
description="Defensive structure. Slows the attack and movement speeds of all nearby Zerg units."),
ItemNames.STRUCTURE_ARMOR:
ItemData(620 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9, SC2Race.TERRAN,
- description="Increases armor of all Terran structures by 2."),
+ description="Increases armor of all Terran structures by 2.", origin={"ext"}),
ItemNames.HI_SEC_AUTO_TRACKING:
ItemData(621 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, SC2Race.TERRAN,
- description="Increases attack range of all Terran structures by 1."),
+ description="Increases attack range of all Terran structures by 1.", origin={"ext"}),
ItemNames.ADVANCED_OPTICS:
ItemData(622 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, SC2Race.TERRAN,
- description="Increases attack range of all Terran mechanical units by 1."),
+ description="Increases attack range of all Terran mechanical units by 1.", origin={"ext"}),
ItemNames.ROGUE_FORCES:
ItemData(623 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, SC2Race.TERRAN,
- description="Mercenary calldowns are no longer limited by charges."),
+ description="Mercenary calldowns are no longer limited by charges.", origin={"ext"}),
ItemNames.ZEALOT:
ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.PROTOSS,
@@ -2369,7 +2369,8 @@ progressive_if_ext = {
ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS,
ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL,
ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM,
- ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL
+ ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL,
+ ItemNames.PROGRESSIVE_ORBITAL_COMMAND
}
kerrigan_actives: typing.List[typing.Set[str]] = [
diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md
index 5983057f7d..7456bcb70b 100644
--- a/worlds/sm64ex/docs/setup_en.md
+++ b/worlds/sm64ex/docs/setup_en.md
@@ -77,9 +77,6 @@ Should your name or password have spaces, enclose it in quotes: `"YourPassword"`
Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that.
Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text.
-**Important:** You must start a new file for every new seed you play. Using `⭐x0` files is **not** sufficient.
-Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file.
-
### Playing offline
To play offline, first generate a seed on the game's options page.
@@ -129,18 +126,6 @@ To use this batch file, double-click it. A window will open. Type the five-digi
Once you provide those two bits of information, the game will open.
- If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail.
-### Addendum - Deleting old saves
-
-Loading an old Mario save alongside a new seed is a bad idea, as it can cause locked doors and castle secret stars to already be unlocked / obtained. You should avoid opening a save that says "Stars x 0" as opposed to one that simply says "New".
-
-You can manually delete these old saves in-game before starting a new game, but that can be tedious. With a small edit to the batch files, you can delete these old saves automatically. Just add the line `del %AppData%\sm64ex\*.bin` to the batch file, above the `start` command. For example, here is `offline.bat` with the additional line:
-
-`del %AppData%\sm64ex\*.bin`
-
-`start sm64.us.f3dex2e.exe --sm64ap_file %1`
-
-This extra line deletes any previous save data before opening the game. Don't worry about lost stars or checks - the AP server (or in the case of offline, the `.save` file) keeps track of your star count, unlocked keys/caps/cannons, and which locations have already been checked, so you won't have to redo them. At worst you'll have to rewatch the door unlocking animations, and catch the rabbit Mips twice for his first star again if you haven't yet collected the second one.
-
## Installation Troubleshooting
Start the game from the command line to view helpful messages regarding SM64EX.
@@ -166,8 +151,9 @@ The Japanese Version should have no problem displaying these.
### Toad does not have an item for me.
-This happens when you load an existing file that had already received an item from that toad.
+This happens on older builds when you load an existing file that had already received an item from that toad.
To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress.
+Alternatively, updating your build will prevent this issue in the future.
### What happens if I lose connection?
diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py
index 73cc8f119a..913fe4b8ad 100644
--- a/worlds/stardew_valley/content/vanilla/pelican_town.py
+++ b/worlds/stardew_valley/content/vanilla/pelican_town.py
@@ -2,7 +2,7 @@ from ..game_content import ContentPack
from ...data import villagers_data, fish_data
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource
from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
-from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement
+from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
from ...strings.book_names import Book
from ...strings.crop_names import Fruit
@@ -250,10 +250,7 @@ pelican_town = ContentPack(
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
Book.the_art_o_crabbing: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
- GenericSource(regions=(Region.beach,),
- other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium),
- SkillRequirement(Skill.fishing, 6),
- SeasonRequirement(Season.winter))),
+ CustomRuleSource(create_rule=lambda logic: logic.festival.has_squidfest_day_1_iridium_reward()),
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
Book.treasure_appraisal_guide: (
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv
index e026090f86..ffcae223e2 100644
--- a/worlds/stardew_valley/data/items.csv
+++ b/worlds/stardew_valley/data/items.csv
@@ -474,7 +474,7 @@ id,name,classification,groups,mod_name
507,Resource Pack: 40 Calico Egg,useful,"FESTIVAL",
508,Resource Pack: 35 Calico Egg,useful,"FESTIVAL",
509,Resource Pack: 30 Calico Egg,useful,"FESTIVAL",
-510,Book: The Art O' Crabbing,useful,"FESTIVAL",
+510,Book: The Art O' Crabbing,progression,"FESTIVAL",
511,Mr Qi's Plane Ride,progression,,
521,Power: Price Catalogue,useful,"BOOK_POWER",
522,Power: Mapping Cave Systems,useful,"BOOK_POWER",
@@ -819,6 +819,7 @@ id,name,classification,groups,mod_name
5289,Prismatic Shard,filler,"RESOURCE_PACK",
5290,Stardrop Tea,filler,"RESOURCE_PACK",
5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK",
+5292,Resource Pack: 20 Cinder Shard,filler,"GINGER_ISLAND,RESOURCE_PACK",
10001,Luck Level,progression,SKILL_LEVEL_UP,Luck Skill
10002,Magic Level,progression,SKILL_LEVEL_UP,Magic
10003,Socializing Level,progression,SKILL_LEVEL_UP,Socializing Skill
diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv
index 608b6a5f57..680ddfcbac 100644
--- a/worlds/stardew_valley/data/locations.csv
+++ b/worlds/stardew_valley/data/locations.csv
@@ -313,14 +313,14 @@ id,region,name,tags,mod_name
611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK",
612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART",
613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART",
-614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART",
+614,Junimo Kart 4,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART",
615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART",
616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART",
617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART",
618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART",
619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART",
620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JOTPK",
-621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART",
+621,Junimo Kart 4,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART",
701,Secret Woods,Old Master Cannoli,MANDATORY,
702,Beach,Beach Bridge Repair,MANDATORY,
703,Desert,Galaxy Sword Shrine,MANDATORY,
diff --git a/worlds/stardew_valley/logic/festival_logic.py b/worlds/stardew_valley/logic/festival_logic.py
new file mode 100644
index 0000000000..2b22617202
--- /dev/null
+++ b/worlds/stardew_valley/logic/festival_logic.py
@@ -0,0 +1,186 @@
+from typing import Union
+
+from .action_logic import ActionLogicMixin
+from .animal_logic import AnimalLogicMixin
+from .artisan_logic import ArtisanLogicMixin
+from .base_logic import BaseLogicMixin, BaseLogic
+from .fishing_logic import FishingLogicMixin
+from .gift_logic import GiftLogicMixin
+from .has_logic import HasLogicMixin
+from .money_logic import MoneyLogicMixin
+from .monster_logic import MonsterLogicMixin
+from .museum_logic import MuseumLogicMixin
+from .received_logic import ReceivedLogicMixin
+from .region_logic import RegionLogicMixin
+from .relationship_logic import RelationshipLogicMixin
+from .skill_logic import SkillLogicMixin
+from .time_logic import TimeLogicMixin
+from ..options import FestivalLocations
+from ..stardew_rule import StardewRule
+from ..strings.book_names import Book
+from ..strings.craftable_names import Fishing
+from ..strings.crop_names import Fruit, Vegetable
+from ..strings.festival_check_names import FestivalCheck
+from ..strings.fish_names import Fish
+from ..strings.forageable_names import Forageable
+from ..strings.generic_names import Generic
+from ..strings.machine_names import Machine
+from ..strings.monster_names import Monster
+from ..strings.region_names import Region
+
+
+class FestivalLogicMixin(BaseLogicMixin):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.festival = FestivalLogic(*args, **kwargs)
+
+
+class FestivalLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, FestivalLogicMixin, ArtisanLogicMixin, AnimalLogicMixin, MoneyLogicMixin, TimeLogicMixin,
+SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, RelationshipLogicMixin, FishingLogicMixin, MuseumLogicMixin, GiftLogicMixin]]):
+
+ def initialize_rules(self):
+ self.registry.festival_rules.update({
+ FestivalCheck.egg_hunt: self.logic.festival.can_win_egg_hunt(),
+ FestivalCheck.strawberry_seeds: self.logic.money.can_spend(1000),
+ FestivalCheck.dance: self.logic.relationship.has_hearts_with_any_bachelor(4),
+ FestivalCheck.tub_o_flowers: self.logic.money.can_spend(2000),
+ FestivalCheck.rarecrow_5: self.logic.money.can_spend(2500),
+ FestivalCheck.luau_soup: self.logic.festival.can_succeed_luau_soup(),
+ FestivalCheck.moonlight_jellies: self.logic.true_,
+ FestivalCheck.moonlight_jellies_banner: self.logic.money.can_spend(800),
+ FestivalCheck.starport_decal: self.logic.money.can_spend(1000),
+ FestivalCheck.smashing_stone: self.logic.true_,
+ FestivalCheck.grange_display: self.logic.festival.can_succeed_grange_display(),
+ FestivalCheck.rarecrow_1: self.logic.true_, # only cost star tokens
+ FestivalCheck.fair_stardrop: self.logic.true_, # only cost star tokens
+ FestivalCheck.spirit_eve_maze: self.logic.true_,
+ FestivalCheck.jack_o_lantern: self.logic.money.can_spend(2000),
+ FestivalCheck.rarecrow_2: self.logic.money.can_spend(5000),
+ FestivalCheck.fishing_competition: self.logic.festival.can_win_fishing_competition(),
+ FestivalCheck.rarecrow_4: self.logic.money.can_spend(5000),
+ FestivalCheck.mermaid_pearl: self.logic.has(Forageable.secret_note),
+ FestivalCheck.cone_hat: self.logic.money.can_spend(2500),
+ FestivalCheck.iridium_fireplace: self.logic.money.can_spend(15000),
+ FestivalCheck.rarecrow_7: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_artifacts(20),
+ FestivalCheck.rarecrow_8: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_items(40),
+ FestivalCheck.lupini_red_eagle: self.logic.money.can_spend(1200),
+ FestivalCheck.lupini_portrait_mermaid: self.logic.money.can_spend(1200),
+ FestivalCheck.lupini_solar_kingdom: self.logic.money.can_spend(1200),
+ FestivalCheck.lupini_clouds: self.logic.time.has_year_two & self.logic.money.can_spend(1200),
+ FestivalCheck.lupini_1000_years: self.logic.time.has_year_two & self.logic.money.can_spend(1200),
+ FestivalCheck.lupini_three_trees: self.logic.time.has_year_two & self.logic.money.can_spend(1200),
+ FestivalCheck.lupini_the_serpent: self.logic.time.has_year_three & self.logic.money.can_spend(1200),
+ FestivalCheck.lupini_tropical_fish: self.logic.time.has_year_three & self.logic.money.can_spend(1200),
+ FestivalCheck.lupini_land_of_clay: self.logic.time.has_year_three & self.logic.money.can_spend(1200),
+ FestivalCheck.secret_santa: self.logic.gifts.has_any_universal_love,
+ FestivalCheck.legend_of_the_winter_star: self.logic.true_,
+ FestivalCheck.rarecrow_3: self.logic.true_,
+ FestivalCheck.all_rarecrows: self.logic.region.can_reach(Region.farm) & self.logic.festival.has_all_rarecrows(),
+ FestivalCheck.calico_race: self.logic.true_,
+ FestivalCheck.mummy_mask: self.logic.true_,
+ FestivalCheck.calico_statue: self.logic.true_,
+ FestivalCheck.emily_outfit_service: self.logic.true_,
+ FestivalCheck.earthy_mousse: self.logic.true_,
+ FestivalCheck.sweet_bean_cake: self.logic.true_,
+ FestivalCheck.skull_cave_casserole: self.logic.true_,
+ FestivalCheck.spicy_tacos: self.logic.true_,
+ FestivalCheck.mountain_chili: self.logic.true_,
+ FestivalCheck.crystal_cake: self.logic.true_,
+ FestivalCheck.cave_kebab: self.logic.true_,
+ FestivalCheck.hot_log: self.logic.true_,
+ FestivalCheck.sour_salad: self.logic.true_,
+ FestivalCheck.superfood_cake: self.logic.true_,
+ FestivalCheck.warrior_smoothie: self.logic.true_,
+ FestivalCheck.rumpled_fruit_skin: self.logic.true_,
+ FestivalCheck.calico_pizza: self.logic.true_,
+ FestivalCheck.stuffed_mushrooms: self.logic.true_,
+ FestivalCheck.elf_quesadilla: self.logic.true_,
+ FestivalCheck.nachos_of_the_desert: self.logic.true_,
+ FestivalCheck.cloppino: self.logic.true_,
+ FestivalCheck.rainforest_shrimp: self.logic.true_,
+ FestivalCheck.shrimp_donut: self.logic.true_,
+ FestivalCheck.smell_of_the_sea: self.logic.true_,
+ FestivalCheck.desert_gumbo: self.logic.true_,
+ FestivalCheck.free_cactis: self.logic.true_,
+ FestivalCheck.monster_hunt: self.logic.monster.can_kill(Monster.serpent),
+ FestivalCheck.deep_dive: self.logic.region.can_reach(Region.skull_cavern_50),
+ FestivalCheck.treasure_hunt: self.logic.region.can_reach(Region.skull_cavern_25),
+ FestivalCheck.touch_calico_statue: self.logic.region.can_reach(Region.skull_cavern_25),
+ FestivalCheck.real_calico_egg_hunter: self.logic.region.can_reach(Region.skull_cavern_100),
+ FestivalCheck.willy_challenge: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.scorpion_carp]),
+ FestivalCheck.desert_scholar: self.logic.true_,
+ FestivalCheck.squidfest_day_1_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]),
+ FestivalCheck.squidfest_day_1_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait),
+ FestivalCheck.squidfest_day_1_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait),
+ FestivalCheck.squidfest_day_1_iridium: self.logic.festival.can_squidfest_day_1_iridium_reward(),
+ FestivalCheck.squidfest_day_2_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]),
+ FestivalCheck.squidfest_day_2_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait),
+ FestivalCheck.squidfest_day_2_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait),
+ FestivalCheck.squidfest_day_2_iridium: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) &
+ self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid]),
+ })
+ for i in range(1, 11):
+ check_name = f"{FestivalCheck.trout_derby_reward_pattern}{i}"
+ self.registry.festival_rules[check_name] = self.logic.fishing.can_catch_fish(self.content.fishes[Fish.rainbow_trout])
+
+ def can_squidfest_day_1_iridium_reward(self) -> StardewRule:
+ return self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid])
+
+ def has_squidfest_day_1_iridium_reward(self) -> StardewRule:
+ if self.options.festival_locations == FestivalLocations.option_disabled:
+ return self.logic.festival.can_squidfest_day_1_iridium_reward()
+ else:
+ return self.logic.received(f"Book: {Book.the_art_o_crabbing}")
+
+ def can_win_egg_hunt(self) -> StardewRule:
+ return self.logic.true_
+
+ def can_succeed_luau_soup(self) -> StardewRule:
+ if self.options.festival_locations != FestivalLocations.option_hard:
+ return self.logic.true_
+ eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish,
+ Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber)
+ fish_rule = self.logic.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray
+ eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon,
+ Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry,
+ Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum,
+ Vegetable.hops, Vegetable.wheat)
+ keg_rules = [self.logic.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items]
+ aged_rule = self.logic.has(Machine.cask) & self.logic.or_(*keg_rules)
+ # There are a few other valid items, but I don't feel like coding them all
+ return fish_rule | aged_rule
+
+ def can_succeed_grange_display(self) -> StardewRule:
+ if self.options.festival_locations != FestivalLocations.option_hard:
+ return self.logic.true_
+
+ animal_rule = self.logic.animal.has_animal(Generic.any)
+ artisan_rule = self.logic.artisan.can_keg(Generic.any) | self.logic.artisan.can_preserves_jar(Generic.any)
+ cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough
+ fish_rule = self.logic.skill.can_fish(difficulty=50)
+ forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall
+ mineral_rule = self.logic.action.can_open_geode(Generic.any) # More than half the minerals are good enough
+ good_fruits = (fruit
+ for fruit in
+ (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate,
+ Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit)
+ if fruit in self.content.game_items)
+ fruit_rule = self.logic.has_any(*good_fruits)
+ good_vegetables = (vegeteable
+ for vegeteable in
+ (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale,
+ Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin)
+ if vegeteable in self.content.game_items)
+ vegetable_rule = self.logic.has_any(*good_vegetables)
+
+ return animal_rule & artisan_rule & cooking_rule & fish_rule & \
+ forage_rule & fruit_rule & mineral_rule & vegetable_rule
+
+ def can_win_fishing_competition(self) -> StardewRule:
+ return self.logic.skill.can_fish(difficulty=60)
+
+ def has_all_rarecrows(self) -> StardewRule:
+ rules = []
+ for rarecrow_number in range(1, 9):
+ rules.append(self.logic.received(f"Rarecrow #{rarecrow_number}"))
+ return self.logic.and_(*rules)
diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py
index fb0d938fbb..9d4447439f 100644
--- a/worlds/stardew_valley/logic/logic.py
+++ b/worlds/stardew_valley/logic/logic.py
@@ -16,6 +16,7 @@ from .combat_logic import CombatLogicMixin
from .cooking_logic import CookingLogicMixin
from .crafting_logic import CraftingLogicMixin
from .farming_logic import FarmingLogicMixin
+from .festival_logic import FestivalLogicMixin
from .fishing_logic import FishingLogicMixin
from .gift_logic import GiftLogicMixin
from .grind_logic import GrindLogicMixin
@@ -62,7 +63,6 @@ from ..strings.crop_names import Fruit, Vegetable
from ..strings.currency_names import Currency
from ..strings.decoration_names import Decoration
from ..strings.fertilizer_names import Fertilizer, SpeedGro, RetainingSoil
-from ..strings.festival_check_names import FestivalCheck
from ..strings.fish_names import Fish, Trash, WaterItem, WaterChest
from ..strings.flower_names import Flower
from ..strings.food_names import Meal, Beverage
@@ -94,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin,
SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin,
SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin,
- RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin):
+ RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin):
player: int
options: StardewValleyOptions
content: StardewContent
@@ -363,89 +363,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
self.quest.initialize_rules()
self.quest.update_rules(self.mod.quest.get_modded_quest_rules())
- self.registry.festival_rules.update({
- FestivalCheck.egg_hunt: self.can_win_egg_hunt(),
- FestivalCheck.strawberry_seeds: self.money.can_spend(1000),
- FestivalCheck.dance: self.relationship.has_hearts_with_any_bachelor(4),
- FestivalCheck.tub_o_flowers: self.money.can_spend(2000),
- FestivalCheck.rarecrow_5: self.money.can_spend(2500),
- FestivalCheck.luau_soup: self.can_succeed_luau_soup(),
- FestivalCheck.moonlight_jellies: True_(),
- FestivalCheck.moonlight_jellies_banner: self.money.can_spend(800),
- FestivalCheck.starport_decal: self.money.can_spend(1000),
- FestivalCheck.smashing_stone: True_(),
- FestivalCheck.grange_display: self.can_succeed_grange_display(),
- FestivalCheck.rarecrow_1: True_(), # only cost star tokens
- FestivalCheck.fair_stardrop: True_(), # only cost star tokens
- FestivalCheck.spirit_eve_maze: True_(),
- FestivalCheck.jack_o_lantern: self.money.can_spend(2000),
- FestivalCheck.rarecrow_2: self.money.can_spend(5000),
- FestivalCheck.fishing_competition: self.can_win_fishing_competition(),
- FestivalCheck.rarecrow_4: self.money.can_spend(5000),
- FestivalCheck.mermaid_pearl: self.has(Forageable.secret_note),
- FestivalCheck.cone_hat: self.money.can_spend(2500),
- FestivalCheck.iridium_fireplace: self.money.can_spend(15000),
- FestivalCheck.rarecrow_7: self.money.can_spend(5000) & self.museum.can_donate_museum_artifacts(20),
- FestivalCheck.rarecrow_8: self.money.can_spend(5000) & self.museum.can_donate_museum_items(40),
- FestivalCheck.lupini_red_eagle: self.money.can_spend(1200),
- FestivalCheck.lupini_portrait_mermaid: self.money.can_spend(1200),
- FestivalCheck.lupini_solar_kingdom: self.money.can_spend(1200),
- FestivalCheck.lupini_clouds: self.time.has_year_two & self.money.can_spend(1200),
- FestivalCheck.lupini_1000_years: self.time.has_year_two & self.money.can_spend(1200),
- FestivalCheck.lupini_three_trees: self.time.has_year_two & self.money.can_spend(1200),
- FestivalCheck.lupini_the_serpent: self.time.has_year_three & self.money.can_spend(1200),
- FestivalCheck.lupini_tropical_fish: self.time.has_year_three & self.money.can_spend(1200),
- FestivalCheck.lupini_land_of_clay: self.time.has_year_three & self.money.can_spend(1200),
- FestivalCheck.secret_santa: self.gifts.has_any_universal_love,
- FestivalCheck.legend_of_the_winter_star: True_(),
- FestivalCheck.rarecrow_3: True_(),
- FestivalCheck.all_rarecrows: self.region.can_reach(Region.farm) & self.has_all_rarecrows(),
- FestivalCheck.calico_race: True_(),
- FestivalCheck.mummy_mask: True_(),
- FestivalCheck.calico_statue: True_(),
- FestivalCheck.emily_outfit_service: True_(),
- FestivalCheck.earthy_mousse: True_(),
- FestivalCheck.sweet_bean_cake: True_(),
- FestivalCheck.skull_cave_casserole: True_(),
- FestivalCheck.spicy_tacos: True_(),
- FestivalCheck.mountain_chili: True_(),
- FestivalCheck.crystal_cake: True_(),
- FestivalCheck.cave_kebab: True_(),
- FestivalCheck.hot_log: True_(),
- FestivalCheck.sour_salad: True_(),
- FestivalCheck.superfood_cake: True_(),
- FestivalCheck.warrior_smoothie: True_(),
- FestivalCheck.rumpled_fruit_skin: True_(),
- FestivalCheck.calico_pizza: True_(),
- FestivalCheck.stuffed_mushrooms: True_(),
- FestivalCheck.elf_quesadilla: True_(),
- FestivalCheck.nachos_of_the_desert: True_(),
- FestivalCheck.cloppino: True_(),
- FestivalCheck.rainforest_shrimp: True_(),
- FestivalCheck.shrimp_donut: True_(),
- FestivalCheck.smell_of_the_sea: True_(),
- FestivalCheck.desert_gumbo: True_(),
- FestivalCheck.free_cactis: True_(),
- FestivalCheck.monster_hunt: self.monster.can_kill(Monster.serpent),
- FestivalCheck.deep_dive: self.region.can_reach(Region.skull_cavern_50),
- FestivalCheck.treasure_hunt: self.region.can_reach(Region.skull_cavern_25),
- FestivalCheck.touch_calico_statue: self.region.can_reach(Region.skull_cavern_25),
- FestivalCheck.real_calico_egg_hunter: self.region.can_reach(Region.skull_cavern_100),
- FestivalCheck.willy_challenge: self.fishing.can_catch_fish(content.fishes[Fish.scorpion_carp]),
- FestivalCheck.desert_scholar: True_(),
- FestivalCheck.squidfest_day_1_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]),
- FestivalCheck.squidfest_day_1_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait),
- FestivalCheck.squidfest_day_1_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait),
- FestivalCheck.squidfest_day_1_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) &
- self.fishing.has_specific_bait(content.fishes[Fish.squid]),
- FestivalCheck.squidfest_day_2_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]),
- FestivalCheck.squidfest_day_2_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait),
- FestivalCheck.squidfest_day_2_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait),
- FestivalCheck.squidfest_day_2_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) &
- self.fishing.has_specific_bait(content.fishes[Fish.squid]),
- })
- for i in range(1, 11):
- self.registry.festival_rules[f"{FestivalCheck.trout_derby_reward_pattern}{i}"] = self.fishing.can_catch_fish(content.fishes[Fish.rainbow_trout])
+ self.festival.initialize_rules()
self.special_order.initialize_rules()
self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules())
@@ -486,53 +404,6 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
]
return self.count(12, *rules_worth_a_point)
- def can_win_egg_hunt(self) -> StardewRule:
- return True_()
-
- def can_succeed_luau_soup(self) -> StardewRule:
- if self.options.festival_locations != FestivalLocations.option_hard:
- return True_()
- eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish,
- Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber)
- fish_rule = self.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray
- eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon,
- Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry,
- Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum,
- Vegetable.hops, Vegetable.wheat)
- keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items]
- aged_rule = self.has(Machine.cask) & self.logic.or_(*keg_rules)
- # There are a few other valid items, but I don't feel like coding them all
- return fish_rule | aged_rule
-
- def can_succeed_grange_display(self) -> StardewRule:
- if self.options.festival_locations != FestivalLocations.option_hard:
- return True_()
-
- animal_rule = self.animal.has_animal(Generic.any)
- artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any)
- cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough
- fish_rule = self.skill.can_fish(difficulty=50)
- forage_rule = self.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall
- mineral_rule = self.action.can_open_geode(Generic.any) # More than half the minerals are good enough
- good_fruits = (fruit
- for fruit in
- (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate,
- Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit)
- if fruit in self.content.game_items)
- fruit_rule = self.has_any(*good_fruits)
- good_vegetables = (vegeteable
- for vegeteable in
- (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale,
- Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin)
- if vegeteable in self.content.game_items)
- vegetable_rule = self.has_any(*good_vegetables)
-
- return animal_rule & artisan_rule & cooking_rule & fish_rule & \
- forage_rule & fruit_rule & mineral_rule & vegetable_rule
-
- def can_win_fishing_competition(self) -> StardewRule:
- return self.skill.can_fish(difficulty=60)
-
def has_island_trader(self) -> StardewRule:
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return False_()
@@ -571,12 +442,6 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules)
- def has_all_rarecrows(self) -> StardewRule:
- rules = []
- for rarecrow_number in range(1, 9):
- rules.append(self.received(f"Rarecrow #{rarecrow_number}"))
- return self.logic.and_(*rules)
-
def has_abandoned_jojamart(self) -> StardewRule:
return self.received(CommunityUpgrade.movie_theater, 1)
diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py
index b0fc7fa0ea..5b7db5ac79 100644
--- a/worlds/stardew_valley/regions.py
+++ b/worlds/stardew_valley/regions.py
@@ -87,7 +87,8 @@ vanilla_regions = [
RegionData(Region.jotpk_world_3),
RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
- RegionData(Region.junimo_kart_3),
+ RegionData(Region.junimo_kart_3, [Entrance.reach_junimo_kart_4]),
+ RegionData(Region.junimo_kart_4),
RegionData(Region.alex_house),
RegionData(Region.trailer),
RegionData(Region.mayor_house),
@@ -330,6 +331,7 @@ vanilla_connections = [
ConnectionData(Entrance.play_junimo_kart, Region.junimo_kart_1),
ConnectionData(Entrance.reach_junimo_kart_2, Region.junimo_kart_2),
ConnectionData(Entrance.reach_junimo_kart_3, Region.junimo_kart_3),
+ ConnectionData(Entrance.reach_junimo_kart_4, Region.junimo_kart_4),
ConnectionData(Entrance.town_to_sam_house, Region.sam_house,
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
ConnectionData(Entrance.town_to_haley_house, Region.haley_house,
diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py
index 7f39ee1ac2..eda2d4377e 100644
--- a/worlds/stardew_valley/rules.py
+++ b/worlds/stardew_valley/rules.py
@@ -891,7 +891,7 @@ def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player
logic.has("Junimo Kart Medium Buff"))
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player),
logic.has("Junimo Kart Big Buff"))
- MultiWorldRules.add_rule(multiworld.get_location("Junimo Kart: Sunset Speedway (Victory)", player),
+ MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_4, player),
logic.has("Junimo Kart Max Buff"))
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player),
logic.has("JotPK Small Buff"))
diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py
index 58a919f2a8..b1c84004eb 100644
--- a/worlds/stardew_valley/strings/entrance_names.py
+++ b/worlds/stardew_valley/strings/entrance_names.py
@@ -94,6 +94,7 @@ class Entrance:
play_junimo_kart = "Play Junimo Kart"
reach_junimo_kart_2 = "Reach Junimo Kart 2"
reach_junimo_kart_3 = "Reach Junimo Kart 3"
+ reach_junimo_kart_4 = "Reach Junimo Kart 4"
enter_locker_room = "Bathhouse Entrance to Locker Room"
enter_public_bath = "Locker Room to Public Bath"
enter_witch_swamp = "Witch Warp Cave to Witch's Swamp"
diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py
index 58763b6fcb..2bbc6228ab 100644
--- a/worlds/stardew_valley/strings/region_names.py
+++ b/worlds/stardew_valley/strings/region_names.py
@@ -114,6 +114,7 @@ class Region:
junimo_kart_1 = "Junimo Kart 1"
junimo_kart_2 = "Junimo Kart 2"
junimo_kart_3 = "Junimo Kart 3"
+ junimo_kart_4 = "Junimo Kart 4"
mines_floor_5 = "The Mines - Floor 5"
mines_floor_10 = "The Mines - Floor 10"
mines_floor_15 = "The Mines - Floor 15"
diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py
index f99dd76155..2423e06bb0 100644
--- a/worlds/timespinner/Locations.py
+++ b/worlds/timespinner/Locations.py
@@ -207,7 +207,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)),
LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)),
# 1337158 Is lost in time
- LocationData('Library', 'Library: Terminal 3 (Emporer Nuvius)', 1337159, lambda state: state.has('Tablet', player)),
+ LocationData('Library', 'Library: Terminal 3 (Emperor Nuvius)', 1337159, lambda state: state.has('Tablet', player)),
LocationData('Library', 'Library: V terminal 1 (War of the Sisters)', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
LocationData('Library', 'Library: V terminal 2 (Lake Desolation Map)', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
LocationData('Library', 'Library: V terminal 3 (Vilete)', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py
index 96b45b4833..d4231b6194 100644
--- a/worlds/tunic/er_rules.py
+++ b/worlds/tunic/er_rules.py
@@ -822,7 +822,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
btv_front_to_main = regions["Beneath the Vault Ladder Exit"].connect(
connecting_region=regions["Beneath the Vault Main"],
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)
- and has_lantern(state, world))
+ and has_lantern(state, world)
+ # there's some boxes in the way
+ and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player)))
+ # on the reverse trip, you can lure an enemy over to break the boxes if needed
regions["Beneath the Vault Main"].connect(
connecting_region=regions["Beneath the Vault Ladder Exit"],
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world))
diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py
index 6501f9a6a6..df467065d7 100644
--- a/worlds/tunic/rules.py
+++ b/worlds/tunic/rules.py
@@ -113,7 +113,9 @@ def set_region_rules(world: "TunicWorld") -> None:
or can_ladder_storage(state, world)
# using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules
world.get_entrance("Overworld -> Beneath the Vault").access_rule = \
- lambda state: has_lantern(state, world) and has_ability(prayer, state, world)
+ lambda state: (has_lantern(state, world) and has_ability(prayer, state, world)
+ # there's some boxes in the way
+ and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player)))
world.get_entrance("Ruined Atoll -> Library").access_rule = \
lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world)
world.get_entrance("Overworld -> Quarry").access_rule = \
diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py
index 2f3210a214..74ea2aef57 100644
--- a/worlds/witness/rules.py
+++ b/worlds/witness/rules.py
@@ -214,7 +214,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S
This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version.
"""
- direct_items = [rule for rule in requirement_option if isinstance(rule, tuple)]
+ direct_items = [rule for rule in requirement_option if isinstance(rule, SimpleItemRepresentation)]
if not direct_items:
return requirement_option
@@ -224,7 +224,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S
return [
rule for rule in requirement_option
- if not (isinstance(rule, tuple) and rule[1] < max_per_item[rule[0]])
+ if not (isinstance(rule, SimpleItemRepresentation) and rule[1] < max_per_item[rule[0]])
]
@@ -234,12 +234,6 @@ def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleIte
Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules.
If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts()
"""
- converted_sublist = []
-
- for rule in requirement:
- if not isinstance(rule, tuple):
- converted_sublist.append(rule)
- continue
collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)]
item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)]
diff --git a/worlds/yachtdice/Items.py b/worlds/yachtdice/Items.py
index fa52c93ad6..c76dc53814 100644
--- a/worlds/yachtdice/Items.py
+++ b/worlds/yachtdice/Items.py
@@ -16,8 +16,6 @@ class YachtDiceItem(Item):
item_table = {
- # victory item, always placed manually at goal location
- "Victory": ItemData(16871244000 - 1, ItemClassification.progression),
"Dice": ItemData(16871244000, ItemClassification.progression),
"Dice Fragment": ItemData(16871244001, ItemClassification.progression),
"Roll": ItemData(16871244002, ItemClassification.progression),
diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py
index d86ee3382d..75993fd394 100644
--- a/worlds/yachtdice/__init__.py
+++ b/worlds/yachtdice/__init__.py
@@ -1,7 +1,7 @@
import math
from typing import Dict
-from BaseClasses import CollectionState, Entrance, Item, Region, Tutorial
+from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region, Tutorial
from worlds.AutoWorld import WebWorld, World
@@ -56,7 +56,7 @@ class YachtDiceWorld(World):
item_name_groups = item_groups
- ap_world_version = "2.1.2"
+ ap_world_version = "2.1.3"
def _get_yachtdice_data(self):
return {
@@ -456,10 +456,12 @@ class YachtDiceWorld(World):
if loc_data.region == board.name
]
- # Add the victory item to the correct location.
- # The website declares that the game is complete when the victory item is obtained.
+ # Change the victory location to an event and place the Victory item there.
victory_location_name = f"{self.goal_score} score"
- self.get_location(victory_location_name).place_locked_item(self.create_item("Victory"))
+ self.get_location(victory_location_name).address = None
+ self.get_location(victory_location_name).place_locked_item(
+ Item("Victory", ItemClassification.progression, None, self.player)
+ )
# add the regions
connection = Entrance(self.player, "New Board", menu)
diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py
index cf61d93ca4..d5e86bb332 100644
--- a/worlds/zillion/__init__.py
+++ b/worlds/zillion/__init__.py
@@ -4,7 +4,7 @@ import functools
import settings
import threading
import typing
-from typing import Any, Dict, List, Set, Tuple, Optional
+from typing import Any, Dict, List, Set, Tuple, Optional, Union
import os
import logging
@@ -12,7 +12,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial
from .gen_data import GenData
-from .logic import cs_to_zz_locs
+from .logic import ZillionLogicCache
from .region import ZillionLocation, ZillionRegion
from .options import ZillionOptions, validate, z_option_groups
from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \
@@ -21,7 +21,6 @@ from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_na
from .item import ZillionItem
from .patch import ZillionPatch
-from zilliandomizer.randomizer import Randomizer as ZzRandomizer
from zilliandomizer.system import System
from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem
from zilliandomizer.logic_components.locations import Location as ZzLocation, Req
@@ -121,6 +120,7 @@ class ZillionWorld(World):
""" This is kind of a cache to avoid iterating through all the multiworld locations in logic. """
slot_data_ready: threading.Event
""" This event is set in `generate_output` when the data is ready for `fill_slot_data` """
+ logic_cache: Union[ZillionLogicCache, None] = None
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
@@ -134,9 +134,6 @@ class ZillionWorld(World):
self.id_to_zz_item = id_to_zz_item
def generate_early(self) -> None:
- if not hasattr(self.multiworld, "zillion_logic_cache"):
- setattr(self.multiworld, "zillion_logic_cache", {})
-
zz_op, item_counts = validate(self.options)
if zz_op.early_scope:
@@ -163,6 +160,8 @@ class ZillionWorld(World):
assert self.zz_system.randomizer, "generate_early hasn't been called"
assert self.id_to_zz_item, "generate_early hasn't been called"
p = self.player
+ logic_cache = ZillionLogicCache(p, self.zz_system.randomizer, self.id_to_zz_item)
+ self.logic_cache = logic_cache
w = self.multiworld
self.my_locations = []
@@ -201,15 +200,12 @@ class ZillionWorld(World):
if not zz_loc.item:
def access_rule_wrapped(zz_loc_local: ZzLocation,
- p: int,
- zz_r: ZzRandomizer,
- id_to_zz_item: Dict[int, ZzItem],
+ lc: ZillionLogicCache,
cs: CollectionState) -> bool:
- accessible = cs_to_zz_locs(cs, p, zz_r, id_to_zz_item)
+ accessible = lc.cs_to_zz_locs(cs)
return zz_loc_local in accessible
- access_rule = functools.partial(access_rule_wrapped,
- zz_loc, self.player, self.zz_system.randomizer, self.id_to_zz_item)
+ access_rule = functools.partial(access_rule_wrapped, zz_loc, logic_cache)
loc_name = self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name]
loc = ZillionLocation(zz_loc, self.player, loc_name, here)
@@ -402,13 +398,6 @@ class ZillionWorld(World):
game = self.zz_system.get_game()
return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty)
- # def modify_multidata(self, multidata: Dict[str, Any]) -> None:
- # """For deeper modification of server multidata."""
- # # not modifying multidata, just want to call this at the end of the generation process
- # cache = getattr(self.multiworld, "zillion_logic_cache")
- # import sys
- # print(sys.getsizeof(cache))
-
# end of ordered Main.py calls
def create_item(self, name: str) -> Item:
diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py
index dcbc6131f1..a14910a200 100644
--- a/worlds/zillion/logic.py
+++ b/worlds/zillion/logic.py
@@ -1,4 +1,4 @@
-from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter
+from typing import Dict, FrozenSet, Mapping, Tuple, List, Counter as _Counter
from BaseClasses import CollectionState
@@ -44,38 +44,51 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id)
-LogicCacheType = Dict[int, Tuple[Dict[int, _Counter[str]], FrozenSet[Location]]]
-""" { hash: (cs.prog_items, accessible_locations) } """
+_cache_miss: Tuple[None, FrozenSet[Location]] = (None, frozenset())
-def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]:
- """
- given an Archipelago `CollectionState`,
- returns frozenset of accessible zilliandomizer locations
- """
- # caching this function because it would be slow
- logic_cache: LogicCacheType = getattr(cs.multiworld, "zillion_logic_cache", {})
- _hash = set_randomizer_locs(cs, p, zz_r)
- counts = item_counts(cs, p)
- _hash += hash(counts)
+class ZillionLogicCache:
+ _cache: Dict[int, Tuple[_Counter[str], FrozenSet[Location]]]
+ """ `{ hash: (counter_from_prog_items, accessible_zz_locations) }` """
+ _player: int
+ _zz_r: Randomizer
+ _id_to_zz_item: Mapping[int, Item]
- if _hash in logic_cache and logic_cache[_hash][0] == cs.prog_items:
- # print("cache hit")
- return logic_cache[_hash][1]
+ def __init__(self, player: int, zz_r: Randomizer, id_to_zz_item: Mapping[int, Item]) -> None:
+ self._cache = {}
+ self._player = player
+ self._zz_r = zz_r
+ self._id_to_zz_item = id_to_zz_item
- # print("cache miss")
- have_items: List[Item] = []
- for name, count in counts:
- have_items.extend([id_to_zz_item[item_name_to_id[name]]] * count)
- # have_req is the result of converting AP CollectionState to zilliandomizer collection state
- have_req = zz_r.make_ability(have_items)
+ def cs_to_zz_locs(self, cs: CollectionState) -> FrozenSet[Location]:
+ """
+ given an Archipelago `CollectionState`,
+ returns frozenset of accessible zilliandomizer locations
+ """
+ # caching this function because it would be slow
+ _hash = set_randomizer_locs(cs, self._player, self._zz_r)
+ counts = item_counts(cs, self._player)
+ _hash += hash(counts)
- # This `get_locations` is where the core of the logic comes in.
- # It takes a zilliandomizer collection state (a set of the abilities that I have)
- # and returns list of all the zilliandomizer locations I can access with those abilities.
- tr = frozenset(zz_r.get_locations(have_req))
+ cntr, locs = self._cache.get(_hash, _cache_miss)
+ if cntr == cs.prog_items[self._player]:
+ # print("cache hit")
+ return locs
- # save result in cache
- logic_cache[_hash] = (cs.prog_items.copy(), tr)
+ # print("cache miss")
+ have_items: List[Item] = []
+ for name, count in counts:
+ have_items.extend([self._id_to_zz_item[item_name_to_id[name]]] * count)
+ # have_req is the result of converting AP CollectionState to zilliandomizer collection state
+ have_req = self._zz_r.make_ability(have_items)
+ # print(f"{have_req=}")
- return tr
+ # This `get_locations` is where the core of the logic comes in.
+ # It takes a zilliandomizer collection state (a set of the abilities that I have)
+ # and returns list of all the zilliandomizer locations I can access with those abilities.
+ tr = frozenset(self._zz_r.get_locations(have_req))
+
+ # save result in cache
+ self._cache[_hash] = (cs.prog_items[self._player].copy(), tr)
+
+ return tr