Compare commits

..

3 Commits

Author SHA1 Message Date
Fabian Dill
1169e62191 pickle is no longer used directly 2025-10-02 00:18:33 +02:00
Fabian Dill
d0bd1d29b1 Merge branch 'Archipelago_Main' into webhost_queue_display
# Conflicts:
#	WebHostLib/api/generate.py
2025-10-02 00:14:27 +02:00
Fabian Dill
d463faa9d9 WebHost: notify of current generation queue length 2025-05-11 15:03:04 +02:00
338 changed files with 2471 additions and 10960 deletions

View File

@@ -9,25 +9,22 @@ on:
- 'setup.py' - 'setup.py'
- 'requirements.txt' - 'requirements.txt'
- '*.iss' - '*.iss'
- 'worlds/*/archipelago.json'
pull_request: pull_request:
paths: paths:
- '.github/workflows/build.yml' - '.github/workflows/build.yml'
- 'setup.py' - 'setup.py'
- 'requirements.txt' - 'requirements.txt'
- '*.iss' - '*.iss'
- 'worlds/*/archipelago.json'
workflow_dispatch: workflow_dispatch:
env: env:
ENEMIZER_VERSION: 7.1 ENEMIZER_VERSION: 7.1
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore, # NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
# we check the sha256 and require manual intervention if it was updated. # we check the sha256 and require manual intervention if it was updated.
APPIMAGE_FORK: 'PopTracker' APPIMAGETOOL_VERSION: continuous
APPIMAGETOOL_VERSION: 'r-2025-11-18' APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2' APPIMAGE_RUNTIME_VERSION: continuous
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07' APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
permissions: # permissions required for attestation permissions: # permissions required for attestation
id-token: 'write' id-token: 'write'
@@ -142,9 +139,9 @@ jobs:
- name: Install build-time dependencies - name: Install build-time dependencies
run: | run: |
echo "PYTHON=python3.12" >> $GITHUB_ENV echo "PYTHON=python3.12" >> $GITHUB_ENV
wget -nv https://github.com/$APPIMAGE_FORK/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage wget -nv https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
echo "$APPIMAGETOOL_X86_64_HASH appimagetool-x86_64.AppImage" | sha256sum -c echo "$APPIMAGETOOL_X86_64_HASH appimagetool-x86_64.AppImage" | sha256sum -c
wget -nv https://github.com/$APPIMAGE_FORK/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-x86_64 wget -nv https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-x86_64
echo "$APPIMAGE_RUNTIME_X86_64_HASH runtime-x86_64" | sha256sum -c echo "$APPIMAGE_RUNTIME_X86_64_HASH runtime-x86_64" | sha256sum -c
chmod a+rx appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract ./appimagetool-x86_64.AppImage --appimage-extract

View File

@@ -11,7 +11,7 @@ on:
- "!.github/workflows/**" - "!.github/workflows/**"
- ".github/workflows/docker.yml" - ".github/workflows/docker.yml"
branches: branches:
- "main" - "*"
tags: tags:
- "v?[0-9]+.[0-9]+.[0-9]*" - "v?[0-9]+.[0-9]+.[0-9]*"
workflow_dispatch: workflow_dispatch:

View File

@@ -11,11 +11,10 @@ env:
ENEMIZER_VERSION: 7.1 ENEMIZER_VERSION: 7.1
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore, # NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
# we check the sha256 and require manual intervention if it was updated. # we check the sha256 and require manual intervention if it was updated.
APPIMAGE_FORK: 'PopTracker' APPIMAGETOOL_VERSION: continuous
APPIMAGETOOL_VERSION: 'r-2025-11-18' APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2' APPIMAGE_RUNTIME_VERSION: continuous
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07' APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
permissions: # permissions required for attestation permissions: # permissions required for attestation
id-token: 'write' id-token: 'write'
@@ -128,9 +127,9 @@ jobs:
- name: Install build-time dependencies - name: Install build-time dependencies
run: | run: |
echo "PYTHON=python3.12" >> $GITHUB_ENV echo "PYTHON=python3.12" >> $GITHUB_ENV
wget -nv https://github.com/$APPIMAGE_FORK/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage wget -nv https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
echo "$APPIMAGETOOL_X86_64_HASH appimagetool-x86_64.AppImage" | sha256sum -c echo "$APPIMAGETOOL_X86_64_HASH appimagetool-x86_64.AppImage" | sha256sum -c
wget -nv https://github.com/$APPIMAGE_FORK/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-x86_64 wget -nv https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-x86_64
echo "$APPIMAGE_RUNTIME_X86_64_HASH runtime-x86_64" | sha256sum -c echo "$APPIMAGE_RUNTIME_X86_64_HASH runtime-x86_64" | sha256sum -c
chmod a+rx appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract ./appimagetool-x86_64.AppImage --appimage-extract

View File

@@ -59,7 +59,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r ci-requirements.txt pip install pytest pytest-subtests pytest-xdist
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
python Launcher.py --update_settings # make sure host.yaml exists for tests python Launcher.py --update_settings # make sure host.yaml exists for tests
- name: Unittests - name: Unittests

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
<module name="Archipelago" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Launcher.py" />
<option name="PARAMETERS" value="&quot;Build APWorlds&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@@ -1346,7 +1346,8 @@ class Region:
for entrance in self.entrances: # BFS might be better here, trying DFS for now. for entrance in self.entrances: # BFS might be better here, trying DFS for now.
return entrance.parent_region.get_connecting_entrance(is_main_entrance) return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Mapping[str, int | None], location_type: type[Location] | None = None) -> None: def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[type[Location]] = None) -> None:
""" """
Adds locations to the Region object, where location_type is your Location class and locations is a dict of Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address. location names to address.
@@ -1434,8 +1435,8 @@ class Region:
entrance.connect(self) entrance.connect(self)
return entrance return entrance
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None], def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Mapping[str, Callable[[CollectionState], bool]] | None = None) -> List[Entrance]: rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
""" """
Connects current region to regions in exit dictionary. Passed region names must exist first. Connects current region to regions in exit dictionary. Passed region names must exist first.
@@ -1443,7 +1444,7 @@ class Region:
created entrances will be named "self.name -> connecting_region" created entrances will be named "self.name -> connecting_region"
:param rules: rules for the exits from this region. format is {"connecting_region": rule} :param rules: rules for the exits from this region. format is {"connecting_region": rule}
""" """
if not isinstance(exits, Mapping): if not isinstance(exits, Dict):
exits = dict.fromkeys(exits) exits = dict.fromkeys(exits)
return [ return [
self.connect( self.connect(
@@ -1721,10 +1722,9 @@ class Spoiler:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % ( logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates]) sphere_candidates])
if not multiworld.has_beaten_game(state): if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
raise RuntimeError("During playthrough generation, the game was determined to be unbeatable. " raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
"Something went terribly wrong here. " f'Something went terribly wrong here.')
f"Unreachable progression items: {sphere_candidates}")
else: else:
self.unreachables = sphere_candidates self.unreachables = sphere_candidates
break break
@@ -1858,9 +1858,6 @@ class Spoiler:
Utils.__version__, self.multiworld.seed)) Utils.__version__, self.multiworld.seed))
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm) outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
outfile.write('Players: %d\n' % self.multiworld.players) outfile.write('Players: %d\n' % self.multiworld.players)
if self.multiworld.players > 1:
loc_count = len([loc for loc in self.multiworld.get_locations() if not loc.is_event])
outfile.write('Total Location Count: %d\n' % loc_count)
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n') outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile) AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)
@@ -1869,9 +1866,6 @@ class Spoiler:
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
outfile.write('Game: %s\n' % self.multiworld.game[player]) outfile.write('Game: %s\n' % self.multiworld.game[player])
loc_count = len([loc for loc in self.multiworld.get_locations(player) if not loc.is_event])
outfile.write('Location Count: %d\n' % loc_count)
for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items(): for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items():
write_option(f_option, option) write_option(f_option, option)

10
CommonClient.py Executable file → Normal file
View File

@@ -323,7 +323,7 @@ class CommonContext:
hint_cost: int | None hint_cost: int | None
"""Current Hint Cost per Hint from the server""" """Current Hint Cost per Hint from the server"""
hint_points: int | None hint_points: int | None
"""Current available Hint Points from the server""" """Current avaliable Hint Points from the server"""
player_names: dict[int, str] player_names: dict[int, str]
"""Current lookup of slot number to player display name from server (includes aliases)""" """Current lookup of slot number to player display name from server (includes aliases)"""
@@ -572,10 +572,6 @@ class CommonContext:
return print_json_packet.get("type", "") == "ItemSend" \ return print_json_packet.get("type", "") == "ItemSend" \
and not self.slot_concerns_self(print_json_packet["receiving"]) \ and not self.slot_concerns_self(print_json_packet["receiving"]) \
and not self.slot_concerns_self(print_json_packet["item"].player) and not self.slot_concerns_self(print_json_packet["item"].player)
def is_connection_change(self, print_json_packet: dict) -> bool:
"""Helper function for filtering out connection changes."""
return print_json_packet.get("type", "") in ["Join","Part"]
def on_print(self, args: dict): def on_print(self, args: dict):
logger.info(args["text"]) logger.info(args["text"])
@@ -860,9 +856,9 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
server_url = urllib.parse.urlparse(address) server_url = urllib.parse.urlparse(address)
if server_url.username: if server_url.username:
ctx.username = urllib.parse.unquote(server_url.username) ctx.username = server_url.username
if server_url.password: if server_url.password:
ctx.password = urllib.parse.unquote(server_url.password) ctx.password = server_url.password
def reconnect_hint() -> str: def reconnect_hint() -> str:
return ", type /connect to reconnect" if ctx.server_address else "" return ", type /connect to reconnect" if ctx.server_address else ""

View File

@@ -129,10 +129,6 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
for i, location in enumerate(placements)) for i, location in enumerate(placements))
for (i, location, unsafe) in swap_attempts: for (i, location, unsafe) in swap_attempts:
placed_item = location.item placed_item = location.item
if item_to_place == placed_item:
# The number of allowed swaps is limited, so do not allow a swap of an item with a copy of
# itself.
continue
# Unplaceable items can sometimes be swapped infinitely. Limit the # Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this # number of times we will swap an individual item to prevent this
swap_count = swapped_items[placed_item.player, placed_item.name, unsafe] swap_count = swapped_items[placed_item.player, placed_item.name, unsafe]

View File

@@ -23,7 +23,7 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
from Utils import parse_yamls, version_tuple, __version__, tuplize_version from Utils import parse_yamls, version_tuple, __version__, tuplize_version
def mystery_argparse(argv: list[str] | None = None): def mystery_argparse():
from settings import get_settings from settings import get_settings
settings = get_settings() settings = get_settings()
defaults = settings.generator defaults = settings.generator
@@ -57,7 +57,7 @@ def mystery_argparse(argv: list[str] | None = None):
parser.add_argument("--spoiler_only", action="store_true", parser.add_argument("--spoiler_only", action="store_true",
help="Skips generation assertion and multidata, outputting only a spoiler log. " help="Skips generation assertion and multidata, outputting only a spoiler log. "
"Intended for debugging and testing purposes.") "Intended for debugging and testing purposes.")
args = parser.parse_args(argv) args = parser.parse_args()
if args.skip_output and args.spoiler_only: if args.skip_output and args.spoiler_only:
parser.error("Cannot mix --skip_output and --spoiler_only") parser.error("Cannot mix --skip_output and --spoiler_only")
@@ -189,11 +189,6 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
yaml[category][key] = option yaml[category][key] = option
elif category_name not in yaml: elif category_name not in yaml:
logging.warning(f"Meta: Category {category_name} is not present in {path}.") logging.warning(f"Meta: Category {category_name} is not present in {path}.")
elif key == "triggers":
if "triggers" not in yaml[category_name]:
yaml[category_name][key] = []
for trigger in option:
yaml[category_name][key].append(trigger)
else: else:
yaml[category_name][key] = option yaml[category_name][key] = option
@@ -347,9 +342,7 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
elif isinstance(new_value, list): elif isinstance(new_value, list):
cleaned_value.extend(new_value) cleaned_value.extend(new_value)
elif isinstance(new_value, dict): elif isinstance(new_value, dict):
counter_value = Counter(cleaned_value) cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
counter_value.update(new_value)
cleaned_value = dict(counter_value)
else: else:
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name}," raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
f" received {type(new_value).__name__}.") f" received {type(new_value).__name__}.")
@@ -363,18 +356,13 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
for element in new_value: for element in new_value:
cleaned_value.remove(element) cleaned_value.remove(element)
elif isinstance(new_value, dict): elif isinstance(new_value, dict):
counter_value = Counter(cleaned_value) cleaned_value = dict(Counter(cleaned_value) - Counter(new_value))
counter_value.subtract(new_value)
cleaned_value = dict(counter_value)
else: else:
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name}," raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
f" received {type(new_value).__name__}.") f" received {type(new_value).__name__}.")
cleaned_weights[option_name] = cleaned_value cleaned_weights[option_name] = cleaned_value
else: else:
# Options starting with + and - may modify values in-place, and new_weights may be shared by multiple slots cleaned_weights[option_name] = new_weights[option]
# using the same .yaml, so ensure that the new value is a copy.
cleaned_value = copy.deepcopy(new_weights[option])
cleaned_weights[option_name] = cleaned_value
new_options = set(cleaned_weights) - set(weights) new_options = set(cleaned_weights) - set(weights)
weights.update(cleaned_weights) weights.update(cleaned_weights)
if new_options: if new_options:
@@ -397,8 +385,6 @@ def roll_meta_option(option_key, game: str, category_dict: dict) -> Any:
if options[option_key].supports_weighting: if options[option_key].supports_weighting:
return get_choice(option_key, category_dict) return get_choice(option_key, category_dict)
return category_dict[option_key] return category_dict[option_key]
if option_key == "triggers":
return category_dict[option_key]
raise Options.OptionError(f"Error generating meta option {option_key} for {game}.") raise Options.OptionError(f"Error generating meta option {option_key} for {game}.")

View File

@@ -75,17 +75,12 @@ def open_patch():
launch([*exe, file], component.cli) launch([*exe, file], component.cli)
def generate_yamls(*args): def generate_yamls():
from Options import generate_yaml_templates from Options import generate_yaml_templates
parser = argparse.ArgumentParser(description="Generate Template Options", usage="[-h] [--skip_open_folder]")
parser.add_argument("--skip_open_folder", action="store_true")
args = parser.parse_args(args)
target = Utils.user_path("Players", "Templates") target = Utils.user_path("Players", "Templates")
generate_yaml_templates(target, False) generate_yaml_templates(target, False)
if not args.skip_open_folder: open_folder(target)
open_folder(target)
def browse_files(): def browse_files():

View File

@@ -3,6 +3,9 @@ ModuleUpdate.update()
import Utils import Utils
if __name__ == "__main__":
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
import asyncio import asyncio
import base64 import base64
import binascii import binascii
@@ -23,14 +26,16 @@ import typing
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger, from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
server_loop) server_loop)
from NetUtils import ClientStatus from NetUtils import ClientStatus
from . import LinksAwakeningWorld from worlds.ladx import LinksAwakeningWorld
from .Common import BASE_ID as LABaseID from worlds.ladx.Common import BASE_ID as LABaseID
from .GpsTracker import GpsTracker from worlds.ladx.GpsTracker import GpsTracker
from .TrackerConsts import storage_key from worlds.ladx.TrackerConsts import storage_key
from .ItemTracker import ItemTracker from worlds.ladx.ItemTracker import ItemTracker
from .LADXR.checkMetadata import checkMetadataTable from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from .Locations import get_locations_to_id, meta_to_name from worlds.ladx.Locations import get_locations_to_id, meta_to_name
from .Tracker import LocationTracker, MagpieBridge, Check from worlds.ladx.Tracker import LocationTracker, MagpieBridge, Check
class GameboyException(Exception): class GameboyException(Exception):
pass pass
@@ -47,10 +52,6 @@ class BadRetroArchResponse(GameboyException):
pass pass
class VersionError(Exception):
pass
class LAClientConstants: class LAClientConstants:
# Connector version # Connector version
VERSION = 0x01 VERSION = 0x01
@@ -140,7 +141,7 @@ class RAGameboy():
return response return response
async def async_recv(self, timeout=1.0): async def async_recv(self, timeout=1.0):
response = await asyncio.wait_for(asyncio.get_running_loop().sock_recv(self.socket, 4096), timeout) response = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 4096), timeout)
return response return response
async def check_safe_gameplay(self, throw=True): async def check_safe_gameplay(self, throw=True):
@@ -522,7 +523,7 @@ class LinksAwakeningContext(CommonContext):
("Client", "Archipelago"), ("Client", "Archipelago"),
("Tracker", "Tracker"), ("Tracker", "Tracker"),
] ]
base_title = f"Links Awakening DX Client {LinksAwakeningWorld.world_version.as_simple_string()} | Archipelago" base_title = "Archipelago Links Awakening DX Client"
def build(self): def build(self):
b = super().build() b = super().build()
@@ -618,20 +619,11 @@ class LinksAwakeningContext(CommonContext):
if cmd == "Connected": if cmd == "Connected":
self.game = self.slot_info[self.slot].game self.game = self.slot_info[self.slot].game
self.slot_data = args.get("slot_data", {}) self.slot_data = args.get("slot_data", {})
generated_version = Utils.tuplize_version(self.slot_data.get("world_version", "2.0.0"))
client_version = LinksAwakeningWorld.world_version
if generated_version.major != client_version.major:
self.disconnected_intentionally = True
raise VersionError(
f"The installed world ({client_version.as_simple_string()}) is incompatible with "
f"the world this game was generated on ({generated_version.as_simple_string()})"
)
# This is sent to magpie over local websocket to make its own connection # This is sent to magpie over local websocket to make its own connection
self.slot_data.update({ self.slot_data.update({
"server_address": self.server_address, "server_address": self.server_address,
"slot_name": self.player_names[self.slot], "slot_name": self.player_names[self.slot],
"password": self.password, "password": self.password,
"client_version": client_version.as_simple_string(),
}) })
# We can process linked items on already-checked checks now that we have slot_data # We can process linked items on already-checked checks now that we have slot_data
@@ -768,44 +760,42 @@ def run_game(romfile: str) -> None:
except FileNotFoundError: except FileNotFoundError:
logger.error(f"Couldn't launch ROM, {args[0]} is missing") logger.error(f"Couldn't launch ROM, {args[0]} is missing")
def launch(*launch_args): async def main():
async def main(): parser = get_base_parser(description="Link's Awakening Client.")
parser = get_base_parser(description="Link's Awakening Client.") parser.add_argument("--url", help="Archipelago connection url")
parser.add_argument("--url", help="Archipelago connection url") parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge") parser.add_argument('diff_file', default="", type=str, nargs="?",
parser.add_argument('diff_file', default="", type=str, nargs="?", help='Path to a .apladx Archipelago Binary Patch file')
help='Path to a .apladx Archipelago Binary Patch file')
args = parser.parse_args(launch_args) args = parser.parse_args()
if args.diff_file: if args.diff_file:
import Patch import Patch
logger.info("patch file was supplied - creating rom...") logger.info("patch file was supplied - creating rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file) meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta and not args.connect: if "server" in meta and not args.connect:
args.connect = meta["server"] args.connect = meta["server"]
logger.info(f"wrote rom file to {rom_file}") logger.info(f"wrote rom file to {rom_file}")
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie) ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
# TODO: nothing about the lambda about has to be in a lambda # TODO: nothing about the lambda about has to be in a lambda
ctx.la_task = create_task_log_exception(ctx.run_game_loop()) ctx.la_task = create_task_log_exception(ctx.run_game_loop())
if gui_enabled: if gui_enabled:
ctx.run_gui() ctx.run_gui()
ctx.run_cli() ctx.run_cli()
# Down below run_gui so that we get errors out of the process # Down below run_gui so that we get errors out of the process
if args.diff_file: if args.diff_file:
run_game(rom_file) run_game(rom_file)
await ctx.exit_event.wait() await ctx.exit_event.wait()
await ctx.shutdown() await ctx.shutdown()
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
if __name__ == '__main__':
colorama.just_fix_windows_console() colorama.just_fix_windows_console()
asyncio.run(main()) asyncio.run(main())
colorama.deinit() colorama.deinit()

15
Main.py
View File

@@ -54,16 +54,13 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:") logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
world_classes = AutoWorld.AutoWorldRegister.world_types.values() item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
version_count = max(len(cls.world_version.as_simple_string()) for cls in world_classes)
item_count = len(str(max(len(cls.item_names) for cls in world_classes)))
location_count = len(str(max(len(cls.location_names) for cls in world_classes)))
for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
if not cls.hidden and len(cls.item_names) > 0: if not cls.hidden and len(cls.item_names) > 0:
logger.info(f" {name:{longest_name}}: " logger.info(f" {name:{longest_name}}: "
f"v{cls.world_version.as_simple_string():{version_count}} | " f"v{cls.world_version.as_simple_string()} |"
f"Items: {len(cls.item_names):{item_count}} | " f"Items: {len(cls.item_names):{item_count}} | "
f"Locations: {len(cls.location_names):{location_count}}") f"Locations: {len(cls.location_names):{location_count}}")
@@ -326,7 +323,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
if current_sphere: if current_sphere:
spheres.append(dict(current_sphere)) spheres.append(dict(current_sphere))
multidata: NetUtils.MultiData = { multidata: NetUtils.MultiData | bytes = {
"slot_data": slot_data, "slot_data": slot_data,
"slot_info": slot_info, "slot_info": slot_info,
"connect_names": {name: (0, player) for player, name in multiworld.player_name.items()}, "connect_names": {name: (0, player) for player, name in multiworld.player_name.items()},
@@ -350,11 +347,11 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
for key in ("slot_data", "er_hint_data"): for key in ("slot_data", "er_hint_data"):
multidata[key] = convert_to_base_types(multidata[key]) multidata[key] = convert_to_base_types(multidata[key])
serialized_multidata = zlib.compress(restricted_dumps(multidata), 9) multidata = zlib.compress(restricted_dumps(multidata), 9)
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f: with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
f.write(bytes([3])) # version of format f.write(bytes([3])) # version of format
f.write(serialized_multidata) f.write(multidata)
output_file_futures.append(pool.submit(write_multidata)) output_file_futures.append(pool.submit(write_multidata))
if not check_accessibility_task.result(): if not check_accessibility_task.result():

View File

@@ -32,7 +32,7 @@ if typing.TYPE_CHECKING:
import colorama import colorama
import websockets import websockets
from websockets.extensions.permessage_deflate import PerMessageDeflate, ServerPerMessageDeflateFactory from websockets.extensions.permessage_deflate import PerMessageDeflate
try: try:
# ponyorm is a requirement for webhost, not default server, so may not be importable # ponyorm is a requirement for webhost, not default server, so may not be importable
from pony.orm.dbapiprovider import OperationalError from pony.orm.dbapiprovider import OperationalError
@@ -50,15 +50,6 @@ from BaseClasses import ItemClassification
min_client_version = Version(0, 5, 0) min_client_version = Version(0, 5, 0)
colorama.just_fix_windows_console() colorama.just_fix_windows_console()
no_version = Version(0, 0, 0)
assert isinstance(no_version, tuple) # assert immutable
server_per_message_deflate_factory = ServerPerMessageDeflateFactory(
server_max_window_bits=11,
client_max_window_bits=11,
compress_settings={"memLevel": 4},
)
def remove_from_list(container, value): def remove_from_list(container, value):
try: try:
@@ -134,31 +125,8 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
class Client(Endpoint): class Client(Endpoint):
__slots__ = ( version = Version(0, 0, 0)
"__weakref__", tags: typing.List[str]
"version",
"auth",
"team",
"slot",
"send_index",
"tags",
"messageprocessor",
"ctx",
"remote_items",
"remote_start_inventory",
"no_items",
"no_locations",
"no_text",
)
version: Version
auth: bool
team: int | None
slot: int | None
send_index: int
tags: list[str]
messageprocessor: ClientMessageProcessor
ctx: weakref.ref[Context]
remote_items: bool remote_items: bool
remote_start_inventory: bool remote_start_inventory: bool
no_items: bool no_items: bool
@@ -167,7 +135,6 @@ class Client(Endpoint):
def __init__(self, socket: "ServerConnection", ctx: Context) -> None: def __init__(self, socket: "ServerConnection", ctx: Context) -> None:
super().__init__(socket) super().__init__(socket)
self.version = no_version
self.auth = False self.auth = False
self.team = None self.team = None
self.slot = None self.slot = None
@@ -175,11 +142,6 @@ class Client(Endpoint):
self.tags = [] self.tags = []
self.messageprocessor = client_message_processor(ctx, self) self.messageprocessor = client_message_processor(ctx, self)
self.ctx = weakref.ref(ctx) self.ctx = weakref.ref(ctx)
self.remote_items = False
self.remote_start_inventory = False
self.no_items = False
self.no_locations = False
self.no_text = False
@property @property
def items_handling(self): def items_handling(self):
@@ -217,7 +179,6 @@ class Context:
"release_mode": str, "release_mode": str,
"remaining_mode": str, "remaining_mode": str,
"collect_mode": str, "collect_mode": str,
"countdown_mode": str,
"item_cheat": bool, "item_cheat": bool,
"compatibility": int} "compatibility": int}
# team -> slot id -> list of clients authenticated to slot. # team -> slot id -> list of clients authenticated to slot.
@@ -247,8 +208,8 @@ class Context:
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",
countdown_mode: str = "auto", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
compatibility: int = 2, log_network: bool = False, logger: logging.Logger = logging.getLogger()): log_network: bool = False, logger: logging.Logger = logging.getLogger()):
self.logger = logger self.logger = logger
super(Context, self).__init__() super(Context, self).__init__()
self.slot_info = {} self.slot_info = {}
@@ -281,7 +242,6 @@ class Context:
self.release_mode: str = release_mode self.release_mode: str = release_mode
self.remaining_mode: str = remaining_mode self.remaining_mode: str = remaining_mode
self.collect_mode: str = collect_mode self.collect_mode: str = collect_mode
self.countdown_mode: str = countdown_mode
self.item_cheat = item_cheat self.item_cheat = item_cheat
self.exit_event = asyncio.Event() self.exit_event = asyncio.Event()
self.client_activity_timers: typing.Dict[ self.client_activity_timers: typing.Dict[
@@ -493,7 +453,7 @@ class Context:
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0) 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},"
f"however this server is of version {version_tuple}") f"however this server is of version {version_tuple}")
self.generator_version = Version(*decoded_obj["version"]) self.generator_version = Version(*decoded_obj["version"])
clients_ver = decoded_obj["minimum_versions"].get("clients", {}) clients_ver = decoded_obj["minimum_versions"].get("clients", {})
@@ -667,7 +627,6 @@ class Context:
"server_password": self.server_password, "password": self.password, "server_password": self.server_password, "password": self.password,
"release_mode": self.release_mode, "release_mode": self.release_mode,
"remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode, "remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode,
"countdown_mode": self.countdown_mode,
"item_cheat": self.item_cheat, "compatibility": self.compatibility} "item_cheat": self.item_cheat, "compatibility": self.compatibility}
} }
@@ -702,7 +661,6 @@ class Context:
self.release_mode = savedata["game_options"]["release_mode"] self.release_mode = savedata["game_options"]["release_mode"]
self.remaining_mode = savedata["game_options"]["remaining_mode"] self.remaining_mode = savedata["game_options"]["remaining_mode"]
self.collect_mode = savedata["game_options"]["collect_mode"] self.collect_mode = savedata["game_options"]["collect_mode"]
self.countdown_mode = savedata["game_options"].get("countdown_mode", self.countdown_mode)
self.item_cheat = savedata["game_options"]["item_cheat"] self.item_cheat = savedata["game_options"]["item_cheat"]
self.compatibility = savedata["game_options"]["compatibility"] self.compatibility = savedata["game_options"]["compatibility"]
@@ -1200,17 +1158,16 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
found = location_id in ctx.location_checks[team, finding_player] found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hint_status = status # Assign again because we're in a for loop
if found: if found:
hint_status = HintStatus.HINT_FOUND status = HintStatus.HINT_FOUND
elif hint_status is None: elif status is None:
if item_flags & ItemClassification.trap: if item_flags & ItemClassification.trap:
hint_status = HintStatus.HINT_AVOID status = HintStatus.HINT_AVOID
else: else:
hint_status = HintStatus.HINT_PRIORITY status = HintStatus.HINT_PRIORITY
hints.append( hints.append(
Hint(receiving_player, finding_player, location_id, item_id, found, entrance, item_flags, hint_status) Hint(receiving_player, finding_player, location_id, item_id, found, entrance, item_flags, status)
) )
return hints return hints
@@ -1535,23 +1492,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
" You can ask the server admin for a /collect") " You can ask the server admin for a /collect")
return False return False
def _cmd_countdown(self, seconds: str = "10") -> bool:
"""Start a countdown in seconds"""
if self.ctx.countdown_mode == "disabled" or \
self.ctx.countdown_mode == "auto" and len(self.ctx.player_names) >= 30:
self.output("Sorry, client countdowns have been disabled on this server. You can ask the server admin for a /countdown")
return False
try:
timer = int(seconds, 10)
except ValueError:
timer = 10
else:
if timer > 60 * 60:
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
async_start(countdown(self.ctx, timer))
return True
def _cmd_remaining(self) -> bool: def _cmd_remaining(self) -> bool:
"""List remaining items in your game, but not their location or recipient""" """List remaining items in your game, but not their location or recipient"""
if self.ctx.remaining_mode == "enabled": if self.ctx.remaining_mode == "enabled":
@@ -2512,11 +2452,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
elif value_type == str and option_name.endswith("password"): elif value_type == str and option_name.endswith("password"):
def value_type(input_text: str): def value_type(input_text: str):
return None if input_text.lower() in {"null", "none", '""', "''"} else input_text return None if input_text.lower() in {"null", "none", '""', "''"} else input_text
elif option_name == "countdown_mode":
valid_values = {"enabled", "disabled", "auto"}
if option_value.lower() not in valid_values:
self.output(f"Unrecognized {option_name} value '{option_value}', known: {', '.join(valid_values)}")
return False
elif value_type == str and option_name.endswith("mode"): elif value_type == str and option_name.endswith("mode"):
valid_values = {"goal", "enabled", "disabled"} valid_values = {"goal", "enabled", "disabled"}
valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else []) valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else [])
@@ -2604,13 +2539,6 @@ def parse_args() -> argparse.Namespace:
goal: !collect can be used after goal completion goal: !collect can be used after goal completion
auto-enabled: !collect is available and automatically triggered on goal completion auto-enabled: !collect is available and automatically triggered on goal completion
''') ''')
parser.add_argument('--countdown_mode', default=defaults["countdown_mode"], nargs='?',
choices=['enabled', 'disabled', "auto"], help='''\
Select !countdown Accessibility. (default: %(default)s)
enabled: !countdown is always available
disabled: !countdown is never available
auto: !countdown is available for rooms with less than 30 players
''')
parser.add_argument('--remaining_mode', default=defaults["remaining_mode"], nargs='?', parser.add_argument('--remaining_mode', default=defaults["remaining_mode"], nargs='?',
choices=['enabled', 'disabled', "goal"], help='''\ choices=['enabled', 'disabled', "goal"], help='''\
Select !remaining Accessibility. (default: %(default)s) Select !remaining Accessibility. (default: %(default)s)
@@ -2676,7 +2604,7 @@ async def main(args: argparse.Namespace):
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode, args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,
args.countdown_mode, args.remaining_mode, args.remaining_mode,
args.auto_shutdown, args.compatibility, args.log_network) args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata data_filename = args.multidata
@@ -2711,13 +2639,7 @@ async def main(args: argparse.Namespace):
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
ctx.server = websockets.serve( ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ssl=ssl_context)
functools.partial(server, ctx=ctx),
host=ctx.host,
port=ctx.port,
ssl=ssl_context,
extensions=[server_per_message_deflate_factory],
)
ip = args.host if args.host else Utils.get_public_ipv4() ip = args.host if args.host else Utils.get_public_ipv4()
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
'No password' if not ctx.password else 'Password: %s' % ctx.password)) 'No password' if not ctx.password else 'Password: %s' % ctx.password))

View File

@@ -174,8 +174,6 @@ decode = JSONDecoder(object_hook=_object_hook).decode
class Endpoint: class Endpoint:
__slots__ = ("socket",)
socket: "ServerConnection" socket: "ServerConnection"
def __init__(self, socket): def __init__(self, socket):

View File

@@ -688,12 +688,6 @@ class Range(NumericOption):
range_start = 0 range_start = 0
range_end = 1 range_end = 1
_RANDOM_OPTS = [
"random", "random-low", "random-middle", "random-high",
"random-range-low-<min>-<max>", "random-range-middle-<min>-<max>",
"random-range-high-<min>-<max>", "random-range-<min>-<max>",
]
def __init__(self, value: int): def __init__(self, value: int):
if value < self.range_start: if value < self.range_start:
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}") raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
@@ -719,26 +713,9 @@ class Range(NumericOption):
# these are the conditions where "true" and "false" make sense # these are the conditions where "true" and "false" make sense
if text == "true": if text == "true":
return cls.from_any(cls.default) return cls.from_any(cls.default)
# "false" else: # "false"
return cls(0) return cls(0)
return cls(int(text))
try:
num = int(text)
except ValueError:
# text is not a number
# Handle conditionally acceptable values here rather than in the f-string
default = ""
truefalse = ""
if hasattr(cls, "default"):
default = ", default"
if cls.range_start == 0 and cls.default != 0:
truefalse = ", \"true\", \"false\""
raise Exception(f"Invalid range value {text!r}. Acceptable values are: "
f"<int>{default}, high, low{truefalse}, "
f"{', '.join(cls._RANDOM_OPTS)}.")
return cls(num)
@classmethod @classmethod
def weighted_range(cls, text) -> Range: def weighted_range(cls, text) -> Range:
@@ -754,7 +731,9 @@ class Range(NumericOption):
return cls(random.randint(cls.range_start, cls.range_end)) return cls(random.randint(cls.range_start, cls.range_end))
else: else:
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. " raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
f"Acceptable values are: {', '.join(cls._RANDOM_OPTS)}.") f"Acceptable values are: random, random-high, random-middle, random-low, "
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
@classmethod @classmethod
def custom_range(cls, text) -> Range: def custom_range(cls, text) -> Range:
@@ -1039,8 +1018,6 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
supports_weighting = False supports_weighting = False
display_name = "Plando Texts" display_name = "Plando Texts"
visibility = Visibility.template | Visibility.complex_ui | Visibility.spoiler
def __init__(self, value: typing.Iterable[PlandoText]) -> None: def __init__(self, value: typing.Iterable[PlandoText]) -> None:
self.value = list(deepcopy(value)) self.value = list(deepcopy(value))
super().__init__() super().__init__()
@@ -1167,8 +1144,6 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
entrances: typing.ClassVar[typing.AbstractSet[str]] entrances: typing.ClassVar[typing.AbstractSet[str]]
exits: typing.ClassVar[typing.AbstractSet[str]] exits: typing.ClassVar[typing.AbstractSet[str]]
visibility = Visibility.template | Visibility.complex_ui | Visibility.spoiler
duplicate_exits: bool = False duplicate_exits: bool = False
"""Whether or not exits should be allowed to be duplicate.""" """Whether or not exits should be allowed to be duplicate."""
@@ -1405,7 +1380,7 @@ class NonLocalItems(ItemSet):
class StartInventory(ItemDict): class StartInventory(ItemDict):
"""Start with the specified amount of these items. Example: "Bomb: 1" """ """Start with these items."""
verify_item_name = True verify_item_name = True
display_name = "Start Inventory" display_name = "Start Inventory"
rich_text_doc = True rich_text_doc = True
@@ -1413,7 +1388,7 @@ class StartInventory(ItemDict):
class StartInventoryPool(StartInventory): class StartInventoryPool(StartInventory):
"""Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1" """Start with these items and don't place them in the world.
The game decides what the replacement items will be. The game decides what the replacement items will be.
""" """
@@ -1460,7 +1435,6 @@ class DeathLink(Toggle):
class ItemLinks(OptionList): class ItemLinks(OptionList):
"""Share part of your item pool with other players.""" """Share part of your item pool with other players."""
display_name = "Item Links" display_name = "Item Links"
visibility = Visibility.template | Visibility.complex_ui | Visibility.spoiler
rich_text_doc = True rich_text_doc = True
default = [] default = []
schema = Schema([ schema = Schema([
@@ -1500,10 +1474,8 @@ class ItemLinks(OptionList):
super(ItemLinks, self).verify(world, player_name, plando_options) super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set() existing_links = set()
for link in self.value: for link in self.value:
link["name"] = link["name"].strip()[:16].strip()
if link["name"] in existing_links: if link["name"] in existing_links:
raise Exception(f"Item link names are limited to their first 16 characters and must be unique. " raise Exception(f"You cannot have more than one link named {link['name']}.")
f"You have more than one link named '{link['name']}'.")
existing_links.add(link["name"]) existing_links.add(link["name"])
pool = self.verify_items(link["item_pool"], link["name"], "item_pool", world) pool = self.verify_items(link["item_pool"], link["name"], "item_pool", world)
@@ -1545,7 +1517,6 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
default = () default = ()
supports_weighting = False supports_weighting = False
display_name = "Plando Items" display_name = "Plando Items"
visibility = Visibility.template | Visibility.spoiler
def __init__(self, value: typing.Iterable[PlandoItem]) -> None: def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
self.value = list(deepcopy(value)) self.value = list(deepcopy(value))
@@ -1739,7 +1710,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
from jinja2 import Template from jinja2 import Template
from worlds import AutoWorldRegister from worlds import AutoWorldRegister
from Utils import local_path, __version__ from Utils import local_path, __version__, tuplize_version
full_path: str full_path: str
@@ -1753,16 +1724,11 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
def dictify_range(option: Range): def dictify_range(option: Range):
data = {option.default: 50} data = {option.default: 50}
for sub_option in ["random", "random-low", "random-high", for sub_option in ["random", "random-low", "random-high"]:
f"random-range-{option.range_start}-{option.range_end}"]:
if sub_option != option.default: if sub_option != option.default:
data[sub_option] = 0 data[sub_option] = 0
notes = {
"random-low": "random value weighted towards lower values", notes = {}
"random-high": "random value weighted towards higher values",
f"random-range-{option.range_start}-{option.range_end}": f"random value between "
f"{option.range_start} and {option.range_end}"
}
for name, number in getattr(option, "special_range_names", {}).items(): for name, number in getattr(option, "special_range_names", {}).items():
notes[name] = f"equivalent to {number}" notes[name] = f"equivalent to {number}"
if number in data: if number in data:

View File

@@ -1,674 +0,0 @@
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
ToggleButton, MarkupDropdown, ResizableTextField)
from kivy.uix.behaviors.button import ButtonBehavior
from kivymd.uix.behaviors import RotateBehavior
from kivymd.uix.anchorlayout import MDAnchorLayout
from kivymd.uix.expansionpanel import MDExpansionPanel, MDExpansionPanelContent, MDExpansionPanelHeader
from kivymd.uix.list import MDListItem, MDListItemTrailingIcon, MDListItemSupportingText
from kivymd.uix.slider import MDSlider
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.button import MDButton, MDButtonText, MDIconButton
from kivymd.uix.dialog import MDDialog
from kivy.core.text.markup import MarkupLabel
from kivy.utils import escape_markup
from kivy.lang.builder import Builder
from kivy.properties import ObjectProperty
from textwrap import dedent
from copy import deepcopy
import Utils
import typing
import webbrowser
import re
from urllib.parse import urlparse
from worlds.AutoWorld import AutoWorldRegister, World
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, Removed,
OptionCounter, Visibility)
def validate_url(x):
try:
result = urlparse(x)
return all([result.scheme, result.netloc])
except AttributeError:
return False
def filter_tooltip(tooltip):
if tooltip is None:
tooltip = "No tooltip available."
tooltip = dedent(tooltip).strip().replace("\n", "<br>").replace("&", "&amp;") \
.replace("[", "&bl;").replace("]", "&br;")
tooltip = re.sub(r"\*\*(.+?)\*\*", r"[b]\g<1>[/b]", tooltip)
tooltip = re.sub(r"\*(.+?)\*", r"[i]\g<1>[/i]", tooltip)
return escape_markup(tooltip)
def option_can_be_randomized(option: typing.Type[Option]):
# most options can be randomized, so we should just check for those that cannot
if not option.supports_weighting:
return False
elif issubclass(option, FreeText) and not issubclass(option, TextChoice):
return False
return True
def check_random(value: typing.Any):
if not isinstance(value, str):
return value # cannot be random if evaluated
if value.startswith("random-"):
return "random"
return value
class TrailingPressedIconButton(ButtonBehavior, RotateBehavior, MDListItemTrailingIcon):
pass
class WorldButton(ToggleButton):
world_cls: typing.Type[World]
class VisualRange(MDBoxLayout):
option: typing.Type[Range]
name: str
tag: MDLabel = ObjectProperty(None)
slider: MDSlider = ObjectProperty(None)
def __init__(self, *args, option: typing.Type[Range], name: str, **kwargs):
self.option = option
self.name = name
super().__init__(*args, **kwargs)
def update_points(*update_args):
pass
self.slider._update_points = update_points
class VisualChoice(MDButton):
option: typing.Type[Choice]
name: str
text: MDButtonText = ObjectProperty(None)
def __init__(self, *args, option: typing.Type[Choice], name: str, **kwargs):
self.option = option
self.name = name
super().__init__(*args, **kwargs)
class VisualNamedRange(MDBoxLayout):
option: typing.Type[NamedRange]
name: str
range: VisualRange = ObjectProperty(None)
choice: MDButton = ObjectProperty(None)
def __init__(self, *args, option: typing.Type[NamedRange], name: str, range_widget: VisualRange, **kwargs):
self.option = option
self.name = name
super().__init__(*args, **kwargs)
self.range = range_widget
self.add_widget(self.range)
class VisualFreeText(ResizableTextField):
option: typing.Type[FreeText] | typing.Type[TextChoice]
name: str
def __init__(self, *args, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str, **kwargs):
self.option = option
self.name = name
super().__init__(*args, **kwargs)
class VisualTextChoice(MDBoxLayout):
option: typing.Type[TextChoice]
name: str
choice: VisualChoice = ObjectProperty(None)
text: VisualFreeText = ObjectProperty(None)
def __init__(self, *args, option: typing.Type[TextChoice], name: str, choice: VisualChoice,
text: VisualFreeText, **kwargs):
self.option = option
self.name = name
super(MDBoxLayout, self).__init__(*args, **kwargs)
self.choice = choice
self.text = text
self.add_widget(self.choice)
self.add_widget(self.text)
class VisualToggle(MDBoxLayout):
button: MDIconButton = ObjectProperty(None)
option: typing.Type[Toggle]
name: str
def __init__(self, *args, option: typing.Type[Toggle], name: str, **kwargs):
self.option = option
self.name = name
super().__init__(*args, **kwargs)
class CounterItemValue(ResizableTextField):
pat = re.compile('[^0-9]')
def insert_text(self, substring, from_undo=False):
return super().insert_text(re.sub(self.pat, "", substring), from_undo=from_undo)
class VisualListSetCounter(MDDialog):
button: MDIconButton = ObjectProperty(None)
option: typing.Type[OptionSet] | typing.Type[OptionList] | typing.Type[OptionCounter]
scrollbox: ScrollBox = ObjectProperty(None)
add: MDIconButton = ObjectProperty(None)
save: MDButton = ObjectProperty(None)
input: ResizableTextField = ObjectProperty(None)
dropdown: MDDropdownMenu
valid_keys: typing.Iterable[str]
def __init__(self, *args, option: typing.Type[OptionSet] | typing.Type[OptionList],
name: str, valid_keys: typing.Iterable[str], **kwargs):
self.option = option
self.name = name
self.valid_keys = valid_keys
super().__init__(*args, **kwargs)
self.dropdown = MarkupDropdown(caller=self.input, border_margin=dp(2),
width=self.input.width, position="bottom")
self.input.bind(text=self.on_text)
self.input.bind(on_text_validate=self.validate_add)
def validate_add(self, instance):
if self.valid_keys:
if self.input.text not in self.valid_keys:
MDSnackbar(MDSnackbarText(text="Item must be a valid key for this option."), y=dp(24),
pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
return
if not issubclass(self.option, OptionList):
if any(self.input.text == child.text.text for child in self.scrollbox.layout.children):
MDSnackbar(MDSnackbarText(text="This value is already in the set."), y=dp(24),
pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
return
self.add_set_item(self.input.text)
self.input.set_text(self.input, "")
def remove_item(self, button: MDIconButton):
list_item = button.parent
self.scrollbox.layout.remove_widget(list_item)
def add_set_item(self, key: str, value: int | None = None):
text = MDListItemSupportingText(text=key, id="value")
if issubclass(self.option, OptionCounter):
value_txt = CounterItemValue(text=str(value) if value else "1")
item = MDListItem(text,
value_txt,
MDIconButton(icon="minus", on_release=self.remove_item), focus_behavior=False)
item.value = value_txt
else:
item = MDListItem(text, MDIconButton(icon="minus", on_release=self.remove_item), focus_behavior=False)
item.text = text
self.scrollbox.layout.add_widget(item)
def on_text(self, instance, value):
if not self.valid_keys:
return
if len(value) >= 3:
self.dropdown.items.clear()
def on_press(txt):
split_text = MarkupLabel(text=txt, markup=True).markup
self.input.set_text(self.input, "".join(text_frag for text_frag in split_text
if not text_frag.startswith("[")))
self.input.focus = True
self.dropdown.dismiss()
lowered = value.lower()
for item_name in self.valid_keys:
try:
index = item_name.lower().index(lowered)
except ValueError:
pass # substring not found
else:
text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index + len(value)] + "[/b]" + text[index + len(value):]
self.dropdown.items.append({
"text": text,
"on_release": lambda txt=text: on_press(txt),
"markup": True
})
if not self.dropdown.parent:
self.dropdown.open()
else:
self.dropdown.dismiss()
class OptionsCreator(ThemedApp):
base_title: str = "Archipelago Options Creator"
container: ContainerLayout
main_layout: MainLayout
scrollbox: ScrollBox
main_panel: MainLayout
player_options: MainLayout
option_layout: MainLayout
name_input: ResizableTextField
game_label: MDLabel
current_game: str
options: typing.Dict[str, typing.Any]
def __init__(self):
self.title = self.base_title + " " + Utils.__version__
self.icon = r"data/icon.png"
self.current_game = ""
self.options = {}
super().__init__()
def export_options(self, button: Widget):
if 0 < len(self.name_input.text) < 17 and self.current_game:
file_name = Utils.save_filename("Export Options File As...", [("YAML", ["*.yaml"])],
Utils.get_file_safe_name(f"{self.name_input.text}.yaml"))
options = {
"name": self.name_input.text,
"description": f"YAML generated by Archipelago {Utils.__version__}.",
"game": self.current_game,
self.current_game: {k: check_random(v) for k, v in self.options.items()}
}
try:
with open(file_name, 'w') as f:
f.write(Utils.dump(options, sort_keys=False))
f.close()
MDSnackbar(MDSnackbarText(text="File saved successfully."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
except FileNotFoundError:
MDSnackbar(MDSnackbarText(text="Saving cancelled."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
elif not self.name_input.text:
MDSnackbar(MDSnackbarText(text="Name must not be empty."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
elif not self.current_game:
MDSnackbar(MDSnackbarText(text="You must select a game to play."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
else:
MDSnackbar(MDSnackbarText(text="Name cannot be longer than 16 characters."), y=dp(24),
pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
def create_range(self, option: typing.Type[Range], name: str):
def update_text(range_box: VisualRange):
self.options[name] = int(range_box.slider.value)
range_box.tag.text = str(int(range_box.slider.value))
return
box = VisualRange(option=option, name=name)
box.slider.bind(on_touch_move=lambda _, _1: update_text(box))
self.options[name] = option.default
return box
def create_named_range(self, option: typing.Type[NamedRange], name: str):
def set_to_custom(range_box: VisualNamedRange):
if (not self.options[name] == range_box.range.slider.value) \
and (not self.options[name] in option.special_range_names or
range_box.range.slider.value != option.special_range_names[self.options[name]]):
# we should validate the touch here,
# but this is much cheaper
self.options[name] = int(range_box.range.slider.value)
range_box.range.tag.text = str(int(range_box.range.slider.value))
set_button_text(range_box.choice, "Custom")
def set_button_text(button: MDButton, text: str):
button.text.text = text
def set_value(text: str, range_box: VisualNamedRange):
range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start),
option.range_end)
range_box.range.tag.text = str(int(range_box.range.slider.value))
set_button_text(range_box.choice, text)
self.options[name] = text.lower()
range_box.range.slider.dropdown.dismiss()
def open_dropdown(button):
# for some reason this fixes an issue causing some to not open
box.range.slider.dropdown.open()
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
if option.default in option.special_range_names:
# value can get mismatched in this case
box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
option.range_end)
box.range.tag.text = str(int(box.range.slider.value))
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
items = [
{
"text": choice.title(),
"on_release": lambda text=choice.title(): set_value(text, box)
}
for choice in option.special_range_names
]
box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items)
box.choice.bind(on_release=open_dropdown)
self.options[name] = option.default
return box
def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
text = VisualFreeText(option=option, name=name)
def set_value(instance):
self.options[name] = instance.text
text.bind(on_text_validate=set_value)
return text
def create_choice(self, option: typing.Type[Choice], name: str):
def set_button_text(button: VisualChoice, text: str):
button.text.text = text
def set_value(text, value):
set_button_text(main_button, text)
self.options[name] = value
dropdown.dismiss()
def open_dropdown(button):
# for some reason this fixes an issue causing some to not open
dropdown.open()
default_string = isinstance(option.default, str)
main_button = VisualChoice(option=option, name=name)
main_button.bind(on_release=open_dropdown)
items = [
{
"text": option.get_option_name(choice),
"on_release": lambda val=choice: set_value(option.get_option_name(val), option.name_lookup[val])
}
for choice in option.name_lookup
]
dropdown = MDDropdownMenu(caller=main_button, items=items)
self.options[name] = option.name_lookup[option.default] if not default_string else option.default
return main_button
def create_text_choice(self, option: typing.Type[TextChoice], name: str):
def set_button_text(button: MDButton, text: str):
for child in button.children:
if isinstance(child, MDButtonText):
child.text = text
box = VisualTextChoice(option=option, name=name, choice=self.create_choice(option, name),
text=self.create_free_text(option, name))
def set_value(instance):
set_button_text(box.choice, "Custom")
self.options[name] = instance.text
box.text.bind(on_text_validate=set_value)
return box
def create_toggle(self, option: typing.Type[Toggle], name: str) -> Widget:
def set_value(instance: MDIconButton):
if instance.icon == "checkbox-outline":
instance.icon = "checkbox-blank-outline"
else:
instance.icon = "checkbox-outline"
self.options[name] = bool(not self.options[name])
self.options[name] = bool(option.default)
checkbox = VisualToggle(option=option, name=name)
checkbox.button.bind(on_release=set_value)
return checkbox
def create_popup(self, option: typing.Type[OptionList] | typing.Type[OptionSet] | typing.Type[OptionCounter],
name: str, world: typing.Type[World]):
valid_keys = sorted(option.valid_keys)
if option.verify_item_name:
valid_keys += list(world.item_name_to_id.keys())
if option.verify_location_name:
valid_keys += list(world.location_name_to_id.keys())
if not issubclass(option, OptionCounter):
def apply_changes(button):
self.options[name].clear()
for list_item in dialog.scrollbox.layout.children:
self.options[name].append(getattr(list_item.text, "text"))
dialog.dismiss()
else:
def apply_changes(button):
self.options[name].clear()
for list_item in dialog.scrollbox.layout.children:
self.options[name][getattr(list_item.text, "text")] = int(getattr(list_item.value, "text"))
dialog.dismiss()
dialog = VisualListSetCounter(option=option, name=name, valid_keys=valid_keys)
dialog.ids.container.spacing = dp(30)
dialog.scrollbox.layout.theme_bg_color = "Custom"
dialog.scrollbox.layout.md_bg_color = self.theme_cls.surfaceContainerLowColor
dialog.scrollbox.layout.spacing = dp(5)
dialog.scrollbox.layout.padding = [0, dp(5), 0, 0]
if name not in self.options:
# convert from non-mutable to mutable
# We use list syntax even for sets, set behavior is enforced through GUI
if issubclass(option, OptionCounter):
self.options[name] = deepcopy(option.default)
else:
self.options[name] = sorted(option.default)
if issubclass(option, OptionCounter):
for value in sorted(self.options[name]):
dialog.add_set_item(value, self.options[name].get(value, None))
else:
for value in sorted(self.options[name]):
dialog.add_set_item(value)
dialog.save.bind(on_release=apply_changes)
dialog.open()
def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] |
typing.Type[OptionCounter], name: str, world: typing.Type[World]):
main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world))
return main_button
def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget:
option_base = MDBoxLayout(orientation="vertical", size_hint_y=None, padding=[0, 0, dp(5), dp(5)])
tooltip = filter_tooltip(option.__doc__)
option_label = TooltipLabel(text=f"[ref=0|{tooltip}]{getattr(option, 'display_name', name)}")
label_box = MDBoxLayout(orientation="horizontal")
label_anchor = MDAnchorLayout(anchor_x="right", anchor_y="center")
label_anchor.add_widget(option_label)
label_box.add_widget(label_anchor)
option_base.add_widget(label_box)
if issubclass(option, NamedRange):
option_base.add_widget(self.create_named_range(option, name))
elif issubclass(option, Range):
option_base.add_widget(self.create_range(option, name))
elif issubclass(option, Toggle):
option_base.add_widget(self.create_toggle(option, name))
elif issubclass(option, TextChoice):
option_base.add_widget(self.create_text_choice(option, name))
elif issubclass(option, Choice):
option_base.add_widget(self.create_choice(option, name))
elif issubclass(option, FreeText):
option_base.add_widget(self.create_free_text(option, name))
elif any(issubclass(option, cls) for cls in (OptionSet, OptionList, OptionCounter)):
option_base.add_widget(self.create_option_set_list_counter(option, name, world))
else:
option_base.add_widget(MDLabel(text="This option isn't supported by the option creator.\n"
"Please edit your yaml manually to set this option."))
if option_can_be_randomized(option):
def randomize_option(instance: Widget, value: str):
value = value == "down"
if value:
self.options[name] = "random-" + str(self.options[name])
else:
self.options[name] = self.options[name].replace("random-", "")
if self.options[name].isnumeric() or self.options[name] in ("True", "False"):
self.options[name] = eval(self.options[name])
base_object = instance.parent.parent
label_object = instance.parent
for child in base_object.children:
if child is not label_object:
child.disabled = value
default_random = option.default == "random"
random_toggle = ToggleButton(MDButtonText(text="Random?"), size_hint_x=None, width=dp(100),
state="down" if default_random else "normal")
random_toggle.bind(state=randomize_option)
label_box.add_widget(random_toggle)
if default_random:
randomize_option(random_toggle, "down")
return option_base
def create_options_panel(self, world_button: WorldButton):
self.option_layout.clear_widgets()
self.options.clear()
cls: typing.Type[World] = world_button.world_cls
self.current_game = cls.game
if not cls.web.options_page:
self.current_game = "None"
return
elif isinstance(cls.web.options_page, str):
self.current_game = "None"
if validate_url(cls.web.options_page):
webbrowser.open(cls.web.options_page)
MDSnackbar(MDSnackbarText(text="Launching in default browser..."), y=dp(24), pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
world_button.state = "normal"
else:
# attach onto archipelago.gg and see if we pass
new_url = "https://archipelago.gg/" + cls.web.options_page
if validate_url(new_url):
webbrowser.open(new_url)
MDSnackbar(MDSnackbarText(text="Launching in default browser..."), y=dp(24),
pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
else:
MDSnackbar(MDSnackbarText(text="Invalid options page, please report to world developer."), y=dp(24),
pos_hint={"center_x": 0.5},
size_hint_x=0.5).open()
world_button.state = "normal"
# else just fall through
else:
expansion_box = ScrollBox()
expansion_box.layout.orientation = "vertical"
expansion_box.layout.spacing = dp(3)
expansion_box.scroll_type = ["bars"]
expansion_box.do_scroll_x = False
group_names = ["Game Options", *(group.name for group in cls.web.option_groups)]
groups = {name: [] for name in group_names}
for name, option in cls.options_dataclass.type_hints.items():
group = next((group.name for group in cls.web.option_groups if option in group.options), "Game Options")
groups[group].append((name, option))
for group, options in groups.items():
options = [(name, option) for name, option in options
if name and option.visibility & Visibility.simple_ui]
if not options:
continue # Game Options can be empty if every other option is in another group
# Can also have an option group of options that should not render on simple ui
group_item = MDExpansionPanel(size_hint_y=None)
group_header = MDExpansionPanelHeader(MDListItem(MDListItemSupportingText(text=group),
TrailingPressedIconButton(icon="chevron-right",
on_release=lambda x,
item=group_item:
self.tap_expansion_chevron(
item, x)),
md_bg_color=self.theme_cls.surfaceContainerLowestColor,
theme_bg_color="Custom",
on_release=lambda x, item=group_item:
self.tap_expansion_chevron(item, x)))
group_content = MDExpansionPanelContent(orientation="vertical", theme_bg_color="Custom",
md_bg_color=self.theme_cls.surfaceContainerLowestColor,
padding=[dp(12), dp(100), dp(12), 0],
spacing=dp(3))
group_item.add_widget(group_header)
group_item.add_widget(group_content)
group_box = ScrollBox()
group_box.layout.orientation = "vertical"
group_box.layout.spacing = dp(3)
for name, option in options:
group_content.add_widget(self.create_option(option, name, cls))
expansion_box.layout.add_widget(group_item)
self.option_layout.add_widget(expansion_box)
self.game_label.text = f"Game: {self.current_game}"
@staticmethod
def tap_expansion_chevron(panel: MDExpansionPanel, chevron: TrailingPressedIconButton | MDListItem):
if isinstance(chevron, MDListItem):
chevron = next((child for child in chevron.ids.trailing_container.children
if isinstance(child, TrailingPressedIconButton)), None)
panel.open() if not panel.is_open else panel.close()
if chevron:
panel.set_chevron_down(
chevron
) if not panel.is_open else panel.set_chevron_up(chevron)
def build(self):
self.set_colors()
self.options = {}
self.container = Builder.load_file(Utils.local_path("data/optionscreator.kv"))
self.root = self.container
self.main_layout = self.container.ids.main
self.scrollbox = self.container.ids.scrollbox
def world_button_action(world_btn: WorldButton):
if self.current_game != world_btn.world_cls.game:
old_button = next((button for button in self.scrollbox.layout.children
if button.world_cls.game == self.current_game), None)
if old_button:
old_button.state = "normal"
else:
world_btn.state = "down"
self.create_options_panel(world_btn)
for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]):
if world == "Archipelago":
continue
world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150),
pos_hint={"x": 0.03, "center_y": 0.5})
world_text.text_size = (world_text.width, None)
world_text.bind(width=lambda *x, text=world_text: text.setter('text_size')(text, (text.width, None)),
texture_size=lambda *x, text=world_text: text.setter("height")(text,
world_text.texture_size[1]))
world_button = WorldButton(world_text, size_hint_x=None, width=dp(150), theme_width="Custom",
radius=(dp(5), dp(5), dp(5), dp(5)))
world_button.bind(on_release=world_button_action)
world_button.world_cls = cls
self.scrollbox.layout.add_widget(world_button)
self.main_panel = self.container.ids.player_layout
self.player_options = self.container.ids.player_options
self.game_label = self.container.ids.game
self.name_input = self.container.ids.player_name
self.option_layout = self.container.ids.options
def set_height(instance, value):
instance.height = value[1]
self.game_label.bind(texture_size=set_height)
# Uncomment to re-enable the Kivy console/live editor
# Ctrl-E to enable it, make sure numlock/capslock is disabled
# from kivy.modules.console import create_console
# from kivy.core.window import Window
# create_console(Window, self.container)
return self.container
def launch():
OptionsCreator().run()
if __name__ == "__main__":
Utils.init_logging("OptionsCreator")
launch()

View File

@@ -82,7 +82,6 @@ Currently, the following games are supported:
* Paint * Paint
* Celeste (Open World) * Celeste (Open World)
* Choo-Choo Charles * Choo-Choo Charles
* APQuest
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -18,7 +18,7 @@ from json import loads, dumps
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
import Utils import Utils
import settings from settings import Settings
from Utils import async_start from Utils import async_start
from MultiServer import mark_raw from MultiServer import mark_raw
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@@ -286,7 +286,7 @@ class SNESState(enum.IntEnum):
def launch_sni() -> None: def launch_sni() -> None:
sni_path = settings.get_settings().sni_options.sni_path sni_path = Settings.sni_options.sni_path
if not os.path.isdir(sni_path): if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path) sni_path = Utils.local_path(sni_path)
@@ -669,7 +669,7 @@ async def game_watcher(ctx: SNIContext) -> None:
async def run_game(romfile: str) -> None: async def run_game(romfile: str) -> None:
auto_start = settings.get_settings().sni_options.snes_rom_start auto_start = Settings.sni_options.snes_rom_start
if auto_start is True: if auto_start is True:
import webbrowser import webbrowser
webbrowser.open(romfile) webbrowser.open(romfile)

112
Utils.py
View File

@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import concurrent.futures
import json import json
import typing import typing
import builtins import builtins
@@ -36,7 +35,7 @@ if typing.TYPE_CHECKING:
def tuplize_version(version: str) -> Version: def tuplize_version(version: str) -> Version:
return Version(*(int(piece) for piece in version.split("."))) return Version(*(int(piece, 10) for piece in version.split(".")))
class Version(typing.NamedTuple): class Version(typing.NamedTuple):
@@ -48,8 +47,9 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.6.5" __version__ = "0.6.4"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
version = Version(*version_tuple)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")
is_macos = sys.platform == "darwin" is_macos = sys.platform == "darwin"
@@ -314,8 +314,12 @@ def get_public_ipv6() -> str:
return ip return ip
OptionsType = Settings # TODO: remove when removing get_options
def get_options() -> Settings: def get_options() -> Settings:
deprecate("Utils.get_options() is deprecated. Use the settings API instead.") # TODO: switch to Utils.deprecate after 0.4.4
warnings.warn("Utils.get_options() is deprecated. Use the settings API instead.", DeprecationWarning)
return get_settings() return get_settings()
@@ -474,7 +478,7 @@ class RestrictedUnpickler(pickle.Unpickler):
mod = importlib.import_module(module) mod = importlib.import_module(module)
obj = getattr(mod, name) obj = getattr(mod, name)
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection, if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection,
self.options_module.PlandoItem, self.options_module.PlandoText)): self.options_module.PlandoText)):
return obj return obj
# Forbid everything else. # Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
@@ -717,22 +721,13 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]: def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]:
"""
Parses the response text from `get_intended_text` to find the suggested input and autocomplete the command in
arguments with it.
:param text: The response text from `get_intended_text`.
:param command: The command to which the input text should be added. Must contain the prefix used by the command
(`!` or `/`).
:return: The command with the suggested input text appended, or None if no suggestion was found.
"""
if "did you mean " in text: if "did you mean " in text:
for question in ("Didn't find something that closely matches", for question in ("Didn't find something that closely matches",
"Too many close matches"): "Too many close matches"):
if text.startswith(question): if text.startswith(question):
name = get_text_between(text, "did you mean '", name = get_text_between(text, "did you mean '",
"'? (") "'? (")
return f"{command} {name}" return f"!{command} {name}"
elif text.startswith("Missing: "): elif text.startswith("Missing: "):
return text.replace("Missing: ", "!hint_location ") return text.replace("Missing: ", "!hint_location ")
return None return None
@@ -751,11 +746,6 @@ def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args:
res.put(open_filename(*args)) res.put(open_filename(*args))
def _mp_save_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
if is_kivy_running():
raise RuntimeError("kivy should not be running in multiprocess")
res.put(save_filename(*args))
def _run_for_stdout(*args: str): def _run_for_stdout(*args: str):
env = os.environ env = os.environ
if "LD_LIBRARY_PATH" in env: if "LD_LIBRARY_PATH" in env:
@@ -806,51 +796,6 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
initialfile=suggest or None) initialfile=suggest or None)
def save_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
-> typing.Optional[str]:
logging.info(f"Opening file save dialog for {title}.")
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_linux:
# prefer native dialog
from shutil import which
kdialog = which("kdialog")
if kdialog:
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
return run(kdialog, f"--title={title}", "--getsavefilename", suggest or ".", k_filters)
zenity = which("zenity")
if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
selection = (f"--filename={suggest}",) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", "--save", *z_filters, *selection)
# fall back to tk
try:
import tkinter
import tkinter.filedialog
except Exception as e:
logging.error('Could not load tkinter, which is likely not installed. '
f'This attempt was made because save_filename was used for "{title}".')
raise e
else:
if is_macos and is_kivy_running():
# on macOS, mixing kivy and tk does not work, so spawn a new process
# FIXME: performance of this is pretty bad, and we should (also) look into alternatives
from multiprocessing import Process, Queue
res: "Queue[typing.Optional[str]]" = Queue()
Process(target=_mp_save_filename, args=(res, title, filetypes, suggest)).start()
return res.get()
try:
root = tkinter.Tk()
except tkinter.TclError:
return None # GUI not available. None is the same as a user clicking "cancel"
root.withdraw()
return tkinter.filedialog.asksaveasfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
initialfile=suggest or None)
def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None: def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
if is_kivy_running(): if is_kivy_running():
raise RuntimeError("kivy should not be running in multiprocess") raise RuntimeError("kivy should not be running in multiprocess")
@@ -1185,40 +1130,3 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]
if isinstance(obj, str): if isinstance(obj, str):
return False return False
return isinstance(obj, typing.Iterable) return isinstance(obj, typing.Iterable)
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
"""
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.
NOTE: use this with caution because killed threads will not properly clean up.
"""
def _adjust_thread_count(self):
# see upstream ThreadPoolExecutor for details
import threading
import weakref
from concurrent.futures.thread import _worker
if self._idle_semaphore.acquire(timeout=0):
return
def weakref_cb(_, q=self._work_queue):
q.put(None)
num_threads = len(self._threads)
if num_threads < self._max_workers:
thread_name = f"{self._thread_name_prefix or self}_{num_threads}"
t = threading.Thread(
name=thread_name,
target=_worker,
args=(
weakref.ref(self, weakref_cb),
self._work_queue,
self._initializer,
self._initargs,
),
daemon=True,
)
t.start()
self._threads.add(t)
# NOTE: don't add to _threads_queues so we don't block on shutdown

View File

@@ -109,13 +109,6 @@ if __name__ == "__main__":
logging.exception(e) logging.exception(e)
logging.warning("Could not update LttP sprites.") logging.warning("Could not update LttP sprites.")
app = get_app() app = get_app()
from worlds import AutoWorldRegister
# Update to only valid WebHost worlds
invalid_worlds = {name for name, world in AutoWorldRegister.world_types.items()
if not hasattr(world.web, "tutorials")}
if invalid_worlds:
logging.error(f"Following worlds not loaded as they are invalid for WebHost: {invalid_worlds}")
AutoWorldRegister.world_types = {k: v for k, v in AutoWorldRegister.world_types.items() if k not in invalid_worlds}
create_options_files() create_options_files()
copy_tutorials_files_to_static() copy_tutorials_files_to_static()
if app.config["SELFLAUNCH"]: if app.config["SELFLAUNCH"]:

View File

@@ -1,7 +1,6 @@
import base64 import base64
import os import os
import socket import socket
import typing
import uuid import uuid
from flask import Flask from flask import Flask
@@ -62,21 +61,20 @@ cache = Cache()
Compress(app) Compress(app)
def to_python(value: str) -> uuid.UUID: def to_python(value):
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '==')) return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=='))
def to_url(value: uuid.UUID) -> str: def to_url(value):
return base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') return base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
class B64UUIDConverter(BaseConverter): class B64UUIDConverter(BaseConverter):
def to_python(self, value: str) -> uuid.UUID: def to_python(self, value):
return to_python(value) return to_python(value)
def to_url(self, value: typing.Any) -> str: def to_url(self, value):
assert isinstance(value, uuid.UUID)
return to_url(value) return to_url(value)
@@ -86,7 +84,7 @@ app.jinja_env.filters["suuid"] = to_url
app.jinja_env.filters["title_sorted"] = title_sorted app.jinja_env.filters["title_sorted"] = title_sorted
def register() -> None: def register():
"""Import submodules, triggering their registering on flask routing. """Import submodules, triggering their registering on flask routing.
Note: initializes worlds subsystem.""" Note: initializes worlds subsystem."""
import importlib import importlib

View File

@@ -1,15 +1,16 @@
import json import json
import typing
from uuid import UUID from uuid import UUID
from flask import request, session, url_for from flask import request, session, url_for
from markupsafe import Markup from markupsafe import Markup
from pony.orm import commit from pony.orm import commit, select
from Utils import restricted_dumps from Utils import restricted_dumps
from WebHostLib import app from WebHostLib import app, cache
from WebHostLib.check import get_yaml_data, roll_options from WebHostLib.check import get_yaml_data, roll_options
from WebHostLib.generate import get_meta from WebHostLib.generate import get_meta
from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR from WebHostLib.models import Generation, STATE_QUEUED, STATE_STARTED, Seed, STATE_ERROR
from . import api_endpoints from . import api_endpoints
@@ -74,12 +75,23 @@ def generate_api():
def wait_seed_api(seed: UUID): def wait_seed_api(seed: UUID):
seed_id = seed seed_id = seed
seed = Seed.get(id=seed_id) seed = Seed.get(id=seed_id)
reply_dict: dict[str, typing.Any] = {"queue_len": get_queue_length()}
if seed: if seed:
return {"text": "Generation done"}, 201 reply_dict["text"] = "Generation done"
return reply_dict, 201
generation = Generation.get(id=seed_id) generation = Generation.get(id=seed_id)
if not generation: if not generation:
return {"text": "Generation not found"}, 404 reply_dict["text"] = "Generation not found"
return reply_dict, 404
elif generation.state == STATE_ERROR: elif generation.state == STATE_ERROR:
return {"text": "Generation failed"}, 500 reply_dict["text"] = "Generation failed"
return {"text": "Generation running"}, 202 return reply_dict, 500
reply_dict["text"] = "Generation running"
return reply_dict, 202
@cache.memoize(timeout=5)
def get_queue_length() -> int:
return select(generation for generation in Generation if
generation.state == STATE_STARTED or generation.state == STATE_QUEUED).count()

View File

@@ -58,12 +58,6 @@ class PlayerLocationsTotal(TypedDict):
total_locations: int total_locations: int
class PlayerGame(TypedDict):
team: int
player: int
game: str
@api_endpoints.route("/tracker/<suuid:tracker>") @api_endpoints.route("/tracker/<suuid:tracker>")
@cache.memoize(timeout=60) @cache.memoize(timeout=60)
def tracker_data(tracker: UUID) -> dict[str, Any]: def tracker_data(tracker: UUID) -> dict[str, Any]:
@@ -86,8 +80,7 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
"""Slot aliases of all players.""" """Slot aliases of all players."""
for team, players in all_players.items(): for team, players in all_players.items():
for player in players: for player in players:
player_aliases.append( player_aliases.append({"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
{"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
player_items_received: list[PlayerItemsReceived] = [] player_items_received: list[PlayerItemsReceived] = []
"""Items received by each player.""" """Items received by each player."""
@@ -101,8 +94,7 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
for team, players in all_players.items(): for team, players in all_players.items():
for player in players: for player in players:
player_checks_done.append( player_checks_done.append(
{"team": team, "player": player, {"team": team, "player": player, "locations": sorted(tracker_data.get_player_checked_locations(team, player))})
"locations": sorted(tracker_data.get_player_checked_locations(team, player))})
total_checks_done: list[TeamTotalChecks] = [ total_checks_done: list[TeamTotalChecks] = [
{"team": team, "checks_done": checks_done} {"team": team, "checks_done": checks_done}
@@ -152,8 +144,7 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
"""The current client status for each player.""" """The current client status for each player."""
for team, players in all_players.items(): for team, players in all_players.items():
for player in players: for player in players:
player_status.append( player_status.append({"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
{"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
return { return {
"aliases": player_aliases, "aliases": player_aliases,
@@ -216,20 +207,12 @@ def static_tracker_data(tracker: UUID) -> dict[str, Any]:
player_locations_total.append( player_locations_total.append(
{"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))}) {"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))})
player_game: list[PlayerGame] = []
"""The played game per player slot."""
for team, players in all_players.items():
for player in players:
player_game.append({"team": team, "player": player, "game": tracker_data.get_player_game(player)})
return { return {
"groups": groups, "groups": groups,
"datapackage": tracker_data._multidata["datapackage"], "datapackage": tracker_data._multidata["datapackage"],
"player_locations_total": player_locations_total, "player_locations_total": player_locations_total,
"player_game": player_game,
} }
# It should be exceedingly rare that slot data is needed, so it's separated out. # It should be exceedingly rare that slot data is needed, so it's separated out.
@api_endpoints.route("/slot_data_tracker/<suuid:tracker>") @api_endpoints.route("/slot_data_tracker/<suuid:tracker>")
@cache.memoize(timeout=300) @cache.memoize(timeout=300)

View File

@@ -17,7 +17,7 @@ from .locker import Locker, AlreadyRunningException
_stop_event = Event() _stop_event = Event()
def stop() -> None: def stop():
"""Stops previously launched threads""" """Stops previously launched threads"""
global _stop_event global _stop_event
stop_event = _stop_event stop_event = _stop_event
@@ -36,39 +36,25 @@ def handle_generation_failure(result: BaseException):
logging.exception(e) logging.exception(e)
def _mp_gen_game( def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None:
gen_options: dict,
meta: dict[str, Any] | None = None,
owner=None,
sid=None,
timeout: int|None = None,
) -> PrimaryKey | None:
from setproctitle import setproctitle from setproctitle import setproctitle
setproctitle(f"Generator ({sid})") setproctitle(f"Generator ({sid})")
try: res = gen_game(gen_options, meta=meta, owner=owner, sid=sid)
return gen_game(gen_options, meta=meta, owner=owner, sid=sid, timeout=timeout) setproctitle(f"Generator (idle)")
finally: return res
setproctitle(f"Generator (idle)")
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation, timeout: int|None) -> None: def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
try: try:
meta = json.loads(generation.meta) meta = json.loads(generation.meta)
options = restricted_loads(generation.options) options = restricted_loads(generation.options)
logging.info(f"Generating {generation.id} for {len(options)} players") logging.info(f"Generating {generation.id} for {len(options)} players")
pool.apply_async( pool.apply_async(_mp_gen_game, (options,),
_mp_gen_game, {"meta": meta,
(options,), "sid": generation.id,
{ "owner": generation.owner},
"meta": meta, handle_generation_success, handle_generation_failure)
"sid": generation.id,
"owner": generation.owner,
"timeout": timeout,
},
handle_generation_success,
handle_generation_failure,
)
except Exception as e: except Exception as e:
generation.state = STATE_ERROR generation.state = STATE_ERROR
commit() commit()
@@ -149,7 +135,6 @@ def autogen(config: dict):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator, with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator,
initargs=(config,), maxtasksperchild=10) as generator_pool: initargs=(config,), maxtasksperchild=10) as generator_pool:
job_time = config["JOB_TIME"]
with db_session: with db_session:
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED) to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
@@ -160,7 +145,7 @@ def autogen(config: dict):
if sid: if sid:
generation.delete() generation.delete()
else: else:
launch_generator(generator_pool, generation, timeout=job_time) launch_generator(generator_pool, generation)
commit() commit()
select(generation for generation in Generation if generation.state == STATE_ERROR).delete() select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
@@ -172,7 +157,7 @@ def autogen(config: dict):
generation for generation in Generation generation for generation in Generation
if generation.state == STATE_QUEUED).for_update() if generation.state == STATE_QUEUED).for_update()
for generation in to_start: for generation in to_start:
launch_generator(generator_pool, generation, timeout=job_time) launch_generator(generator_pool, generation)
except AlreadyRunningException: except AlreadyRunningException:
logging.info("Autogen reports as already running, not starting another.") logging.info("Autogen reports as already running, not starting another.")

View File

@@ -19,10 +19,7 @@ from pony.orm import commit, db_session, select
import Utils import Utils
from MultiServer import ( from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert,
server_per_message_deflate_factory,
)
from Utils import restricted_loads, cache_argsless from Utils import restricted_loads, cache_argsless
from .locker import Locker from .locker import Locker
from .models import Command, GameDataPackage, Room, db from .models import Command, GameDataPackage, Room, db
@@ -100,7 +97,6 @@ class WebHostContext(Context):
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext) self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
command.delete() command.delete()
commit() commit()
del commands
time.sleep(5) time.sleep(5)
@db_session @db_session
@@ -150,13 +146,13 @@ class WebHostContext(Context):
self.location_name_groups = static_location_name_groups self.location_name_groups = static_location_name_groups
return self._load(multidata, game_data_packages, True) return self._load(multidata, game_data_packages, True)
@db_session
def init_save(self, enabled: bool = True): def init_save(self, enabled: bool = True):
self.saving = enabled self.saving = enabled
if self.saving: if self.saving:
with db_session: savegame_data = Room.get(id=self.room_id).multisave
savegame_data = Room.get(id=self.room_id).multisave if savegame_data:
if savegame_data: self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
self._start_async_saving(atexit_save=False) self._start_async_saving(atexit_save=False)
threading.Thread(target=self.listen_to_db_commands, daemon=True).start() threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
@@ -286,12 +282,8 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
assert ctx.server is None assert ctx.server is None
try: try:
ctx.server = websockets.serve( ctx.server = websockets.serve(
functools.partial(server, ctx=ctx), functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context())
ctx.host,
ctx.port,
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
await ctx.server await ctx.server
except OSError: # likely port in use except OSError: # likely port in use
ctx.server = websockets.serve( ctx.server = websockets.serve(
@@ -312,7 +304,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
with db_session: with db_session:
room = Room.get(id=ctx.room_id) room = Room.get(id=ctx.room_id)
room.last_port = port room.last_port = port
del room
else: else:
ctx.logger.exception("Could not determine port. Likely hosting failure.") ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session: with db_session:
@@ -331,7 +322,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
with db_session: with db_session:
room = Room.get(id=room_id) room = Room.get(id=room_id)
room.last_port = -1 room.last_port = -1
del room
logger.exception(e) logger.exception(e)
raise raise
else: else:
@@ -343,12 +333,11 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
ctx.exit_event.set() # make sure the saving thread stops at some point ctx.exit_event.set() # make sure the saving thread stops at some point
# NOTE: async saving should probably be an async task and could be merged with shutdown_task # NOTE: async saving should probably be an async task and could be merged with shutdown_task
with db_session: with (db_session):
# ensure the Room does not spin up again on its own, minute of safety buffer # ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id) room = Room.get(id=room_id)
room.last_activity = datetime.datetime.utcnow() - \ room.last_activity = datetime.datetime.utcnow() - \
datetime.timedelta(minutes=1, seconds=room.timeout) datetime.timedelta(minutes=1, seconds=room.timeout)
del room
logging.info(f"Shutting down room {room_id} on {name}.") logging.info(f"Shutting down room {room_id} on {name}.")
finally: finally:
await asyncio.sleep(5) await asyncio.sleep(5)

View File

@@ -14,7 +14,7 @@ from pony.orm import commit, db_session
from BaseClasses import get_seed, seeddigits from BaseClasses import get_seed, seeddigits
from Generate import PlandoOptions, handle_name, mystery_argparse from Generate import PlandoOptions, handle_name, mystery_argparse
from Main import main as ERmain from Main import main as ERmain
from Utils import __version__, restricted_dumps, DaemonThreadPoolExecutor from Utils import __version__, restricted_dumps
from WebHostLib import app from WebHostLib import app
from settings import ServerOptions, GeneratorOptions from settings import ServerOptions, GeneratorOptions
from .check import get_yaml_data, roll_options from .check import get_yaml_data, roll_options
@@ -33,7 +33,6 @@ def get_meta(options_source: dict, race: bool = False) -> dict[str, list[str] |
"release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)), "release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)),
"remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)), "remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)),
"collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)), "collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)),
"countdown_mode": str(options_source.get("countdown_mode", ServerOptions.countdown_mode)),
"item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))), "item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))),
"server_password": str(options_source.get("server_password", None)), "server_password": str(options_source.get("server_password", None)),
} }
@@ -73,10 +72,6 @@ def generate(race=False):
return render_template("generate.html", race=race, version=__version__) return render_template("generate.html", race=race, version=__version__)
def format_exception(e: BaseException) -> str:
return f"{e.__class__.__name__}: {e}"
def start_generation(options: dict[str, dict | str], meta: dict[str, Any]): def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
results, gen_options = roll_options(options, set(meta["plando_options"])) results, gen_options = roll_options(options, set(meta["plando_options"]))
@@ -97,9 +92,7 @@ def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
except PicklingError as e: except PicklingError as e:
from .autolauncher import handle_generation_failure from .autolauncher import handle_generation_failure
handle_generation_failure(e) handle_generation_failure(e)
meta["error"] = format_exception(e) return render_template("seedError.html", seed_error=("PicklingError: " + str(e)))
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
commit() commit()
@@ -107,18 +100,16 @@ def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
else: else:
try: try:
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()}, seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
meta=meta, owner=session["_id"].int, timeout=app.config["JOB_TIME"]) meta=meta, owner=session["_id"].int)
except BaseException as e: except BaseException as e:
from .autolauncher import handle_generation_failure from .autolauncher import handle_generation_failure
handle_generation_failure(e) handle_generation_failure(e)
meta["error"] = format_exception(e) return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
return redirect(url_for("view_seed", seed=seed_id)) return redirect(url_for("view_seed", seed=seed_id))
def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None, timeout: int|None = None): def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None):
if meta is None: if meta is None:
meta = {} meta = {}
@@ -137,7 +128,7 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)) seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
args = mystery_argparse([]) # Just to set up the Namespace with defaults args = mystery_argparse()
args.multi = playercount args.multi = playercount
args.seed = seed args.seed = seed
args.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery args.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
@@ -172,12 +163,11 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
ERmain(args, seed, baked_server_options=meta["server_options"]) ERmain(args, seed, baked_server_options=meta["server_options"])
return upload_to_db(target.name, sid, owner, race) return upload_to_db(target.name, sid, owner, race)
thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
thread_pool = DaemonThreadPoolExecutor(max_workers=1)
thread = thread_pool.submit(task) thread = thread_pool.submit(task)
try: try:
return thread.result(timeout) return thread.result(app.config["JOB_TIME"])
except concurrent.futures.TimeoutError as e: except concurrent.futures.TimeoutError as e:
if sid: if sid:
with db_session: with db_session:
@@ -185,14 +175,11 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
if gen is not None: if gen is not None:
gen.state = STATE_ERROR gen.state = STATE_ERROR
meta = json.loads(gen.meta) meta = json.loads(gen.meta)
meta["error"] = ("Allowed time for Generation exceeded, " + meta["error"] = (
"please consider generating locally instead. " + "Allowed time for Generation exceeded, please consider generating locally instead. " +
format_exception(e)) e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta) gen.meta = json.dumps(meta)
commit() commit()
except (KeyboardInterrupt, SystemExit):
# don't update db, retry next time
raise
except BaseException as e: except BaseException as e:
if sid: if sid:
with db_session: with db_session:
@@ -200,15 +187,10 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
if gen is not None: if gen is not None:
gen.state = STATE_ERROR gen.state = STATE_ERROR
meta = json.loads(gen.meta) meta = json.loads(gen.meta)
meta["error"] = format_exception(e) meta["error"] = (e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta) gen.meta = json.dumps(meta)
commit() commit()
raise raise
finally:
# free resources claimed by thread pool, if possible
# NOTE: Timeout depends on the process being killed at some point
# since we can't actually cancel a running gen at the moment.
thread_pool.shutdown(wait=False, cancel_futures=True)
@app.route('/wait/<suuid:seed>') @app.route('/wait/<suuid:seed>')
@@ -222,9 +204,7 @@ def wait_seed(seed: UUID):
if not generation: if not generation:
return "Generation not found." return "Generation not found."
elif generation.state == STATE_ERROR: elif generation.state == STATE_ERROR:
meta = json.loads(generation.meta) return render_template("seedError.html", seed_error=generation.meta)
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
return render_template("waitSeed.html", seed_id=seed_id) return render_template("waitSeed.html", seed_id=seed_id)

View File

@@ -1,90 +0,0 @@
import re
from collections import Counter
import mistune
from werkzeug.utils import secure_filename
__all__ = [
"ImgUrlRewriteInlineParser",
'render_markdown',
]
class ImgUrlRewriteInlineParser(mistune.InlineParser):
relative_url_base: str
def __init__(self, relative_url_base: str, hard_wrap: bool = False) -> None:
super().__init__(hard_wrap)
self.relative_url_base = relative_url_base
@staticmethod
def _find_game_name_by_folder_name(name: str) -> str | None:
from worlds.AutoWorld import AutoWorldRegister
for world_name, world_type in AutoWorldRegister.world_types.items():
if world_type.__module__ == f"worlds.{name}":
return world_name
return None
def parse_link(self, m: re.Match[str], state: mistune.InlineState) -> int | None:
res = super().parse_link(m, state)
if res is not None and state.tokens and state.tokens[-1]["type"] == "image":
image_token = state.tokens[-1]
url: str = image_token["attrs"]["url"]
if not url.startswith("/") and not "://" in url:
# replace relative URL to another world's doc folder with the webhost folder layout
if url.startswith("../../") and "/docs/" in self.relative_url_base:
parts = url.split("/", 4)
if parts[2] != ".." and parts[3] == "docs":
game_name = self._find_game_name_by_folder_name(parts[2])
if game_name is not None:
url = "/".join(parts[1:2] + [secure_filename(game_name)] + parts[4:])
# change relative URL to point to deployment folder
url = f"{self.relative_url_base}/{url}"
image_token['attrs']['url'] = url
return res
def render_markdown(path: str, img_url_base: str | None = None) -> str:
markdown = mistune.create_markdown(
escape=False,
plugins=[
"strikethrough",
"footnotes",
"table",
"speedup",
],
)
heading_id_count: Counter[str] = Counter()
def heading_id(text: str) -> str:
nonlocal heading_id_count
# there is no good way to do this without regex
s = re.sub(r"[^\w\- ]", "", text.lower()).replace(" ", "-").strip("-")
n = heading_id_count[s]
heading_id_count[s] += 1
if n > 0:
s += f"-{n}"
return s
def id_hook(_: mistune.Markdown, state: mistune.BlockState) -> None:
for tok in state.tokens:
if tok["type"] == "heading" and tok["attrs"]["level"] < 4:
text = tok["text"]
assert isinstance(text, str)
unique_id = heading_id(text)
tok["attrs"]["id"] = unique_id
tok["text"] = f"<a href=\"#{unique_id}\">{text}</a>" # make header link to itself
markdown.before_render_hooks.append(id_hook)
if img_url_base:
markdown.inline = ImgUrlRewriteInlineParser(img_url_base)
with open(path, encoding="utf-8-sig") as f:
document = f.read()
html = markdown(document)
assert isinstance(html, str), "Unexpected mistune renderer in render_markdown"
return html

View File

@@ -1,7 +1,5 @@
import datetime import datetime
import os import os
import warnings
from enum import StrEnum
from typing import Any, IO, Dict, Iterator, List, Tuple, Union from typing import Any, IO, Dict, Iterator, List, Tuple, Union
import jinja2.exceptions import jinja2.exceptions
@@ -11,29 +9,14 @@ from werkzeug.utils import secure_filename
from worlds.AutoWorld import AutoWorldRegister, World from worlds.AutoWorld import AutoWorldRegister, World
from . import app, cache from . import app, cache
from .markdown import render_markdown
from .models import Seed, Room, Command, UUID, uuid4 from .models import Seed, Room, Command, UUID, uuid4
from Utils import title_sorted from Utils import title_sorted
class WebWorldTheme(StrEnum):
DIRT = "dirt"
GRASS = "grass"
GRASS_FLOWERS = "grassFlowers"
ICE = "ice"
JUNGLE = "jungle"
OCEAN = "ocean"
PARTY_TIME = "partyTime"
STONE = "stone"
def get_world_theme(game_name: str) -> str: def get_world_theme(game_name: str) -> str:
if game_name not in AutoWorldRegister.world_types: if game_name in AutoWorldRegister.world_types:
return "grass" return AutoWorldRegister.world_types[game_name].web.theme
chosen_theme = AutoWorldRegister.world_types[game_name].web.theme return 'grass'
available_themes = [theme.value for theme in WebWorldTheme]
if chosen_theme not in available_themes:
warnings.warn(f"Theme '{chosen_theme}' for {game_name} not valid, switching to default 'grass' theme.")
return "grass"
return chosen_theme
def get_visible_worlds() -> dict[str, type(World)]: def get_visible_worlds() -> dict[str, type(World)]:
@@ -44,6 +27,49 @@ def get_visible_worlds() -> dict[str, type(World)]:
return worlds return worlds
def render_markdown(path: str) -> str:
import mistune
from collections import Counter
markdown = mistune.create_markdown(
escape=False,
plugins=[
"strikethrough",
"footnotes",
"table",
"speedup",
],
)
heading_id_count: Counter[str] = Counter()
def heading_id(text: str) -> str:
nonlocal heading_id_count
import re # there is no good way to do this without regex
s = re.sub(r"[^\w\- ]", "", text.lower()).replace(" ", "-").strip("-")
n = heading_id_count[s]
heading_id_count[s] += 1
if n > 0:
s += f"-{n}"
return s
def id_hook(_: mistune.Markdown, state: mistune.BlockState) -> None:
for tok in state.tokens:
if tok["type"] == "heading" and tok["attrs"]["level"] < 4:
text = tok["text"]
assert isinstance(text, str)
unique_id = heading_id(text)
tok["attrs"]["id"] = unique_id
tok["text"] = f"<a href=\"#{unique_id}\">{text}</a>" # make header link to itself
markdown.before_render_hooks.append(id_hook)
with open(path, encoding="utf-8-sig") as f:
document = f.read()
return markdown(document)
@app.errorhandler(404) @app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound) @app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err): def page_not_found(err):
@@ -65,9 +91,10 @@ def game_info(game, lang):
theme = get_world_theme(game) theme = get_world_theme(game)
secure_game_name = secure_filename(game) secure_game_name = secure_filename(game)
lang = secure_filename(lang) lang = secure_filename(lang)
file_dir = os.path.join(app.static_folder, "generated", "docs", secure_game_name) document = render_markdown(os.path.join(
file_dir_url = url_for("static", filename=f"generated/docs/{secure_game_name}") app.static_folder, "generated", "docs",
document = render_markdown(os.path.join(file_dir, f"{lang}_{secure_game_name}.md"), file_dir_url) secure_game_name, f"{lang}_{secure_game_name}.md"
))
return render_template( return render_template(
"markdown_document.html", "markdown_document.html",
title=f"{game} Guide", title=f"{game} Guide",
@@ -92,9 +119,10 @@ def tutorial(game: str, file: str):
theme = get_world_theme(game) theme = get_world_theme(game)
secure_game_name = secure_filename(game) secure_game_name = secure_filename(game)
file = secure_filename(file) file = secure_filename(file)
file_dir = os.path.join(app.static_folder, "generated", "docs", secure_game_name) document = render_markdown(os.path.join(
file_dir_url = url_for("static", filename=f"generated/docs/{secure_game_name}") app.static_folder, "generated", "docs",
document = render_markdown(os.path.join(file_dir, f"{file}.md"), file_dir_url) secure_game_name, file+".md"
))
return render_template( return render_template(
"markdown_document.html", "markdown_document.html",
title=f"{game} Guide", title=f"{game} Guide",
@@ -243,9 +271,9 @@ def host_room(room: UUID):
or "Discordbot" in request.user_agent.string or "Discordbot" in request.user_agent.string
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens)) or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
def get_log(max_size: int = 0 if automated else 1024000) -> Tuple[str, int]: def get_log(max_size: int = 0 if automated else 1024000) -> str:
if max_size == 0: if max_size == 0:
return "", 0 return ""
try: try:
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log: with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
raw_size = 0 raw_size = 0
@@ -256,9 +284,9 @@ def host_room(room: UUID):
break break
raw_size += len(block) raw_size += len(block)
fragments.append(block.decode("utf-8")) fragments.append(block.decode("utf-8"))
return "".join(fragments), raw_size return "".join(fragments)
except FileNotFoundError: except FileNotFoundError:
return "", 0 return ""
return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log) return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log)

View File

@@ -13,7 +13,6 @@ from Utils import local_path
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from . import app, cache from . import app, cache
from .generate import get_meta from .generate import get_meta
from .misc import get_world_theme
def create() -> None: def create() -> None:
@@ -23,6 +22,12 @@ def create() -> None:
Options.generate_yaml_templates(yaml_folder) Options.generate_yaml_templates(yaml_folder)
def get_world_theme(game_name: str) -> str:
if game_name in AutoWorldRegister.world_types:
return AutoWorldRegister.world_types[game_name].web.theme
return 'grass'
def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]: def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]:
world = AutoWorldRegister.world_types[world_name] world = AutoWorldRegister.world_types[world_name]
if world.hidden or world.web.options_page is False: if world.hidden or world.web.options_page is False:
@@ -71,7 +76,7 @@ def filter_rst_to_html(text: str) -> str:
lines = text.splitlines() lines = text.splitlines()
text = lines[0] + "\n" + dedent("\n".join(lines[1:])) text = lines[0] + "\n" + dedent("\n".join(lines[1:]))
return publish_parts(text, writer='html', settings=None, settings_overrides={ return publish_parts(text, writer_name='html', settings=None, settings_overrides={
'raw_enable': False, 'raw_enable': False,
'file_insertion_enabled': False, 'file_insertion_enabled': False,
'output_encoding': 'unicode' 'output_encoding': 'unicode'
@@ -226,7 +231,7 @@ def generate_yaml(game: str):
if key_parts[-1] == "qty": if key_parts[-1] == "qty":
if key_parts[0] not in options: if key_parts[0] not in options:
options[key_parts[0]] = {} options[key_parts[0]] = {}
if val and val != "0": if val != "0":
options[key_parts[0]][key_parts[1]] = int(val) options[key_parts[0]][key_parts[1]] = int(val)
del options[key] del options[key]

View File

@@ -4,10 +4,9 @@ pony>=0.7.19; python_version <= '3.12'
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13' pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
waitress>=3.0.2 waitress>=3.0.2
Flask-Caching>=2.3.0 Flask-Caching>=2.3.0
Flask-Compress==1.18 # pkg_resources can't resolve the "backports.zstd" dependency of >1.18, breaking ModuleUpdate.py Flask-Compress>=1.17
Flask-Limiter>=3.12 Flask-Limiter>=3.12
bokeh>=3.6.3 bokeh>=3.6.3
markupsafe>=3.0.2 markupsafe>=3.0.2
setproctitle>=1.3.5 setproctitle>=1.3.5
mistune>=3.1.3 mistune>=3.1.3
docutils>=0.22.2

View File

@@ -23,7 +23,7 @@ players to rely upon each other to complete their game.
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
players to randomize any of the supported games, and send items between them. This allows players of different players to randomize any of the supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds. games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
Here is a list of our [Supported Games](/games). Here is a list of our [Supported Games](https://archipelago.gg/games).
## Can I generate a single-player game with Archipelago? ## Can I generate a single-player game with Archipelago?
@@ -33,7 +33,7 @@ play, open the Settings Page, pick your settings, and click Generate Game.
## How do I get started? ## How do I get started?
We have a [Getting Started](/tutorial/Archipelago/setup/en) guide that will help you get the We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
including multiple games, and hosting multiworlds on the website for ease and convenience. including multiple games, and hosting multiworlds on the website for ease and convenience.
@@ -57,7 +57,7 @@ their multiworld.
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
in that game belonging to other players are sent out automatically. This allows other players to continue to play in that game belonging to other players are sent out automatically. This allows other players to continue to play
uninterrupted. Here is a list of all of our [Server Commands](/tutorial/Archipelago/commands/en). uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en).
## What happens if an item is placed somewhere it is impossible to get? ## What happens if an item is placed somewhere it is impossible to get?
@@ -66,7 +66,7 @@ is to ensure items necessary to complete the game will be accessible to the play
rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are
comfortable exploiting certain glitches in the game. comfortable exploiting certain glitches in the game.
## I want to develop a game implementation for Archipelago. How do I do that? ## I want to add a game to the Archipelago randomizer. How do I do that?
The best way to get started is to take a look at our code on GitHub: The best way to get started is to take a look at our code on GitHub:
[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago). [Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
@@ -77,5 +77,4 @@ There, you will find examples of games in the `worlds` folder:
You may also find developer documentation in the `docs` folder: You may also find developer documentation in the `docs` folder:
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
If you have more questions regarding development of a game implementation, feel free to ask in the **#ap-world-dev** If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
channel on our Discord.

View File

@@ -241,9 +241,12 @@ input[type="checkbox"]{
} }
/* Hidden items */ /* Hidden items */
.hidden-class:not(:has(.f:not(.unacquired))), .hidden-item{ .hidden-class:not(:has(img.acquired)){
display: none; display: none;
} }
.hidden-item:not(.acquired){
display:none;
}
/* Keys */ /* Keys */
#keys ol, #keys ul{ #keys ol, #keys ul{

View File

@@ -72,13 +72,3 @@ code{
padding-right: 0.25rem; padding-right: 0.25rem;
color: #000000; color: #000000;
} }
code.grassy {
background-color: #b5e9a4;
border: 1px solid #2a6c2f;
white-space: preserve;
text-align: left;
display: block;
font-size: 14px;
line-height: 20px;
}

View File

@@ -13,7 +13,3 @@
min-height: 360px; min-height: 360px;
text-align: center; text-align: center;
} }
h2, h4 {
color: #ffffff;
}

View File

@@ -58,7 +58,8 @@
Open Log File... Open Log File...
</a> </a>
</div> </div>
{% set log, log_len = get_log() -%} {% set log = get_log() -%}
{%- set log_len = log | length - 1 if log.endswith("…") else log | length -%}
<div id="logger" style="white-space: pre">{{ log }}</div> <div id="logger" style="white-space: pre">{{ log }}</div>
<script> <script>
let url = '{{ url_for('display_log', room = room.id) }}'; let url = '{{ url_for('display_log', room = room.id) }}';

View File

@@ -4,20 +4,16 @@
{% block head %} {% block head %}
<title>Generation failed, please retry.</title> <title>Generation failed, please retry.</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/waitSeed.css') }}"/> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% include 'header/oceanIslandHeader.html' %} {% include 'header/oceanIslandHeader.html' %}
<div id="wait-seed-wrapper" class="grass-island"> <div id="wait-seed-wrapper" class="grass-island">
<div id="wait-seed"> <div id="wait-seed">
<h1>Generation Failed</h1> <h1>Generation failed</h1>
<h2>Please try again!</h2> <h2>please retry</h2>
<p>{{ seed_error }}</p> {{ seed_error }}
<h4>More details:</h4>
<p>
<code class="grassy">{{ details }}</code>
</p>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -31,9 +31,6 @@
{% include 'header/oceanHeader.html' %} {% include 'header/oceanHeader.html' %}
<div id="games" class="markdown"> <div id="games" class="markdown">
<h1>Currently Supported Games</h1> <h1>Currently Supported Games</h1>
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
custom worlds</a> section of the setup guide.</p>
<div class="js-only"> <div class="js-only">
<label for="game-search">Search for your game below!</label><br /> <label for="game-search">Search for your game below!</label><br />
<div class="page-controls"> <div class="page-controls">

File diff suppressed because it is too large Load Diff

View File

@@ -30,10 +30,21 @@
} }
const data = await response.json(); const data = await response.json();
waitSeedDiv.innerHTML = ` if (data.queue_len === 1){
<h1>Generation in Progress</h1> waitSeedDiv.innerHTML = `
<p>${data.text}</p> <h1>Generation in Progress</h1>
`; <p>${data.text}</p>
<p>This is the only generation in the queue.</p>
`;
}
else {
waitSeedDiv.innerHTML = `
<h1>Generation in Progress</h1>
<p>${data.text}</p>
<p>There are ${data.queue_len} generations in the queue.</p>
`;
}
setTimeout(checkStatus, 1000); // Continue polling. setTimeout(checkStatus, 1000); // Continue polling.
} catch (error) { } catch (error) {

View File

@@ -959,7 +959,7 @@ if "Timespinner" in network_data_package["games"]:
timespinner_location_ids = { timespinner_location_ids = {
"Present": list(range(1337000, 1337085)), "Present": list(range(1337000, 1337085)),
"Past": list(range(1337086, 1337157)) + list(range(1337159, 1337175)), "Past": list(range(1337086, 1337175)),
"Ancient Pyramid": [ "Ancient Pyramid": [
1337236, 1337236,
1337246, 1337247, 1337248, 1337249] 1337246, 1337247, 1337248, 1337249]
@@ -1228,7 +1228,7 @@ if "Starcraft 2" in network_data_package["games"]:
def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) -> str: def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
SC2WOL_ITEM_ID_OFFSET = 1000 SC2WOL_ITEM_ID_OFFSET = 1000
SC2HOTS_ITEM_ID_OFFSET = 2000 SC2HOTS_ITEM_ID_OFFSET = 2000
SC2LOTV_ITEM_ID_OFFSET = 3000 SC2LOTV_ITEM_ID_OFFSET = 2000
SC2_KEY_ITEM_ID_OFFSET = 4000 SC2_KEY_ITEM_ID_OFFSET = 4000
NCO_LOCATION_ID_LOW = 20004500 NCO_LOCATION_ID_LOW = 20004500
NCO_LOCATION_ID_HIGH = NCO_LOCATION_ID_LOW + 1000 NCO_LOCATION_ID_HIGH = NCO_LOCATION_ID_LOW + 1000

View File

@@ -289,7 +289,7 @@ async def nes_sync_task(ctx: ZeldaContext):
if not ctx.auth: if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0]) ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '': if ctx.auth == '':
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate " logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
"the ROM using the same link but adding your slot name") "the ROM using the same link but adding your slot name")
if ctx.awaiting_rom: if ctx.awaiting_rom:
await ctx.server_auth(False) await ctx.server_auth(False)

View File

@@ -1,2 +0,0 @@
pytest>=9.0.1,<10 # this includes subtests support
pytest-xdist>=3.8.0

View File

@@ -224,7 +224,6 @@
height: self.content.texture_size[1] + 80 height: self.content.texture_size[1] + 80
<ScrollBox>: <ScrollBox>:
layout: layout layout: layout
box_height: dp(100)
bar_width: "12dp" bar_width: "12dp"
scroll_wheel_distance: 40 scroll_wheel_distance: 40
do_scroll_x: False do_scroll_x: False
@@ -235,11 +234,4 @@
orientation: "vertical" orientation: "vertical"
spacing: 10 spacing: 10
size_hint_y: None size_hint_y: None
height: max(self.minimum_height, root.box_height) height: self.minimum_height
<MessageBoxLabel>:
valign: "middle"
halign: "center"
text_size: self.width, None
height: self.texture_size[1]

View File

@@ -1,174 +0,0 @@
<VisualRange>:
id: this
spacing: 15
orientation: "horizontal"
slider: slider
tag: tag
MDLabel:
id: tag
text: str(this.option.default) if not isinstance(this.option.default, str) else str(this.option.range_start)
MDSlider:
id: slider
min: this.option.range_start
max: this.option.range_end
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if not isinstance(this.option.default, str) else this.option.range_start
step: 1
step_point_size: 0
MDSliderHandle:
MDSliderValueLabel:
<VisualChoice>:
id: this
text: text
MDButtonText:
id: text
text: this.option.get_option_name(this.option.default if not isinstance(this.option.default, str) else list(this.option.options.values())[0])
theme_text_color: "Primary"
<VisualNamedRange>:
id: this
orientation: "horizontal"
spacing: "10dp"
padding: (0, 0, "10dp", 0)
choice: choice
MDButton:
id: choice
text: text
MDButtonText:
id: text
text: this.option.default.title() if this.option.default in this.option.special_range_names else "Custom"
<VisualFreeText>:
multiline: False
font_size: "15sp"
text: self.option.default if isinstance(self.option.default, str) else ""
theme_height: "Custom"
height: "30dp"
<VisualTextChoice>:
id: this
orientation: "horizontal"
spacing: "5dp"
padding: (0, 0, "10dp", 0)
<VisualToggle>:
id: this
button: button
MDIconButton:
id: button
icon: "checkbox-outline" if this.option.default else "checkbox-blank-outline"
<VisualListSetEntry@ResizableTextField>:
height: "20dp"
<CounterItemValue>:
height: "30dp"
<VisualListSetCounter>:
id: this
scrollbox: scrollbox
add: add
save: save
input: input
focus_behavior: False
MDDialogHeadlineText:
text: getattr(this.option, "display_name", this.name)
MDDialogSupportingText:
text: "Add or Remove Entries"
MDDialogContentContainer:
orientation: "vertical"
spacing: 10
MDBoxLayout:
orientation: "horizontal"
VisualListSetEntry:
id: input
height: "20dp"
MDIconButton:
id: add
icon: "plus"
theme_height: "Custom"
height: "20dp"
on_press: root.validate_add(input)
ScrollBox:
id: scrollbox
size_hint_y: None
adapt_minimum: False
MDButton:
id: save
MDButtonText:
text: "Save Changes"
ContainerLayout:
md_bg_color: app.theme_cls.backgroundColor
MainLayout:
id: main
cols: 3
padding: 3, 5, 0, 3
spacing: "2dp"
ScrollBox:
id: scrollbox
size_hint_x: None
width: "150dp"
MDDivider:
orientation: "vertical"
width: "4dp"
MainLayout:
id: player_layout
rows: 2
spacing: "20dp"
MDBoxLayout:
id: player_options
orientation: "horizontal"
height: "75dp"
size_hint_y: None
padding: ["10dp", "30dp", "10dp", 0]
spacing: "10dp"
ResizableTextField:
id: player_name
multiline: False
MDTextFieldHintText:
text: "Player Name"
MDTextFieldMaxLengthText:
max_text_length: 16
MDBoxLayout:
orientation: "vertical"
spacing: "15dp"
MDLabel:
id: game
text: "Game: None"
pos_hint: {"center_x": 0.5, "center_y": 0.5}
MDButton:
pos_hint: {"center_x": 0.5, "center_y": 0.5}
on_press: app.export_options(self)
theme_width: "Custom"
size_hint_y: 1
size_hint_x: 1
MDButtonText:
pos_hint: {"center_x": 0.5, "center_y": 0.5}
text: "Export Options"
MainLayout:
cols: 1
id: options

View File

@@ -8,7 +8,3 @@ SELFLAUNCH: false
# Host Address. This is the address encoded into the patch that will be used for client auto-connect. # Host Address. This is the address encoded into the patch that will be used for client auto-connect.
# Set as your local IP (192.168.x.x) to serve over LAN. # Set as your local IP (192.168.x.x) to serve over LAN.
HOST_ADDRESS: localhost HOST_ADDRESS: localhost
# Asset redistribution rights. If true, the host affirms they have been given explicit permission to redistribute
# the proprietary assets in WebHostLib
#ASSET_RIGHTS: false

View File

@@ -15,10 +15,6 @@
# A Link to the Past # A Link to the Past
/worlds/alttp/ @Berserker66 /worlds/alttp/ @Berserker66
# APQuest
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
/worlds/apquest/ @NewSoupVi
# Sudoku (APSudoku) # Sudoku (APSudoku)
/worlds/apsudoku/ @EmilyV99 /worlds/apsudoku/ @EmilyV99

View File

@@ -1,83 +1,40 @@
# APWorld Specification # apworld Specification
Archipelago depends on worlds to provide game-specific details like items, locations and output generation. Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
These are called "APWorlds". Those are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
They are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
See [world api.md](world%20api.md) for details. See [world api.md](world%20api.md) for details.
APWorlds can either be a folder, or they can be packaged as an .apworld file.
## .apworld File Format apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
file into the worlds folder.
The `.apworld` file format provides a way to package and ship an APWorld that is not part of the main distribution **Warning:** apworlds have to be all lower case, otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
by placing a `*.apworld` file into the worlds folder.
`.apworld` files are zip archives, all lower case, with the file ending `.apworld`.
## File Format
apworld files are zip archives, all lower case, with the file ending `.apworld`.
The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in
the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`. the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`.
**Warning:** `.apworld` files have to be all lower case,
otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
## Metadata ## Metadata
Metadata about the APWorld is defined in an `archipelago.json` file. Metadata about the apworld is defined in an `archipelago.json` file inside the zip archive.
The current format version has at minimum:
If the APWorld is a folder, the only required field is "game":
```json ```json
{ {
"game": "Game Name" "version": 6,
} "compatible_version": 5,
```
There are also the following optional fields:
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
Archipelago version respectively to filter those files from being loaded.
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
An APWorld without a world_version is always treated as older than one with a version
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
package managers. Should always be a list of strings.
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
These get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
["Build apworlds" launcher component](#build-apworlds-launcher-component),
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
### "Build APWorlds" Launcher Component
In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
and add `archipelago.json` manifest files to them.
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
The `archipelago.json` file in each .apworld will automatically include the appropriate
`version` and `compatible_version`.
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
So, a world folder with an `archipelago.json` that looks like this:
```json
{
"game": "Game Name",
"minimum_ap_version": "0.6.4",
"world_version": "2.1.4",
"authors": ["NewSoupVi"]
}
```
will be packaged into an `.apworld` with a manifest file inside of it that looks like this:
```json
{
"minimum_ap_version": "0.6.4",
"world_version": "2.1.4",
"authors": ["NewSoupVi"],
"version": 7,
"compatible_version": 7,
"game": "Game Name" "game": "Game Name"
} }
``` ```
This is the recommended workflow for packaging your world to an `.apworld`. with the following optional version fields using the format `"1.0.0"` to represent major.minor.build:
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
Archipelago version respectively to filter those files from being loaded
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
An apworld without a world_version is always treated as older than one with a version
## Extra Data ## Extra Data
@@ -86,7 +43,7 @@ The zip can contain arbitrary files in addition what was specified above.
## Caveats ## Caveats
Imports from other files inside the APWorld have to use relative imports. e.g. `from .options import MyGameOptions` Imports from other files inside the apworld have to use relative imports. e.g. `from .options import MyGameOptions`
Imports from AP base have to use absolute imports, e.g. `from Options import Toggle` or Imports from AP base have to use absolute imports, e.g. `from Options import Toggle` or
`from worlds.AutoWorld import World` `from worlds.AutoWorld import World`

View File

@@ -647,16 +647,6 @@ class Version(NamedTuple):
build: int build: int
``` ```
If constructing version information as a dict for a custom client rather than as a NamedTuple built into the CommonClient, you must add the `class` key to allow Archipelago to compare version support.
```
"version": {
"class": "Version",
"build": X,
"major": Y,
"minor": Z
}
```
### SlotType ### SlotType
An enum representing the nature of a slot. An enum representing the nature of a slot.

View File

@@ -269,8 +269,7 @@ placed on them.
### PriorityLocations ### PriorityLocations
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
the pool. Progression items without a deprioritized flag will be used first when filling priority_locations. Progression items with the pool.
a deprioritized flag will be used next.
### ItemLinks ### ItemLinks
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between Allows users to share their item pool with other players. Currently item links are per game. A link of one game between

View File

@@ -385,8 +385,6 @@ Will provide a dict of static tracker data with the following keys:
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary - This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
- The number of checks found vs. total checks available per player (`player_locations_total`) - The number of checks found vs. total checks available per player (`player_locations_total`)
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks). - Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
- The game each player is playing (`player_game`)
- Provided as a list of objects with `team`, `player`, and `game`.
Example: Example:
```json ```json
@@ -411,10 +409,10 @@ Example:
], ],
"datapackage": { "datapackage": {
"Archipelago": { "Archipelago": {
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb" "checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb",
}, },
"The Messenger": { "The Messenger": {
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b" "checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b",
} }
}, },
"player_locations_total": [ "player_locations_total": [
@@ -429,18 +427,6 @@ Example:
"total_locations": 20 "total_locations": 20
} }
], ],
"player_game": [
{
"team": 0,
"player": 1,
"game": "Archipelago"
},
{
"team": 0,
"player": 2,
"game": "The Messenger"
}
]
} }
``` ```

View File

@@ -525,7 +525,7 @@ def randomize_entrances(
running_time = time.perf_counter() - start_time running_time = time.perf_counter() - start_time
if running_time > 1.0: if running_time > 1.0:
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player}, " logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player},"
f"named {world.multiworld.player_name[world.player]}") f"named {world.multiworld.player_name[world.player]}")
return er_state return er_state

View File

@@ -180,8 +180,8 @@ Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{a
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";

32
kvui.py
View File

@@ -34,17 +34,6 @@ from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch") Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set("kivy", "exit_on_escape", "0") Config.set("kivy", "exit_on_escape", "0")
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
# Workaround for an issue where importing kivy.core.window before loading sounds
# will hang the whole application on Linux once the first sound is loaded.
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import.
# No longer necessary when we switch to kivy 3.0.0, which fixes this issue.
from kivy.core.audio import SoundLoader
for classobj in SoundLoader._classes:
# The least invasive way to force a SoundLoader class to load its audio engine seems to be calling
# .extensions(), which e.g. in audio_sdl2.pyx then calls a function called "mix_init()"
classobj.extensions()
from kivymd.uix.divider import MDDivider from kivymd.uix.divider import MDDivider
from kivy.core.window import Window from kivy.core.window import Window
from kivy.core.clipboard import Clipboard from kivy.core.clipboard import Clipboard
@@ -127,7 +116,7 @@ class ImageButton(MDIconButton):
val = kwargs.pop(kwarg, "None") val = kwargs.pop(kwarg, "None")
if val != "None": if val != "None":
image_args[kwarg.replace("image_", "")] = val image_args[kwarg.replace("image_", "")] = val
super().__init__(**kwargs) super().__init__()
self.image = ApAsyncImage(**image_args) self.image = ApAsyncImage(**image_args)
def set_center(button, center): def set_center(button, center):
@@ -143,7 +132,6 @@ class ImageButton(MDIconButton):
class ScrollBox(MDScrollView): class ScrollBox(MDScrollView):
layout: MDBoxLayout = ObjectProperty(None) layout: MDBoxLayout = ObjectProperty(None)
box_height: int = NumericProperty(dp(100))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -154,7 +142,6 @@ class ToggleButton(MDButton, ToggleButtonBehavior):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ToggleButton, self).__init__(*args, **kwargs) super(ToggleButton, self).__init__(*args, **kwargs)
self.bind(state=self._update_bg) self.bind(state=self._update_bg)
self._update_bg(self, self.state)
def _update_bg(self, _, state: str): def _update_bg(self, _, state: str):
if self.disabled: if self.disabled:
@@ -172,7 +159,7 @@ class ToggleButton(MDButton, ToggleButtonBehavior):
child.text_color = self.theme_cls.onPrimaryColor child.text_color = self.theme_cls.onPrimaryColor
child.icon_color = self.theme_cls.onPrimaryColor child.icon_color = self.theme_cls.onPrimaryColor
else: else:
self.md_bg_color = self.theme_cls.surfaceContainerLowColor self.md_bg_color = self.theme_cls.surfaceContainerLowestColor
for child in self.children: for child in self.children:
if child.theme_text_color == "Primary": if child.theme_text_color == "Primary":
child.theme_text_color = "Custom" child.theme_text_color = "Custom"
@@ -186,6 +173,7 @@ class ToggleButton(MDButton, ToggleButtonBehavior):
class ResizableTextField(MDTextField): class ResizableTextField(MDTextField):
""" """
Resizable MDTextField that manually overrides the builtin sizing. Resizable MDTextField that manually overrides the builtin sizing.
Note that in order to use this, the sizing must be specified from within a .kv rule. Note that in order to use this, the sizing must be specified from within a .kv rule.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -249,7 +237,7 @@ Factory.register("HoverBehavior", HoverBehavior)
class ToolTip(MDTooltipPlain): class ToolTip(MDTooltipPlain):
markup = True pass
class ServerToolTip(ToolTip): class ServerToolTip(ToolTip):
@@ -284,8 +272,6 @@ class TooltipLabel(HovererableLabel, MDTooltip):
def on_mouse_pos(self, window, pos): def on_mouse_pos(self, window, pos):
if not self.get_root_window(): if not self.get_root_window():
return # Abort if not displayed return # Abort if not displayed
if self.disabled:
return
super().on_mouse_pos(window, pos) super().on_mouse_pos(window, pos)
if self.refs and self.hovered: if self.refs and self.hovered:
@@ -852,15 +838,15 @@ class GameManager(ThemedApp):
self.log_panels: typing.Dict[str, Widget] = {} self.log_panels: typing.Dict[str, Widget] = {}
# keep track of last used command to autofill on click # keep track of last used command to autofill on click
self.last_autofillable_command = "!hint" self.last_autofillable_command = "hint"
autofillable_commands = ("!hint_location", "!hint", "!getitem") autofillable_commands = ("hint_location", "hint", "getitem")
original_say = ctx.on_user_say original_say = ctx.on_user_say
def intercept_say(text): def intercept_say(text):
text = original_say(text) text = original_say(text)
if text: if text:
for command in autofillable_commands: for command in autofillable_commands:
if text.startswith(command): if text.startswith("!" + command):
self.last_autofillable_command = command self.last_autofillable_command = command
break break
return text return text
@@ -1113,6 +1099,10 @@ class GameManager(ThemedApp):
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", []) hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
self.hint_log.refresh_hints(hints) self.hint_log.refresh_hints(hints)
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
pass
class LogtoUI(logging.Handler): class LogtoUI(logging.Handler):
def __init__(self, on_log): def __init__(self, on_log):

View File

@@ -1,17 +1,17 @@
colorama>=0.4.6 colorama>=0.4.6
websockets>=13.0.1,<14 websockets>=13.0.1,<14
PyYAML>=6.0.3 PyYAML>=6.0.2
jellyfish>=1.2.1 jellyfish>=1.1.3
jinja2>=3.1.6 jinja2>=3.1.6
schema>=0.7.8 schema>=0.7.7
kivy>=2.3.1 kivy>=2.3.1
bsdiff4>=1.2.6 bsdiff4>=1.2.6
platformdirs>=4.5.0 platformdirs>=4.3.6
certifi>=2025.11.12 certifi>=2025.4.26
cython>=3.2.1 cython>=3.0.12
cymem>=2.0.13 cymem>=2.0.11
orjson>=3.11.4 orjson>=3.10.15
typing_extensions>=4.15.0 typing_extensions>=4.12.2
pyshortcuts>=1.9.6 pyshortcuts>=1.9.1
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
kivymd>=2.0.1.dev0 kivymd>=2.0.1.dev0

View File

@@ -1,16 +0,0 @@
line-length = 120
indent-width = 4
target-version = "py311"
[lint]
select = ["B", "C", "E", "F", "W", "I", "N", "Q", "UP", "RET", "RSE", "RUF", "ISC", "PLC", "PLE", "PLW", "T20", "PERF"]
ignore = [
"B011", # In AP, the use of assert False is essential because we optimise out these statements for release builds.
"C901", # Author disagrees with limiting branch complexity
"N818", # Author agrees with this rule, but Core AP violates this and changing it would be a hassle.
"PLC0415", # In AP, we consider local imports totally fine & necessary
"PLC1802", # Author agrees with this rule, but it literally changes the functionality of the code, which is unsafe.
"PLC1901", # This is just not equivalent
"PLE1141", # Gives false positives when the dict keys are tuples, but does not mention this in the suggested fix.
"UP015", # Explicit is better than implicit, so we'd prefer to keep "r" in open() calls.
]

View File

@@ -579,17 +579,6 @@ class ServerOptions(Group):
"goal" -> Client can ask for remaining items after goal completion "goal" -> Client can ask for remaining items after goal completion
""" """
class CountdownMode(str):
"""
Countdown modes
Determines whether or not a player can initiate a countdown with !countdown
Note that /countdown is always available to the host.
"enabled" -> Client can always initiate a countdown with !countdown.
"disabled" -> Client can never initiate a countdown with !countdown.
"auto" -> !countdown will be available for any room with less than 30 slots.
"""
class AutoShutdown(int): class AutoShutdown(int):
"""Automatically shut down the server after this many seconds without new location checks, 0 to keep running""" """Automatically shut down the server after this many seconds without new location checks, 0 to keep running"""
@@ -624,7 +613,6 @@ class ServerOptions(Group):
release_mode: ReleaseMode = ReleaseMode("auto") release_mode: ReleaseMode = ReleaseMode("auto")
collect_mode: CollectMode = CollectMode("auto") collect_mode: CollectMode = CollectMode("auto")
remaining_mode: RemainingMode = RemainingMode("goal") remaining_mode: RemainingMode = RemainingMode("goal")
countdown_mode: CountdownMode = CountdownMode("auto")
auto_shutdown: AutoShutdown = AutoShutdown(0) auto_shutdown: AutoShutdown = AutoShutdown(0)
compatibility: Compatibility = Compatibility(2) compatibility: Compatibility = Compatibility(2)
log_network: LogNetwork = LogNetwork(0) log_network: LogNetwork = LogNetwork(0)

View File

@@ -146,16 +146,7 @@ def download_SNI() -> None:
signtool: str | None = None signtool: str | None = None
try: try:
import socket with urllib.request.urlopen('http://192.168.206.4:12345/connector/status') as response:
sign_host, sign_port = "192.168.206.4", 12345
# check if the sign_host is on a local network
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect((sign_host, sign_port))
if s.getsockname()[0].rsplit(".", 1)[0] != sign_host.rsplit(".", 1)[0]:
raise ConnectionError() # would go through default route
# configure signtool
with urllib.request.urlopen(f"http://{sign_host}:{sign_port}/connector/status") as response:
html = response.read() html = response.read()
if b"status=OK\n" in html: if b"status=OK\n" in html:
signtool = (r'signtool sign /sha1 6df76fe776b82869a5693ddcb1b04589cffa6faf /fd sha256 /td sha256 ' signtool = (r'signtool sign /sha1 6df76fe776b82869a5693ddcb1b04589cffa6faf /fd sha256 /td sha256 '
@@ -381,6 +372,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
from Options import generate_yaml_templates from Options import generate_yaml_templates
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from worlds.Files import APWorldContainer from worlds.Files import APWorldContainer
from Utils import version
assert not non_apworlds - set(AutoWorldRegister.world_types), \ assert not non_apworlds - set(AutoWorldRegister.world_types), \
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
folders_to_remove: list[str] = [] folders_to_remove: list[str] = []
@@ -390,25 +382,15 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1] file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
world_directory = self.libfolder / "worlds" / file_name world_directory = self.libfolder / "worlds" / file_name
if os.path.isfile(world_directory / "archipelago.json"): if os.path.isfile(world_directory / "archipelago.json"):
with open(os.path.join(world_directory, "archipelago.json"), mode="r", encoding="utf-8") as manifest_file: manifest = json.load(open(world_directory / "archipelago.json"))
manifest = json.load(manifest_file)
assert "game" in manifest, (
f"World directory {world_directory} has an archipelago.json manifest file, but it "
"does not define a \"game\"."
)
assert manifest["game"] == worldtype.game, (
f"World directory {world_directory} has an archipelago.json manifest file, but value of the "
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
)
else: else:
manifest = {} manifest = {}
# this method creates an apworld that cannot be moved to a different OS or minor python version, # this method creates an apworld that cannot be moved to a different OS or minor python version,
# which should be ok # which should be ok
zip_path = self.libfolder / "worlds" / (file_name + ".apworld") zip_path = self.libfolder / "worlds" / (file_name + ".apworld")
apworld = APWorldContainer(str(zip_path)) apworld = APWorldContainer(str(zip_path))
apworld.minimum_ap_version = version_tuple apworld.minimum_ap_version = version
apworld.maximum_ap_version = version_tuple apworld.maximum_ap_version = version
apworld.game = worldtype.game apworld.game = worldtype.game
manifest.update(apworld.get_manifest()) manifest.update(apworld.get_manifest())
apworld.manifest_path = f"{file_name}/archipelago.json" apworld.manifest_path = f"{file_name}/archipelago.json"

View File

@@ -1,227 +0,0 @@
#!/usr/bin/env python
# based on python-websockets compression benchmark (c) Aymeric Augustin and contributors
# https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py
import collections
import time
import zlib
from typing import Iterable
REPEAT = 10
WB, ML = 12, 5 # defaults used as a reference
WBITS = range(9, 16)
MEMLEVELS = range(1, 10)
def benchmark(data: Iterable[bytes]) -> None:
size: dict[int, dict[int, float]] = collections.defaultdict(dict)
duration: dict[int, dict[int, float]] = collections.defaultdict(dict)
for wbits in WBITS:
for memLevel in MEMLEVELS:
encoder = zlib.compressobj(wbits=-wbits, memLevel=memLevel)
encoded = []
print(f"Compressing {REPEAT} times with {wbits=} and {memLevel=}")
t0 = time.perf_counter()
for _ in range(REPEAT):
for item in data:
# Taken from PerMessageDeflate.encode
item = encoder.compress(item) + encoder.flush(zlib.Z_SYNC_FLUSH)
if item.endswith(b"\x00\x00\xff\xff"):
item = item[:-4]
encoded.append(item)
t1 = time.perf_counter()
size[wbits][memLevel] = sum(len(item) for item in encoded) / REPEAT
duration[wbits][memLevel] = (t1 - t0) / REPEAT
raw_size = sum(len(item) for item in data)
print("=" * 79)
print("Compression ratio")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (1 - size[wbits][memLevel] / raw_size):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print("CPU time")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{1000 * duration[wbits][memLevel]:.1f}ms"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print(f"Size vs. {WB} \\ {ML}")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (size[wbits][memLevel] / size[WB][ML] - 1):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print(f"Time vs. {WB} \\ {ML}")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (duration[wbits][memLevel] / duration[WB][ML] - 1):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
def generate_data_package_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +4.6% size, -5.0% time .. +1.1% time
# 10, 4 saves 20K RAM, gives +10.2% size, -3.8% time .. +0.6% time
# 11, 3 saves 20K RAM, gives +6.5% size, +14.2% time
# 10, 3 saves 24K RAM, gives +12.8% size, +0.5% time .. +6.9% time
# NOTE: time delta is highly unstable; time is ~100ms
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from NetUtils import encode
from worlds import network_data_package
return [encode(network_data_package).encode("utf-8")]
def generate_solo_release_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +0.9% size, +3.9% time
# 10, 4 saves 20K RAM, gives +1.4% size, +3.4% time
# 11, 3 saves 20K RAM, gives +1.8% size, +13.9% time
# 10, 3 saves 24K RAM, gives +2.1% size, +4.8% time
# NOTE: time delta is highly unstable; time is ~0.4ms
from random import Random
from MultiServer import json_format_send_event
from NetUtils import encode, NetworkItem
r = Random()
r.seed(0)
solo_release = []
solo_release_locations = [r.randint(1000, 1999) for _ in range(200)]
solo_release_items = sorted([r.randint(1000, 1999) for _ in range(200)]) # currently sorted by item
solo_player = 1
for location, item in zip(solo_release_locations, solo_release_items):
flags = r.choice((0, 0, 0, 0, 0, 0, 0, 1, 2, 3))
network_item = NetworkItem(item, location, solo_player, flags)
solo_release.append(json_format_send_event(network_item, solo_player))
solo_release.append({
"cmd": "ReceivedItems",
"index": 0,
"items": solo_release_items,
})
solo_release.append({
"cmd": "RoomUpdate",
"hint_points": 200,
"checked_locations": solo_release_locations,
})
return [encode(solo_release).encode("utf-8")]
def generate_gameplay_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +13.6% size, +4.1% time
# 10, 4 saves 20K RAM, gives +22.3% size, +2.2% time
# 10, 3 saves 24K RAM, gives +26.2% size, +1.6% time
# NOTE: time delta is highly unstable; time is 4ms
from copy import copy
from random import Random
from MultiServer import json_format_send_event
from NetUtils import encode, NetworkItem
r = Random()
r.seed(0)
gameplay = []
observer = 1
hint_points = 0
index = 0
players = list(range(1, 10))
player_locations = {player: [r.randint(1000, 1999) for _ in range(200)] for player in players}
player_items = {player: [r.randint(1000, 1999) for _ in range(200)] for player in players}
player_receiver = {player: [r.randint(1, len(players)) for _ in range(200)] for player in players}
for i in range(0, len(player_locations[1])):
player_sequence = copy(players)
r.shuffle(player_sequence)
for finder in player_sequence:
flags = r.choice((0, 0, 0, 0, 0, 0, 0, 1, 2, 3))
receiver = player_receiver[finder][i]
item = player_items[finder][i]
location = player_locations[finder][i]
network_item = NetworkItem(item, location, receiver, flags)
gameplay.append(json_format_send_event(network_item, observer))
if finder == observer:
hint_points += 1
gameplay.append({
"cmd": "RoomUpdate",
"hint_points": hint_points,
"checked_locations": [location],
})
if receiver == observer:
gameplay.append({
"cmd": "ReceivedItems",
"index": index,
"items": [item],
})
index += 1
return [encode(gameplay).encode("utf-8")]
def main() -> None:
#corpus = generate_data_package_corpus()
#corpus = generate_solo_release_corpus()
#corpus = generate_gameplay_corpus()
corpus = generate_data_package_corpus() + generate_solo_release_corpus() + generate_gameplay_corpus()
benchmark(corpus)
print(f"raw size: {sum(len(data) for data in corpus)}")
if __name__ == "__main__":
main()

View File

@@ -1,12 +1,4 @@
def run_locations_benchmark(freeze_gc: bool = True) -> None: def run_locations_benchmark():
"""
Run a benchmark of location access rule performance against an empty_state and an all_state.
:param freeze_gc: Whether to freeze gc before benchmarking and unfreeze gc afterward. Freezing gc moves all objects
tracked by the garbage collector to a permanent generation, ignoring them in all future collections. Freezing
greatly reduces the duration of running gc.collect() within benchmarks, which otherwise often takes much longer
than running all iterations for the location rule being benchmarked.
"""
import argparse import argparse
import logging import logging
import gc import gc
@@ -42,8 +34,6 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float: def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
if freeze_gc:
gc.freeze()
with TimeIt(f"{test_location.game} {self.rule_iterations} " with TimeIt(f"{test_location.game} {self.rule_iterations} "
f"runs of {test_location}.access_rule({state_name})", logger) as t: f"runs of {test_location}.access_rule({state_name})", logger) as t:
for _ in range(self.rule_iterations): for _ in range(self.rule_iterations):
@@ -51,8 +41,6 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
# if time is taken to disentangle complex ref chains, # if time is taken to disentangle complex ref chains,
# this time should be attributed to the rule. # this time should be attributed to the rule.
gc.collect() gc.collect()
if freeze_gc:
gc.unfreeze()
return t.dif return t.dif
def main(self): def main(self):
@@ -76,13 +64,9 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
gc.collect() gc.collect()
for step in self.gen_steps: for step in self.gen_steps:
if freeze_gc:
gc.freeze()
with TimeIt(f"{game} step {step}", logger): with TimeIt(f"{game} step {step}", logger):
call_all(multiworld, step) call_all(multiworld, step)
gc.collect() gc.collect()
if freeze_gc:
gc.unfreeze()
locations = sorted(multiworld.get_unfilled_locations()) locations = sorted(multiworld.get_unfilled_locations())
if not locations: if not locations:

View File

@@ -6,9 +6,9 @@ from Utils import get_intended_text, get_input_text_from_response
class TestClient(unittest.TestCase): class TestClient(unittest.TestCase):
def test_autofill_hint_from_fuzzy_hint(self) -> None: def test_autofill_hint_from_fuzzy_hint(self) -> None:
tests = ( tests = (
("item", ["item1", "item2"]), # Multiple close matches ("item", ["item1", "item2"]), # Multiple close matches
("itm", ["item1", "item21"]), # No close match, multiple option ("itm", ["item1", "item21"]), # No close match, multiple option
("item", ["item1"]), # No close match, single option ("item", ["item1"]), # No close match, single option
("item", ["\"item\" 'item' (item)"]), # Testing different special characters ("item", ["\"item\" 'item' (item)"]), # Testing different special characters
) )
@@ -16,7 +16,7 @@ class TestClient(unittest.TestCase):
item_name, usable, response = get_intended_text(input_text, possible_answers) item_name, usable, response = get_intended_text(input_text, possible_answers)
self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed") self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed")
hint_command = get_input_text_from_response(response, "!hint") hint_command = get_input_text_from_response(response, "hint")
self.assertIsNotNone(hint_command, self.assertIsNotNone(hint_command,
"The response to fuzzy hints is no longer recognized by the hint autofill") "The response to fuzzy hints is no longer recognized by the hint autofill")
self.assertEqual(hint_command, f"!hint {item_name}", self.assertEqual(hint_command, f"!hint {item_name}",

View File

@@ -1,7 +1,7 @@
import unittest import unittest
from BaseClasses import PlandoOptions from BaseClasses import PlandoOptions
from Options import Choice, ItemLinks, PlandoConnections, PlandoItems, PlandoTexts from Options import ItemLinks, Choice
from Utils import restricted_dumps from Utils import restricted_dumps
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
@@ -44,19 +44,19 @@ class TestOptions(unittest.TestCase):
}], }],
[{ [{
"name": "ItemLinkGroup", "name": "ItemLinkGroup",
"item_pool": ["Hammer", "Sword"], "item_pool": ["Hammer", "Bow"],
"link_replacement": False, "link_replacement": False,
"replacement_item": None, "replacement_item": None,
}] }]
] ]
# we really need some sort of test world but generic doesn't have enough items for this # we really need some sort of test world but generic doesn't have enough items for this
world = AutoWorldRegister.world_types["APQuest"] world = AutoWorldRegister.world_types["A Link to the Past"]
plando_options = PlandoOptions.from_option_string("bosses") plando_options = PlandoOptions.from_option_string("bosses")
item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])] item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])]
for link in item_links: for link in item_links:
link.verify(world, "tester", plando_options) link.verify(world, "tester", plando_options)
self.assertIn("Hammer", link.value[0]["item_pool"]) self.assertIn("Hammer", link.value[0]["item_pool"])
self.assertIn("Sword", link.value[0]["item_pool"]) self.assertIn("Bow", link.value[0]["item_pool"])
# TODO test that the group created using these options has the items # TODO test that the group created using these options has the items
@@ -72,8 +72,8 @@ class TestOptions(unittest.TestCase):
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_default(self): def test_pickle_dumps(self):
"""Test that default option values can be pickled into database for WebHost generation""" """Test options can be pickled into database for WebHost generation"""
for gamename, world_type in AutoWorldRegister.world_types.items(): for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden: if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items(): for option_key, option in world_type.options_dataclass.type_hints.items():
@@ -81,23 +81,3 @@ class TestOptions(unittest.TestCase):
restricted_dumps(option.from_any(option.default)) restricted_dumps(option.from_any(option.default))
if issubclass(option, Choice) and option.default in option.name_lookup: if issubclass(option, Choice) and option.default in option.name_lookup:
restricted_dumps(option.from_text(option.name_lookup[option.default])) restricted_dumps(option.from_text(option.name_lookup[option.default]))
def test_pickle_dumps_plando(self):
"""Test that plando options using containers of a custom type can be pickled"""
# The base PlandoConnections class can't be instantiated directly, create a subclass and then cast it
class TestPlandoConnections(PlandoConnections):
entrances = {"An Entrance"}
exits = {"An Exit"}
plando_connection_value = PlandoConnections(
TestPlandoConnections.from_any([{"entrance": "An Entrance", "exit": "An Exit"}])
)
plando_values = {
"PlandoConnections": plando_connection_value,
"PlandoItems": PlandoItems.from_any([{"item": "Something", "location": "Somewhere"}]),
"PlandoTexts": PlandoTexts.from_any([{"text": "Some text.", "at": "text_box"}]),
}
for option_key, value in plando_values.items():
with self.subTest(option=option_key):
restricted_dumps(value)

View File

@@ -37,23 +37,3 @@ class TestPlayerOptions(unittest.TestCase):
self.assertEqual(new_weights["dict_2"]["option_g"], 50) self.assertEqual(new_weights["dict_2"]["option_g"], 50)
self.assertEqual(len(new_weights["set_1"]), 2) self.assertEqual(len(new_weights["set_1"]), 2)
self.assertIn("option_d", new_weights["set_1"]) self.assertIn("option_d", new_weights["set_1"])
def test_update_dict_supports_negatives_and_zeroes(self):
original_options = {
"dict_1": {"a": 1, "b": -1},
"dict_2": {"a": 1, "b": -1},
}
new_weights = Generate.update_weights(
original_options,
{
"+dict_1": {"a": -2, "b": 2},
"-dict_2": {"a": 1, "b": 2},
},
"Tested",
"",
)
self.assertEqual(new_weights["dict_1"]["a"], -1)
self.assertEqual(new_weights["dict_1"]["b"], 1)
self.assertEqual(new_weights["dict_2"]["a"], 0)
self.assertEqual(new_weights["dict_2"]["b"], -3)
self.assertIn("a", new_weights["dict_2"])

View File

@@ -1,102 +0,0 @@
"""Check world sources' manifest files"""
import json
import unittest
from pathlib import Path
from typing import Any, ClassVar
import test
from Utils import home_path, local_path
from worlds.AutoWorld import AutoWorldRegister
from ..param import classvar_matrix
test_path = Path(test.__file__).parent
worlds_paths = [
Path(local_path("worlds")),
Path(local_path("custom_worlds")),
Path(home_path("worlds")),
Path(home_path("custom_worlds")),
]
# Only check source folders for now. Zip validation should probably be in the loader and/or installer.
source_world_names = [
k
for k, v in AutoWorldRegister.world_types.items()
if not v.zip_path and not Path(v.__file__).is_relative_to(test_path)
]
def get_source_world_manifest_path(game: str) -> Path | None:
"""Get path of archipelago.json in the world's root folder from game name."""
# TODO: add a feature to AutoWorld that makes this less annoying
world_type = AutoWorldRegister.world_types[game]
world_type_path = Path(world_type.__file__)
for worlds_path in worlds_paths:
if world_type_path.is_relative_to(worlds_path):
world_root = worlds_path / world_type_path.relative_to(worlds_path).parents[0]
manifest_path = world_root / "archipelago.json"
return manifest_path if manifest_path.exists() else None
assert False, f"{world_type_path} not found in any worlds path"
# TODO: remove the filter once manifests are mandatory.
@classvar_matrix(game=filter(get_source_world_manifest_path, source_world_names))
class TestWorldManifest(unittest.TestCase):
game: ClassVar[str]
manifest: ClassVar[dict[str, Any]]
@classmethod
def setUpClass(cls) -> None:
world_type = AutoWorldRegister.world_types[cls.game]
assert world_type.game == cls.game
manifest_path = get_source_world_manifest_path(cls.game)
assert manifest_path # make mypy happy
with manifest_path.open("r", encoding="utf-8") as f:
cls.manifest = json.load(f)
def test_game(self) -> None:
"""Test that 'game' will be correctly defined when generating APWorld manifest from source."""
self.assertIn(
"game",
self.manifest,
f"archipelago.json manifest exists for {self.game} but does not contain 'game'",
)
self.assertEqual(
self.manifest["game"],
self.game,
f"archipelago.json manifest for {self.game} specifies wrong game '{self.manifest['game']}'",
)
def test_world_version(self) -> None:
"""Test that world_version matches the requirements in apworld specification.md"""
if "world_version" in self.manifest:
world_version: str = self.manifest["world_version"]
self.assertIsInstance(
world_version,
str,
f"world_version in archipelago.json for '{self.game}' has to be string if provided.",
)
parts = world_version.split(".")
self.assertEqual(
len(parts),
3,
f"world_version in archipelago.json for '{self.game}' has to be in the form of 'major.minor.build'.",
)
for part in parts:
self.assertTrue(
part.isdigit(),
f"world_version in archipelago.json for '{self.game}' may only contain numbers.",
)
def test_no_container_version(self) -> None:
self.assertNotIn(
"version",
self.manifest,
f"archipelago.json for '{self.game}' must not define 'version', see apworld specification.md.",
)
self.assertNotIn(
"compatible_version",
self.manifest,
f"archipelago.json for '{self.game}' must not define 'compatible_version', see apworld specification.md.",
)

View File

@@ -3,7 +3,6 @@
# Run with `python test/hosting` instead, # Run with `python test/hosting` instead,
import logging import logging
import traceback import traceback
from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from time import sleep from time import sleep
from typing import Any from typing import Any
@@ -12,7 +11,7 @@ from test.hosting.client import Client
from test.hosting.generate import generate_local from test.hosting.generate import generate_local
from test.hosting.serve import ServeGame, LocalServeGame, WebHostServeGame from test.hosting.serve import ServeGame, LocalServeGame, WebHostServeGame
from test.hosting.webhost import (create_room, get_app, get_multidata_for_room, set_multidata_for_room, start_room, from test.hosting.webhost import (create_room, get_app, get_multidata_for_room, set_multidata_for_room, start_room,
stop_autogen, stop_autohost, upload_multidata, generate_remote) stop_autohost, upload_multidata)
from test.hosting.world import copy as copy_world, delete as delete_world from test.hosting.world import copy as copy_world, delete as delete_world
failure = False failure = False
@@ -57,62 +56,35 @@ else:
if __name__ == "__main__": if __name__ == "__main__":
import sys
import warnings import warnings
warnings.simplefilter("ignore", ResourceWarning) warnings.simplefilter("ignore", ResourceWarning)
warnings.simplefilter("ignore", UserWarning) warnings.simplefilter("ignore", UserWarning)
warnings.simplefilter("ignore", DeprecationWarning)
spacer = '=' * 80 spacer = '=' * 80
with TemporaryDirectory() as tempdir: with TemporaryDirectory() as tempdir:
empty_file = str(Path(tempdir) / "empty") multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]]
open(empty_file, "w").close() p1_games = []
sys.argv += ["--config_override", empty_file] # tests #5541 data_paths = []
multis = [["APQuest"], ["Temp World"], ["APQuest", "Temp World"]] rooms = []
p1_games: list[str] = []
data_paths: list[Path | None] = []
rooms: list[str] = []
multidata: Path | None
copy_world("APQuest", "Temp World") copy_world("VVVVVV", "Temp World")
try: try:
for n, games in enumerate(multis, 1): for n, games in enumerate(multis, 1):
print(f"Generating [{n}] {', '.join(games)} offline") print(f"Generating [{n}] {', '.join(games)}")
multidata = generate_local(games, tempdir) multidata = generate_local(games, tempdir)
print(f"Generated [{n}] {', '.join(games)} as {multidata}\n") print(f"Generated [{n}] {', '.join(games)} as {multidata}\n")
data_paths.append(multidata)
p1_games.append(games[0]) p1_games.append(games[0])
data_paths.append(multidata)
finally: finally:
delete_world("Temp World") delete_world("Temp World")
webapp = get_app(tempdir) webapp = get_app(tempdir)
webhost_client = webapp.test_client() webhost_client = webapp.test_client()
for n, multidata in enumerate(data_paths, 1): for n, multidata in enumerate(data_paths, 1):
assert multidata
seed = upload_multidata(webhost_client, multidata) seed = upload_multidata(webhost_client, multidata)
print(f"Uploaded [{n}] {multidata} as {seed}\n")
room = create_room(webhost_client, seed) room = create_room(webhost_client, seed)
print(f"Started [{n}] {seed} as {room}\n") print(f"Uploaded [{n}] {multidata} as {room}\n")
rooms.append(room)
# Generate 1 extra game on WebHost
from WebHostLib.autolauncher import autogen
for n, games in enumerate(multis[:1], len(multis) + 1):
multis.append(games)
try:
print(f"Generating [{n}] {', '.join(games)} online")
autogen(webapp.config)
sleep(5) # until we have lazy loading of worlds, wait here for the process to start up
seed = generate_remote(webhost_client, games)
print(f"Generated [{n}] {', '.join(games)} as {seed}\n")
finally:
stop_autogen()
data_paths.append(None) # WebHost-only
room = create_room(webhost_client, seed)
print(f"Started [{n}] {seed} as {room}\n")
rooms.append(room) rooms.append(room)
print("Starting autohost") print("Starting autohost")
@@ -124,10 +96,31 @@ if __name__ == "__main__":
for n, (multidata, room, game, multi_games) in enumerate(zip(data_paths, rooms, p1_games, multis), 1): for n, (multidata, room, game, multi_games) in enumerate(zip(data_paths, rooms, p1_games, multis), 1):
involved_games = {"Archipelago"} | set(multi_games) involved_games = {"Archipelago"} | set(multi_games)
for collected_items in range(3): for collected_items in range(3):
print(f"\nTesting [{n}] {game} in {multidata} on MultiServer with {collected_items} items collected")
with LocalServeGame(multidata) as host:
with Client(host.address, game, "Player1") as client:
local_data_packages = client.games_packages
local_collected_items = len(client.checked_locations)
if collected_items < 2: # Don't collect anything on the last iteration
client.collect_any()
# TODO: Ctrl+C test here as well
for game_name in sorted(involved_games):
expect_true(game_name in local_data_packages,
f"{game_name} missing from MultiServer datap ackage")
expect_true("item_name_groups" not in local_data_packages.get(game_name, {}),
f"item_name_groups are not supposed to be in MultiServer data for {game_name}")
expect_true("location_name_groups" not in local_data_packages.get(game_name, {}),
f"location_name_groups are not supposed to be in MultiServer data for {game_name}")
for game_name in local_data_packages:
expect_true(game_name in involved_games,
f"Received unexpected extra data package for {game_name} from MultiServer")
assert_equal(local_collected_items, collected_items,
"MultiServer did not load or save correctly")
print(f"\nTesting [{n}] {game} in {multidata} on customserver with {collected_items} items collected") print(f"\nTesting [{n}] {game} in {multidata} on customserver with {collected_items} items collected")
prev_host_adr: str prev_host_adr: str
with WebHostServeGame(webhost_client, room) as host: with WebHostServeGame(webhost_client, room) as host:
sleep(.1) # wait for the server to fully start before doing anything
prev_host_adr = host.address prev_host_adr = host.address
with Client(host.address, game, "Player1") as client: with Client(host.address, game, "Player1") as client:
web_data_packages = client.games_packages web_data_packages = client.games_packages
@@ -141,7 +134,6 @@ if __name__ == "__main__":
autohost(webapp.config) # this will spin the room right up again autohost(webapp.config) # this will spin the room right up again
sleep(1) # make log less annoying sleep(1) # make log less annoying
# if saving failed, the next iteration will fail below # if saving failed, the next iteration will fail below
sleep(2) # work around issue #5571
# verify server shut down # verify server shut down
try: try:
@@ -164,31 +156,6 @@ if __name__ == "__main__":
"customserver did not load or save correctly during/after " "customserver did not load or save correctly during/after "
+ ("Ctrl+C" if collected_items == 2 else "/exit")) + ("Ctrl+C" if collected_items == 2 else "/exit"))
if not multidata:
continue # games rolled on WebHost can not be tested against MultiServer
print(f"\nTesting [{n}] {game} in {multidata} on MultiServer with {collected_items} items collected")
with LocalServeGame(multidata) as host:
with Client(host.address, game, "Player1") as client:
local_data_packages = client.games_packages
local_collected_items = len(client.checked_locations)
if collected_items < 2: # Don't collect anything on the last iteration
client.collect_any()
# TODO: Ctrl+C test here as well
for game_name in sorted(involved_games):
expect_true(game_name in local_data_packages,
f"{game_name} missing from MultiServer datapackage")
expect_true("item_name_groups" not in local_data_packages.get(game_name, {}),
f"item_name_groups are not supposed to be in MultiServer data for {game_name}")
expect_true("location_name_groups" not in local_data_packages.get(game_name, {}),
f"location_name_groups are not supposed to be in MultiServer data for {game_name}")
for game_name in local_data_packages:
expect_true(game_name in involved_games,
f"Received unexpected extra data package for {game_name} from MultiServer")
assert_equal(local_collected_items, collected_items,
"MultiServer did not load or save correctly")
# compare customserver to MultiServer # compare customserver to MultiServer
expect_equal(local_data_packages, web_data_packages, expect_equal(local_data_packages, web_data_packages,
"customserver datapackage differs from MultiServer") "customserver datapackage differs from MultiServer")
@@ -209,12 +176,10 @@ if __name__ == "__main__":
print(f"Restoring multidata for {room}") print(f"Restoring multidata for {room}")
set_multidata_for_room(webhost_client, room, old_data) set_multidata_for_room(webhost_client, room, old_data)
with WebHostServeGame(webhost_client, room) as host: with WebHostServeGame(webhost_client, room) as host:
sleep(.1) # wait for the server to fully start before doing anything
with Client(host.address, game, "Player1") as client: with Client(host.address, game, "Player1") as client:
assert_equal(len(client.checked_locations), 2, assert_equal(len(client.checked_locations), 2,
"Save was destroyed during exception in customserver") "Save was destroyed during exception in customserver")
print("Save file is not busted 🥳") print("Save file is not busted 🥳")
sleep(2) # work around issue #5571
finally: finally:
print("Stopping autohost") print("Stopping autohost")

View File

@@ -1,10 +1,6 @@
import io
import json
import re import re
import time
import zipfile
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Iterable, Optional, cast from typing import TYPE_CHECKING, Optional, cast
from WebHostLib import to_python from WebHostLib import to_python
@@ -14,7 +10,6 @@ if TYPE_CHECKING:
__all__ = [ __all__ = [
"get_app", "get_app",
"generate_remote",
"upload_multidata", "upload_multidata",
"create_room", "create_room",
"start_room", "start_room",
@@ -22,7 +17,6 @@ __all__ = [
"set_room_timeout", "set_room_timeout",
"get_multidata_for_room", "get_multidata_for_room",
"set_multidata_for_room", "set_multidata_for_room",
"stop_autogen",
"stop_autohost", "stop_autohost",
] ]
@@ -39,43 +33,10 @@ def get_app(tempdir: str) -> "Flask":
"TESTING": True, "TESTING": True,
"HOST_ADDRESS": "localhost", "HOST_ADDRESS": "localhost",
"HOSTERS": 1, "HOSTERS": 1,
"GENERATORS": 1,
"JOB_THRESHOLD": 1,
}) })
return get_app() return get_app()
def generate_remote(app_client: "FlaskClient", games: Iterable[str]) -> str:
data = io.BytesIO()
with zipfile.ZipFile(data, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
for n, game in enumerate(games, 1):
name = f"{n}.yaml"
zip_file.writestr(name, json.dumps({
"name": f"Player{n}",
"game": game,
game: {},
"description": f"generate_remote slot {n} ('Player{n}'): {game}",
}))
data.seek(0)
response = app_client.post("/generate", content_type="multipart/form-data", data={
"file": (data, "yamls.zip"),
})
assert response.status_code < 400, f"Starting gen failed: status {response.status_code}"
assert "Location" in response.headers, f"Starting gen failed: no redirect"
location = response.headers["Location"]
assert isinstance(location, str)
assert location.startswith("/wait/"), f"Starting WebHost gen failed: unexpected redirect to {location}"
for attempt in range(10):
response = app_client.get(location)
if "Location" in response.headers:
location = response.headers["Location"]
assert isinstance(location, str)
assert location.startswith("/seed/"), f"Finishing WebHost gen failed: unexpected redirect to {location}"
return location[6:]
time.sleep(1)
raise TimeoutError("WebHost gen did not finish")
def upload_multidata(app_client: "FlaskClient", multidata: Path) -> str: def upload_multidata(app_client: "FlaskClient", multidata: Path) -> str:
response = app_client.post("/uploads", data={ response = app_client.post("/uploads", data={
"file": multidata.open("rb"), "file": multidata.open("rb"),
@@ -227,7 +188,7 @@ def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: by
room.seed.multidata = data room.seed.multidata = data
def _stop_webhost_mp(name_filter: str, graceful: bool = True) -> None: def stop_autohost(graceful: bool = True) -> None:
import os import os
import signal import signal
@@ -237,30 +198,13 @@ def _stop_webhost_mp(name_filter: str, graceful: bool = True) -> None:
stop() stop()
proc: multiprocessing.process.BaseProcess proc: multiprocessing.process.BaseProcess
for proc in filter(lambda child: child.name.startswith(name_filter), multiprocessing.active_children()): for proc in filter(lambda child: child.name.startswith("MultiHoster"), multiprocessing.active_children()):
# FIXME: graceful currently does not work on Windows because the signals are not properly emulated
# and ungraceful may not save the game
if proc.pid == os.getpid():
continue
if graceful and proc.pid: if graceful and proc.pid:
os.kill(proc.pid, getattr(signal, "CTRL_C_EVENT", signal.SIGINT)) os.kill(proc.pid, getattr(signal, "CTRL_C_EVENT", signal.SIGINT))
else: else:
proc.kill() proc.kill()
try: try:
try: proc.join(30)
proc.join(30)
except TimeoutError:
raise
except KeyboardInterrupt:
# on Windows, the MP exception may be forwarded to the host, so ignore once and retry
proc.join(30)
except TimeoutError: except TimeoutError:
proc.kill() proc.kill()
proc.join() proc.join()
def stop_autogen(graceful: bool = True) -> None:
# FIXME: this name filter is jank, but there seems to be no way to add a custom prefix for a Pool
_stop_webhost_mp("SpawnPoolWorker-", graceful)
def stop_autohost(graceful: bool = True) -> None:
_stop_webhost_mp("MultiHoster", graceful)

View File

@@ -11,7 +11,7 @@ _new_worlds: dict[str, str] = {}
def copy(src: str, dst: str) -> None: def copy(src: str, dst: str) -> None:
from Utils import get_file_safe_name from Utils import get_file_safe_name
from worlds.AutoWorld import AutoWorldRegister from worlds import AutoWorldRegister
assert dst not in _new_worlds, "World already created" assert dst not in _new_worlds, "World already created"
if '"' in dst or "\\" in dst: # easier to reject than to escape if '"' in dst or "\\" in dst: # easier to reject than to escape
@@ -20,7 +20,7 @@ def copy(src: str, dst: str) -> None:
src_cls = AutoWorldRegister.world_types[src] src_cls = AutoWorldRegister.world_types[src]
src_folder = Path(src_cls.__file__).parent src_folder = Path(src_cls.__file__).parent
worlds_folder = src_folder.parent worlds_folder = src_folder.parent
if (not src_cls.__file__.endswith(("__init__.py", "world.py")) or not src_folder.is_dir() if (not src_cls.__file__.endswith("__init__.py") or not src_folder.is_dir()
or not (worlds_folder / "generic").is_dir()): or not (worlds_folder / "generic").is_dir()):
raise ValueError(f"Unsupported layout for copy_world from {src}") raise ValueError(f"Unsupported layout for copy_world from {src}")
dst_folder = worlds_folder / dst_folder_name dst_folder = worlds_folder / dst_folder_name
@@ -28,14 +28,11 @@ def copy(src: str, dst: str) -> None:
raise ValueError(f"Destination {dst_folder} already exists") raise ValueError(f"Destination {dst_folder} already exists")
shutil.copytree(src_folder, dst_folder) shutil.copytree(src_folder, dst_folder)
_new_worlds[dst] = str(dst_folder) _new_worlds[dst] = str(dst_folder)
with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f:
for potential_world_class_file in ("__init__.py", "world.py"): contents = f.read()
with open(dst_folder / potential_world_class_file, "r", encoding="utf-8-sig") as f: contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
contents = f.read() with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
r_src = re.escape(src) f.write(contents)
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + r_src + r'[\'"]', f'game = "{dst}"', contents)
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
f.write(contents)
def delete(name: str) -> None: def delete(name: str) -> None:

View File

@@ -2,8 +2,8 @@ description: Almost blank test yaml
name: Player{NUMBER} name: Player{NUMBER}
game: game:
APQuest: 1 # what else Timespinner: 1 # what else
requires: requires:
version: 0.2.6 version: 0.2.6
APQuest: {} Timespinner: {}

View File

@@ -1,14 +0,0 @@
import unittest
from Utils import DaemonThreadPoolExecutor
class DaemonThreadPoolExecutorTest(unittest.TestCase):
def test_is_daemon(self) -> None:
def run() -> None:
pass
with DaemonThreadPoolExecutor(1) as executor:
executor.submit(run)
self.assertTrue(next(iter(executor._threads)).daemon)

View File

@@ -1,78 +0,0 @@
import os
import unittest
from tempfile import NamedTemporaryFile
from mistune import HTMLRenderer, Markdown
from WebHostLib.markdown import ImgUrlRewriteInlineParser, render_markdown
class ImgUrlRewriteTest(unittest.TestCase):
markdown: Markdown
base_url = "/static/generated/docs/some_game"
def setUp(self) -> None:
self.markdown = Markdown(
renderer=HTMLRenderer(escape=False),
inline=ImgUrlRewriteInlineParser(self.base_url),
)
def test_relative_img_rewrite(self) -> None:
html = self.markdown("![Image](image.png)")
self.assertIn(f'src="{self.base_url}/image.png"', html)
def test_absolute_img_no_rewrite(self) -> None:
html = self.markdown("![Image](/image.png)")
self.assertIn(f'src="/image.png"', html)
self.assertNotIn(self.base_url, html)
def test_remote_img_no_rewrite(self) -> None:
html = self.markdown("![Image](https://example.com/image.png)")
self.assertIn(f'src="https://example.com/image.png"', html)
self.assertNotIn(self.base_url, html)
def test_relative_link_no_rewrite(self) -> None:
# The parser is only supposed to update images, not links.
html = self.markdown("[Link](image.png)")
self.assertIn(f'href="image.png"', html)
self.assertNotIn(self.base_url, html)
def test_absolute_link_no_rewrite(self) -> None:
html = self.markdown("[Link](/image.png)")
self.assertIn(f'href="/image.png"', html)
self.assertNotIn(self.base_url, html)
def test_auto_link_no_rewrite(self) -> None:
html = self.markdown("<https://example.com/image.png>")
self.assertIn(f'href="https://example.com/image.png"', html)
self.assertNotIn(self.base_url, html)
def test_relative_img_to_other_game(self) -> None:
html = self.markdown("![Image](../../generic/docs/image.png)")
self.assertIn(f'src="{self.base_url}/../Archipelago/image.png"', html)
class RenderMarkdownTest(unittest.TestCase):
"""Tests that render_markdown does the right thing."""
base_url = "/static/generated/docs/some_game"
def test_relative_img_rewrite(self) -> None:
f = NamedTemporaryFile(delete=False)
try:
f.write("![Image](image.png)".encode("utf-8"))
f.close()
html = render_markdown(f.name, self.base_url)
self.assertIn(f'src="{self.base_url}/image.png"', html)
finally:
os.unlink(f.name)
def test_no_img_rewrite(self) -> None:
f = NamedTemporaryFile(delete=False)
try:
f.write("![Image](image.png)".encode("utf-8"))
f.close()
html = render_markdown(f.name)
self.assertIn(f'src="image.png"', html)
self.assertNotIn(self.base_url, html)
finally:
os.unlink(f.name)

View File

@@ -1,26 +1,14 @@
from __future__ import annotations from __future__ import annotations
import abc import abc
from bisect import bisect_right
from dataclasses import dataclass
import enum
import logging import logging
from typing import (TYPE_CHECKING, Any, ClassVar, Dict, Generic, Iterable, from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
Optional, Sequence, Tuple, TypeGuard, TypeVar, Union)
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
if TYPE_CHECKING: if TYPE_CHECKING:
from SNIClient import SNIContext from SNIClient import SNIContext
SNES_READ_CHUNK_SIZE = 2048
"""
note: SNI v0.0.101 currently has a bug where reads from
RetroArch >2048 bytes will only return the last ~2048 bytes read.
https://github.com/alttpo/sni/issues/51
"""
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"), component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
description="A client for connecting to SNES consoles via Super Nintendo Interface.") description="A client for connecting to SNES consoles via Super Nintendo Interface.")
components.append(component) components.append(component)
@@ -103,119 +91,3 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None: def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
""" override this with code to handle packages from the server """ """ override this with code to handle packages from the server """
pass pass
@dataclass(frozen=True, slots=True, order=True)
class Read:
""" snes memory read - address and size in bytes """
address: int
size: int
@dataclass(frozen=True, slots=True)
class _MemRead:
location: Read
data: bytes
_T_Enum = TypeVar("_T_Enum", bound=enum.Enum)
class SnesData(Generic[_T_Enum]):
_ranges: Sequence[_MemRead]
""" sorted by address """
def __init__(self, ranges: Sequence[tuple[Read, bytes]]) -> None:
self._ranges = [_MemRead(r, d) for r, d in ranges]
def get(self, read: _T_Enum) -> bytes:
assert isinstance(read.value, Read), read.value
address = read.value.address
index = bisect_right(self._ranges, address, key=lambda r: r.location.address) - 1
assert index >= 0, (self._ranges, read.value)
mem_read = self._ranges[index]
sub_index = address - mem_read.location.address
return mem_read.data[sub_index:sub_index + read.value.size]
class SnesReader(Generic[_T_Enum]):
"""
how to use:
```
from enum import Enum
from worlds.AutoSNIClient import Read, SNIClient, SnesReader
class MyGameMemory(Enum):
game_mode = Read(WRAM_START + 0x0998, 1)
send_queue = Read(SEND_QUEUE_START, 8 * 127)
...
snes_reader = SnesReader(MyGameMemory)
snes_data = await snes_reader.read(ctx)
if snes_data is None:
snes_logger.info("error reading from snes")
return
game_mode = snes_data.get(MyGameMemory.game_mode)
```
"""
_ranges: Sequence[Read]
""" sorted by address """
def __init__(self, reads: type[_T_Enum]) -> None:
self._ranges = self._make_ranges(reads)
@staticmethod
def _make_ranges(reads: type[enum.Enum]) -> Sequence[Read]:
unprocessed_reads: list[Read] = []
for e in reads:
assert isinstance(e.value, Read), (reads.__name__, e, e.value)
unprocessed_reads.append(e.value)
unprocessed_reads.sort()
ranges: list[Read] = []
for read in unprocessed_reads:
# v end of the previous range
if len(ranges) == 0 or read.address - (ranges[-1].address + ranges[-1].size) > 255:
ranges.append(read)
else: # combine with previous range
chunk_address = ranges[-1].address
assert read.address >= chunk_address, "sort() didn't work? or something"
original_chunk_size = ranges[-1].size
new_size = max((read.address + read.size) - chunk_address,
original_chunk_size)
ranges[-1] = Read(chunk_address, new_size)
logging.debug(f"{len(ranges)=} {max(r.size for r in ranges)=}")
return ranges
async def read(self, ctx: "SNIContext") -> SnesData[_T_Enum] | None:
"""
returns `None` if reading fails,
otherwise returns the data for the registered `Enum`
"""
from SNIClient import snes_read
reads: list[tuple[Read, bytes]] = []
for r in self._ranges:
if r.size < SNES_READ_CHUNK_SIZE: # most common
response = await snes_read(ctx, r.address, r.size)
if response is None:
return None
reads.append((r, response))
else: # big read
# Problems were reported with big reads,
# so we chunk it into smaller pieces.
read_so_far = 0
collection: list[bytes] = []
while read_so_far < r.size:
remaining_size = r.size - read_so_far
chunk_size = min(SNES_READ_CHUNK_SIZE, remaining_size)
response = await snes_read(ctx, r.address + read_so_far, chunk_size)
if response is None:
return None
collection.append(response)
read_so_far += chunk_size
reads.append((r, b"".join(collection)))
return SnesData(reads)

View File

@@ -224,7 +224,7 @@ class WebWorld(metaclass=WebWorldRegister):
tutorials: List["Tutorial"] tutorials: List["Tutorial"]
"""docs folder will also be scanned for tutorial guides. Each Tutorial class is to be used for one guide.""" """docs folder will also be scanned for tutorial guides. Each Tutorial class is to be used for one guide."""
theme: str = "grass" theme = "grass"
"""Choose a theme for you /game/* pages. """Choose a theme for you /game/* pages.
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone""" Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""

View File

@@ -21,10 +21,6 @@ if TYPE_CHECKING:
from Utils import Version from Utils import Version
class ImproperlyConfiguredAutoPatchError(Exception):
pass
class AutoPatchRegister(abc.ABCMeta): class AutoPatchRegister(abc.ABCMeta):
patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {} patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {}
file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {} file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {}
@@ -34,28 +30,8 @@ class AutoPatchRegister(abc.ABCMeta):
new_class = super().__new__(mcs, name, bases, dct) new_class = super().__new__(mcs, name, bases, dct)
if "game" in dct: if "game" in dct:
AutoPatchRegister.patch_types[dct["game"]] = new_class AutoPatchRegister.patch_types[dct["game"]] = new_class
if not dct["patch_file_ending"]:
if not callable(getattr(new_class, "patch", None)): raise Exception(f"Need an expected file ending for {name}")
raise ImproperlyConfiguredAutoPatchError(
f"Container {new_class} uses metaclass AutoPatchRegister, but does not have a patch method defined."
)
patch_file_ending = dct.get("patch_file_ending")
if patch_file_ending == ".zip":
raise ImproperlyConfiguredAutoPatchError(
f'Auto patch container {new_class} uses file ending ".zip", which is not allowed.'
)
if patch_file_ending is None:
raise ImproperlyConfiguredAutoPatchError(
f"Need an expected file ending for auto patch container {new_class}"
)
existing_handler = AutoPatchRegister.file_endings.get(patch_file_ending)
if existing_handler:
raise ImproperlyConfiguredAutoPatchError(
f"Two auto patch containers are using the same file extension: {new_class}, {existing_handler}"
)
AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class
return new_class return new_class
@@ -199,12 +175,12 @@ class APWorldContainer(APContainer):
maximum_ap_version: "Version | None" = None maximum_ap_version: "Version | None" = None
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]: def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
from Utils import tuplize_version from Utils import tuplize_version, Version
manifest = super().read_contents(opened_zipfile) manifest = super().read_contents(opened_zipfile)
self.game = manifest["game"] self.game = manifest["game"]
for version_key in ("world_version", "minimum_ap_version", "maximum_ap_version"): for version_key in ("world_version", "minimum_ap_version", "maximum_ap_version"):
if version_key in manifest: if version_key in manifest:
setattr(self, version_key, tuplize_version(manifest[version_key])) setattr(self, version_key, Version(*tuplize_version(manifest[version_key])))
return manifest return manifest
def get_manifest(self) -> Dict[str, Any]: def get_manifest(self) -> Dict[str, Any]:

View File

@@ -5,7 +5,7 @@ import weakref
from enum import Enum, auto from enum import Enum, auto
from typing import Optional, Callable, List, Iterable, Tuple from typing import Optional, Callable, List, Iterable, Tuple
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path from Utils import local_path, open_filename, is_frozen, is_kivy_running
class Type(Enum): class Type(Enum):
@@ -180,7 +180,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
if found_already_loaded and is_kivy_running(): if found_already_loaded and is_kivy_running():
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded, " raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded, "
"so a Launcher restart is required to use the new installation.") "so a Launcher restart is required to use the new installation.")
world_source = worlds.WorldSource(str(target), is_zip=True, relative=False) world_source = worlds.WorldSource(str(target), is_zip=True)
bisect.insort(worlds.world_sources, world_source) bisect.insort(worlds.world_sources, world_source)
world_source.load() world_source.load()
@@ -204,18 +204,6 @@ def install_apworld(apworld_path: str = "") -> None:
Utils.messagebox("Install complete.", f"Installed APWorld from {source}.") Utils.messagebox("Install complete.", f"Installed APWorld from {source}.")
def export_datapackage() -> None:
import json
from worlds import network_data_package
path = user_path("datapackage_export.json")
with open(path, "w") as f:
json.dump(network_data_package, f, indent=4)
open_file(path)
components: List[Component] = [ components: List[Component] = [
# Launcher # Launcher
Component('Launcher', 'Launcher', component_type=Type.HIDDEN), Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
@@ -225,12 +213,12 @@ components: List[Component] = [
description="Host a generated multiworld on your computer."), description="Host a generated multiworld on your computer."),
Component('Generate', 'Generate', cli=True, Component('Generate', 'Generate', cli=True,
description="Generate a multiworld with the YAMLs in the players folder."), description="Generate a multiworld with the YAMLs in the players folder."),
Component("Options Creator", "OptionsCreator", "ArchipelagoOptionsCreator", component_type=Type.TOOL,
description="Visual creator for Archipelago option files."),
Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld"), Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld"),
description="Install an APWorld to play games not included with Archipelago by default."), description="Install an APWorld to play games not included with Archipelago by default."),
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient, Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient,
description="Connect to a multiworld using the text client."), description="Connect to a multiworld using the text client."),
Component('Links Awakening DX Client', 'LinksAwakeningClient',
file_identifier=SuffixIdentifier('.apladx')),
Component('LttP Adjuster', 'LttPAdjuster'), Component('LttP Adjuster', 'LttPAdjuster'),
# Ocarina of Time # Ocarina of Time
Component('OoT Client', 'OoTClient', Component('OoT Client', 'OoTClient',
@@ -244,10 +232,8 @@ components: List[Component] = [
Component('Zillion Client', 'ZillionClient', Component('Zillion Client', 'ZillionClient',
file_identifier=SuffixIdentifier('.apzl')), file_identifier=SuffixIdentifier('.apzl')),
# MegaMan Battle Network 3 #MegaMan Battle Network 3
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3')), Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3'))
Component("Export Datapackage", func=export_datapackage, component_type=Type.TOOL),
] ]
@@ -258,46 +244,21 @@ icon_paths = {
} }
if not is_frozen(): if not is_frozen():
def _build_apworlds(*launch_args: str): def _build_apworlds():
import json import json
import os import os
import zipfile import zipfile
from worlds import AutoWorldRegister from worlds import AutoWorldRegister
from worlds.Files import APWorldContainer from worlds.Files import APWorldContainer
from Launcher import open_folder
import argparse
parser = argparse.ArgumentParser("Build script for APWorlds")
parser.add_argument("worlds", type=str, default=(), nargs="*", help="Names of APWorlds to build.")
args = parser.parse_args(launch_args)
if args.worlds:
games = [(game, AutoWorldRegister.world_types.get(game, None)) for game in args.worlds]
else:
games = [(worldname, worldtype) for worldname, worldtype in AutoWorldRegister.world_types.items()
if not worldtype.zip_path]
apworlds_folder = os.path.join("build", "apworlds") apworlds_folder = os.path.join("build", "apworlds")
os.makedirs(apworlds_folder, exist_ok=True) os.makedirs(apworlds_folder, exist_ok=True)
for worldname, worldtype in games: for worldname, worldtype in AutoWorldRegister.world_types.items():
if not worldtype:
logging.error(f"Requested APWorld \"{worldname}\" does not exist.")
continue
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1] file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
world_directory = os.path.join("worlds", file_name) world_directory = os.path.join("worlds", file_name)
if os.path.isfile(os.path.join(world_directory, "archipelago.json")): if os.path.isfile(os.path.join(world_directory, "archipelago.json")):
with open(os.path.join(world_directory, "archipelago.json"), mode="r", encoding="utf-8") as manifest_file: manifest = json.load(open(os.path.join(world_directory, "archipelago.json")))
manifest = json.load(manifest_file)
assert "game" in manifest, (
f"World directory {world_directory} has an archipelago.json manifest file, but it "
"does not define a \"game\"."
)
assert manifest["game"] == worldtype.game, (
f"World directory {world_directory} has an archipelago.json manifest file, but value of the "
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
)
else: else:
manifest = {} manifest = {}
@@ -308,15 +269,12 @@ if not is_frozen():
apworld.manifest_path = f"{file_name}/archipelago.json" apworld.manifest_path = f"{file_name}/archipelago.json"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
compresslevel=9) as zf: compresslevel=9) as zf:
for path in pathlib.Path(world_directory).rglob("*"): for path in pathlib.Path(world_directory).rglob("*.*"):
relative_path = os.path.join(*path.parts[path.parts.index("worlds") + 1:]) relative_path = os.path.join(*path.parts[path.parts.index("worlds") + 1:])
if "__MACOSX" in relative_path or ".DS_STORE" in relative_path or "__pycache__" in relative_path: if "__MACOSX" in relative_path or ".DS_STORE" in relative_path or "__pycache__" in relative_path:
continue continue
if not relative_path.endswith("archipelago.json"): if not relative_path.endswith("archipelago.json"):
zf.write(path, relative_path) zf.write(path, relative_path)
zf.writestr(apworld.manifest_path, json.dumps(manifest)) zf.writestr(apworld.manifest_path, json.dumps(manifest))
open_folder(apworlds_folder)
components.append(Component('Build apworlds', func=_build_apworlds, cli=True,))
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
description="Build APWorlds from loose-file world folders."))

View File

@@ -122,14 +122,14 @@ for world_source in world_sources:
for dirpath, dirnames, filenames in os.walk(world_source.resolved_path): for dirpath, dirnames, filenames in os.walk(world_source.resolved_path):
for file in filenames: for file in filenames:
if file.endswith("archipelago.json"): if file.endswith("archipelago.json"):
with open(os.path.join(dirpath, file), mode="r", encoding="utf-8") as manifest_file: manifest = json.load(open(os.path.join(dirpath, file), "r"))
manifest = json.load(manifest_file)
break break
if manifest: if manifest:
break break
game = manifest.get("game") game = manifest.get("game")
if game in AutoWorldRegister.world_types: if game in AutoWorldRegister.world_types:
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0")) AutoWorldRegister.world_types[game].world_version = Version(*tuplize_version(manifest.get("world_version",
"0.0.0")))
if apworlds: if apworlds:
# encapsulation for namespace / gc purposes # encapsulation for namespace / gc purposes

View File

@@ -9,7 +9,7 @@ from collections import Counter
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from typing import Dict, Generator, Iterable, List, Set, Tuple, Union, final from typing import Dict, Generator, Iterable, List, Set, Tuple, Union, final
from .proto import sc2api_pb2 as sc_pb from s2clientprotocol import sc2api_pb2 as sc_pb
from .constants import ( from .constants import (
IS_PLACEHOLDER, IS_PLACEHOLDER,

View File

@@ -4,11 +4,11 @@ from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
from worlds._sc2common.bot import logger from worlds._sc2common.bot import logger
from .proto import debug_pb2 as debug_pb from s2clientprotocol import debug_pb2 as debug_pb
from .proto import query_pb2 as query_pb from s2clientprotocol import query_pb2 as query_pb
from .proto import raw_pb2 as raw_pb from s2clientprotocol import raw_pb2 as raw_pb
from .proto import sc2api_pb2 as sc_pb from s2clientprotocol import sc2api_pb2 as sc_pb
from .proto import spatial_pb2 as spatial_pb from s2clientprotocol import spatial_pb2 as spatial_pb
from .data import ActionResult, ChatChannel, Race, Result, Status from .data import ActionResult, ChatChannel, Race, Result, Status
from .game_data import AbilityData, GameData from .game_data import AbilityData, GameData

View File

@@ -2,7 +2,7 @@ import platform
from pathlib import Path from pathlib import Path
from worlds._sc2common.bot import logger from worlds._sc2common.bot import logger
from .proto import sc2api_pb2 as sc_pb from s2clientprotocol import sc2api_pb2 as sc_pb
from .player import Computer from .player import Computer
from .protocol import Protocol from .protocol import Protocol

View File

@@ -7,11 +7,11 @@ https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188f
""" """
import enum import enum
from .proto import common_pb2 as common_pb from s2clientprotocol import common_pb2 as common_pb
from .proto import data_pb2 as data_pb from s2clientprotocol import data_pb2 as data_pb
from .proto import error_pb2 as error_pb from s2clientprotocol import error_pb2 as error_pb
from .proto import raw_pb2 as raw_pb from s2clientprotocol import raw_pb2 as raw_pb
from .proto import sc2api_pb2 as sc_pb from s2clientprotocol import sc2api_pb2 as sc_pb
CreateGameError = enum.Enum("CreateGameError", sc_pb.ResponseCreateGame.Error.items()) CreateGameError = enum.Enum("CreateGameError", sc_pb.ResponseCreateGame.Error.items())

View File

@@ -15,7 +15,7 @@ import mpyq
import portpicker import portpicker
from aiohttp import ClientSession, ClientWebSocketResponse from aiohttp import ClientSession, ClientWebSocketResponse
from worlds._sc2common.bot import logger from worlds._sc2common.bot import logger
from .proto import sc2api_pb2 as sc_pb from s2clientprotocol import sc2api_pb2 as sc_pb
from .bot_ai import BotAI from .bot_ai import BotAI
from .client import Client from .client import Client

View File

@@ -5,7 +5,7 @@ import math
import random import random
from typing import TYPE_CHECKING, Iterable, List, Set, Tuple, Union from typing import TYPE_CHECKING, Iterable, List, Set, Tuple, Union
from .proto import common_pb2 as common_pb from s2clientprotocol import common_pb2 as common_pb
if TYPE_CHECKING: if TYPE_CHECKING:
from .unit import Unit from .unit import Unit

View File

@@ -1,50 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: s2clientprotocol/common.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
's2clientprotocol/common.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ds2clientprotocol/common.proto\x12\x0eSC2APIProtocol\">\n\x10\x41vailableAbility\x12\x12\n\nability_id\x18\x01 \x01(\x05\x12\x16\n\x0erequires_point\x18\x02 \x01(\x08\"X\n\tImageData\x12\x16\n\x0e\x62its_per_pixel\x18\x01 \x01(\x05\x12%\n\x04size\x18\x02 \x01(\x0b\x32\x17.SC2APIProtocol.Size2DI\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"\x1e\n\x06PointI\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05\"T\n\nRectangleI\x12\"\n\x02p0\x18\x01 \x01(\x0b\x32\x16.SC2APIProtocol.PointI\x12\"\n\x02p1\x18\x02 \x01(\x0b\x32\x16.SC2APIProtocol.PointI\"\x1f\n\x07Point2D\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\"(\n\x05Point\x12\t\n\x01x\x18\x01 \x01(\x02\x12\t\n\x01y\x18\x02 \x01(\x02\x12\t\n\x01z\x18\x03 \x01(\x02\"\x1f\n\x07Size2DI\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05*A\n\x04Race\x12\n\n\x06NoRace\x10\x00\x12\n\n\x06Terran\x10\x01\x12\x08\n\x04Zerg\x10\x02\x12\x0b\n\x07Protoss\x10\x03\x12\n\n\x06Random\x10\x04')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 's2clientprotocol.common_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_RACE']._serialized_start=429
_globals['_RACE']._serialized_end=494
_globals['_AVAILABLEABILITY']._serialized_start=49
_globals['_AVAILABLEABILITY']._serialized_end=111
_globals['_IMAGEDATA']._serialized_start=113
_globals['_IMAGEDATA']._serialized_end=201
_globals['_POINTI']._serialized_start=203
_globals['_POINTI']._serialized_end=233
_globals['_RECTANGLEI']._serialized_start=235
_globals['_RECTANGLEI']._serialized_end=319
_globals['_POINT2D']._serialized_start=321
_globals['_POINT2D']._serialized_end=352
_globals['_POINT']._serialized_start=354
_globals['_POINT']._serialized_end=394
_globals['_SIZE2DI']._serialized_start=396
_globals['_SIZE2DI']._serialized_end=427
# @@protoc_insertion_point(module_scope)

View File

@@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: s2clientprotocol/data.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
's2clientprotocol/data.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from . import common_pb2 as s2clientprotocol_dot_common__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bs2clientprotocol/data.proto\x12\x0eSC2APIProtocol\x1a\x1ds2clientprotocol/common.proto\"\xc4\x03\n\x0b\x41\x62ilityData\x12\x12\n\nability_id\x18\x01 \x01(\r\x12\x11\n\tlink_name\x18\x02 \x01(\t\x12\x12\n\nlink_index\x18\x03 \x01(\r\x12\x13\n\x0b\x62utton_name\x18\x04 \x01(\t\x12\x15\n\rfriendly_name\x18\x05 \x01(\t\x12\x0e\n\x06hotkey\x18\x06 \x01(\t\x12\x1c\n\x14remaps_to_ability_id\x18\x07 \x01(\r\x12\x11\n\tavailable\x18\x08 \x01(\x08\x12\x32\n\x06target\x18\t \x01(\x0e\x32\".SC2APIProtocol.AbilityData.Target\x12\x15\n\rallow_minimap\x18\n \x01(\x08\x12\x16\n\x0e\x61llow_autocast\x18\x0b \x01(\x08\x12\x13\n\x0bis_building\x18\x0c \x01(\x08\x12\x18\n\x10\x66ootprint_radius\x18\r \x01(\x02\x12\x1c\n\x14is_instant_placement\x18\x0e \x01(\x08\x12\x12\n\ncast_range\x18\x0f \x01(\x02\"I\n\x06Target\x12\x08\n\x04None\x10\x01\x12\t\n\x05Point\x10\x02\x12\x08\n\x04Unit\x10\x03\x12\x0f\n\x0bPointOrUnit\x10\x04\x12\x0f\n\x0bPointOrNone\x10\x05\"J\n\x0b\x44\x61mageBonus\x12,\n\tattribute\x18\x01 \x01(\x0e\x32\x19.SC2APIProtocol.Attribute\x12\r\n\x05\x62onus\x18\x02 \x01(\x02\"\xd7\x01\n\x06Weapon\x12/\n\x04type\x18\x01 \x01(\x0e\x32!.SC2APIProtocol.Weapon.TargetType\x12\x0e\n\x06\x64\x61mage\x18\x02 \x01(\x02\x12\x31\n\x0c\x64\x61mage_bonus\x18\x03 \x03(\x0b\x32\x1b.SC2APIProtocol.DamageBonus\x12\x0f\n\x07\x61ttacks\x18\x04 \x01(\r\x12\r\n\x05range\x18\x05 \x01(\x02\x12\r\n\x05speed\x18\x06 \x01(\x02\"*\n\nTargetType\x12\n\n\x06Ground\x10\x01\x12\x07\n\x03\x41ir\x10\x02\x12\x07\n\x03\x41ny\x10\x03\"\x95\x04\n\x0cUnitTypeData\x12\x0f\n\x07unit_id\x18\x01 \x01(\r\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x11\n\tavailable\x18\x03 \x01(\x08\x12\x12\n\ncargo_size\x18\x04 \x01(\r\x12\x14\n\x0cmineral_cost\x18\x0c \x01(\r\x12\x14\n\x0cvespene_cost\x18\r \x01(\r\x12\x15\n\rfood_required\x18\x0e \x01(\x02\x12\x15\n\rfood_provided\x18\x12 \x01(\x02\x12\x12\n\nability_id\x18\x0f \x01(\r\x12\"\n\x04race\x18\x10 \x01(\x0e\x32\x14.SC2APIProtocol.Race\x12\x12\n\nbuild_time\x18\x11 \x01(\x02\x12\x13\n\x0bhas_vespene\x18\x13 \x01(\x08\x12\x14\n\x0chas_minerals\x18\x14 \x01(\x08\x12\x13\n\x0bsight_range\x18\x19 \x01(\x02\x12\x12\n\ntech_alias\x18\x15 \x03(\r\x12\x12\n\nunit_alias\x18\x16 \x01(\r\x12\x18\n\x10tech_requirement\x18\x17 \x01(\r\x12\x18\n\x10require_attached\x18\x18 \x01(\x08\x12-\n\nattributes\x18\x08 \x03(\x0e\x32\x19.SC2APIProtocol.Attribute\x12\x16\n\x0emovement_speed\x18\t \x01(\x02\x12\r\n\x05\x61rmor\x18\n \x01(\x02\x12\'\n\x07weapons\x18\x0b \x03(\x0b\x32\x16.SC2APIProtocol.Weapon\"\x86\x01\n\x0bUpgradeData\x12\x12\n\nupgrade_id\x18\x01 \x01(\r\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x14\n\x0cmineral_cost\x18\x03 \x01(\r\x12\x14\n\x0cvespene_cost\x18\x04 \x01(\r\x12\x15\n\rresearch_time\x18\x05 \x01(\x02\x12\x12\n\nability_id\x18\x06 \x01(\r\")\n\x08\x42uffData\x12\x0f\n\x07\x62uff_id\x18\x01 \x01(\r\x12\x0c\n\x04name\x18\x02 \x01(\t\"T\n\nEffectData\x12\x11\n\teffect_id\x18\x01 \x01(\r\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x15\n\rfriendly_name\x18\x03 \x01(\t\x12\x0e\n\x06radius\x18\x04 \x01(\x02*\x9e\x01\n\tAttribute\x12\t\n\x05Light\x10\x01\x12\x0b\n\x07\x41rmored\x10\x02\x12\x0e\n\nBiological\x10\x03\x12\x0e\n\nMechanical\x10\x04\x12\x0b\n\x07Robotic\x10\x05\x12\x0b\n\x07Psionic\x10\x06\x12\x0b\n\x07Massive\x10\x07\x12\r\n\tStructure\x10\x08\x12\t\n\x05Hover\x10\t\x12\n\n\x06Heroic\x10\n\x12\x0c\n\x08Summoned\x10\x0b')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 's2clientprotocol.data_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_ATTRIBUTE']._serialized_start=1630
_globals['_ATTRIBUTE']._serialized_end=1788
_globals['_ABILITYDATA']._serialized_start=79
_globals['_ABILITYDATA']._serialized_end=531
_globals['_ABILITYDATA_TARGET']._serialized_start=458
_globals['_ABILITYDATA_TARGET']._serialized_end=531
_globals['_DAMAGEBONUS']._serialized_start=533
_globals['_DAMAGEBONUS']._serialized_end=607
_globals['_WEAPON']._serialized_start=610
_globals['_WEAPON']._serialized_end=825
_globals['_WEAPON_TARGETTYPE']._serialized_start=783
_globals['_WEAPON_TARGETTYPE']._serialized_end=825
_globals['_UNITTYPEDATA']._serialized_start=828
_globals['_UNITTYPEDATA']._serialized_end=1361
_globals['_UPGRADEDATA']._serialized_start=1364
_globals['_UPGRADEDATA']._serialized_end=1498
_globals['_BUFFDATA']._serialized_start=1500
_globals['_BUFFDATA']._serialized_end=1541
_globals['_EFFECTDATA']._serialized_start=1543
_globals['_EFFECTDATA']._serialized_end=1627
# @@protoc_insertion_point(module_scope)

View File

@@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: s2clientprotocol/debug.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
's2clientprotocol/debug.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from . import common_pb2 as s2clientprotocol_dot_common__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cs2clientprotocol/debug.proto\x12\x0eSC2APIProtocol\x1a\x1ds2clientprotocol/common.proto\"\xbb\x03\n\x0c\x44\x65\x62ugCommand\x12)\n\x04\x64raw\x18\x01 \x01(\x0b\x32\x19.SC2APIProtocol.DebugDrawH\x00\x12\x34\n\ngame_state\x18\x02 \x01(\x0e\x32\x1e.SC2APIProtocol.DebugGameStateH\x00\x12\x36\n\x0b\x63reate_unit\x18\x03 \x01(\x0b\x32\x1f.SC2APIProtocol.DebugCreateUnitH\x00\x12\x32\n\tkill_unit\x18\x04 \x01(\x0b\x32\x1d.SC2APIProtocol.DebugKillUnitH\x00\x12\x38\n\x0ctest_process\x18\x05 \x01(\x0b\x32 .SC2APIProtocol.DebugTestProcessH\x00\x12.\n\x05score\x18\x06 \x01(\x0b\x32\x1d.SC2APIProtocol.DebugSetScoreH\x00\x12\x30\n\x08\x65nd_game\x18\x07 \x01(\x0b\x32\x1c.SC2APIProtocol.DebugEndGameH\x00\x12\x37\n\nunit_value\x18\x08 \x01(\x0b\x32!.SC2APIProtocol.DebugSetUnitValueH\x00\x42\t\n\x07\x63ommand\"\xb5\x01\n\tDebugDraw\x12\'\n\x04text\x18\x01 \x03(\x0b\x32\x19.SC2APIProtocol.DebugText\x12(\n\x05lines\x18\x02 \x03(\x0b\x32\x19.SC2APIProtocol.DebugLine\x12\'\n\x05\x62oxes\x18\x03 \x03(\x0b\x32\x18.SC2APIProtocol.DebugBox\x12,\n\x07spheres\x18\x04 \x03(\x0b\x32\x1b.SC2APIProtocol.DebugSphere\"L\n\x04Line\x12!\n\x02p0\x18\x01 \x01(\x0b\x32\x15.SC2APIProtocol.Point\x12!\n\x02p1\x18\x02 \x01(\x0b\x32\x15.SC2APIProtocol.Point\"(\n\x05\x43olor\x12\t\n\x01r\x18\x01 \x01(\r\x12\t\n\x01g\x18\x02 \x01(\r\x12\t\n\x01\x62\x18\x03 \x01(\r\"\xa3\x01\n\tDebugText\x12$\n\x05\x63olor\x18\x01 \x01(\x0b\x32\x15.SC2APIProtocol.Color\x12\x0c\n\x04text\x18\x02 \x01(\t\x12*\n\x0bvirtual_pos\x18\x03 \x01(\x0b\x32\x15.SC2APIProtocol.Point\x12(\n\tworld_pos\x18\x04 \x01(\x0b\x32\x15.SC2APIProtocol.Point\x12\x0c\n\x04size\x18\x05 \x01(\r\"U\n\tDebugLine\x12$\n\x05\x63olor\x18\x01 \x01(\x0b\x32\x15.SC2APIProtocol.Color\x12\"\n\x04line\x18\x02 \x01(\x0b\x32\x14.SC2APIProtocol.Line\"x\n\x08\x44\x65\x62ugBox\x12$\n\x05\x63olor\x18\x01 \x01(\x0b\x32\x15.SC2APIProtocol.Color\x12\"\n\x03min\x18\x02 \x01(\x0b\x32\x15.SC2APIProtocol.Point\x12\"\n\x03max\x18\x03 \x01(\x0b\x32\x15.SC2APIProtocol.Point\"`\n\x0b\x44\x65\x62ugSphere\x12$\n\x05\x63olor\x18\x01 \x01(\x0b\x32\x15.SC2APIProtocol.Color\x12 \n\x01p\x18\x02 \x01(\x0b\x32\x15.SC2APIProtocol.Point\x12\t\n\x01r\x18\x03 \x01(\x02\"k\n\x0f\x44\x65\x62ugCreateUnit\x12\x11\n\tunit_type\x18\x01 \x01(\r\x12\r\n\x05owner\x18\x02 \x01(\x05\x12$\n\x03pos\x18\x03 \x01(\x0b\x32\x17.SC2APIProtocol.Point2D\x12\x10\n\x08quantity\x18\x04 \x01(\r\"\x1c\n\rDebugKillUnit\x12\x0b\n\x03tag\x18\x01 \x03(\x04\"\x80\x01\n\x10\x44\x65\x62ugTestProcess\x12\x33\n\x04test\x18\x01 \x01(\x0e\x32%.SC2APIProtocol.DebugTestProcess.Test\x12\x10\n\x08\x64\x65lay_ms\x18\x02 \x01(\x05\"%\n\x04Test\x12\x08\n\x04hang\x10\x01\x12\t\n\x05\x63rash\x10\x02\x12\x08\n\x04\x65xit\x10\x03\"\x1e\n\rDebugSetScore\x12\r\n\x05score\x18\x01 \x01(\x02\"z\n\x0c\x44\x65\x62ugEndGame\x12:\n\nend_result\x18\x01 \x01(\x0e\x32&.SC2APIProtocol.DebugEndGame.EndResult\".\n\tEndResult\x12\r\n\tSurrender\x10\x01\x12\x12\n\x0e\x44\x65\x63lareVictory\x10\x02\"\xa5\x01\n\x11\x44\x65\x62ugSetUnitValue\x12?\n\nunit_value\x18\x01 \x01(\x0e\x32+.SC2APIProtocol.DebugSetUnitValue.UnitValue\x12\r\n\x05value\x18\x02 \x01(\x02\x12\x10\n\x08unit_tag\x18\x03 \x01(\x04\".\n\tUnitValue\x12\n\n\x06\x45nergy\x10\x01\x12\x08\n\x04Life\x10\x02\x12\x0b\n\x07Shields\x10\x03*\xb2\x01\n\x0e\x44\x65\x62ugGameState\x12\x0c\n\x08show_map\x10\x01\x12\x11\n\rcontrol_enemy\x10\x02\x12\x08\n\x04\x66ood\x10\x03\x12\x08\n\x04\x66ree\x10\x04\x12\x11\n\rall_resources\x10\x05\x12\x07\n\x03god\x10\x06\x12\x0c\n\x08minerals\x10\x07\x12\x07\n\x03gas\x10\x08\x12\x0c\n\x08\x63ooldown\x10\t\x12\r\n\ttech_tree\x10\n\x12\x0b\n\x07upgrade\x10\x0b\x12\x0e\n\nfast_build\x10\x0c')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 's2clientprotocol.debug_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_DEBUGGAMESTATE']._serialized_start=1897
_globals['_DEBUGGAMESTATE']._serialized_end=2075
_globals['_DEBUGCOMMAND']._serialized_start=80
_globals['_DEBUGCOMMAND']._serialized_end=523
_globals['_DEBUGDRAW']._serialized_start=526
_globals['_DEBUGDRAW']._serialized_end=707
_globals['_LINE']._serialized_start=709
_globals['_LINE']._serialized_end=785
_globals['_COLOR']._serialized_start=787
_globals['_COLOR']._serialized_end=827
_globals['_DEBUGTEXT']._serialized_start=830
_globals['_DEBUGTEXT']._serialized_end=993
_globals['_DEBUGLINE']._serialized_start=995
_globals['_DEBUGLINE']._serialized_end=1080
_globals['_DEBUGBOX']._serialized_start=1082
_globals['_DEBUGBOX']._serialized_end=1202
_globals['_DEBUGSPHERE']._serialized_start=1204
_globals['_DEBUGSPHERE']._serialized_end=1300
_globals['_DEBUGCREATEUNIT']._serialized_start=1302
_globals['_DEBUGCREATEUNIT']._serialized_end=1409
_globals['_DEBUGKILLUNIT']._serialized_start=1411
_globals['_DEBUGKILLUNIT']._serialized_end=1439
_globals['_DEBUGTESTPROCESS']._serialized_start=1442
_globals['_DEBUGTESTPROCESS']._serialized_end=1570
_globals['_DEBUGTESTPROCESS_TEST']._serialized_start=1533
_globals['_DEBUGTESTPROCESS_TEST']._serialized_end=1570
_globals['_DEBUGSETSCORE']._serialized_start=1572
_globals['_DEBUGSETSCORE']._serialized_end=1602
_globals['_DEBUGENDGAME']._serialized_start=1604
_globals['_DEBUGENDGAME']._serialized_end=1726
_globals['_DEBUGENDGAME_ENDRESULT']._serialized_start=1680
_globals['_DEBUGENDGAME_ENDRESULT']._serialized_end=1726
_globals['_DEBUGSETUNITVALUE']._serialized_start=1729
_globals['_DEBUGSETUNITVALUE']._serialized_end=1894
_globals['_DEBUGSETUNITVALUE_UNITVALUE']._serialized_start=1848
_globals['_DEBUGSETUNITVALUE_UNITVALUE']._serialized_end=1894
# @@protoc_insertion_point(module_scope)

File diff suppressed because one or more lines are too long

View File

@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: s2clientprotocol/query.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
's2clientprotocol/query.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from . import common_pb2 as s2clientprotocol_dot_common__pb2
from . import error_pb2 as s2clientprotocol_dot_error__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cs2clientprotocol/query.proto\x12\x0eSC2APIProtocol\x1a\x1ds2clientprotocol/common.proto\x1a\x1cs2clientprotocol/error.proto\"\xf0\x01\n\x0cRequestQuery\x12\x34\n\x07pathing\x18\x01 \x03(\x0b\x32#.SC2APIProtocol.RequestQueryPathing\x12\x41\n\tabilities\x18\x02 \x03(\x0b\x32..SC2APIProtocol.RequestQueryAvailableAbilities\x12\x41\n\nplacements\x18\x03 \x03(\x0b\x32-.SC2APIProtocol.RequestQueryBuildingPlacement\x12$\n\x1cignore_resource_requirements\x18\x04 \x01(\x08\"\xce\x01\n\rResponseQuery\x12\x35\n\x07pathing\x18\x01 \x03(\x0b\x32$.SC2APIProtocol.ResponseQueryPathing\x12\x42\n\tabilities\x18\x02 \x03(\x0b\x32/.SC2APIProtocol.ResponseQueryAvailableAbilities\x12\x42\n\nplacements\x18\x03 \x03(\x0b\x32..SC2APIProtocol.ResponseQueryBuildingPlacement\"\x8a\x01\n\x13RequestQueryPathing\x12,\n\tstart_pos\x18\x01 \x01(\x0b\x32\x17.SC2APIProtocol.Point2DH\x00\x12\x12\n\x08unit_tag\x18\x02 \x01(\x04H\x00\x12(\n\x07\x65nd_pos\x18\x03 \x01(\x0b\x32\x17.SC2APIProtocol.Point2DB\x07\n\x05start\"(\n\x14ResponseQueryPathing\x12\x10\n\x08\x64istance\x18\x01 \x01(\x02\"2\n\x1eRequestQueryAvailableAbilities\x12\x10\n\x08unit_tag\x18\x01 \x01(\x04\"~\n\x1fResponseQueryAvailableAbilities\x12\x33\n\tabilities\x18\x01 \x03(\x0b\x32 .SC2APIProtocol.AvailableAbility\x12\x10\n\x08unit_tag\x18\x02 \x01(\x04\x12\x14\n\x0cunit_type_id\x18\x03 \x01(\r\"z\n\x1dRequestQueryBuildingPlacement\x12\x12\n\nability_id\x18\x01 \x01(\x05\x12+\n\ntarget_pos\x18\x02 \x01(\x0b\x32\x17.SC2APIProtocol.Point2D\x12\x18\n\x10placing_unit_tag\x18\x03 \x01(\x04\"N\n\x1eResponseQueryBuildingPlacement\x12,\n\x06result\x18\x01 \x01(\x0e\x32\x1c.SC2APIProtocol.ActionResult')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 's2clientprotocol.query_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_REQUESTQUERY']._serialized_start=110
_globals['_REQUESTQUERY']._serialized_end=350
_globals['_RESPONSEQUERY']._serialized_start=353
_globals['_RESPONSEQUERY']._serialized_end=559
_globals['_REQUESTQUERYPATHING']._serialized_start=562
_globals['_REQUESTQUERYPATHING']._serialized_end=700
_globals['_RESPONSEQUERYPATHING']._serialized_start=702
_globals['_RESPONSEQUERYPATHING']._serialized_end=742
_globals['_REQUESTQUERYAVAILABLEABILITIES']._serialized_start=744
_globals['_REQUESTQUERYAVAILABLEABILITIES']._serialized_end=794
_globals['_RESPONSEQUERYAVAILABLEABILITIES']._serialized_start=796
_globals['_RESPONSEQUERYAVAILABLEABILITIES']._serialized_end=922
_globals['_REQUESTQUERYBUILDINGPLACEMENT']._serialized_start=924
_globals['_REQUESTQUERYBUILDINGPLACEMENT']._serialized_end=1046
_globals['_RESPONSEQUERYBUILDINGPLACEMENT']._serialized_start=1048
_globals['_RESPONSEQUERYBUILDINGPLACEMENT']._serialized_end=1126
# @@protoc_insertion_point(module_scope)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: s2clientprotocol/score.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
's2clientprotocol/score.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cs2clientprotocol/score.proto\x12\x0eSC2APIProtocol\"\xa8\x01\n\x05Score\x12\x33\n\nscore_type\x18\x06 \x01(\x0e\x32\x1f.SC2APIProtocol.Score.ScoreType\x12\r\n\x05score\x18\x07 \x01(\x05\x12\x33\n\rscore_details\x18\x08 \x01(\x0b\x32\x1c.SC2APIProtocol.ScoreDetails\"&\n\tScoreType\x12\x0e\n\nCurriculum\x10\x01\x12\t\n\x05Melee\x10\x02\"h\n\x14\x43\x61tegoryScoreDetails\x12\x0c\n\x04none\x18\x01 \x01(\x02\x12\x0c\n\x04\x61rmy\x18\x02 \x01(\x02\x12\x0f\n\x07\x65\x63onomy\x18\x03 \x01(\x02\x12\x12\n\ntechnology\x18\x04 \x01(\x02\x12\x0f\n\x07upgrade\x18\x05 \x01(\x02\"B\n\x11VitalScoreDetails\x12\x0c\n\x04life\x18\x01 \x01(\x02\x12\x0f\n\x07shields\x18\x02 \x01(\x02\x12\x0e\n\x06\x65nergy\x18\x03 \x01(\x02\"\x8a\n\n\x0cScoreDetails\x12\x1c\n\x14idle_production_time\x18\x01 \x01(\x02\x12\x18\n\x10idle_worker_time\x18\x02 \x01(\x02\x12\x19\n\x11total_value_units\x18\x03 \x01(\x02\x12\x1e\n\x16total_value_structures\x18\x04 \x01(\x02\x12\x1a\n\x12killed_value_units\x18\x05 \x01(\x02\x12\x1f\n\x17killed_value_structures\x18\x06 \x01(\x02\x12\x1a\n\x12\x63ollected_minerals\x18\x07 \x01(\x02\x12\x19\n\x11\x63ollected_vespene\x18\x08 \x01(\x02\x12 \n\x18\x63ollection_rate_minerals\x18\t \x01(\x02\x12\x1f\n\x17\x63ollection_rate_vespene\x18\n \x01(\x02\x12\x16\n\x0espent_minerals\x18\x0b \x01(\x02\x12\x15\n\rspent_vespene\x18\x0c \x01(\x02\x12\x37\n\tfood_used\x18\r \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12=\n\x0fkilled_minerals\x18\x0e \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12<\n\x0ekilled_vespene\x18\x0f \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12;\n\rlost_minerals\x18\x10 \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12:\n\x0clost_vespene\x18\x11 \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12\x44\n\x16\x66riendly_fire_minerals\x18\x12 \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12\x43\n\x15\x66riendly_fire_vespene\x18\x13 \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12;\n\rused_minerals\x18\x14 \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12:\n\x0cused_vespene\x18\x15 \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12\x41\n\x13total_used_minerals\x18\x16 \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12@\n\x12total_used_vespene\x18\x17 \x01(\x0b\x32$.SC2APIProtocol.CategoryScoreDetails\x12=\n\x12total_damage_dealt\x18\x18 \x01(\x0b\x32!.SC2APIProtocol.VitalScoreDetails\x12=\n\x12total_damage_taken\x18\x19 \x01(\x0b\x32!.SC2APIProtocol.VitalScoreDetails\x12\x37\n\x0ctotal_healed\x18\x1a \x01(\x0b\x32!.SC2APIProtocol.VitalScoreDetails\x12\x13\n\x0b\x63urrent_apm\x18\x1b \x01(\x02\x12\x1d\n\x15\x63urrent_effective_apm\x18\x1c \x01(\x02')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 's2clientprotocol.score_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_SCORE']._serialized_start=49
_globals['_SCORE']._serialized_end=217
_globals['_SCORE_SCORETYPE']._serialized_start=179
_globals['_SCORE_SCORETYPE']._serialized_end=217
_globals['_CATEGORYSCOREDETAILS']._serialized_start=219
_globals['_CATEGORYSCOREDETAILS']._serialized_end=323
_globals['_VITALSCOREDETAILS']._serialized_start=325
_globals['_VITALSCOREDETAILS']._serialized_end=391
_globals['_SCOREDETAILS']._serialized_start=394
_globals['_SCOREDETAILS']._serialized_end=1684
# @@protoc_insertion_point(module_scope)

View File

@@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: s2clientprotocol/spatial.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
's2clientprotocol/spatial.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from . import common_pb2 as s2clientprotocol_dot_common__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1es2clientprotocol/spatial.proto\x12\x0eSC2APIProtocol\x1a\x1ds2clientprotocol/common.proto\"\x88\x01\n\x17ObservationFeatureLayer\x12.\n\x07renders\x18\x01 \x01(\x0b\x32\x1d.SC2APIProtocol.FeatureLayers\x12=\n\x0fminimap_renders\x18\x02 \x01(\x0b\x32$.SC2APIProtocol.FeatureLayersMinimap\"\x9c\n\n\rFeatureLayers\x12-\n\nheight_map\x18\x01 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x31\n\x0evisibility_map\x18\x02 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12(\n\x05\x63reep\x18\x03 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12(\n\x05power\x18\x04 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12,\n\tplayer_id\x18\x05 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12,\n\tunit_type\x18\x06 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12+\n\x08selected\x18\x07 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x32\n\x0funit_hit_points\x18\x08 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x38\n\x15unit_hit_points_ratio\x18\x11 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12.\n\x0bunit_energy\x18\t \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x34\n\x11unit_energy_ratio\x18\x12 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12/\n\x0cunit_shields\x18\n \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x35\n\x12unit_shields_ratio\x18\x13 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x32\n\x0fplayer_relative\x18\x0b \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x32\n\x0funit_density_aa\x18\x0e \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12/\n\x0cunit_density\x18\x0f \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12*\n\x07\x65\x66\x66\x65\x63ts\x18\x14 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x31\n\x0ehallucinations\x18\x15 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12*\n\x07\x63loaked\x18\x16 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\'\n\x04\x62lip\x18\x17 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12(\n\x05\x62uffs\x18\x18 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x30\n\rbuff_duration\x18\x1a \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12)\n\x06\x61\x63tive\x18\x19 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x31\n\x0e\x62uild_progress\x18\x1b \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12,\n\tbuildable\x18\x1c \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12+\n\x08pathable\x18\x1d \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12.\n\x0bplaceholder\x18\x1e \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\"\x90\x04\n\x14\x46\x65\x61tureLayersMinimap\x12-\n\nheight_map\x18\x01 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x31\n\x0evisibility_map\x18\x02 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12(\n\x05\x63reep\x18\x03 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12)\n\x06\x63\x61mera\x18\x04 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12,\n\tplayer_id\x18\x05 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12\x32\n\x0fplayer_relative\x18\x06 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12+\n\x08selected\x18\x07 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12)\n\x06\x61lerts\x18\t \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12,\n\tbuildable\x18\n \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12+\n\x08pathable\x18\x0b \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12,\n\tunit_type\x18\x08 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\"g\n\x11ObservationRender\x12&\n\x03map\x18\x01 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\x12*\n\x07minimap\x18\x02 \x01(\x0b\x32\x19.SC2APIProtocol.ImageData\"\xbb\x02\n\rActionSpatial\x12@\n\x0cunit_command\x18\x01 \x01(\x0b\x32(.SC2APIProtocol.ActionSpatialUnitCommandH\x00\x12>\n\x0b\x63\x61mera_move\x18\x02 \x01(\x0b\x32\'.SC2APIProtocol.ActionSpatialCameraMoveH\x00\x12O\n\x14unit_selection_point\x18\x03 \x01(\x0b\x32/.SC2APIProtocol.ActionSpatialUnitSelectionPointH\x00\x12M\n\x13unit_selection_rect\x18\x04 \x01(\x0b\x32..SC2APIProtocol.ActionSpatialUnitSelectionRectH\x00\x42\x08\n\x06\x61\x63tion\"\xbe\x01\n\x18\x41\x63tionSpatialUnitCommand\x12\x12\n\nability_id\x18\x01 \x01(\x05\x12\x35\n\x13target_screen_coord\x18\x02 \x01(\x0b\x32\x16.SC2APIProtocol.PointIH\x00\x12\x36\n\x14target_minimap_coord\x18\x03 \x01(\x0b\x32\x16.SC2APIProtocol.PointIH\x00\x12\x15\n\rqueue_command\x18\x04 \x01(\x08\x42\x08\n\x06target\"I\n\x17\x41\x63tionSpatialCameraMove\x12.\n\x0e\x63\x65nter_minimap\x18\x01 \x01(\x0b\x32\x16.SC2APIProtocol.PointI\"\xda\x01\n\x1f\x41\x63tionSpatialUnitSelectionPoint\x12\x36\n\x16selection_screen_coord\x18\x01 \x01(\x0b\x32\x16.SC2APIProtocol.PointI\x12\x42\n\x04type\x18\x02 \x01(\x0e\x32\x34.SC2APIProtocol.ActionSpatialUnitSelectionPoint.Type\";\n\x04Type\x12\n\n\x06Select\x10\x01\x12\n\n\x06Toggle\x10\x02\x12\x0b\n\x07\x41llType\x10\x03\x12\x0e\n\nAddAllType\x10\x04\"s\n\x1e\x41\x63tionSpatialUnitSelectionRect\x12:\n\x16selection_screen_coord\x18\x01 \x03(\x0b\x32\x1a.SC2APIProtocol.RectangleI\x12\x15\n\rselection_add\x18\x02 \x01(\x08')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 's2clientprotocol.spatial_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_OBSERVATIONFEATURELAYER']._serialized_start=82
_globals['_OBSERVATIONFEATURELAYER']._serialized_end=218
_globals['_FEATURELAYERS']._serialized_start=221
_globals['_FEATURELAYERS']._serialized_end=1529
_globals['_FEATURELAYERSMINIMAP']._serialized_start=1532
_globals['_FEATURELAYERSMINIMAP']._serialized_end=2060
_globals['_OBSERVATIONRENDER']._serialized_start=2062
_globals['_OBSERVATIONRENDER']._serialized_end=2165
_globals['_ACTIONSPATIAL']._serialized_start=2168
_globals['_ACTIONSPATIAL']._serialized_end=2483
_globals['_ACTIONSPATIALUNITCOMMAND']._serialized_start=2486
_globals['_ACTIONSPATIALUNITCOMMAND']._serialized_end=2676
_globals['_ACTIONSPATIALCAMERAMOVE']._serialized_start=2678
_globals['_ACTIONSPATIALCAMERAMOVE']._serialized_end=2751
_globals['_ACTIONSPATIALUNITSELECTIONPOINT']._serialized_start=2754
_globals['_ACTIONSPATIALUNITSELECTIONPOINT']._serialized_end=2972
_globals['_ACTIONSPATIALUNITSELECTIONPOINT_TYPE']._serialized_start=2913
_globals['_ACTIONSPATIALUNITSELECTIONPOINT_TYPE']._serialized_end=2972
_globals['_ACTIONSPATIALUNITSELECTIONRECT']._serialized_start=2974
_globals['_ACTIONSPATIALUNITSELECTIONRECT']._serialized_end=3089
# @@protoc_insertion_point(module_scope)

View File

@@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: s2clientprotocol/ui.proto
# Protobuf Python Version: 6.31.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
6,
31,
1,
'',
's2clientprotocol/ui.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19s2clientprotocol/ui.proto\x12\x0eSC2APIProtocol\"\x86\x02\n\rObservationUI\x12,\n\x06groups\x18\x01 \x03(\x0b\x32\x1c.SC2APIProtocol.ControlGroup\x12-\n\x06single\x18\x02 \x01(\x0b\x32\x1b.SC2APIProtocol.SinglePanelH\x00\x12+\n\x05multi\x18\x03 \x01(\x0b\x32\x1a.SC2APIProtocol.MultiPanelH\x00\x12+\n\x05\x63\x61rgo\x18\x04 \x01(\x0b\x32\x1a.SC2APIProtocol.CargoPanelH\x00\x12\x35\n\nproduction\x18\x05 \x01(\x0b\x32\x1f.SC2APIProtocol.ProductionPanelH\x00\x42\x07\n\x05panel\"T\n\x0c\x43ontrolGroup\x12\x1b\n\x13\x63ontrol_group_index\x18\x01 \x01(\r\x12\x18\n\x10leader_unit_type\x18\x02 \x01(\r\x12\r\n\x05\x63ount\x18\x03 \x01(\r\"\x85\x02\n\x08UnitInfo\x12\x11\n\tunit_type\x18\x01 \x01(\r\x12\x17\n\x0fplayer_relative\x18\x02 \x01(\r\x12\x0e\n\x06health\x18\x03 \x01(\x05\x12\x0f\n\x07shields\x18\x04 \x01(\x05\x12\x0e\n\x06\x65nergy\x18\x05 \x01(\x05\x12\x1d\n\x15transport_slots_taken\x18\x06 \x01(\x05\x12\x16\n\x0e\x62uild_progress\x18\x07 \x01(\x02\x12(\n\x06\x61\x64\x64_on\x18\x08 \x01(\x0b\x32\x18.SC2APIProtocol.UnitInfo\x12\x12\n\nmax_health\x18\t \x01(\x05\x12\x13\n\x0bmax_shields\x18\n \x01(\x05\x12\x12\n\nmax_energy\x18\x0b \x01(\x05\"\x9d\x01\n\x0bSinglePanel\x12&\n\x04unit\x18\x01 \x01(\x0b\x32\x18.SC2APIProtocol.UnitInfo\x12\x1c\n\x14\x61ttack_upgrade_level\x18\x02 \x01(\x05\x12\x1b\n\x13\x61rmor_upgrade_level\x18\x03 \x01(\x05\x12\x1c\n\x14shield_upgrade_level\x18\x04 \x01(\x05\x12\r\n\x05\x62uffs\x18\x05 \x03(\x05\"5\n\nMultiPanel\x12\'\n\x05units\x18\x01 \x03(\x0b\x32\x18.SC2APIProtocol.UnitInfo\"{\n\nCargoPanel\x12&\n\x04unit\x18\x01 \x01(\x0b\x32\x18.SC2APIProtocol.UnitInfo\x12,\n\npassengers\x18\x02 \x03(\x0b\x32\x18.SC2APIProtocol.UnitInfo\x12\x17\n\x0fslots_available\x18\x03 \x01(\x05\"7\n\tBuildItem\x12\x12\n\nability_id\x18\x01 \x01(\r\x12\x16\n\x0e\x62uild_progress\x18\x02 \x01(\x02\"\x9d\x01\n\x0fProductionPanel\x12&\n\x04unit\x18\x01 \x01(\x0b\x32\x18.SC2APIProtocol.UnitInfo\x12-\n\x0b\x62uild_queue\x18\x02 \x03(\x0b\x32\x18.SC2APIProtocol.UnitInfo\x12\x33\n\x10production_queue\x18\x03 \x03(\x0b\x32\x19.SC2APIProtocol.BuildItem\"\xda\x04\n\x08\x41\x63tionUI\x12;\n\rcontrol_group\x18\x01 \x01(\x0b\x32\".SC2APIProtocol.ActionControlGroupH\x00\x12\x37\n\x0bselect_army\x18\x02 \x01(\x0b\x32 .SC2APIProtocol.ActionSelectArmyH\x00\x12\x42\n\x11select_warp_gates\x18\x03 \x01(\x0b\x32%.SC2APIProtocol.ActionSelectWarpGatesH\x00\x12\x39\n\x0cselect_larva\x18\x04 \x01(\x0b\x32!.SC2APIProtocol.ActionSelectLarvaH\x00\x12\x44\n\x12select_idle_worker\x18\x05 \x01(\x0b\x32&.SC2APIProtocol.ActionSelectIdleWorkerH\x00\x12\x37\n\x0bmulti_panel\x18\x06 \x01(\x0b\x32 .SC2APIProtocol.ActionMultiPanelH\x00\x12=\n\x0b\x63\x61rgo_panel\x18\x07 \x01(\x0b\x32&.SC2APIProtocol.ActionCargoPanelUnloadH\x00\x12P\n\x10production_panel\x18\x08 \x01(\x0b\x32\x34.SC2APIProtocol.ActionProductionPanelRemoveFromQueueH\x00\x12?\n\x0ftoggle_autocast\x18\t \x01(\x0b\x32$.SC2APIProtocol.ActionToggleAutocastH\x00\x42\x08\n\x06\x61\x63tion\"\xd4\x01\n\x12\x41\x63tionControlGroup\x12\x45\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x35.SC2APIProtocol.ActionControlGroup.ControlGroupAction\x12\x1b\n\x13\x63ontrol_group_index\x18\x02 \x01(\r\"Z\n\x12\x43ontrolGroupAction\x12\n\n\x06Recall\x10\x01\x12\x07\n\x03Set\x10\x02\x12\n\n\x06\x41ppend\x10\x03\x12\x0f\n\x0bSetAndSteal\x10\x04\x12\x12\n\x0e\x41ppendAndSteal\x10\x05\")\n\x10\x41\x63tionSelectArmy\x12\x15\n\rselection_add\x18\x01 \x01(\x08\".\n\x15\x41\x63tionSelectWarpGates\x12\x15\n\rselection_add\x18\x01 \x01(\x08\"\x13\n\x11\x41\x63tionSelectLarva\"\x82\x01\n\x16\x41\x63tionSelectIdleWorker\x12\x39\n\x04type\x18\x01 \x01(\x0e\x32+.SC2APIProtocol.ActionSelectIdleWorker.Type\"-\n\x04Type\x12\x07\n\x03Set\x10\x01\x12\x07\n\x03\x41\x64\x64\x10\x02\x12\x07\n\x03\x41ll\x10\x03\x12\n\n\x06\x41\x64\x64\x41ll\x10\x04\"\xb3\x01\n\x10\x41\x63tionMultiPanel\x12\x33\n\x04type\x18\x01 \x01(\x0e\x32%.SC2APIProtocol.ActionMultiPanel.Type\x12\x12\n\nunit_index\x18\x02 \x01(\x05\"V\n\x04Type\x12\x10\n\x0cSingleSelect\x10\x01\x12\x10\n\x0c\x44\x65selectUnit\x10\x02\x12\x13\n\x0fSelectAllOfType\x10\x03\x12\x15\n\x11\x44\x65selectAllOfType\x10\x04\",\n\x16\x41\x63tionCargoPanelUnload\x12\x12\n\nunit_index\x18\x01 \x01(\x05\":\n$ActionProductionPanelRemoveFromQueue\x12\x12\n\nunit_index\x18\x01 \x01(\x05\"*\n\x14\x41\x63tionToggleAutocast\x12\x12\n\nability_id\x18\x01 \x01(\x05')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 's2clientprotocol.ui_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_OBSERVATIONUI']._serialized_start=46
_globals['_OBSERVATIONUI']._serialized_end=308
_globals['_CONTROLGROUP']._serialized_start=310
_globals['_CONTROLGROUP']._serialized_end=394
_globals['_UNITINFO']._serialized_start=397
_globals['_UNITINFO']._serialized_end=658
_globals['_SINGLEPANEL']._serialized_start=661
_globals['_SINGLEPANEL']._serialized_end=818
_globals['_MULTIPANEL']._serialized_start=820
_globals['_MULTIPANEL']._serialized_end=873
_globals['_CARGOPANEL']._serialized_start=875
_globals['_CARGOPANEL']._serialized_end=998
_globals['_BUILDITEM']._serialized_start=1000
_globals['_BUILDITEM']._serialized_end=1055
_globals['_PRODUCTIONPANEL']._serialized_start=1058
_globals['_PRODUCTIONPANEL']._serialized_end=1215
_globals['_ACTIONUI']._serialized_start=1218
_globals['_ACTIONUI']._serialized_end=1820
_globals['_ACTIONCONTROLGROUP']._serialized_start=1823
_globals['_ACTIONCONTROLGROUP']._serialized_end=2035
_globals['_ACTIONCONTROLGROUP_CONTROLGROUPACTION']._serialized_start=1945
_globals['_ACTIONCONTROLGROUP_CONTROLGROUPACTION']._serialized_end=2035
_globals['_ACTIONSELECTARMY']._serialized_start=2037
_globals['_ACTIONSELECTARMY']._serialized_end=2078
_globals['_ACTIONSELECTWARPGATES']._serialized_start=2080
_globals['_ACTIONSELECTWARPGATES']._serialized_end=2126
_globals['_ACTIONSELECTLARVA']._serialized_start=2128
_globals['_ACTIONSELECTLARVA']._serialized_end=2147
_globals['_ACTIONSELECTIDLEWORKER']._serialized_start=2150
_globals['_ACTIONSELECTIDLEWORKER']._serialized_end=2280
_globals['_ACTIONSELECTIDLEWORKER_TYPE']._serialized_start=2235
_globals['_ACTIONSELECTIDLEWORKER_TYPE']._serialized_end=2280
_globals['_ACTIONMULTIPANEL']._serialized_start=2283
_globals['_ACTIONMULTIPANEL']._serialized_end=2462
_globals['_ACTIONMULTIPANEL_TYPE']._serialized_start=2376
_globals['_ACTIONMULTIPANEL_TYPE']._serialized_end=2462
_globals['_ACTIONCARGOPANELUNLOAD']._serialized_start=2464
_globals['_ACTIONCARGOPANELUNLOAD']._serialized_end=2508
_globals['_ACTIONPRODUCTIONPANELREMOVEFROMQUEUE']._serialized_start=2510
_globals['_ACTIONPRODUCTIONPANELREMOVEFROMQUEUE']._serialized_end=2568
_globals['_ACTIONTOGGLEAUTOCAST']._serialized_start=2570
_globals['_ACTIONTOGGLEAUTOCAST']._serialized_end=2612
# @@protoc_insertion_point(module_scope)

View File

@@ -4,7 +4,7 @@ from contextlib import suppress
from aiohttp import ClientWebSocketResponse from aiohttp import ClientWebSocketResponse
from worlds._sc2common.bot import logger from worlds._sc2common.bot import logger
from .proto import sc2api_pb2 as sc_pb from s2clientprotocol import sc2api_pb2 as sc_pb
from .data import Status from .data import Status

View File

@@ -8,7 +8,7 @@ import traceback
from aiohttp import WSMsgType, web from aiohttp import WSMsgType, web
from worlds._sc2common.bot import logger from worlds._sc2common.bot import logger
from .proto import sc2api_pb2 as sc_pb from s2clientprotocol import sc2api_pb2 as sc_pb
from .controller import Controller from .controller import Controller
from .data import Result, Status from .data import Result, Status

View File

@@ -1,6 +1,6 @@
import datetime import datetime
from .proto import score_pb2 as score_pb from s2clientprotocol import score_pb2 as score_pb
from .position import Point2 from .position import Point2

View File

@@ -1,5 +1,6 @@
s2clientprotocol>=5.0.11.90136.0
mpyq>=0.2.5 mpyq>=0.2.5
portpicker>=1.5.2 portpicker>=1.5.2
aiohttp>=3.8.4 aiohttp>=3.8.4
loguru>=0.7.0 loguru>=0.7.0
protobuf==6.31.1 protobuf==3.20.3

View File

@@ -8,7 +8,7 @@ import bsdiff4
import Utils import Utils
from settings import get_settings from settings import get_settings
from worlds.Files import APPatch from worlds.Files import APPatch, AutoPatchRegister
from .Locations import LocationData from .Locations import LocationData
ADVENTUREHASH: str = "157bddb7192754a45372be196797f284" ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"
@@ -78,7 +78,7 @@ class BatNoTouchLocation:
return ret_dict return ret_dict
class AdventureDeltaPatch(APPatch): class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister):
hash = ADVENTUREHASH hash = ADVENTUREHASH
game = "Adventure" game = "Adventure"
patch_file_ending = ".apadvn" patch_file_ending = ".apadvn"

View File

@@ -1,6 +1,4 @@
import asyncio import asyncio
import time
import Utils import Utils
import websockets import websockets
import functools import functools
@@ -210,9 +208,6 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
if not ctx.is_proxy_connected(): if not ctx.is_proxy_connected():
break break
if msg["cmd"] == "Bounce" and msg.get("tags") == ["DeathLink"] and "data" in msg:
msg["data"]["time"] = time.time()
await ctx.send_msgs([msg]) await ctx.send_msgs([msg])
except Exception as e: except Exception as e:

View File

@@ -243,7 +243,7 @@ guaranteed_first_acts = [
"Time Rift - Mafia of Cooks", "Time Rift - Mafia of Cooks",
"Time Rift - Dead Bird Studio", "Time Rift - Dead Bird Studio",
"Time Rift - Sleepy Subcon", "Time Rift - Sleepy Subcon",
"Time Rift - Alpine Skyline", "Time Rift - Alpine Skyline"
"Time Rift - Tour", "Time Rift - Tour",
"Time Rift - Rumbi Factory", "Time Rift - Rumbi Factory",
] ]

View File

@@ -239,7 +239,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
multiworld.worlds[item.player].collect(all_state_base, item) multiworld.worlds[item.player].collect(all_state_base, item)
pre_fill_items = [] pre_fill_items = []
for player in in_dungeon_player_ids: for player in in_dungeon_player_ids:
pre_fill_items += [item for item in multiworld.worlds[player].get_pre_fill_items() if not item.crystal] pre_fill_items += multiworld.worlds[player].get_pre_fill_items()
for item in in_dungeon_items: for item in in_dungeon_items:
try: try:
pre_fill_items.remove(item) pre_fill_items.remove(item)

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