mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-04 14:23:29 -07:00
Launcher: Loading Screen
This commit is contained in:
233
Launcher.py
233
Launcher.py
@@ -16,97 +16,24 @@ 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
|
||||
|
||||
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, Type
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging('Launcher')
|
||||
|
||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
||||
|
||||
|
||||
def open_host_yaml():
|
||||
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 open_patch():
|
||||
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")
|
||||
|
||||
launch([*exe, file], component.cli)
|
||||
|
||||
|
||||
def generate_yamls(*args):
|
||||
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 = Utils.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):
|
||||
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}")
|
||||
|
||||
|
||||
def update_settings():
|
||||
@@ -114,27 +41,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 = []
|
||||
@@ -148,7 +56,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
|
||||
@@ -160,41 +68,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.
|
||||
@@ -224,7 +97,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:
|
||||
@@ -243,11 +116,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
|
||||
@@ -257,11 +133,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)
|
||||
@@ -284,6 +160,10 @@ 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):
|
||||
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||
persistent = Utils.persistent_load()
|
||||
if "launcher" in persistent:
|
||||
@@ -298,7 +178,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:
|
||||
@@ -308,7 +187,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.
|
||||
|
||||
@@ -316,6 +195,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])
|
||||
|
||||
@@ -376,38 +256,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..."
|
||||
@@ -416,6 +328,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..."
|
||||
|
||||
@@ -424,6 +337,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)
|
||||
@@ -459,15 +373,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):
|
||||
@@ -487,6 +397,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
|
||||
|
||||
10
Utils.py
10
Utils.py
@@ -19,6 +19,7 @@ import warnings
|
||||
|
||||
from argparse import Namespace
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum, auto
|
||||
|
||||
from settings import Settings, get_settings
|
||||
from time import sleep
|
||||
@@ -1374,3 +1375,12 @@ def get_all_causes(ex: Exception) -> str:
|
||||
top = causes[-1]
|
||||
others = "".join(f"\n{' ' * (i + 1)}Which caused: {c}" for i, c in enumerate(reversed(causes[:-1])))
|
||||
return f"{top}{others}"
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
TOOL = auto()
|
||||
MISC = auto()
|
||||
CLIENT = auto()
|
||||
ADJUSTER = auto()
|
||||
FUNC = auto() # do not use anymore
|
||||
HIDDEN = auto()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#:import Utils Utils
|
||||
#:import Type Utils.Type
|
||||
<LauncherCard>:
|
||||
id: main
|
||||
style: "filled"
|
||||
@@ -61,7 +63,6 @@
|
||||
text: "Open"
|
||||
|
||||
|
||||
#:import Type worlds.LauncherComponents.Type
|
||||
MDFloatLayout:
|
||||
id: top_screen
|
||||
|
||||
@@ -159,3 +160,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
kvui.py
16
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)
|
||||
|
||||
@@ -2,19 +2,12 @@ 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 typing import Optional, Callable, Iterable, Sequence
|
||||
|
||||
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
TOOL = auto()
|
||||
MISC = auto()
|
||||
CLIENT = auto()
|
||||
ADJUSTER = auto()
|
||||
FUNC = auto() # do not use anymore
|
||||
HIDDEN = auto()
|
||||
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore, \
|
||||
is_windows, Type
|
||||
|
||||
|
||||
class Component:
|
||||
@@ -86,18 +79,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 +126,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 +217,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 +349,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',
|
||||
|
||||
@@ -35,6 +35,9 @@ __all__ = [
|
||||
|
||||
failed_world_loads: List[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
|
||||
|
||||
@@ -72,8 +75,12 @@ class WorldSource:
|
||||
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return Path(self.path).stem
|
||||
|
||||
# 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
|
||||
@@ -92,6 +99,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:
|
||||
@@ -99,6 +107,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
|
||||
@@ -132,6 +141,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:
|
||||
@@ -205,6 +215,7 @@ if apworlds:
|
||||
del apworlds
|
||||
|
||||
# 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()},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user