Launcher: Loading Screen (#6115)

This commit is contained in:
Fabian Dill
2026-07-18 02:38:34 +02:00
committed by GitHub
parent feab54daec
commit c8cf2c4112
5 changed files with 290 additions and 116 deletions
+78 -91
View File
@@ -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
+27 -6
View File
@@ -1,3 +1,4 @@
#:import Utils Utils
<LauncherCard>:
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
<LoadingScreen>:
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"
+16
View File
@@ -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)
+157 -18
View File
@@ -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',
+12 -1
View File
@@ -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()},
}