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
+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',