Compare commits
49 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f90571fd | ||
|
|
f709d61d04 | ||
|
|
c6d2971d67 | ||
|
|
af14045c3a | ||
|
|
ede59ef5a1 | ||
|
|
63d471514f | ||
|
|
ff297f2951 | ||
|
|
a0f49dd7d9 | ||
|
|
79cec89e24 | ||
|
|
2b0cab82fa | ||
|
|
48822227b5 | ||
|
|
375b5796d9 | ||
|
|
c12ed316cf | ||
|
|
26577b16dc | ||
|
|
af0b5f8cf2 | ||
|
|
618564c60a | ||
|
|
f2ac937d1e | ||
|
|
d4d777b101 | ||
|
|
b772d42df5 | ||
|
|
e8f3aa96da | ||
|
|
2d0bdebaa9 | ||
|
|
ef4d1e77e3 | ||
|
|
f495bf7261 | ||
|
|
2751ccdaab | ||
|
|
6287bc27a6 | ||
|
|
97f2c25924 | ||
|
|
e5a0ef799f | ||
|
|
216e0603e1 | ||
|
|
05a67386c6 | ||
|
|
0ec9039ca6 | ||
|
|
f06f95d03d | ||
|
|
5a853dfccd | ||
|
|
23469fa5c3 | ||
|
|
dc1da4e88b | ||
|
|
67f6b458d7 | ||
|
|
8193fa12b2 | ||
|
|
de0c498470 | ||
|
|
7337309426 | ||
|
|
3205e9b3a0 | ||
|
|
05439012dc | ||
|
|
177c0fef52 | ||
|
|
5c4e81d046 | ||
|
|
a2d585ba5c | ||
|
|
5ea55d77b0 | ||
|
|
ab8caea8be | ||
|
|
a043ed50a6 | ||
|
|
e85a835b47 | ||
|
|
9a9fea0ca2 | ||
|
|
e910a37273 |
@@ -194,7 +194,9 @@ 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 = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
|
self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id])
|
||||||
|
self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id])
|
||||||
|
self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id])
|
||||||
self.player_name[new_id] = name
|
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,
|
||||||
@@ -720,7 +722,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 Region"
|
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected 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)
|
||||||
@@ -946,6 +948,7 @@ 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)))
|
||||||
@@ -1166,7 +1169,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, "Can't reach location without region"
|
assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region"
|
||||||
return self.parent_region.can_reach(state) and self.access_rule(state)
|
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):
|
||||||
@@ -1261,6 +1264,10 @@ class Item:
|
|||||||
def trap(self) -> bool:
|
def trap(self) -> bool:
|
||||||
return ItemClassification.trap in self.classification
|
return ItemClassification.trap in self.classification
|
||||||
|
|
||||||
|
@property
|
||||||
|
def excludable(self) -> bool:
|
||||||
|
return not (self.advancement or self.useful)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def flags(self) -> int:
|
def flags(self) -> int:
|
||||||
return self.classification.as_flag()
|
return self.classification.as_flag()
|
||||||
|
|||||||
@@ -45,10 +45,21 @@ 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:
|
||||||
@@ -164,13 +175,14 @@ 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:
|
||||||
# Should be adjusted as needed in subclasses
|
# The following attributes are used to Connect and should be adjusted as needed in subclasses
|
||||||
tags: typing.Set[str] = {"AP"}
|
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
|
||||||
@@ -343,6 +355,8 @@ 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)
|
||||||
@@ -429,7 +443,10 @@ 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,
|
||||||
@@ -439,6 +456,7 @@ 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:
|
||||||
@@ -459,6 +477,7 @@ 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:
|
||||||
@@ -466,6 +485,7 @@ 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
|
||||||
@@ -504,6 +524,7 @@ class CommonContext:
|
|||||||
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)
|
||||||
@@ -552,26 +573,34 @@ class CommonContext:
|
|||||||
needed_updates.add(game)
|
needed_updates.add(game)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
|
cached_version: int = self.versions.get(game, 0)
|
||||||
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
|
cached_checksum: typing.Optional[str] = self.checksums.get(game)
|
||||||
# no action required if local version is new enough
|
# no action required if cached version is new enough
|
||||||
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
|
if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \
|
||||||
or remote_checksum != local_checksum:
|
or remote_checksum != cached_checksum:
|
||||||
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
|
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
|
||||||
cache_version: int = cached_game.get("version", 0)
|
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
|
||||||
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
|
if ((remote_checksum or remote_version <= local_version and remote_version != 0)
|
||||||
# download remote version if cache is not new enough
|
and remote_checksum == local_checksum):
|
||||||
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
|
self.update_game(network_data_package["games"][game], game)
|
||||||
or remote_checksum != cache_checksum:
|
|
||||||
needed_updates.add(game)
|
|
||||||
else:
|
else:
|
||||||
self.update_game(cached_game, game)
|
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
|
||||||
|
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():
|
||||||
@@ -613,6 +642,7 @@ 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()
|
||||||
@@ -626,6 +656,7 @@ 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")
|
||||||
@@ -635,7 +666,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"""
|
"""Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework"""
|
||||||
if not self.ui:
|
if not self.ui:
|
||||||
return None
|
return None
|
||||||
title = title or "Error"
|
title = title or "Error"
|
||||||
@@ -987,6 +1018,7 @@ 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.')
|
||||||
@@ -1037,6 +1069,7 @@ 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":
|
||||||
@@ -1048,6 +1081,7 @@ 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))
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ from Utils import is_frozen, user_path, local_path, init_logging, open_filename,
|
|||||||
|
|
||||||
|
|
||||||
def open_host_yaml():
|
def open_host_yaml():
|
||||||
file = settings.get_settings().filename
|
s = settings.get_settings()
|
||||||
|
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 \
|
||||||
|
|||||||
1
Main.py
@@ -338,6 +338,7 @@ 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)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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
|
||||||
@@ -184,11 +185,9 @@ 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]]
|
||||||
@@ -197,7 +196,6 @@ 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,
|
||||||
@@ -268,6 +266,10 @@ 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()
|
||||||
@@ -427,6 +429,8 @@ 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},"
|
||||||
@@ -1150,7 +1154,10 @@ class CommandProcessor(metaclass=CommandMeta):
|
|||||||
if not raw:
|
if not raw:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
command = raw.split()
|
try:
|
||||||
|
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)
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any])
|
|||||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
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()}),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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
|
||||||
@@ -69,14 +70,28 @@ def tutorial_landing():
|
|||||||
|
|
||||||
@app.route('/faq/<string:lang>/')
|
@app.route('/faq/<string:lang>/')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def faq(lang):
|
def faq(lang: str):
|
||||||
return render_template("faq.html", lang=lang)
|
import markdown
|
||||||
|
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 terms(lang):
|
def glossary(lang: str):
|
||||||
return render_template("glossary.html", lang=lang)
|
import markdown
|
||||||
|
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>')
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ 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
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
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>`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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>`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -288,6 +288,11 @@ 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"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 119 KiB |
66
WebHostLib/static/static/branding/header-logo-full.svg
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -1,66 +1 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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>
|
||||||
<!-- 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 After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 258 B |
@@ -1,17 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
13
WebHostLib/templates/markdown_document.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -268,6 +268,7 @@ 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)
|
||||||
@@ -395,6 +396,7 @@ Some special keys exist with specific return data, all of them have the prefix `
|
|||||||
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
|
| 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.
|
||||||
|
|||||||
3
kvui.py
@@ -243,6 +243,9 @@ 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."
|
||||||
|
|
||||||
|
|||||||
@@ -59,3 +59,12 @@ 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))
|
||||||
|
|||||||
@@ -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", seed=self.multiworld.seed):
|
with self.subTest("filling multiworld", games=world_type.game, 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")
|
||||||
|
|||||||
73
test/webhost/test_generate.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import zipfile
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
from . import TestBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerate(TestBase):
|
||||||
|
def test_valid_yaml(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify that posting a valid yaml will start generating a game.
|
||||||
|
"""
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
yaml_data = """
|
||||||
|
name: Player1
|
||||||
|
game: Archipelago
|
||||||
|
Archipelago: {}
|
||||||
|
"""
|
||||||
|
response = self.client.post(url_for("generate"),
|
||||||
|
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue("/seed/" in response.request.path or
|
||||||
|
"/wait/" in response.request.path,
|
||||||
|
f"Response did not properly redirect ({response.request.path})")
|
||||||
|
|
||||||
|
def test_empty_zip(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify that posting an empty zip will give an error.
|
||||||
|
"""
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
zip_data = BytesIO()
|
||||||
|
zipfile.ZipFile(zip_data, "w").close()
|
||||||
|
zip_data.seek(0)
|
||||||
|
self.assertGreater(len(zip_data.read()), 0)
|
||||||
|
zip_data.seek(0)
|
||||||
|
response = self.client.post(url_for("generate"),
|
||||||
|
data={"file": (zip_data, "test.zip")},
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertIn("user-message", response.text,
|
||||||
|
"Request did not call flash()")
|
||||||
|
self.assertIn("not find any valid files", response.text,
|
||||||
|
"Response shows unexpected error")
|
||||||
|
self.assertIn("generate-game-form", response.text,
|
||||||
|
"Response did not get user back to the form")
|
||||||
|
|
||||||
|
def test_too_many_players(self) -> None:
|
||||||
|
"""
|
||||||
|
Verify that posting too many players will give an error.
|
||||||
|
"""
|
||||||
|
max_roll = self.app.config["MAX_ROLL"]
|
||||||
|
# validate that max roll has a sensible value, otherwise we probably changed how it works
|
||||||
|
self.assertIsInstance(max_roll, int)
|
||||||
|
self.assertGreater(max_roll, 1)
|
||||||
|
self.assertLess(max_roll, 100)
|
||||||
|
# create a yaml with max_roll+1 players and watch it fail
|
||||||
|
with self.app.app_context(), self.app.test_request_context():
|
||||||
|
yaml_data = "---\n".join([
|
||||||
|
f"name: Player{n}\n"
|
||||||
|
"game: Archipelago\n"
|
||||||
|
"Archipelago: {}\n"
|
||||||
|
for n in range(1, max_roll + 2)
|
||||||
|
])
|
||||||
|
response = self.client.post(url_for("generate"),
|
||||||
|
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
|
||||||
|
follow_redirects=True)
|
||||||
|
self.assertIn("user-message", response.text,
|
||||||
|
"Request did not call flash()")
|
||||||
|
self.assertIn("limited to", response.text,
|
||||||
|
"Response shows unexpected error")
|
||||||
|
self.assertIn("generate-game-form", response.text,
|
||||||
|
"Response did not get user back to the form")
|
||||||
@@ -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 an argument, and it gets called once for the entire multiworld.
|
# in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld.
|
||||||
# An example of this can be found in alttp as stage_pre_fill
|
# An example of this can be found in alttp as stage_pre_fill
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -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.List[typing.Tuple[int, int, str]],
|
async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]],
|
||||||
guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]:
|
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]:
|
||||||
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
|
"""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.List[typing.Tuple[
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
|
async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
|
||||||
"""Reads data at 1 or more addresses.
|
"""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.List[typing.Tuple[int, int
|
|||||||
return await guarded_read(ctx, read_list, [])
|
return await guarded_read(ctx, read_list, [])
|
||||||
|
|
||||||
|
|
||||||
async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]],
|
async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]],
|
||||||
guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool:
|
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool:
|
||||||
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
|
"""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.List[typing.Tupl
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None:
|
async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None:
|
||||||
"""Writes data to 1 or more addresses.
|
"""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
|
||||||
|
|||||||
@@ -738,9 +738,7 @@ 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))
|
||||||
@@ -775,14 +773,11 @@ 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_one_way_regions("Abyss right area", "Abyss right area, transturtle",
|
self.__connect_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
|
||||||
@@ -1092,12 +1087,10 @@ 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),
|
||||||
@@ -1151,6 +1144,10 @@ 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",
|
||||||
|
|||||||
@@ -130,12 +130,13 @@ class AquariaWorld(World):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None:
|
def __pre_fill_item(self, item_name: str, location_name: str, precollected,
|
||||||
|
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.useful, data.id, self.player)
|
item = AquariaItem(item_name, itemClassification, 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):
|
||||||
@@ -164,7 +165,8 @@ 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:
|
||||||
@@ -212,4 +214,8 @@ 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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
## 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
|
||||||
|
|
||||||
@@ -113,3 +115,16 @@ 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.
|
||||||
|
|||||||
@@ -2,9 +2,14 @@
|
|||||||
|
|
||||||
## Logiciels nécessaires
|
## Logiciels nécessaires
|
||||||
|
|
||||||
- Le jeu Aquaria original (trouvable sur la majorité des sites de ventes de jeux vidéo en ligne)
|
- Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
|
||||||
- Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases)
|
- Le client du Randomizer d'Aquaria [Aquaria randomizer]
|
||||||
|
(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
|
||||||
|
|
||||||
@@ -116,3 +121,15 @@ 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.
|
||||||
|
|||||||
@@ -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("Booster Bumper", self.player, 5) and \
|
lambda state: state.has_all_counts({"Booster Bumper": 5, "Treasure Bumper": 32, "Hazard Bumper": 25}, \
|
||||||
state.has("Treasure Bumper", self.player, 32)
|
self.player)
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ 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.
|
||||||
@@ -294,6 +295,7 @@ 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:
|
||||||
@@ -1305,7 +1307,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 not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return
|
if region not in self.created_regions: 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
|
||||||
|
|||||||
@@ -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/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
|
- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest)
|
||||||
|
|
||||||
## Optional Software
|
## Optional Software
|
||||||
|
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
|
|
||||||
## Setting Up
|
## Setting Up
|
||||||
|
|
||||||
First, download the client from the link above. It doesn't need to go into any particular directory;
|
First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go
|
||||||
it'll automatically locate _Dark Souls III_ in your Steam installation folder.
|
into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam
|
||||||
|
installation folder.
|
||||||
|
|
||||||
Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This
|
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
|
||||||
@@ -35,8 +36,9 @@ randomized item and (optionally) enemy locations. You only need to do this once
|
|||||||
|
|
||||||
To run _Dark Souls III_ in Archipelago mode:
|
To run _Dark Souls III_ in Archipelago mode:
|
||||||
|
|
||||||
1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the
|
1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain
|
||||||
DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn.
|
scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu
|
||||||
|
screen.
|
||||||
|
|
||||||
2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that
|
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.
|
||||||
@@ -52,4 +54,21 @@ 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.
|
configure your personal options and export them into a config file. The [AP client archive] also
|
||||||
|
includes an options template.
|
||||||
|
|
||||||
|
[AP client archive]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest
|
||||||
|
|
||||||
|
### Does this work with Proton?
|
||||||
|
|
||||||
|
The *Dark Souls III* Archipelago randomizer supports running on Linux under Proton. There are a few
|
||||||
|
things to keep in mind:
|
||||||
|
|
||||||
|
* Because `DS3Randomizer.exe` relies on the .NET runtime, you'll need to install
|
||||||
|
the [.NET Runtime] under **plain [WINE]**, then run `DS3Randomizer.exe` under
|
||||||
|
plain WINE as well. It won't work as a Proton app!
|
||||||
|
|
||||||
|
* To run the game itself, just run `launchmod_darksouls3.bat` under Proton.
|
||||||
|
|
||||||
|
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
|
||||||
|
[WINE]: https://www.winehq.org/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -534,26 +534,16 @@ 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)
|
||||||
except TypeError:
|
|
||||||
pass # C# side is currently typed as dict[str, int], drop what doesn't fit
|
|
||||||
else:
|
|
||||||
options[option_name] = optionvalue
|
options[option_name] = optionvalue
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
# 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):
|
||||||
@@ -566,7 +556,7 @@ class HKWorld(World):
|
|||||||
|
|
||||||
slot_data["grub_count"] = self.grub_count
|
slot_data["grub_count"] = self.grub_count
|
||||||
|
|
||||||
slot_data["is_race"] = int(self.settings.disable_spoilers or self.multiworld.is_race)
|
slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race
|
||||||
|
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
patch = KDL3ProcedurePatch(player=self.player, player_name=self.player_name)
|
||||||
patch_rom(self, patch)
|
patch_rom(self, patch)
|
||||||
|
|
||||||
self.rom_name = patch.name
|
self.rom_name = patch.name
|
||||||
|
|||||||
@@ -101,7 +101,18 @@ 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("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired")
|
slot_data = self.options.as_dict(
|
||||||
|
"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,
|
||||||
|
|||||||
@@ -81,23 +81,23 @@ talking:
|
|||||||
|
|
||||||
; Give powder
|
; Give powder
|
||||||
ld a, [$DB4C]
|
ld a, [$DB4C]
|
||||||
cp $10
|
cp $20
|
||||||
jr nc, doNotGivePowder
|
jr nc, doNotGivePowder
|
||||||
ld a, $10
|
ld a, $20
|
||||||
ld [$DB4C], a
|
ld [$DB4C], a
|
||||||
doNotGivePowder:
|
doNotGivePowder:
|
||||||
|
|
||||||
ld a, [$DB4D]
|
ld a, [$DB4D]
|
||||||
cp $10
|
cp $30
|
||||||
jr nc, doNotGiveBombs
|
jr nc, doNotGiveBombs
|
||||||
ld a, $10
|
ld a, $30
|
||||||
ld [$DB4D], a
|
ld [$DB4D], a
|
||||||
doNotGiveBombs:
|
doNotGiveBombs:
|
||||||
|
|
||||||
ld a, [$DB45]
|
ld a, [$DB45]
|
||||||
cp $10
|
cp $30
|
||||||
jr nc, doNotGiveArrows
|
jr nc, doNotGiveArrows
|
||||||
ld a, $10
|
ld a, $30
|
||||||
ld [$DB45], a
|
ld [$DB45], a
|
||||||
doNotGiveArrows:
|
doNotGiveArrows:
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ You can find items wherever items can be picked up in the original game. This in
|
|||||||
|
|
||||||
When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a
|
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. The groups you can use for The Messenger are:
|
for it.
|
||||||
|
|
||||||
|
The groups you can use for The Messenger are:
|
||||||
* Notes - This covers the music notes
|
* 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
|
||||||
@@ -50,26 +52,26 @@ for it. The groups you can use for The Messenger are:
|
|||||||
|
|
||||||
* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu
|
* 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?
|
||||||
|
|
||||||
|
|||||||
@@ -41,14 +41,27 @@ 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
|
||||||
|
|||||||
@@ -220,6 +220,8 @@ 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,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -89,3 +90,15 @@ 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))
|
||||||
|
|||||||
@@ -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.plando_connections:
|
for conn in self.options.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
|
||||||
|
|||||||
@@ -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, 1),
|
"Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
||||||
"Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1),
|
"Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
||||||
"Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression, 1),
|
"Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
||||||
"Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression, 1),
|
"Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
||||||
"Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression, 1),
|
"Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
||||||
"Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1),
|
"Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
||||||
"All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1),
|
"All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression | ItemClassification.useful, 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),
|
||||||
|
|||||||
@@ -184,6 +184,10 @@ 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()
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
|
|
||||||
### 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.
|
||||||
|
|||||||
@@ -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.starter_blacklist.value:
|
if "_Legendaries" in self.options.trainer_party_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
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ LEGENDARY_NAMES = {k.lower(): v for k, v in {
|
|||||||
DEFEATED_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_DEFEATED_{name}"]: name for name in LEGENDARY_NAMES.values()}
|
DEFEATED_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_DEFEATED_{name}"]: name for name in LEGENDARY_NAMES.values()}
|
||||||
CAUGHT_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_CAUGHT_{name}"]: name for name in LEGENDARY_NAMES.values()}
|
CAUGHT_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_CAUGHT_{name}"]: name for name in LEGENDARY_NAMES.values()}
|
||||||
|
|
||||||
|
SHOAL_CAVE_MAPS = tuple(data.constants[map_name] for map_name in [
|
||||||
|
"MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM",
|
||||||
|
"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class PokemonEmeraldClient(BizHawkClient):
|
class PokemonEmeraldClient(BizHawkClient):
|
||||||
game = "Pokemon Emerald"
|
game = "Pokemon Emerald"
|
||||||
@@ -414,13 +419,17 @@ class PokemonEmeraldClient(BizHawkClient):
|
|||||||
|
|
||||||
read_result = await bizhawk.guarded_read(
|
read_result = await bizhawk.guarded_read(
|
||||||
ctx.bizhawk_ctx,
|
ctx.bizhawk_ctx,
|
||||||
[(sb1_address + 0x4, 2, "System Bus")],
|
[
|
||||||
[guards["SAVE BLOCK 1"]]
|
(sb1_address + 0x4, 2, "System Bus"), # Current map
|
||||||
|
(sb1_address + 0x1450 + (data.constants["FLAG_SYS_SHOAL_TIDE"] // 8), 1, "System Bus"),
|
||||||
|
],
|
||||||
|
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
|
||||||
)
|
)
|
||||||
if read_result is None: # Save block moved
|
if read_result is None: # Save block moved
|
||||||
return
|
return
|
||||||
|
|
||||||
current_map = int.from_bytes(read_result[0], "big")
|
current_map = int.from_bytes(read_result[0], "big")
|
||||||
|
shoal_cave = int(read_result[1][0] & (1 << (data.constants["FLAG_SYS_SHOAL_TIDE"] % 8)) > 0)
|
||||||
if current_map != self.current_map:
|
if current_map != self.current_map:
|
||||||
self.current_map = current_map
|
self.current_map = current_map
|
||||||
await ctx.send_msgs([{
|
await ctx.send_msgs([{
|
||||||
@@ -429,6 +438,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
|||||||
"data": {
|
"data": {
|
||||||
"type": "MapUpdate",
|
"type": "MapUpdate",
|
||||||
"mapId": current_map,
|
"mapId": current_map,
|
||||||
|
**({"tide": shoal_cave} if current_map in SHOAL_CAVE_MAPS else {}),
|
||||||
},
|
},
|
||||||
}])
|
}])
|
||||||
|
|
||||||
@@ -545,11 +555,12 @@ class PokemonEmeraldClient(BizHawkClient):
|
|||||||
if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2:
|
if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2:
|
||||||
# Game has wonder trade data to send. Send it to data storage, remove it from the game's memory,
|
# Game has wonder trade data to send. Send it to data storage, remove it from the game's memory,
|
||||||
# and mark that the game is waiting on receiving a trade
|
# and mark that the game is waiting on receiving a trade
|
||||||
Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
|
success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [
|
||||||
await bizhawk.write(ctx.bizhawk_ctx, [
|
|
||||||
(sb1_address + 0x377C, bytes(0x50), "System Bus"),
|
(sb1_address + 0x377C, bytes(0x50), "System Bus"),
|
||||||
(sb1_address + 0x37CC, [1], "System Bus"),
|
(sb1_address + 0x37CC, [1], "System Bus"),
|
||||||
])
|
], [guards["SAVE BLOCK 1"]])
|
||||||
|
if success:
|
||||||
|
Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
|
||||||
elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2:
|
elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2:
|
||||||
# Game is waiting on receiving a trade.
|
# Game is waiting on receiving a trade.
|
||||||
if self.queued_received_trade is not None:
|
if self.queued_received_trade is not None:
|
||||||
|
|||||||
@@ -1274,16 +1274,16 @@ item_table = {
|
|||||||
description="Defensive structure. Slows the attack and movement speeds of all nearby Zerg units."),
|
description="Defensive structure. Slows the attack and movement speeds of all nearby Zerg units."),
|
||||||
ItemNames.STRUCTURE_ARMOR:
|
ItemNames.STRUCTURE_ARMOR:
|
||||||
ItemData(620 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9, SC2Race.TERRAN,
|
ItemData(620 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9, SC2Race.TERRAN,
|
||||||
description="Increases armor of all Terran structures by 2."),
|
description="Increases armor of all Terran structures by 2.", origin={"ext"}),
|
||||||
ItemNames.HI_SEC_AUTO_TRACKING:
|
ItemNames.HI_SEC_AUTO_TRACKING:
|
||||||
ItemData(621 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, SC2Race.TERRAN,
|
ItemData(621 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, SC2Race.TERRAN,
|
||||||
description="Increases attack range of all Terran structures by 1."),
|
description="Increases attack range of all Terran structures by 1.", origin={"ext"}),
|
||||||
ItemNames.ADVANCED_OPTICS:
|
ItemNames.ADVANCED_OPTICS:
|
||||||
ItemData(622 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, SC2Race.TERRAN,
|
ItemData(622 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, SC2Race.TERRAN,
|
||||||
description="Increases attack range of all Terran mechanical units by 1."),
|
description="Increases attack range of all Terran mechanical units by 1.", origin={"ext"}),
|
||||||
ItemNames.ROGUE_FORCES:
|
ItemNames.ROGUE_FORCES:
|
||||||
ItemData(623 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, SC2Race.TERRAN,
|
ItemData(623 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, SC2Race.TERRAN,
|
||||||
description="Mercenary calldowns are no longer limited by charges."),
|
description="Mercenary calldowns are no longer limited by charges.", origin={"ext"}),
|
||||||
|
|
||||||
ItemNames.ZEALOT:
|
ItemNames.ZEALOT:
|
||||||
ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.PROTOSS,
|
ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.PROTOSS,
|
||||||
@@ -2369,7 +2369,8 @@ progressive_if_ext = {
|
|||||||
ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS,
|
ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS,
|
||||||
ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL,
|
ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL,
|
||||||
ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM,
|
ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM,
|
||||||
ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL
|
ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL,
|
||||||
|
ItemNames.PROGRESSIVE_ORBITAL_COMMAND
|
||||||
}
|
}
|
||||||
|
|
||||||
kerrigan_actives: typing.List[typing.Set[str]] = [
|
kerrigan_actives: typing.List[typing.Set[str]] = [
|
||||||
|
|||||||
@@ -77,9 +77,6 @@ Should your name or password have spaces, enclose it in quotes: `"YourPassword"`
|
|||||||
Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that.
|
Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that.
|
||||||
Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text.
|
Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text.
|
||||||
|
|
||||||
**Important:** You must start a new file for every new seed you play. Using `⭐x0` files is **not** sufficient.
|
|
||||||
Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file.
|
|
||||||
|
|
||||||
### Playing offline
|
### Playing offline
|
||||||
|
|
||||||
To play offline, first generate a seed on the game's options page.
|
To play offline, first generate a seed on the game's options page.
|
||||||
@@ -129,18 +126,6 @@ To use this batch file, double-click it. A window will open. Type the five-digi
|
|||||||
Once you provide those two bits of information, the game will open.
|
Once you provide those two bits of information, the game will open.
|
||||||
- If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail.
|
- If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail.
|
||||||
|
|
||||||
### Addendum - Deleting old saves
|
|
||||||
|
|
||||||
Loading an old Mario save alongside a new seed is a bad idea, as it can cause locked doors and castle secret stars to already be unlocked / obtained. You should avoid opening a save that says "Stars x 0" as opposed to one that simply says "New".
|
|
||||||
|
|
||||||
You can manually delete these old saves in-game before starting a new game, but that can be tedious. With a small edit to the batch files, you can delete these old saves automatically. Just add the line `del %AppData%\sm64ex\*.bin` to the batch file, above the `start` command. For example, here is `offline.bat` with the additional line:
|
|
||||||
|
|
||||||
`del %AppData%\sm64ex\*.bin`
|
|
||||||
|
|
||||||
`start sm64.us.f3dex2e.exe --sm64ap_file %1`
|
|
||||||
|
|
||||||
This extra line deletes any previous save data before opening the game. Don't worry about lost stars or checks - the AP server (or in the case of offline, the `.save` file) keeps track of your star count, unlocked keys/caps/cannons, and which locations have already been checked, so you won't have to redo them. At worst you'll have to rewatch the door unlocking animations, and catch the rabbit Mips twice for his first star again if you haven't yet collected the second one.
|
|
||||||
|
|
||||||
## Installation Troubleshooting
|
## Installation Troubleshooting
|
||||||
|
|
||||||
Start the game from the command line to view helpful messages regarding SM64EX.
|
Start the game from the command line to view helpful messages regarding SM64EX.
|
||||||
@@ -166,8 +151,9 @@ The Japanese Version should have no problem displaying these.
|
|||||||
|
|
||||||
### Toad does not have an item for me.
|
### Toad does not have an item for me.
|
||||||
|
|
||||||
This happens when you load an existing file that had already received an item from that toad.
|
This happens on older builds when you load an existing file that had already received an item from that toad.
|
||||||
To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress.
|
To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress.
|
||||||
|
Alternatively, updating your build will prevent this issue in the future.
|
||||||
|
|
||||||
### What happens if I lose connection?
|
### What happens if I lose connection?
|
||||||
|
|
||||||
|
|||||||
@@ -819,6 +819,7 @@ id,name,classification,groups,mod_name
|
|||||||
5289,Prismatic Shard,filler,"RESOURCE_PACK",
|
5289,Prismatic Shard,filler,"RESOURCE_PACK",
|
||||||
5290,Stardrop Tea,filler,"RESOURCE_PACK",
|
5290,Stardrop Tea,filler,"RESOURCE_PACK",
|
||||||
5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK",
|
5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK",
|
||||||
|
5292,Resource Pack: 20 Cinder Shard,filler,"GINGER_ISLAND,RESOURCE_PACK",
|
||||||
10001,Luck Level,progression,SKILL_LEVEL_UP,Luck Skill
|
10001,Luck Level,progression,SKILL_LEVEL_UP,Luck Skill
|
||||||
10002,Magic Level,progression,SKILL_LEVEL_UP,Magic
|
10002,Magic Level,progression,SKILL_LEVEL_UP,Magic
|
||||||
10003,Socializing Level,progression,SKILL_LEVEL_UP,Socializing Skill
|
10003,Socializing Level,progression,SKILL_LEVEL_UP,Socializing Skill
|
||||||
|
|||||||
|
@@ -207,7 +207,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
|
|||||||
LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)),
|
LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)),
|
||||||
LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)),
|
LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)),
|
||||||
# 1337158 Is lost in time
|
# 1337158 Is lost in time
|
||||||
LocationData('Library', 'Library: Terminal 3 (Emporer Nuvius)', 1337159, lambda state: state.has('Tablet', player)),
|
LocationData('Library', 'Library: Terminal 3 (Emperor Nuvius)', 1337159, lambda state: state.has('Tablet', player)),
|
||||||
LocationData('Library', 'Library: V terminal 1 (War of the Sisters)', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
LocationData('Library', 'Library: V terminal 1 (War of the Sisters)', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||||
LocationData('Library', 'Library: V terminal 2 (Lake Desolation Map)', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
LocationData('Library', 'Library: V terminal 2 (Lake Desolation Map)', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||||
LocationData('Library', 'Library: V terminal 3 (Vilete)', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
LocationData('Library', 'Library: V terminal 3 (Vilete)', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)),
|
||||||
|
|||||||
@@ -417,13 +417,16 @@ class HiddenTraps(Traps):
|
|||||||
"""List of traps that may be in the item pool to find"""
|
"""List of traps that may be in the item pool to find"""
|
||||||
visibility = Visibility.none
|
visibility = Visibility.none
|
||||||
|
|
||||||
class OptionsHider:
|
class HiddenDeathLink(DeathLink):
|
||||||
@classmethod
|
"""When you die, everyone who enabled death link dies. Of course, the reverse is true too."""
|
||||||
def hidden(cls, option: Type[Option[Any]]) -> Type[Option]:
|
visibility = Visibility.none
|
||||||
new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy())
|
|
||||||
new_option.visibility = Visibility.none
|
def hidden(option: Type[Option[Any]]) -> Type[Option]:
|
||||||
new_option.__doc__ = option.__doc__
|
new_option = AssembleOptions(f"{option.__name__}Hidden", option.__bases__, vars(option).copy())
|
||||||
return new_option
|
new_option.visibility = Visibility.none
|
||||||
|
new_option.__doc__ = option.__doc__
|
||||||
|
globals()[f"{option.__name__}Hidden"] = new_option
|
||||||
|
return new_option
|
||||||
|
|
||||||
class HasReplacedCamelCase(Toggle):
|
class HasReplacedCamelCase(Toggle):
|
||||||
"""For internal use will display a warning message if true"""
|
"""For internal use will display a warning message if true"""
|
||||||
@@ -431,41 +434,41 @@ class HasReplacedCamelCase(Toggle):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
|
class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
|
||||||
StartWithJewelryBox: OptionsHider.hidden(StartWithJewelryBox) # type: ignore
|
StartWithJewelryBox: hidden(StartWithJewelryBox) # type: ignore
|
||||||
DownloadableItems: OptionsHider.hidden(DownloadableItems) # type: ignore
|
DownloadableItems: hidden(DownloadableItems) # type: ignore
|
||||||
EyeSpy: OptionsHider.hidden(EyeSpy) # type: ignore
|
EyeSpy: hidden(EyeSpy) # type: ignore
|
||||||
StartWithMeyef: OptionsHider.hidden(StartWithMeyef) # type: ignore
|
StartWithMeyef: hidden(StartWithMeyef) # type: ignore
|
||||||
QuickSeed: OptionsHider.hidden(QuickSeed) # type: ignore
|
QuickSeed: hidden(QuickSeed) # type: ignore
|
||||||
SpecificKeycards: OptionsHider.hidden(SpecificKeycards) # type: ignore
|
SpecificKeycards: hidden(SpecificKeycards) # type: ignore
|
||||||
Inverted: OptionsHider.hidden(Inverted) # type: ignore
|
Inverted: hidden(Inverted) # type: ignore
|
||||||
GyreArchives: OptionsHider.hidden(GyreArchives) # type: ignore
|
GyreArchives: hidden(GyreArchives) # type: ignore
|
||||||
Cantoran: OptionsHider.hidden(Cantoran) # type: ignore
|
Cantoran: hidden(Cantoran) # type: ignore
|
||||||
LoreChecks: OptionsHider.hidden(LoreChecks) # type: ignore
|
LoreChecks: hidden(LoreChecks) # type: ignore
|
||||||
BossRando: OptionsHider.hidden(BossRando) # type: ignore
|
BossRando: hidden(BossRando) # type: ignore
|
||||||
DamageRando: OptionsHider.hidden(DamageRando) # type: ignore
|
DamageRando: hidden(DamageRando) # type: ignore
|
||||||
DamageRandoOverrides: HiddenDamageRandoOverrides
|
DamageRandoOverrides: HiddenDamageRandoOverrides
|
||||||
HpCap: OptionsHider.hidden(HpCap) # type: ignore
|
HpCap: hidden(HpCap) # type: ignore
|
||||||
LevelCap: OptionsHider.hidden(LevelCap) # type: ignore
|
LevelCap: hidden(LevelCap) # type: ignore
|
||||||
ExtraEarringsXP: OptionsHider.hidden(ExtraEarringsXP) # type: ignore
|
ExtraEarringsXP: hidden(ExtraEarringsXP) # type: ignore
|
||||||
BossHealing: OptionsHider.hidden(BossHealing) # type: ignore
|
BossHealing: hidden(BossHealing) # type: ignore
|
||||||
ShopFill: OptionsHider.hidden(ShopFill) # type: ignore
|
ShopFill: hidden(ShopFill) # type: ignore
|
||||||
ShopWarpShards: OptionsHider.hidden(ShopWarpShards) # type: ignore
|
ShopWarpShards: hidden(ShopWarpShards) # type: ignore
|
||||||
ShopMultiplier: OptionsHider.hidden(ShopMultiplier) # type: ignore
|
ShopMultiplier: hidden(ShopMultiplier) # type: ignore
|
||||||
LootPool: OptionsHider.hidden(LootPool) # type: ignore
|
LootPool: hidden(LootPool) # type: ignore
|
||||||
DropRateCategory: OptionsHider.hidden(DropRateCategory) # type: ignore
|
DropRateCategory: hidden(DropRateCategory) # type: ignore
|
||||||
FixedDropRate: OptionsHider.hidden(FixedDropRate) # type: ignore
|
FixedDropRate: hidden(FixedDropRate) # type: ignore
|
||||||
LootTierDistro: OptionsHider.hidden(LootTierDistro) # type: ignore
|
LootTierDistro: hidden(LootTierDistro) # type: ignore
|
||||||
ShowBestiary: OptionsHider.hidden(ShowBestiary) # type: ignore
|
ShowBestiary: hidden(ShowBestiary) # type: ignore
|
||||||
ShowDrops: OptionsHider.hidden(ShowDrops) # type: ignore
|
ShowDrops: hidden(ShowDrops) # type: ignore
|
||||||
EnterSandman: OptionsHider.hidden(EnterSandman) # type: ignore
|
EnterSandman: hidden(EnterSandman) # type: ignore
|
||||||
DadPercent: OptionsHider.hidden(DadPercent) # type: ignore
|
DadPercent: hidden(DadPercent) # type: ignore
|
||||||
RisingTides: OptionsHider.hidden(RisingTides) # type: ignore
|
RisingTides: hidden(RisingTides) # type: ignore
|
||||||
RisingTidesOverrides: HiddenRisingTidesOverrides
|
RisingTidesOverrides: HiddenRisingTidesOverrides
|
||||||
UnchainedKeys: OptionsHider.hidden(UnchainedKeys) # type: ignore
|
UnchainedKeys: hidden(UnchainedKeys) # type: ignore
|
||||||
PresentAccessWithWheelAndSpindle: OptionsHider.hidden(PresentAccessWithWheelAndSpindle) # type: ignore
|
PresentAccessWithWheelAndSpindle: hidden(PresentAccessWithWheelAndSpindle) # type: ignore
|
||||||
TrapChance: OptionsHider.hidden(TrapChance) # type: ignore
|
TrapChance: hidden(TrapChance) # type: ignore
|
||||||
Traps: HiddenTraps # type: ignore
|
Traps: HiddenTraps # type: ignore
|
||||||
DeathLink: OptionsHider.hidden(DeathLink) # type: ignore
|
DeathLink: HiddenDeathLink # type: ignore
|
||||||
has_replaced_options: HasReplacedCamelCase
|
has_replaced_options: HasReplacedCamelCase
|
||||||
|
|
||||||
def handle_backward_compatibility(self) -> None:
|
def handle_backward_compatibility(self) -> None:
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class TimespinnerWorld(World):
|
|||||||
|
|
||||||
if self.options.has_replaced_options:
|
if self.options.has_replaced_options:
|
||||||
warning = \
|
warning = \
|
||||||
f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \
|
f"NOTICE: Timespinner options for player '{self.player_name}' were renamed from PascalCase to snake_case, " \
|
||||||
"please update your yaml"
|
"please update your yaml"
|
||||||
|
|
||||||
spoiler_handle.write("\n")
|
spoiler_handle.write("\n")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from .data import static_items as static_witness_items
|
|||||||
from .data import static_locations as static_witness_locations
|
from .data import static_locations as static_witness_locations
|
||||||
from .data import static_logic as static_witness_logic
|
from .data import static_logic as static_witness_logic
|
||||||
from .data.item_definition_classes import DoorItemDefinition, ItemData
|
from .data.item_definition_classes import DoorItemDefinition, ItemData
|
||||||
from .data.utils import get_audio_logs
|
from .data.utils import cast_not_none, get_audio_logs
|
||||||
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
|
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
|
||||||
from .locations import WitnessPlayerLocations
|
from .locations import WitnessPlayerLocations
|
||||||
from .options import TheWitnessOptions, witness_option_groups
|
from .options import TheWitnessOptions, witness_option_groups
|
||||||
@@ -55,7 +55,7 @@ class WitnessWorld(World):
|
|||||||
|
|
||||||
item_name_to_id = {
|
item_name_to_id = {
|
||||||
# ITEM_DATA doesn't have any event items in it
|
# ITEM_DATA doesn't have any event items in it
|
||||||
name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
|
name: cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
|
||||||
}
|
}
|
||||||
location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
|
location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
|
||||||
item_name_groups = static_witness_items.ITEM_GROUPS
|
item_name_groups = static_witness_items.ITEM_GROUPS
|
||||||
@@ -336,7 +336,7 @@ class WitnessWorld(World):
|
|||||||
for item_name, hint in laser_hints.items():
|
for item_name, hint in laser_hints.items():
|
||||||
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
|
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
|
||||||
self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
|
self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
|
||||||
already_hinted_locations.add(cast(Location, hint.location))
|
already_hinted_locations.add(cast_not_none(hint.location))
|
||||||
|
|
||||||
# Audio Log Hints
|
# Audio Log Hints
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from math import floor
|
from math import floor
|
||||||
from pkgutil import get_data
|
from pkgutil import get_data
|
||||||
from random import Random
|
from random import Random
|
||||||
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar
|
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@@ -13,6 +13,11 @@ T = TypeVar("T")
|
|||||||
WitnessRule = FrozenSet[FrozenSet[str]]
|
WitnessRule = FrozenSet[FrozenSet[str]]
|
||||||
|
|
||||||
|
|
||||||
|
def cast_not_none(value: Optional[T]) -> T:
|
||||||
|
assert value is not None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]:
|
def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]:
|
||||||
positions = range(len(population))
|
positions = range(len(population))
|
||||||
indices: List[int] = []
|
indices: List[int] = []
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from .data.item_definition_classes import (
|
|||||||
ProgressiveItemDefinition,
|
ProgressiveItemDefinition,
|
||||||
WeightedItemDefinition,
|
WeightedItemDefinition,
|
||||||
)
|
)
|
||||||
from .data.utils import build_weighted_int_list
|
from .data.utils import build_weighted_int_list, cast_not_none
|
||||||
from .locations import WitnessPlayerLocations
|
from .locations import WitnessPlayerLocations
|
||||||
from .player_logic import WitnessPlayerLogic
|
from .player_logic import WitnessPlayerLogic
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ class WitnessPlayerItems:
|
|||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
# data.ap_code is guaranteed for a symbol definition
|
# data.ap_code is guaranteed for a symbol definition
|
||||||
cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
|
cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items()
|
||||||
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
|
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -211,8 +211,8 @@ class WitnessPlayerItems:
|
|||||||
if isinstance(item.definition, ProgressiveItemDefinition):
|
if isinstance(item.definition, ProgressiveItemDefinition):
|
||||||
# Note: we need to reference the static table here rather than the player-specific one because the child
|
# Note: we need to reference the static table here rather than the player-specific one because the child
|
||||||
# items were removed from the pool when we pruned out all progression items not in the options.
|
# items were removed from the pool when we pruned out all progression items not in the options.
|
||||||
output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code)
|
output[cast_not_none(item.ap_code)] = [cast_not_none(static_witness_items.ITEM_DATA[child_item].ap_code)
|
||||||
for child_item in item.definition.child_item_names]
|
for child_item in item.definition.child_item_names]
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast
|
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Entrance, Item, Location, Region
|
from BaseClasses import CollectionState, Entrance, Item, Location, Region
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ from test.general import gen_steps, setup_multiworld
|
|||||||
from test.multiworld.test_multiworlds import MultiworldTestBase
|
from test.multiworld.test_multiworlds import MultiworldTestBase
|
||||||
|
|
||||||
from .. import WitnessWorld
|
from .. import WitnessWorld
|
||||||
|
from ..data.utils import cast_not_none
|
||||||
|
|
||||||
|
|
||||||
class WitnessTestBase(WorldTestBase):
|
class WitnessTestBase(WorldTestBase):
|
||||||
@@ -32,7 +33,7 @@ class WitnessTestBase(WorldTestBase):
|
|||||||
event_items = [item for item in self.multiworld.get_items() if item.name == item_name]
|
event_items = [item for item in self.multiworld.get_items() if item.name == item_name]
|
||||||
self.assertTrue(event_items, f"Event item {item_name} does not exist.")
|
self.assertTrue(event_items, f"Event item {item_name} does not exist.")
|
||||||
|
|
||||||
event_locations = [cast(Location, event_item.location) for event_item in event_items]
|
event_locations = [cast_not_none(event_item.location) for event_item in event_items]
|
||||||
|
|
||||||
# Checking for an access dependency on an event item requires a bit of extra work,
|
# Checking for an access dependency on an event item requires a bit of extra work,
|
||||||
# as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it.
|
# as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class YachtDiceItem(Item):
|
|||||||
|
|
||||||
|
|
||||||
item_table = {
|
item_table = {
|
||||||
"Dice": ItemData(16871244000, ItemClassification.progression),
|
"Dice": ItemData(16871244000, ItemClassification.progression | ItemClassification.useful),
|
||||||
"Dice Fragment": ItemData(16871244001, ItemClassification.progression),
|
"Dice Fragment": ItemData(16871244001, ItemClassification.progression),
|
||||||
"Roll": ItemData(16871244002, ItemClassification.progression),
|
"Roll": ItemData(16871244002, ItemClassification.progression),
|
||||||
"Roll Fragment": ItemData(16871244003, ItemClassification.progression),
|
"Roll Fragment": ItemData(16871244003, ItemClassification.progression),
|
||||||
@@ -64,7 +64,7 @@ item_table = {
|
|||||||
# These points are included in the logic and might be necessary to progress.
|
# These points are included in the logic and might be necessary to progress.
|
||||||
"1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing),
|
"1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing),
|
||||||
"10 Points": ItemData(16871244302, ItemClassification.progression),
|
"10 Points": ItemData(16871244302, ItemClassification.progression),
|
||||||
"100 Points": ItemData(16871244303, ItemClassification.progression),
|
"100 Points": ItemData(16871244303, ItemClassification.progression | ItemClassification.useful),
|
||||||
}
|
}
|
||||||
|
|
||||||
# item groups for better hinting
|
# item groups for better hinting
|
||||||
|
|||||||