| 
									
										
										
										
											2022-03-31 05:08:15 +02: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 | 
					
						
							|  |  |  | import itertools | 
					
						
							|  |  |  | import shlex | 
					
						
							| 
									
										
										
										
											2022-08-27 02:28:46 +02:00
										 |  |  | import subprocess | 
					
						
							|  |  |  | import sys | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  | from enum import Enum, auto | 
					
						
							| 
									
										
										
										
											2022-08-27 02:28:46 +02:00
										 |  |  | from os.path import isfile | 
					
						
							|  |  |  | from shutil import which | 
					
						
							|  |  |  | from typing import Iterable, Sequence, Callable, Union, Optional | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-04 23:43:03 +02:00
										 |  |  | if __name__ == "__main__": | 
					
						
							|  |  |  |     import ModuleUpdate | 
					
						
							|  |  |  |     ModuleUpdate.update() | 
					
						
							| 
									
										
										
										
											2022-08-27 02:28:46 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ | 
					
						
							|  |  |  |     is_windows, is_macos, is_linux | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 18:36:50 +02: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 05:08:15 +02:00
										 |  |  |     try: | 
					
						
							| 
									
										
										
										
											2022-06-04 18:36:50 +02:00
										 |  |  |         filename = open_filename('Select patch', (('Patches', suffixes),)) | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  |     except Exception as e: | 
					
						
							| 
									
										
										
										
											2022-06-04 18:36:50 +02:00
										 |  |  |         messagebox('Error', str(e), error=True) | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  |     else: | 
					
						
							| 
									
										
										
										
											2022-04-02 04:49:27 +02:00
										 |  |  |         file, _, component = identify(filename) | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  |         if file and component: | 
					
						
							| 
									
										
										
										
											2022-04-02 04:49:27 +02:00
										 |  |  |             launch([*get_exe(component), file], component.cli) | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02: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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-04 23:43:03 +02:00
										 |  |  | # noinspection PyArgumentList | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  | 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', | 
					
						
							| 
									
										
										
										
											2022-09-29 14:16:59 -04:00
										 |  |  |               file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw')), | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  |     Component('LttP Adjuster', 'LttPAdjuster'), | 
					
						
							|  |  |  |     # Factorio | 
					
						
							|  |  |  |     Component('Factorio Client', 'FactorioClient'), | 
					
						
							|  |  |  |     # Minecraft | 
					
						
							| 
									
										
										
										
											2022-04-02 04:49:27 +02:00
										 |  |  |     Component('Minecraft Client', 'MinecraftClient', icon='mcicon', cli=True, | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02: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'), | 
					
						
							| 
									
										
										
										
											2022-10-13 01:45:52 -04:00
										 |  |  |     # Pokémon | 
					
						
							|  |  |  |     Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  |     # ChecksFinder | 
					
						
							|  |  |  |     Component('ChecksFinder Client', 'ChecksFinderClient'), | 
					
						
							| 
									
										
										
										
											2022-05-18 17:27:38 -04:00
										 |  |  |     # Starcraft 2 | 
					
						
							| 
									
										
										
										
											2022-05-20 00:55:12 +02:00
										 |  |  |     Component('Starcraft 2 Client', 'Starcraft2Client'), | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02: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 04:49:27 +02:00
										 |  |  |         return None, None, None | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  |     for component in components: | 
					
						
							|  |  |  |         if component.handles_file(path): | 
					
						
							| 
									
										
										
										
											2022-04-02 04:49:27 +02:00
										 |  |  |             return path, component.script_name, component | 
					
						
							|  |  |  |     return (None, None, None) if '/' in path or '\\' in path else (None, path, None) | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02: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(): | 
					
						
							| 
									
										
										
										
											2022-06-17 20:05:09 +02:00
										 |  |  |     from kvui import App, ContainerLayout, GridLayout, Button, Label | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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 04:49:27 +02:00
										 |  |  |         file, component, _ = identify(args["Patch|Game|Component"]) | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02: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()) |