Compare commits
16 Commits
NewSoupVi-
...
0.6.4-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8837e617e4 | ||
|
|
2bf410f285 | ||
|
|
04fe43d53a | ||
|
|
643f61e7f4 | ||
|
|
6b91ffecf1 | ||
|
|
4f7f092b9b | ||
|
|
df3c6b7980 | ||
|
|
19839399e5 | ||
|
|
4847be98d2 | ||
|
|
3105320038 | ||
|
|
e8c8b0dbc5 | ||
|
|
c199775c48 | ||
|
|
d2bf7fdaf7 | ||
|
|
621ec274c3 | ||
|
|
7cd73e2710 | ||
|
|
708df4d1e2 |
2
.github/workflows/build.yml
vendored
@@ -24,7 +24,7 @@ env:
|
||||
# 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.
|
||||
APPIMAGETOOL_VERSION: continuous
|
||||
APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
|
||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
||||
APPIMAGE_RUNTIME_VERSION: continuous
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
||||
|
||||
|
||||
2
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ env:
|
||||
# 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.
|
||||
APPIMAGETOOL_VERSION: continuous
|
||||
APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
|
||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
||||
APPIMAGE_RUNTIME_VERSION: continuous
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
||||
|
||||
|
||||
def mystery_argparse():
|
||||
def mystery_argparse(argv: list[str] | None = None):
|
||||
from settings import get_settings
|
||||
settings = get_settings()
|
||||
defaults = settings.generator
|
||||
@@ -57,7 +57,7 @@ def mystery_argparse():
|
||||
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()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.skip_output and args.spoiler_only:
|
||||
parser.error("Cannot mix --skip_output and --spoiler_only")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import base64
|
||||
import os
|
||||
import socket
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
from flask import Flask
|
||||
@@ -61,20 +62,21 @@ cache = Cache()
|
||||
Compress(app)
|
||||
|
||||
|
||||
def to_python(value):
|
||||
def to_python(value: str) -> uuid.UUID:
|
||||
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=='))
|
||||
|
||||
|
||||
def to_url(value):
|
||||
def to_url(value: uuid.UUID) -> str:
|
||||
return base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
|
||||
|
||||
|
||||
class B64UUIDConverter(BaseConverter):
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str) -> uuid.UUID:
|
||||
return to_python(value)
|
||||
|
||||
def to_url(self, value):
|
||||
def to_url(self, value: typing.Any) -> str:
|
||||
assert isinstance(value, uuid.UUID)
|
||||
return to_url(value)
|
||||
|
||||
|
||||
@@ -84,7 +86,7 @@ app.jinja_env.filters["suuid"] = to_url
|
||||
app.jinja_env.filters["title_sorted"] = title_sorted
|
||||
|
||||
|
||||
def register():
|
||||
def register() -> None:
|
||||
"""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():
|
||||
def stop() -> None:
|
||||
"""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()
|
||||
args = mystery_argparse([]) # Just to set up the Namespace with defaults
|
||||
args.multi = playercount
|
||||
args.seed = seed
|
||||
args.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
|
||||
|
||||
90
WebHostLib/markdown.py
Normal file
@@ -0,0 +1,90 @@
|
||||
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
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
@@ -27,49 +28,6 @@ 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):
|
||||
@@ -91,10 +49,9 @@ def game_info(game, lang):
|
||||
theme = get_world_theme(game)
|
||||
secure_game_name = secure_filename(game)
|
||||
lang = secure_filename(lang)
|
||||
document = render_markdown(os.path.join(
|
||||
app.static_folder, "generated", "docs",
|
||||
secure_game_name, f"{lang}_{secure_game_name}.md"
|
||||
))
|
||||
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)
|
||||
return render_template(
|
||||
"markdown_document.html",
|
||||
title=f"{game} Guide",
|
||||
@@ -119,10 +76,9 @@ def tutorial(game: str, file: str):
|
||||
theme = get_world_theme(game)
|
||||
secure_game_name = secure_filename(game)
|
||||
file = secure_filename(file)
|
||||
document = render_markdown(os.path.join(
|
||||
app.static_folder, "generated", "docs",
|
||||
secure_game_name, file+".md"
|
||||
))
|
||||
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)
|
||||
return render_template(
|
||||
"markdown_document.html",
|
||||
title=f"{game} Guide",
|
||||
|
||||
@@ -4,9 +4,11 @@ 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.17
|
||||
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
|
||||
|
||||
11
kvui.py
@@ -34,6 +34,17 @@ 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
|
||||
|
||||
16
ruff.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
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
@@ -146,7 +146,16 @@ def download_SNI() -> None:
|
||||
|
||||
signtool: str | None = None
|
||||
try:
|
||||
with urllib.request.urlopen('http://192.168.206.4:12345/connector/status') as response:
|
||||
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:
|
||||
html = response.read()
|
||||
if b"status=OK\n" in html:
|
||||
signtool = (r'signtool sign /sha1 6df76fe776b82869a5693ddcb1b04589cffa6faf /fd sha256 /td sha256 '
|
||||
|
||||
102
test/general/test_world_manifest.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""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,6 +3,7 @@
|
||||
# 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
|
||||
@@ -11,7 +12,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_autohost, upload_multidata)
|
||||
stop_autogen, stop_autohost, upload_multidata, generate_remote)
|
||||
from test.hosting.world import copy as copy_world, delete as delete_world
|
||||
|
||||
failure = False
|
||||
@@ -56,35 +57,62 @@ 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 = []
|
||||
data_paths = []
|
||||
rooms = []
|
||||
p1_games: list[str] = []
|
||||
data_paths: list[Path | None] = []
|
||||
rooms: list[str] = []
|
||||
multidata: Path | None
|
||||
|
||||
copy_world("VVVVVV", "Temp World")
|
||||
try:
|
||||
for n, games in enumerate(multis, 1):
|
||||
print(f"Generating [{n}] {', '.join(games)}")
|
||||
print(f"Generating [{n}] {', '.join(games)} offline")
|
||||
multidata = generate_local(games, tempdir)
|
||||
print(f"Generated [{n}] {', '.join(games)} as {multidata}\n")
|
||||
p1_games.append(games[0])
|
||||
data_paths.append(multidata)
|
||||
p1_games.append(games[0])
|
||||
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"Uploaded [{n}] {multidata} as {room}\n")
|
||||
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")
|
||||
rooms.append(room)
|
||||
|
||||
print("Starting autohost")
|
||||
@@ -96,31 +124,10 @@ 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
|
||||
@@ -134,6 +141,7 @@ 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:
|
||||
@@ -156,6 +164,31 @@ 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")
|
||||
@@ -176,10 +209,12 @@ 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,6 +1,10 @@
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional, cast
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, cast
|
||||
|
||||
from WebHostLib import to_python
|
||||
|
||||
@@ -10,6 +14,7 @@ if TYPE_CHECKING:
|
||||
|
||||
__all__ = [
|
||||
"get_app",
|
||||
"generate_remote",
|
||||
"upload_multidata",
|
||||
"create_room",
|
||||
"start_room",
|
||||
@@ -17,6 +22,7 @@ __all__ = [
|
||||
"set_room_timeout",
|
||||
"get_multidata_for_room",
|
||||
"set_multidata_for_room",
|
||||
"stop_autogen",
|
||||
"stop_autohost",
|
||||
]
|
||||
|
||||
@@ -33,10 +39,43 @@ 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"),
|
||||
@@ -188,7 +227,7 @@ def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: by
|
||||
room.seed.multidata = data
|
||||
|
||||
|
||||
def stop_autohost(graceful: bool = True) -> None:
|
||||
def _stop_webhost_mp(name_filter: str, graceful: bool = True) -> None:
|
||||
import os
|
||||
import signal
|
||||
|
||||
@@ -198,13 +237,30 @@ def stop_autohost(graceful: bool = True) -> None:
|
||||
|
||||
stop()
|
||||
proc: multiprocessing.process.BaseProcess
|
||||
for proc in filter(lambda child: child.name.startswith("MultiHoster"), multiprocessing.active_children()):
|
||||
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
|
||||
if graceful and proc.pid:
|
||||
os.kill(proc.pid, getattr(signal, "CTRL_C_EVENT", signal.SIGINT))
|
||||
else:
|
||||
proc.kill()
|
||||
try:
|
||||
proc.join(30)
|
||||
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)
|
||||
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 import AutoWorldRegister
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
assert dst not in _new_worlds, "World already created"
|
||||
if '"' in dst or "\\" in dst: # easier to reject than to escape
|
||||
|
||||
78
test/webhost/test_markdown.py
Normal file
@@ -0,0 +1,78 @@
|
||||
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)
|
||||
@@ -1,4 +1,6 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import Utils
|
||||
import websockets
|
||||
import functools
|
||||
@@ -208,6 +210,9 @@ 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",
|
||||
]
|
||||
|
||||
@@ -88,9 +88,8 @@ 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,9 +88,8 @@ 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,9 +89,8 @@ 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.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -111,9 +111,8 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
|
||||
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)".
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 627 KiB After Width: | Height: | Size: 493 KiB |
@@ -92,7 +92,7 @@ appropriate to your operating system, and extract the folder to a convenient loc
|
||||
Archipelago is to place the extracted game folder into the `Archipelago` directory and rename it to just be "Factorio".
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
Next, you should launch your Factorio Server by running `factorio.exe`, which is located at: `bin/x64/factorio.exe`. You
|
||||
will be asked to log in to your Factorio account using the same credentials you used on Factorio's website. After you
|
||||
@@ -122,7 +122,7 @@ This allows you to host your own Factorio game.
|
||||
Archipelago if you chose to include it during the installation process.
|
||||
6. Enter `/connect [server-address]` into the input box at the bottom of the Archipelago Client and press "Enter"
|
||||
|
||||

|
||||

|
||||
|
||||
7. Launch your Factorio Client
|
||||
8. Click on "Multiplayer" in the main menu
|
||||
|
||||
@@ -115,9 +115,8 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
|
||||
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)".
|
||||
|
||||
|
||||
@@ -123,10 +123,8 @@ Vous ne devez faire ces étapes qu'une fois. À noter que RetroArch 1.9.x ne fon
|
||||
1. Entrez dans le menu principal de RetroArch.
|
||||
2. Allez dans Settings --> User Interface. Activez l'option "Show Advanced Settings".
|
||||
3. Allez dans Settings --> Network. Activez l'option "Network Commands", qui se trouve sous "Request Device 16".
|
||||
Laissez le "Network Command Port" à sa valeur par defaut, qui devrait être 55355.
|
||||
|
||||
|
||||

|
||||
Laissez le "Network Command Port" à sa valeur par defaut, qui devrait être 55355. \
|
||||

|
||||
4. Allez dans le Menu Principal --> Online Updater --> Core Downloader. Trouvez et sélectionnez "Nintendo - SNES / SFC (bsnes-mercury
|
||||
Performance)".
|
||||
|
||||
|
||||
BIN
worlds/generic/docs/retroarch-network-commands-en.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
worlds/generic/docs/retroarch-network-commands-fr.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -134,13 +134,13 @@ class KH1Context(CommonContext):
|
||||
os.makedirs(self.game_communication_path)
|
||||
for ss in self.checked_locations:
|
||||
filename = f"send{ss}"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w', encoding='utf-8') as f:
|
||||
f.close()
|
||||
|
||||
# Handle Slot Data
|
||||
self.slot_data = args['slot_data']
|
||||
for key in list(args['slot_data'].keys()):
|
||||
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w', encoding='utf-8') as f:
|
||||
f.write(str(args['slot_data'][key]))
|
||||
f.close()
|
||||
if key == "remote_location_ids":
|
||||
@@ -161,7 +161,7 @@ class KH1Context(CommonContext):
|
||||
found = True
|
||||
if not found:
|
||||
if (NetworkItem(*item).player == self.slot and (NetworkItem(*item).location in self.remote_location_ids) or (NetworkItem(*item).location < 0)) or NetworkItem(*item).player != self.slot:
|
||||
with open(os.path.join(self.game_communication_path, item_filename), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, item_filename), 'w', encoding='utf-8') as f:
|
||||
f.write(str(NetworkItem(*item).item) + "\n" + str(NetworkItem(*item).location) + "\n" + str(NetworkItem(*item).player))
|
||||
f.close()
|
||||
self.item_num += 1
|
||||
@@ -170,7 +170,7 @@ class KH1Context(CommonContext):
|
||||
if "checked_locations" in args:
|
||||
for ss in self.checked_locations:
|
||||
filename = f"send{ss}"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w', encoding='utf-8') as f:
|
||||
f.close()
|
||||
|
||||
if cmd in {"PrintJSON"} and "type" in args:
|
||||
@@ -195,7 +195,7 @@ class KH1Context(CommonContext):
|
||||
filename = "msg"
|
||||
if message != "":
|
||||
if not os.path.exists(self.game_communication_path + "/" + filename):
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w', encoding='utf-8') as f:
|
||||
f.write(message)
|
||||
f.close()
|
||||
if args["type"] == "ItemCheat":
|
||||
@@ -207,7 +207,7 @@ class KH1Context(CommonContext):
|
||||
filename = "msg"
|
||||
message = "Received " + itemName + "\nfrom server"
|
||||
if not os.path.exists(self.game_communication_path + "/" + filename):
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w', encoding='utf-8') as f:
|
||||
f.write(message)
|
||||
f.close()
|
||||
|
||||
@@ -218,7 +218,7 @@ class KH1Context(CommonContext):
|
||||
logger.info(f"DeathLink: {text}")
|
||||
else:
|
||||
logger.info(f"DeathLink: Received from {data['source']}")
|
||||
with open(os.path.join(self.game_communication_path, 'dlreceive'), 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, 'dlreceive'), 'w', encoding='utf-8') as f:
|
||||
f.write(str(int(data["time"])))
|
||||
f.close()
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ from .patches import bingo as _
|
||||
from .patches import multiworld as _
|
||||
from .patches import tradeSequence as _
|
||||
from . import hints
|
||||
from . import utils
|
||||
|
||||
from .patches import bank34
|
||||
from .roomEditor import RoomEditor, Object
|
||||
@@ -231,10 +232,10 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
rom.patch(0, 0x0003, "00", "01")
|
||||
|
||||
# Patch the sword check on the shopkeeper turning around.
|
||||
#if ladxr_settings["steal"] == 'never':
|
||||
# rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
|
||||
#elif ladxr_settings["steal"] == 'always':
|
||||
# rom.patch(4, 0x36F9, "FA4EDB", "3E0100")
|
||||
if options["stealing"] == Options.Stealing.option_disabled:
|
||||
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
|
||||
rom.texts[0x2E] = utils.formatText("Hey! Welcome! Did you know that I have eyes on the back of my head?")
|
||||
rom.texts[0x2F] = utils.formatText("Nothing escapes my gaze! Your thieving ways shall never prosper!")
|
||||
|
||||
#if ladxr_settings["hpmode"] == 'inverted':
|
||||
# patches.health.setStartHealth(rom, 9)
|
||||
@@ -325,7 +326,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
0x1A9, 0x1AA, 0x1AB, 0x1AC, 0x1AD,
|
||||
|
||||
# Prices
|
||||
0x02C, 0x02D, 0x030, 0x031, 0x032, 0x033, # Shop items
|
||||
0x02C, 0x02D, 0x02E, 0x02F, 0x030, 0x031, 0x032, 0x033, # Shop items
|
||||
0x03B, # Trendy Game
|
||||
0x045, # Fisherman
|
||||
0x018, 0x019, # Crazy Tracy
|
||||
|
||||
@@ -43,8 +43,12 @@ class World:
|
||||
self._addEntrance("start_house", mabe_village, start_house, None)
|
||||
|
||||
shop = Location("Shop")
|
||||
Location().add(ShopItem(0)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 500)), SWORD))
|
||||
Location().add(ShopItem(1)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 1480)), SWORD))
|
||||
if options.steal == "inlogic":
|
||||
Location().add(ShopItem(0)).connect(shop, OR(SWORD, AND(r.can_farm, COUNT("RUPEES", 500))))
|
||||
Location().add(ShopItem(1)).connect(shop, OR(SWORD, AND(r.can_farm, COUNT("RUPEES", 1480))))
|
||||
else:
|
||||
Location().add(ShopItem(0)).connect(shop, AND(r.can_farm, COUNT("RUPEES", 500)))
|
||||
Location().add(ShopItem(1)).connect(shop, AND(r.can_farm, COUNT("RUPEES", 1480)))
|
||||
self._addEntrance("shop", mabe_village, shop, None)
|
||||
|
||||
dream_hut = Location("Dream Hut")
|
||||
|
||||
@@ -162,8 +162,8 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
|
||||
[Hero] Switch version hero mode, double damage, no heart/fairy drops.
|
||||
[One hit KO] You die on a single hit, always."""),
|
||||
Setting('steal', 'Gameplay', 't', 'Stealing from the shop',
|
||||
options=[('always', 'a', 'Always'), ('never', 'n', 'Never'), ('default', '', 'Normal')], default='default',
|
||||
description="""Effects when you can steal from the shop. Stealing is bad and never in logic.
|
||||
options=[('inlogic', 'a', 'In logic'), ('disabled', 'n', 'Disabled'), ('outoflogic', '', 'Out of logic')], default='outoflogic',
|
||||
description="""Effects when you can steal from the shop and if it is in logic.
|
||||
[Normal] requires the sword before you can steal.
|
||||
[Always] you can always steal from the shop
|
||||
[Never] you can never steal from the shop."""),
|
||||
@@ -286,7 +286,7 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
|
||||
if self.goal in ("bingo", "bingo-full"):
|
||||
req("overworld", "normal", "Bingo goal does not work with dungeondive")
|
||||
req("accessibility", "all", "Bingo goal needs 'all' accessibility")
|
||||
dis("steal", "never", "default", "With bingo goal, stealing should be allowed")
|
||||
dis("steal", "disabled", "default", "With bingo goal, stealing should be allowed")
|
||||
dis("boss", "random", "shuffle", "With bingo goal, bosses need to be on normal or shuffle")
|
||||
dis("miniboss", "random", "shuffle", "With bingo goal, minibosses need to be on normal or shuffle")
|
||||
if self.overworld == "dungeondive":
|
||||
|
||||
@@ -325,6 +325,18 @@ class HardMode(Choice, LADXROption):
|
||||
default = option_none
|
||||
|
||||
|
||||
class Stealing(Choice, LADXROption):
|
||||
"""
|
||||
Puts stealing from the shop in logic if the player has a sword.
|
||||
"""
|
||||
display_name = "Stealing"
|
||||
ladxr_name = "steal"
|
||||
option_in_logic = 1
|
||||
option_out_of_logic = 2
|
||||
option_disabled = 3
|
||||
default = option_out_of_logic
|
||||
|
||||
|
||||
class Overworld(Choice, LADXROption):
|
||||
"""
|
||||
**Open Mabe:** Replaces rock on the east side of Mabe Village with bushes,
|
||||
@@ -656,6 +668,7 @@ class LinksAwakeningOptions(PerGameCommonOptions):
|
||||
nag_messages: NagMessages
|
||||
ap_title_screen: APTitleScreen
|
||||
boots_controls: BootsControls
|
||||
stealing: Stealing
|
||||
quickswap: Quickswap
|
||||
hard_mode: HardMode
|
||||
low_hp_beep: LowHpBeep
|
||||
|
||||
@@ -97,7 +97,7 @@ def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
|
||||
"nag_messages",
|
||||
"ap_title_screen",
|
||||
"boots_controls",
|
||||
# "stealing",
|
||||
"stealing",
|
||||
"quickswap",
|
||||
"hard_mode",
|
||||
"low_hp_beep",
|
||||
|
||||
@@ -73,9 +73,8 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
|
||||
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 - Gameboy / Color (SameBoy)".
|
||||
|
||||
#### BizHawk 2.8 or newer (older versions untested)
|
||||
|
||||
@@ -48,7 +48,7 @@ A window will open with a few settings to enter:
|
||||
- **Slot name**: Put the player name you specified in your YAML config file in this field.
|
||||
- **Password**: If the server has a password, put it there.
|
||||
|
||||

|
||||

|
||||
|
||||
Once all those fields were filled appropriately, click on the `Connect to Archipelago` button below to try connecting to
|
||||
the Archipelago server.
|
||||
@@ -67,7 +67,7 @@ You should see a window with settings to fill:
|
||||
- **Output ROM directory**: This is where the randomized ROMs will be put. No need to change this unless you want them
|
||||
to be created in a very specific folder.
|
||||
|
||||

|
||||

|
||||
|
||||
There also a few cosmetic options you can fill before clicking the `Build ROM` button which should create your
|
||||
randomized seed if everything went right.
|
||||
@@ -83,7 +83,7 @@ the items you have received from other players.
|
||||
|
||||
You should see the following window:
|
||||
|
||||

|
||||

|
||||
|
||||
As written, you have to open the newly generated ROM inside either Retroarch or Bizhawk using the Genesis Plus GX core.
|
||||
Be careful to select that core, because any other core (e.g. BlastEm) won't work.
|
||||
@@ -116,6 +116,6 @@ The client is packaged with both an **automatic item tracker** and an **automati
|
||||
If you don't know all checks in the game, don't be afraid: you can click the `Where is it?` button that will show
|
||||
you a screenshot of where the location actually is.
|
||||
|
||||

|
||||

|
||||
|
||||
Have fun!
|
||||
Have fun!
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 986 B |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -106,9 +106,8 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
|
||||
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)".
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
||||
from . import MM2World
|
||||
|
||||
MM2LCHASH = "37f2c36ce7592f1e16b3434b3985c497"
|
||||
PROTEUSHASH = "9ff045a3ca30018b6e874c749abb3ec4"
|
||||
PROTEUSHASH = "b69fff40212b80c94f19e786d1efbf61"
|
||||
MM2NESHASH = "0527a0ee512f69e08b8db6dc97964632"
|
||||
MM2VCHASH = "0c78dfe8e90fb8f3eed022ff01126ad3"
|
||||
|
||||
@@ -404,7 +404,7 @@ def get_base_rom_path(file_name: str = "") -> str:
|
||||
return file_name
|
||||
|
||||
|
||||
PRG_OFFSET = 0x8ED70
|
||||
PRG_OFFSET = 0x8F170
|
||||
PRG_SIZE = 0x40000
|
||||
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ Wenn du einer Multiworld beitrittst, wirst du gefordert eine YAML-Datei bei dem
|
||||
erhälst du (in der Regel) einen Link vom Host der Multiworld. Dieser führt dich zu einem Raum, in dem alle
|
||||
teilnehmenden Spieler (bzw. Welten) aufgelistet sind. Du solltest dich dann auf **deine** Welt konzentrieren
|
||||
und klicke dann auf `Download APZ5 File...`.
|
||||

|
||||

|
||||
|
||||
Führe die `.apz5`-Datei mit einem Doppelklick aus, um deinen Ocarina Of Time-Client zu starten, sowie das patchen
|
||||
deiner ROM. Ist dieser Prozess fertig (kann etwas dauern), startet sich der Client und der Emulator automatisch
|
||||
|
||||
@@ -1237,7 +1237,7 @@ saffron_gym_warps = [
|
||||
|
||||
entrance_only = [
|
||||
"Route 4-W to Mt Moon 1F", "Saffron City-G to Saffron Gym-S", "Saffron City-Copycat to Saffron Copycat's House 1F",
|
||||
"Saffron City-Pidgey to Saffron Pidgey House", "Celadon Game Corner-Hidden Stairs to Rocket Hideout B1F"
|
||||
"Saffron City-Pidgey to Saffron Pidgey House", "Celadon Game Corner-Hidden Stairs to Rocket Hideout B1F",
|
||||
"Cinnabar Island-M to Pokemon Mansion 1F", "Mt Moon B2F to Mt Moon B1F-W", "Silph Co 7F-NW to Silph Co 11F-W",
|
||||
"Viridian City-G", "Cerulean City-Cave to Cerulean Cave 1F-SE", "Cerulean City-T to Cerulean Trashed House",
|
||||
"Route 10-P to Power Plant", "S.S. Anne 2F to S.S. Anne Captain's Room", "Pewter City-M to Pewter Museum 1F-E",
|
||||
|
||||
@@ -124,9 +124,8 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
|
||||
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)".
|
||||
|
||||
|
||||
@@ -100,9 +100,8 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
|
||||
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)".
|
||||
|
||||
|
||||
@@ -120,9 +120,8 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
|
||||
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)".
|
||||
|
||||
|
||||
@@ -108,8 +108,8 @@ 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)".
|
||||
|
||||
|
||||
@@ -423,7 +423,7 @@ booster_contents: Dict[str, List[str]] = {
|
||||
"Kaiser Glider",
|
||||
"Horus the Black Flame Dragon LV6",
|
||||
"Luster Dragon",
|
||||
"Luster Dragon #2"
|
||||
"Luster Dragon #2",
|
||||
"Spear Dragon",
|
||||
"Armed Dragon LV3",
|
||||
"Armed Dragon LV5",
|
||||
@@ -634,7 +634,7 @@ booster_contents: Dict[str, List[str]] = {
|
||||
"Mystic Swordsman LV6",
|
||||
"Horus the Black Flame Dragon LV6",
|
||||
"Horus the Black Flame Dragon LV4",
|
||||
"Armed Dragon LV3"
|
||||
"Armed Dragon LV3",
|
||||
"Armed Dragon LV5",
|
||||
"Silent Swordsman Lv3",
|
||||
"Silent Swordsman Lv5",
|
||||
@@ -750,7 +750,7 @@ booster_contents: Dict[str, List[str]] = {
|
||||
"Formation Union",
|
||||
"Princess Pikeru",
|
||||
"Skull Zoma",
|
||||
"Metal Reflect Slime"
|
||||
"Metal Reflect Slime",
|
||||
"Level Up!",
|
||||
"Howling Insect",
|
||||
"Tribute Doll",
|
||||
|
||||
@@ -668,7 +668,7 @@ def only_dragon(state, player):
|
||||
], player) and (state.count_from_list_unique([
|
||||
"Luster Dragon",
|
||||
"Spear Dragon",
|
||||
"Cave Dragon"
|
||||
"Cave Dragon",
|
||||
"Armed Dragon LV3",
|
||||
"Masked Dragon",
|
||||
"Twin-Headed Behemoth",
|
||||
|
||||
@@ -20,9 +20,8 @@ RetroArch 1.9.x will not work, as it is older than 1.10.3.
|
||||
- "Sega - MS/GG/MD/CD (Genesis Plus GX)
|
||||
3. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
|
||||
4. 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. \
|
||||

|
||||
|
||||
### Linux Setup
|
||||
|
||||
|
||||