Compare commits

...

27 Commits

Author SHA1 Message Date
NewSoupVi
83d8bd584b Revert "DS3: Make your own region cache (#4040)"
This reverts commit 2751ccdaab.
2024-10-11 03:03:32 +02:00
Exempt-Medic
2751ccdaab DS3: Make your own region cache (#4040)
* Make your own region cache

* Using a string
2024-10-11 03:02:31 +02:00
black-sliver
6287bc27a6 WebHost: Fix too-many-players error not showing (#4033)
* WebHost: fix 'too many players' error not showing

* WebHost, Tests: add basic tests for generate endpoint

* WebHost: hopefully make CodeQL happy with MAX_ROLL redirect
2024-10-05 18:14:22 +02:00
palex00
97f2c25924 [KH2] Adds more options to slot data #4031 2024-10-05 02:13:04 +02:00
Bryce Wilson
e5a0ef799f Pokemon Emerald: Update changelog (#4003) 2024-10-04 21:28:43 +02:00
Silvris
216e0603e1 KDL3: Fix webhost not giving a patch #4023 2024-10-04 21:27:23 +02:00
Fabian Dill
05a67386c6 Core: use shlex splitting instead of whitespace splitting for client and server commands (#4011) 2024-10-02 03:09:43 +02:00
NewSoupVi
0ec9039ca6 The Witness: Small code refactor (cast_not_none) (#3798)
* cast not none

* ruff

* Missed a spot
2024-10-02 00:02:17 +02:00
Aaron Wagener
f06f95d03d Core: move race_mode to read_data instead of stored_data (#4020)
* move race_mode to read_data

* add race_mode to docs
2024-10-01 23:55:34 +02:00
Mysteryem
5a853dfccd Tests: Fix indentation in TestTwoPlayerMulti (#4010)
The "filling multiworld" subtest was at the wrong indentation, so was
only running for the last world_type.

"games" has additionally been added to the subtest to help better
identify failures.

Now that the subtest is actually being run for each world type, this
adds about 20 seconds to the duration of the test on my machine.
2024-10-01 21:30:45 +02:00
Alex Nordstrom
23469fa5c3 LADX: ghost fills ammo to initial max (#4005)
* ghost fills ammo to max

* Revert "ghost fills ammo to max"

This reverts commit 68804fef14.

* fill to first max
2024-10-01 21:09:23 +02:00
Bryce Wilson
dc1da4e88b Pokemon Emerald: Another wonder trade fix (#4014)
* Pokemon Emerald: Another guarded write on wonder trades

* Pokemon Emerald: Reorder sending wonder trade and erasing data

In case the guarded write fails
2024-10-01 21:08:43 +02:00
Aaron Wagener
67f6b458d7 Core: add race mode to multidata and datastore (#4017)
* add race mode to multidata and datastore

* have commonclient check race mode on connect and add it to the tooltip ui
2024-10-01 21:08:13 +02:00
Bryce Wilson
8193fa12b2 BizHawkClient: Fix typing mistake (#3938) 2024-09-28 22:49:11 +02:00
Fabian Dill
de0c498470 Core: update World method comment (#3866) 2024-09-28 22:37:42 +02:00
qwint
7337309426 CommonClient: add more docstrings and comments #3821 2024-09-27 01:34:54 +02:00
Natalie Weizenbaum
3205e9b3a0 DS3: Update setup instructions (#3817)
* DS3: Point the DS3 client link to my GitHub

It's not clear if/when my PR will land for the upstream fork, or if we'll just start using my fork as the primary source of truth. For now, it's the only one with 3.0.0-compatible releases.

* DS3: Document Proton support

* DS3: Document another way to get a YAML template

* DS3: Don't say that the mod will force offline mode

ModEngine2 is *supposed to* do this, but in practice it does not

* Code review

* Update Linux instructions per user experiences
2024-09-27 01:31:50 +02:00
palex00
05439012dc Adjusts Whitespaces in the Plando Doc to be able to be copied directly (#3902)
* Update plando_en.md

* Also adjusts plando_connections indentation

* ughh
2024-09-27 01:30:23 +02:00
soopercool101
177c0fef52 SM64: Remove outdated information on save bugs from setup guide (#3879)
* Remove outdated information from SM64 setup guide

Recent build changes have made it so that old saves no longer remove logical gates or prevent Toads from granting stars, remove info highlighting these issues.

* Better line break location
2024-09-27 01:29:26 +02:00
BadMagic100
5c4e81d046 Hollow Knight: Clean outdated slot data code and comments #3988 2024-09-27 01:27:22 +02:00
agilbert1412
a2d585ba5c Stardew Valley: Add Cinder Shard resource pack (#4001)
* - Add Cinder Shard resource pack

* - Make it ginger island exclusive
2024-09-27 01:26:06 +02:00
Aaron Wagener
5ea55d77b0 The Messenger: add webhost auto connection steps to guide (#3904)
* The Messenger: add webhost auto connection steps to guide and fix doc spacing

* rever comments

* add notes about potential steam popup

* medic's feedback

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-27 01:25:41 +02:00
Ziktofel
ab8caea8be SC2: Fix item origins, so including/excluding NCO/BW/EXT items works properly (#3990) 2024-09-27 00:57:21 +02:00
Benny D
a043ed50a6 Timespinner: Fix Typo in Download Location #3997 2024-09-27 00:56:36 +02:00
qwint
e85a835b47 Core: use base collect/remove for item link groups (#3999)
* use base collect/remove for item link groups

* Update BaseClasses.py

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-27 00:02:10 +02:00
Felix R
9a9fea0ca2 bumpstik: add hazard bumpers to completion (#3991)
* bumpstik: add hazard bumpers to completion

* bumpstik: update to use has_all_counts for completion
as suggested by ScipioWright
2024-09-26 20:47:03 +02:00
NewSoupVi
e910a37273 Core: Put an assert for parent region in Entrance.can_reach just like the one in Location.can_reach (#3998)
* Core: Move connection.parent_region assert to can_reach

This is how it already works for locations and it feels more correct to me to check in the place where the crash would happen.

Also update location error to be a bit more verbose

* Bring back the other assert

* Update BaseClasses.py
2024-09-25 17:47:38 +02:00
30 changed files with 330 additions and 188 deletions

View File

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

View File

@@ -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,
@@ -439,6 +454,7 @@ class CommonContext:
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
async def console_input(self) -> str:
if self.ui:
@@ -459,6 +475,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 +483,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 +515,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 +632,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 +646,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 +656,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 +1008,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 +1059,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 +1071,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))

View File

@@ -338,6 +338,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
"seed_name": multiworld.seed_name,
"spheres": spheres,
"datapackage": data_package,
"race_mode": int(multiworld.is_race),
}
AutoWorld.call_all(multiworld, "modify_multidata", multidata)

View File

@@ -15,6 +15,7 @@ import math
import operator
import pickle
import random
import shlex
import threading
import time
import typing
@@ -427,6 +428,8 @@ class Context:
use_embedded_server_options: bool):
self.read_data = {}
# there might be a better place to put this.
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > version_tuple:
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
@@ -1150,7 +1153,7 @@ class CommandProcessor(metaclass=CommandMeta):
if not raw:
return
try:
command = raw.split()
command = shlex.split(raw, comments=False)
basecommand = command[0]
if basecommand[0] == self.marker:
method = self.commands.get(basecommand[1:].lower(), None)

View File

@@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any])
elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
f"If you have a larger group, please generate it yourself and upload it.")
return redirect(url_for(request.endpoint, **(request.view_args or {})))
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),

View File

@@ -395,6 +395,7 @@ Some special keys exist with specific return data, all of them have the prefix `
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. |
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
| race_mode | int | 0 if race mode is disabled, and 1 if it's enabled. |
### Set
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.

View File

@@ -243,6 +243,9 @@ class ServerLabel(HovererableLabel):
f"\nYou currently have {ctx.hint_points} points."
elif ctx.hint_cost == 0:
text += "\n!hint is free to use."
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
text += "\nRace mode is enabled." \
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
else:
text += f"\nYou are not authenticated yet."

View File

@@ -71,7 +71,7 @@ class TestTwoPlayerMulti(MultiworldTestBase):
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")

View File

@@ -0,0 +1,73 @@
import zipfile
from io import BytesIO
from flask import url_for
from . import TestBase
class TestGenerate(TestBase):
def test_valid_yaml(self) -> None:
"""
Verify that posting a valid yaml will start generating a game.
"""
with self.app.app_context(), self.app.test_request_context():
yaml_data = """
name: Player1
game: Archipelago
Archipelago: {}
"""
response = self.client.post(url_for("generate"),
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertTrue("/seed/" in response.request.path or
"/wait/" in response.request.path,
f"Response did not properly redirect ({response.request.path})")
def test_empty_zip(self) -> None:
"""
Verify that posting an empty zip will give an error.
"""
with self.app.app_context(), self.app.test_request_context():
zip_data = BytesIO()
zipfile.ZipFile(zip_data, "w").close()
zip_data.seek(0)
self.assertGreater(len(zip_data.read()), 0)
zip_data.seek(0)
response = self.client.post(url_for("generate"),
data={"file": (zip_data, "test.zip")},
follow_redirects=True)
self.assertIn("user-message", response.text,
"Request did not call flash()")
self.assertIn("not find any valid files", response.text,
"Response shows unexpected error")
self.assertIn("generate-game-form", response.text,
"Response did not get user back to the form")
def test_too_many_players(self) -> None:
"""
Verify that posting too many players will give an error.
"""
max_roll = self.app.config["MAX_ROLL"]
# validate that max roll has a sensible value, otherwise we probably changed how it works
self.assertIsInstance(max_roll, int)
self.assertGreater(max_roll, 1)
self.assertLess(max_roll, 100)
# create a yaml with max_roll+1 players and watch it fail
with self.app.app_context(), self.app.test_request_context():
yaml_data = "---\n".join([
f"name: Player{n}\n"
"game: Archipelago\n"
"Archipelago: {}\n"
for n in range(1, max_roll + 2)
])
response = self.client.post(url_for("generate"),
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
follow_redirects=True)
self.assertIn("user-message", response.text,
"Request did not call flash()")
self.assertIn("limited to", response.text,
"Response shows unexpected error")
self.assertIn("generate-game-form", response.text,
"Response did not get user back to the form")

View File

@@ -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_<original_name>",
# 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -534,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):
@@ -566,7 +556,7 @@ class HKWorld(World):
slot_data["grub_count"] = self.grub_count
slot_data["is_race"] = int(self.settings.disable_spoilers or self.multiworld.is_race)
slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race
return slot_data

View File

@@ -325,7 +325,7 @@ class KDL3World(World):
def generate_output(self, output_directory: str) -> None:
try:
patch = KDL3ProcedurePatch()
patch = KDL3ProcedurePatch(player=self.player, player_name=self.player_name)
patch_rom(self, patch)
self.rom_name = patch.name

View File

@@ -101,7 +101,18 @@ class KH2World(World):
if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1:
self.goofy_ability_dict[ability] -= 1
slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired")
slot_data = self.options.as_dict(
"Goal",
"FinalXemnas",
"LuckyEmblemsRequired",
"BountyRequired",
"FightLogic",
"FinalFormLogic",
"AutoFormLogic",
"LevelDepth",
"DonaldGoofyStatsanity",
"CorSkipToggle"
)
slot_data.update({
"hitlist": [], # remove this after next update
"PoptrackerVersionCheck": 4.3,

View File

@@ -81,23 +81,23 @@ talking:
; Give powder
ld a, [$DB4C]
cp $10
cp $20
jr nc, doNotGivePowder
ld a, $10
ld a, $20
ld [$DB4C], a
doNotGivePowder:
ld a, [$DB4D]
cp $10
cp $30
jr nc, doNotGiveBombs
ld a, $10
ld a, $30
ld [$DB4D], a
doNotGiveBombs:
ld a, [$DB45]
cp $10
cp $30
jr nc, doNotGiveArrows
ld a, $10
ld a, $30
ld [$DB45], a
doNotGiveArrows:

View File

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

View File

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

View File

@@ -8,6 +8,9 @@
### Fixes
- Fixed a rare issue where receiving a wonder trade could partially corrupt the save data, preventing the player from
receiving new items.
- Fixed the client spamming the "goal complete" status update to the server instead of sending it once.
- Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if
the player randomized NPC gifts.
- The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower.

View File

@@ -545,11 +545,12 @@ class PokemonEmeraldClient(BizHawkClient):
if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2:
# Game has wonder trade data to send. Send it to data storage, remove it from the game's memory,
# and mark that the game is waiting on receiving a trade
Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
await bizhawk.write(ctx.bizhawk_ctx, [
success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [
(sb1_address + 0x377C, bytes(0x50), "System Bus"),
(sb1_address + 0x37CC, [1], "System Bus"),
])
], [guards["SAVE BLOCK 1"]])
if success:
Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2:
# Game is waiting on receiving a trade.
if self.queued_received_trade is not None:

View File

@@ -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]] = [

View File

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

View File

@@ -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
1 id name classification groups mod_name
819 5289 Prismatic Shard filler RESOURCE_PACK
820 5290 Stardrop Tea filler RESOURCE_PACK
821 5291 Resource Pack: 2 Artifact Trove filler RESOURCE_PACK
822 5292 Resource Pack: 20 Cinder Shard filler GINGER_ISLAND,RESOURCE_PACK
823 10001 Luck Level progression SKILL_LEVEL_UP Luck Skill
824 10002 Magic Level progression SKILL_LEVEL_UP Magic
825 10003 Socializing Level progression SKILL_LEVEL_UP Socializing Skill

View File

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

View File

@@ -14,7 +14,7 @@ from .data import static_items as static_witness_items
from .data import static_locations as static_witness_locations
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemData
from .data.utils import get_audio_logs
from .data.utils import cast_not_none, get_audio_logs
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations
from .options import TheWitnessOptions, witness_option_groups
@@ -55,7 +55,7 @@ class WitnessWorld(World):
item_name_to_id = {
# ITEM_DATA doesn't have any event items in it
name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
name: cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
}
location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
item_name_groups = static_witness_items.ITEM_GROUPS
@@ -336,7 +336,7 @@ class WitnessWorld(World):
for item_name, hint in laser_hints.items():
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
already_hinted_locations.add(cast(Location, hint.location))
already_hinted_locations.add(cast_not_none(hint.location))
# Audio Log Hints

View File

@@ -1,7 +1,7 @@
from math import floor
from pkgutil import get_data
from random import Random
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar
T = TypeVar("T")
@@ -13,6 +13,11 @@ T = TypeVar("T")
WitnessRule = FrozenSet[FrozenSet[str]]
def cast_not_none(value: Optional[T]) -> T:
assert value is not None
return value
def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]:
positions = range(len(population))
indices: List[int] = []

View File

@@ -15,7 +15,7 @@ from .data.item_definition_classes import (
ProgressiveItemDefinition,
WeightedItemDefinition,
)
from .data.utils import build_weighted_int_list
from .data.utils import build_weighted_int_list, cast_not_none
from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic
@@ -200,7 +200,7 @@ class WitnessPlayerItems:
"""
return [
# data.ap_code is guaranteed for a symbol definition
cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
]
@@ -211,8 +211,8 @@ class WitnessPlayerItems:
if isinstance(item.definition, ProgressiveItemDefinition):
# Note: we need to reference the static table here rather than the player-specific one because the child
# items were removed from the pool when we pruned out all progression items not in the options.
output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code)
for child_item in item.definition.child_item_names]
output[cast_not_none(item.ap_code)] = [cast_not_none(static_witness_items.ITEM_DATA[child_item].ap_code)
for child_item in item.definition.child_item_names]
return output

View File

@@ -1,4 +1,4 @@
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union
from BaseClasses import CollectionState, Entrance, Item, Location, Region
@@ -7,6 +7,7 @@ from test.general import gen_steps, setup_multiworld
from test.multiworld.test_multiworlds import MultiworldTestBase
from .. import WitnessWorld
from ..data.utils import cast_not_none
class WitnessTestBase(WorldTestBase):
@@ -32,7 +33,7 @@ class WitnessTestBase(WorldTestBase):
event_items = [item for item in self.multiworld.get_items() if item.name == item_name]
self.assertTrue(event_items, f"Event item {item_name} does not exist.")
event_locations = [cast(Location, event_item.location) for event_item in event_items]
event_locations = [cast_not_none(event_item.location) for event_item in event_items]
# Checking for an access dependency on an event item requires a bit of extra work,
# as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it.