Compare commits

..

29 Commits

Author SHA1 Message Date
NewSoupVi
80e89db812 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-10-28 22:34:23 +01:00
NewSoupVi
8d0f1be791 Update world api.md 2024-10-28 22:25:07 +01:00
NewSoupVi
febcd84995 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-10-28 22:24:00 +01:00
NewSoupVi
d0a555a185 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-10-28 22:23:36 +01:00
NewSoupVi
da72d40f14 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-10-28 22:23:04 +01:00
NewSoupVi
f78f906ced line break issues 2024-10-28 21:20:39 +01:00
NewSoupVi
245a66828d Update world api.md 2024-10-28 21:18:17 +01:00
NewSoupVi
798556fd3f Update world api.md 2024-10-28 21:16:47 +01:00
NewSoupVi
14609100cf Write a big motivation paragraph 2024-10-28 21:14:33 +01:00
NewSoupVi
fe366e2985 Update world api.md 2024-10-28 21:03:40 +01:00
NewSoupVi
819710ff61 Reorganize / Rewrite the parts about optimisations a bit 2024-10-28 21:00:27 +01:00
NewSoupVi
c0cd03f436 Update world api.md 2024-09-27 01:21:11 +02:00
NewSoupVi
e694804509 Update world api.md 2024-09-27 01:20:47 +02:00
NewSoupVi
c21c10a602 Update docs/world api.md
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2024-09-25 13:13:06 +02:00
NewSoupVi
54ab181514 Code corrections / actually follow own spec 2024-09-22 15:27:49 +02:00
NewSoupVi
88de34dfd7 Update world api.md 2024-09-22 15:26:00 +02:00
NewSoupVi
555b7211ef Update docs/world api.md 2024-09-21 19:13:41 +02:00
NewSoupVi
7b097c918e Update docs/world api.md
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-21 19:13:10 +02:00
NewSoupVi
4eeac945fb Update world api.md 2024-09-21 19:03:24 +02:00
NewSoupVi
db577ad899 Update world api.md 2024-09-21 19:03:01 +02:00
NewSoupVi
8aed3efe97 Update world api.md 2024-09-21 19:02:30 +02:00
NewSoupVi
f7d6f689fb Update world api.md 2024-09-21 19:00:57 +02:00
NewSoupVi
910d4b93c9 Update world api.md 2024-09-21 19:00:03 +02:00
NewSoupVi
584dd8fe75 Update world api.md 2024-09-21 18:59:36 +02:00
NewSoupVi
259d010a86 Update world api.md 2024-09-21 18:56:46 +02:00
NewSoupVi
98d808bb87 Update world api.md 2024-09-21 18:55:57 +02:00
NewSoupVi
f9108f4331 Update world api.md 2024-09-21 18:52:02 +02:00
NewSoupVi
4b6ad12192 Update world api.md 2024-09-21 18:51:30 +02:00
NewSoupVi
a08fbc66a8 Docs: Make an actual LogicMixin spec & explanation 2024-09-21 18:46:56 +02:00
128 changed files with 1840 additions and 2029 deletions

View File

@@ -194,9 +194,7 @@ class MultiWorld():
self.player_types[new_id] = NetUtils.SlotType.group self.player_types[new_id] = NetUtils.SlotType.group
world_type = AutoWorld.AutoWorldRegister.world_types[game] world_type = AutoWorld.AutoWorldRegister.world_types[game]
self.worlds[new_id] = world_type.create_group(self, new_id, players) self.worlds[new_id] = world_type.create_group(self, new_id, players)
self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id]) self.worlds[new_id].collect_item = classmethod(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 self.player_name[new_id] = name
new_group = self.groups[new_id] = Group(name=name, game=game, players=players, new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
@@ -722,7 +720,7 @@ class CollectionState():
if new_region in reachable_regions: if new_region in reachable_regions:
blocked_connections.remove(connection) blocked_connections.remove(connection)
elif connection.can_reach(self): elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region" assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
reachable_regions.add(new_region) reachable_regions.add(new_region)
blocked_connections.remove(connection) blocked_connections.remove(connection)
blocked_connections.update(new_region.exits) blocked_connections.update(new_region.exits)
@@ -948,7 +946,6 @@ class Entrance:
self.player = player self.player = player
def can_reach(self, state: CollectionState) -> bool: 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 self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path: 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))) state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
@@ -1169,7 +1166,7 @@ class Location:
def can_reach(self, state: CollectionState) -> bool: def can_reach(self, state: CollectionState) -> bool:
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average # Region.can_reach is just a cache lookup, so placing it first for faster abort on average
assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region" assert self.parent_region, "Can't reach location without region"
return self.parent_region.can_reach(state) and self.access_rule(state) return self.parent_region.can_reach(state) and self.access_rule(state)
def place_locked_item(self, item: Item): def place_locked_item(self, item: Item):

View File

@@ -45,21 +45,10 @@ def get_ssl_context():
class ClientCommandProcessor(CommandProcessor): 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): def __init__(self, ctx: CommonContext):
self.ctx = ctx self.ctx = ctx
def output(self, text: str): def output(self, text: str):
"""Helper function to abstract logging to the CommonClient UI"""
logger.info(text) logger.info(text)
def _cmd_exit(self) -> bool: def _cmd_exit(self) -> bool:
@@ -175,14 +164,13 @@ class ClientCommandProcessor(CommandProcessor):
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def default(self, raw: str): 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) raw = self.ctx.on_user_say(raw)
if raw: if raw:
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say") async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
class CommonContext: class CommonContext:
# The following attributes are used to Connect and should be adjusted as needed in subclasses # Should be adjusted as needed in subclasses
tags: typing.Set[str] = {"AP"} tags: typing.Set[str] = {"AP"}
game: typing.Optional[str] = None game: typing.Optional[str] = None
items_handling: typing.Optional[int] = None items_handling: typing.Optional[int] = None
@@ -355,8 +343,6 @@ class CommonContext:
self.item_names = self.NameLookupDict(self, "item") self.item_names = self.NameLookupDict(self, "item")
self.location_names = self.NameLookupDict(self, "location") self.location_names = self.NameLookupDict(self, "location")
self.versions = {}
self.checksums = {}
self.jsontotextparser = JSONtoTextParser(self) self.jsontotextparser = JSONtoTextParser(self)
self.rawjsontotextparser = RawJSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self)
@@ -443,10 +429,7 @@ class CommonContext:
self.auth = await self.console_input() self.auth = await self.console_input()
async def send_connect(self, **kwargs: typing.Any) -> None: 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 = { payload = {
'cmd': 'Connect', 'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
@@ -456,7 +439,6 @@ class CommonContext:
if kwargs: if kwargs:
payload.update(kwargs) payload.update(kwargs)
await self.send_msgs([payload]) await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
async def console_input(self) -> str: async def console_input(self) -> str:
if self.ui: if self.ui:
@@ -477,7 +459,6 @@ class CommonContext:
return False return False
def slot_concerns_self(self, slot) -> bool: 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: if slot == self.slot:
return True return True
if slot in self.slot_info: if slot in self.slot_info:
@@ -485,7 +466,6 @@ class CommonContext:
return False return False
def is_echoed_chat(self, print_json_packet: dict) -> bool: 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" \ return print_json_packet.get("type", "") == "Chat" \
and print_json_packet.get("team", None) == self.team \ and print_json_packet.get("team", None) == self.team \
and print_json_packet.get("slot", None) == self.slot and print_json_packet.get("slot", None) == self.slot
@@ -517,14 +497,13 @@ class CommonContext:
"""Gets called before sending a Say to the server from the user. """Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned.""" Returned text is sent, or sending is aborted if None is returned."""
return text return text
def on_ui_command(self, text: str) -> None: def on_ui_command(self, text: str) -> None:
"""Gets called by kivy when the user executes a command starting with `/` or `!`. """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.""" The command processor is still called; this is just intended for command echoing."""
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}]) self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
def update_permissions(self, permissions: typing.Dict[str, int]): 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(): for permission_name, permission_flag in permissions.items():
try: try:
flag = Permission(permission_flag) flag = Permission(permission_flag)
@@ -573,34 +552,26 @@ class CommonContext:
needed_updates.add(game) needed_updates.add(game)
continue continue
cached_version: int = self.versions.get(game, 0) local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
cached_checksum: typing.Optional[str] = self.checksums.get(game) local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
# no action required if cached version is new enough # no action required if local version is new enough
if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \ if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
or remote_checksum != cached_checksum: or remote_checksum != local_checksum:
local_version: int = network_data_package["games"].get(game, {}).get("version", 0) cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") cache_version: int = cached_game.get("version", 0)
if ((remote_checksum or remote_version <= local_version and remote_version != 0) cache_checksum: typing.Optional[str] = cached_game.get("checksum")
and remote_checksum == local_checksum): # download remote version if cache is not new enough
self.update_game(network_data_package["games"][game], game) if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
or remote_checksum != cache_checksum:
needed_updates.add(game)
else: else:
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) self.update_game(cached_game, game)
cache_version: int = cached_game.get("version", 0)
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
# download remote version if cache is not new enough
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
or remote_checksum != cache_checksum:
needed_updates.add(game)
else:
self.update_game(cached_game, game)
if needed_updates: if needed_updates:
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
def update_game(self, game_package: dict, game: str): def update_game(self, game_package: dict, game: str):
self.item_names.update_game(game, game_package["item_name_to_id"]) self.item_names.update_game(game, game_package["item_name_to_id"])
self.location_names.update_game(game, game_package["location_name_to_id"]) self.location_names.update_game(game, game_package["location_name_to_id"])
self.versions[game] = game_package.get("version", 0)
self.checksums[game] = game_package.get("checksum")
def update_data_package(self, data_package: dict): def update_data_package(self, data_package: dict):
for game, game_data in data_package["games"].items(): for game, game_data in data_package["games"].items():
@@ -642,7 +613,6 @@ class CommonContext:
logger.info(f"DeathLink: Received from {data['source']}") logger.info(f"DeathLink: Received from {data['source']}")
async def send_death(self, death_text: str = ""): 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: if self.server and self.server.socket:
logger.info("DeathLink: Sending death to your friends...") logger.info("DeathLink: Sending death to your friends...")
self.last_death_link = time.time() self.last_death_link = time.time()
@@ -656,7 +626,6 @@ class CommonContext:
}]) }])
async def update_death_link(self, death_link: bool): 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() old_tags = self.tags.copy()
if death_link: if death_link:
self.tags.add("DeathLink") self.tags.add("DeathLink")
@@ -666,7 +635,7 @@ class CommonContext:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]) await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]: def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
"""Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework""" """Displays an error messagebox"""
if not self.ui: if not self.ui:
return None return None
title = title or "Error" title = title or "Error"
@@ -1018,7 +987,6 @@ async def console_loop(ctx: CommonContext):
def get_base_parser(description: typing.Optional[str] = None): def get_base_parser(description: typing.Optional[str] = None):
"""Base argument parser to be reused for components subclassing off of CommonClient"""
import argparse import argparse
parser = argparse.ArgumentParser(description=description) parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.') parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
@@ -1069,7 +1037,6 @@ def run_as_textclient(*args):
parser.add_argument("url", nargs="?", help="Archipelago connection url") parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args) args = parser.parse_args(args)
# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
if args.url: if args.url:
url = urllib.parse.urlparse(args.url) url = urllib.parse.urlparse(args.url)
if url.scheme == "archipelago": if url.scheme == "archipelago":
@@ -1081,7 +1048,6 @@ def run_as_textclient(*args):
else: else:
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") 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() colorama.init()
asyncio.run(main(args)) asyncio.run(main(args))

View File

@@ -35,9 +35,7 @@ from Utils import is_frozen, user_path, local_path, init_logging, open_filename,
def open_host_yaml(): def open_host_yaml():
s = settings.get_settings() file = settings.get_settings().filename
file = s.filename
s.save()
assert file, "host.yaml missing" assert file, "host.yaml missing"
if is_linux: if is_linux:
exe = which('sensible-editor') or which('gedit') or \ exe = which('sensible-editor') or which('gedit') or \

View File

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

View File

@@ -15,7 +15,6 @@ import math
import operator import operator
import pickle import pickle
import random import random
import shlex
import threading import threading
import time import time
import typing import typing
@@ -185,9 +184,11 @@ class Context:
slot_info: typing.Dict[int, NetworkSlot] slot_info: typing.Dict[int, NetworkSlot]
generator_version = Version(0, 0, 0) generator_version = Version(0, 0, 0)
checksums: typing.Dict[str, str] checksums: typing.Dict[str, str]
item_names: typing.Dict[str, typing.Dict[int, str]] item_names: typing.Dict[str, typing.Dict[int, str]] = (
collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')))
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
location_names: typing.Dict[str, typing.Dict[int, str]] location_names: typing.Dict[str, typing.Dict[int, str]] = (
collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')))
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_item_and_group_names: typing.Dict[str, typing.Set[str]]
all_location_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]]
@@ -196,6 +197,7 @@ class Context:
""" each sphere is { player: { location_id, ... } } """ """ each sphere is { player: { location_id, ... } } """
logger: logging.Logger logger: logging.Logger
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
@@ -266,10 +268,6 @@ class Context:
self.location_name_groups = {} self.location_name_groups = {}
self.all_item_and_group_names = {} self.all_item_and_group_names = {}
self.all_location_and_group_names = {} self.all_location_and_group_names = {}
self.item_names = collections.defaultdict(
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))
self.location_names = collections.defaultdict(
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))
self.non_hintable_names = collections.defaultdict(frozenset) self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data() self._load_game_data()
@@ -429,8 +427,6 @@ class Context:
use_embedded_server_options: bool): use_embedded_server_options: bool):
self.read_data = {} 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"] mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > version_tuple: if mdata_ver > version_tuple:
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
@@ -1154,10 +1150,7 @@ class CommandProcessor(metaclass=CommandMeta):
if not raw: if not raw:
return return
try: try:
try: command = raw.split()
command = shlex.split(raw, comments=False)
except ValueError: # most likely: "ValueError: No closing quotation"
command = raw.split()
basecommand = command[0] basecommand = command[0]
if basecommand[0] == self.marker: if basecommand[0] == self.marker:
method = self.commands.get(basecommand[1:].lower(), None) method = self.commands.get(basecommand[1:].lower(), None)

View File

@@ -423,7 +423,7 @@ class RestrictedUnpickler(pickle.Unpickler):
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}: if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
return getattr(self.net_utils_module, name) return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate # Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name == "PlandoItem": if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
if not self.generic_properties_module: if not self.generic_properties_module:
self.generic_properties_module = importlib.import_module("worlds.generic") self.generic_properties_module = importlib.import_module("worlds.generic")
return getattr(self.generic_properties_module, name) return getattr(self.generic_properties_module, name)
@@ -434,7 +434,7 @@ class RestrictedUnpickler(pickle.Unpickler):
else: else:
mod = importlib.import_module(module) mod = importlib.import_module(module)
obj = getattr(mod, name) obj = getattr(mod, name)
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)): if issubclass(obj, self.options_module.Option):
return obj return obj
# Forbid everything else. # Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")

View File

@@ -267,7 +267,9 @@ class WargrooveContext(CommonContext):
def build(self): def build(self):
container = super().build() container = super().build()
self.add_client_tab("Wargroove", self.build_tracker()) panel = TabbedPanelItem(text="Wargroove")
panel.content = self.build_tracker()
self.tabs.add_widget(panel)
return container return container
def build_tracker(self) -> TrackerLayout: def build_tracker(self) -> TrackerLayout:

View File

@@ -81,7 +81,6 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any])
elif len(gen_options) > app.config["MAX_ROLL"]: elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " 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.") 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"]: elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation( gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),

View File

@@ -5,7 +5,6 @@ from typing import Any, IO, Dict, Iterator, List, Tuple, Union
import jinja2.exceptions import jinja2.exceptions
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from pony.orm import count, commit, db_session from pony.orm import count, commit, db_session
from werkzeug.utils import secure_filename
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from . import app, cache from . import app, cache
@@ -70,28 +69,14 @@ def tutorial_landing():
@app.route('/faq/<string:lang>/') @app.route('/faq/<string:lang>/')
@cache.cached() @cache.cached()
def faq(lang: str): def faq(lang):
import markdown return render_template("faq.html", lang=lang)
with open(os.path.join(app.static_folder, "assets", "faq", secure_filename(lang)+".md")) as f:
document = f.read()
return render_template(
"markdown_document.html",
title="Frequently Asked Questions",
html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]),
)
@app.route('/glossary/<string:lang>/') @app.route('/glossary/<string:lang>/')
@cache.cached() @cache.cached()
def glossary(lang: str): def terms(lang):
import markdown return render_template("glossary.html", lang=lang)
with open(os.path.join(app.static_folder, "assets", "glossary", secure_filename(lang)+".md")) as f:
document = f.read()
return render_template(
"markdown_document.html",
title="Glossary",
html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]),
)
@app.route('/seed/<suuid:seed>') @app.route('/seed/<suuid:seed>')

View File

@@ -9,5 +9,3 @@ bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.3; python_version == '3.9' bokeh>=3.4.3; python_version == '3.9'
bokeh>=3.5.2; python_version >= '3.10' bokeh>=3.5.2; python_version >= '3.10'
markupsafe>=2.1.5 markupsafe>=2.1.5
Markdown>=3.7
mdx-breakless-lists>=1.0.1

View File

@@ -0,0 +1,51 @@
window.addEventListener('load', () => {
const tutorialWrapper = document.getElementById('faq-wrapper');
new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status === 404) {
reject("Sorry, the tutorial is not available in that language yet.");
return;
}
if (ajax.status !== 200) {
reject("Something went wrong while loading the tutorial.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
`faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
ajax.send();
}).then((results) => {
// Populate page with HTML generated from markdown
showdown.setOption('tables', true);
showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true);
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
});
});

View File

@@ -0,0 +1,51 @@
window.addEventListener('load', () => {
const tutorialWrapper = document.getElementById('glossary-wrapper');
new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status === 404) {
reject("Sorry, the glossary page is not available in that language yet.");
return;
}
if (ajax.status !== 200) {
reject("Something went wrong while loading the glossary.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
`glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
ajax.send();
}).then((results) => {
// Populate page with HTML generated from markdown
showdown.setOption('tables', true);
showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true);
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
});
});

View File

@@ -288,11 +288,6 @@ const applyPresets = (presetName) => {
} }
}); });
namedRangeSelect.value = trueValue; namedRangeSelect.value = trueValue;
// It is also possible for a preset to use an unnamed value. If this happens, set the dropdown to "Custom"
if (namedRangeSelect.selectedIndex == -1)
{
namedRangeSelect.value = "custom";
}
} }
// Handle options whose presets are "random" // Handle options whose presets are "random"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 229 KiB

View File

@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 240 38" style="enable-background:new 0 0 240 38;" xml:space="preserve">
<style type="text/css">
.st0{fill:#316B84;}
</style>
<g>
<g>
<path class="st0" d="M59.72,27.96L53.03,4.21L42.25,4.04l1.42,4.37l1.41-0.26l-7.9,24.22h8.44l-0.56-2.27l-0.81-3.27l8.9-5.7
l1.78,11.24h7.97v-4.73L59.72,27.96z M45.62,20.21l3.13-10.84h1.5l2.02,7.44L45.62,20.21z"/>
<path class="st0" d="M78.67,27.96V20.4l-4.11-2.5l3.29-3.78l-0.47-7.46l-2.82-2.45H56.65v5.27l3.81-1.11l2.31,13.36l-2.79,0.73
L61,26.34l5.06-0.52l0.36-6.15l4.32,0.13l3.16,3.62v8.94l12.89,1.49v-5.34L78.67,27.96z M73.27,13.33l-2.18,1.45h-4.64l-0.42-6.57
h5.68l1.55,1.37V13.33z"/>
<polygon class="st0" points="84.65,4.21 93.01,4.21 95.75,6.46 96.26,10.9 92.23,12.43 91.77,9.74 88.97,8.28 85.86,9.82
83.88,15.02 85.51,20.94 88.49,22.38 91.99,20.59 91.99,18.59 96.26,16.96 95.85,22.85 91.81,26.87 84.17,26.55 80.79,23.58
78.87,14.87 80.79,6.94 "/>
<polygon class="st0" points="97.62,4.21 103.33,4.21 102.96,21.08 108.7,20.14 108.34,6.42 113.85,3.28 113.9,19.9 115.75,19.71
115.27,25.86 113.88,25.86 114.27,32.36 108.7,32.36 108.7,26.39 102.96,26.39 102.96,32.36 91.77,33.85 92.2,28.85 97.88,27.96
"/>
<polygon class="st0" points="147.43,28.86 147.43,32.36 162.85,32.36 162.48,25.36 159.5,26 158.89,27.68 154.1,27.24
154.1,21.51 160.81,20.85 160.81,16.48 153.86,16.54 153.86,9.18 158.62,8.43 159.22,9.77 161.85,10.06 162.59,4 147.43,4
147.43,6.54 148.68,7.46 148.68,28.4 "/>
<polygon class="st0" points="163.89,9.24 163.89,4 172.31,4 170.35,26.87 179.55,24.74 179.55,32.36 164.51,32.34 164.65,28.71
165.73,27.84 165.73,9.59 "/>
<path class="st0" d="M193.69,32.36l-0.63-2.51l-2.84-1.89l-4.29-20.14L185.9,4h-11.27l-0.03,3.2l1.87-0.34l-2.79,14.07l-1.37,0.57
v2.85l6.29-1.33l0.4-2.7l4.65-0.89l1.69,12.93H193.69z M179.39,15.11l1.65-6.52l0.89,0.25l0.92,5.45L179.39,15.11z"/>
<polygon class="st0" points="208.47,21.68 210.62,21.12 210.04,18.15 200.51,17.46 198.87,21.13 203.56,21.9 203.32,23.91
200.58,25.19 196.44,23.77 194.48,17.19 196.2,10.02 200.08,8.52 203.31,9.62 202.85,11.75 207.79,13.6 208.83,9.69 204.71,4.21
195.57,4.21 191.24,7.36 189.29,16.87 192.06,27.54 199.03,30.53 203.2,29.3 203.09,32.36 209.01,32.36 209.4,29.95 207.38,28.99
"/>
<path class="st0" d="M230.45,6.26L226.39,4l-8.59-0.01l-4.07,2.86l-2.58,8.9l1.52,11.82l5.61,4.73l7.65,0.01l5.72-4.59l2.47-12.46
L230.45,6.26z M228.23,21.75l-3.95,5.45l-2.16,0.43l-4.6-3.46L216,15.72l2.4-7.02l5.14-0.48l2.97,1.79l1.74,5.83L228.23,21.75z"/>
<path class="st0" d="M116.13,27.48l-0.24,4.88l12.26,0.09l-0.83-5.01l-2.86-0.48l0.14-17.62l2.45-0.42l-0.14-4.85l-10.92,0.36
l0.1,4.6l3.2,0.63l-0.42,17.67L116.13,27.48z"/>
<path class="st0" d="M141.34,4.21l-12.88-0.39v4.26l1.95,0.62v25.15l-1.8,1.41l-0.02,2.63h8.23L136,27.96h6.09l4.57-4.46V7.27
L141.34,4.21z M141.38,20.51l-2.54,1.89l-3.23,0.16L135.4,9.32h3.88l2.1,1.68V20.51z"/>
</g>
<g>
<path class="st0" d="M14.14,11.28c0,0.35-0.02,0.71-0.07,1.05c0.38,0.07,0.76,0.11,1.16,0.11s0.79-0.04,1.16-0.11
c-0.05-0.34-0.07-0.7-0.07-1.05c0-3.23,1.94-6.02,4.72-7.25C20.17,1.68,17.9,0,15.24,0S10.3,1.68,9.42,4.03
C12.2,5.26,14.14,8.04,14.14,11.28z"/>
<path class="st0" d="M18.04,11.28c0,0.16,0.01,0.32,0.02,0.48c0.02,0.3,0.06,0.6,0.13,0.88c0.06,0.28,0.15,0.56,0.25,0.83
c0.11,0.3,0.24,0.58,0.39,0.85c1.42-1.33,3.33-2.15,5.42-2.15s4.01,0.82,5.42,2.15c0.51-0.9,0.79-1.94,0.79-3.04
c0-3.42-2.79-6.22-6.22-6.22c-0.4,0-0.79,0.04-1.16,0.11c-0.28,0.06-0.56,0.13-0.83,0.22c-0.28,0.09-0.56,0.21-0.83,0.35
C19.42,6.77,18.04,8.87,18.04,11.28z"/>
<path class="st0" d="M6.22,12.16c2.1,0,4.01,0.82,5.42,2.15c0.15-0.27,0.28-0.55,0.39-0.85c0.1-0.27,0.19-0.54,0.25-0.83
c0.06-0.28,0.11-0.58,0.13-0.88c0.02-0.15,0.02-0.32,0.02-0.48c0-2.41-1.38-4.51-3.39-5.54C8.77,5.6,8.5,5.49,8.21,5.39
c-0.27-0.1-0.55-0.17-0.83-0.22C7,5.1,6.61,5.06,6.22,5.06C2.79,5.06,0,7.85,0,11.28c0,1.1,0.28,2.14,0.79,3.04
C2.21,12.98,4.12,12.16,6.22,12.16z"/>
<path class="st0" d="M29.21,16.33c-0.18-0.23-0.36-0.44-0.57-0.65c-1.12-1.12-2.67-1.81-4.38-1.81c-1.71,0-3.25,0.69-4.38,1.81
c-0.2,0.2-0.39,0.42-0.56,0.64c-0.18,0.23-0.34,0.47-0.47,0.72c-0.2,0.34-0.36,0.71-0.48,1.09c2.83,1.21,4.81,4.02,4.81,7.28
c0,0.26-0.01,0.52-0.04,0.78c0.37,0.07,0.75,0.1,1.13,0.1c3.43,0,6.22-2.79,6.22-6.22c0-1.11-0.29-2.14-0.8-3.04
C29.54,16.8,29.38,16.56,29.21,16.33z"/>
<path class="st0" d="M12.12,18.14c-0.13-0.38-0.28-0.75-0.48-1.09c-0.14-0.26-0.3-0.5-0.47-0.72c-0.17-0.23-0.36-0.44-0.56-0.64
c-1.12-1.12-2.67-1.81-4.38-1.81s-3.26,0.69-4.38,1.81c-0.21,0.2-0.39,0.42-0.56,0.64c-0.18,0.23-0.34,0.47-0.47,0.72
C0.29,17.94,0,18.98,0,20.08c0,3.43,2.79,6.22,6.22,6.22c0.39,0,0.76-0.03,1.13-0.1c-0.03-0.26-0.04-0.52-0.04-0.78
C7.31,22.15,9.29,19.34,12.12,18.14z"/>
<path class="st0" d="M18.04,19.87c-0.27-0.14-0.55-0.26-0.84-0.35c-0.27-0.09-0.55-0.17-0.84-0.22c-0.37-0.07-0.75-0.1-1.13-0.1
s-0.76,0.03-1.13,0.1c-0.28,0.05-0.57,0.13-0.84,0.22c-0.29,0.1-0.57,0.22-0.84,0.35C10.4,20.9,9.02,23,9.02,25.42
c0,0.07,0,0.14,0.01,0.21c0.01,0.31,0.04,0.61,0.1,0.9c0.05,0.28,0.12,0.57,0.21,0.84c0.82,2.48,3.16,4.27,5.9,4.27
s5.08-1.79,5.9-4.27c0.09-0.27,0.17-0.55,0.21-0.84c0.06-0.3,0.09-0.6,0.1-0.91c0.01-0.07,0.01-0.14,0.01-0.21
C21.45,23,20.07,20.9,18.04,19.87z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1 +1,66 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 240 38" style="enable-background:new 0 0 240 38" xml:space="preserve"><style>.st0{fill:#316b84}</style><path class="st0" d="M59.72 27.96 53.03 4.21l-10.78-.17 1.42 4.37 1.41-.26-7.9 24.22h8.44l-.56-2.27-.81-3.27 8.9-5.7 1.78 11.24h7.97v-4.73l-3.18.32zm-14.1-7.75 3.13-10.84h1.5l2.02 7.44-6.65 3.4z"/><path class="st0" d="M78.67 27.96V20.4l-4.11-2.5 3.29-3.78-.47-7.46-2.82-2.45H56.65v5.27l3.81-1.11 2.31 13.36-2.79.73L61 26.34l5.06-.52.36-6.15 4.32.13 3.16 3.62v8.94l12.89 1.49v-5.34l-8.12-.55zm-5.4-14.63-2.18 1.45h-4.64l-.42-6.57h5.68l1.55 1.37v3.75z"/><path class="st0" d="M84.65 4.21h8.36l2.74 2.25.51 4.44-4.03 1.53-.46-2.69-2.8-1.46-3.11 1.54-1.98 5.2 1.63 5.92 2.98 1.44 3.5-1.79v-2l4.27-1.63-.41 5.89-4.04 4.02-7.64-.32-3.38-2.97-1.92-8.71 1.92-7.93z"/><path class="st0" d="M97.62 4.21h5.71l-.37 16.87 5.74-.94-.36-13.72 5.51-3.14.05 16.62 1.85-.19-.48 6.15h-1.39l.39 6.5h-5.57v-5.97h-5.74v5.97l-11.19 1.49.43-5 5.68-.89zm49.81 24.65v3.5h15.42l-.37-7-2.98.64-.61 1.68-4.79-.44v-5.73l6.71-.66v-4.37l-6.95.06V9.18l4.76-.75.6 1.34 2.63.29.74-6.06h-15.16v2.54l1.25.92V28.4zm16.46-19.62V4h8.42l-1.96 22.87 9.2-2.13v7.62l-15.04-.02.14-3.63 1.08-.87V9.59z"/><path class="st0" d="m193.69 32.36-.63-2.51-2.84-1.89-4.29-20.14L185.9 4h-11.27l-.03 3.2 1.87-.34-2.79 14.07-1.37.57v2.85l6.29-1.33.4-2.7 4.65-.89 1.69 12.93h8.35zm-14.3-17.25 1.65-6.52.89.25.92 5.45-3.46.82z"/><path class="st0" d="m208.47 21.68 2.15-.56-.58-2.97-9.53-.69-1.64 3.67 4.69.77-.24 2.01-2.74 1.28-4.14-1.42-1.96-6.58 1.72-7.17 3.88-1.5 3.23 1.1-.46 2.13 4.94 1.85 1.04-3.91-4.12-5.48h-9.14l-4.33 3.15-1.95 9.51 2.77 10.67 6.97 2.99 4.17-1.23-.11 3.06h5.92l.39-2.41-2.02-.96zm21.98-15.42L226.39 4l-8.59-.01-4.07 2.86-2.58 8.9 1.52 11.82 5.61 4.73 7.65.01 5.72-4.59 2.47-12.46-3.67-9zm-2.22 15.49-3.95 5.45-2.16.43-4.6-3.46-1.52-8.45 2.4-7.02 5.14-.48 2.97 1.79 1.74 5.83-.02 5.91zm-112.1 5.73-.24 4.88 12.26.09-.83-5.01-2.86-.48.14-17.62 2.45-.42-.14-4.85-10.92.36.1 4.6 3.2.63-.42 17.67-2.74.15zm25.21-23.27-12.88-.39v4.26l1.95.62v25.15l-1.8 1.41-.02 2.63h8.23l-.82-9.93h6.09l4.57-4.46V7.27l-5.32-3.06zm.04 16.3-2.54 1.89-3.23.16-.21-13.24h3.88l2.1 1.68v9.51zM14.14 11.28c0 .35-.02.71-.07 1.05.38.07.76.11 1.16.11s.79-.04 1.16-.11a7.933 7.933 0 0 1 4.65-8.3C20.17 1.68 17.9 0 15.24 0S10.3 1.68 9.42 4.03a7.922 7.922 0 0 1 4.72 7.25z"/><path class="st0" d="M18.04 11.28c0 .16.01.32.02.48.02.3.06.6.13.88.06.28.15.56.25.83.11.3.24.58.39.85 1.42-1.33 3.33-2.15 5.42-2.15s4.01.82 5.42 2.15c.51-.9.79-1.94.79-3.04 0-3.42-2.79-6.22-6.22-6.22-.4 0-.79.04-1.16.11-.28.06-.56.13-.83.22-.28.09-.56.21-.83.35a6.24 6.24 0 0 0-3.38 5.54zm-11.82.88c2.1 0 4.01.82 5.42 2.15.15-.27.28-.55.39-.85.1-.27.19-.54.25-.83.06-.28.11-.58.13-.88.02-.15.02-.32.02-.48a6.23 6.23 0 0 0-3.39-5.54c-.27-.13-.54-.24-.83-.34-.27-.1-.55-.17-.83-.22a6.42 6.42 0 0 0-1.16-.11 6.227 6.227 0 0 0-5.43 9.26 7.885 7.885 0 0 1 5.43-2.16z"/><path class="st0" d="M29.21 16.33c-.18-.23-.36-.44-.57-.65a6.174 6.174 0 0 0-4.38-1.81 6.192 6.192 0 0 0-4.94 2.45c-.18.23-.34.47-.47.72-.2.34-.36.71-.48 1.09a7.923 7.923 0 0 1 4.77 8.06c.37.07.75.1 1.13.1 3.43 0 6.22-2.79 6.22-6.22 0-1.11-.29-2.14-.8-3.04-.15-.23-.31-.47-.48-.7zm-17.09 1.81c-.13-.38-.28-.75-.48-1.09-.14-.26-.3-.5-.47-.72-.17-.23-.36-.44-.56-.64-1.12-1.12-2.67-1.81-4.38-1.81s-3.26.69-4.38 1.81c-.21.2-.39.42-.56.64-.18.23-.34.47-.47.72-.53.89-.82 1.93-.82 3.03 0 3.43 2.79 6.22 6.22 6.22.39 0 .76-.03 1.13-.1a7.902 7.902 0 0 1 4.77-8.06z"/><path class="st0" d="M18.04 19.87c-.27-.14-.55-.26-.84-.35-.27-.09-.55-.17-.84-.22-.37-.07-.75-.1-1.13-.1s-.76.03-1.13.1c-.28.05-.57.13-.84.22-.29.1-.57.22-.84.35a6.225 6.225 0 0 0-3.4 5.55c0 .07 0 .14.01.21.01.31.04.61.1.9.05.28.12.57.21.84.82 2.48 3.16 4.27 5.9 4.27s5.08-1.79 5.9-4.27c.09-.27.17-.55.21-.84.06-.3.09-.6.1-.91.01-.07.01-.14.01-.21a6.24 6.24 0 0 0-3.42-5.54z"/></svg> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 240 38" style="enable-background:new 0 0 240 38;" xml:space="preserve">
<style type="text/css">
.st0{fill:#316B84;}
</style>
<g>
<g>
<path class="st0" d="M59.72,27.96L53.03,4.21L42.25,4.04l1.42,4.37l1.41-0.26l-7.9,24.22h8.44l-0.56-2.27l-0.81-3.27l8.9-5.7
l1.78,11.24h7.97v-4.73L59.72,27.96z M45.62,20.21l3.13-10.84h1.5l2.02,7.44L45.62,20.21z"/>
<path class="st0" d="M78.67,27.96V20.4l-4.11-2.5l3.29-3.78l-0.47-7.46l-2.82-2.45H56.65v5.27l3.81-1.11l2.31,13.36l-2.79,0.73
L61,26.34l5.06-0.52l0.36-6.15l4.32,0.13l3.16,3.62v8.94l12.89,1.49v-5.34L78.67,27.96z M73.27,13.33l-2.18,1.45h-4.64l-0.42-6.57
h5.68l1.55,1.37V13.33z"/>
<polygon class="st0" points="84.65,4.21 93.01,4.21 95.75,6.46 96.26,10.9 92.23,12.43 91.77,9.74 88.97,8.28 85.86,9.82
83.88,15.02 85.51,20.94 88.49,22.38 91.99,20.59 91.99,18.59 96.26,16.96 95.85,22.85 91.81,26.87 84.17,26.55 80.79,23.58
78.87,14.87 80.79,6.94 "/>
<polygon class="st0" points="97.62,4.21 103.33,4.21 102.96,21.08 108.7,20.14 108.34,6.42 113.85,3.28 113.9,19.9 115.75,19.71
115.27,25.86 113.88,25.86 114.27,32.36 108.7,32.36 108.7,26.39 102.96,26.39 102.96,32.36 91.77,33.85 92.2,28.85 97.88,27.96
"/>
<polygon class="st0" points="147.43,28.86 147.43,32.36 162.85,32.36 162.48,25.36 159.5,26 158.89,27.68 154.1,27.24
154.1,21.51 160.81,20.85 160.81,16.48 153.86,16.54 153.86,9.18 158.62,8.43 159.22,9.77 161.85,10.06 162.59,4 147.43,4
147.43,6.54 148.68,7.46 148.68,28.4 "/>
<polygon class="st0" points="163.89,9.24 163.89,4 172.31,4 170.35,26.87 179.55,24.74 179.55,32.36 164.51,32.34 164.65,28.71
165.73,27.84 165.73,9.59 "/>
<path class="st0" d="M193.69,32.36l-0.63-2.51l-2.84-1.89l-4.29-20.14L185.9,4h-11.27l-0.03,3.2l1.87-0.34l-2.79,14.07l-1.37,0.57
v2.85l6.29-1.33l0.4-2.7l4.65-0.89l1.69,12.93H193.69z M179.39,15.11l1.65-6.52l0.89,0.25l0.92,5.45L179.39,15.11z"/>
<polygon class="st0" points="208.47,21.68 210.62,21.12 210.04,18.15 200.51,17.46 198.87,21.13 203.56,21.9 203.32,23.91
200.58,25.19 196.44,23.77 194.48,17.19 196.2,10.02 200.08,8.52 203.31,9.62 202.85,11.75 207.79,13.6 208.83,9.69 204.71,4.21
195.57,4.21 191.24,7.36 189.29,16.87 192.06,27.54 199.03,30.53 203.2,29.3 203.09,32.36 209.01,32.36 209.4,29.95 207.38,28.99
"/>
<path class="st0" d="M230.45,6.26L226.39,4l-8.59-0.01l-4.07,2.86l-2.58,8.9l1.52,11.82l5.61,4.73l7.65,0.01l5.72-4.59l2.47-12.46
L230.45,6.26z M228.23,21.75l-3.95,5.45l-2.16,0.43l-4.6-3.46L216,15.72l2.4-7.02l5.14-0.48l2.97,1.79l1.74,5.83L228.23,21.75z"/>
<path class="st0" d="M116.13,27.48l-0.24,4.88l12.26,0.09l-0.83-5.01l-2.86-0.48l0.14-17.62l2.45-0.42l-0.14-4.85l-10.92,0.36
l0.1,4.6l3.2,0.63l-0.42,17.67L116.13,27.48z"/>
<path class="st0" d="M141.34,4.21l-12.88-0.39v4.26l1.95,0.62v25.15l-1.8,1.41l-0.02,2.63h8.23L136,27.96h6.09l4.57-4.46V7.27
L141.34,4.21z M141.38,20.51l-2.54,1.89l-3.23,0.16L135.4,9.32h3.88l2.1,1.68V20.51z"/>
</g>
<g>
<path class="st0" d="M14.14,11.28c0,0.35-0.02,0.71-0.07,1.05c0.38,0.07,0.76,0.11,1.16,0.11s0.79-0.04,1.16-0.11
c-0.05-0.34-0.07-0.7-0.07-1.05c0-3.23,1.94-6.02,4.72-7.25C20.17,1.68,17.9,0,15.24,0S10.3,1.68,9.42,4.03
C12.2,5.26,14.14,8.04,14.14,11.28z"/>
<path class="st0" d="M18.04,11.28c0,0.16,0.01,0.32,0.02,0.48c0.02,0.3,0.06,0.6,0.13,0.88c0.06,0.28,0.15,0.56,0.25,0.83
c0.11,0.3,0.24,0.58,0.39,0.85c1.42-1.33,3.33-2.15,5.42-2.15s4.01,0.82,5.42,2.15c0.51-0.9,0.79-1.94,0.79-3.04
c0-3.42-2.79-6.22-6.22-6.22c-0.4,0-0.79,0.04-1.16,0.11c-0.28,0.06-0.56,0.13-0.83,0.22c-0.28,0.09-0.56,0.21-0.83,0.35
C19.42,6.77,18.04,8.87,18.04,11.28z"/>
<path class="st0" d="M6.22,12.16c2.1,0,4.01,0.82,5.42,2.15c0.15-0.27,0.28-0.55,0.39-0.85c0.1-0.27,0.19-0.54,0.25-0.83
c0.06-0.28,0.11-0.58,0.13-0.88c0.02-0.15,0.02-0.32,0.02-0.48c0-2.41-1.38-4.51-3.39-5.54C8.77,5.6,8.5,5.49,8.21,5.39
c-0.27-0.1-0.55-0.17-0.83-0.22C7,5.1,6.61,5.06,6.22,5.06C2.79,5.06,0,7.85,0,11.28c0,1.1,0.28,2.14,0.79,3.04
C2.21,12.98,4.12,12.16,6.22,12.16z"/>
<path class="st0" d="M29.21,16.33c-0.18-0.23-0.36-0.44-0.57-0.65c-1.12-1.12-2.67-1.81-4.38-1.81c-1.71,0-3.25,0.69-4.38,1.81
c-0.2,0.2-0.39,0.42-0.56,0.64c-0.18,0.23-0.34,0.47-0.47,0.72c-0.2,0.34-0.36,0.71-0.48,1.09c2.83,1.21,4.81,4.02,4.81,7.28
c0,0.26-0.01,0.52-0.04,0.78c0.37,0.07,0.75,0.1,1.13,0.1c3.43,0,6.22-2.79,6.22-6.22c0-1.11-0.29-2.14-0.8-3.04
C29.54,16.8,29.38,16.56,29.21,16.33z"/>
<path class="st0" d="M12.12,18.14c-0.13-0.38-0.28-0.75-0.48-1.09c-0.14-0.26-0.3-0.5-0.47-0.72c-0.17-0.23-0.36-0.44-0.56-0.64
c-1.12-1.12-2.67-1.81-4.38-1.81s-3.26,0.69-4.38,1.81c-0.21,0.2-0.39,0.42-0.56,0.64c-0.18,0.23-0.34,0.47-0.47,0.72
C0.29,17.94,0,18.98,0,20.08c0,3.43,2.79,6.22,6.22,6.22c0.39,0,0.76-0.03,1.13-0.1c-0.03-0.26-0.04-0.52-0.04-0.78
C7.31,22.15,9.29,19.34,12.12,18.14z"/>
<path class="st0" d="M18.04,19.87c-0.27-0.14-0.55-0.26-0.84-0.35c-0.27-0.09-0.55-0.17-0.84-0.22c-0.37-0.07-0.75-0.1-1.13-0.1
s-0.76,0.03-1.13,0.1c-0.28,0.05-0.57,0.13-0.84,0.22c-0.29,0.1-0.57,0.22-0.84,0.35C10.4,20.9,9.02,23,9.02,25.42
c0,0.07,0,0.14,0.01,0.21c0.01,0.31,0.04,0.61,0.1,0.9c0.05,0.28,0.12,0.57,0.21,0.84c0.82,2.48,3.16,4.27,5.9,4.27
s5.08-1.79,5.9-4.27c0.09-0.27,0.17-0.55,0.21-0.84c0.06-0.3,0.09-0.6,0.1-0.91c0.01-0.07,0.01-0.14,0.01-0.21
C21.45,23,20.07,20.9,18.04,19.87z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,17 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Frequently Asked Questions</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/faq.js") }}"></script>
{% endblock %}
{% block body %}
<div id="faq-wrapper" data-lang="{{ lang }}" class="markdown">
<!-- Content generated by JavaScript -->
</div>
{% endblock %}

View File

@@ -99,18 +99,14 @@
{% if hint.finding_player == player %} {% if hint.finding_player == player %}
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b> <b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
{% else %} {% else %}
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}"> {{ player_names_with_alias[(team, hint.finding_player)] }}
{{ player_names_with_alias[(team, hint.finding_player)] }}
</a>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if hint.receiving_player == player %} {% if hint.receiving_player == player %}
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b> <b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
{% else %} {% else %}
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}"> {{ player_names_with_alias[(team, hint.receiving_player)] }}
{{ player_names_with_alias[(team, hint.receiving_player)] }}
</a>
{% endif %} {% endif %}
</td> </td>
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td> <td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>

View File

@@ -0,0 +1,17 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Glossary</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/glossary.js") }}"></script>
{% endblock %}
{% block body %}
<div id="glossary-wrapper" data-lang="{{ lang }}" class="markdown">
<!-- Content generated by JavaScript -->
</div>
{% endblock %}

View File

@@ -1,13 +0,0 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>{{ title }}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
{% endblock %}
{% block body %}
<div class="markdown">
{{ html_from_markdown | safe}}
</div>
{% endblock %}

View File

@@ -1,21 +1,5 @@
{% extends 'tablepage.html' %} {% 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 %} {% block head %}
{{ super() }} {{ super() }}
<title>User Content</title> <title>User Content</title>
@@ -49,12 +33,10 @@
<tr> <tr>
<td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td> <td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
<td><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td> <td><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td>
<td title="{{ games(room.seed.slots) }}"> <td>{{ room.seed.slots|length }}</td>
{{ room.seed.slots|length }}
</td>
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td> <td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td> <td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</a></td> <td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -78,15 +60,10 @@
{% for seed in seeds %} {% for seed in seeds %}
<tr> <tr>
<td><a href="{{ url_for("view_seed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td> <td><a href="{{ url_for("view_seed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
<td title="{{ games(seed.slots) }}"> <td>{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
{% if seed.multidata %}
{{ seed.slots|length }}
{% else %}
1
{% endif %}
</td> </td>
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td> <td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</a></td> <td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -268,7 +268,6 @@ Additional arguments added to the [Set](#Set) package that triggered this [SetRe
These packets are sent purely from client to server. They are not accepted by clients. These packets are sent purely from client to server. They are not accepted by clients.
* [Connect](#Connect) * [Connect](#Connect)
* [ConnectUpdate](#ConnectUpdate)
* [Sync](#Sync) * [Sync](#Sync)
* [LocationChecks](#LocationChecks) * [LocationChecks](#LocationChecks)
* [LocationScouts](#LocationScouts) * [LocationScouts](#LocationScouts)
@@ -396,7 +395,6 @@ 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. | | 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. | | 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. | | 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 ### 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. 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

@@ -696,9 +696,92 @@ When importing a file that defines a class that inherits from `worlds.AutoWorld.
is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
world since the namespace is shared with all other logic mixins. world since the namespace is shared with all other logic mixins.
Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified LogicMixin is handy when your logic is more complex than one-to-one location-item relationships.
with the state. A game in which "The red key opens the red door" can just express this relationship through a one-line access rule.
Please do this with caution and only when necessary. But now, consider a game with a heavy focus on combat, where the main logical consideration is which enemies you can
defeat with your current items.
There could be dozens of weapons, armor pieces, or consumables that each improve your ability to defeat
specific enemies to varying degrees. It would be useful to be able to keep track of "defeatable enemies" as a state variable,
and have this variable be recalculated as necessary based on newly collected/removed items.
This is the capability of LogicMixin: Adding custom variables to state that get recalculated as necessary.
In general, a LogicMixin class should have at least one mutable variable that is tracking some custom state per player,
as well as `init_mixin` and `copy_mixin` functions so that this variable gets initialized and copied correctly when
`CollectionState()` and `CollectionState.copy()` are called respectively.
```python
from BaseClasses import CollectionState, MultiWorld
from worlds.AutoWorld import LogicMixin
class MyGameState(LogicMixin):
mygame_defeatable_enemies: Dict[int, Set[str]] # per player
def init_mixin(self, multiworld: MultiWorld) -> None:
# Initialize per player with the corresponding "nothing" value, such as 0 or an empty set.
# You can also use something like Collections.defaultdict
self.mygame_defeatable_enemies = {
player: set() for player in multiworld.get_game_players("My Game")
}
def copy_mixin(self, new_state: CollectionState) -> CollectionState:
# Be careful to make a "deep enough" copy here!
new_state.mygame_defeatable_enemies = {
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
}
```
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
Usually, doing this coincides with an override of `World.collect` and `World.remove`, where the custom state variable
gets recalculated when a relevant item is collected or removed.
```python
# __init__.py
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if change and item in COMBAT_ITEMS:
state.mygame_defeatable_enemies[self.player] |= get_newly_unlocked_enemies(state)
return change
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change and item in COMBAT_ITEMS:
state.mygame_defeatable_enemies[self.player] -= get_newly_locked_enemies(state)
return change
```
Using LogicMixin can greatly slow down your code if you don't use it intelligently. This is because `collect`
and `remove` are called very frequently during fill. If your `collect` & `remove` cause a heavy calculation
every time, your code might end up being *slower* than just doing calculations in your access rules.
One way to optimise recalculations is to make use of the fact that `collect` should only unlock things,
and `remove` should only lock things.
In our example, we have two different functions: `get_newly_unlocked_enemies` and `get_newly_locked_enemies`.
`get_newly_unlocked_enemies` should only consider enemies that are *not already in the set*
and check whether they were **unlocked**.
`get_newly_locked_enemies` should only consider enemies that are *already in the set*
and check whether they **became locked**.
Another impactful way to optimise LogicMixin is to use caching.
Your custom state variables don't actually need to be recalculated on every `collect` / `remove`, because there are
often multiple calls to `collect` / `remove` between access rule calls. Thus, it would be much more efficient to hold
off on recaculating until the an actual access rule call happens.
A common way to realize this is to define a `mygame_state_is_stale` variable that is set to True in `collect`, `remove`,
and `init_mixin`. The calls to the actual recalculating functions are then moved to the start of the relevant
access rules like this:
```python
def can_defeat_enemy(state: CollectionState, player: int, enemy: str) -> bool:
if state.mygame_state_is_stale[player]:
state.mygame_defeatable_enemies[player] = recalculate_defeatable_enemies(state)
state.mygame_state_is_stale[player] = False
return enemy in state.mygame_defeatable_enemies[player]
```
Only use LogicMixin if necessary. There are often other ways to achieve what it does, like making clever use of
`state.prog_items`, using event items, pseudo-regions, etc.
#### pre_fill #### pre_fill

16
kvui.py
View File

@@ -243,9 +243,6 @@ class ServerLabel(HovererableLabel):
f"\nYou currently have {ctx.hint_points} points." f"\nYou currently have {ctx.hint_points} points."
elif ctx.hint_cost == 0: elif ctx.hint_cost == 0:
text += "\n!hint is free to use." 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: else:
text += f"\nYou are not authenticated yet." text += f"\nYou are not authenticated yet."
@@ -539,8 +536,9 @@ class GameManager(App):
# show Archipelago tab if other logging is present # show Archipelago tab if other logging is present
self.tabs.add_widget(panel) self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser)) hint_panel = TabbedPanelItem(text="Hints")
self.log_panels["Hints"] = hint_panel.content self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser)
self.tabs.add_widget(hint_panel)
if len(self.logging_pairs) == 1: if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago" self.tabs.default_tab_text = "Archipelago"
@@ -574,14 +572,6 @@ class GameManager(App):
return self.container 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): def update_texts(self, dt):
if hasattr(self.tabs.content.children[0], "fix_heights"): if hasattr(self.tabs.content.children[0], "fix_heights"):
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream

View File

@@ -5,6 +5,7 @@ import platform
import shutil import shutil
import sys import sys
import sysconfig import sysconfig
import typing
import warnings import warnings
import zipfile import zipfile
import urllib.request import urllib.request
@@ -13,14 +14,14 @@ import json
import threading import threading
import subprocess import subprocess
from collections.abc import Iterable
from hashlib import sha3_512 from hashlib import sha3_512
from pathlib import Path from pathlib import Path
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
requirement = 'cx-Freeze==7.2.0'
try: try:
requirement = 'cx-Freeze==7.2.0'
import pkg_resources import pkg_resources
try: try:
pkg_resources.require(requirement) pkg_resources.require(requirement)
@@ -29,7 +30,7 @@ try:
install_cx_freeze = True install_cx_freeze = True
except ImportError: except ImportError:
install_cx_freeze = True install_cx_freeze = True
pkg_resources = None # type: ignore[assignment] pkg_resources = None # type: ignore [assignment]
if install_cx_freeze: if install_cx_freeze:
# check if pip is available # check if pip is available
@@ -60,7 +61,7 @@ from Cython.Build import cythonize
# On Python < 3.10 LogicMixin is not currently supported. # On Python < 3.10 LogicMixin is not currently supported.
non_apworlds: Set[str] = { non_apworlds: set = {
"A Link to the Past", "A Link to the Past",
"Adventure", "Adventure",
"ArchipIDLE", "ArchipIDLE",
@@ -83,7 +84,7 @@ non_apworlds: Set[str] = {
if sys.version_info < (3,10): if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight") non_apworlds.add("Hollow Knight")
def download_SNI() -> None: def download_SNI():
print("Updating SNI") print("Updating SNI")
machine_to_go = { machine_to_go = {
"x86_64": "amd64", "x86_64": "amd64",
@@ -113,8 +114,8 @@ def download_SNI() -> None:
if source_url and source_url.endswith(".zip"): if source_url and source_url.endswith(".zip"):
with urllib.request.urlopen(source_url) as download: with urllib.request.urlopen(source_url) as download:
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
for zf_member in zf.infolist(): for member in zf.infolist():
zf.extract(zf_member, path="SNI") zf.extract(member, path="SNI")
print(f"Downloaded SNI from {source_url}") print(f"Downloaded SNI from {source_url}")
elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
@@ -128,13 +129,11 @@ def download_SNI() -> None:
raise ValueError(f"Unexpected file '{member.name}' in {source_url}") raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
elif member.isdir() and not sni_dir: elif member.isdir() and not sni_dir:
sni_dir = member.name sni_dir = member.name
elif member.isfile() and not sni_dir or sni_dir and not member.name.startswith(sni_dir): elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir):
raise ValueError(f"Expected folder before '{member.name}' in {source_url}") raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
elif member.isfile() and sni_dir: elif member.isfile() and sni_dir:
tf.extract(member) tf.extract(member)
# sadly SNI is in its own folder on non-windows, so we need to rename # sadly SNI is in its own folder on non-windows, so we need to rename
if not sni_dir:
raise ValueError("Did not find SNI in archive")
shutil.rmtree("SNI", True) shutil.rmtree("SNI", True)
os.rename(sni_dir, "SNI") os.rename(sni_dir, "SNI")
print(f"Downloaded SNI from {source_url}") print(f"Downloaded SNI from {source_url}")
@@ -146,7 +145,7 @@ def download_SNI() -> None:
print(f"No SNI found for system spec {platform_name} {machine_name}") print(f"No SNI found for system spec {platform_name} {machine_name}")
signtool: Optional[str] signtool: typing.Optional[str]
if os.path.exists("X:/pw.txt"): if os.path.exists("X:/pw.txt"):
print("Using signtool") print("Using signtool")
with open("X:/pw.txt", encoding="utf-8-sig") as f: with open("X:/pw.txt", encoding="utf-8-sig") as f:
@@ -198,13 +197,13 @@ extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
def remove_sprites_from_folder(folder: Path) -> None: def remove_sprites_from_folder(folder):
for file in os.listdir(folder): for file in os.listdir(folder):
if file != ".gitignore": if file != ".gitignore":
os.remove(folder / file) os.remove(folder / file)
def _threaded_hash(filepath: Union[str, Path]) -> str: def _threaded_hash(filepath):
hasher = sha3_512() hasher = sha3_512()
hasher.update(open(filepath, "rb").read()) hasher.update(open(filepath, "rb").read())
return base64.b85encode(hasher.digest()).decode() return base64.b85encode(hasher.digest()).decode()
@@ -218,11 +217,11 @@ class BuildCommand(setuptools.command.build.build):
yes: bool yes: bool
last_yes: bool = False # used by sub commands of build last_yes: bool = False # used by sub commands of build
def initialize_options(self) -> None: def initialize_options(self):
super().initialize_options() super().initialize_options()
type(self).last_yes = self.yes = False type(self).last_yes = self.yes = False
def finalize_options(self) -> None: def finalize_options(self):
super().finalize_options() super().finalize_options()
type(self).last_yes = self.yes type(self).last_yes = self.yes
@@ -234,27 +233,27 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
('extra-data=', None, 'Additional files to add.'), ('extra-data=', None, 'Additional files to add.'),
] ]
yes: bool yes: bool
extra_data: Iterable[str] extra_data: Iterable # [any] not available in 3.8
extra_libs: Iterable[str] # work around broken include_files extra_libs: Iterable # work around broken include_files
buildfolder: Path buildfolder: Path
libfolder: Path libfolder: Path
library: Path library: Path
buildtime: datetime.datetime buildtime: datetime.datetime
def initialize_options(self) -> None: def initialize_options(self):
super().initialize_options() super().initialize_options()
self.yes = BuildCommand.last_yes self.yes = BuildCommand.last_yes
self.extra_data = [] self.extra_data = []
self.extra_libs = [] self.extra_libs = []
def finalize_options(self) -> None: def finalize_options(self):
super().finalize_options() super().finalize_options()
self.buildfolder = self.build_exe self.buildfolder = self.build_exe
self.libfolder = Path(self.buildfolder, "lib") self.libfolder = Path(self.buildfolder, "lib")
self.library = Path(self.libfolder, "library.zip") self.library = Path(self.libfolder, "library.zip")
def installfile(self, path: Path, subpath: Optional[Union[str, Path]] = None, keep_content: bool = False) -> None: def installfile(self, path, subpath=None, keep_content: bool = False):
folder = self.buildfolder folder = self.buildfolder
if subpath: if subpath:
folder /= subpath folder /= subpath
@@ -269,7 +268,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
else: else:
print('Warning,', path, 'not found') print('Warning,', path, 'not found')
def create_manifest(self, create_hashes: bool = False) -> None: def create_manifest(self, create_hashes=False):
# Since the setup is now split into components and the manifest is not, # Since the setup is now split into components and the manifest is not,
# it makes most sense to just remove the hashes for now. Not aware of anyone using them. # it makes most sense to just remove the hashes for now. Not aware of anyone using them.
hashes = {} hashes = {}
@@ -291,7 +290,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
json.dump(manifest, open(manifestpath, "wt"), indent=4) json.dump(manifest, open(manifestpath, "wt"), indent=4)
print("Created Manifest") print("Created Manifest")
def run(self) -> None: def run(self):
# start downloading sni asap # start downloading sni asap
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
sni_thread.start() sni_thread.start()
@@ -342,7 +341,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
# post build steps # post build steps
if is_windows: # kivy_deps is win32 only, linux picks them up automatically if is_windows: # kivy_deps is win32 only, linux picks them up automatically
from kivy_deps import sdl2, glew # type: ignore from kivy_deps import sdl2, glew
for folder in sdl2.dep_bins + glew.dep_bins: for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
print(f"copying {folder} -> {self.libfolder}") print(f"copying {folder} -> {self.libfolder}")
@@ -363,7 +362,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
self.installfile(Path(data)) self.installfile(Path(data))
# kivi data files # kivi data files
import kivy # type: ignore[import-untyped] import kivy
shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"),
self.buildfolder / "data", self.buildfolder / "data",
dirs_exist_ok=True) dirs_exist_ok=True)
@@ -373,7 +372,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
assert not non_apworlds - set(AutoWorldRegister.world_types), \ assert not non_apworlds - set(AutoWorldRegister.world_types), \
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
folders_to_remove: List[str] = [] folders_to_remove: typing.List[str] = []
disabled_worlds_folder = "worlds_disabled" disabled_worlds_folder = "worlds_disabled"
for entry in os.listdir(disabled_worlds_folder): for entry in os.listdir(disabled_worlds_folder):
if os.path.isdir(os.path.join(disabled_worlds_folder, entry)): if os.path.isdir(os.path.join(disabled_worlds_folder, entry)):
@@ -394,7 +393,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
shutil.rmtree(world_directory) shutil.rmtree(world_directory)
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
try: try:
from maseya import z3pr # type: ignore[import-untyped] from maseya import z3pr
except ImportError: except ImportError:
print("Maseya Palette Shuffle not found, skipping data files.") print("Maseya Palette Shuffle not found, skipping data files.")
else: else:
@@ -445,16 +444,16 @@ class AppImageCommand(setuptools.Command):
("app-exec=", None, "The application to run inside the image."), ("app-exec=", None, "The application to run inside the image."),
("yes", "y", 'Answer "yes" to all questions.'), ("yes", "y", 'Answer "yes" to all questions.'),
] ]
build_folder: Optional[Path] build_folder: typing.Optional[Path]
dist_file: Optional[Path] dist_file: typing.Optional[Path]
app_dir: Optional[Path] app_dir: typing.Optional[Path]
app_name: str app_name: str
app_exec: Optional[Path] app_exec: typing.Optional[Path]
app_icon: Optional[Path] # source file app_icon: typing.Optional[Path] # source file
app_id: str # lower case name, used for icon and .desktop app_id: str # lower case name, used for icon and .desktop
yes: bool yes: bool
def write_desktop(self) -> None: def write_desktop(self):
assert self.app_dir, "Invalid app_dir" assert self.app_dir, "Invalid app_dir"
desktop_filename = self.app_dir / f"{self.app_id}.desktop" desktop_filename = self.app_dir / f"{self.app_id}.desktop"
with open(desktop_filename, 'w', encoding="utf-8") as f: with open(desktop_filename, 'w', encoding="utf-8") as f:
@@ -469,7 +468,7 @@ class AppImageCommand(setuptools.Command):
))) )))
desktop_filename.chmod(0o755) desktop_filename.chmod(0o755)
def write_launcher(self, default_exe: Path) -> None: def write_launcher(self, default_exe: Path):
assert self.app_dir, "Invalid app_dir" assert self.app_dir, "Invalid app_dir"
launcher_filename = self.app_dir / "AppRun" launcher_filename = self.app_dir / "AppRun"
with open(launcher_filename, 'w', encoding="utf-8") as f: with open(launcher_filename, 'w', encoding="utf-8") as f:
@@ -492,7 +491,7 @@ $APPDIR/$exe "$@"
""") """)
launcher_filename.chmod(0o755) launcher_filename.chmod(0o755)
def install_icon(self, src: Path, name: Optional[str] = None, symlink: Optional[Path] = None) -> None: def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None):
assert self.app_dir, "Invalid app_dir" assert self.app_dir, "Invalid app_dir"
try: try:
from PIL import Image from PIL import Image
@@ -514,8 +513,7 @@ $APPDIR/$exe "$@"
if symlink: if symlink:
symlink.symlink_to(dest_file.relative_to(symlink.parent)) symlink.symlink_to(dest_file.relative_to(symlink.parent))
def initialize_options(self) -> None: def initialize_options(self):
assert self.distribution.metadata.name
self.build_folder = None self.build_folder = None
self.app_dir = None self.app_dir = None
self.app_name = self.distribution.metadata.name self.app_name = self.distribution.metadata.name
@@ -529,22 +527,17 @@ $APPDIR/$exe "$@"
)) ))
self.yes = False self.yes = False
def finalize_options(self) -> None: def finalize_options(self):
assert self.build_folder
if not self.app_dir: if not self.app_dir:
self.app_dir = self.build_folder.parent / "AppDir" self.app_dir = self.build_folder.parent / "AppDir"
self.app_id = self.app_name.lower() self.app_id = self.app_name.lower()
def run(self) -> None: def run(self):
assert self.build_folder and self.dist_file, "Command not properly set up"
assert (
self.app_icon and self.app_id and self.app_dir and self.app_exec and self.app_name
), "AppImageCommand not properly set up"
self.dist_file.parent.mkdir(parents=True, exist_ok=True) self.dist_file.parent.mkdir(parents=True, exist_ok=True)
if self.app_dir.is_dir(): if self.app_dir.is_dir():
shutil.rmtree(self.app_dir) shutil.rmtree(self.app_dir)
self.app_dir.mkdir(parents=True) self.app_dir.mkdir(parents=True)
opt_dir = self.app_dir / "opt" / self.app_name opt_dir = self.app_dir / "opt" / self.distribution.metadata.name
shutil.copytree(self.build_folder, opt_dir) shutil.copytree(self.build_folder, opt_dir)
root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}'
self.install_icon(self.app_icon, self.app_id, symlink=root_icon) self.install_icon(self.app_icon, self.app_id, symlink=root_icon)
@@ -555,7 +548,7 @@ $APPDIR/$exe "$@"
subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True)
def find_libs(*args: str) -> Sequence[Tuple[str, str]]: def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
"""Try to find system libraries to be included.""" """Try to find system libraries to be included."""
if not args: if not args:
return [] return []
@@ -563,7 +556,7 @@ def find_libs(*args: str) -> Sequence[Tuple[str, str]]:
arch = build_arch.replace('_', '-') arch = build_arch.replace('_', '-')
libc = 'libc6' # we currently don't support musl libc = 'libc6' # we currently don't support musl
def parse(line: str) -> Tuple[Tuple[str, str, str], str]: def parse(line):
lib, path = line.strip().split(' => ') lib, path = line.strip().split(' => ')
lib, typ = lib.split(' ', 1) lib, typ = lib.split(' ', 1)
for test_arch in ('x86-64', 'i386', 'aarch64'): for test_arch in ('x86-64', 'i386', 'aarch64'):
@@ -584,29 +577,26 @@ def find_libs(*args: str) -> Sequence[Tuple[str, str]]:
ldconfig = shutil.which("ldconfig") ldconfig = shutil.which("ldconfig")
assert ldconfig, "Make sure ldconfig is in PATH" assert ldconfig, "Make sure ldconfig is in PATH"
data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:]
find_libs.cache = { # type: ignore[attr-defined] find_libs.cache = { # type: ignore [attr-defined]
k: v for k, v in (parse(line) for line in data if "=>" in line) k: v for k, v in (parse(line) for line in data if "=>" in line)
} }
def find_lib(lib: str, arch: str, libc: str) -> Optional[str]: def find_lib(lib, arch, libc):
cache: Dict[Tuple[str, str, str], str] = getattr(find_libs, "cache") for k, v in find_libs.cache.items():
for k, v in cache.items():
if k == (lib, arch, libc): if k == (lib, arch, libc):
return v return v
for k, v, in cache.items(): for k, v, in find_libs.cache.items():
if k[0].startswith(lib) and k[1] == arch and k[2] == libc: if k[0].startswith(lib) and k[1] == arch and k[2] == libc:
return v return v
return None return None
res: List[Tuple[str, str]] = [] res = []
for arg in args: for arg in args:
# try exact match, empty libc, empty arch, empty arch and libc # try exact match, empty libc, empty arch, empty arch and libc
file = find_lib(arg, arch, libc) file = find_lib(arg, arch, libc)
file = file or find_lib(arg, arch, '') file = file or find_lib(arg, arch, '')
file = file or find_lib(arg, '', libc) file = file or find_lib(arg, '', libc)
file = file or find_lib(arg, '', '') file = file or find_lib(arg, '', '')
if not file:
raise ValueError(f"Could not find lib {arg}")
# resolve symlinks # resolve symlinks
for n in range(0, 5): for n in range(0, 5):
res.append((file, os.path.join('lib', os.path.basename(file)))) res.append((file, os.path.join('lib', os.path.basename(file))))

View File

@@ -59,12 +59,3 @@ class TestOptions(unittest.TestCase):
item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)} item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)}
for link in item_links.values(): for link in item_links.values():
self.assertEqual(link.value[0], item_link_group[0]) self.assertEqual(link.value[0], item_link_group[0])
def test_pickle_dumps(self):
"""Test options can be pickled into database for WebHost generation"""
import pickle
for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
pickle.dumps(option(option.default))

View File

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

View File

@@ -1,73 +0,0 @@
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

@@ -10,7 +10,7 @@ from dataclasses import make_dataclass
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple,
TYPE_CHECKING, Type, Union) TYPE_CHECKING, Type, Union)
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions
from BaseClasses import CollectionState from BaseClasses import CollectionState
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -342,7 +342,7 @@ class World(metaclass=AutoWorldRegister):
# overridable methods that get called by Main.py, sorted by execution order # overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_<original_name>", # can also be implemented as a classmethod and called "stage_<original_name>",
# in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld. # in that case the MultiWorld object is passed as an argument, and it gets called once for the entire multiworld.
# An example of this can be found in alttp as stage_pre_fill # An example of this can be found in alttp as stage_pre_fill
@classmethod @classmethod
@@ -480,7 +480,6 @@ class World(metaclass=AutoWorldRegister):
group = cls(multiworld, new_player_id) group = cls(multiworld, new_player_id)
group.options = cls.options_dataclass(**{option_key: option.from_any(option.default) group.options = cls.options_dataclass(**{option_key: option.from_any(option.default)
for option_key, option in cls.options_dataclass.type_hints.items()}) for option_key, option in cls.options_dataclass.type_hints.items()})
group.options.accessibility = ItemsAccessibility(ItemsAccessibility.option_items)
return group return group

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']}") raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]], async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]],
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]: guard_list: typing.List[typing.Tuple[int, typing.Iterable[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 """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
value. value.
@@ -266,7 +266,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tu
return ret return ret
async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]: async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
"""Reads data at 1 or more addresses. """Reads data at 1 or more addresses.
Items in `read_list` should be organized `(address, size, domain)` where Items in `read_list` should be organized `(address, size, domain)` where
@@ -278,8 +278,8 @@ async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int,
return await guarded_read(ctx, read_list, []) return await guarded_read(ctx, read_list, [])
async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]], async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]],
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool: guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool:
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. """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 Items in `write_list` should be organized `(address, value, domain)` where
@@ -316,7 +316,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.
return True return True
async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None: async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None:
"""Writes data to 1 or more addresses. """Writes data to 1 or more addresses.
Items in write_list should be organized `(address, value, domain)` where Items in write_list should be organized `(address, value, domain)` where

View File

@@ -4,7 +4,7 @@ import websockets
import functools import functools
from copy import deepcopy from copy import deepcopy
from typing import List, Any, Iterable from typing import List, Any, Iterable
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem
from MultiServer import Endpoint from MultiServer import Endpoint
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
@@ -101,35 +101,12 @@ class AHITContext(CommonContext):
def on_package(self, cmd: str, args: dict): def on_package(self, cmd: str, args: dict):
if cmd == "Connected": if cmd == "Connected":
json = args self.connected_msg = encode([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: if self.awaiting_info:
self.server_msgs.append(self.room_info) self.server_msgs.append(self.room_info)
self.update_items() self.update_items()
self.awaiting_info = False 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": elif cmd == "ReceivedItems":
if args["index"] == 0: if args["index"] == 0:
self.full_inventory.clear() self.full_inventory.clear()
@@ -189,17 +166,6 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
await ctx.disconnect_proxy() await ctx.disconnect_proxy()
break 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(): if ctx.connected_msg and ctx.is_connected():
await ctx.send_msgs_proxy(ctx.connected_msg) await ctx.send_msgs_proxy(ctx.connected_msg)
ctx.update_items() ctx.update_items()

View File

@@ -152,10 +152,10 @@ def create_dw_regions(world: "HatInTimeWorld"):
for name in annoying_dws: for name in annoying_dws:
world.excluded_dws.append(name) world.excluded_dws.append(name)
if not world.options.DWEnableBonus and world.options.DWAutoCompleteBonuses: if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses:
for name in death_wishes: for name in death_wishes:
world.excluded_bonuses.append(name) world.excluded_bonuses.append(name)
if world.options.DWExcludeAnnoyingBonuses and not world.options.DWAutoCompleteBonuses: elif world.options.DWExcludeAnnoyingBonuses:
for name in annoying_bonuses: for name in annoying_bonuses:
world.excluded_bonuses.append(name) world.excluded_bonuses.append(name)

View File

@@ -253,8 +253,7 @@ class HatInTimeWorld(World):
else: else:
item_name = loc.item.name item_name = loc.item.name
shop_item_names.setdefault(str(loc.address), shop_item_names.setdefault(str(loc.address), item_name)
f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})")
slot_data["ShopItemNames"] = shop_item_names slot_data["ShopItemNames"] = shop_item_names

View File

@@ -738,7 +738,9 @@ class AquariaRegions:
self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.__connect_regions("Sun Temple left area", "Veil left of sun temple",
self.sun_temple_l, self.veil_tr_l) self.sun_temple_l, self.veil_tr_l)
self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", self.__connect_regions("Sun Temple left area", "Sun Temple before boss area",
self.sun_temple_l, self.sun_temple_boss_path) self.sun_temple_l, self.sun_temple_boss_path,
lambda state: _has_light(state, self.player) or
_has_sun_crystal(state, self.player))
self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area",
self.sun_temple_boss_path, self.sun_temple_boss, self.sun_temple_boss_path, self.sun_temple_boss,
lambda state: _has_energy_attack_item(state, self.player)) lambda state: _has_energy_attack_item(state, self.player))
@@ -773,11 +775,14 @@ class AquariaRegions:
self.abyss_l, self.king_jellyfish_cave, self.abyss_l, self.king_jellyfish_cave,
lambda state: (_has_energy_form(state, self.player) and lambda state: (_has_energy_form(state, self.player) and
_has_beast_form(state, self.player)) or _has_beast_form(state, self.player)) or
_has_dual_form(state, self.player)) _has_dual_form(state, self.player))
self.__connect_regions("Abyss left area", "Abyss right area", self.__connect_regions("Abyss left area", "Abyss right area",
self.abyss_l, self.abyss_r) self.abyss_l, self.abyss_r)
self.__connect_regions("Abyss right area", "Abyss right area, transturtle", self.__connect_one_way_regions("Abyss right area", "Abyss right area, transturtle",
self.abyss_r, self.abyss_r_transturtle) self.abyss_r, self.abyss_r_transturtle)
self.__connect_one_way_regions("Abyss right area, transturtle", "Abyss right area",
self.abyss_r_transturtle, self.abyss_r,
lambda state: _has_light(state, self.player))
self.__connect_regions("Abyss right area", "Inside the whale", self.__connect_regions("Abyss right area", "Inside the whale",
self.abyss_r, self.whale, self.abyss_r, self.whale,
lambda state: _has_spirit_form(state, self.player) and lambda state: _has_spirit_form(state, self.player) and
@@ -1087,10 +1092,12 @@ class AquariaRegions:
lambda state: _has_light(state, self.player)) lambda state: _has_light(state, self.player))
add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player), add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player),
lambda state: _has_light(state, self.player)) lambda state: _has_light(state, self.player))
add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player),
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player),
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player), add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player),
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
add_rule(self.multiworld.get_entrance("Abyss right area, transturtle to Abyss right area", self.player),
lambda state: _has_light(state, self.player))
def __adjusting_manual_rules(self) -> None: def __adjusting_manual_rules(self) -> None:
add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player),
@@ -1144,10 +1151,6 @@ class AquariaRegions:
lambda state: state.has("Sun God beated", self.player)) lambda state: state.has("Sun God beated", self.player))
add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player), add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player),
lambda state: _has_tongue_cleared(state, self.player)) lambda state: _has_tongue_cleared(state, self.player))
add_rule(self.multiworld.get_location(
"Open Water top right area, bulb in the small path before Mithalas",
self.player), lambda state: _has_bind_song(state, self.player)
)
def __no_progression_hard_or_hidden_location(self) -> None: def __no_progression_hard_or_hidden_location(self) -> None:
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",

View File

@@ -130,13 +130,12 @@ class AquariaWorld(World):
return result return result
def __pre_fill_item(self, item_name: str, location_name: str, precollected, def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None:
itemClassification: ItemClassification = ItemClassification.useful) -> None:
"""Pre-assign an item to a location""" """Pre-assign an item to a location"""
if item_name not in precollected: if item_name not in precollected:
self.exclude.append(item_name) self.exclude.append(item_name)
data = item_table[item_name] data = item_table[item_name]
item = AquariaItem(item_name, itemClassification, data.id, self.player) item = AquariaItem(item_name, ItemClassification.useful, data.id, self.player)
self.multiworld.get_location(location_name, self.player).place_locked_item(item) self.multiworld.get_location(location_name, self.player).place_locked_item(item)
def get_filler_item_name(self): def get_filler_item_name(self):
@@ -165,8 +164,7 @@ class AquariaWorld(World):
self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected) self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected)
# The last two are inverted because in the original game, they are special turtle that communicate directly # The last two are inverted because in the original game, they are special turtle that communicate directly
self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected, self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected)
ItemClassification.progression)
self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
for name, data in item_table.items(): for name, data in item_table.items():
if name not in self.exclude: if name not in self.exclude:
@@ -214,8 +212,4 @@ class AquariaWorld(World):
"skip_first_vision": bool(self.options.skip_first_vision.value), "skip_first_vision": bool(self.options.skip_first_vision.value),
"unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3], "unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3],
"unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3], "unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
"bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb),
"no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations),
"light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places),
"turtle_randomizer": self.options.turtle_randomizer.value,
} }

View File

@@ -8,8 +8,6 @@
## Optional Software ## Optional Software
- For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) - For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), for use with
[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
## Installation and execution Procedures ## Installation and execution Procedures
@@ -115,16 +113,3 @@ sure that your executable has executable permission:
```bash ```bash
chmod +x aquaria_randomizer chmod +x aquaria_randomizer
``` ```
## Auto-Tracking
Aquaria has a fully functional map tracker that supports auto-tracking.
1. Download [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest) and
[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest).
2. Put the tracker pack into /packs/ in your PopTracker install.
3. Open PopTracker, and load the Aquaria pack.
4. For autotracking, click on the "AP" symbol at the top.
5. Enter the Archipelago server address (the one you connected your client to), slot name, and password.
This pack will automatically prompt you to update if one is available.

View File

@@ -2,14 +2,9 @@
## Logiciels nécessaires ## Logiciels nécessaires
- Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne) - Le jeu Aquaria original (trouvable sur la majorité des sites de ventes de jeux vidéo en ligne)
- Le client du Randomizer d'Aquaria [Aquaria randomizer] - Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
(https://github.com/tioui/Aquaria_Randomizer/releases)
## Logiciels optionnels
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) - De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
## Procédures d'installation et d'exécution ## Procédures d'installation et d'exécution
@@ -121,15 +116,3 @@ pour vous assurer que votre fichier est exécutable:
```bash ```bash
chmod +x aquaria_randomizer chmod +x aquaria_randomizer
``` ```
## Tracking automatique
Aquaria a un tracker complet qui supporte le tracking automatique.
1. Téléchargez [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest) et [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest).
2. Mettre le fichier compressé du tracker dans le sous-répertoire /packs/ du répertoire d'installation de PopTracker.
3. Lancez PopTracker, et ouvrez le pack d'Aquaria.
4. Pour activer le tracking automatique, cliquez sur le symbole "AP" dans le haut de la fenêtre.
5. Entrez l'adresse du serveur Archipelago (le serveur auquel vous avez connecté le client), le nom de votre slot, et le mot de passe (si un mot de passe est nécessaire).
Le logiciel vous indiquera si une mise à jour du pack est disponible.

View File

@@ -125,6 +125,6 @@ class BumpStikWorld(World):
lambda state: state.has("Hazard Bumper", self.player, 25) lambda state: state.has("Hazard Bumper", self.player, 25)
self.multiworld.completion_condition[self.player] = \ self.multiworld.completion_condition[self.player] = \
lambda state: state.has_all_counts({"Booster Bumper": 5, "Treasure Bumper": 32, "Hazard Bumper": 25}, \ lambda state: state.has("Booster Bumper", self.player, 5) and \
self.player) state.has("Treasure Bumper", self.player, 32)

View File

@@ -89,7 +89,6 @@ class DarkSouls3World(World):
self.all_excluded_locations = set() self.all_excluded_locations = set()
def generate_early(self) -> None: def generate_early(self) -> None:
self.created_regions = set()
self.all_excluded_locations.update(self.options.exclude_locations.value) self.all_excluded_locations.update(self.options.exclude_locations.value)
# Inform Universal Tracker where Yhorm is being randomized to. # Inform Universal Tracker where Yhorm is being randomized to.
@@ -295,7 +294,6 @@ class DarkSouls3World(World):
new_region.locations.append(new_location) new_region.locations.append(new_location)
self.multiworld.regions.append(new_region) self.multiworld.regions.append(new_region)
self.created_regions.add(region_name)
return new_region return new_region
def create_items(self) -> None: def create_items(self) -> None:
@@ -1307,7 +1305,7 @@ class DarkSouls3World(World):
def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None:
"""Sets a rule for the entrance to the given region.""" """Sets a rule for the entrance to the given region."""
assert region in location_tables assert region in location_tables
if region not in self.created_regions: return if not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return
if isinstance(rule, str): if isinstance(rule, str):
if " -> " not in rule: if " -> " not in rule:
assert item_dictionary[rule].classification == ItemClassification.progression assert item_dictionary[rule].classification == ItemClassification.progression

View File

@@ -3,7 +3,7 @@
## Required Software ## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest) - [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
## Optional Software ## Optional Software
@@ -11,9 +11,8 @@
## Setting Up ## Setting Up
First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go First, download the client from the link above. It doesn't need to go into any particular directory;
into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam it'll automatically locate _Dark Souls III_ in your Steam installation folder.
installation folder.
Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This 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 is the latest version, so you don't need to do any downpatching! However, if you've already
@@ -36,9 +35,8 @@ randomized item and (optionally) enemy locations. You only need to do this once
To run _Dark Souls III_ in Archipelago mode: To run _Dark Souls III_ in Archipelago mode:
1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain 1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the
scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn.
screen.
2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that 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. you can use to interact with the Archipelago server.
@@ -54,21 +52,4 @@ To run _Dark Souls III_ in Archipelago mode:
### Where do I get a config file? ### Where do I get a config file?
The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to 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. The [AP client archive] also configure your personal options and export them into a config file.
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

@@ -2214,13 +2214,13 @@ location_table: Dict[int, LocationDict] = {
'map': 2, 'map': 2,
'index': 217, 'index': 217,
'doom_type': 2006, 'doom_type': 2006,
'region': "Perfect Hatred (E4M2) Upper"}, 'region': "Perfect Hatred (E4M2) Blue"},
351367: {'name': 'Perfect Hatred (E4M2) - Exit', 351367: {'name': 'Perfect Hatred (E4M2) - Exit',
'episode': 4, 'episode': 4,
'map': 2, 'map': 2,
'index': -1, 'index': -1,
'doom_type': -1, 'doom_type': -1,
'region': "Perfect Hatred (E4M2) Upper"}, 'region': "Perfect Hatred (E4M2) Blue"},
351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability', 351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability',
'episode': 4, 'episode': 4,
'map': 3, 'map': 3,

View File

@@ -502,12 +502,13 @@ regions:List[RegionDict] = [
"episode":4, "episode":4,
"connections":[ "connections":[
{"target":"Perfect Hatred (E4M2) Blue","pro":False}, {"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", {"name":"Perfect Hatred (E4M2) Blue",
"connects_to_hub":False, "connects_to_hub":False,
"episode":4, "episode":4,
"connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]}, "connections":[
{"target":"Perfect Hatred (E4M2) Main","pro":False},
{"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
{"name":"Perfect Hatred (E4M2) Yellow", {"name":"Perfect Hatred (E4M2) Yellow",
"connects_to_hub":False, "connects_to_hub":False,
"episode":4, "episode":4,
@@ -517,13 +518,7 @@ regions:List[RegionDict] = [
{"name":"Perfect Hatred (E4M2) Cave", {"name":"Perfect Hatred (E4M2) Cave",
"connects_to_hub":False, "connects_to_hub":False,
"episode":4, "episode":4,
"connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]}, "connections":[]},
{"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) # Sever the Wicked (E4M3)
{"name":"Sever the Wicked (E4M3) Main", {"name":"Sever the Wicked (E4M3) Main",

View File

@@ -403,8 +403,9 @@ def set_episode4_rules(player, multiworld, pro):
state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1))) state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state: set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state:
(state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or state.has("Shotgun", player, 1) or
state.has("Chaingun", player, 1))) state.has("Chaingun", player, 1) or
state.has("Hell Beneath (E4M1) - Blue skull key", player, 1))
# Perfect Hatred (E4M2) # Perfect Hatred (E4M2)
set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state: set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state:

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: * 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 ```yaml
requires: requires:
version: current.version.number version: current.version.number
plando: bosses, items, texts, connections plando: bosses, items, texts, connections
``` ```
## Item Plando ## Item Plando
@@ -74,77 +74,77 @@ A list of all available items and locations can be found in the [website's datap
### Examples ### Examples
```yaml ```yaml
plando_items: plando_items:
# example block 1 - Timespinner # example block 1 - Timespinner
- item: - item:
Empire Orb: 1 Empire Orb: 1
Radiant Orb: 1 Radiant Orb: 1
location: Starter Chest 1 location: Starter Chest 1
from_pool: true from_pool: true
world: true world: true
percentage: 50 percentage: 50
# example block 2 - Ocarina of Time # example block 2 - Ocarina of Time
- items: - items:
Kokiri Sword: 1 Kokiri Sword: 1
Biggoron Sword: 1 Biggoron Sword: 1
Bow: 1 Bow: 1
Magic Meter: 1 Magic Meter: 1
Progressive Strength Upgrade: 3 Progressive Strength Upgrade: 3
Progressive Hookshot: 2 Progressive Hookshot: 2
locations: locations:
- Deku Tree Slingshot Chest - Deku Tree Slingshot Chest
- Dodongos Cavern Bomb Bag Chest - Dodongos Cavern Bomb Bag Chest
- Jabu Jabus Belly Boomerang Chest - Jabu Jabus Belly Boomerang Chest
- Bottom of the Well Lens of Truth Chest - Bottom of the Well Lens of Truth Chest
- Forest Temple Bow Chest - Forest Temple Bow Chest
- Fire Temple Megaton Hammer Chest - Fire Temple Megaton Hammer Chest
- Water Temple Longshot Chest - Water Temple Longshot Chest
- Shadow Temple Hover Boots Chest - Shadow Temple Hover Boots Chest
- Spirit Temple Silver Gauntlets Chest - Spirit Temple Silver Gauntlets Chest
world: false world: false
# example block 3 - Slay the Spire # example block 3 - Slay the Spire
- items: - items:
Boss Relic: 3 Boss Relic: 3
locations: locations:
- Boss Relic 1 - Boss Relic 1
- Boss Relic 2 - Boss Relic 2
- Boss Relic 3 - Boss Relic 3
# example block 4 - Factorio # example block 4 - Factorio
- items: - items:
progressive-electric-energy-distribution: 2 progressive-electric-energy-distribution: 2
electric-energy-accumulators: 1 electric-energy-accumulators: 1
progressive-turret: 2 progressive-turret: 2
locations: locations:
- military - military
- gun-turret - gun-turret
- logistic-science-pack - logistic-science-pack
- steel-processing - steel-processing
percentage: 80 percentage: 80
force: true force: true
# example block 5 - Secret of Evermore # example block 5 - Secret of Evermore
- items: - items:
Levitate: 1 Levitate: 1
Revealer: 1 Revealer: 1
Energize: 1 Energize: 1
locations: locations:
- Master Sword Pedestal - Master Sword Pedestal
- Boss Relic 1 - Boss Relic 1
world: true world: true
count: 2 count: 2
# example block 6 - A Link to the Past # example block 6 - A Link to the Past
- items: - items:
Progressive Sword: 4 Progressive Sword: 4
world: world:
- BobsSlaytheSpire - BobsSlaytheSpire
- BobsRogueLegacy - BobsRogueLegacy
count: count:
min: 1 min: 1
max: 4 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 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. 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 ### Examples
```yaml ```yaml
plando_connections: plando_connections:
# example block 1 - A Link to the Past # example block 1 - A Link to the Past
- entrance: Cave Shop (Lake Hylia) - entrance: Cave Shop (Lake Hylia)
exit: Cave 45 exit: Cave 45
direction: entrance direction: entrance
- entrance: Cave 45 - entrance: Cave 45
exit: Cave Shop (Lake Hylia) exit: Cave Shop (Lake Hylia)
direction: entrance direction: entrance
- entrance: Agahnims Tower - entrance: Agahnims Tower
exit: Old Man Cave Exit (West) exit: Old Man Cave Exit (West)
direction: exit direction: exit
# example block 2 - Minecraft # example block 2 - Minecraft
- entrance: Overworld Structure 1 - entrance: Overworld Structure 1
exit: Nether Fortress exit: Nether Fortress
direction: both direction: both
- entrance: Overworld Structure 2 - entrance: Overworld Structure 2
exit: Village exit: Village
direction: both 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 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

@@ -77,7 +77,7 @@ option_docstrings = {
"RandomizeLoreTablets": "Randomize Lore items into the itempool, one per Lore Tablet, and place randomized item " "RandomizeLoreTablets": "Randomize Lore items into the itempool, one per Lore Tablet, and place randomized item "
"grants on the tablets themselves.\n You must still read the tablet to get the item.", "grants on the tablets themselves.\n You must still read the tablet to get the item.",
"PreciseMovement": "Places skips into logic which require extremely precise player movement, possibly without " "PreciseMovement": "Places skips into logic which require extremely precise player movement, possibly without "
"movement skills such as\n dash or claw.", "movement skills such as\n dash or hook.",
"ProficientCombat": "Places skips into logic which require proficient combat, possibly with limited items.", "ProficientCombat": "Places skips into logic which require proficient combat, possibly with limited items.",
"BackgroundObjectPogos": "Places skips into logic for locations which are reachable via pogoing off of " "BackgroundObjectPogos": "Places skips into logic for locations which are reachable via pogoing off of "
"background objects.", "background objects.",

View File

@@ -534,16 +534,26 @@ class HKWorld(World):
for option_name in hollow_knight_options: for option_name in hollow_knight_options:
option = getattr(self.options, option_name) option = getattr(self.options, option_name)
try: 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) optionvalue = int(option.value)
options[option_name] = optionvalue
except TypeError: except TypeError:
pass pass # C# side is currently typed as dict[str, int], drop what doesn't fit
else:
options[option_name] = optionvalue
# 32 bit int # 32 bit int
slot_data["seed"] = self.random.randint(-2147483647, 2147483646) 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. # HKAP 0.1.0 and later cost data.
location_costs = {} location_costs = {}
for region in self.multiworld.get_regions(self.player): for region in self.multiworld.get_regions(self.player):
@@ -556,7 +566,7 @@ class HKWorld(World):
slot_data["grub_count"] = self.grub_count slot_data["grub_count"] = self.grub_count
slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race slot_data["is_race"] = int(self.settings.disable_spoilers or self.multiworld.is_race)
return slot_data return slot_data

View File

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

View File

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

View File

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

View File

@@ -39,9 +39,7 @@ 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 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 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. for it. The groups you can use for The Messenger are:
The groups you can use for The Messenger are:
* Notes - This covers the music notes * Notes - This covers the music notes
* Keys - An alternative name for the music notes * Keys - An alternative name for the music notes
* Crest - The Sun and Moon Crests * Crest - The Sun and Moon Crests
@@ -52,26 +50,26 @@ 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 * 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 * 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 * During Boss fights
* After Courage Note collection (Corrupted Future chase) * After Courage Note collection (Corrupted Future chase)
* After reaching ninja village a teleport option is added to the menu to reach it quickly * 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 * 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 * 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 * 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 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. be entered in game.
## Known issues ## Known issues
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item * 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 * 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 * 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 * 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 * 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? ## What do I do if I have a problem?

View File

@@ -41,27 +41,14 @@ These steps can also be followed to launch the game and check for mod updates af
## Joining a MultiWorld Game ## 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 1. Launch the game
2. Navigate to `Options > Archipelago Options` 2. Navigate to `Options > Archipelago Options`
3. Enter connection info using the relevant option buttons 3. Enter connection info using the relevant option buttons
* **The game is limited to alphanumerical characters, `.`, and `-`.** * **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 * 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 * 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 4. Select the `Connect to Archipelago` button
5. Navigate to save file selection 5. Navigate to save file selection
6. Start a new game 6. Start a new game

View File

@@ -220,8 +220,6 @@ class MessengerRules:
} }
self.location_rules = { self.location_rules = {
# hq
"Money Wrench": self.can_shop,
# ninja village # ninja village
"Ninja Village Seal - Tree House": "Ninja Village Seal - Tree House":
self.has_dart, self.has_dart,

View File

@@ -1,6 +1,5 @@
from typing import Dict from typing import Dict
from BaseClasses import CollectionState
from . import MessengerTestBase from . import MessengerTestBase
from ..shop import SHOP_ITEMS, FIGURINES from ..shop import SHOP_ITEMS, FIGURINES
@@ -90,15 +89,3 @@ class PlandoTest(MessengerTestBase):
self.assertTrue(loc in FIGURINES) self.assertTrue(loc in FIGURINES)
self.assertEqual(len(figures), len(FIGURINES)) self.assertEqual(len(figures), len(FIGURINES))
max_cost_state = CollectionState(self.multiworld)
self.assertFalse(self.world.get_location("Money Wrench").can_reach(max_cost_state))
prog_shards = []
for item in self.multiworld.itempool:
if "Time Shard " in item.name:
value = int(item.name.strip("Time Shard ()"))
if value >= 100:
prog_shards.append(item)
for shard in prog_shards:
max_cost_state.collect(shard, True)
self.assertTrue(self.world.get_location("Money Wrench").can_reach(max_cost_state))

View File

@@ -29,7 +29,7 @@ def shuffle_structures(self: "MinecraftWorld") -> None:
# Connect plando structures first # Connect plando structures first
if self.options.plando_connections: if self.options.plando_connections:
for conn in self.options.plando_connections: for conn in self.plando_connections:
set_pair(conn.entrance, conn.exit) set_pair(conn.entrance, conn.exit)
# The algorithm tries to place the most restrictive structures first. This algorithm always works on the # The algorithm tries to place the most restrictive structures first. This algorithm always works on the

View File

@@ -100,13 +100,13 @@ item_table: Dict[str, ItemData] = {
"Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1), "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1),
"Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1), "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1),
"Kantele": ItemData(110012, "Wands", ItemClassification.useful), "Kantele": ItemData(110012, "Wands", ItemClassification.useful),
"Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1),
"Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1),
"Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression, 1),
"Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression, 1),
"Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression, 1),
"Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1),
"All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1),
"Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression), "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression),
"Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1), "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1),
"Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing), "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing),

View File

@@ -184,10 +184,6 @@ class OOTWorld(World):
"Small Key Ring (Spirit Temple)", "Small Key Ring (Thieves Hideout)", "Small Key Ring (Water Temple)", "Small Key Ring (Spirit Temple)", "Small Key Ring (Thieves Hideout)", "Small Key Ring (Water Temple)",
"Boss Key (Fire Temple)", "Boss Key (Forest Temple)", "Boss Key (Ganons Castle)", "Boss Key (Fire Temple)", "Boss Key (Forest Temple)", "Boss Key (Ganons Castle)",
"Boss Key (Shadow Temple)", "Boss Key (Spirit Temple)", "Boss Key (Water Temple)"}, "Boss Key (Shadow Temple)", "Boss Key (Spirit Temple)", "Boss Key (Water Temple)"},
# aliases
"Longshot": {"Progressive Hookshot"}, # fuzzy hinting thought Longshot was Slingshot
"Hookshot": {"Progressive Hookshot"}, # for consistency, mostly
} }
location_name_groups = build_location_name_groups() location_name_groups = build_location_name_groups()

View File

@@ -8,9 +8,6 @@
### Fixes ### 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 - 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 player randomized NPC gifts.
- The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower. - The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower.

View File

@@ -177,7 +177,7 @@ class PokemonEmeraldWorld(World):
for species_name in self.options.trainer_party_blacklist.value for species_name in self.options.trainer_party_blacklist.value
if species_name != "_Legendaries" if species_name != "_Legendaries"
} }
if "_Legendaries" in self.options.trainer_party_blacklist.value: if "_Legendaries" in self.options.starter_blacklist.value:
self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON
# In race mode we don't patch any item location information into the ROM # In race mode we don't patch any item location information into the ROM

Some files were not shown because too many files have changed in this diff Show More