From 4404cf00c2dc3636bdf1e21dfcd3d5f9e5398c91 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 5 Jun 2024 20:18:48 +0200 Subject: [PATCH] Launcher: handle apworld installation --- inno_setup.iss | 5 ++++ worlds/LauncherComponents.py | 57 +++++++++++++++++++++++++++++++++++- worlds/__init__.py | 3 +- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index b016f224dfcf..40bc9a7339ce 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -209,6 +209,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 Server 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..6e6ae80e7637 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 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,55 @@ def launch_textclient(): launch_subprocess(CommonClient.run_as_textclient, name="TextClient") +def _install_apworld(apworld_path: str = ""): + if not apworld_path: + apworld_path = open_filename('Select APWorld file to install', (('APWorld', ('.apworld',)),)) + if not apworld_path: + # user closed menu + return + + if not apworld_path.endswith(".apworld"): + raise Exception(f"Wrong file format, looking for .apworld. File identified: {apworld_path}") + + apworld_path = pathlib.Path(apworld_path) + + try: + import zipfile + print(pathlib.Path(apworld_path.name).stem + "/__init__.py") + 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 + 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 = ""): + try: + source, target = _install_apworld(apworld_path) + except Exception as e: + import Utils + Utils.messagebox(e.__class__.__name__, str(e)) + 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 +138,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..87628e8a9265 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -10,7 +10,8 @@ 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") +os.makedirs(user_folder, exist_ok=True) __all__ = { "network_data_package",