mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-07-17 19:00:43 -07:00
Launcher: Loading Screen (#6115)
This commit is contained in:
+78
-91
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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()},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user