Compare commits
1 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f90571fd |
6
.github/workflows/codeql-analysis.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -72,4 +72,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
2
.github/workflows/unittests.yml
vendored
@@ -89,4 +89,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
export PYTHONPATH=$(pwd)
|
export PYTHONPATH=$(pwd)
|
||||||
timeout 600 python test/hosting/__main__.py
|
python test/hosting/__main__.py
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ class MultiWorld():
|
|||||||
new_item.classification |= classifications[item_name]
|
new_item.classification |= classifications[item_name]
|
||||||
new_itempool.append(new_item)
|
new_itempool.append(new_item)
|
||||||
|
|
||||||
region = Region(group["world"].origin_region_name, group_id, self, "ItemLink")
|
region = Region("Menu", group_id, self, "ItemLink")
|
||||||
self.regions.append(region)
|
self.regions.append(region)
|
||||||
locations = region.locations
|
locations = region.locations
|
||||||
# ensure that progression items are linked first, then non-progression
|
# ensure that progression items are linked first, then non-progression
|
||||||
|
|||||||
@@ -710,11 +710,6 @@ class CommonContext:
|
|||||||
|
|
||||||
def run_cli(self):
|
def run_cli(self):
|
||||||
if sys.stdin:
|
if sys.stdin:
|
||||||
if sys.stdin.fileno() != 0:
|
|
||||||
from multiprocessing import parent_process
|
|
||||||
if parent_process():
|
|
||||||
return # ignore MultiProcessing pipe
|
|
||||||
|
|
||||||
# steam overlay breaks when starting console_loop
|
# steam overlay breaks when starting console_loop
|
||||||
if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''):
|
if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''):
|
||||||
logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.")
|
logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.")
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
player_files = {}
|
player_files = {}
|
||||||
for file in os.scandir(args.player_files_path):
|
for file in os.scandir(args.player_files_path):
|
||||||
fname = file.name
|
fname = file.name
|
||||||
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
|
if file.is_file() and not fname.startswith(".") and \
|
||||||
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
|
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
|
||||||
path = os.path.join(args.player_files_path, fname)
|
path = os.path.join(args.player_files_path, fname)
|
||||||
try:
|
try:
|
||||||
@@ -453,10 +453,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
|||||||
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
||||||
|
|
||||||
ret.game = get_choice("game", weights)
|
ret.game = get_choice("game", weights)
|
||||||
if not isinstance(ret.game, str):
|
|
||||||
if ret.game is None:
|
|
||||||
raise Exception('"game" not specified')
|
|
||||||
raise Exception(f"Invalid game: {ret.game}")
|
|
||||||
if ret.game not in AutoWorldRegister.world_types:
|
if ret.game not in AutoWorldRegister.world_types:
|
||||||
from worlds import failed_world_loads
|
from worlds import failed_world_loads
|
||||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
|
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
|
||||||
|
|||||||
19
Launcher.py
@@ -22,15 +22,16 @@ from os.path import isfile
|
|||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
import settings
|
||||||
|
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
import settings
|
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
|
||||||
import Utils
|
is_windows, is_macos, is_linux
|
||||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
|
||||||
user_path)
|
|
||||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
|
||||||
|
|
||||||
|
|
||||||
def open_host_yaml():
|
def open_host_yaml():
|
||||||
@@ -103,7 +104,6 @@ components.extend([
|
|||||||
Component("Open host.yaml", func=open_host_yaml),
|
Component("Open host.yaml", func=open_host_yaml),
|
||||||
Component("Open Patch", func=open_patch),
|
Component("Open Patch", func=open_patch),
|
||||||
Component("Generate Template Options", func=generate_yamls),
|
Component("Generate Template Options", func=generate_yamls),
|
||||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
|
||||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||||
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||||
Component("Browse Files", func=browse_files),
|
Component("Browse Files", func=browse_files),
|
||||||
@@ -181,11 +181,6 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
|||||||
App.get_running_app().stop()
|
App.get_running_app().stop()
|
||||||
Window.close()
|
Window.close()
|
||||||
|
|
||||||
def _stop(self, *largs):
|
|
||||||
# see run_gui Launcher _stop comment for details
|
|
||||||
self.root_window.close()
|
|
||||||
super()._stop(*largs)
|
|
||||||
|
|
||||||
Popup().run()
|
Popup().run()
|
||||||
|
|
||||||
|
|
||||||
@@ -259,7 +254,7 @@ def run_gui():
|
|||||||
_client_layout: Optional[ScrollBox] = None
|
_client_layout: Optional[ScrollBox] = None
|
||||||
|
|
||||||
def __init__(self, ctx=None):
|
def __init__(self, ctx=None):
|
||||||
self.title = self.base_title + " " + Utils.__version__
|
self.title = self.base_title
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.icon = r"data/icon.png"
|
self.icon = r"data/icon.png"
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|||||||
@@ -727,15 +727,15 @@ class Context:
|
|||||||
if not hint.local and data not in concerns[hint.finding_player]:
|
if not hint.local and data not in concerns[hint.finding_player]:
|
||||||
concerns[hint.finding_player].append(data)
|
concerns[hint.finding_player].append(data)
|
||||||
# remember hints in all cases
|
# remember hints in all cases
|
||||||
|
if not hint.found:
|
||||||
# since hints are bidirectional, finding player and receiving player,
|
# since hints are bidirectional, finding player and receiving player,
|
||||||
# we can check once if hint already exists
|
# we can check once if hint already exists
|
||||||
if hint not in self.hints[team, hint.finding_player]:
|
if hint not in self.hints[team, hint.finding_player]:
|
||||||
self.hints[team, hint.finding_player].add(hint)
|
self.hints[team, hint.finding_player].add(hint)
|
||||||
new_hint_events.add(hint.finding_player)
|
new_hint_events.add(hint.finding_player)
|
||||||
for player in self.slot_set(hint.receiving_player):
|
for player in self.slot_set(hint.receiving_player):
|
||||||
self.hints[team, player].add(hint)
|
self.hints[team, player].add(hint)
|
||||||
new_hint_events.add(player)
|
new_hint_events.add(player)
|
||||||
|
|
||||||
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
||||||
for slot in new_hint_events:
|
for slot in new_hint_events:
|
||||||
@@ -1960,10 +1960,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
def _cmd_exit(self) -> bool:
|
def _cmd_exit(self) -> bool:
|
||||||
"""Shutdown the server"""
|
"""Shutdown the server"""
|
||||||
try:
|
self.ctx.server.ws_server.close()
|
||||||
self.ctx.server.ws_server.close()
|
self.ctx.exit_event.set()
|
||||||
finally:
|
|
||||||
self.ctx.exit_event.set()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from dataclasses import dataclass
|
|||||||
from schema import And, Optional, Or, Schema
|
from schema import And, Optional, Or, Schema
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from Utils import get_file_safe_name, get_fuzzy_results, is_iterable_except_str, output_path
|
from Utils import get_fuzzy_results, is_iterable_except_str, output_path
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from BaseClasses import MultiWorld, PlandoOptions
|
from BaseClasses import MultiWorld, PlandoOptions
|
||||||
@@ -1531,7 +1531,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
|||||||
|
|
||||||
del file_data
|
del file_data
|
||||||
|
|
||||||
with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f:
|
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
|
||||||
f.write(res)
|
f.write(res)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
16
SNIClient.py
@@ -633,13 +633,7 @@ async def game_watcher(ctx: SNIContext) -> None:
|
|||||||
if not ctx.client_handler:
|
if not ctx.client_handler:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
rom_validated = await ctx.client_handler.validate_rom(ctx)
|
||||||
rom_validated = await ctx.client_handler.validate_rom(ctx)
|
|
||||||
except Exception as e:
|
|
||||||
snes_logger.error(f"An error occurred, see logs for details: {e}")
|
|
||||||
text_file_logger = logging.getLogger()
|
|
||||||
text_file_logger.exception(e)
|
|
||||||
rom_validated = False
|
|
||||||
|
|
||||||
if not rom_validated or (ctx.auth and ctx.auth != ctx.rom):
|
if not rom_validated or (ctx.auth and ctx.auth != ctx.rom):
|
||||||
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
|
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
|
||||||
@@ -655,13 +649,7 @@ async def game_watcher(ctx: SNIContext) -> None:
|
|||||||
|
|
||||||
perf_counter = time.perf_counter()
|
perf_counter = time.perf_counter()
|
||||||
|
|
||||||
try:
|
await ctx.client_handler.game_watcher(ctx)
|
||||||
await ctx.client_handler.game_watcher(ctx)
|
|
||||||
except Exception as e:
|
|
||||||
snes_logger.error(f"An error occurred, see logs for details: {e}")
|
|
||||||
text_file_logger = logging.getLogger()
|
|
||||||
text_file_logger.exception(e)
|
|
||||||
await snes_disconnect(ctx)
|
|
||||||
|
|
||||||
|
|
||||||
async def run_game(romfile: str) -> None:
|
async def run_game(romfile: str) -> None:
|
||||||
|
|||||||
49
Utils.py
@@ -18,7 +18,6 @@ import warnings
|
|||||||
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from settings import Settings, get_settings
|
from settings import Settings, get_settings
|
||||||
from time import sleep
|
|
||||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
||||||
from typing_extensions import TypeGuard
|
from typing_extensions import TypeGuard
|
||||||
from yaml import load, load_all, dump
|
from yaml import load, load_all, dump
|
||||||
@@ -32,7 +31,6 @@ if typing.TYPE_CHECKING:
|
|||||||
import tkinter
|
import tkinter
|
||||||
import pathlib
|
import pathlib
|
||||||
from BaseClasses import Region
|
from BaseClasses import Region
|
||||||
import multiprocessing
|
|
||||||
|
|
||||||
|
|
||||||
def tuplize_version(version: str) -> Version:
|
def tuplize_version(version: str) -> Version:
|
||||||
@@ -425,7 +423,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
|
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
|
||||||
return getattr(self.net_utils_module, name)
|
return getattr(self.net_utils_module, name)
|
||||||
# Options and Plando are unpickled by WebHost -> Generate
|
# Options and Plando are unpickled by WebHost -> Generate
|
||||||
if module == "worlds.generic" and name == "PlandoItem":
|
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
||||||
if not self.generic_properties_module:
|
if not self.generic_properties_module:
|
||||||
self.generic_properties_module = importlib.import_module("worlds.generic")
|
self.generic_properties_module = importlib.import_module("worlds.generic")
|
||||||
return getattr(self.generic_properties_module, name)
|
return getattr(self.generic_properties_module, name)
|
||||||
@@ -436,7 +434,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
else:
|
else:
|
||||||
mod = importlib.import_module(module)
|
mod = importlib.import_module(module)
|
||||||
obj = getattr(mod, name)
|
obj = getattr(mod, name)
|
||||||
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)):
|
if issubclass(obj, self.options_module.Option):
|
||||||
return obj
|
return obj
|
||||||
# Forbid everything else.
|
# Forbid everything else.
|
||||||
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
||||||
@@ -569,8 +567,6 @@ def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"):
|
|||||||
else:
|
else:
|
||||||
if text:
|
if text:
|
||||||
queue.put_nowait(text)
|
queue.put_nowait(text)
|
||||||
else:
|
|
||||||
sleep(0.01) # non-blocking stream
|
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
|
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
|
||||||
@@ -668,19 +664,6 @@ def get_input_text_from_response(text: str, command: str) -> typing.Optional[str
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def is_kivy_running() -> bool:
|
|
||||||
if "kivy" in sys.modules:
|
|
||||||
from kivy.app import App
|
|
||||||
return App.get_running_app() is not None
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _mp_open_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(open_filename(*args))
|
|
||||||
|
|
||||||
|
|
||||||
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
||||||
-> typing.Optional[str]:
|
-> typing.Optional[str]:
|
||||||
logging.info(f"Opening file input dialog for {title}.")
|
logging.info(f"Opening file input dialog for {title}.")
|
||||||
@@ -710,13 +693,6 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
|||||||
f'This attempt was made because open_filename was used for "{title}".')
|
f'This attempt was made because open_filename was used for "{title}".')
|
||||||
raise e
|
raise e
|
||||||
else:
|
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_open_filename, args=(res, title, filetypes, suggest)).start()
|
|
||||||
return res.get()
|
|
||||||
try:
|
try:
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
except tkinter.TclError:
|
except tkinter.TclError:
|
||||||
@@ -726,12 +702,6 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
|||||||
initialfile=suggest or None)
|
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")
|
|
||||||
res.put(open_directory(*args))
|
|
||||||
|
|
||||||
|
|
||||||
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||||
def run(*args: str):
|
def run(*args: str):
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||||
@@ -755,16 +725,9 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
|||||||
import tkinter.filedialog
|
import tkinter.filedialog
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('Could not load tkinter, which is likely not installed. '
|
logging.error('Could not load tkinter, which is likely not installed. '
|
||||||
f'This attempt was made because open_directory was used for "{title}".')
|
f'This attempt was made because open_filename was used for "{title}".')
|
||||||
raise e
|
raise e
|
||||||
else:
|
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_open_directory, args=(res, title, suggest)).start()
|
|
||||||
return res.get()
|
|
||||||
try:
|
try:
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
except tkinter.TclError:
|
except tkinter.TclError:
|
||||||
@@ -777,6 +740,12 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
|||||||
def run(*args: str):
|
def run(*args: str):
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||||
|
|
||||||
|
def is_kivy_running():
|
||||||
|
if "kivy" in sys.modules:
|
||||||
|
from kivy.app import App
|
||||||
|
return App.get_running_app() is not None
|
||||||
|
return False
|
||||||
|
|
||||||
if is_kivy_running():
|
if is_kivy_running():
|
||||||
from kvui import MessageBox
|
from kvui import MessageBox
|
||||||
MessageBox(title, text, error).open()
|
MessageBox(title, text, error).open()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ ModuleUpdate.update()
|
|||||||
# in case app gets imported by something like gunicorn
|
# in case app gets imported by something like gunicorn
|
||||||
import Utils
|
import Utils
|
||||||
import settings
|
import settings
|
||||||
from Utils import get_file_safe_name
|
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
@@ -72,7 +71,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
|||||||
shutil.rmtree(base_target_path, ignore_errors=True)
|
shutil.rmtree(base_target_path, ignore_errors=True)
|
||||||
for game, world in worlds.items():
|
for game, world in worlds.items():
|
||||||
# copy files from world's docs folder to the generated folder
|
# copy files from world's docs folder to the generated folder
|
||||||
target_path = os.path.join(base_target_path, get_file_safe_name(game))
|
target_path = os.path.join(base_target_path, game)
|
||||||
os.makedirs(target_path, exist_ok=True)
|
os.makedirs(target_path, exist_ok=True)
|
||||||
|
|
||||||
if world.zip_path:
|
if world.zip_path:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from flask_compress import Compress
|
|||||||
from pony.flask import Pony
|
from pony.flask import Pony
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
from Utils import title_sorted, get_file_safe_name
|
from Utils import title_sorted
|
||||||
|
|
||||||
UPLOAD_FOLDER = os.path.relpath('uploads')
|
UPLOAD_FOLDER = os.path.relpath('uploads')
|
||||||
LOGS_FOLDER = os.path.relpath('logs')
|
LOGS_FOLDER = os.path.relpath('logs')
|
||||||
@@ -20,7 +20,6 @@ Pony(app)
|
|||||||
|
|
||||||
app.jinja_env.filters['any'] = any
|
app.jinja_env.filters['any'] = any
|
||||||
app.jinja_env.filters['all'] = all
|
app.jinja_env.filters['all'] = all
|
||||||
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
|
|
||||||
|
|
||||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||||
|
|||||||
@@ -77,13 +77,7 @@ def faq(lang: str):
|
|||||||
return render_template(
|
return render_template(
|
||||||
"markdown_document.html",
|
"markdown_document.html",
|
||||||
title="Frequently Asked Questions",
|
title="Frequently Asked Questions",
|
||||||
html_from_markdown=markdown.markdown(
|
html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]),
|
||||||
document,
|
|
||||||
extensions=["toc", "mdx_breakless_lists"],
|
|
||||||
extension_configs={
|
|
||||||
"toc": {"anchorlink": True}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -96,13 +90,7 @@ def glossary(lang: str):
|
|||||||
return render_template(
|
return render_template(
|
||||||
"markdown_document.html",
|
"markdown_document.html",
|
||||||
title="Glossary",
|
title="Glossary",
|
||||||
html_from_markdown=markdown.markdown(
|
html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]),
|
||||||
document,
|
|
||||||
extensions=["toc", "mdx_breakless_lists"],
|
|
||||||
extension_configs={
|
|
||||||
"toc": {"anchorlink": True}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
flask>=3.0.3
|
flask>=3.0.3
|
||||||
werkzeug>=3.0.6
|
werkzeug>=3.0.4
|
||||||
pony>=0.7.19
|
pony>=0.7.19
|
||||||
waitress>=3.0.0
|
waitress>=3.0.0
|
||||||
Flask-Caching>=2.3.0
|
Flask-Caching>=2.3.0
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 512 B |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 166 B |
@@ -28,7 +28,7 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-family: LondrinaSolid-Regular, sans-serif;
|
font-family: LondrinaSolid-Regular, sans-serif;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer; /* TODO: remove once we drop showdown.js */
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-shadow: 1px 1px 4px #000000;
|
text-shadow: 1px 1px 4px #000000;
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
font-size: 38px;
|
font-size: 38px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-family: LondrinaSolid-Light, sans-serif;
|
font-family: LondrinaSolid-Light, sans-serif;
|
||||||
cursor: pointer; /* TODO: remove once we drop showdown.js */
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer; /* TODO: remove once we drop showdown.js */
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: pointer; /* TODO: remove once we drop showdown.js */
|
cursor: pointer;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,29 +67,20 @@
|
|||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
cursor: pointer; /* TODO: remove once we drop showdown.js */
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h6, .markdown details summary.h6{
|
.markdown h6, .markdown details summary.h6{
|
||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
cursor: pointer; /* TODO: remove once we drop showdown.js */
|
cursor: pointer;;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h4, .markdown h5, .markdown h6{
|
.markdown h4, .markdown h5, .markdown h6{
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown h1 > a,
|
|
||||||
.markdown h2 > a,
|
|
||||||
.markdown h3 > a,
|
|
||||||
.markdown h4 > a,
|
|
||||||
.markdown h5 > a,
|
|
||||||
.markdown h6 > a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown ul{
|
.markdown ul{
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/'+theme+'Header.html' %}
|
{% include 'header/'+theme+'Header.html' %}
|
||||||
<div id="game-info" class="markdown" data-lang="{{ lang }}" data-game="{{ game | get_file_safe_name }}">
|
<div id="game-info" class="markdown" data-lang="{{ lang }}" data-game="{{ game }}">
|
||||||
<!-- Populated my JS / MD -->
|
<!-- Populated my JS / MD -->
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -98,8 +98,6 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if hint.finding_player == player %}
|
{% if hint.finding_player == player %}
|
||||||
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
||||||
{% elif get_slot_info(team, hint.finding_player).type == 2 %}
|
|
||||||
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
|
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
|
||||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||||
@@ -109,8 +107,6 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if hint.receiving_player == player %}
|
{% if hint.receiving_player == player %}
|
||||||
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
||||||
{% elif get_slot_info(team, hint.receiving_player).type == 2 %}
|
|
||||||
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
|
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
|
||||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||||
|
|||||||
@@ -21,20 +21,8 @@
|
|||||||
)
|
)
|
||||||
-%}
|
-%}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>{{ player_names_with_alias[(team, hint.finding_player)] }}</td>
|
||||||
{% if get_slot_info(team, hint.finding_player).type == 2 %}
|
<td>{{ player_names_with_alias[(team, hint.receiving_player)] }}</td>
|
||||||
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
|
|
||||||
{% else %}
|
|
||||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if get_slot_info(team, hint.receiving_player).type == 2 %}
|
|
||||||
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
|
|
||||||
{% else %}
|
|
||||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||||
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
|
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
|
||||||
<td>{{ games[(team, hint.finding_player)] }}</td>
|
<td>{{ games[(team, hint.finding_player)] }}</td>
|
||||||
|
|||||||
@@ -196,14 +196,13 @@
|
|||||||
{% macro OptionTitle(option_name, option) %}
|
{% macro OptionTitle(option_name, option) %}
|
||||||
<label for="{{ option_name }}">
|
<label for="{{ option_name }}">
|
||||||
{{ option.display_name|default(option_name) }}:
|
{{ option.display_name|default(option_name) }}:
|
||||||
{% set rich_text = option.rich_text_doc or (option.rich_text_doc is none and world.web.rich_text_options_doc) %}
|
|
||||||
<span
|
<span
|
||||||
class="interactive tooltip-container"
|
class="interactive tooltip-container"
|
||||||
{% if not rich_text %}
|
{% if not (option.rich_text_doc | default(world.web.rich_text_options_doc, true)) %}
|
||||||
data-tooltip="{{(option.__doc__ | default("Please document me!"))|replace('\n ', '\n')|escape|trim}}"
|
data-tooltip="{{(option.__doc__ | default("Please document me!"))|replace('\n ', '\n')|escape|trim}}"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
(?)
|
(?)
|
||||||
{% if rich_text %}
|
{% if option.rich_text_doc | default(world.web.rich_text_options_doc, true) %}
|
||||||
<div class="tooltip">
|
<div class="tooltip">
|
||||||
{{ option.__doc__ | default("**Please document me!**") | rst_to_html | safe }}
|
{{ option.__doc__ | default("**Please document me!**") | rst_to_html | safe }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||||
<br />
|
<br />
|
||||||
You may also download the
|
You may also download the
|
||||||
<a href="/static/generated/configs/{{ world_name | get_file_safe_name }}.yaml">template file for this game</a>.
|
<a href="/static/generated/configs/{{ world_name }}.yaml">template file for this game</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form id="options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-yaml">
|
<form id="options-form" method="post" enctype="application/x-www-form-urlencoded" action="generate-yaml">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div id="tutorial-wrapper" class="markdown" data-game="{{ game | get_file_safe_name }}" data-file="{{ file | get_file_safe_name }}" data-lang="{{ lang }}">
|
<div id="tutorial-wrapper" class="markdown" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
|
||||||
<!-- Content generated by JavaScript -->
|
<!-- Content generated by JavaScript -->
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, NamedTuple,
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from email.utils import parsedate_to_datetime
|
from email.utils import parsedate_to_datetime
|
||||||
|
|
||||||
from flask import make_response, render_template, request, Request, Response
|
from flask import render_template, make_response, Response, request
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
from MultiServer import Context, get_saving_second
|
from MultiServer import Context, get_saving_second
|
||||||
@@ -298,25 +298,17 @@ class TrackerData:
|
|||||||
return self._multidata.get("spheres", [])
|
return self._multidata.get("spheres", [])
|
||||||
|
|
||||||
|
|
||||||
def _process_if_request_valid(incoming_request: Request, room: Optional[Room]) -> Optional[Response]:
|
def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]:
|
||||||
if not room:
|
if not room:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if_modified_str: Optional[str] = incoming_request.headers.get("If-Modified-Since", None)
|
if_modified = incoming_request.headers.get("If-Modified-Since", None)
|
||||||
if if_modified_str:
|
if if_modified:
|
||||||
if_modified = parsedate_to_datetime(if_modified_str)
|
if_modified = parsedate_to_datetime(if_modified)
|
||||||
if if_modified.tzinfo is None:
|
|
||||||
abort(400) # standard requires "GMT" timezone
|
|
||||||
# database may use datetime.utcnow(), which is timezone-naive. convert to timezone-aware.
|
|
||||||
last_activity = room.last_activity
|
|
||||||
if last_activity.tzinfo is None:
|
|
||||||
last_activity = room.last_activity.replace(tzinfo=datetime.timezone.utc)
|
|
||||||
# if_modified has less precision than last_activity, so we bring them to same precision
|
# if_modified has less precision than last_activity, so we bring them to same precision
|
||||||
if if_modified >= last_activity.replace(microsecond=0):
|
if if_modified >= room.last_activity.replace(microsecond=0):
|
||||||
return make_response("", 304)
|
return make_response("", 304)
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>")
|
@app.route("/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>")
|
||||||
def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response:
|
def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response:
|
||||||
@@ -423,7 +415,6 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) ->
|
|||||||
template_name_or_list="genericTracker.html",
|
template_name_or_list="genericTracker.html",
|
||||||
game_specific_tracker=game in _player_trackers,
|
game_specific_tracker=game in _player_trackers,
|
||||||
room=tracker_data.room,
|
room=tracker_data.room,
|
||||||
get_slot_info=tracker_data.get_slot_info,
|
|
||||||
team=team,
|
team=team,
|
||||||
player=player,
|
player=player,
|
||||||
player_name=tracker_data.get_room_long_player_names()[team, player],
|
player_name=tracker_data.get_room_long_player_names()[team, player],
|
||||||
@@ -447,7 +438,6 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
|
|||||||
enabled_trackers=enabled_trackers,
|
enabled_trackers=enabled_trackers,
|
||||||
current_tracker="Generic",
|
current_tracker="Generic",
|
||||||
room=tracker_data.room,
|
room=tracker_data.room,
|
||||||
get_slot_info=tracker_data.get_slot_info,
|
|
||||||
all_slots=tracker_data.get_all_slots(),
|
all_slots=tracker_data.get_all_slots(),
|
||||||
room_players=tracker_data.get_all_players(),
|
room_players=tracker_data.get_all_players(),
|
||||||
locations=tracker_data.get_room_locations(),
|
locations=tracker_data.get_room_locations(),
|
||||||
@@ -499,7 +489,7 @@ if "Factorio" in network_data_package["games"]:
|
|||||||
(team, player): collections.Counter({
|
(team, player): collections.Counter({
|
||||||
tracker_data.item_id_to_name["Factorio"][item_id]: count
|
tracker_data.item_id_to_name["Factorio"][item_id]: count
|
||||||
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
|
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
|
||||||
}) for team, players in tracker_data.get_all_players().items() for player in players
|
}) for team, players in tracker_data.get_all_slots().items() for player in players
|
||||||
if tracker_data.get_player_game(team, player) == "Factorio"
|
if tracker_data.get_player_game(team, player) == "Factorio"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +498,6 @@ if "Factorio" in network_data_package["games"]:
|
|||||||
enabled_trackers=enabled_trackers,
|
enabled_trackers=enabled_trackers,
|
||||||
current_tracker="Factorio",
|
current_tracker="Factorio",
|
||||||
room=tracker_data.room,
|
room=tracker_data.room,
|
||||||
get_slot_info=tracker_data.get_slot_info,
|
|
||||||
all_slots=tracker_data.get_all_slots(),
|
all_slots=tracker_data.get_all_slots(),
|
||||||
room_players=tracker_data.get_all_players(),
|
room_players=tracker_data.get_all_players(),
|
||||||
locations=tracker_data.get_room_locations(),
|
locations=tracker_data.get_room_locations(),
|
||||||
@@ -641,7 +630,6 @@ if "A Link to the Past" in network_data_package["games"]:
|
|||||||
enabled_trackers=enabled_trackers,
|
enabled_trackers=enabled_trackers,
|
||||||
current_tracker="A Link to the Past",
|
current_tracker="A Link to the Past",
|
||||||
room=tracker_data.room,
|
room=tracker_data.room,
|
||||||
get_slot_info=tracker_data.get_slot_info,
|
|
||||||
all_slots=tracker_data.get_all_slots(),
|
all_slots=tracker_data.get_all_slots(),
|
||||||
room_players=tracker_data.get_all_players(),
|
room_players=tracker_data.get_all_players(),
|
||||||
locations=tracker_data.get_room_locations(),
|
locations=tracker_data.get_room_locations(),
|
||||||
|
|||||||
@@ -28,9 +28,9 @@
|
|||||||
name: Player{number}
|
name: Player{number}
|
||||||
|
|
||||||
# Used to describe your yaml. Useful if you have multiple files.
|
# Used to describe your yaml. Useful if you have multiple files.
|
||||||
description: {{ yaml_dump("Default %s Template" % game) }}
|
description: Default {{ game }} Template
|
||||||
|
|
||||||
game: {{ yaml_dump(game) }}
|
game: {{ game }}
|
||||||
requires:
|
requires:
|
||||||
version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected.
|
version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected.
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ requires:
|
|||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{{ yaml_dump(game) }}:
|
{{ game }}:
|
||||||
{%- for group_name, group_options in option_groups.items() %}
|
{%- for group_name, group_options in option_groups.items() %}
|
||||||
# {{ group_name }}
|
# {{ group_name }}
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,7 @@
|
|||||||
/worlds/shivers/ @GodlFire
|
/worlds/shivers/ @GodlFire
|
||||||
|
|
||||||
# A Short Hike
|
# A Short Hike
|
||||||
/worlds/shorthike/ @chandler05 @BrandenEK
|
/worlds/shorthike/ @chandler05
|
||||||
|
|
||||||
# Sonic Adventure 2 Battle
|
# Sonic Adventure 2 Battle
|
||||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||||
|
|||||||
@@ -85,4 +85,4 @@ PyCharm has a built-in version control integration that supports Git.
|
|||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
Information about running tests can be found in [tests.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/tests.md#running-tests)
|
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
||||||
|
|||||||
@@ -84,19 +84,7 @@ testing portions of your code that can be tested without relying on a multiworld
|
|||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
#### Using Pycharm
|
In PyCharm, running all tests can be done by right-clicking the root `test` directory and selecting `run Python tests`.
|
||||||
|
If you do not have pytest installed, you may get import failures. To solve this, edit the run configuration, and set the
|
||||||
In PyCharm, running all tests can be done by right-clicking the root test directory and selecting Run 'Archipelago Unittests'.
|
working directory of the run to the Archipelago directory. If you only want to run your world's defined tests, repeat
|
||||||
Unless you configured PyCharm to use pytest as a test runner, you may get import failures. To solve this, edit the run configuration,
|
the steps for the test directory within your world.
|
||||||
and set the working directory to the Archipelago directory which contains all the project files.
|
|
||||||
|
|
||||||
If you only want to run your world's defined tests, repeat the steps for the test directory within your world.
|
|
||||||
Your working directory should be the directory of your world in the worlds directory and the script should be the
|
|
||||||
tests folder within your world.
|
|
||||||
|
|
||||||
You can also find the 'Archipelago Unittests' as an option in the dropdown at the top of the window
|
|
||||||
next to the run and debug buttons.
|
|
||||||
|
|
||||||
#### Running Tests without Pycharm
|
|
||||||
|
|
||||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
colorama>=0.4.6
|
colorama>=0.4.6
|
||||||
websockets>=13.0.1,<14
|
websockets>=13.0.1
|
||||||
PyYAML>=6.0.2
|
PyYAML>=6.0.2
|
||||||
jellyfish>=1.1.0
|
jellyfish>=1.1.0
|
||||||
jinja2>=3.1.4
|
jinja2>=3.1.4
|
||||||
|
|||||||
108
setup.py
@@ -5,6 +5,7 @@ import platform
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
|
import typing
|
||||||
import warnings
|
import warnings
|
||||||
import zipfile
|
import zipfile
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -13,14 +14,14 @@ import json
|
|||||||
import threading
|
import threading
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
from hashlib import sha3_512
|
from hashlib import sha3_512
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
|
|
||||||
|
|
||||||
|
|
||||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||||
requirement = 'cx-Freeze==7.2.0'
|
|
||||||
try:
|
try:
|
||||||
|
requirement = 'cx-Freeze==7.2.0'
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
try:
|
try:
|
||||||
pkg_resources.require(requirement)
|
pkg_resources.require(requirement)
|
||||||
@@ -29,7 +30,7 @@ try:
|
|||||||
install_cx_freeze = True
|
install_cx_freeze = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
install_cx_freeze = True
|
install_cx_freeze = True
|
||||||
pkg_resources = None # type: ignore[assignment]
|
pkg_resources = None # type: ignore [assignment]
|
||||||
|
|
||||||
if install_cx_freeze:
|
if install_cx_freeze:
|
||||||
# check if pip is available
|
# check if pip is available
|
||||||
@@ -60,7 +61,7 @@ from Cython.Build import cythonize
|
|||||||
|
|
||||||
|
|
||||||
# On Python < 3.10 LogicMixin is not currently supported.
|
# On Python < 3.10 LogicMixin is not currently supported.
|
||||||
non_apworlds: Set[str] = {
|
non_apworlds: set = {
|
||||||
"A Link to the Past",
|
"A Link to the Past",
|
||||||
"Adventure",
|
"Adventure",
|
||||||
"ArchipIDLE",
|
"ArchipIDLE",
|
||||||
@@ -83,7 +84,7 @@ non_apworlds: Set[str] = {
|
|||||||
if sys.version_info < (3,10):
|
if sys.version_info < (3,10):
|
||||||
non_apworlds.add("Hollow Knight")
|
non_apworlds.add("Hollow Knight")
|
||||||
|
|
||||||
def download_SNI() -> None:
|
def download_SNI():
|
||||||
print("Updating SNI")
|
print("Updating SNI")
|
||||||
machine_to_go = {
|
machine_to_go = {
|
||||||
"x86_64": "amd64",
|
"x86_64": "amd64",
|
||||||
@@ -93,7 +94,7 @@ def download_SNI() -> None:
|
|||||||
platform_name = platform.system().lower()
|
platform_name = platform.system().lower()
|
||||||
machine_name = platform.machine().lower()
|
machine_name = platform.machine().lower()
|
||||||
# force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH
|
# force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH
|
||||||
machine_name = "universal" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name)
|
machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name)
|
||||||
with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request:
|
with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request:
|
||||||
data = json.load(request)
|
data = json.load(request)
|
||||||
files = data["assets"]
|
files = data["assets"]
|
||||||
@@ -104,19 +105,17 @@ def download_SNI() -> None:
|
|||||||
download_url: str = file["browser_download_url"]
|
download_url: str = file["browser_download_url"]
|
||||||
machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name
|
machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name
|
||||||
if platform_name in download_url and machine_match:
|
if platform_name in download_url and machine_match:
|
||||||
source_url = download_url
|
|
||||||
# prefer "many" builds
|
# prefer "many" builds
|
||||||
if "many" in download_url:
|
if "many" in download_url:
|
||||||
|
source_url = download_url
|
||||||
break
|
break
|
||||||
# prefer the correct windows or windows7 build
|
source_url = download_url
|
||||||
if platform_name == "windows" and ("windows7" in download_url) == (sys.version_info < (3, 9)):
|
|
||||||
break
|
|
||||||
|
|
||||||
if source_url and source_url.endswith(".zip"):
|
if source_url and source_url.endswith(".zip"):
|
||||||
with urllib.request.urlopen(source_url) as download:
|
with urllib.request.urlopen(source_url) as download:
|
||||||
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
|
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
|
||||||
for zf_member in zf.infolist():
|
for member in zf.infolist():
|
||||||
zf.extract(zf_member, path="SNI")
|
zf.extract(member, path="SNI")
|
||||||
print(f"Downloaded SNI from {source_url}")
|
print(f"Downloaded SNI from {source_url}")
|
||||||
|
|
||||||
elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
|
elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
|
||||||
@@ -130,13 +129,11 @@ def download_SNI() -> None:
|
|||||||
raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
|
raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
|
||||||
elif member.isdir() and not sni_dir:
|
elif member.isdir() and not sni_dir:
|
||||||
sni_dir = member.name
|
sni_dir = member.name
|
||||||
elif member.isfile() and not sni_dir or sni_dir and not member.name.startswith(sni_dir):
|
elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir):
|
||||||
raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
|
raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
|
||||||
elif member.isfile() and sni_dir:
|
elif member.isfile() and sni_dir:
|
||||||
tf.extract(member)
|
tf.extract(member)
|
||||||
# sadly SNI is in its own folder on non-windows, so we need to rename
|
# sadly SNI is in its own folder on non-windows, so we need to rename
|
||||||
if not sni_dir:
|
|
||||||
raise ValueError("Did not find SNI in archive")
|
|
||||||
shutil.rmtree("SNI", True)
|
shutil.rmtree("SNI", True)
|
||||||
os.rename(sni_dir, "SNI")
|
os.rename(sni_dir, "SNI")
|
||||||
print(f"Downloaded SNI from {source_url}")
|
print(f"Downloaded SNI from {source_url}")
|
||||||
@@ -148,7 +145,7 @@ def download_SNI() -> None:
|
|||||||
print(f"No SNI found for system spec {platform_name} {machine_name}")
|
print(f"No SNI found for system spec {platform_name} {machine_name}")
|
||||||
|
|
||||||
|
|
||||||
signtool: Optional[str]
|
signtool: typing.Optional[str]
|
||||||
if os.path.exists("X:/pw.txt"):
|
if os.path.exists("X:/pw.txt"):
|
||||||
print("Using signtool")
|
print("Using signtool")
|
||||||
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
||||||
@@ -200,13 +197,13 @@ extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
|
|||||||
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
||||||
|
|
||||||
|
|
||||||
def remove_sprites_from_folder(folder: Path) -> None:
|
def remove_sprites_from_folder(folder):
|
||||||
for file in os.listdir(folder):
|
for file in os.listdir(folder):
|
||||||
if file != ".gitignore":
|
if file != ".gitignore":
|
||||||
os.remove(folder / file)
|
os.remove(folder / file)
|
||||||
|
|
||||||
|
|
||||||
def _threaded_hash(filepath: Union[str, Path]) -> str:
|
def _threaded_hash(filepath):
|
||||||
hasher = sha3_512()
|
hasher = sha3_512()
|
||||||
hasher.update(open(filepath, "rb").read())
|
hasher.update(open(filepath, "rb").read())
|
||||||
return base64.b85encode(hasher.digest()).decode()
|
return base64.b85encode(hasher.digest()).decode()
|
||||||
@@ -220,11 +217,11 @@ class BuildCommand(setuptools.command.build.build):
|
|||||||
yes: bool
|
yes: bool
|
||||||
last_yes: bool = False # used by sub commands of build
|
last_yes: bool = False # used by sub commands of build
|
||||||
|
|
||||||
def initialize_options(self) -> None:
|
def initialize_options(self):
|
||||||
super().initialize_options()
|
super().initialize_options()
|
||||||
type(self).last_yes = self.yes = False
|
type(self).last_yes = self.yes = False
|
||||||
|
|
||||||
def finalize_options(self) -> None:
|
def finalize_options(self):
|
||||||
super().finalize_options()
|
super().finalize_options()
|
||||||
type(self).last_yes = self.yes
|
type(self).last_yes = self.yes
|
||||||
|
|
||||||
@@ -236,27 +233,27 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
|||||||
('extra-data=', None, 'Additional files to add.'),
|
('extra-data=', None, 'Additional files to add.'),
|
||||||
]
|
]
|
||||||
yes: bool
|
yes: bool
|
||||||
extra_data: Iterable[str]
|
extra_data: Iterable # [any] not available in 3.8
|
||||||
extra_libs: Iterable[str] # work around broken include_files
|
extra_libs: Iterable # work around broken include_files
|
||||||
|
|
||||||
buildfolder: Path
|
buildfolder: Path
|
||||||
libfolder: Path
|
libfolder: Path
|
||||||
library: Path
|
library: Path
|
||||||
buildtime: datetime.datetime
|
buildtime: datetime.datetime
|
||||||
|
|
||||||
def initialize_options(self) -> None:
|
def initialize_options(self):
|
||||||
super().initialize_options()
|
super().initialize_options()
|
||||||
self.yes = BuildCommand.last_yes
|
self.yes = BuildCommand.last_yes
|
||||||
self.extra_data = []
|
self.extra_data = []
|
||||||
self.extra_libs = []
|
self.extra_libs = []
|
||||||
|
|
||||||
def finalize_options(self) -> None:
|
def finalize_options(self):
|
||||||
super().finalize_options()
|
super().finalize_options()
|
||||||
self.buildfolder = self.build_exe
|
self.buildfolder = self.build_exe
|
||||||
self.libfolder = Path(self.buildfolder, "lib")
|
self.libfolder = Path(self.buildfolder, "lib")
|
||||||
self.library = Path(self.libfolder, "library.zip")
|
self.library = Path(self.libfolder, "library.zip")
|
||||||
|
|
||||||
def installfile(self, path: Path, subpath: Optional[Union[str, Path]] = None, keep_content: bool = False) -> None:
|
def installfile(self, path, subpath=None, keep_content: bool = False):
|
||||||
folder = self.buildfolder
|
folder = self.buildfolder
|
||||||
if subpath:
|
if subpath:
|
||||||
folder /= subpath
|
folder /= subpath
|
||||||
@@ -271,7 +268,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
|||||||
else:
|
else:
|
||||||
print('Warning,', path, 'not found')
|
print('Warning,', path, 'not found')
|
||||||
|
|
||||||
def create_manifest(self, create_hashes: bool = False) -> None:
|
def create_manifest(self, create_hashes=False):
|
||||||
# Since the setup is now split into components and the manifest is not,
|
# Since the setup is now split into components and the manifest is not,
|
||||||
# it makes most sense to just remove the hashes for now. Not aware of anyone using them.
|
# it makes most sense to just remove the hashes for now. Not aware of anyone using them.
|
||||||
hashes = {}
|
hashes = {}
|
||||||
@@ -293,7 +290,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
|||||||
json.dump(manifest, open(manifestpath, "wt"), indent=4)
|
json.dump(manifest, open(manifestpath, "wt"), indent=4)
|
||||||
print("Created Manifest")
|
print("Created Manifest")
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self):
|
||||||
# start downloading sni asap
|
# start downloading sni asap
|
||||||
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
|
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
|
||||||
sni_thread.start()
|
sni_thread.start()
|
||||||
@@ -344,7 +341,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
|||||||
|
|
||||||
# post build steps
|
# post build steps
|
||||||
if is_windows: # kivy_deps is win32 only, linux picks them up automatically
|
if is_windows: # kivy_deps is win32 only, linux picks them up automatically
|
||||||
from kivy_deps import sdl2, glew # type: ignore
|
from kivy_deps import sdl2, glew
|
||||||
for folder in sdl2.dep_bins + glew.dep_bins:
|
for folder in sdl2.dep_bins + glew.dep_bins:
|
||||||
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
|
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
|
||||||
print(f"copying {folder} -> {self.libfolder}")
|
print(f"copying {folder} -> {self.libfolder}")
|
||||||
@@ -365,7 +362,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
|||||||
self.installfile(Path(data))
|
self.installfile(Path(data))
|
||||||
|
|
||||||
# kivi data files
|
# kivi data files
|
||||||
import kivy # type: ignore[import-untyped]
|
import kivy
|
||||||
shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"),
|
shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"),
|
||||||
self.buildfolder / "data",
|
self.buildfolder / "data",
|
||||||
dirs_exist_ok=True)
|
dirs_exist_ok=True)
|
||||||
@@ -375,7 +372,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
|||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
assert not non_apworlds - set(AutoWorldRegister.world_types), \
|
assert not non_apworlds - set(AutoWorldRegister.world_types), \
|
||||||
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
|
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
|
||||||
folders_to_remove: List[str] = []
|
folders_to_remove: typing.List[str] = []
|
||||||
disabled_worlds_folder = "worlds_disabled"
|
disabled_worlds_folder = "worlds_disabled"
|
||||||
for entry in os.listdir(disabled_worlds_folder):
|
for entry in os.listdir(disabled_worlds_folder):
|
||||||
if os.path.isdir(os.path.join(disabled_worlds_folder, entry)):
|
if os.path.isdir(os.path.join(disabled_worlds_folder, entry)):
|
||||||
@@ -396,7 +393,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
|||||||
shutil.rmtree(world_directory)
|
shutil.rmtree(world_directory)
|
||||||
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
|
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
|
||||||
try:
|
try:
|
||||||
from maseya import z3pr # type: ignore[import-untyped]
|
from maseya import z3pr
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Maseya Palette Shuffle not found, skipping data files.")
|
print("Maseya Palette Shuffle not found, skipping data files.")
|
||||||
else:
|
else:
|
||||||
@@ -447,16 +444,16 @@ class AppImageCommand(setuptools.Command):
|
|||||||
("app-exec=", None, "The application to run inside the image."),
|
("app-exec=", None, "The application to run inside the image."),
|
||||||
("yes", "y", 'Answer "yes" to all questions.'),
|
("yes", "y", 'Answer "yes" to all questions.'),
|
||||||
]
|
]
|
||||||
build_folder: Optional[Path]
|
build_folder: typing.Optional[Path]
|
||||||
dist_file: Optional[Path]
|
dist_file: typing.Optional[Path]
|
||||||
app_dir: Optional[Path]
|
app_dir: typing.Optional[Path]
|
||||||
app_name: str
|
app_name: str
|
||||||
app_exec: Optional[Path]
|
app_exec: typing.Optional[Path]
|
||||||
app_icon: Optional[Path] # source file
|
app_icon: typing.Optional[Path] # source file
|
||||||
app_id: str # lower case name, used for icon and .desktop
|
app_id: str # lower case name, used for icon and .desktop
|
||||||
yes: bool
|
yes: bool
|
||||||
|
|
||||||
def write_desktop(self) -> None:
|
def write_desktop(self):
|
||||||
assert self.app_dir, "Invalid app_dir"
|
assert self.app_dir, "Invalid app_dir"
|
||||||
desktop_filename = self.app_dir / f"{self.app_id}.desktop"
|
desktop_filename = self.app_dir / f"{self.app_id}.desktop"
|
||||||
with open(desktop_filename, 'w', encoding="utf-8") as f:
|
with open(desktop_filename, 'w', encoding="utf-8") as f:
|
||||||
@@ -471,7 +468,7 @@ class AppImageCommand(setuptools.Command):
|
|||||||
)))
|
)))
|
||||||
desktop_filename.chmod(0o755)
|
desktop_filename.chmod(0o755)
|
||||||
|
|
||||||
def write_launcher(self, default_exe: Path) -> None:
|
def write_launcher(self, default_exe: Path):
|
||||||
assert self.app_dir, "Invalid app_dir"
|
assert self.app_dir, "Invalid app_dir"
|
||||||
launcher_filename = self.app_dir / "AppRun"
|
launcher_filename = self.app_dir / "AppRun"
|
||||||
with open(launcher_filename, 'w', encoding="utf-8") as f:
|
with open(launcher_filename, 'w', encoding="utf-8") as f:
|
||||||
@@ -494,7 +491,7 @@ $APPDIR/$exe "$@"
|
|||||||
""")
|
""")
|
||||||
launcher_filename.chmod(0o755)
|
launcher_filename.chmod(0o755)
|
||||||
|
|
||||||
def install_icon(self, src: Path, name: Optional[str] = None, symlink: Optional[Path] = None) -> None:
|
def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None):
|
||||||
assert self.app_dir, "Invalid app_dir"
|
assert self.app_dir, "Invalid app_dir"
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -516,8 +513,7 @@ $APPDIR/$exe "$@"
|
|||||||
if symlink:
|
if symlink:
|
||||||
symlink.symlink_to(dest_file.relative_to(symlink.parent))
|
symlink.symlink_to(dest_file.relative_to(symlink.parent))
|
||||||
|
|
||||||
def initialize_options(self) -> None:
|
def initialize_options(self):
|
||||||
assert self.distribution.metadata.name
|
|
||||||
self.build_folder = None
|
self.build_folder = None
|
||||||
self.app_dir = None
|
self.app_dir = None
|
||||||
self.app_name = self.distribution.metadata.name
|
self.app_name = self.distribution.metadata.name
|
||||||
@@ -531,22 +527,17 @@ $APPDIR/$exe "$@"
|
|||||||
))
|
))
|
||||||
self.yes = False
|
self.yes = False
|
||||||
|
|
||||||
def finalize_options(self) -> None:
|
def finalize_options(self):
|
||||||
assert self.build_folder
|
|
||||||
if not self.app_dir:
|
if not self.app_dir:
|
||||||
self.app_dir = self.build_folder.parent / "AppDir"
|
self.app_dir = self.build_folder.parent / "AppDir"
|
||||||
self.app_id = self.app_name.lower()
|
self.app_id = self.app_name.lower()
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self):
|
||||||
assert self.build_folder and self.dist_file, "Command not properly set up"
|
|
||||||
assert (
|
|
||||||
self.app_icon and self.app_id and self.app_dir and self.app_exec and self.app_name
|
|
||||||
), "AppImageCommand not properly set up"
|
|
||||||
self.dist_file.parent.mkdir(parents=True, exist_ok=True)
|
self.dist_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if self.app_dir.is_dir():
|
if self.app_dir.is_dir():
|
||||||
shutil.rmtree(self.app_dir)
|
shutil.rmtree(self.app_dir)
|
||||||
self.app_dir.mkdir(parents=True)
|
self.app_dir.mkdir(parents=True)
|
||||||
opt_dir = self.app_dir / "opt" / self.app_name
|
opt_dir = self.app_dir / "opt" / self.distribution.metadata.name
|
||||||
shutil.copytree(self.build_folder, opt_dir)
|
shutil.copytree(self.build_folder, opt_dir)
|
||||||
root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}'
|
root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}'
|
||||||
self.install_icon(self.app_icon, self.app_id, symlink=root_icon)
|
self.install_icon(self.app_icon, self.app_id, symlink=root_icon)
|
||||||
@@ -557,7 +548,7 @@ $APPDIR/$exe "$@"
|
|||||||
subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True)
|
subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True)
|
||||||
|
|
||||||
|
|
||||||
def find_libs(*args: str) -> Sequence[Tuple[str, str]]:
|
def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
|
||||||
"""Try to find system libraries to be included."""
|
"""Try to find system libraries to be included."""
|
||||||
if not args:
|
if not args:
|
||||||
return []
|
return []
|
||||||
@@ -565,7 +556,7 @@ def find_libs(*args: str) -> Sequence[Tuple[str, str]]:
|
|||||||
arch = build_arch.replace('_', '-')
|
arch = build_arch.replace('_', '-')
|
||||||
libc = 'libc6' # we currently don't support musl
|
libc = 'libc6' # we currently don't support musl
|
||||||
|
|
||||||
def parse(line: str) -> Tuple[Tuple[str, str, str], str]:
|
def parse(line):
|
||||||
lib, path = line.strip().split(' => ')
|
lib, path = line.strip().split(' => ')
|
||||||
lib, typ = lib.split(' ', 1)
|
lib, typ = lib.split(' ', 1)
|
||||||
for test_arch in ('x86-64', 'i386', 'aarch64'):
|
for test_arch in ('x86-64', 'i386', 'aarch64'):
|
||||||
@@ -586,29 +577,26 @@ def find_libs(*args: str) -> Sequence[Tuple[str, str]]:
|
|||||||
ldconfig = shutil.which("ldconfig")
|
ldconfig = shutil.which("ldconfig")
|
||||||
assert ldconfig, "Make sure ldconfig is in PATH"
|
assert ldconfig, "Make sure ldconfig is in PATH"
|
||||||
data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:]
|
data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:]
|
||||||
find_libs.cache = { # type: ignore[attr-defined]
|
find_libs.cache = { # type: ignore [attr-defined]
|
||||||
k: v for k, v in (parse(line) for line in data if "=>" in line)
|
k: v for k, v in (parse(line) for line in data if "=>" in line)
|
||||||
}
|
}
|
||||||
|
|
||||||
def find_lib(lib: str, arch: str, libc: str) -> Optional[str]:
|
def find_lib(lib, arch, libc):
|
||||||
cache: Dict[Tuple[str, str, str], str] = getattr(find_libs, "cache")
|
for k, v in find_libs.cache.items():
|
||||||
for k, v in cache.items():
|
|
||||||
if k == (lib, arch, libc):
|
if k == (lib, arch, libc):
|
||||||
return v
|
return v
|
||||||
for k, v, in cache.items():
|
for k, v, in find_libs.cache.items():
|
||||||
if k[0].startswith(lib) and k[1] == arch and k[2] == libc:
|
if k[0].startswith(lib) and k[1] == arch and k[2] == libc:
|
||||||
return v
|
return v
|
||||||
return None
|
return None
|
||||||
|
|
||||||
res: List[Tuple[str, str]] = []
|
res = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
# try exact match, empty libc, empty arch, empty arch and libc
|
# try exact match, empty libc, empty arch, empty arch and libc
|
||||||
file = find_lib(arg, arch, libc)
|
file = find_lib(arg, arch, libc)
|
||||||
file = file or find_lib(arg, arch, '')
|
file = file or find_lib(arg, arch, '')
|
||||||
file = file or find_lib(arg, '', libc)
|
file = file or find_lib(arg, '', libc)
|
||||||
file = file or find_lib(arg, '', '')
|
file = file or find_lib(arg, '', '')
|
||||||
if not file:
|
|
||||||
raise ValueError(f"Could not find lib {arg}")
|
|
||||||
# resolve symlinks
|
# resolve symlinks
|
||||||
for n in range(0, 5):
|
for n in range(0, 5):
|
||||||
res.append((file, os.path.join('lib', os.path.basename(file))))
|
res.append((file, os.path.join('lib', os.path.basename(file))))
|
||||||
@@ -632,7 +620,7 @@ cx_Freeze.setup(
|
|||||||
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
||||||
"includes": [],
|
"includes": [],
|
||||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||||
"pandas", "zstandard"],
|
"pandas"],
|
||||||
"zip_include_packages": ["*"],
|
"zip_include_packages": ["*"],
|
||||||
"zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support
|
"zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support
|
||||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import warnings
|
|||||||
import settings
|
import settings
|
||||||
|
|
||||||
warnings.simplefilter("always")
|
warnings.simplefilter("always")
|
||||||
warnings.filterwarnings(action="ignore", category=DeprecationWarning, module="s2clientprotocol")
|
|
||||||
settings.no_gui = True
|
settings.no_gui = True
|
||||||
settings.skip_autosave = True
|
settings.skip_autosave = True
|
||||||
|
|
||||||
|
|||||||
@@ -688,8 +688,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
|||||||
for item in multiworld.get_items():
|
for item in multiworld.get_items():
|
||||||
item.classification = ItemClassification.useful
|
item.classification = ItemClassification.useful
|
||||||
|
|
||||||
multiworld.worlds[player1.id].options.local_items.value = set(names(player1.basic_items))
|
multiworld.local_items[player1.id].value = set(names(player1.basic_items))
|
||||||
multiworld.worlds[player2.id].options.local_items.value = set(names(player2.basic_items))
|
multiworld.local_items[player2.id].value = set(names(player2.basic_items))
|
||||||
locality_rules(multiworld)
|
locality_rules(multiworld)
|
||||||
|
|
||||||
distribute_items_restrictive(multiworld)
|
distribute_items_restrictive(multiworld)
|
||||||
@@ -795,8 +795,8 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
|||||||
|
|
||||||
def test_balances_progression(self) -> None:
|
def test_balances_progression(self) -> None:
|
||||||
"""Tests that progression balancing moves progression items earlier"""
|
"""Tests that progression balancing moves progression items earlier"""
|
||||||
self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 50
|
self.multiworld.progression_balancing[self.player1.id].value = 50
|
||||||
self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 50
|
self.multiworld.progression_balancing[self.player2.id].value = 50
|
||||||
|
|
||||||
self.assertRegionContains(
|
self.assertRegionContains(
|
||||||
self.player1.regions[2], self.player2.prog_items[0])
|
self.player1.regions[2], self.player2.prog_items[0])
|
||||||
@@ -808,8 +808,8 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
|||||||
|
|
||||||
def test_balances_progression_light(self) -> None:
|
def test_balances_progression_light(self) -> None:
|
||||||
"""Test that progression balancing still moves items earlier on minimum value"""
|
"""Test that progression balancing still moves items earlier on minimum value"""
|
||||||
self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 1
|
self.multiworld.progression_balancing[self.player1.id].value = 1
|
||||||
self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 1
|
self.multiworld.progression_balancing[self.player2.id].value = 1
|
||||||
|
|
||||||
self.assertRegionContains(
|
self.assertRegionContains(
|
||||||
self.player1.regions[2], self.player2.prog_items[0])
|
self.player1.regions[2], self.player2.prog_items[0])
|
||||||
@@ -822,8 +822,8 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
|||||||
|
|
||||||
def test_balances_progression_heavy(self) -> None:
|
def test_balances_progression_heavy(self) -> None:
|
||||||
"""Test that progression balancing moves items earlier on maximum value"""
|
"""Test that progression balancing moves items earlier on maximum value"""
|
||||||
self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 99
|
self.multiworld.progression_balancing[self.player1.id].value = 99
|
||||||
self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 99
|
self.multiworld.progression_balancing[self.player2.id].value = 99
|
||||||
|
|
||||||
self.assertRegionContains(
|
self.assertRegionContains(
|
||||||
self.player1.regions[2], self.player2.prog_items[0])
|
self.player1.regions[2], self.player2.prog_items[0])
|
||||||
@@ -836,8 +836,8 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
|||||||
|
|
||||||
def test_skips_balancing_progression(self) -> None:
|
def test_skips_balancing_progression(self) -> None:
|
||||||
"""Test that progression balancing is skipped when players have it disabled"""
|
"""Test that progression balancing is skipped when players have it disabled"""
|
||||||
self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 0
|
self.multiworld.progression_balancing[self.player1.id].value = 0
|
||||||
self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 0
|
self.multiworld.progression_balancing[self.player2.id].value = 0
|
||||||
|
|
||||||
self.assertRegionContains(
|
self.assertRegionContains(
|
||||||
self.player1.regions[2], self.player2.prog_items[0])
|
self.player1.regions[2], self.player2.prog_items[0])
|
||||||
@@ -849,8 +849,8 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
|||||||
|
|
||||||
def test_ignores_priority_locations(self) -> None:
|
def test_ignores_priority_locations(self) -> None:
|
||||||
"""Test that progression items on priority locations don't get moved by balancing"""
|
"""Test that progression items on priority locations don't get moved by balancing"""
|
||||||
self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 50
|
self.multiworld.progression_balancing[self.player1.id].value = 50
|
||||||
self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 50
|
self.multiworld.progression_balancing[self.player2.id].value = 50
|
||||||
|
|
||||||
self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
|
self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
|
||||||
|
|
||||||
|
|||||||
@@ -21,17 +21,6 @@ class TestOptions(unittest.TestCase):
|
|||||||
self.assertFalse(hasattr(world_type, "options"),
|
self.assertFalse(hasattr(world_type, "options"),
|
||||||
f"Unexpected assignment to {world_type.__name__}.options!")
|
f"Unexpected assignment to {world_type.__name__}.options!")
|
||||||
|
|
||||||
def test_duplicate_options(self) -> None:
|
|
||||||
"""Tests that a world doesn't reuse the same option class."""
|
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
|
||||||
with self.subTest(game=game_name):
|
|
||||||
seen_options = set()
|
|
||||||
for option in world_type.options_dataclass.type_hints.values():
|
|
||||||
if not option.visibility:
|
|
||||||
continue
|
|
||||||
self.assertFalse(option in seen_options, f"{option} found in assigned options multiple times.")
|
|
||||||
seen_options.add(option)
|
|
||||||
|
|
||||||
def test_item_links_name_groups(self):
|
def test_item_links_name_groups(self):
|
||||||
"""Tests that item links successfully unfold item_name_groups"""
|
"""Tests that item links successfully unfold item_name_groups"""
|
||||||
item_link_groups = [
|
item_link_groups = [
|
||||||
@@ -78,4 +67,4 @@ class TestOptions(unittest.TestCase):
|
|||||||
if not world_type.hidden:
|
if not world_type.hidden:
|
||||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||||
with self.subTest(game=gamename, option=option_key):
|
with self.subTest(game=gamename, option=option_key):
|
||||||
pickle.dumps(option.from_any(option.default))
|
pickle.dumps(option(option.default))
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
from typing import TYPE_CHECKING, Dict, Type
|
|
||||||
from Utils import parse_yaml
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from worlds.AutoWorld import World
|
|
||||||
|
|
||||||
|
|
||||||
class TestGenerateYamlTemplates(unittest.TestCase):
|
|
||||||
old_world_types: Dict[str, Type["World"]]
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
import worlds.AutoWorld
|
|
||||||
|
|
||||||
self.old_world_types = worlds.AutoWorld.AutoWorldRegister.world_types
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
import worlds.AutoWorld
|
|
||||||
|
|
||||||
worlds.AutoWorld.AutoWorldRegister.world_types = self.old_world_types
|
|
||||||
|
|
||||||
if "World: with colon" in worlds.AutoWorld.AutoWorldRegister.world_types:
|
|
||||||
del worlds.AutoWorld.AutoWorldRegister.world_types["World: with colon"]
|
|
||||||
|
|
||||||
def test_name_with_colon(self) -> None:
|
|
||||||
from Options import generate_yaml_templates
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
|
||||||
from worlds.AutoWorld import World
|
|
||||||
|
|
||||||
class WorldWithColon(World):
|
|
||||||
game = "World: with colon"
|
|
||||||
item_name_to_id = {}
|
|
||||||
location_name_to_id = {}
|
|
||||||
|
|
||||||
AutoWorldRegister.world_types = {WorldWithColon.game: WorldWithColon}
|
|
||||||
with TemporaryDirectory(f"archipelago_{__name__}") as temp_dir:
|
|
||||||
generate_yaml_templates(temp_dir)
|
|
||||||
path: Path
|
|
||||||
for path in Path(temp_dir).iterdir():
|
|
||||||
self.assertTrue(path.is_file())
|
|
||||||
self.assertTrue(path.suffix == ".yaml")
|
|
||||||
with path.open(encoding="utf-8") as f:
|
|
||||||
try:
|
|
||||||
data = parse_yaml(f)
|
|
||||||
except:
|
|
||||||
f.seek(0)
|
|
||||||
print(f"Error in {path.name}:\n{f.read()}")
|
|
||||||
raise
|
|
||||||
self.assertIn("game", data)
|
|
||||||
self.assertIn(":", data["game"])
|
|
||||||
self.assertIn(data["game"], data)
|
|
||||||
self.assertIsInstance(data[data["game"]], dict)
|
|
||||||