mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 09:33:46 -07:00
Compare commits
1 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e93235f68 |
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
@@ -23,10 +23,9 @@ env:
|
||||
ENEMIZER_VERSION: 7.1
|
||||
# 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.
|
||||
APPIMAGE_FORK: 'PopTracker'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
||||
APPIMAGETOOL_VERSION: continuous
|
||||
APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
|
||||
APPIMAGE_RUNTIME_VERSION: continuous
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
||||
|
||||
permissions: # permissions required for attestation
|
||||
@@ -142,9 +141,9 @@ jobs:
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
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
|
||||
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
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- "!.github/workflows/**"
|
||||
- ".github/workflows/docker.yml"
|
||||
branches:
|
||||
- "main"
|
||||
- "*"
|
||||
tags:
|
||||
- "v?[0-9]+.[0-9]+.[0-9]*"
|
||||
workflow_dispatch:
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -11,10 +11,9 @@ env:
|
||||
ENEMIZER_VERSION: 7.1
|
||||
# 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.
|
||||
APPIMAGE_FORK: 'PopTracker'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
||||
APPIMAGETOOL_VERSION: continuous
|
||||
APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
|
||||
APPIMAGE_RUNTIME_VERSION: continuous
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
||||
|
||||
permissions: # permissions required for attestation
|
||||
@@ -128,9 +127,9 @@ jobs:
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
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
|
||||
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
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
|
||||
2
.github/workflows/unittests.yml
vendored
2
.github/workflows/unittests.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
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 Launcher.py --update_settings # make sure host.yaml exists for tests
|
||||
- name: Unittests
|
||||
|
||||
@@ -1721,10 +1721,9 @@ class Spoiler:
|
||||
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
|
||||
sphere_candidates])
|
||||
if not multiworld.has_beaten_game(state):
|
||||
raise RuntimeError("During playthrough generation, the game was determined to be unbeatable. "
|
||||
"Something went terribly wrong here. "
|
||||
f"Unreachable progression items: {sphere_candidates}")
|
||||
if any([multiworld.worlds[location.item.player].options.accessibility != 'minimal' for location in sphere_candidates]):
|
||||
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
||||
f'Something went terribly wrong here.')
|
||||
else:
|
||||
self.unreachables = sphere_candidates
|
||||
break
|
||||
|
||||
4
CommonClient.py
Executable file → Normal file
4
CommonClient.py
Executable file → Normal file
@@ -572,10 +572,6 @@ class CommonContext:
|
||||
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["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):
|
||||
logger.info(args["text"])
|
||||
|
||||
16
Generate.py
16
Generate.py
@@ -23,7 +23,7 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||
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
|
||||
settings = get_settings()
|
||||
defaults = settings.generator
|
||||
@@ -57,7 +57,7 @@ def mystery_argparse(argv: list[str] | None = None):
|
||||
parser.add_argument("--spoiler_only", action="store_true",
|
||||
help="Skips generation assertion and multidata, outputting only a spoiler log. "
|
||||
"Intended for debugging and testing purposes.")
|
||||
args = parser.parse_args(argv)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.skip_output and args.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
|
||||
elif category_name not in yaml:
|
||||
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:
|
||||
yaml[category_name][key] = option
|
||||
|
||||
@@ -367,10 +362,7 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
||||
f" received {type(new_value).__name__}.")
|
||||
cleaned_weights[option_name] = cleaned_value
|
||||
else:
|
||||
# Options starting with + and - may modify values in-place, and new_weights may be shared by multiple slots
|
||||
# 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
|
||||
cleaned_weights[option_name] = new_weights[option]
|
||||
new_options = set(cleaned_weights) - set(weights)
|
||||
weights.update(cleaned_weights)
|
||||
if new_options:
|
||||
@@ -393,8 +385,6 @@ def roll_meta_option(option_key, game: str, category_dict: dict) -> Any:
|
||||
if options[option_key].supports_weighting:
|
||||
return get_choice(option_key, category_dict)
|
||||
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}.")
|
||||
|
||||
|
||||
|
||||
@@ -75,17 +75,12 @@ def open_patch():
|
||||
launch([*exe, file], component.cli)
|
||||
|
||||
|
||||
def generate_yamls(*args):
|
||||
def generate_yamls():
|
||||
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")
|
||||
generate_yaml_templates(target, False)
|
||||
if not args.skip_open_folder:
|
||||
open_folder(target)
|
||||
open_folder(target)
|
||||
|
||||
|
||||
def browse_files():
|
||||
|
||||
6
Main.py
6
Main.py
@@ -326,7 +326,7 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
if current_sphere:
|
||||
spheres.append(dict(current_sphere))
|
||||
|
||||
multidata: NetUtils.MultiData = {
|
||||
multidata: NetUtils.MultiData | bytes = {
|
||||
"slot_data": slot_data,
|
||||
"slot_info": slot_info,
|
||||
"connect_names": {name: (0, player) for player, name in multiworld.player_name.items()},
|
||||
@@ -350,11 +350,11 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
|
||||
for key in ("slot_data", "er_hint_data"):
|
||||
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:
|
||||
f.write(bytes([3])) # version of format
|
||||
f.write(serialized_multidata)
|
||||
f.write(multidata)
|
||||
|
||||
output_file_futures.append(pool.submit(write_multidata))
|
||||
if not check_accessibility_task.result():
|
||||
|
||||
49
Options.py
49
Options.py
@@ -688,12 +688,6 @@ class Range(NumericOption):
|
||||
range_start = 0
|
||||
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):
|
||||
if value < self.range_start:
|
||||
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
|
||||
if text == "true":
|
||||
return cls.from_any(cls.default)
|
||||
# "false"
|
||||
return cls(0)
|
||||
|
||||
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)
|
||||
|
||||
else: # "false"
|
||||
return cls(0)
|
||||
return cls(int(text))
|
||||
|
||||
@classmethod
|
||||
def weighted_range(cls, text) -> Range:
|
||||
@@ -754,7 +731,9 @@ class Range(NumericOption):
|
||||
return cls(random.randint(cls.range_start, cls.range_end))
|
||||
else:
|
||||
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
|
||||
def custom_range(cls, text) -> Range:
|
||||
@@ -1039,8 +1018,6 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
|
||||
supports_weighting = False
|
||||
display_name = "Plando Texts"
|
||||
|
||||
visibility = Visibility.template | Visibility.complex_ui | Visibility.spoiler
|
||||
|
||||
def __init__(self, value: typing.Iterable[PlandoText]) -> None:
|
||||
self.value = list(deepcopy(value))
|
||||
super().__init__()
|
||||
@@ -1167,8 +1144,6 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
||||
entrances: typing.ClassVar[typing.AbstractSet[str]]
|
||||
exits: typing.ClassVar[typing.AbstractSet[str]]
|
||||
|
||||
visibility = Visibility.template | Visibility.complex_ui | Visibility.spoiler
|
||||
|
||||
duplicate_exits: bool = False
|
||||
"""Whether or not exits should be allowed to be duplicate."""
|
||||
|
||||
@@ -1460,7 +1435,6 @@ class DeathLink(Toggle):
|
||||
class ItemLinks(OptionList):
|
||||
"""Share part of your item pool with other players."""
|
||||
display_name = "Item Links"
|
||||
visibility = Visibility.template | Visibility.complex_ui | Visibility.spoiler
|
||||
rich_text_doc = True
|
||||
default = []
|
||||
schema = Schema([
|
||||
@@ -1752,16 +1726,11 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
|
||||
def dictify_range(option: Range):
|
||||
data = {option.default: 50}
|
||||
for sub_option in ["random", "random-low", "random-high",
|
||||
f"random-range-{option.range_start}-{option.range_end}"]:
|
||||
for sub_option in ["random", "random-low", "random-high"]:
|
||||
if sub_option != option.default:
|
||||
data[sub_option] = 0
|
||||
notes = {
|
||||
"random-low": "random value weighted towards lower values",
|
||||
"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}"
|
||||
}
|
||||
|
||||
notes = {}
|
||||
for name, number in getattr(option, "special_range_names", {}).items():
|
||||
notes[name] = f"equivalent to {number}"
|
||||
if number in data:
|
||||
|
||||
@@ -1,661 +0,0 @@
|
||||
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("&", "&") \
|
||||
.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))
|
||||
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_random = option.default == "random"
|
||||
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_random 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():
|
||||
if not options:
|
||||
continue # Game Options can be empty if every other option is in another group
|
||||
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:
|
||||
if name and option is not Removed and option.visibility & Visibility.simple_ui:
|
||||
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()
|
||||
@@ -82,7 +82,6 @@ Currently, the following games are supported:
|
||||
* Paint
|
||||
* Celeste (Open World)
|
||||
* Choo-Choo Charles
|
||||
* APQuest
|
||||
|
||||
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
|
||||
|
||||
58
Utils.py
58
Utils.py
@@ -48,7 +48,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.6.5"
|
||||
__version__ = "0.6.4"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -314,8 +314,12 @@ def get_public_ipv6() -> str:
|
||||
return ip
|
||||
|
||||
|
||||
OptionsType = Settings # TODO: remove when removing get_options
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -751,11 +755,6 @@ def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *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):
|
||||
env = os.environ
|
||||
if "LD_LIBRARY_PATH" in env:
|
||||
@@ -806,51 +805,6 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
||||
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:
|
||||
if is_kivy_running():
|
||||
raise RuntimeError("kivy should not be running in multiprocess")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import base64
|
||||
import os
|
||||
import socket
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
from flask import Flask
|
||||
@@ -62,21 +61,20 @@ cache = Cache()
|
||||
Compress(app)
|
||||
|
||||
|
||||
def to_python(value: str) -> uuid.UUID:
|
||||
def to_python(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')
|
||||
|
||||
|
||||
class B64UUIDConverter(BaseConverter):
|
||||
|
||||
def to_python(self, value: str) -> uuid.UUID:
|
||||
def to_python(self, value):
|
||||
return to_python(value)
|
||||
|
||||
def to_url(self, value: typing.Any) -> str:
|
||||
assert isinstance(value, uuid.UUID)
|
||||
def to_url(self, value):
|
||||
return to_url(value)
|
||||
|
||||
|
||||
@@ -86,7 +84,7 @@ app.jinja_env.filters["suuid"] = to_url
|
||||
app.jinja_env.filters["title_sorted"] = title_sorted
|
||||
|
||||
|
||||
def register() -> None:
|
||||
def register():
|
||||
"""Import submodules, triggering their registering on flask routing.
|
||||
Note: initializes worlds subsystem."""
|
||||
import importlib
|
||||
|
||||
@@ -17,7 +17,7 @@ from .locker import Locker, AlreadyRunningException
|
||||
_stop_event = Event()
|
||||
|
||||
|
||||
def stop() -> None:
|
||||
def stop():
|
||||
"""Stops previously launched threads"""
|
||||
global _stop_event
|
||||
stop_event = _stop_event
|
||||
|
||||
@@ -137,7 +137,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))
|
||||
|
||||
args = mystery_argparse([]) # Just to set up the Namespace with defaults
|
||||
args = mystery_argparse()
|
||||
args.multi = playercount
|
||||
args.seed = seed
|
||||
args.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
|
||||
|
||||
@@ -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
|
||||
@@ -1,7 +1,5 @@
|
||||
import datetime
|
||||
import os
|
||||
import warnings
|
||||
from enum import StrEnum
|
||||
from typing import Any, IO, Dict, Iterator, List, Tuple, Union
|
||||
|
||||
import jinja2.exceptions
|
||||
@@ -11,29 +9,14 @@ from werkzeug.utils import secure_filename
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister, World
|
||||
from . import app, cache
|
||||
from .markdown import render_markdown
|
||||
from .models import Seed, Room, Command, UUID, uuid4
|
||||
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:
|
||||
if game_name not in AutoWorldRegister.world_types:
|
||||
return "grass"
|
||||
chosen_theme = AutoWorldRegister.world_types[game_name].web.theme
|
||||
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
|
||||
if game_name in AutoWorldRegister.world_types:
|
||||
return AutoWorldRegister.world_types[game_name].web.theme
|
||||
return 'grass'
|
||||
|
||||
|
||||
def get_visible_worlds() -> dict[str, type(World)]:
|
||||
@@ -44,6 +27,49 @@ def get_visible_worlds() -> dict[str, type(World)]:
|
||||
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(jinja2.exceptions.TemplateNotFound)
|
||||
def page_not_found(err):
|
||||
@@ -65,9 +91,10 @@ def game_info(game, lang):
|
||||
theme = get_world_theme(game)
|
||||
secure_game_name = secure_filename(game)
|
||||
lang = secure_filename(lang)
|
||||
file_dir = os.path.join(app.static_folder, "generated", "docs", secure_game_name)
|
||||
file_dir_url = url_for("static", filename=f"generated/docs/{secure_game_name}")
|
||||
document = render_markdown(os.path.join(file_dir, f"{lang}_{secure_game_name}.md"), file_dir_url)
|
||||
document = render_markdown(os.path.join(
|
||||
app.static_folder, "generated", "docs",
|
||||
secure_game_name, f"{lang}_{secure_game_name}.md"
|
||||
))
|
||||
return render_template(
|
||||
"markdown_document.html",
|
||||
title=f"{game} Guide",
|
||||
@@ -92,9 +119,10 @@ def tutorial(game: str, file: str):
|
||||
theme = get_world_theme(game)
|
||||
secure_game_name = secure_filename(game)
|
||||
file = secure_filename(file)
|
||||
file_dir = os.path.join(app.static_folder, "generated", "docs", secure_game_name)
|
||||
file_dir_url = url_for("static", filename=f"generated/docs/{secure_game_name}")
|
||||
document = render_markdown(os.path.join(file_dir, f"{file}.md"), file_dir_url)
|
||||
document = render_markdown(os.path.join(
|
||||
app.static_folder, "generated", "docs",
|
||||
secure_game_name, file+".md"
|
||||
))
|
||||
return render_template(
|
||||
"markdown_document.html",
|
||||
title=f"{game} Guide",
|
||||
|
||||
@@ -13,7 +13,6 @@ from Utils import local_path
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from . import app, cache
|
||||
from .generate import get_meta
|
||||
from .misc import get_world_theme
|
||||
|
||||
|
||||
def create() -> None:
|
||||
@@ -23,6 +22,12 @@ def create() -> None:
|
||||
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]:
|
||||
world = AutoWorldRegister.world_types[world_name]
|
||||
if world.hidden or world.web.options_page is False:
|
||||
|
||||
@@ -4,10 +4,10 @@ pony>=0.7.19; python_version <= '3.12'
|
||||
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
|
||||
waitress>=3.0.2
|
||||
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; python_version >= '3.12'
|
||||
Flask-Compress==1.18; python_version <= '3.11' # 3.11's pkg_resources can't resolve the new "backports.zstd" dependency
|
||||
Flask-Limiter>=3.12
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
setproctitle>=1.3.5
|
||||
mistune>=3.1.3
|
||||
docutils>=0.22.2
|
||||
|
||||
@@ -241,9 +241,12 @@ input[type="checkbox"]{
|
||||
}
|
||||
|
||||
/* Hidden items */
|
||||
.hidden-class:not(:has(.f:not(.unacquired))), .hidden-item{
|
||||
.hidden-class:not(:has(img.acquired)){
|
||||
display: none;
|
||||
}
|
||||
.hidden-item:not(.acquired){
|
||||
display:none;
|
||||
}
|
||||
|
||||
/* Keys */
|
||||
#keys ol, #keys ul{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1228,7 +1228,7 @@ if "Starcraft 2" in network_data_package["games"]:
|
||||
def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
|
||||
SC2WOL_ITEM_ID_OFFSET = 1000
|
||||
SC2HOTS_ITEM_ID_OFFSET = 2000
|
||||
SC2LOTV_ITEM_ID_OFFSET = 3000
|
||||
SC2LOTV_ITEM_ID_OFFSET = 2000
|
||||
SC2_KEY_ITEM_ID_OFFSET = 4000
|
||||
NCO_LOCATION_ID_LOW = 20004500
|
||||
NCO_LOCATION_ID_HIGH = NCO_LOCATION_ID_LOW + 1000
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pytest>=9.0.1,<10 # this includes subtests support
|
||||
pytest-xdist>=3.8.0
|
||||
@@ -224,7 +224,6 @@
|
||||
height: self.content.texture_size[1] + 80
|
||||
<ScrollBox>:
|
||||
layout: layout
|
||||
box_height: dp(100)
|
||||
bar_width: "12dp"
|
||||
scroll_wheel_distance: 40
|
||||
do_scroll_x: False
|
||||
@@ -235,11 +234,4 @@
|
||||
orientation: "vertical"
|
||||
spacing: 10
|
||||
size_hint_y: None
|
||||
height: max(self.minimum_height, root.box_height)
|
||||
|
||||
<MessageBoxLabel>:
|
||||
valign: "middle"
|
||||
halign: "center"
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
|
||||
height: self.minimum_height
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
<VisualRange>:
|
||||
id: this
|
||||
spacing: 15
|
||||
orientation: "horizontal"
|
||||
slider: slider
|
||||
tag: tag
|
||||
MDLabel:
|
||||
id: tag
|
||||
text: str(this.option.default) if this.option.default != "random" else 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 this.option.default != "random" 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 this.option.default != "random" 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.special_range_names.get(list(this.option.special_range_names.values()).index(this.option.default)) 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
|
||||
@@ -8,7 +8,3 @@ SELFLAUNCH: false
|
||||
# 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.
|
||||
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
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
# A Link to the Past
|
||||
/worlds/alttp/ @Berserker66
|
||||
|
||||
# APQuest
|
||||
# NewSoupVi is acting maintainer, but world belongs to core with the exception of the music
|
||||
/worlds/apquest/ @NewSoupVi
|
||||
|
||||
# Sudoku (APSudoku)
|
||||
/worlds/apsudoku/ @EmilyV99
|
||||
|
||||
|
||||
@@ -647,16 +647,6 @@ class Version(NamedTuple):
|
||||
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
|
||||
An enum representing the nature of a slot.
|
||||
|
||||
|
||||
22
kvui.py
22
kvui.py
@@ -34,17 +34,6 @@ from kivy.config import Config
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Config.set("kivy", "exit_on_escape", "0")
|
||||
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 kivy.core.window import Window
|
||||
from kivy.core.clipboard import Clipboard
|
||||
@@ -127,7 +116,7 @@ class ImageButton(MDIconButton):
|
||||
val = kwargs.pop(kwarg, "None")
|
||||
if val != "None":
|
||||
image_args[kwarg.replace("image_", "")] = val
|
||||
super().__init__(**kwargs)
|
||||
super().__init__()
|
||||
self.image = ApAsyncImage(**image_args)
|
||||
|
||||
def set_center(button, center):
|
||||
@@ -143,7 +132,6 @@ class ImageButton(MDIconButton):
|
||||
|
||||
class ScrollBox(MDScrollView):
|
||||
layout: MDBoxLayout = ObjectProperty(None)
|
||||
box_height: int = NumericProperty(dp(100))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -154,7 +142,6 @@ class ToggleButton(MDButton, ToggleButtonBehavior):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ToggleButton, self).__init__(*args, **kwargs)
|
||||
self.bind(state=self._update_bg)
|
||||
self._update_bg(self, self.state)
|
||||
|
||||
def _update_bg(self, _, state: str):
|
||||
if self.disabled:
|
||||
@@ -172,7 +159,7 @@ class ToggleButton(MDButton, ToggleButtonBehavior):
|
||||
child.text_color = self.theme_cls.onPrimaryColor
|
||||
child.icon_color = self.theme_cls.onPrimaryColor
|
||||
else:
|
||||
self.md_bg_color = self.theme_cls.surfaceContainerLowColor
|
||||
self.md_bg_color = self.theme_cls.surfaceContainerLowestColor
|
||||
for child in self.children:
|
||||
if child.theme_text_color == "Primary":
|
||||
child.theme_text_color = "Custom"
|
||||
@@ -186,6 +173,7 @@ class ToggleButton(MDButton, ToggleButtonBehavior):
|
||||
class ResizableTextField(MDTextField):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -249,7 +237,7 @@ Factory.register("HoverBehavior", HoverBehavior)
|
||||
|
||||
|
||||
class ToolTip(MDTooltipPlain):
|
||||
markup = True
|
||||
pass
|
||||
|
||||
|
||||
class ServerToolTip(ToolTip):
|
||||
@@ -284,8 +272,6 @@ class TooltipLabel(HovererableLabel, MDTooltip):
|
||||
def on_mouse_pos(self, window, pos):
|
||||
if not self.get_root_window():
|
||||
return # Abort if not displayed
|
||||
if self.disabled:
|
||||
return
|
||||
super().on_mouse_pos(window, pos)
|
||||
if self.refs and self.hovered:
|
||||
|
||||
|
||||
16
ruff.toml
16
ruff.toml
@@ -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.
|
||||
]
|
||||
11
setup.py
11
setup.py
@@ -146,16 +146,7 @@ def download_SNI() -> None:
|
||||
|
||||
signtool: str | None = None
|
||||
try:
|
||||
import socket
|
||||
|
||||
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:
|
||||
with urllib.request.urlopen('http://192.168.206.4:12345/connector/status') as response:
|
||||
html = response.read()
|
||||
if b"status=OK\n" in html:
|
||||
signtool = (r'signtool sign /sha1 6df76fe776b82869a5693ddcb1b04589cffa6faf /fd sha256 /td sha256 '
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -3,7 +3,6 @@
|
||||
# Run with `python test/hosting` instead,
|
||||
import logging
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
@@ -12,7 +11,7 @@ from test.hosting.client import Client
|
||||
from test.hosting.generate import generate_local
|
||||
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,
|
||||
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
|
||||
|
||||
failure = False
|
||||
@@ -57,62 +56,35 @@ else:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
warnings.simplefilter("ignore", ResourceWarning)
|
||||
warnings.simplefilter("ignore", UserWarning)
|
||||
warnings.simplefilter("ignore", DeprecationWarning)
|
||||
|
||||
spacer = '=' * 80
|
||||
|
||||
with TemporaryDirectory() as tempdir:
|
||||
empty_file = str(Path(tempdir) / "empty")
|
||||
open(empty_file, "w").close()
|
||||
sys.argv += ["--config_override", empty_file] # tests #5541
|
||||
multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]]
|
||||
p1_games: list[str] = []
|
||||
data_paths: list[Path | None] = []
|
||||
rooms: list[str] = []
|
||||
multidata: Path | None
|
||||
p1_games = []
|
||||
data_paths = []
|
||||
rooms = []
|
||||
|
||||
copy_world("VVVVVV", "Temp World")
|
||||
try:
|
||||
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)
|
||||
print(f"Generated [{n}] {', '.join(games)} as {multidata}\n")
|
||||
data_paths.append(multidata)
|
||||
p1_games.append(games[0])
|
||||
data_paths.append(multidata)
|
||||
finally:
|
||||
delete_world("Temp World")
|
||||
|
||||
webapp = get_app(tempdir)
|
||||
webhost_client = webapp.test_client()
|
||||
|
||||
for n, multidata in enumerate(data_paths, 1):
|
||||
assert multidata
|
||||
seed = upload_multidata(webhost_client, multidata)
|
||||
print(f"Uploaded [{n}] {multidata} as {seed}\n")
|
||||
room = create_room(webhost_client, seed)
|
||||
print(f"Started [{n}] {seed} 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")
|
||||
print(f"Uploaded [{n}] {multidata} as {room}\n")
|
||||
rooms.append(room)
|
||||
|
||||
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):
|
||||
involved_games = {"Archipelago"} | set(multi_games)
|
||||
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")
|
||||
prev_host_adr: str
|
||||
with WebHostServeGame(webhost_client, room) as host:
|
||||
sleep(.1) # wait for the server to fully start before doing anything
|
||||
prev_host_adr = host.address
|
||||
with Client(host.address, game, "Player1") as client:
|
||||
web_data_packages = client.games_packages
|
||||
@@ -141,7 +134,6 @@ if __name__ == "__main__":
|
||||
autohost(webapp.config) # this will spin the room right up again
|
||||
sleep(1) # make log less annoying
|
||||
# if saving failed, the next iteration will fail below
|
||||
sleep(2) # work around issue #5571
|
||||
|
||||
# verify server shut down
|
||||
try:
|
||||
@@ -164,31 +156,6 @@ if __name__ == "__main__":
|
||||
"customserver did not load or save correctly during/after "
|
||||
+ ("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
|
||||
expect_equal(local_data_packages, web_data_packages,
|
||||
"customserver datapackage differs from MultiServer")
|
||||
@@ -209,12 +176,10 @@ if __name__ == "__main__":
|
||||
print(f"Restoring multidata for {room}")
|
||||
set_multidata_for_room(webhost_client, room, old_data)
|
||||
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:
|
||||
assert_equal(len(client.checked_locations), 2,
|
||||
"Save was destroyed during exception in customserver")
|
||||
print("Save file is not busted 🥳")
|
||||
sleep(2) # work around issue #5571
|
||||
|
||||
finally:
|
||||
print("Stopping autohost")
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, cast
|
||||
from typing import TYPE_CHECKING, Optional, cast
|
||||
|
||||
from WebHostLib import to_python
|
||||
|
||||
@@ -14,7 +10,6 @@ if TYPE_CHECKING:
|
||||
|
||||
__all__ = [
|
||||
"get_app",
|
||||
"generate_remote",
|
||||
"upload_multidata",
|
||||
"create_room",
|
||||
"start_room",
|
||||
@@ -22,7 +17,6 @@ __all__ = [
|
||||
"set_room_timeout",
|
||||
"get_multidata_for_room",
|
||||
"set_multidata_for_room",
|
||||
"stop_autogen",
|
||||
"stop_autohost",
|
||||
]
|
||||
|
||||
@@ -39,43 +33,10 @@ def get_app(tempdir: str) -> "Flask":
|
||||
"TESTING": True,
|
||||
"HOST_ADDRESS": "localhost",
|
||||
"HOSTERS": 1,
|
||||
"GENERATORS": 1,
|
||||
"JOB_THRESHOLD": 1,
|
||||
})
|
||||
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:
|
||||
response = app_client.post("/uploads", data={
|
||||
"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
|
||||
|
||||
|
||||
def _stop_webhost_mp(name_filter: str, graceful: bool = True) -> None:
|
||||
def stop_autohost(graceful: bool = True) -> None:
|
||||
import os
|
||||
import signal
|
||||
|
||||
@@ -237,30 +198,13 @@ def _stop_webhost_mp(name_filter: str, graceful: bool = True) -> None:
|
||||
|
||||
stop()
|
||||
proc: multiprocessing.process.BaseProcess
|
||||
for proc in filter(lambda child: child.name.startswith(name_filter), 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
|
||||
for proc in filter(lambda child: child.name.startswith("MultiHoster"), multiprocessing.active_children()):
|
||||
if graceful and proc.pid:
|
||||
os.kill(proc.pid, getattr(signal, "CTRL_C_EVENT", signal.SIGINT))
|
||||
else:
|
||||
proc.kill()
|
||||
try:
|
||||
try:
|
||||
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)
|
||||
proc.join(30)
|
||||
except TimeoutError:
|
||||
proc.kill()
|
||||
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)
|
||||
|
||||
@@ -11,7 +11,7 @@ _new_worlds: dict[str, str] = {}
|
||||
|
||||
def copy(src: str, dst: str) -> None:
|
||||
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"
|
||||
if '"' in dst or "\\" in dst: # easier to reject than to escape
|
||||
|
||||
@@ -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("")
|
||||
self.assertIn(f'src="{self.base_url}/image.png"', html)
|
||||
|
||||
def test_absolute_img_no_rewrite(self) -> None:
|
||||
html = self.markdown("")
|
||||
self.assertIn(f'src="/image.png"', html)
|
||||
self.assertNotIn(self.base_url, html)
|
||||
|
||||
def test_remote_img_no_rewrite(self) -> None:
|
||||
html = self.markdown("")
|
||||
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("")
|
||||
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("".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("".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)
|
||||
@@ -224,7 +224,7 @@ class WebWorld(metaclass=WebWorldRegister):
|
||||
tutorials: List["Tutorial"]
|
||||
"""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.
|
||||
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
|
||||
|
||||
|
||||
@@ -21,10 +21,6 @@ if TYPE_CHECKING:
|
||||
from Utils import Version
|
||||
|
||||
|
||||
class ImproperlyConfiguredAutoPatchError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AutoPatchRegister(abc.ABCMeta):
|
||||
patch_types: 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)
|
||||
if "game" in dct:
|
||||
AutoPatchRegister.patch_types[dct["game"]] = new_class
|
||||
|
||||
if not callable(getattr(new_class, "patch", None)):
|
||||
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}"
|
||||
)
|
||||
|
||||
if not dct["patch_file_ending"]:
|
||||
raise Exception(f"Need an expected file ending for {name}")
|
||||
AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class
|
||||
return new_class
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import weakref
|
||||
from enum import Enum, auto
|
||||
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):
|
||||
@@ -204,18 +204,6 @@ def install_apworld(apworld_path: str = "") -> None:
|
||||
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] = [
|
||||
# Launcher
|
||||
Component('Launcher', 'Launcher', component_type=Type.HIDDEN),
|
||||
@@ -225,8 +213,6 @@ components: List[Component] = [
|
||||
description="Host a generated multiworld on your computer."),
|
||||
Component('Generate', 'Generate', cli=True,
|
||||
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"),
|
||||
description="Install an APWorld to play games not included with Archipelago by default."),
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient,
|
||||
@@ -244,10 +230,8 @@ components: List[Component] = [
|
||||
Component('Zillion Client', 'ZillionClient',
|
||||
file_identifier=SuffixIdentifier('.apzl')),
|
||||
|
||||
# MegaMan Battle Network 3
|
||||
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3')),
|
||||
|
||||
Component("Export Datapackage", func=export_datapackage, component_type=Type.TOOL),
|
||||
#MegaMan Battle Network 3
|
||||
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3'))
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from collections import Counter
|
||||
from typing import TYPE_CHECKING, Any
|
||||
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 (
|
||||
IS_PLACEHOLDER,
|
||||
|
||||
@@ -4,11 +4,11 @@ from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||
|
||||
from worlds._sc2common.bot import logger
|
||||
|
||||
from .proto import debug_pb2 as debug_pb
|
||||
from .proto import query_pb2 as query_pb
|
||||
from .proto import raw_pb2 as raw_pb
|
||||
from .proto import sc2api_pb2 as sc_pb
|
||||
from .proto import spatial_pb2 as spatial_pb
|
||||
from s2clientprotocol import debug_pb2 as debug_pb
|
||||
from s2clientprotocol import query_pb2 as query_pb
|
||||
from s2clientprotocol import raw_pb2 as raw_pb
|
||||
from s2clientprotocol import sc2api_pb2 as sc_pb
|
||||
from s2clientprotocol import spatial_pb2 as spatial_pb
|
||||
|
||||
from .data import ActionResult, ChatChannel, Race, Result, Status
|
||||
from .game_data import AbilityData, GameData
|
||||
|
||||
@@ -2,7 +2,7 @@ import platform
|
||||
from pathlib import Path
|
||||
|
||||
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 .protocol import Protocol
|
||||
|
||||
@@ -7,11 +7,11 @@ https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188f
|
||||
"""
|
||||
import enum
|
||||
|
||||
from .proto import common_pb2 as common_pb
|
||||
from .proto import data_pb2 as data_pb
|
||||
from .proto import error_pb2 as error_pb
|
||||
from .proto import raw_pb2 as raw_pb
|
||||
from .proto import sc2api_pb2 as sc_pb
|
||||
from s2clientprotocol import common_pb2 as common_pb
|
||||
from s2clientprotocol import data_pb2 as data_pb
|
||||
from s2clientprotocol import error_pb2 as error_pb
|
||||
from s2clientprotocol import raw_pb2 as raw_pb
|
||||
from s2clientprotocol import sc2api_pb2 as sc_pb
|
||||
|
||||
CreateGameError = enum.Enum("CreateGameError", sc_pb.ResponseCreateGame.Error.items())
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import mpyq
|
||||
import portpicker
|
||||
from aiohttp import ClientSession, ClientWebSocketResponse
|
||||
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 .client import Client
|
||||
|
||||
@@ -5,7 +5,7 @@ import math
|
||||
import random
|
||||
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:
|
||||
from .unit import Unit
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -4,7 +4,7 @@ from contextlib import suppress
|
||||
|
||||
from aiohttp import ClientWebSocketResponse
|
||||
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
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import traceback
|
||||
|
||||
from aiohttp import WSMsgType, web
|
||||
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 .data import Result, Status
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import datetime
|
||||
|
||||
from .proto import score_pb2 as score_pb
|
||||
from s2clientprotocol import score_pb2 as score_pb
|
||||
|
||||
from .position import Point2
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
s2clientprotocol>=5.0.11.90136.0
|
||||
mpyq>=0.2.5
|
||||
portpicker>=1.5.2
|
||||
aiohttp>=3.8.4
|
||||
loguru>=0.7.0
|
||||
protobuf==6.31.1
|
||||
protobuf==3.20.3
|
||||
|
||||
@@ -8,7 +8,7 @@ import bsdiff4
|
||||
|
||||
import Utils
|
||||
from settings import get_settings
|
||||
from worlds.Files import APPatch
|
||||
from worlds.Files import APPatch, AutoPatchRegister
|
||||
from .Locations import LocationData
|
||||
|
||||
ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"
|
||||
@@ -78,7 +78,7 @@ class BatNoTouchLocation:
|
||||
return ret_dict
|
||||
|
||||
|
||||
class AdventureDeltaPatch(APPatch):
|
||||
class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister):
|
||||
hash = ADVENTUREHASH
|
||||
game = "Adventure"
|
||||
patch_file_ending = ".apadvn"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import Utils
|
||||
import websockets
|
||||
import functools
|
||||
@@ -210,9 +208,6 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
|
||||
if not ctx.is_proxy_connected():
|
||||
break
|
||||
|
||||
if msg["cmd"] == "Bounce" and msg.get("tags") == ["DeathLink"] and "data" in msg:
|
||||
msg["data"]["time"] = time.time()
|
||||
|
||||
await ctx.send_msgs([msg])
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -243,7 +243,7 @@ guaranteed_first_acts = [
|
||||
"Time Rift - Mafia of Cooks",
|
||||
"Time Rift - Dead Bird Studio",
|
||||
"Time Rift - Sleepy Subcon",
|
||||
"Time Rift - Alpine Skyline",
|
||||
"Time Rift - Alpine Skyline"
|
||||
"Time Rift - Tour",
|
||||
"Time Rift - Rumbi Factory",
|
||||
]
|
||||
|
||||
@@ -239,7 +239,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
multiworld.worlds[item.player].collect(all_state_base, item)
|
||||
pre_fill_items = []
|
||||
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:
|
||||
try:
|
||||
pre_fill_items.remove(item)
|
||||
|
||||
@@ -2,7 +2,7 @@ from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from Options import OptionError
|
||||
from Fill import FillError
|
||||
|
||||
from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType
|
||||
from .Shops import TakeAny, total_shop_slots, set_up_shops, shop_table_by_location, ShopType
|
||||
@@ -410,16 +410,15 @@ def generate_itempool(world):
|
||||
pool_count = len(items)
|
||||
new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)]
|
||||
if world.options.shuffle_capacity_upgrades or world.options.bombless_start:
|
||||
progressive = world.options.progressive.want_progressives(world.random)
|
||||
progressive = world.options.progressive
|
||||
progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
|
||||
if world.options.shuffle_capacity_upgrades == "on_combined":
|
||||
new_items.append("Bomb Upgrade (50)")
|
||||
elif world.options.shuffle_capacity_upgrades == "on":
|
||||
new_items += ["Bomb Upgrade (+5)"] * 6
|
||||
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
||||
if world.options.bombless_start:
|
||||
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
||||
elif world.options.bombless_start:
|
||||
new_items.append("Bomb Upgrade (+10)")
|
||||
if world.options.shuffle_capacity_upgrades != "on_combined" and world.options.bombless_start:
|
||||
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
||||
|
||||
if world.options.shuffle_capacity_upgrades and not world.options.retro_bow:
|
||||
if world.options.shuffle_capacity_upgrades == "on_combined":
|
||||
@@ -467,9 +466,6 @@ def generate_itempool(world):
|
||||
items_were_cut = items_were_cut or cut_item(items, *reduce_item)
|
||||
elif len(reduce_item) == 4:
|
||||
items_were_cut = items_were_cut or condense_items(items, *reduce_item)
|
||||
if reduce_item[0] == "Piece of Heart" and world.logical_heart_pieces:
|
||||
world.logical_heart_pieces -= reduce_item[2]
|
||||
world.logical_heart_containers += reduce_item[3]
|
||||
elif len(reduce_item) == 1: # Bottles
|
||||
bottles = [item for item in items if item.name in item_name_groups["Bottles"]]
|
||||
if len(bottles) > 4:
|
||||
@@ -480,7 +476,7 @@ def generate_itempool(world):
|
||||
if items_were_cut:
|
||||
break
|
||||
else:
|
||||
raise OptionError(f"Failed to limit item pool size for player {player}")
|
||||
raise Exception(f"Failed to limit item pool size for player {player}")
|
||||
if len(items) < pool_count:
|
||||
items += removed_filler[len(items) - pool_count:]
|
||||
|
||||
|
||||
@@ -1197,8 +1197,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade
|
||||
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
|
||||
0x58, 0x01, 0x36 if local_world.options.retro_bow else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
|
||||
0x3E, local_world.logical_heart_containers, 0x47, 0xff, # boss heart -> green 20
|
||||
0x17, local_world.logical_heart_pieces, 0x47, 0xff, # piece of heart -> green 20
|
||||
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
|
||||
0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20
|
||||
0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel
|
||||
])
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from .StateHelpers import (can_extend_magic, can_kill_most_things,
|
||||
has_fire_source, has_hearts, has_melee_weapon,
|
||||
has_misery_mire_medallion, has_sword, has_turtle_rock_medallion,
|
||||
has_triforce_pieces, can_use_bombs, can_bomb_or_bonk,
|
||||
can_activate_crystal_switch, can_kill_standard_start)
|
||||
can_activate_crystal_switch)
|
||||
from .UnderworldGlitchRules import underworld_glitches_rules
|
||||
|
||||
|
||||
@@ -1093,23 +1093,22 @@ def standard_rules(world, player):
|
||||
if world.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal:
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
|
||||
and can_kill_standard_start(state, player, 2))
|
||||
and can_kill_most_things(state, player, 2))
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
|
||||
and can_kill_standard_start(state, player, 1))
|
||||
set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player),
|
||||
lambda state: can_kill_standard_start(state, player, 1))
|
||||
and can_kill_most_things(state, player, 1))
|
||||
|
||||
set_rule(world.get_location('Hyrule Castle - Big Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
|
||||
and state.has('Big Key (Hyrule Castle)', player)
|
||||
and (world.worlds[player].options.enemy_health in ("easy", "default")
|
||||
or can_kill_standard_start(state, player, 1)))
|
||||
or can_kill_most_things(state, player, 1)))
|
||||
|
||||
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3)
|
||||
and can_kill_standard_start(state, player, 1))
|
||||
and can_kill_most_things(state, player, 1))
|
||||
else:
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state.has('Big Key (Hyrule Castle)', player))
|
||||
|
||||
@@ -59,11 +59,10 @@ def has_hearts(state: CollectionState, player: int, count: int) -> int:
|
||||
|
||||
def heart_count(state: CollectionState, player: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
max_heart_pieces = state.multiworld.worlds[player].logical_heart_pieces
|
||||
max_heart_containers = state.multiworld.worlds[player].logical_heart_containers
|
||||
return min(state.count('Boss Heart Container', player), max_heart_containers) \
|
||||
diff = state.multiworld.worlds[player].difficulty_requirements
|
||||
return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
||||
+ state.count('Sanctuary Heart Container', player) \
|
||||
+ min(state.count('Piece of Heart', player), max_heart_pieces) // 4 \
|
||||
+ min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
||||
+ 3 # starting hearts
|
||||
|
||||
|
||||
@@ -140,16 +139,6 @@ def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5)
|
||||
and can_use_bombs(state, player, enemies * 4)))
|
||||
|
||||
|
||||
def can_kill_standard_start(state: CollectionState, player: int, enemies: int = 5) -> bool:
|
||||
# Enemizer does not randomize standard start enemies
|
||||
return (has_melee_weapon(state, player)
|
||||
or state.has('Cane of Somaria', player)
|
||||
or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
|
||||
or state.has_any(["Bow", "Progressive Bow"], player)
|
||||
or state.has('Fire Rod', player)
|
||||
or can_use_bombs(state, player, enemies)) # Escape assist is set
|
||||
|
||||
|
||||
def can_get_good_bee(state: CollectionState, player: int) -> bool:
|
||||
cave = state.multiworld.get_region('Good Bee Cave', player)
|
||||
return (
|
||||
|
||||
@@ -305,8 +305,6 @@ class ALTTPWorld(World):
|
||||
self.required_medallions = ["Ether", "Quake"]
|
||||
self.escape_assist = []
|
||||
self.shops = []
|
||||
self.logical_heart_containers = 10
|
||||
self.logical_heart_pieces = 24
|
||||
super(ALTTPWorld, self).__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -386,8 +384,6 @@ class ALTTPWorld(World):
|
||||
self.options.local_items.value |= self.dungeon_local_item_names
|
||||
|
||||
self.difficulty_requirements = difficulties[self.options.item_pool.current_key]
|
||||
self.logical_heart_pieces = self.difficulty_requirements.heart_piece_limit
|
||||
self.logical_heart_containers = self.difficulty_requirements.boss_heart_container_limit
|
||||
|
||||
# enforce pre-defined local items.
|
||||
if self.options.goal in ["local_triforce_hunt", "local_ganon_triforce_hunt"]:
|
||||
|
||||
@@ -88,8 +88,9 @@ You only have to do these steps once.
|
||||
1. Enter the RetroArch main menu screen.
|
||||
2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
|
||||
3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
|
||||
Network Command Port at 55355. \
|
||||

|
||||
Network Command Port at 55355.
|
||||
|
||||

|
||||
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
|
||||
Performance)".
|
||||
|
||||
|
||||
@@ -88,8 +88,9 @@ Sólo hay que seguir estos pasos una vez.
|
||||
1. Comienza en la pantalla del menú principal de RetroArch.
|
||||
2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON.
|
||||
3. Ve a Ajustes --> Red. Pon "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 el valor por defecto,
|
||||
el Puerto de comandos de red. \
|
||||

|
||||
el Puerto de comandos de red.
|
||||
|
||||

|
||||
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
|
||||
SFC (bsnes-mercury Performance)".
|
||||
|
||||
|
||||
@@ -89,8 +89,9 @@ Vous n'avez qu'à faire ces étapes qu'une fois.
|
||||
1. Entrez dans le menu principal RetroArch
|
||||
2. Allez dans Réglages --> Interface utilisateur. Mettez "Afficher les réglages avancés" sur ON.
|
||||
3. Allez dans Réglages --> Réseau. Mettez "Commandes Réseau" sur ON. (trouvé sous Request Device 16.) Laissez le
|
||||
Port des commandes réseau à 555355. \
|
||||

|
||||
Port des commandes réseau à 555355.
|
||||
|
||||

|
||||
4. Allez dans Menu Principal --> Mise à jour en ligne --> Téléchargement de cœurs. Descendez jusqu'a"Nintendo - SNES / SFC (bsnes-mercury Performance)" et
|
||||
sélectionnez le.
|
||||
|
||||
|
||||
BIN
worlds/alttp/docs/retroarch-network-commands-en.png
Normal file
BIN
worlds/alttp/docs/retroarch-network-commands-en.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
worlds/alttp/docs/retroarch-network-commands-fr.png
Normal file
BIN
worlds/alttp/docs/retroarch-network-commands-fr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1,46 +0,0 @@
|
||||
This apworld is meant as a learning tool for new apworld devs.
|
||||
It is a completely standalone resource, but there will be links to additional resources when appropriate.
|
||||
|
||||
#################
|
||||
# Prerequisites #
|
||||
#################
|
||||
|
||||
APQuest will only explain how to write the generation-side code for your game, not how to write a client or mod for it.
|
||||
For a more zoomed out view of how to add a game to Archipelago, you can read this document:
|
||||
https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md
|
||||
|
||||
APQuest assumes you already vaguely know what an apworld is.
|
||||
If you don't know, read this first:
|
||||
https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/apworld%20specification.md
|
||||
|
||||
To write an apworld, you need to be running Archipelago from source (Python) instead of using e.g. the .exe build.
|
||||
Here's an explanation for how to do that.
|
||||
https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/running%20from%20source.md
|
||||
|
||||
#######################
|
||||
# How to read APQuest #
|
||||
#######################
|
||||
|
||||
You'll want to start with __init__.py, then move to world.py.
|
||||
If you also want to learn how to write unit tests, go to test/__init__.py.
|
||||
|
||||
You can ignore the game/ folder, it contains the actual game code, graphics and music.
|
||||
|
||||
The client/ folder is NOT meant for teaching.
|
||||
While the client was written to the best of its author's ability, it does not meet the same standard as the world code.
|
||||
The client code is also lacking the explanatory comments.
|
||||
Copy from it at your own risk.
|
||||
|
||||
###################
|
||||
# Further reading #
|
||||
###################
|
||||
|
||||
APQuest is a very simple game, so not every edge case will be covered.
|
||||
The world API document goes a lot more in-depth on certain topics:
|
||||
https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md
|
||||
|
||||
There is also the "APWorld dev FAQ" document with common emergent problems:
|
||||
https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/apworld_dev_faq.md
|
||||
|
||||
In general, but especially if you want your apworld to be verified by core, you should follow our style guide:
|
||||
https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md
|
||||
@@ -1,12 +0,0 @@
|
||||
# The first thing you should make for your world is an archipelago.json manifest file.
|
||||
# You can reference APQuest's, but you should change the "game" field (obviously),
|
||||
# and you should also change the "minimum_ap_version" - probably to the current value of Utils.__version__.
|
||||
|
||||
# Apart from the regular apworld code that allows generating multiworld seeds with your game,
|
||||
# your apworld might have other "components" that should be launchable from the Archipelago Launcher.
|
||||
# You can ignore this for now. If you are specifically interested in components, you can read components.py.
|
||||
from . import components as components
|
||||
|
||||
# The main thing we do in our __init__.py is importing our world class from our world.py to initialize it.
|
||||
# Obviously, this world class needs to exist first. For this, read world.py.
|
||||
from .world import APQuestWorld as APQuestWorld
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"game": "APQuest",
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "1.0.0",
|
||||
"authors": ["NewSoupVi"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# !!! IMPORTANT !!!
|
||||
# The client implementation is *not* meant for teaching.
|
||||
# Obviously, it is written to the best of its author's abilities,
|
||||
# but it is not to the same standard as the rest of the apworld.
|
||||
# Copy things from here at your own risk.
|
||||
@@ -1,56 +0,0 @@
|
||||
<ConfettiView>:
|
||||
size_hint: None, None
|
||||
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||
spacing: 0
|
||||
padding: 0
|
||||
|
||||
<APQuestGrid>:
|
||||
cols: 12
|
||||
rows: 11
|
||||
spacing: 0
|
||||
padding: 0
|
||||
size_hint: None, None
|
||||
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||
|
||||
<APQuestGameView>:
|
||||
RelativeLayout:
|
||||
id: game_container
|
||||
|
||||
<APQuestControlsView>:
|
||||
Label:
|
||||
markup: True
|
||||
font_size: "20sp"
|
||||
valign: "middle"
|
||||
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||
text:
|
||||
"""[b]Controls:[/b]
|
||||
|
||||
WASD or Arrow Keys to move
|
||||
Space to attack or interact
|
||||
C to fire available Confetti Cannons
|
||||
Number Keys + Backspace for Math Trap\n
|
||||
|
||||
Rebinding controls might be added in the future :)"""
|
||||
|
||||
<VolumeSliderView>:
|
||||
orientation: "horizontal"
|
||||
size_hint: 1, None
|
||||
padding: 0
|
||||
height: 50
|
||||
|
||||
Label:
|
||||
size_hint: None, 1
|
||||
text: "Volume:"
|
||||
|
||||
Slider:
|
||||
id: volume_slider
|
||||
size_hint: 1, 1
|
||||
min: 0
|
||||
max: 100
|
||||
step: 1
|
||||
value: 50
|
||||
orientation: "horizontal"
|
||||
|
||||
Label:
|
||||
size_hint: None, 1
|
||||
text: str(int(volume_slider.value))
|
||||
@@ -1,290 +0,0 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from CommonClient import CommonContext, gui_enabled, logger, server_loop
|
||||
from NetUtils import ClientStatus
|
||||
|
||||
from ..game.events import ConfettiFired, LocationClearedEvent, MathProblemSolved, MathProblemStarted, VictoryEvent
|
||||
from ..game.game import Game
|
||||
from ..game.inputs import Input
|
||||
from ..game.items import Item
|
||||
from ..game.locations import Location
|
||||
from .game_manager import APQuestManager
|
||||
from .graphics import PlayerSprite
|
||||
from .item_quality import get_quality_for_network_item
|
||||
from .sounds import (
|
||||
CONFETTI_CANNON,
|
||||
ITEM_JINGLES,
|
||||
MATH_PROBLEM_SOLVED_JINGLE,
|
||||
MATH_PROBLEM_STARTED_JINGLE,
|
||||
VICTORY_JINGLE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import kvui
|
||||
|
||||
|
||||
# !!! IMPORTANT !!!
|
||||
# The client implementation is *not* meant for teaching.
|
||||
# Obviously, it is written to the best of its author's abilities,
|
||||
# but it is not to the same standard as the rest of the apworld.
|
||||
# Copy things from here at your own risk.
|
||||
|
||||
|
||||
class ConnectionStatus(Enum):
|
||||
NOT_CONNECTED = 0
|
||||
SCOUTS_NOT_SENT = 1
|
||||
SCOUTS_SENT = 2
|
||||
GAME_RUNNING = 3
|
||||
|
||||
|
||||
class APQuestContext(CommonContext):
|
||||
game = "APQuest"
|
||||
items_handling = 0b111 # full remote
|
||||
|
||||
client_loop: asyncio.Task[None]
|
||||
|
||||
last_connected_slot: int | None = None
|
||||
|
||||
slot_data: dict[str, Any]
|
||||
|
||||
ap_quest_game: Game | None = None
|
||||
hard_mode: bool = False
|
||||
hammer: bool = False
|
||||
extra_starting_chest: bool = False
|
||||
player_sprite: PlayerSprite = PlayerSprite.HUMAN
|
||||
|
||||
connection_status: ConnectionStatus = ConnectionStatus.NOT_CONNECTED
|
||||
|
||||
highest_processed_item_index: int = 0
|
||||
queued_locations: list[int]
|
||||
|
||||
delay_intro_song: bool
|
||||
|
||||
ui: APQuestManager
|
||||
|
||||
def __init__(
|
||||
self, server_address: str | None = None, password: str | None = None, delay_intro_song: bool = False
|
||||
) -> None:
|
||||
super().__init__(server_address, password)
|
||||
|
||||
self.queued_locations = []
|
||||
self.slot_data = {}
|
||||
self.delay_intro_song = delay_intro_song
|
||||
|
||||
async def server_auth(self, password_requested: bool = False) -> None:
|
||||
if password_requested and not self.password:
|
||||
self.ui.allow_intro_song()
|
||||
await super().server_auth(password_requested)
|
||||
await self.get_username()
|
||||
await self.send_connect(game=self.game)
|
||||
|
||||
def handle_connection_loss(self, msg: str) -> None:
|
||||
self.ui.allow_intro_song()
|
||||
super().handle_connection_loss(msg)
|
||||
|
||||
async def connect(self, address: str | None = None) -> None:
|
||||
self.ui.switch_to_regular_tab()
|
||||
await super().connect(address)
|
||||
|
||||
async def apquest_loop(self) -> None:
|
||||
while not self.exit_event.is_set():
|
||||
if self.connection_status != ConnectionStatus.GAME_RUNNING:
|
||||
if self.connection_status == ConnectionStatus.SCOUTS_NOT_SENT:
|
||||
await self.send_msgs([{"cmd": "LocationScouts", "locations": self.server_locations}])
|
||||
self.connection_status = ConnectionStatus.SCOUTS_SENT
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
|
||||
if not self.ap_quest_game or not self.ap_quest_game.gameboard or not self.ap_quest_game.gameboard.ready:
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
|
||||
try:
|
||||
while self.queued_locations:
|
||||
location = self.queued_locations.pop(0)
|
||||
self.location_checked_side_effects(location)
|
||||
self.locations_checked.add(location)
|
||||
await self.check_locations({location})
|
||||
|
||||
rerender = False
|
||||
|
||||
new_items = self.items_received[self.highest_processed_item_index :]
|
||||
for item in new_items:
|
||||
self.highest_processed_item_index += 1
|
||||
self.ap_quest_game.receive_item(item.item, item.location, item.player)
|
||||
rerender = True
|
||||
|
||||
for new_remotely_cleared_location in self.checked_locations - self.locations_checked:
|
||||
self.ap_quest_game.force_clear_location(new_remotely_cleared_location)
|
||||
rerender = True
|
||||
|
||||
if rerender:
|
||||
self.render()
|
||||
|
||||
if self.ap_quest_game.player.has_won and not self.finished_game:
|
||||
await self.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
self.finished_game = True
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
def on_package(self, cmd: str, args: dict[str, Any]) -> None:
|
||||
if cmd == "ConnectionRefused":
|
||||
self.ui.allow_intro_song()
|
||||
|
||||
if cmd == "Connected":
|
||||
if self.connection_status == ConnectionStatus.GAME_RUNNING:
|
||||
# In a connection loss -> auto reconnect scenario, we can seamlessly keep going
|
||||
return
|
||||
|
||||
self.last_connected_slot = self.slot
|
||||
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED # for safety, it will get set again later
|
||||
|
||||
self.slot_data = args["slot_data"]
|
||||
self.hard_mode = self.slot_data["hard_mode"]
|
||||
self.hammer = self.slot_data["hammer"]
|
||||
self.extra_starting_chest = self.slot_data["extra_starting_chest"]
|
||||
try:
|
||||
self.player_sprite = PlayerSprite(self.slot_data["player_sprite"])
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
self.player_sprite = PlayerSprite.UNKNOWN
|
||||
|
||||
self.ap_quest_game = Game(self.hard_mode, self.hammer, self.extra_starting_chest)
|
||||
self.highest_processed_item_index = 0
|
||||
self.render()
|
||||
|
||||
self.connection_status = ConnectionStatus.SCOUTS_NOT_SENT
|
||||
if cmd == "LocationInfo":
|
||||
remote_item_graphic_overrides = {
|
||||
Location(location): Item(network_item.item)
|
||||
for location, network_item in self.locations_info.items()
|
||||
if self.slot_info[network_item.player].game == self.game
|
||||
}
|
||||
|
||||
assert self.ap_quest_game is not None
|
||||
self.ap_quest_game.gameboard.fill_remote_location_content(remote_item_graphic_overrides)
|
||||
self.render()
|
||||
self.ui.game_view.bind_keyboard()
|
||||
|
||||
self.connection_status = ConnectionStatus.GAME_RUNNING
|
||||
self.ui.game_started()
|
||||
|
||||
async def disconnect(self, *args: Any, **kwargs: Any) -> None:
|
||||
self.finished_game = False
|
||||
self.locations_checked = set()
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
await super().disconnect(*args, **kwargs)
|
||||
|
||||
def render(self) -> None:
|
||||
if self.ap_quest_game is None:
|
||||
raise RuntimeError("Tried to render before self.ap_quest_game was initialized.")
|
||||
|
||||
self.ui.render(self.ap_quest_game, self.player_sprite)
|
||||
self.handle_game_events()
|
||||
|
||||
def location_checked_side_effects(self, location: int) -> None:
|
||||
network_item = self.locations_info[location]
|
||||
|
||||
if network_item.player == self.slot and network_item.item == Item.MATH_TRAP.value:
|
||||
# In case of a local math trap, we only play the math trap trigger jingle
|
||||
return
|
||||
|
||||
item_quality = get_quality_for_network_item(network_item)
|
||||
self.play_jingle(ITEM_JINGLES[item_quality])
|
||||
|
||||
def play_jingle(self, audio_filename: str) -> None:
|
||||
self.ui.play_jingle(audio_filename)
|
||||
|
||||
def handle_game_events(self) -> None:
|
||||
if self.ap_quest_game is None:
|
||||
return
|
||||
|
||||
while self.ap_quest_game.queued_events:
|
||||
event = self.ap_quest_game.queued_events.pop(0)
|
||||
|
||||
if isinstance(event, LocationClearedEvent):
|
||||
self.queued_locations.append(event.location_id)
|
||||
continue
|
||||
|
||||
if isinstance(event, VictoryEvent):
|
||||
self.play_jingle(VICTORY_JINGLE)
|
||||
continue
|
||||
|
||||
if isinstance(event, ConfettiFired):
|
||||
gameboard_x, gameboard_y = self.ap_quest_game.gameboard.size
|
||||
gameboard_x += 1 # vertical item column
|
||||
x = (event.x + 0.5) / gameboard_x
|
||||
y = 1 - (event.y + 0.5) / gameboard_y # Kivy's y is bottom to top (ew)
|
||||
|
||||
self.ui.play_jingle(CONFETTI_CANNON)
|
||||
self.ui.add_confetti((x, y), (self.slot_data["confetti_explosiveness"] + 1) * 5)
|
||||
continue
|
||||
|
||||
if isinstance(event, MathProblemStarted):
|
||||
self.play_jingle(MATH_PROBLEM_STARTED_JINGLE)
|
||||
continue
|
||||
|
||||
if isinstance(event, MathProblemSolved):
|
||||
self.play_jingle(MATH_PROBLEM_SOLVED_JINGLE)
|
||||
continue
|
||||
|
||||
def input_and_rerender(self, input_key: Input) -> None:
|
||||
if self.ap_quest_game is None:
|
||||
return
|
||||
if not self.ap_quest_game.gameboard.ready:
|
||||
return
|
||||
self.ap_quest_game.input(input_key)
|
||||
self.render()
|
||||
|
||||
def make_gui(self) -> "type[kvui.GameManager]":
|
||||
self.load_kv()
|
||||
return APQuestManager
|
||||
|
||||
def load_kv(self) -> None:
|
||||
import pkgutil
|
||||
|
||||
from kivy.lang import Builder
|
||||
|
||||
data = pkgutil.get_data(__name__, "ap_quest_client.kv")
|
||||
if data is None:
|
||||
raise RuntimeError("ap_quest_client.kv could not be loaded.")
|
||||
|
||||
Builder.load_string(data.decode())
|
||||
|
||||
|
||||
async def main(args: Namespace) -> None:
|
||||
if not gui_enabled:
|
||||
raise RuntimeError("APQuest cannot be played without gui.")
|
||||
|
||||
# Assume we shouldn't play the intro song in the auto-connect scenario, because the game will instantly start.
|
||||
delay_intro_song = args.connect and args.name
|
||||
|
||||
ctx = APQuestContext(args.connect, args.password, delay_intro_song=delay_intro_song)
|
||||
ctx.auth = args.name
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
ctx.client_loop = asyncio.create_task(ctx.apquest_loop(), name="Client Loop")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
await ctx.shutdown()
|
||||
|
||||
|
||||
def launch(*args: str) -> None:
|
||||
from .launch import launch_ap_quest_client
|
||||
|
||||
launch_ap_quest_client(*args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
launch(*sys.argv[1:])
|
||||
@@ -1,256 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from math import sqrt
|
||||
from random import choice, random
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from kivy.core.window import Keyboard, Window
|
||||
from kivy.graphics import Color, Triangle
|
||||
from kivy.graphics.instructions import Canvas
|
||||
from kivy.input import MotionEvent
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivymd.uix.recycleview import MDRecycleView
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
from ..game.inputs import Input
|
||||
|
||||
|
||||
INPUT_MAP = {
|
||||
"up": Input.UP,
|
||||
"w": Input.UP,
|
||||
"down": Input.DOWN,
|
||||
"s": Input.DOWN,
|
||||
"right": Input.RIGHT,
|
||||
"d": Input.RIGHT,
|
||||
"left": Input.LEFT,
|
||||
"a": Input.LEFT,
|
||||
"spacebar": Input.ACTION,
|
||||
"c": Input.CONFETTI,
|
||||
"0": Input.ZERO,
|
||||
"1": Input.ONE,
|
||||
"2": Input.TWO,
|
||||
"3": Input.THREE,
|
||||
"4": Input.FOUR,
|
||||
"5": Input.FIVE,
|
||||
"6": Input.SIX,
|
||||
"7": Input.SEVEN,
|
||||
"8": Input.EIGHT,
|
||||
"9": Input.NINE,
|
||||
"backspace": Input.BACKSPACE,
|
||||
}
|
||||
|
||||
|
||||
class APQuestGameView(MDRecycleView):
|
||||
_keyboard: Keyboard | None = None
|
||||
input_function: Callable[[Input], None]
|
||||
|
||||
def __init__(self, input_function: Callable[[Input], None], **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.input_function = input_function
|
||||
self.bind_keyboard()
|
||||
|
||||
def on_touch_down(self, touch: MotionEvent) -> None:
|
||||
self.bind_keyboard()
|
||||
|
||||
def bind_keyboard(self) -> None:
|
||||
if self._keyboard is not None:
|
||||
return
|
||||
self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
|
||||
self._keyboard.bind(on_key_down=self._on_keyboard_down)
|
||||
|
||||
def _keyboard_closed(self) -> None:
|
||||
if self._keyboard is None:
|
||||
return
|
||||
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
|
||||
self._keyboard = None
|
||||
|
||||
def _on_keyboard_down(self, _: Any, keycode: tuple[int, str], _1: Any, _2: Any) -> bool:
|
||||
if keycode[1] in INPUT_MAP:
|
||||
self.input_function(INPUT_MAP[keycode[1]])
|
||||
return True
|
||||
|
||||
|
||||
class APQuestGrid(GridLayout):
|
||||
def check_resize(self, _: int, _1: int) -> None:
|
||||
parent_width, parent_height = self.parent.size
|
||||
|
||||
self_width_according_to_parent_height = parent_height * 12 / 11
|
||||
self_height_according_to_parent_width = parent_height * 11 / 12
|
||||
|
||||
if self_width_according_to_parent_height > parent_width:
|
||||
self.size = parent_width, self_height_according_to_parent_width
|
||||
else:
|
||||
self.size = self_width_according_to_parent_height, parent_height
|
||||
|
||||
|
||||
CONFETTI_COLORS = [
|
||||
(220 / 255, 0, 212 / 255), # PINK
|
||||
(0, 0, 252 / 255), # BLUE
|
||||
(252 / 255, 220 / 255, 0), # YELLOW
|
||||
(0, 184 / 255, 0), # GREEN
|
||||
(252 / 255, 56 / 255, 0), # ORANGE
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Confetti:
|
||||
x_pos: float
|
||||
y_pos: float
|
||||
x_speed: float
|
||||
y_speed: float
|
||||
color: tuple[float, float, float]
|
||||
life: float = 3
|
||||
|
||||
triangle1: Triangle | None = None
|
||||
triangle2: Triangle | None = None
|
||||
color_instruction: Color | None = None
|
||||
|
||||
def update_speed(self, dt: float) -> None:
|
||||
if self.x_speed > 0:
|
||||
self.x_speed -= 2.7 * dt
|
||||
if self.x_speed < 0:
|
||||
self.x_speed = 0
|
||||
else:
|
||||
self.x_speed += 2.7 * dt
|
||||
if self.x_speed > 0:
|
||||
self.x_speed = 0
|
||||
|
||||
if self.y_speed > -0.03:
|
||||
self.y_speed -= 2.7 * dt
|
||||
if self.y_speed < -0.03:
|
||||
self.y_speed = -0.03
|
||||
else:
|
||||
self.y_speed += 2.7 * dt
|
||||
if self.y_speed > -0.03:
|
||||
self.y_speed = -0.03
|
||||
|
||||
def move(self, dt: float) -> None:
|
||||
self.update_speed(dt)
|
||||
|
||||
if self.y_pos > 1:
|
||||
self.y_pos = 1
|
||||
self.y_speed = 0
|
||||
if self.x_pos < 0.01:
|
||||
self.x_pos = 0.01
|
||||
self.x_speed = 0
|
||||
if self.x_pos > 0.99:
|
||||
self.x_pos = 0.99
|
||||
self.x_speed = 0
|
||||
|
||||
self.x_pos += self.x_speed * dt
|
||||
self.y_pos += self.y_speed * dt
|
||||
|
||||
def render(self, offset_x: float, offset_y: float, max_x: int, max_y: int) -> None:
|
||||
if self.x_speed == 0 and self.y_speed == 0:
|
||||
x_normalized, y_normalized = 0.0, 1.0
|
||||
else:
|
||||
speed_magnitude = sqrt(self.x_speed**2 + self.y_speed**2)
|
||||
x_normalized, y_normalized = self.x_speed / speed_magnitude, self.y_speed / speed_magnitude
|
||||
|
||||
half_top_to_bottom = 0.006
|
||||
half_left_to_right = 0.018
|
||||
|
||||
upwards_delta_x = x_normalized * half_top_to_bottom
|
||||
upwards_delta_y = y_normalized * half_top_to_bottom
|
||||
sideways_delta_x = y_normalized * half_left_to_right
|
||||
sideways_delta_y = x_normalized * half_left_to_right
|
||||
|
||||
top_left_x, top_left_y = upwards_delta_x - sideways_delta_x, upwards_delta_y + sideways_delta_y
|
||||
bottom_left_x, bottom_left_y = -upwards_delta_x - sideways_delta_x, -upwards_delta_y + sideways_delta_y
|
||||
top_right_x, top_right_y = -bottom_left_x, -bottom_left_y
|
||||
bottom_right_x, bottom_right_y = -top_left_x, -top_left_y
|
||||
|
||||
top_left_x, top_left_y = top_left_x + self.x_pos, top_left_y + self.y_pos
|
||||
bottom_left_x, bottom_left_y = bottom_left_x + self.x_pos, bottom_left_y + self.y_pos
|
||||
top_right_x, top_right_y = top_right_x + self.x_pos, top_right_y + self.y_pos
|
||||
bottom_right_x, bottom_right_y = bottom_right_x + self.x_pos, bottom_right_y + self.y_pos
|
||||
|
||||
top_left_x, top_left_y = top_left_x * max_x + offset_x, top_left_y * max_y + offset_y
|
||||
bottom_left_x, bottom_left_y = bottom_left_x * max_x + offset_x, bottom_left_y * max_y + offset_y
|
||||
top_right_x, top_right_y = top_right_x * max_x + offset_x, top_right_y * max_y + offset_y
|
||||
bottom_right_x, bottom_right_y = bottom_right_x * max_x + offset_x, bottom_right_y * max_y + offset_y
|
||||
|
||||
points1 = (top_left_x, top_left_y, top_right_x, top_right_y, bottom_left_x, bottom_left_y)
|
||||
points2 = (bottom_right_x, bottom_right_y, top_right_x, top_right_y, bottom_left_x, bottom_left_y)
|
||||
|
||||
if self.color_instruction is None:
|
||||
self.color_instruction = Color(*self.color)
|
||||
|
||||
if self.triangle1 is None:
|
||||
self.triangle1 = Triangle(points=points1)
|
||||
else:
|
||||
self.triangle1.points = points1
|
||||
|
||||
if self.triangle2 is None:
|
||||
self.triangle2 = Triangle(points=points2)
|
||||
else:
|
||||
self.triangle2.points = points2
|
||||
|
||||
def reduce_life(self, dt: float, canvas: Canvas) -> bool:
|
||||
self.life -= dt
|
||||
|
||||
if self.life <= 0:
|
||||
if self.color_instruction is not None:
|
||||
canvas.remove(self.color_instruction)
|
||||
if self.triangle1 is not None:
|
||||
canvas.remove(self.triangle1)
|
||||
if self.triangle2 is not None:
|
||||
canvas.remove(self.triangle2)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ConfettiView(MDRecycleView):
|
||||
confetti: list[Confetti]
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.confetti = []
|
||||
|
||||
def check_resize(self, _: int, _1: int) -> None:
|
||||
parent_width, parent_height = self.parent.size
|
||||
|
||||
self_width_according_to_parent_height = parent_height * 12 / 11
|
||||
self_height_according_to_parent_width = parent_height * 11 / 12
|
||||
|
||||
if self_width_according_to_parent_height > parent_width:
|
||||
self.size = parent_width, self_height_according_to_parent_width
|
||||
else:
|
||||
self.size = self_width_according_to_parent_height, parent_height
|
||||
|
||||
def redraw_confetti(self, dt: float) -> None:
|
||||
try:
|
||||
with self.canvas:
|
||||
for confetti in self.confetti:
|
||||
confetti.move(dt)
|
||||
|
||||
self.confetti = [confetti for confetti in self.confetti if confetti.reduce_life(dt, self.canvas)]
|
||||
|
||||
for confetti in self.confetti:
|
||||
confetti.render(self.pos[0], self.pos[1], self.size[0], self.size[1])
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
def add_confetti(self, initial_position: tuple[float, float], amount: int) -> None:
|
||||
for i in range(amount):
|
||||
self.confetti.append(
|
||||
Confetti(
|
||||
initial_position[0],
|
||||
initial_position[1],
|
||||
random() * 3.2 - 1.6 - (initial_position[0] - 0.5) * 1.2,
|
||||
random() * 3.2 - 1.3 - (initial_position[1] - 0.5) * 1.2,
|
||||
choice(CONFETTI_COLORS),
|
||||
3 + i * 0.05,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VolumeSliderView(BoxLayout):
|
||||
pass
|
||||
|
||||
|
||||
class APQuestControlsView(BoxLayout):
|
||||
pass
|
||||
@@ -1,200 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# isort: off
|
||||
from kvui import GameManager, MDNavigationItemBase
|
||||
|
||||
# isort: on
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.image import Image
|
||||
from kivy.uix.layout import Layout
|
||||
from kivymd.uix.recycleview import MDRecycleView
|
||||
|
||||
from ..game.game import Game
|
||||
from .custom_views import APQuestControlsView, APQuestGameView, APQuestGrid, ConfettiView, VolumeSliderView
|
||||
from .graphics import PlayerSprite, get_texture
|
||||
from .sounds import SoundManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .ap_quest_client import APQuestContext
|
||||
|
||||
|
||||
class APQuestManager(GameManager):
|
||||
base_title = "APQuest for AP version"
|
||||
ctx: APQuestContext
|
||||
|
||||
lower_game_grid: GridLayout
|
||||
upper_game_grid: GridLayout
|
||||
|
||||
game_view: MDRecycleView
|
||||
game_view_tab: MDNavigationItemBase
|
||||
|
||||
sound_manager: SoundManager
|
||||
|
||||
bottom_image_grid: list[list[Image]]
|
||||
top_image_grid: list[list[Image]]
|
||||
confetti_view: ConfettiView
|
||||
|
||||
bottom_grid_is_grass: bool
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.sound_manager = SoundManager()
|
||||
self.sound_manager.allow_intro_to_play = not self.ctx.delay_intro_song
|
||||
self.top_image_grid = []
|
||||
self.bottom_image_grid = []
|
||||
self.bottom_grid_is_grass = False
|
||||
|
||||
def allow_intro_song(self) -> None:
|
||||
self.sound_manager.allow_intro_to_play = True
|
||||
|
||||
def add_confetti(self, position: tuple[float, float], amount: int) -> None:
|
||||
self.confetti_view.add_confetti(position, amount)
|
||||
|
||||
def play_jingle(self, audio_filename: str) -> None:
|
||||
self.sound_manager.play_jingle(audio_filename)
|
||||
|
||||
def switch_to_tab(self, desired_tab: MDNavigationItemBase) -> None:
|
||||
if self.screens.current_tab == desired_tab:
|
||||
return
|
||||
self.screens.current_tab.active = False
|
||||
self.screens.switch_screens(desired_tab)
|
||||
desired_tab.active = True
|
||||
|
||||
def switch_to_game_tab(self) -> None:
|
||||
self.switch_to_tab(self.game_view_tab)
|
||||
|
||||
def switch_to_regular_tab(self) -> None:
|
||||
self.switch_to_tab(self.tabs.children[-1])
|
||||
|
||||
def game_started(self) -> None:
|
||||
self.switch_to_game_tab()
|
||||
self.sound_manager.game_started = True
|
||||
|
||||
def render(self, game: Game, player_sprite: PlayerSprite) -> None:
|
||||
self.setup_game_grid_if_not_setup(game.gameboard.size)
|
||||
|
||||
# This calls game.render(), which needs to happen to update the state of math traps
|
||||
self.render_gameboard(game, player_sprite)
|
||||
# Only now can we check whether a math problem is active
|
||||
self.render_background_game_grid(game.gameboard.size, game.active_math_problem is None)
|
||||
self.sound_manager.math_trap_active = game.active_math_problem is not None
|
||||
|
||||
self.render_item_column(game)
|
||||
|
||||
def render_gameboard(self, game: Game, player_sprite: PlayerSprite) -> None:
|
||||
rendered_gameboard = game.render()
|
||||
|
||||
for gameboard_row, image_row in zip(rendered_gameboard, self.top_image_grid, strict=False):
|
||||
for graphic, image in zip(gameboard_row, image_row[:11], strict=False):
|
||||
texture = get_texture(graphic, player_sprite)
|
||||
|
||||
if texture is None:
|
||||
image.opacity = 0
|
||||
image.texture = None
|
||||
continue
|
||||
|
||||
image.texture = texture
|
||||
image.opacity = 1
|
||||
|
||||
def render_item_column(self, game: Game) -> None:
|
||||
rendered_item_column = game.render_health_and_inventory(vertical=True)
|
||||
for item_graphic, image_row in zip(rendered_item_column, self.top_image_grid, strict=False):
|
||||
image = image_row[-1]
|
||||
|
||||
texture = get_texture(item_graphic)
|
||||
if texture is None:
|
||||
image.opacity = 0
|
||||
image.texture = None
|
||||
continue
|
||||
|
||||
image.texture = texture
|
||||
image.opacity = 1
|
||||
|
||||
def render_background_game_grid(self, size: tuple[int, int], grass: bool) -> None:
|
||||
if grass == self.bottom_grid_is_grass:
|
||||
return
|
||||
|
||||
for row in range(size[1]):
|
||||
for column in range(size[0]):
|
||||
image = self.bottom_image_grid[row][column]
|
||||
|
||||
if not grass:
|
||||
image.color = (0.3, 0.3, 0.3)
|
||||
image.texture = None
|
||||
continue
|
||||
|
||||
boss_room = (row in (0, 1, 2) and (size[1] - column) in (1, 2, 3)) or (row, column) == (3, size[1] - 2)
|
||||
if boss_room:
|
||||
image.color = (0.45, 0.35, 0.1)
|
||||
image.texture = None
|
||||
continue
|
||||
image.texture = get_texture("Grass")
|
||||
image.color = (1.0, 1.0, 1.0)
|
||||
|
||||
self.bottom_grid_is_grass = grass
|
||||
|
||||
def setup_game_grid_if_not_setup(self, size: tuple[int, int]) -> None:
|
||||
if self.upper_game_grid.children:
|
||||
return
|
||||
|
||||
self.top_image_grid = []
|
||||
self.bottom_image_grid = []
|
||||
|
||||
for _row in range(size[1]):
|
||||
self.top_image_grid.append([])
|
||||
self.bottom_image_grid.append([])
|
||||
|
||||
for _column in range(size[0]):
|
||||
bottom_image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3))
|
||||
self.lower_game_grid.add_widget(bottom_image)
|
||||
self.bottom_image_grid[-1].append(bottom_image)
|
||||
|
||||
top_image = Image(fit_mode="fill")
|
||||
self.upper_game_grid.add_widget(top_image)
|
||||
self.top_image_grid[-1].append(top_image)
|
||||
|
||||
# Right side: Inventory
|
||||
image = Image(fit_mode="fill", color=(0.3, 0.3, 0.3))
|
||||
self.lower_game_grid.add_widget(image)
|
||||
|
||||
image2 = Image(fit_mode="fill", opacity=0)
|
||||
self.upper_game_grid.add_widget(image2)
|
||||
|
||||
self.top_image_grid[-1].append(image2)
|
||||
|
||||
def build(self) -> Layout:
|
||||
container = super().build()
|
||||
|
||||
self.game_view = APQuestGameView(self.ctx.input_and_rerender)
|
||||
|
||||
self.game_view_tab = self.add_client_tab("APQuest", self.game_view)
|
||||
|
||||
controls = APQuestControlsView()
|
||||
|
||||
self.add_client_tab("Controls", controls)
|
||||
|
||||
game_container = self.game_view.ids["game_container"]
|
||||
self.lower_game_grid = APQuestGrid()
|
||||
self.upper_game_grid = APQuestGrid()
|
||||
self.confetti_view = ConfettiView()
|
||||
game_container.add_widget(self.lower_game_grid)
|
||||
game_container.add_widget(self.upper_game_grid)
|
||||
game_container.add_widget(self.confetti_view)
|
||||
|
||||
game_container.bind(size=self.lower_game_grid.check_resize)
|
||||
game_container.bind(size=self.upper_game_grid.check_resize)
|
||||
game_container.bind(size=self.confetti_view.check_resize)
|
||||
|
||||
volume_slider_container = VolumeSliderView()
|
||||
volume_slider = volume_slider_container.ids["volume_slider"]
|
||||
volume_slider.value = self.sound_manager.volume_percentage
|
||||
volume_slider.bind(value=lambda _, new_volume: self.sound_manager.set_volume_percentage(new_volume))
|
||||
|
||||
self.grid.add_widget(volume_slider_container, index=3)
|
||||
|
||||
Clock.schedule_interval(lambda dt: self.confetti_view.redraw_confetti(dt), 1 / 60)
|
||||
|
||||
return container
|
||||
@@ -1,181 +0,0 @@
|
||||
import pkgutil
|
||||
from collections.abc import Buffer
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
from typing import Literal, NamedTuple, cast
|
||||
|
||||
from bokeh.protocol import Protocol
|
||||
from kivy.uix.image import CoreImage
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
from .. import game
|
||||
from ..game.graphics import Graphic
|
||||
|
||||
|
||||
# The import "from kivy.graphics.texture import Texture" does not work correctly.
|
||||
# We never need the class directly, so we need to use a protocol.
|
||||
class Texture(Protocol):
|
||||
mag_filter: Literal["nearest"]
|
||||
|
||||
def get_region(self, x: int, y: int, w: int, h: int) -> "Texture": ...
|
||||
|
||||
|
||||
class RelatedTexture(NamedTuple):
|
||||
base_texture_file: str
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
IMAGE_GRAPHICS: dict[Graphic, str | RelatedTexture] = {
|
||||
Graphic.WALL: RelatedTexture("inanimates.png", 16, 32, 16, 16),
|
||||
Graphic.BREAKABLE_BLOCK: RelatedTexture("inanimates.png", 32, 32, 16, 16),
|
||||
Graphic.CHEST: RelatedTexture("inanimates.png", 0, 16, 16, 16),
|
||||
Graphic.BUSH: RelatedTexture("inanimates.png", 16, 16, 16, 16),
|
||||
Graphic.KEY_DOOR: RelatedTexture("inanimates.png", 32, 16, 16, 16),
|
||||
Graphic.BUTTON_NOT_ACTIVATED: RelatedTexture("inanimates.png", 0, 0, 16, 16),
|
||||
Graphic.BUTTON_ACTIVATED: RelatedTexture("inanimates.png", 16, 0, 16, 16),
|
||||
Graphic.BUTTON_DOOR: RelatedTexture("inanimates.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.NORMAL_ENEMY_1_HEALTH: RelatedTexture("normal_enemy.png", 0, 0, 16, 16),
|
||||
Graphic.NORMAL_ENEMY_2_HEALTH: RelatedTexture("normal_enemy.png", 16, 0, 16, 16),
|
||||
|
||||
Graphic.BOSS_5_HEALTH: RelatedTexture("boss.png", 16, 16, 16, 16),
|
||||
Graphic.BOSS_4_HEALTH: RelatedTexture("boss.png", 0, 16, 16, 16),
|
||||
Graphic.BOSS_3_HEALTH: RelatedTexture("boss.png", 32, 32, 16, 16),
|
||||
Graphic.BOSS_2_HEALTH: RelatedTexture("boss.png", 16, 32, 16, 16),
|
||||
Graphic.BOSS_1_HEALTH: RelatedTexture("boss.png", 0, 32, 16, 16),
|
||||
|
||||
Graphic.EMPTY_HEART: RelatedTexture("hearts.png", 0, 0, 16, 16),
|
||||
Graphic.HEART: RelatedTexture("hearts.png", 16, 0, 16, 16),
|
||||
Graphic.HALF_HEART: RelatedTexture("hearts.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.REMOTE_ITEM: RelatedTexture("items.png", 0, 16, 16, 16),
|
||||
Graphic.CONFETTI_CANNON: RelatedTexture("items.png", 16, 16, 16, 16),
|
||||
Graphic.HAMMER: RelatedTexture("items.png", 32, 16, 16, 16),
|
||||
Graphic.KEY: RelatedTexture("items.png", 0, 0, 16, 16),
|
||||
Graphic.SHIELD: RelatedTexture("items.png", 16, 0, 16, 16),
|
||||
Graphic.SWORD: RelatedTexture("items.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.ITEMS_TEXT: "items_text.png",
|
||||
|
||||
Graphic.ZERO: RelatedTexture("numbers.png", 0, 16, 16, 16),
|
||||
Graphic.ONE: RelatedTexture("numbers.png", 16, 16, 16, 16),
|
||||
Graphic.TWO: RelatedTexture("numbers.png", 32, 16, 16, 16),
|
||||
Graphic.THREE: RelatedTexture("numbers.png", 48, 16, 16, 16),
|
||||
Graphic.FOUR: RelatedTexture("numbers.png", 64, 16, 16, 16),
|
||||
Graphic.FIVE: RelatedTexture("numbers.png", 0, 0, 16, 16),
|
||||
Graphic.SIX: RelatedTexture("numbers.png", 16, 0, 16, 16),
|
||||
Graphic.SEVEN: RelatedTexture("numbers.png", 32, 0, 16, 16),
|
||||
Graphic.EIGHT: RelatedTexture("numbers.png", 48, 0, 16, 16),
|
||||
Graphic.NINE: RelatedTexture("numbers.png", 64, 0, 16, 16),
|
||||
|
||||
Graphic.LETTER_A: RelatedTexture("letters.png", 0, 16, 16, 16),
|
||||
Graphic.LETTER_E: RelatedTexture("letters.png", 16, 16, 16, 16),
|
||||
Graphic.LETTER_H: RelatedTexture("letters.png", 32, 16, 16, 16),
|
||||
Graphic.LETTER_I: RelatedTexture("letters.png", 0, 0, 16, 16),
|
||||
Graphic.LETTER_M: RelatedTexture("letters.png", 16, 0, 16, 16),
|
||||
Graphic.LETTER_T: RelatedTexture("letters.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.DIVIDE: RelatedTexture("symbols.png", 0, 16, 16, 16),
|
||||
Graphic.EQUALS: RelatedTexture("symbols.png", 16, 16, 16, 16),
|
||||
Graphic.MINUS: RelatedTexture("symbols.png", 32, 16, 16, 16),
|
||||
Graphic.PLUS: RelatedTexture("symbols.png", 0, 0, 16, 16),
|
||||
Graphic.TIMES: RelatedTexture("symbols.png", 16, 0, 16, 16),
|
||||
Graphic.NO: RelatedTexture("symbols.png", 32, 0, 16, 16),
|
||||
|
||||
Graphic.UNKNOWN: RelatedTexture("symbols.png", 32, 0, 16, 16), # Same as "No"
|
||||
}
|
||||
|
||||
BACKGROUND_TILE = RelatedTexture("inanimates.png", 0, 32, 16, 16)
|
||||
|
||||
|
||||
class PlayerSprite(Enum):
|
||||
HUMAN = 0
|
||||
DUCK = 1
|
||||
HORSE = 2
|
||||
CAT = 3
|
||||
UNKNOWN = -1
|
||||
|
||||
|
||||
PLAYER_GRAPHICS = {
|
||||
Graphic.PLAYER_DOWN: {
|
||||
PlayerSprite.HUMAN: RelatedTexture("human.png", 0, 16, 16, 16),
|
||||
PlayerSprite.DUCK: RelatedTexture("duck.png", 0, 16, 16, 16),
|
||||
PlayerSprite.HORSE: RelatedTexture("horse.png", 0, 16, 16, 16),
|
||||
PlayerSprite.CAT: RelatedTexture("cat.png", 0, 16, 16, 16),
|
||||
},
|
||||
Graphic.PLAYER_UP: {
|
||||
PlayerSprite.HUMAN: RelatedTexture("human.png", 16, 0, 16, 16),
|
||||
PlayerSprite.DUCK: RelatedTexture("duck.png", 16, 0, 16, 16),
|
||||
PlayerSprite.HORSE: RelatedTexture("horse.png", 16, 0, 16, 16),
|
||||
PlayerSprite.CAT: RelatedTexture("cat.png", 16, 0, 16, 16),
|
||||
},
|
||||
Graphic.PLAYER_LEFT: {
|
||||
PlayerSprite.HUMAN: RelatedTexture("human.png", 16, 16, 16, 16),
|
||||
PlayerSprite.DUCK: RelatedTexture("duck.png", 16, 16, 16, 16),
|
||||
PlayerSprite.HORSE: RelatedTexture("horse.png", 16, 16, 16, 16),
|
||||
PlayerSprite.CAT: RelatedTexture("cat.png", 16, 16, 16, 16),
|
||||
},
|
||||
Graphic.PLAYER_RIGHT: {
|
||||
PlayerSprite.HUMAN: RelatedTexture("human.png", 0, 0, 16, 16),
|
||||
PlayerSprite.DUCK: RelatedTexture("duck.png", 0, 0, 16, 16),
|
||||
PlayerSprite.HORSE: RelatedTexture("horse.png", 0, 0, 16, 16),
|
||||
PlayerSprite.CAT: RelatedTexture("cat.png", 0, 0, 16, 16),
|
||||
},
|
||||
}
|
||||
|
||||
ALL_GRAPHICS = [
|
||||
BACKGROUND_TILE,
|
||||
*IMAGE_GRAPHICS.values(),
|
||||
*[graphic for sub_dict in PLAYER_GRAPHICS.values() for graphic in sub_dict.values()],
|
||||
]
|
||||
|
||||
_textures: dict[str | RelatedTexture, Texture] = {}
|
||||
|
||||
|
||||
def get_texture_by_identifier(texture_identifier: str | RelatedTexture) -> Texture:
|
||||
if texture_identifier in _textures:
|
||||
return _textures[texture_identifier]
|
||||
|
||||
if isinstance(texture_identifier, str):
|
||||
image_data = pkgutil.get_data(game.__name__, f"graphics/{texture_identifier}")
|
||||
if image_data is None:
|
||||
raise RuntimeError(f'Could not find file "graphics/{texture_identifier}" for texture {texture_identifier}')
|
||||
|
||||
image_bytes = BytesIO(cast(Buffer, image_data))
|
||||
texture = cast(Texture, CoreImage(image_bytes, ext="png").texture)
|
||||
texture.mag_filter = "nearest"
|
||||
_textures[texture_identifier] = texture
|
||||
return texture
|
||||
|
||||
base_texture_filename, x, y, w, h = texture_identifier
|
||||
|
||||
base_texture = get_texture_by_identifier(base_texture_filename)
|
||||
|
||||
sub_texture = base_texture.get_region(x, y, w, h)
|
||||
sub_texture.mag_filter = "nearest"
|
||||
_textures[texture_identifier] = sub_texture
|
||||
return sub_texture
|
||||
|
||||
|
||||
def get_texture(graphic: Graphic | Literal["Grass"], player_sprite: PlayerSprite | None = None) -> Texture | None:
|
||||
if graphic == Graphic.EMPTY:
|
||||
return None
|
||||
|
||||
if graphic == "Grass":
|
||||
return get_texture_by_identifier(BACKGROUND_TILE)
|
||||
|
||||
if graphic in IMAGE_GRAPHICS:
|
||||
return get_texture_by_identifier(IMAGE_GRAPHICS[graphic])
|
||||
|
||||
if graphic in PLAYER_GRAPHICS:
|
||||
if player_sprite is None:
|
||||
raise ValueError("Tried to load a player graphic without specifying a player_sprite")
|
||||
|
||||
return get_texture_by_identifier(PLAYER_GRAPHICS[graphic][player_sprite])
|
||||
|
||||
logger.exception(f"Tried to load unknown graphic {graphic}.")
|
||||
return get_texture(Graphic.UNKNOWN)
|
||||
@@ -1,25 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from NetUtils import NetworkItem
|
||||
|
||||
|
||||
class ItemQuality(Enum):
|
||||
FILLER = 0
|
||||
TRAP = 1
|
||||
USEFUL = 2
|
||||
PROGRESSION = 3
|
||||
PROGUSEFUL = 4
|
||||
|
||||
|
||||
def get_quality_for_network_item(network_item: NetworkItem) -> ItemQuality:
|
||||
flags = ItemClassification(network_item.flags)
|
||||
if ItemClassification.progression in flags:
|
||||
if ItemClassification.useful in flags:
|
||||
return ItemQuality.PROGUSEFUL
|
||||
return ItemQuality.PROGRESSION
|
||||
if ItemClassification.useful in flags:
|
||||
return ItemQuality.USEFUL
|
||||
if ItemClassification.trap in flags:
|
||||
return ItemQuality.TRAP
|
||||
return ItemQuality.FILLER
|
||||
@@ -1,27 +0,0 @@
|
||||
import asyncio
|
||||
from collections.abc import Sequence
|
||||
|
||||
import colorama
|
||||
|
||||
from CommonClient import get_base_parser, handle_url_arg
|
||||
|
||||
# !!! IMPORTANT !!!
|
||||
# The client implementation is *not* meant for teaching.
|
||||
# Obviously, it is written to the best of its author's abilities,
|
||||
# but it is not to the same standard as the rest of the apworld.
|
||||
# Copy things from here at your own risk.
|
||||
|
||||
|
||||
def launch_ap_quest_client(*args: Sequence[str]) -> None:
|
||||
from .ap_quest_client import main
|
||||
|
||||
parser = get_base_parser()
|
||||
parser.add_argument("--name", default=None, help="Slot Name to connect as.")
|
||||
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
||||
|
||||
launch_args = handle_url_arg(parser.parse_args(args))
|
||||
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(launch_args))
|
||||
colorama.deinit()
|
||||
@@ -1,249 +0,0 @@
|
||||
import asyncio
|
||||
import pkgutil
|
||||
from asyncio import Task
|
||||
from collections.abc import Buffer
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from kivy import Config
|
||||
from kivy.core.audio import Sound, SoundLoader
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
from .. import game
|
||||
from .item_quality import ItemQuality
|
||||
from .utils import make_data_directory
|
||||
|
||||
ITEM_JINGLES = {
|
||||
ItemQuality.PROGUSEFUL: "8bit ProgUseful.ogg",
|
||||
ItemQuality.PROGRESSION: "8bit Progression.ogg",
|
||||
ItemQuality.USEFUL: "8bit Useful.ogg",
|
||||
ItemQuality.TRAP: "8bit Trap.ogg",
|
||||
ItemQuality.FILLER: "8bit Filler.ogg",
|
||||
}
|
||||
|
||||
CONFETTI_CANNON = "APQuest Confetti Cannon.ogg"
|
||||
MATH_PROBLEM_STARTED_JINGLE = "APQuest Math Problem Starter Jingle.ogg"
|
||||
MATH_PROBLEM_SOLVED_JINGLE = "APQuest Math Problem Solved Jingle.ogg"
|
||||
VICTORY_JINGLE = "8bit Victory.ogg"
|
||||
|
||||
ALL_JINGLES = [
|
||||
MATH_PROBLEM_SOLVED_JINGLE,
|
||||
MATH_PROBLEM_STARTED_JINGLE,
|
||||
CONFETTI_CANNON,
|
||||
VICTORY_JINGLE,
|
||||
*ITEM_JINGLES.values(),
|
||||
]
|
||||
|
||||
BACKGROUND_MUSIC_INTRO = "APQuest Intro.ogg"
|
||||
BACKGROUND_MUSIC = "APQuest BGM.ogg"
|
||||
MATH_TIME_BACKGROUND_MUSIC = "APQuest Math BGM.ogg"
|
||||
|
||||
ALL_BGM = [
|
||||
BACKGROUND_MUSIC_INTRO,
|
||||
BACKGROUND_MUSIC,
|
||||
MATH_TIME_BACKGROUND_MUSIC,
|
||||
]
|
||||
|
||||
ALL_SOUNDS = [
|
||||
*ALL_JINGLES,
|
||||
*ALL_BGM,
|
||||
]
|
||||
|
||||
|
||||
class SoundManager:
|
||||
sound_paths: dict[str, Path]
|
||||
|
||||
jingles: dict[str, Sound]
|
||||
bgm_songs: dict[str, Sound]
|
||||
|
||||
active_bgm_song: str = BACKGROUND_MUSIC_INTRO
|
||||
|
||||
current_background_music_volume: float = 1.0
|
||||
background_music_target_volume: float = 0.0
|
||||
|
||||
background_music_task: Task[None] | None = None
|
||||
background_music_last_position: int = 0
|
||||
|
||||
volume_percentage: int = 0
|
||||
|
||||
game_started: bool
|
||||
math_trap_active: bool
|
||||
allow_intro_to_play: bool
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.extract_sounds()
|
||||
self.populate_sounds()
|
||||
|
||||
self.game_started = False
|
||||
self.allow_intro_to_play = False
|
||||
self.math_trap_active = False
|
||||
|
||||
self.ensure_config()
|
||||
|
||||
self.background_music_task = asyncio.create_task(self.sound_manager_loop())
|
||||
|
||||
def ensure_config(self) -> None:
|
||||
Config.adddefaultsection("APQuest")
|
||||
Config.setdefault("APQuest", "volume", 50)
|
||||
self.set_volume_percentage(Config.getint("APQuest", "volume"))
|
||||
|
||||
async def sound_manager_loop(self) -> None:
|
||||
while True:
|
||||
self.update_background_music()
|
||||
self.do_fade()
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
def extract_sounds(self) -> None:
|
||||
# Kivy appears to have no good way of loading audio from bytes.
|
||||
# So, we have to extract it out of the .apworld first
|
||||
|
||||
sound_paths = {}
|
||||
|
||||
sound_directory = make_data_directory("sounds")
|
||||
|
||||
for sound in ALL_SOUNDS:
|
||||
sound_file_location = sound_directory / sound
|
||||
|
||||
sound_paths[sound] = sound_file_location
|
||||
|
||||
if sound_file_location.exists():
|
||||
continue
|
||||
|
||||
with open(sound_file_location, "wb") as sound_file:
|
||||
data = pkgutil.get_data(game.__name__, f"audio/{sound}")
|
||||
if data is None:
|
||||
logger.exception(f"Unable to extract sound {sound} to Archipelago/data")
|
||||
continue
|
||||
sound_file.write(cast(Buffer, data))
|
||||
|
||||
self.sound_paths = sound_paths
|
||||
|
||||
def load_audio(self, sound_filename: str) -> Sound:
|
||||
audio_path = self.sound_paths[sound_filename]
|
||||
|
||||
sound_object = SoundLoader.load(str(audio_path.absolute()))
|
||||
sound_object.seek(0)
|
||||
return sound_object
|
||||
|
||||
def populate_sounds(self) -> None:
|
||||
try:
|
||||
self.jingles = {sound_filename: self.load_audio(sound_filename) for sound_filename in ALL_JINGLES}
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
try:
|
||||
self.bgm_songs = {sound_filename: self.load_audio(sound_filename) for sound_filename in ALL_BGM}
|
||||
for bgm_song in self.bgm_songs.values():
|
||||
bgm_song.loop = True
|
||||
bgm_song.seek(0)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
def play_jingle(self, audio_filename: str) -> None:
|
||||
higher_priority_sound_is_playing = False
|
||||
|
||||
for sound_name, sound in self.jingles.items():
|
||||
if higher_priority_sound_is_playing: # jingles are ordered by priority, lower priority gets eaten
|
||||
sound.stop()
|
||||
continue
|
||||
|
||||
if sound_name == audio_filename:
|
||||
sound.play()
|
||||
self.update_background_music()
|
||||
higher_priority_sound_is_playing = True
|
||||
|
||||
elif sound.state == "play":
|
||||
higher_priority_sound_is_playing = True
|
||||
|
||||
def update_background_music(self) -> None:
|
||||
self.update_active_song()
|
||||
if any(sound.state == "play" for sound in self.jingles.values()):
|
||||
self.play_background_music(False)
|
||||
else:
|
||||
if self.math_trap_active:
|
||||
# Don't fade math trap song, it ends up feeling better
|
||||
self.play_background_music(True)
|
||||
else:
|
||||
self.fade_background_music(True)
|
||||
|
||||
def play_background_music(self, play: bool = True) -> None:
|
||||
if play:
|
||||
self.background_music_target_volume = 1
|
||||
self.set_background_music_volume(1)
|
||||
else:
|
||||
self.background_music_target_volume = 0
|
||||
self.set_background_music_volume(0)
|
||||
|
||||
def set_background_music_volume(self, volume: float) -> None:
|
||||
self.current_background_music_volume = volume
|
||||
|
||||
for song_filename, song in self.bgm_songs.items():
|
||||
if song_filename != self.active_bgm_song:
|
||||
song.volume = 0
|
||||
continue
|
||||
song.volume = volume * self.volume_percentage / 100
|
||||
|
||||
def fade_background_music(self, fade_in: bool = True) -> None:
|
||||
if fade_in:
|
||||
self.background_music_target_volume = 1
|
||||
else:
|
||||
self.background_music_target_volume = 0
|
||||
|
||||
def set_volume_percentage(self, volume_percentage: float) -> None:
|
||||
volume_percentage_int = int(volume_percentage)
|
||||
if self.volume_percentage != volume_percentage:
|
||||
self.volume_percentage = volume_percentage_int
|
||||
Config.set("APQuest", "volume", volume_percentage_int)
|
||||
Config.write()
|
||||
self.set_background_music_volume(self.current_background_music_volume)
|
||||
|
||||
for jingle in self.jingles.values():
|
||||
jingle.volume = self.volume_percentage / 100
|
||||
|
||||
def do_fade(self) -> None:
|
||||
if self.current_background_music_volume > self.background_music_target_volume:
|
||||
self.set_background_music_volume(max(0.0, self.current_background_music_volume - 0.02))
|
||||
if self.current_background_music_volume < self.background_music_target_volume:
|
||||
self.set_background_music_volume(min(1.0, self.current_background_music_volume + 0.02))
|
||||
|
||||
for song_filename, song in self.bgm_songs.items():
|
||||
if song_filename != self.active_bgm_song:
|
||||
if song_filename == BACKGROUND_MUSIC:
|
||||
# It ends up feeling better if this just always continues playing quietly after being started.
|
||||
# Even "fading in at a random spot" is better than restarting the song after a jingle / math trap.
|
||||
if self.game_started and song.state == "stop":
|
||||
song.play()
|
||||
song.seek(0)
|
||||
continue
|
||||
|
||||
song.stop()
|
||||
song.seek(0)
|
||||
continue
|
||||
|
||||
if self.active_bgm_song == BACKGROUND_MUSIC_INTRO and not self.allow_intro_to_play:
|
||||
song.stop()
|
||||
song.seek(0)
|
||||
continue
|
||||
|
||||
if self.current_background_music_volume != 0:
|
||||
if song.state == "stop":
|
||||
song.play()
|
||||
song.seek(0)
|
||||
|
||||
def update_active_song(self) -> None:
|
||||
new_active_song = self.determine_correct_song()
|
||||
if new_active_song == self.active_bgm_song:
|
||||
return
|
||||
self.active_bgm_song = new_active_song
|
||||
# reevaluate song volumes
|
||||
self.set_background_music_volume(self.current_background_music_volume)
|
||||
|
||||
def determine_correct_song(self) -> str:
|
||||
if not self.game_started:
|
||||
return BACKGROUND_MUSIC_INTRO
|
||||
|
||||
if self.math_trap_active:
|
||||
return MATH_TIME_BACKGROUND_MUSIC
|
||||
|
||||
return BACKGROUND_MUSIC
|
||||
@@ -1,21 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from Utils import user_path
|
||||
|
||||
|
||||
def make_data_directory(dir_name: str) -> Path:
|
||||
root_directory = Path(user_path())
|
||||
if not root_directory.exists():
|
||||
raise FileNotFoundError(f"Unable to find AP directory {root_directory.absolute()}.")
|
||||
|
||||
data_directory = root_directory / "data"
|
||||
|
||||
specific_data_directory = data_directory / "apquest" / dir_name
|
||||
specific_data_directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
gitignore = specific_data_directory / ".gitignore"
|
||||
|
||||
with open(gitignore, "w") as f:
|
||||
f.write("*\n")
|
||||
|
||||
return specific_data_directory
|
||||
@@ -1,33 +0,0 @@
|
||||
from worlds.LauncherComponents import Component, Type, components, launch
|
||||
|
||||
|
||||
# The most common type of component is a client, but there are other components, such as sprite/palette adjusters.
|
||||
# (Note: Some worlds distribute their clients as separate, standalone programs,
|
||||
# while others include them in the apworld itself. Standalone clients are not an apworld component,
|
||||
# although you could make a component that e.g. auto-installs and launches the standalone client for the user.)
|
||||
# APQuest has a Python client inside the apworld that contains the entire game. This is a component.
|
||||
# APQuest will not teach you how to make a client or any other type of component.
|
||||
# However, let's quickly talk about how you register a component to be launchable from the Archipelago Launcher.
|
||||
# First, you'll need a function that takes a list of args (e.g. from the command line) that launches your component.
|
||||
def run_client(*args: str) -> None:
|
||||
# Ideally, you should lazily import your component code so that it doesn't have to be loaded until necessary.
|
||||
from .client.launch import launch_ap_quest_client
|
||||
|
||||
# Also, if your component has its own lifecycle, like if it is its own window that can be interacted with,
|
||||
# you should use the LauncherComponents.launch helper (which itself calls launch_subprocess).
|
||||
# This will create a subprocess for your component, launching it in a separate window from the Archipelago Launcher.
|
||||
launch(launch_ap_quest_client, name="APQuest Client", args=args)
|
||||
|
||||
|
||||
# You then add this function as a component by appending a Component instance to LauncherComponents.components.
|
||||
# Now, it will show up in the Launcher with its display name,
|
||||
# and when the user clicks on the "Open" button, your function will be run.
|
||||
components.append(
|
||||
Component(
|
||||
"APQuest Client",
|
||||
func=run_client,
|
||||
game_name="APQuest",
|
||||
component_type=Type.CLIENT,
|
||||
supports_uri=True,
|
||||
)
|
||||
)
|
||||
@@ -1,78 +0,0 @@
|
||||
# APQuest
|
||||
|
||||
## Wo ist die Seite für die Einstellungen?
|
||||
|
||||
Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt, um
|
||||
eine YAML-Datei zu konfigurieren und zu exportieren.
|
||||
|
||||
## Was ist APQuest?
|
||||
|
||||
APQuest ist ein Spiel, welches von NewSoupVi für Archipelago entwickelt wurde.
|
||||
Es ist ein minimalistisches 8bit-inspiriertes Abenteuerspiel mit gitterförmiger Bewegungssteuerung.
|
||||
APQuest ist ungefähr 20 Sekunden lang. Der Client kann aber nahtlos zwischen mehreren APQuest-Slots wechseln.
|
||||
Wenn du 10 APQuest-Slots in einer Multiworld haben willst, sollte das also problemlos möglich sein.if you want to have 10 of them, that should work pretty well.
|
||||
|
||||
Ausschlaggebend ist bei APQuest, dass das gesamte Spiel in der .apworld enthalten ist.
|
||||
Wenn du also die .apworld in deine
|
||||
[Archipelago-Installation](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installiert hast,
|
||||
kannst du APQuest spielen.
|
||||
|
||||
## Warum existiert APQuest?
|
||||
|
||||
APQuest ist als Beispiel-.apworld geschrieben, mit welchem neue .apworld-Entwickler lernen können, wie man eine
|
||||
.apworld schreibt.
|
||||
Der [APQuest-Quellcode](https://github.com/NewSoupVi/Archipelago/tree/apquest/worlds/apquest) enthält unzählige Kommentare und Beispiele, die erklären,
|
||||
wie jeder Teil der World-API funktioniert.
|
||||
Dabei nutzt er nur die modernsten API-Funktionen (Stand: 2025-08-24).
|
||||
|
||||
Das sekundäre Ziel von APQuest ist, eine semi-minimale, generische .apworld zu sein, die Archipelago selbst gehört.
|
||||
Damit kann sie für Archipelagos Unit-Tests benutzt werden,
|
||||
ohne dass sich die Archipelago-Entwickler davor fürchten müssen, dass APQuest irgendwann gelöscht wird.
|
||||
|
||||
Das dritte Ziel von APQuest ist, das erste "Spiel in einer .apworld" zu sein,
|
||||
wobei das ganze Spiel in Python und Kivy programmiert ist
|
||||
und innerhalb seines CommonClient-basierten Clients spielbar ist.
|
||||
Ich bin mir nicht ganz sicher, dass es wirklich das erste Spiel dieser Art ist, aber ich kenne bis jetzt keine anderen.
|
||||
|
||||
## Wenn ich mich im APQuest-Client angemeldet habe, wie spiele ich dann das Spiel?
|
||||
|
||||
WASD oder Pfeiltasten zum Bewegen.
|
||||
Leertaste, um dein Schwert zu schwingen (wenn du es hast) und um mit Objekten zu interagieren.
|
||||
C, um die Konfettikanone zu feuern.
|
||||
|
||||
Öffne Kisten, zerhacke Büsche, öffne Türen, aktiviere Knöpfe, besiege Gegner.
|
||||
Sobald du den Drachen im oberen rechten Raum bezwingst, gewinnst du das Spiel.
|
||||
Das ist alles! Viel Spaß!
|
||||
|
||||
## Ein Statement zum Besitz von APQuest
|
||||
|
||||
APQuest ist mit der [MIT-Lizenz](https://opensource.org/license/mit) lizenziert,
|
||||
was heißt, dass es von jedem für jeden Zweck modifiziert und verbreitet werden kann.
|
||||
Archipelago hat jedoch seine eigenen Besitztumsstrukturen, die über der MIT-Lizenz stehen.
|
||||
Diese Strukturen machen es unklar,
|
||||
ob eine .apworld-Implementierung überhaupt permanent verlässlich in Archipelago bleibt.
|
||||
|
||||
Im Zusammenhang mit diesen unverbindlichen, nicht gesetzlich verpflichtenden Besitztumsstrukturen
|
||||
mache ich die folgende Aussage.
|
||||
|
||||
Ich, NewSoupVi, verzichte hiermit auf alle Rechte, APQuest aus Archipelago zu entfernen.
|
||||
Dies bezieht sich auf alle Teile von APQuest mit der Ausnahme der Musik und der Soundeffekte.
|
||||
Wenn ich die Töne entfernt haben möchte, muss ich dafür selbst einen PR öffnen.
|
||||
Dieser PR darf nur die Töne entfernen und muss APQuest intakt und spielbar halten.
|
||||
|
||||
Solang ich der Maintainer von APQuest bin, möchte ich als solcher agieren.
|
||||
Das heißt, dass jegliche Änderungen an APQuest zuerst von mir genehmigt werden müssen.
|
||||
|
||||
Wenn ich jedoch aufhöre, der Maintainer von APQuest zu sein,
|
||||
egal ob es mein eigener Wunsch war oder ich meinen Maintainer-Verantwortungen nicht mehr nachkomme,
|
||||
dann wird APQuest automatisch Eigentum der Core-Maintainer von Archipelago,
|
||||
die dann frei entscheiden können, was mit APQuest passieren soll.
|
||||
Es wäre mein Wunsch, dass wenn APQuest an eine andere Einzelperson übergeben wird,
|
||||
diese Person sich an ähnliche Eigentumsregelungen hält wie ich.
|
||||
|
||||
Hoffentlich stellt dieses Statement sicher, dass APQuest für immer eine .apworld sein kann,
|
||||
auf die Archipelago sich verlassen kann.
|
||||
Wenn die Besitztumsstrukturen von Archipelago geändert werden,
|
||||
vertraue ich den Core-Maintainern (bzw. den Eigentümern von Archipelago generell) damit,
|
||||
angemessene Entscheidungen darüber zu treffen,
|
||||
wie dieses Statement im Kontext der neuen Regeln interpretiert werden sollte.
|
||||
@@ -1,69 +0,0 @@
|
||||
# APQuest
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What is APQuest?
|
||||
|
||||
APQuest is an original game made entirely by NewSoupVi.
|
||||
It is a minimal 8bit-era inspired adventure game with grid-like movement.
|
||||
It is about 20 seconds long. However, the client can seamlessly switch between different slots,
|
||||
so if you want to have 10 of them, that should work pretty well.
|
||||
|
||||
Crucially, this game is entirely integrated into the client sitting inside its .apworld.
|
||||
If you have the .apworld installed into your [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
install, you can play APQuest.
|
||||
|
||||
## Why does APQuest exist?
|
||||
|
||||
APQuest is implemented to be an example .apworld that can be used as a learning tool for new .apworld developers.
|
||||
Its [source code](https://github.com/NewSoupVi/Archipelago/tree/apquest/worlds/apquest)
|
||||
contains countless comments explaining how each part of the World API works.
|
||||
Also, as of the writing of this setup guide (2025-08-24), it is up to date with all the modern Archipelago APIs.
|
||||
|
||||
The secondary goal of APQuest is to be a semi-minimal generic world that is owned by Archipelago.
|
||||
This means it can be used for Archipelago's unit tests without fear of eventual removal.
|
||||
|
||||
Finally, APQuest was designed to be the first ever "game inside an .apworld",
|
||||
where the entire game is coded in Python and Kivy and is playable from within its CommonClient-based Client.
|
||||
I'm not actually sure if it's the first, but I'm not aware of any others.
|
||||
|
||||
## Once I'm inside the APQuest client, how do I actually play APQuest?
|
||||
|
||||
WASD or Arrow Keys for movement.
|
||||
Space to swing your sword (if you have it) or interact with objects.
|
||||
C to fire the Confetti Cannon.
|
||||
|
||||
Open chests, slash bushes, open doors, press buttons, defeat enemies.
|
||||
Once you beat the dragon in the top right room, you win.
|
||||
That's all there is! Have fun!
|
||||
|
||||
## A statement on the ownership over APQuest
|
||||
|
||||
APQuest is licensed using the [MIT license](https://opensource.org/license/mit),
|
||||
meaning it can be modified and redistributed by anyone for any purpose.
|
||||
However, Archipelago has its own ownership structures built ontop of the license.
|
||||
These ownership structures call into question whether any world implementation can permanently be relied on.
|
||||
|
||||
In terms of these non-binding, non-legal Archipelago ownership structures, I will make the following statement.
|
||||
|
||||
I, NewSoupVi, hereby relinquish any and all rights to remove APQuest from Archipelago.
|
||||
This applies to all parts of APQuest with the sole exception of the music and sounds.
|
||||
If I want the sounds to be removed, I must do so via a PR to the Archipelago repository myself.
|
||||
Said PR must keep APQuest intact and playable, just with the music removed.
|
||||
|
||||
As long as I am the maintainer of APQuest, I wish to act as such.
|
||||
This means that any updates to APQuest must go through me.
|
||||
|
||||
However, if I ever cease to be the maintainer of APQuest,
|
||||
due to my own wishes or because I fail to uphold the maintainership "contract",
|
||||
the maintainership of APQuest will go to the Core Maintainers of Archipelago, who may then decide what to do with it.
|
||||
They can decide freely, but if the maintainership goes to another singular person,
|
||||
it is my wish that this person adheres to a similar set of rules that I've laid out here for myself.
|
||||
|
||||
Hopefully, this set of commitments should ensure that APQuest will forever be an apworld that can be relied on in Core.
|
||||
If the ownership structures of Archipelago change,
|
||||
I trust the Core Maintainers (or the owners in general) of Archipelago to make reasonable assumptions
|
||||
about how this statement should be reinterpreted to fit the new rules.
|
||||
@@ -1,43 +0,0 @@
|
||||
# APQuest Randomizer Setup-Anleitung
|
||||
|
||||
## Benötigte Software
|
||||
|
||||
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||
falls diese nicht mit deiner Version von Archipelago gebündelt ist.
|
||||
|
||||
## Wie man spielt
|
||||
|
||||
Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst.
|
||||
Dafür musst du oder jemand den du kennst ein Spiel generieren.
|
||||
Dieser Schritt wird hier nicht erklärt, aber du kannst den
|
||||
[Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game) lesen.
|
||||
|
||||
Du musst außerdem [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben
|
||||
und die [APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) darin installieren.
|
||||
|
||||
Von hier ist es einfach, dich mit deinem Slot zu verbinden.
|
||||
|
||||
### Webhost-Raum
|
||||
|
||||
Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](archipelago.gg))
|
||||
kannst du einfach auf deinen Namen in der Spielerliste klicken.
|
||||
Dies öffnet den Archipelago Launcher, welcher dich dann fragt,
|
||||
ob du den Text Client oder den APQuest Client öffnen willst.
|
||||
Wähle hier den APQuest Client. Der Rest sollte automatisch passieren, sodass du APQuest direkt spielen kannst.
|
||||
|
||||
### Lokaler Server
|
||||
|
||||
Falls für deinen Raum keine WebHost-Raumseite verfügbar ist, kannst du APQuest manuell starten.
|
||||
|
||||
Öffne den Archipelago Launcher und finde den APQuest Client in der Komponentenliste. Klicke auf "Open".
|
||||
Nach einer kurzen Wartezeit sollte sich der APQuest Client öffnen.
|
||||
Tippe in der oberen Zeile die Server-Adresse ein und klicke dann auf "Connect".
|
||||
Gib deinen Spielernamen ein. Wenn ein Passwort existiert, tippe dieses auch ein.
|
||||
Du solltest jetzt verbunden sein und kannst APQuest spielen.
|
||||
|
||||
## Slotwechsel
|
||||
|
||||
Der APQuest Client kann zwischen verschiedenen Slots wechseln, ohne neugestartet werden zu müssen,
|
||||
|
||||
Klicke einfach den "Disconnect"-Knopf. Dann verbinde dich mit dem anderen Raum / Slot.
|
||||
@@ -1,42 +0,0 @@
|
||||
# APQuest Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||
- [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases),
|
||||
if not bundled with your version of Archipelago
|
||||
|
||||
## How to play
|
||||
|
||||
First, you need a room to connect to. For this, you or someone you know has to generate a game.
|
||||
This will not be explained here,
|
||||
but you can check the [Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game).
|
||||
|
||||
You also need to have [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installed
|
||||
and the [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) installed into Archipelago.
|
||||
|
||||
From here, connecting to your APQuest slot is easy. There are two scenarios.
|
||||
|
||||
### Webhost Room
|
||||
|
||||
If your room is hosted on a WebHost (e.g. [archipelago.gg](archipelago.gg)),
|
||||
you should be able to simply click on your name in the player list.
|
||||
This will open the Archipelago Launcher
|
||||
and ask you whether you want to connect with the Text Client or the APQuest Client.
|
||||
Choose "APQuest Client". The rest should happen completely automatically and you should be able to play APQuest.
|
||||
|
||||
### Locally hosted room
|
||||
|
||||
If your room does not have a WebHost room page available, you can launch APQuest manually.
|
||||
|
||||
Open the Archipelago Launcher, and then select the APQuest Client from the list.
|
||||
After a short while, the APQuest client should open.
|
||||
Enter the server address at the top and click "Connect".
|
||||
Then, enter your name. If a password exists, enter the password.
|
||||
You should now be connected and able to play APQuest.
|
||||
|
||||
## Switching Rooms
|
||||
|
||||
The APQuest Client can seamlessly switch rooms without restarting.
|
||||
|
||||
Simply click the "Disconnect" button, then connect to a different slot/room.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user