""" Archipelago launcher for bundled app. * if run with APBP as argument, launch corresponding client. * if run with executable as argument, run it passing argv[2:] as arguments * if run without arguments, open launcher GUI Scroll down to components= to add components to the launcher as well as setup.py """ import argparse from os.path import isfile import sys from typing import Iterable, Sequence, Callable, Union, Optional import subprocess import itertools from Utils import is_frozen, user_path, local_path, init_logging from shutil import which import shlex from enum import Enum, auto import logging is_linux = sys.platform.startswith('linux') is_macos = sys.platform == 'darwin' is_windows = sys.platform in ("win32", "cygwin", "msys") def open_host_yaml(): file = user_path('host.yaml') if is_linux: exe = which('sensible-editor') or which('gedit') or \ which('xdg-open') or which('gnome-open') or which('kde-open') subprocess.Popen([exe, file]) elif is_macos: exe = which("open") subprocess.Popen([exe, file]) else: import webbrowser webbrowser.open(file) def open_patch(): try: import tkinter import tkinter.filedialog except Exception as e: logging.error("Could not load tkinter, which is likely not installed. " "This attempt was made because Launcher.open_patch was used.") raise e else: root = tkinter.Tk() root.withdraw() suffixes = [] for c in components: if isfile(get_exe(c)[-1]): suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \ isinstance(c.file_identifier, SuffixIdentifier) else [] filename = tkinter.filedialog.askopenfilename(filetypes=(('Patches', ' '.join(suffixes)),)) file, _, component = identify(filename) if file and component: launch([*get_exe(component), file], component.cli) def browse_files(): file = user_path() if is_linux: exe = which('xdg-open') or which('gnome-open') or which('kde-open') subprocess.Popen([exe, file]) elif is_macos: exe = which("open") subprocess.Popen([exe, file]) else: import webbrowser webbrowser.open(file) 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')), 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'), # ChecksFinder Component('ChecksFinder Client', 'ChecksFinderClient'), # Starcraft 2 Component('Starcraft 2 Client', 'Starcraft2Client'), # 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]): if path is None: return None, None, None for component in components: if component.handles_file(path): return path, component.script_name, component return (None, None, None) if '/' in path or '\\' in path else (None, path, None) def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]: 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}')] else: return [sys.executable, local_path(f'{component.script_name}.py')] def launch(exe, in_terminal=False): if in_terminal: if is_windows: subprocess.Popen(['start', *exe], shell=True) return elif is_linux: terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm') if terminal: subprocess.Popen([terminal, '-e', shlex.join(exe)]) return elif is_macos: terminal = [which('open'), '-W', '-a', 'Terminal.app'] subprocess.Popen([*terminal, *exe]) return subprocess.Popen(exe) def run_gui(): if not sys.stdout: from kvui import App, ContainerLayout, GridLayout, Button, Label # this kills stdout else: from kivy.app import App from kivy.uix.button import Button from kivy.uix.floatlayout import FloatLayout as ContainerLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.label import Label class Launcher(App): base_title: str = "Archipelago Launcher" container: ContainerLayout grid: GridLayout _tools = {c.display_name: c for c in components if c.type == Type.TOOL and isfile(get_exe(c)[-1])} _clients = {c.display_name: c for c in components if c.type == Type.CLIENT and isfile(get_exe(c)[-1])} _adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER and isfile(get_exe(c)[-1])} _funcs = {c.display_name: c for c in components if c.type == Type.FUNC} def __init__(self, ctx=None): self.title = self.base_title self.ctx = ctx self.icon = r"data/icon.png" super().__init__() def build(self): self.container = ContainerLayout() self.grid = GridLayout(cols=2) self.container.add_widget(self.grid) button_layout = self.grid # make buttons fill the window for (tool, client) in itertools.zip_longest(itertools.chain( self._tools.items(), self._funcs.items(), self._adjusters.items()), self._clients.items()): # column 1 if tool: button = Button(text=tool[0]) button.component = tool[1] button.bind(on_release=self.component_action) button_layout.add_widget(button) else: button_layout.add_widget(Label()) # column 2 if client: button = Button(text=client[0]) button.component = client[1] button.bind(on_press=self.component_action) button_layout.add_widget(button) else: button_layout.add_widget(Label()) return self.container @staticmethod def component_action(button): if button.component.type == Type.FUNC: button.component.func() else: launch(get_exe(button.component), button.component.cli) Launcher().run() def main(args: Optional[Union[argparse.Namespace, dict]] = None): if isinstance(args, argparse.Namespace): args = {k: v for k, v in args._get_kwargs()} elif not args: args = {} if "Patch|Game|Component" in args: file, component, _ = identify(args["Patch|Game|Component"]) if file: args['file'] = file if component: args['component'] = component if 'file' in args: subprocess.run([*get_exe(args['component']), args['file'], *args['args']]) elif 'component' in args: subprocess.run([*get_exe(args['component']), *args['args']]) else: run_gui() if __name__ == '__main__': init_logging('Launcher') parser = argparse.ArgumentParser(description='Archipelago Launcher') parser.add_argument('Patch|Game|Component', type=str, nargs='?', help="Pass either a patch file, a generated game or the name of a component to run.") parser.add_argument('args', nargs="*", help="Arguments to pass to component.") main(parser.parse_args())