2022-03-31 03:08:15 +00:00
|
|
|
"""
|
|
|
|
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
|
2022-06-04 16:10:34 +00:00
|
|
|
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox,\
|
|
|
|
is_windows, is_macos, is_linux
|
2022-03-31 03:08:15 +00:00
|
|
|
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():
|
2022-06-04 16:36:50 +00:00
|
|
|
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 []
|
2022-03-31 03:08:15 +00:00
|
|
|
try:
|
2022-06-04 16:36:50 +00:00
|
|
|
filename = open_filename('Select patch', (('Patches', suffixes),))
|
2022-03-31 03:08:15 +00:00
|
|
|
except Exception as e:
|
2022-06-04 16:36:50 +00:00
|
|
|
messagebox('Error', str(e), error=True)
|
2022-03-31 03:08:15 +00:00
|
|
|
else:
|
2022-04-02 02:49:27 +00:00
|
|
|
file, _, component = identify(filename)
|
2022-03-31 03:08:15 +00:00
|
|
|
if file and component:
|
2022-04-02 02:49:27 +00:00
|
|
|
launch([*get_exe(component), file], component.cli)
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2022-04-02 02:49:27 +00:00
|
|
|
Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True,
|
2022-03-31 03:08:15 +00:00
|
|
|
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'),
|
2022-05-18 21:27:38 +00:00
|
|
|
# Starcraft 2
|
2022-05-19 22:55:12 +00:00
|
|
|
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
2022-03-31 03:08:15 +00:00
|
|
|
# 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:
|
2022-04-02 02:49:27 +00:00
|
|
|
return None, None, None
|
2022-03-31 03:08:15 +00:00
|
|
|
for component in components:
|
|
|
|
if component.handles_file(path):
|
2022-04-02 02:49:27 +00:00
|
|
|
return path, component.script_name, component
|
|
|
|
return (None, None, None) if '/' in path or '\\' in path else (None, path, None)
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
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:
|
2022-04-02 02:49:27 +00:00
|
|
|
file, component, _ = identify(args["Patch|Game|Component"])
|
2022-03-31 03:08:15 +00:00
|
|
|
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())
|