diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index b59336fafe..6788abd30a 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -65,7 +65,7 @@ jobs: continue-on-error: false if: env.diff != '' && matrix.task == 'flake8' run: | - flake8 --count --select=E9,F63,F7,F82 --show-source --statistics ${{ env.diff }} + flake8 --count --select=E9,F63,F7,F82 --ignore F824 --show-source --statistics ${{ env.diff }} - name: "flake8: Lint modified files" continue-on-error: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b450fe46e..7529f693bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,8 +99,8 @@ jobs: if-no-files-found: error retention-days: 7 # keep for 7 days, should be enough - build-ubuntu2004: - runs-on: ubuntu-20.04 + build-ubuntu2204: + runs-on: ubuntu-22.04 steps: # - copy code below to release.yml - - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f12e8fb80c..20d4d2fe32 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,8 +29,8 @@ jobs: # build-release-windows: # this is done by hand because of signing # build-release-macos: # LF volunteer - build-release-ubuntu2004: - runs-on: ubuntu-20.04 + build-release-ubuntu2204: + runs-on: ubuntu-22.04 steps: - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV diff --git a/BaseClasses.py b/BaseClasses.py index 4db3917985..4074108b4b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -616,7 +616,7 @@ class MultiWorld(): locations: Set[Location] = set() events: Set[Location] = set() for location in self.get_filled_locations(): - if type(location.item.code) is int: + if type(location.item.code) is int and type(location.address) is int: locations.add(location) else: events.add(location) diff --git a/Fill.py b/Fill.py index fe39b74fbe..cce7aec209 100644 --- a/Fill.py +++ b/Fill.py @@ -75,9 +75,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati items_to_place.append(reachable_items[next_player].pop()) for item in items_to_place: - for p, pool_item in enumerate(item_pool): + # The items added into `reachable_items` are placed starting from the end of each deque in + # `reachable_items`, so the items being placed are more likely to be found towards the end of `item_pool`. + for p, pool_item in enumerate(reversed(item_pool), start=1): if pool_item is item: - item_pool.pop(p) + del item_pool[-p] break maximum_exploration_state = sweep_from_pool( diff --git a/Launcher.py b/Launcher.py index 609c109470..29bd71764e 100644 --- a/Launcher.py +++ b/Launcher.py @@ -8,9 +8,7 @@ Archipelago Launcher Scroll down to components= to add components to the launcher as well as setup.py """ -import os import argparse -import itertools import logging import multiprocessing import shlex @@ -132,7 +130,7 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogContentContainer, MDDialogSupportingText from kivymd.uix.divider import MDDivider - if client_component is None: + if not client_component: run_component(text_client_component, *launch_args) return else: @@ -228,14 +226,13 @@ refresh_components: Optional[Callable[[], None]] = None def run_gui(path: str, args: Any) -> None: - from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, MDButton, MDLabel, MDButtonText, ScrollBox, ApAsyncImage) + from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox) from kivy.properties import ObjectProperty from kivy.core.window import Window from kivy.metrics import dp from kivymd.uix.button import MDIconButton from kivymd.uix.card import MDCard from kivymd.uix.menu import MDDropdownMenu - from kivymd.uix.relativelayout import MDRelativeLayout from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText from kivy.lang.builder import Builder @@ -250,7 +247,6 @@ def run_gui(path: str, args: Any) -> None: self.image = image_path super().__init__(args, kwargs) - class Launcher(ThemedApp): base_title: str = "Archipelago Launcher" top_screen: MDFloatLayout = ObjectProperty(None) @@ -337,6 +333,11 @@ def run_gui(path: str, args: Any) -> None: for card in cards: self.button_layout.layout.add_widget(card) + top = self.button_layout.children[0].y + self.button_layout.children[0].height \ + - self.button_layout.height + scroll_percent = self.button_layout.convert_distance_to_scroll(0, top) + self.button_layout.scroll_y = max(0, min(1, scroll_percent[1])) + def filter_clients(self, caller): self._refresh_components(caller.type) @@ -358,6 +359,11 @@ def run_gui(path: str, args: Any) -> None: self._refresh_components(self.current_filter) + # Uncomment to re-enable the Kivy console/live editor + # Ctrl-E to enable it, make sure numlock/capslock is disabled + # from kivy.modules.console import create_console + # create_console(Window, self.top_screen) + return self.top_screen def on_start(self): diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py index bdfaa74625..69f50938d2 100644 --- a/LinksAwakeningClient.py +++ b/LinksAwakeningClient.py @@ -26,6 +26,7 @@ import typing from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger, server_loop) from NetUtils import ClientStatus +from worlds.ladx import LinksAwakeningWorld from worlds.ladx.Common import BASE_ID as LABaseID from worlds.ladx.GpsTracker import GpsTracker from worlds.ladx.TrackerConsts import storage_key @@ -741,8 +742,8 @@ class LinksAwakeningContext(CommonContext): await asyncio.sleep(1.0) def run_game(romfile: str) -> None: - auto_start = typing.cast(typing.Union[bool, str], - Utils.get_options()["ladx_options"].get("rom_start", True)) + auto_start = LinksAwakeningWorld.settings.rom_start + if auto_start is True: import webbrowser webbrowser.open(romfile) diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 0ddb6fe83e..2524cc40a6 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -28,6 +28,6 @@ def get_seeds(): response.append({ "seed_id": seed.id, "creation_time": seed.creation_time, - "players": get_players(seed.slots), + "players": get_players(seed), }) return jsonify(response) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 8ba093e014..b330146277 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -9,7 +9,7 @@ from threading import Event, Thread from typing import Any from uuid import UUID -from pony.orm import db_session, select, commit +from pony.orm import db_session, select, commit, PrimaryKey from Utils import restricted_loads from .locker import Locker, AlreadyRunningException @@ -36,12 +36,21 @@ def handle_generation_failure(result: BaseException): logging.exception(e) +def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None: + from setproctitle import setproctitle + + setproctitle(f"Generator ({sid})") + res = gen_game(gen_options, meta=meta, owner=owner, sid=sid) + setproctitle(f"Generator (idle)") + return res + + def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): try: meta = json.loads(generation.meta) options = restricted_loads(generation.options) logging.info(f"Generating {generation.id} for {len(options)} players") - pool.apply_async(gen_game, (options,), + pool.apply_async(_mp_gen_game, (options,), {"meta": meta, "sid": generation.id, "owner": generation.owner}, @@ -55,6 +64,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): def init_generator(config: dict[str, Any]) -> None: + from setproctitle import setproctitle + + setproctitle("Generator (idle)") + try: import resource except ModuleNotFoundError: diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 76a2b8a4dc..2ebb40d673 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -227,6 +227,9 @@ def set_up_logging(room_id) -> logging.Logger: def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, cert_file: typing.Optional[str], cert_key_file: typing.Optional[str], host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue): + from setproctitle import setproctitle + + setproctitle(name) Utils.init_logging(name) try: import resource @@ -247,8 +250,23 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, raise Exception("Worlds system should not be loaded in the custom server.") import gc - ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None - del cert_file, cert_key_file, ponyconfig + + if not cert_file: + def get_ssl_context(): + return None + else: + load_date = None + ssl_context = load_server_cert(cert_file, cert_key_file) + + def get_ssl_context(): + nonlocal load_date, ssl_context + today = datetime.date.today() + if load_date != today: + ssl_context = load_server_cert(cert_file, cert_key_file) + load_date = today + return ssl_context + + del ponyconfig gc.collect() # free intermediate objects used during setup loop = asyncio.get_event_loop() @@ -263,12 +281,12 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, assert ctx.server is None try: ctx.server = websockets.serve( - functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) + functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context()) await ctx.server except OSError: # likely port in use ctx.server = websockets.serve( - functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context) + functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context()) await ctx.server port = 0 diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 190409d9a2..a9cd33dd6d 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -9,3 +9,4 @@ bokeh>=3.6.3 markupsafe>=3.0.2 Markdown>=3.7 mdx-breakless-lists>=1.0.1 +setproctitle>=1.3.5 diff --git a/WebHostLib/static/assets/gameInfo.js b/WebHostLib/static/assets/gameInfo.js index 1d6d136135..797c9f6448 100644 --- a/WebHostLib/static/assets/gameInfo.js +++ b/WebHostLib/static/assets/gameInfo.js @@ -23,7 +23,6 @@ window.addEventListener('load', () => { showdown.setOption('strikethrough', true); showdown.setOption('literalMidWordUnderscores', true); gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); // Reset the id of all header divs to something nicer for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { diff --git a/WebHostLib/static/assets/hostGame.js b/WebHostLib/static/assets/hostGame.js index db1ab1ddde..01a8da06e5 100644 --- a/WebHostLib/static/assets/hostGame.js +++ b/WebHostLib/static/assets/hostGame.js @@ -6,6 +6,4 @@ window.addEventListener('load', () => { document.getElementById('file-input').addEventListener('change', () => { document.getElementById('host-game-form').submit(); }); - - adjustFooterHeight(); }); diff --git a/WebHostLib/static/assets/styleController.js b/WebHostLib/static/assets/styleController.js deleted file mode 100644 index 924e86ee26..0000000000 --- a/WebHostLib/static/assets/styleController.js +++ /dev/null @@ -1,47 +0,0 @@ -const adjustFooterHeight = () => { - // If there is no footer on this page, do nothing - const footer = document.getElementById('island-footer'); - if (!footer) { return; } - - // If the body is taller than the window, also do nothing - if (document.body.offsetHeight > window.innerHeight) { - footer.style.marginTop = '0'; - return; - } - - // Add a margin-top to the footer to position it at the bottom of the screen - const sibling = footer.previousElementSibling; - const margin = (window.innerHeight - sibling.offsetTop - sibling.offsetHeight - footer.offsetHeight); - if (margin < 1) { - footer.style.marginTop = '0'; - return; - } - footer.style.marginTop = `${margin}px`; -}; - -const adjustHeaderWidth = () => { - // If there is no header, do nothing - const header = document.getElementById('base-header'); - if (!header) { return; } - - const tempDiv = document.createElement('div'); - tempDiv.style.width = '100px'; - tempDiv.style.height = '100px'; - tempDiv.style.overflow = 'scroll'; - tempDiv.style.position = 'absolute'; - tempDiv.style.top = '-500px'; - document.body.appendChild(tempDiv); - const scrollbarWidth = tempDiv.offsetWidth - tempDiv.clientWidth; - document.body.removeChild(tempDiv); - - const documentRoot = document.compatMode === 'BackCompat' ? document.body : document.documentElement; - const margin = (documentRoot.scrollHeight > documentRoot.clientHeight) ? 0-scrollbarWidth : 0; - document.getElementById('base-header-right').style.marginRight = `${margin}px`; -}; - -window.addEventListener('load', () => { - window.addEventListener('resize', adjustFooterHeight); - window.addEventListener('resize', adjustHeaderWidth); - adjustFooterHeight(); - adjustHeaderWidth(); -}); diff --git a/WebHostLib/static/assets/tutorial.js b/WebHostLib/static/assets/tutorial.js index d527966005..c9022719fb 100644 --- a/WebHostLib/static/assets/tutorial.js +++ b/WebHostLib/static/assets/tutorial.js @@ -25,7 +25,6 @@ window.addEventListener('load', () => { showdown.setOption('literalMidWordUnderscores', true); showdown.setOption('disableForced4SpacesIndentedSublists', true); tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); const title = document.querySelector('h1') if (title) { diff --git a/WebHostLib/static/styles/globalStyles.css b/WebHostLib/static/styles/globalStyles.css index 1a0144830e..adcee6581b 100644 --- a/WebHostLib/static/styles/globalStyles.css +++ b/WebHostLib/static/styles/globalStyles.css @@ -36,6 +36,13 @@ html{ body{ margin: 0; + display: flex; + flex-direction: column; + min-height: calc(100vh - 110px); +} + +main { + flex-grow: 1; } a{ diff --git a/WebHostLib/templates/404.html b/WebHostLib/templates/404.html index 9d567510ee..6c91fed4ac 100644 --- a/WebHostLib/templates/404.html +++ b/WebHostLib/templates/404.html @@ -1,5 +1,6 @@ {% extends 'pageWrapper.html' %} {% import "macros.html" as macros %} +{% set show_footer = True %} {% block head %}