From 93cd13736ae326a9812cf3782484e6c3b04bd3d3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 6 Jun 2024 01:36:02 +0200 Subject: [PATCH] Launcher: handle apworld installation (#3472) --- inno_setup.iss | 5 +++ worlds/LauncherComponents.py | 64 ++++++++++++++++++++++++++++++++++-- worlds/__init__.py | 6 +++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index 7ae90622a1de..a0f4944d989f 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -213,6 +213,11 @@ Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Arc Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: ".apworld"; ValueData: "{#MyAppName}worlddata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}worlddata"; ValueData: "Archipelago World Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}worlddata\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; + Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0"; diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 78ec14b4a4f5..890b41aafa63 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -1,8 +1,10 @@ +import logging +import pathlib import weakref from enum import Enum, auto -from typing import Optional, Callable, List, Iterable +from typing import Optional, Callable, List, Iterable, Tuple -from Utils import local_path +from Utils import local_path, open_filename class Type(Enum): @@ -49,8 +51,10 @@ def handles_file(self, path: str): def __repr__(self): return f"{self.__class__.__name__}({self.display_name})" + processes = weakref.WeakSet() + def launch_subprocess(func: Callable, name: str = None): global processes import multiprocessing @@ -58,6 +62,7 @@ def launch_subprocess(func: Callable, name: str = None): process.start() processes.add(process) + class SuffixIdentifier: suffixes: Iterable[str] @@ -77,6 +82,60 @@ def launch_textclient(): launch_subprocess(CommonClient.run_as_textclient, name="TextClient") +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: + # user closed menu + return + + if not apworld_src.endswith(".apworld"): + raise Exception(f"Wrong file format, looking for .apworld. File identified: {apworld_src}") + + apworld_path = pathlib.Path(apworld_src) + + try: + import zipfile + zipfile.ZipFile(apworld_path).open(pathlib.Path(apworld_path.name).stem + "/__init__.py") + except ValueError as e: + raise Exception("Archive appears invalid or damaged.") from e + except KeyError as e: + raise Exception("Archive appears to not be an apworld. (missing __init__.py)") from e + + import worlds + if worlds.user_folder is None: + raise Exception("Custom Worlds directory appears to not be writable.") + for world_source in worlds.world_sources: + if apworld_path.samefile(world_source.resolved_path): + raise Exception(f"APWorld is already installed at {world_source.resolved_path}.") + + # TODO: run generic test suite over the apworld. + # TODO: have some kind of version system to tell from metadata if the apworld should be compatible. + + target = pathlib.Path(worlds.user_folder) / apworld_path.name + import shutil + shutil.copyfile(apworld_path, target) + + return apworld_path, target + + +def install_apworld(apworld_path: str = "") -> None: + try: + res = _install_apworld(apworld_path) + if res is None: + logging.info("Aborting APWorld installation.") + return + source, target = res + except Exception as e: + import Utils + Utils.messagebox(e.__class__.__name__, str(e), error=True) + logging.exception(e) + else: + import Utils + logging.info(f"Installed APWorld successfully, copied {source} to {target}.") + Utils.messagebox("Install complete.", f"Installed APWorld from {source}.") + + components: List[Component] = [ # Launcher Component('Launcher', 'Launcher', component_type=Type.HIDDEN), @@ -84,6 +143,7 @@ def launch_textclient(): Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True, file_identifier=SuffixIdentifier('.archipelago', '.zip')), Component('Generate', 'Generate', cli=True), + Component("Install APWorld", func=install_apworld, file_identifier=SuffixIdentifier(".apworld")), Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient), Component('Links Awakening DX Client', 'LinksAwakeningClient', file_identifier=SuffixIdentifier('.apladx')), diff --git a/worlds/__init__.py b/worlds/__init__.py index 09f72882195e..4da9d8e87c9e 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -10,7 +10,11 @@ from Utils import local_path, user_path local_folder = os.path.dirname(__file__) -user_folder = user_path("worlds") if user_path() != local_path() else None +user_folder = user_path("worlds") if user_path() != local_path() else user_path("custom_worlds") +try: + os.makedirs(user_folder, exist_ok=True) +except OSError: # can't access/write? + user_folder = None __all__ = { "network_data_package",