From 958829d4913647d00ff8cb1fa2a08e5c9856adeb Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 20 Mar 2023 21:24:47 +0100 Subject: [PATCH] Launcher: dynamic Launcher --- Launcher.py | 105 ++-------------------------------- inno_setup.iss | 1 + setup.py | 2 +- worlds/LauncherComponents.py | 106 +++++++++++++++++++++++++++++++++++ worlds/__init__.py | 48 ++++++++++------ worlds/factorio/__init__.py | 3 + 6 files changed, 147 insertions(+), 118 deletions(-) create mode 100644 worlds/LauncherComponents.py diff --git a/Launcher.py b/Launcher.py index be6fbd76..be40987e 100644 --- a/Launcher.py +++ b/Launcher.py @@ -14,10 +14,11 @@ import itertools import shlex import subprocess import sys -from enum import Enum, auto from os.path import isfile from shutil import which -from typing import Iterable, Sequence, Callable, Union, Optional +from typing import Sequence, Union, Optional + +from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier if __name__ == "__main__": import ModuleUpdate @@ -70,108 +71,12 @@ def browse_files(): webbrowser.open(file) -# noinspection PyArgumentList -class Type(Enum): - TOOL = auto() - FUNC = auto() # not a real component - CLIENT = auto() - ADJUSTER = auto() - - -class SuffixIdentifier: - suffixes: Iterable[str] - - def __init__(self, *args: str): - self.suffixes = args - - def __call__(self, path: str): - if isinstance(path, str): - for suffix in self.suffixes: - if path.endswith(suffix): - return True - return False - - -class Component: - display_name: str - type: Optional[Type] - script_name: Optional[str] - frozen_name: Optional[str] - icon: str # just the name, no suffix - cli: bool - func: Optional[Callable] - file_identifier: Optional[Callable[[str], bool]] - - def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, - cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None, - file_identifier: Optional[Callable[[str], bool]] = None): - self.display_name = display_name - self.script_name = script_name - self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None - self.icon = icon - self.cli = cli - self.type = component_type or \ - None if not display_name else \ - Type.FUNC if func else \ - Type.CLIENT if 'Client' in display_name else \ - Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL - self.func = func - self.file_identifier = file_identifier - - def handles_file(self, path: str): - return self.file_identifier(path) if self.file_identifier else False - - -components: Iterable[Component] = ( - # Launcher - Component('', 'Launcher'), - # Core - Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True, - file_identifier=SuffixIdentifier('.archipelago', '.zip')), - Component('Generate', 'Generate', cli=True), - Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), - # SNI - Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', - '.apsmw', '.apl2ac')), - Component('Links Awakening DX Client', 'LinksAwakeningClient', - file_identifier=SuffixIdentifier('.apladx')), - Component('LttP Adjuster', 'LttPAdjuster'), - # Factorio - Component('Factorio Client', 'FactorioClient'), - # Minecraft - Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True, - file_identifier=SuffixIdentifier('.apmc')), - # Ocarina of Time - Component('OoT Client', 'OoTClient', - file_identifier=SuffixIdentifier('.apz5')), - Component('OoT Adjuster', 'OoTAdjuster'), - # FF1 - Component('FF1 Client', 'FF1Client'), - # Pokémon - Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), - # TLoZ - Component('Zelda 1 Client', 'Zelda1Client'), - # ChecksFinder - Component('ChecksFinder Client', 'ChecksFinderClient'), - # Starcraft 2 - Component('Starcraft 2 Client', 'Starcraft2Client'), - # Wargroove - Component('Wargroove Client', 'WargrooveClient'), - # Zillion - Component('Zillion Client', 'ZillionClient', - file_identifier=SuffixIdentifier('.apzl')), - #Kingdom Hearts 2 - Component('KH2 Client', "KH2Client"), +components.extend([ # Functions Component('Open host.yaml', func=open_host_yaml), Component('Open Patch', func=open_patch), Component('Browse Files', func=browse_files), -) -icon_paths = { - 'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'), - 'mcicon': local_path('data', 'mcicon.ico') -} +]) def identify(path: Union[None, str]): diff --git a/inno_setup.iss b/inno_setup.iss index 815f2c01..4587396a 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -105,6 +105,7 @@ Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, E Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp +Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion; Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio diff --git a/setup.py b/setup.py index 5f109d7a..4e54fab5 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ if __name__ == "__main__": ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) ModuleUpdate.update_ran = False # restore for later -from Launcher import components, icon_paths +from worlds.LauncherComponents import components, icon_paths from Utils import version_tuple, is_windows, is_linux diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py new file mode 100644 index 00000000..7bf3ea29 --- /dev/null +++ b/worlds/LauncherComponents.py @@ -0,0 +1,106 @@ +from enum import Enum, auto +from typing import Optional, Callable, List, Iterable + +from Utils import local_path, is_windows + + +class Type(Enum): + TOOL = auto() + FUNC = auto() # not a real component + CLIENT = auto() + ADJUSTER = auto() + + +class Component: + display_name: str + type: Optional[Type] + script_name: Optional[str] + frozen_name: Optional[str] + icon: str # just the name, no suffix + cli: bool + func: Optional[Callable] + file_identifier: Optional[Callable[[str], bool]] + + def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, + cli: bool = False, icon: str = 'icon', component_type: Type = None, func: Optional[Callable] = None, + file_identifier: Optional[Callable[[str], bool]] = None): + self.display_name = display_name + self.script_name = script_name + self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None + self.icon = icon + self.cli = cli + self.type = component_type or \ + None if not display_name else \ + Type.FUNC if func else \ + Type.CLIENT if 'Client' in display_name else \ + Type.ADJUSTER if 'Adjuster' in display_name else Type.TOOL + self.func = func + self.file_identifier = file_identifier + + def handles_file(self, path: str): + return self.file_identifier(path) if self.file_identifier else False + + def __repr__(self): + return f"{self.__class__.__name__}({self.display_name})" + + +class SuffixIdentifier: + suffixes: Iterable[str] + + def __init__(self, *args: str): + self.suffixes = args + + def __call__(self, path: str): + if isinstance(path, str): + for suffix in self.suffixes: + if path.endswith(suffix): + return True + return False + + +components: List[Component] = [ + # Launcher + Component('', 'Launcher'), + # Core + Component('Host', 'MultiServer', 'ArchipelagoServer', cli=True, + file_identifier=SuffixIdentifier('.archipelago', '.zip')), + Component('Generate', 'Generate', cli=True), + Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'), + # SNI + Component('SNI Client', 'SNIClient', + file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', + '.apsmw', '.apl2ac')), + Component('Links Awakening DX Client', 'LinksAwakeningClient', + file_identifier=SuffixIdentifier('.apladx')), + Component('LttP Adjuster', 'LttPAdjuster'), + # Minecraft + Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True, + file_identifier=SuffixIdentifier('.apmc')), + # Ocarina of Time + Component('OoT Client', 'OoTClient', + file_identifier=SuffixIdentifier('.apz5')), + Component('OoT Adjuster', 'OoTAdjuster'), + # FF1 + Component('FF1 Client', 'FF1Client'), + # Pokémon + Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), + # TLoZ + Component('Zelda 1 Client', 'Zelda1Client'), + # ChecksFinder + Component('ChecksFinder Client', 'ChecksFinderClient'), + # Starcraft 2 + Component('Starcraft 2 Client', 'Starcraft2Client'), + # Wargroove + Component('Wargroove Client', 'WargrooveClient'), + # Zillion + Component('Zillion Client', 'ZillionClient', + file_identifier=SuffixIdentifier('.apzl')), + #Kingdom Hearts 2 + Component('KH2 Client', "KH2Client"), +] + + +icon_paths = { + 'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'), + 'mcicon': local_path('data', 'mcicon.ico') +} diff --git a/worlds/__init__.py b/worlds/__init__.py index 3470c1a3..e2ebb786 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -40,6 +40,9 @@ class WorldSource(typing.NamedTuple): path: str # typically relative path from this module is_zip: bool = False + def __repr__(self): + return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip})" + # find potential world containers, currently folders and zip-importable .apworld's world_sources: typing.List[WorldSource] = [] @@ -55,24 +58,35 @@ for file in os.scandir(folder): # import all submodules to trigger AutoWorldRegister world_sources.sort() for world_source in world_sources: - if world_source.is_zip: - importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) - if hasattr(importer, "find_spec"): # new in Python 3.10 - spec = importer.find_spec(world_source.path.split(".", 1)[0]) - mod = importlib.util.module_from_spec(spec) - else: # TODO: remove with 3.8 support - mod = importer.load_module(world_source.path.split(".", 1)[0]) + try: + if world_source.is_zip: + importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) + if hasattr(importer, "find_spec"): # new in Python 3.10 + spec = importer.find_spec(world_source.path.split(".", 1)[0]) + mod = importlib.util.module_from_spec(spec) + else: # TODO: remove with 3.8 support + mod = importer.load_module(world_source.path.split(".", 1)[0]) - mod.__package__ = f"worlds.{mod.__package__}" - mod.__name__ = f"worlds.{mod.__name__}" - sys.modules[mod.__name__] = mod - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") - # Found no equivalent for < 3.10 - if hasattr(importer, "exec_module"): - importer.exec_module(mod) - else: - importlib.import_module(f".{world_source.path}", "worlds") + mod.__package__ = f"worlds.{mod.__package__}" + mod.__name__ = f"worlds.{mod.__name__}" + sys.modules[mod.__name__] = mod + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") + # Found no equivalent for < 3.10 + if hasattr(importer, "exec_module"): + importer.exec_module(mod) + else: + importlib.import_module(f".{world_source.path}", "worlds") + except Exception as e: + # A single world failing can still mean enough is working for the user, log and carry on + import traceback + import io + file_like = io.StringIO() + print(f"Could not load world {world_source}:", file=file_like) + traceback.print_exc(file=file_like) + file_like.seek(0) + import logging + logging.exception(file_like.read()) lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 6391701d..567ab0bb 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -15,6 +15,9 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ fluids, stacking_items, valid_ingredients, progressive_rows from .Locations import location_pools, location_table +from worlds.LauncherComponents import Component, components + +components.append(Component("Factorio Client", "FactorioClient")) class FactorioWeb(WebWorld):