diff --git a/Launcher.py b/Launcher.py index d1e076b4c3..9c2764fe10 100644 --- a/Launcher.py +++ b/Launcher.py @@ -16,21 +16,20 @@ import shlex import subprocess import sys import urllib.parse -import webbrowser from collections.abc import Callable, Sequence -from os.path import isfile from shutil import which -from typing import Any +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + from worlds.LauncherComponents import Component, Type if __name__ == "__main__": import ModuleUpdate ModuleUpdate.update() -import settings import Utils -from Utils import (env_cleared_lib_path, init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, - messagebox, open_filename, user_path) +from Utils import env_cleared_lib_path, init_logging, is_linux, is_macos, is_windows, local_path if __name__ == "__main__": init_logging('Launcher') @@ -115,27 +114,8 @@ def update_settings(): get_settings().save() -components.extend([ - # Functions - Component("Open host.yaml", func=open_host_yaml, - description="Open the host.yaml file to change settings for generation, games, and more."), - Component("Open Patch", func=open_patch, - description="Open a patch file, downloaded from the room page or provided by the host."), - Component("Generate Template Options", func=generate_yamls, - description="Generate template YAMLs for currently installed games."), - Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/"), - description="Open archipelago.gg in your browser."), - Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2"), - description="Join the Discord server to play public multiworlds, report issues, or just chat!"), - Component("Unrated/18+ Discord Server", icon="discord", - func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4"), - description="Find unrated and 18+ games in the After Dark Discord server."), - Component("Browse Files", func=browse_files, - description="Open the Archipelago installation folder in your file browser."), -]) - - -def handle_uri(path: str) -> tuple[list[Component], Component]: +def handle_uri(path: str) -> tuple[list["Component"], "Component"]: + from worlds.LauncherComponents import components url = urllib.parse.urlparse(path) queries = urllib.parse.parse_qs(url.query) client_components = [] @@ -149,7 +129,7 @@ def handle_uri(path: str) -> tuple[list[Component], Component]: return client_components, text_client_component -def build_uri_popup(component_list: list[Component], launch_args: tuple[str, ...]) -> None: +def build_uri_popup(component_list: list["Component"], launch_args: tuple[str, ...]) -> None: from kvui import ButtonsPrompt component_options = { component.display_name: component for component in component_list @@ -161,41 +141,6 @@ def build_uri_popup(component_list: list[Component], launch_args: tuple[str, ... popup.open() -def identify(path: None | str) -> tuple[None | str, None | Component]: - if path is None: - return None, None - for component in components: - if component.handles_file(path): - return path, component - elif path == component.display_name or path == component.script_name: - return None, component - return None, None - - -def get_exe(component: str | Component) -> Sequence[str] | None: - if isinstance(component, str): - name = component - component = None - if name.startswith("Archipelago"): - name = name[11:] - if name.endswith(".exe"): - name = name[:-4] - if name.endswith(".py"): - name = name[:-3] - if not name: - return None - for c in components: - if c.script_name == name or c.frozen_name == f"Archipelago{name}": - component = c - break - if not component: - return None - if is_frozen(): - suffix = ".exe" if is_windows else "" - return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None - else: - return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None - def launch(exe: Sequence[str], in_terminal: bool = False) -> bool: """Runs the given command/args in `exe` in a new process. @@ -225,7 +170,7 @@ def launch(exe: Sequence[str], in_terminal: bool = False) -> bool: return False -def create_shortcut(button: Any, component: Component) -> None: +def create_shortcut(button: Any, component: "Component") -> None: from pyshortcuts import make_shortcut env = os.environ if "APPIMAGE" in env: @@ -244,11 +189,14 @@ def create_shortcut(button: Any, component: Component) -> None: refresh_components: Callable[[], None] | None = None -def run_gui(launch_components: list[Component], args: Any) -> None: - from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox) +def run_gui(launch_components: list["Component"], args: Any) -> None: + import threading + from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox, + MDScreenManager, MDScreen, LoadingScreen, LogtoLoadingScreen) from kivy.properties import ObjectProperty from kivy.core.window import Window from kivy.metrics import dp + from kivy.clock import Clock from kivymd.uix.button import MDIconButton, MDButton from kivymd.uix.card import MDCard from kivymd.uix.menu import MDDropdownMenu @@ -258,11 +206,11 @@ def run_gui(launch_components: list[Component], args: Any) -> None: from kivy.lang.builder import Builder class LauncherCard(MDCard): - component: Component | None + component: "Component | None" image: str context_button: MDIconButton = ObjectProperty(None) - def __init__(self, *args, component: Component | None = None, image_path: str = "", **kwargs): + def __init__(self, *args, component: "Component | None" = None, image_path: str = "", **kwargs): self.component = component self.image = image_path super().__init__(args, kwargs) @@ -275,7 +223,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None: button_layout: ScrollBox = ObjectProperty(None) search_box: MDTextField = ObjectProperty(None) cards: list[LauncherCard] - current_filter: Sequence[str | Type] | None + current_filter: Sequence[str, "Type"] | None failed_worlds: bool = bool(failed_world_loads) def __init__(self, ctx=None, components=None, args=None): @@ -286,6 +234,11 @@ def run_gui(launch_components: list[Component], args: Any) -> None: self.launch_components = components self.launch_args = args self.cards = [] + self.current_filter = () + super().__init__() + + def load_filter(self): + from worlds.LauncherComponents import Type self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC) persistent = Utils.persistent_load() if "launcher" in persistent: @@ -300,7 +253,6 @@ def run_gui(launch_components: list[Component], args: Any) -> None: else: filters.append(Type[filter]) self.current_filter = filters - super().__init__() def set_favorite(self, caller): if caller.component.display_name in self.favorites: @@ -310,7 +262,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None: self.favorites.append(caller.component.display_name) caller.icon = "star" - def build_card(self, component: Component) -> LauncherCard: + def build_card(self, component: "Component") -> LauncherCard: """ Builds a card widget for a given component. @@ -318,6 +270,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None: :return: The created Card Widget. """ + from worlds.LauncherComponents import icon_paths button_card = LauncherCard(component=component, image_path=icon_paths[component.icon]) @@ -336,8 +289,9 @@ def run_gui(launch_components: list[Component], args: Any) -> None: return button_card - def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None: + def _refresh_components(self, type_filter: Sequence["Type"] | None = None) -> None: if not type_filter: + from worlds.LauncherComponents import Type type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC] favorites = "favorites" in type_filter @@ -368,6 +322,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None: if len(name) == 0: self._refresh_components(self.current_filter) return + from worlds.LauncherComponents import Type sub_matches = [ card for card in self.cards @@ -378,38 +333,70 @@ def run_gui(launch_components: list[Component], args: Any) -> None: self.button_layout.layout.add_widget(card) def build(self): + self.set_colors() + self.screen_manager = MDScreenManager() self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv")) + self.loading_screen = LoadingScreen(name="loading") + self.screen_manager.add_widget(self.loading_screen) self.grid = self.top_screen.ids.grid self.navigation = self.top_screen.ids.navigation self.button_layout = self.top_screen.ids.button_layout self.search_box = self.top_screen.ids.search_box - self.set_colors() self.top_screen.md_bg_color = self.theme_cls.backgroundColor - global refresh_components - refresh_components = self._refresh_components - Window.bind(on_drop_file=self._on_drop_file) Window.bind(on_keyboard=self._on_keyboard) - for component in components: - self.cards.append(self.build_card(component)) - - 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 + main_screen = MDScreen(name="main") + main_screen.add_widget(self.top_screen) + self.screen_manager.add_widget(main_screen) + + return self.screen_manager def on_start(self): + super().on_start() + logger = logging.getLogger("Worlds") + logger.propagate = False + self.loading_handler = LogtoLoadingScreen(self.loading_screen.update_text) + logger.addHandler(self.loading_handler) + threading.Thread(target=self.do_loading, name="WorldLoading").start() + if self.launch_components: build_uri_popup(self.launch_components, self.launch_args) self.launch_components = None self.launch_args = None + def do_loading(self): + import importlib + import time + start = time.perf_counter() + assert "worlds" not in sys.modules, "worlds module already loaded." + importlib.import_module("worlds") + logging.error(f"Worlds module loaded in {time.perf_counter() - start:.2f} seconds") + + global refresh_components + logger = logging.getLogger("Worlds") + logger.info("User Data") + self.load_filter() + + refresh_components = self._refresh_components + logger.info("Finalizing startup") + Clock.schedule_once(self.finish_loading) + + def finish_loading(self, dt): + from worlds.LauncherComponents import components + logger = logging.getLogger("Worlds") + for component in components: + self.cards.append(self.build_card(component)) + self._refresh_components(self.current_filter) + logger.removeHandler(self.loading_handler) + self.screen_manager.current = "main" + @staticmethod def component_action(button): open_text = "Opening in a new window..." @@ -418,6 +405,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None: button.component.func() else: # if launch returns False, it started the process in background (not in a new terminal) + from worlds.LauncherComponents import get_exe if not launch(get_exe(button.component), button.component.cli) and button.component.cli: open_text = "Running in the background..." @@ -459,6 +447,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None: 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. """ + from worlds.LauncherComponents import identify file, component = identify(filename.decode()) if file and component: run_component(component, file) @@ -481,6 +470,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None: super()._stop(*largs) def on_stop(self): + from worlds.LauncherComponents import Type Utils.persistent_store("launcher", "favorites", self.favorites) Utils.persistent_store("launcher", "filter", ", ".join(filter.name if isinstance(filter, Type) else filter for filter in self.current_filter)) @@ -494,15 +484,11 @@ def run_gui(launch_components: list[Component], args: Any) -> None: refresh_components = None -def run_component(component: Component, *args): - if component.func: - component.func(*args) - if refresh_components: - refresh_components() - elif component.script_name: - subprocess.run([*get_exe(component.script_name), *args]) - else: - logging.warning(f"Component {component} does not appear to be executable.") +def run_component(component: "Component", *args): + global refresh_components + component.run(*args) + if refresh_components: + refresh_components() def main(args: argparse.Namespace | dict | None = None): @@ -522,6 +508,7 @@ def main(args: argparse.Namespace | dict | None = None): else: args['launch_components'] = [text_client_component, *components] else: + from worlds.LauncherComponents import identify file, component = identify(path) if file: args['file'] = file diff --git a/data/launcher.kv b/data/launcher.kv index a52214a7a4..53877c7fcb 100644 --- a/data/launcher.kv +++ b/data/launcher.kv @@ -1,3 +1,4 @@ +#:import Utils Utils : id: main style: "filled" @@ -61,7 +62,6 @@ text: "Open" -#:import Type worlds.LauncherComponents.Type MDFloatLayout: id: top_screen @@ -79,7 +79,7 @@ MDFloatLayout: MDButton: id: all style: "text" - type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC) + type: ("CLIENT", "TOOL", "ADJUSTER", "MISC") on_release: app.filter_clients_by_type(self) MDButtonIcon: @@ -89,7 +89,7 @@ MDFloatLayout: MDButton: id: client style: "text" - type: (Type.CLIENT, ) + type: ("CLIENT", ) on_release: app.filter_clients_by_type(self) MDButtonIcon: @@ -99,7 +99,7 @@ MDFloatLayout: MDButton: id: Tool style: "text" - type: (Type.TOOL, ) + type: ("TOOL", ) on_release: app.filter_clients_by_type(self) MDButtonIcon: @@ -109,7 +109,7 @@ MDFloatLayout: MDButton: id: adjuster style: "text" - type: (Type.ADJUSTER, ) + type: ("ADJUSTER", ) on_release: app.filter_clients_by_type(self) MDButtonIcon: @@ -119,7 +119,7 @@ MDFloatLayout: MDButton: id: misc style: "text" - type: (Type.MISC, ) + type: ("MISC", ) on_release: app.filter_clients_by_type(self) MDButtonIcon: @@ -168,3 +168,24 @@ MDFloatLayout: ScrollBox: id: button_layout + + +: + label: label + md_bg_color: self.theme_cls.backgroundColor + ApAsyncImage: + source: Utils.local_path("data/icon.png") + pos_hint: {"center_x": 0.5, "center_y": 0.5} + size_hint: None, None + size: 512, 512 + opacity: 0.5 + MDCircularProgressIndicator: + pos_hint: {"center_x": 0.5, "center_y": 0.5} + size: 550, 550 + size_hint: None, None + MDLabel: + id: label + text: "Loading..." + halign: "center" + pos_hint: {"center_x": 0.5, "center_y": 0.5} + theme_text_color: "Primary" diff --git a/kvui.py b/kvui.py index 8fcf9a7542..12cc9319a0 100644 --- a/kvui.py +++ b/kvui.py @@ -122,6 +122,22 @@ class ThemedApp(MDApp): self.theme_cls.dynamic_scheme_contrast = text_colors.dynamic_scheme_contrast +class LogtoLoadingScreen(logging.Handler): + def __init__(self, on_log): + super().__init__() + self.on_log = on_log + + def handle(self, record: logging.LogRecord): + self.on_log(record.getMessage()) + + +class LoadingScreen(MDScreen): + label = ObjectProperty(None) + + def update_text(self, text): + self.label.text = text + + class ImageIcon(MDButtonIcon, AsyncImage): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index e0d981c6a3..24910f0e24 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -2,19 +2,21 @@ import bisect import logging import pathlib import weakref -from enum import Enum, auto -from typing import Optional, Callable, List, Iterable, Tuple +import sys +import webbrowser +from enum import Enum +from typing import Optional, Callable, Iterable, Sequence -from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore +from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore, \ + is_windows -class Type(Enum): - TOOL = auto() - MISC = auto() - CLIENT = auto() - ADJUSTER = auto() - FUNC = auto() # do not use anymore - HIDDEN = auto() +class Type(str, Enum): + TOOL = "TOOL" + MISC = "MISC" + CLIENT = "CLIENT" + ADJUSTER = "ADJUSTER" + HIDDEN = "HIDDEN" class Component: @@ -67,10 +69,6 @@ class Component: self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None self.icon = icon self.cli = cli - if component_type == Type.FUNC: - from Utils import deprecate - deprecate(f"Launcher Component {self.display_name} is using Type.FUNC Type, which is pending removal.") - component_type = Type.MISC self.type = component_type or ( Type.CLIENT if "Client" in display_name else @@ -86,18 +84,27 @@ class Component: def __repr__(self): return f"{self.__class__.__name__}({self.display_name})" + def run(self, *args) -> bool: + if self.func: + self.func(*args) + elif self.script_name: + import subprocess + subprocess.run([*get_exe(self.script_name), *args]) + else: + logging.warning(f"Component {self} does not appear to be executable.") + processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None: +def launch_subprocess(func: Callable, name: str | None = None, args: tuple[str, ...] = ()) -> None: import multiprocessing process = multiprocessing.Process(target=func, name=name, args=args) process.start() processes.add(process) -def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None: +def launch(func: Callable, name: str | None = None, args: tuple[str, ...] = ()) -> None: from Utils import is_kivy_running if is_kivy_running(): launch_subprocess(func, name, args) @@ -124,7 +131,7 @@ def launch_textclient(*args): launch(CommonClient.run_as_textclient, name="TextClient", args=args) -def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: +def _install_apworld(apworld_src: str = "") -> Optional[tuple[pathlib.Path, pathlib.Path]]: if not apworld_src: apworld_src = open_filename('Select APWorld file to install', (('APWorld', ('.apworld',)),)) if not apworld_src: @@ -215,8 +222,124 @@ def export_datapackage() -> None: open_file(path) +def open_patch(): + from Utils import messagebox + from os.path import isfile + suffixes = [] + for c in components: + if c.type == Type.CLIENT and \ + isinstance(c.file_identifier, SuffixIdentifier) and \ + (c.script_name is None or isfile(get_exe(c)[-1])): + suffixes += c.file_identifier.suffixes + try: + filename = open_filename("Select patch", (("Patches", suffixes),)) + except Exception as e: + messagebox("Error", str(e), error=True) + else: + file, component = identify(filename) + if file and component: + exe = get_exe(component) + if exe is None or not isfile(exe[-1]): + exe = get_exe("Launcher") -components: List[Component] = [ + launch([*exe, file], component.cli) + + +def get_exe(component: str | Component) -> Sequence[str] | None: + if isinstance(component, str): + name = component + component = None + if name.startswith("Archipelago"): + name = name[11:] + if name.endswith(".exe"): + name = name[:-4] + if name.endswith(".py"): + name = name[:-3] + if not name: + return None + for c in components: + if c.script_name == name or c.frozen_name == f"Archipelago{name}": + component = c + break + if not component: + return None + if is_frozen(): + suffix = ".exe" if is_windows else "" + return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None + else: + return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None + +def identify(path: None | str) -> tuple[None | str, None | Component]: + if path is None: + return None, None + for component in components: + if component.handles_file(path): + return path, component + elif path == component.display_name or path == component.script_name: + return None, component + return None, None + +def open_host_yaml(): + import settings + import subprocess + from shutil import which + from Utils import is_linux, is_macos, env_cleared_lib_path + s = settings.get_settings() + file = s.filename + s.save() + assert file, "host.yaml missing" + if is_linux: + exe = which('sensible-editor') or which('gedit') or \ + which('xdg-open') or which('gnome-open') or which('kde-open') + elif is_macos: + exe = which("open") + else: + webbrowser.open(file) + return + + env = env_cleared_lib_path() + subprocess.Popen([exe, file], env=env) + +def generate_yamls(*args): + import argparse + + from Options import generate_yaml_templates + + parser = argparse.ArgumentParser(description="Generate Template Options", usage="[-h] [--skip_open_folder]") + parser.add_argument("--skip_open_folder", action="store_true") + args = parser.parse_args(args) + + target = user_path("Players", "Templates") + generate_yaml_templates(target, False) + if not args.skip_open_folder: + open_folder(target) + + +def browse_files(): + open_folder(user_path()) + + +def open_folder(folder_path): + import subprocess + from shutil import which + from Utils import is_linux, is_macos, env_cleared_lib_path + + if is_linux: + exe = which('xdg-open') or which('gnome-open') or which('kde-open') + elif is_macos: + exe = which("open") + else: + webbrowser.open(folder_path) + return + + if exe: + env = env_cleared_lib_path() + subprocess.Popen([exe, folder_path], env=env) + else: + logging.warning(f"No file browser available to open {folder_path}") + + +components: list[Component] = [ # Launcher Component('Launcher', 'Launcher', component_type=Type.HIDDEN), # Core @@ -231,6 +354,22 @@ components: List[Component] = [ description="Install an APWorld to play games not included with Archipelago by default."), Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient, description="Connect to a multiworld using the text client."), + # Functions + Component("Open host.yaml", func=open_host_yaml, + description="Open the host.yaml file to change settings for generation, games, and more."), + Component("Open Patch", func=open_patch, + description="Open a patch file, downloaded from the room page or provided by the host."), + Component("Generate Template Options", func=generate_yamls, + description="Generate template YAMLs for currently installed games."), + Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/"), + description="Open archipelago.gg in your browser."), + Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2"), + description="Join the Discord server to play public multiworlds, report issues, or just chat!"), + Component("Unrated/18+ Discord Server", icon="discord", + func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4"), + description="Find unrated and 18+ games in the After Dark Discord server."), + Component("Browse Files", func=browse_files, + description="Open the Archipelago installation folder in your file browser."), Component('LttP Adjuster', 'LttPAdjuster'), # Ocarina of Time Component('OoT Client', 'OoTClient', diff --git a/worlds/__init__.py b/worlds/__init__.py index c5fc00746d..da948b9cf3 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -35,6 +35,9 @@ __all__ = [ failed_world_loads: dict[str, str] = {} +logger = logging.getLogger("Worlds") +logger.propagate = False +logger.setLevel(logging.INFO) @dataclasses.dataclass(order=True) class WorldSource: @@ -56,7 +59,7 @@ class WorldSource: def load(self) -> bool: try: start = time.perf_counter() - importlib.import_module(f".{Path(self.path).stem}", "worlds") + importlib.import_module(f".{self.name}", "worlds") self.time_taken = time.perf_counter()-start return True @@ -73,6 +76,9 @@ class WorldSource: failed_world_loads[os.path.basename(self.path).rsplit(".", 1)[0]] = reason return False + @property + def name(self) -> str: + return Path(self.path).stem # AP_TEST_WORLDS (pytest only) scopes auto-loading to the named worlds; these are always loaded for the # suite's fixtures but aren't themselves worlds under test. @@ -82,6 +88,7 @@ _requested_worlds = {name.strip() for name in _test_worlds_env.split(",") if nam test_worlds_filter = _requested_worlds | _SUITE_FIXTURE_WORLDS if _requested_worlds else None # find potential world containers, currently folders and zip-importable .apworld's +logger.info("Indexing worlds") world_sources: List[WorldSource] = [] for folder in (folder for folder in (user_folder, local_folder) if folder): relative = folder == local_folder @@ -102,6 +109,7 @@ for folder in (folder for folder in (user_folder, local_folder) if folder): world_sources.append(WorldSource(file_name, is_zip=True, relative=relative)) # import all submodules to trigger AutoWorldRegister +logger.info("Processing found worlds") world_sources.sort() apworlds: list[WorldSource] = [] for world_source in world_sources: @@ -109,6 +117,7 @@ for world_source in world_sources: if world_source.is_zip: apworlds.append(world_source) else: + logger.info(world_source.name) world_source.load() from .AutoWorld import AutoWorldRegister @@ -143,6 +152,7 @@ if apworlds: logging.warning(reason) for apworld_source in apworlds: + logger.info(apworld_source.name) apworld: APWorldContainer = APWorldContainer(apworld_source.resolved_path) # populate metadata try: @@ -235,6 +245,7 @@ else: AutoWorldRegister.testable_worlds = dict(AutoWorldRegister.world_types) # Build the data package for each game. +logger.info("Datapackage") network_data_package: DataPackage = { "games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()}, }