292 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			292 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""
 | 
						|
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, open_filename, messagebox,\
 | 
						|
    is_windows, is_macos, is_linux
 | 
						|
from shutil import which
 | 
						|
import shlex
 | 
						|
from enum import Enum, auto
 | 
						|
 | 
						|
 | 
						|
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():
 | 
						|
    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 []
 | 
						|
    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:
 | 
						|
            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():
 | 
						|
    from kvui import App, ContainerLayout, GridLayout, Button, 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())
 |