Skip to content

Commit

Permalink
Merge branch 'main' into more_door_panels
Browse files Browse the repository at this point in the history
  • Loading branch information
NewSoupVi authored Jun 6, 2024
2 parents 9009ed7 + 86da3eb commit 5a0f3cd
Show file tree
Hide file tree
Showing 76 changed files with 1,818 additions and 507 deletions.
31 changes: 30 additions & 1 deletion .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ on:
- '.github/workflows/unittests.yml'

jobs:
build:
unit:
runs-on: ${{ matrix.os }}
name: Test Python ${{ matrix.python.version }} ${{ matrix.os }}

Expand Down Expand Up @@ -60,3 +60,32 @@ jobs:
- name: Unittests
run: |
pytest -n auto
hosting:
runs-on: ${{ matrix.os }}
name: Test hosting with ${{ matrix.python.version }} on ${{ matrix.os }}

strategy:
matrix:
os:
- ubuntu-latest
python:
- {version: '3.11'} # current

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
- name: Test hosting
run: |
source venv/bin/activate
export PYTHONPATH=$(pwd)
python test/hosting/__main__.py
11 changes: 11 additions & 0 deletions Launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def launch(exe, in_terminal=False):

def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
from kivy.core.window import Window
from kivy.uix.image import AsyncImage
from kivy.uix.relativelayout import RelativeLayout

Expand Down Expand Up @@ -226,6 +227,8 @@ def build_button(component: Component) -> Widget:
if client:
client_layout.layout.add_widget(build_button(client[1]))

Window.bind(on_drop_file=self._on_drop_file)

return self.container

@staticmethod
Expand All @@ -235,6 +238,14 @@ def component_action(button):
else:
launch(get_exe(button.component), button.component.cli)

def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
""" When a patch file is dropped into the window, run the associated component. """
file, component = identify(filename.decode())
if file and component:
run_component(component, file)
else:
logging.warning(f"unable to identify component for {filename}")

def _stop(self, *largs):
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
# Closing the window explicitly cleans it up.
Expand Down
18 changes: 12 additions & 6 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import asyncio
import collections
import contextlib
import copy
import datetime
import functools
Expand Down Expand Up @@ -176,7 +177,7 @@ class Context:
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
non_hintable_names: typing.Dict[str, typing.Set[str]]
non_hintable_names: typing.Dict[str, typing.AbstractSet[str]]
spheres: typing.List[typing.Dict[int, typing.Set[int]]]
""" each sphere is { player: { location_id, ... } } """
logger: logging.Logger
Expand Down Expand Up @@ -231,7 +232,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo
self.embedded_blacklist = {"host", "port"}
self.client_ids: typing.Dict[typing.Tuple[int, int], datetime.datetime] = {}
self.auto_save_interval = 60 # in seconds
self.auto_saver_thread = None
self.auto_saver_thread: typing.Optional[threading.Thread] = None
self.save_dirty = False
self.tags = ['AP']
self.games: typing.Dict[int, str] = {}
Expand Down Expand Up @@ -268,6 +269,11 @@ def _load_game_data(self):
for world_name, world in worlds.AutoWorldRegister.world_types.items():
self.non_hintable_names[world_name] = world.hint_blacklist

for game_package in self.gamespackage.values():
# remove groups from data sent to clients
del game_package["item_name_groups"]
del game_package["location_name_groups"]

def _init_game_data(self):
for game_name, game_package in self.gamespackage.items():
if "checksum" in game_package:
Expand Down Expand Up @@ -1926,8 +1932,6 @@ def _cmd_status(self, tag: str = "") -> bool:
def _cmd_exit(self) -> bool:
"""Shutdown the server"""
self.ctx.server.ws_server.close()
if self.ctx.shutdown_task:
self.ctx.shutdown_task.cancel()
self.ctx.exit_event.set()
return True

Expand Down Expand Up @@ -2285,7 +2289,8 @@ def parse_args() -> argparse.Namespace:


async def auto_shutdown(ctx, to_cancel=None):
await asyncio.sleep(ctx.auto_shutdown)
with contextlib.suppress(asyncio.TimeoutError):
await asyncio.wait_for(ctx.exit_event.wait(), ctx.auto_shutdown)

def inactivity_shutdown():
ctx.server.ws_server.close()
Expand All @@ -2305,7 +2310,8 @@ def inactivity_shutdown():
if seconds < 0:
inactivity_shutdown()
else:
await asyncio.sleep(seconds)
with contextlib.suppress(asyncio.TimeoutError):
await asyncio.wait_for(ctx.exit_event.wait(), seconds)


def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLContext":
Expand Down
10 changes: 8 additions & 2 deletions Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,14 @@ class KeyedDefaultDict(collections.defaultdict):
"""defaultdict variant that uses the missing key as argument to default_factory"""
default_factory: typing.Callable[[typing.Any], typing.Any]

def __init__(self, default_factory: typing.Callable[[Any], Any] = None, **kwargs):
super().__init__(default_factory, **kwargs)
def __init__(self,
default_factory: typing.Callable[[Any], Any] = None,
seq: typing.Union[typing.Mapping, typing.Iterable, None] = None,
**kwargs):
if seq is not None:
super().__init__(default_factory, seq, **kwargs)
else:
super().__init__(default_factory, **kwargs)

def __missing__(self, key):
self[key] = value = self.default_factory(key)
Expand Down
5 changes: 4 additions & 1 deletion WebHost.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
import Utils
import settings

if typing.TYPE_CHECKING:
from flask import Flask

Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
settings.no_gui = True
configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home
configpath = os.path.abspath(Utils.user_path('config.yaml'))


def get_app():
def get_app() -> "Flask":
from WebHostLib import register, cache, app as raw_app
from WebHostLib.models import db

Expand Down
59 changes: 48 additions & 11 deletions WebHostLib/customserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,28 @@ def get_random_port():
def get_static_server_data() -> dict:
import worlds
data = {
"non_hintable_names": {},
"gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
"location_name_groups": {world_name: world.location_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
"non_hintable_names": {
world_name: world.hint_blacklist
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
"gamespackage": {
world_name: {
key: value
for key, value in game_package.items()
if key not in ("item_name_groups", "location_name_groups")
}
for world_name, game_package in worlds.network_data_package["games"].items()
},
"item_name_groups": {
world_name: world.item_name_groups
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
"location_name_groups": {
world_name: world.location_name_groups
for world_name, world in worlds.AutoWorldRegister.world_types.items()
},
}

for world_name, world in worlds.AutoWorldRegister.world_types.items():
data["non_hintable_names"][world_name] = world.hint_blacklist

return data


Expand Down Expand Up @@ -266,12 +277,15 @@ async def start_room(room_id):
ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout
if ctx.saving:
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task

except (KeyboardInterrupt, SystemExit):
if ctx.saving:
ctx._save()
setattr(asyncio.current_task(), "save", None)
except Exception as e:
with db_session:
room = Room.get(id=room_id)
Expand All @@ -281,8 +295,12 @@ async def start_room(room_id):
else:
if ctx.saving:
ctx._save()
setattr(asyncio.current_task(), "save", None)
finally:
try:
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
ctx.exit_event.set() # make sure the saving thread stops at some point
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
with (db_session):
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
Expand All @@ -294,13 +312,32 @@ async def start_room(room_id):
rooms_shutting_down.put(room_id)

class Starter(threading.Thread):
_tasks: typing.List[asyncio.Future]

def __init__(self):
super().__init__()
self._tasks = []

def _done(self, task: asyncio.Future):
self._tasks.remove(task)
task.result()

def run(self):
while 1:
next_room = rooms_to_run.get(block=True, timeout=None)
asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
self._tasks.append(task)
task.add_done_callback(self._done)
logging.info(f"Starting room {next_room} on {name}.")

starter = Starter()
starter.daemon = True
starter.start()
loop.run_forever()
try:
loop.run_forever()
finally:
# save all tasks that want to be saved during shutdown
for task in asyncio.all_tasks(loop):
save: typing.Optional[typing.Callable[[], typing.Any]] = getattr(task, "save", None)
if save:
save()
2 changes: 1 addition & 1 deletion WebHostLib/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def option_presets(game: str) -> Response:
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."

presets[preset_name][preset_option_name] = option.value
elif isinstance(option, Options.Range):
elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.ItemDict)):
presets[preset_name][preset_option_name] = option.value
elif isinstance(preset_option, str):
# Ensure the option value is valid for Choice and Toggle options
Expand Down
15 changes: 8 additions & 7 deletions WebHostLib/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
flask>=3.0.0
flask>=3.0.3
werkzeug>=3.0.3
pony>=0.7.17
waitress>=2.1.2
Flask-Caching>=2.1.0
Flask-Compress>=1.14
Flask-Limiter>=3.5.0
waitress>=3.0.0
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.7.0
bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.3.2; python_version >= '3.9'
markupsafe>=2.1.3
bokeh>=3.4.1; python_version >= '3.9'
markupsafe>=2.1.5
2 changes: 1 addition & 1 deletion WebHostLib/static/styles/playerOptions/playerOptions.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion WebHostLib/static/styles/playerOptions/playerOptions.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ html{
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
word-break: break-all;
word-break: break-word;

#player-options-header{
h1{
Expand Down
Loading

0 comments on commit 5a0f3cd

Please sign in to comment.