Compare commits

..

1 Commits

Author SHA1 Message Date
NewSoupVi
68f90571fd Core: Add Item.excludable helper function
Some worlds might want to check for "Item is junk", i.e. an excludable item.

Because this is both `filler` and `trap`, and because `filler` is `0`, there are many "wrong ways" to do this. So I think we should provide a helper function for it.
2024-10-22 17:39:37 +02:00
268 changed files with 2255 additions and 3184 deletions

View File

@@ -16,7 +16,7 @@
"reportMissingImports": true, "reportMissingImports": true,
"reportMissingTypeStubs": true, "reportMissingTypeStubs": true,
"pythonVersion": "3.10", "pythonVersion": "3.8",
"pythonPlatform": "Windows", "pythonPlatform": "Windows",
"executionEnvironments": [ "executionEnvironments": [

View File

@@ -53,7 +53,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
if: env.diff != '' if: env.diff != ''
with: with:
python-version: '3.10' python-version: 3.8
- name: "Install dependencies" - name: "Install dependencies"
if: env.diff != '' if: env.diff != ''

View File

@@ -24,14 +24,14 @@ env:
jobs: jobs:
# build-release-macos: # LF volunteer # build-release-macos: # LF volunteer
build-win-py310: # RCs will still be built and signed by hand build-win-py38: # RCs will still be built and signed by hand
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install python - name: Install python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: '3.10' python-version: '3.8'
- name: Download run-time dependencies - name: Download run-time dependencies
run: | run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip

View File

@@ -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

View File

@@ -33,11 +33,13 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
python: python:
- {version: '3.8'}
- {version: '3.9'}
- {version: '3.10'} - {version: '3.10'}
- {version: '3.11'} - {version: '3.11'}
- {version: '3.12'} - {version: '3.12'}
include: include:
- python: {version: '3.10'} # old compat - python: {version: '3.8'} # win7 compat
os: windows-latest os: windows-latest
- python: {version: '3.12'} # current - python: {version: '3.12'} # current
os: windows-latest os: windows-latest
@@ -87,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

View File

@@ -1,16 +1,18 @@
from __future__ import annotations from __future__ import annotations
import collections import collections
import itertools
import functools import functools
import logging import logging
import random import random
import secrets import secrets
import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace from argparse import Namespace
from collections import Counter, deque from collections import Counter, deque
from collections.abc import Collection, MutableSequence from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING) Optional, Protocol, Set, Tuple, Union, Type)
from typing_extensions import NotRequired, TypedDict from typing_extensions import NotRequired, TypedDict
@@ -18,7 +20,7 @@ import NetUtils
import Options import Options
import Utils import Utils
if TYPE_CHECKING: if typing.TYPE_CHECKING:
from worlds import AutoWorld from worlds import AutoWorld
@@ -229,7 +231,7 @@ class MultiWorld():
for player in self.player_ids: for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player) self.worlds[player] = world_type(self, player)
options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints}) for option_key in options_dataclass.type_hints})
@@ -339,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
@@ -973,7 +975,7 @@ class Region:
entrances: List[Entrance] entrances: List[Entrance]
exits: List[Entrance] exits: List[Entrance]
locations: List[Location] locations: List[Location]
entrance_type: ClassVar[type[Entrance]] = Entrance entrance_type: ClassVar[Type[Entrance]] = Entrance
class Register(MutableSequence): class Register(MutableSequence):
region_manager: MultiWorld.RegionManager region_manager: MultiWorld.RegionManager
@@ -1073,7 +1075,7 @@ class Region:
return entrance.parent_region.get_connecting_entrance(is_main_entrance) return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Dict[str, Optional[int]], def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[type[Location]] = None) -> None: location_type: Optional[Type[Location]] = None) -> None:
""" """
Adds locations to the Region object, where location_type is your Location class and locations is a dict of Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address. location names to address.

View File

@@ -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.")

View File

@@ -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]

View File

@@ -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__()

View File

@@ -5,8 +5,8 @@ import multiprocessing
import warnings import warnings
if sys.version_info < (3, 10, 11): if sys.version_info < (3, 8, 6):
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ is supported.") raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) _skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -18,8 +18,8 @@ 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, TypeGuard from typing_extensions import TypeGuard
from yaml import load, load_all, dump from yaml import load, load_all, dump
try: try:
@@ -31,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:
@@ -47,7 +46,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.6.0" __version__ = "0.5.1"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")
@@ -424,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)
@@ -435,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")
@@ -568,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)
@@ -667,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}.")
@@ -709,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:
@@ -725,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
@@ -754,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:
@@ -776,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()

View File

@@ -12,12 +12,11 @@ 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
Utils.local_path.cached_path = os.path.dirname(__file__) Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
settings.no_gui = True settings.no_gui = True
configpath = os.path.abspath("config.yaml") configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home if not os.path.exists(configpath): # fall back to config.yaml in home
@@ -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:

View File

@@ -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

View File

@@ -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}
}
),
) )

View File

@@ -1,11 +1,13 @@
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
Flask-Compress>=1.15 Flask-Compress>=1.15
Flask-Limiter>=3.8.0 Flask-Limiter>=3.8.0
bokeh>=3.5.2 bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.3; python_version == '3.9'
bokeh>=3.5.2; python_version >= '3.10'
markupsafe>=2.1.5 markupsafe>=2.1.5
Markdown>=3.7 Markdown>=3.7
mdx-breakless-lists>=1.0.1 mdx-breakless-lists>=1.0.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 B

View File

@@ -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;

View File

@@ -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 %}

View File

@@ -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)] }}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -53,7 +53,7 @@
<table class="range-rows" data-option="{{ option_name }}"> <table class="range-rows" data-option="{{ option_name }}">
<tbody> <tbody>
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }} {{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
{% if option.default is number and option.range_start < option.default < option.range_end %} {% if option.range_start < option.default < option.range_end %}
{{ RangeRow(option_name, option, option.default, option.default, True) }} {{ RangeRow(option_name, option, option.default, option.default, True) }}
{% endif %} {% endif %}
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }} {{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}

View File

@@ -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(),

View File

@@ -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 }}

View File

@@ -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

View File

@@ -16,7 +16,7 @@ game contributions:
* **Do not introduce unit test failures/regressions.** * **Do not introduce unit test failures/regressions.**
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
your changes. Currently, the oldest supported version your changes. Currently, the oldest supported version
is [Python 3.10](https://www.python.org/downloads/release/python-31015/). is [Python 3.8](https://www.python.org/downloads/release/python-380/).
It is recommended that automated github actions are turned on in your fork to have github run unit tests after It is recommended that automated github actions are turned on in your fork to have github run unit tests after
pushing. pushing.
You can turn them on here: You can turn them on here:

View File

@@ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r
## General ## General
What you'll need: What you'll need:
* [Python 3.10.15 or newer](https://www.python.org/downloads/), not the Windows Store version * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
* Python 3.12.x is currently the newest supported version * Python 3.12.x is currently the newest supported version
* pip: included in downloads from python.org, separate in many Linux distributions * pip: included in downloads from python.org, separate in many Linux distributions
* Matching C compiler * Matching C compiler
@@ -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.

View File

@@ -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.

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