 265ee7098a
			
		
	
	265ee7098a
	
	
	
		
			
			* Option RangeWithSpecialMax * amendment to typing in web options * compare string with number * lots of work on zillion * fix zillion fill logic * fix a few more issues in zillion fill logic * can make zillion patch and use it * put multi items in zillion rom * work on ZillionClient * logging and auth in client * work on sending and receiving items * implement item_handling flag * fix locations ids to NuktiServer package * use rewrite of zri * cache logic rule data for performance * use new id maps * fix some problems with the big recent merge * ZillionClient: use new context manager for Memory class * fix ItemClassification for Zillion items and some debug statements for asserts, documentation on running scripts for manual testing type correction in CommonContext * fix some issues in client, start on docs, put rescue and item ram addresses in slot data * use new location name system fix item locations getting out of sync in progression balancing * zillion client can read slot name from game * zillion: new item names * remove extra unneeded import * newer options (room gen and starting cards) * update comment in zillion patch * zillion non static regions * change some logging, update some comments * allow ZillionClient to exit in certain situations * todo note to fix options doc strings * don't force auto forfeit * rework validation of floppy requirement and item counts and fix race condition in generate_output * reorganize Zillion component structure with System class * documentation updates for Zillion * attempt inno_setup.iss * remove todo comment for something done * update comment * rework item count zillion options and some small cleanups * fix location check count * data package version 1 * Zillion can pass unit tests without rom * fix freeze if closing ZillionClient while it's waiting for server login * specify commit hash for zilliandomizer package * some changes to options validation * Zillion doors saved on multiworld server * add missing function in inno_setup and name of vanilla continues in options * rework zillion sync task and context * Apply documentation suggestions from SoldierofOrder Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * update zillion package * workaround for asyncio udp bug There is a bug in Python in Windows https://github.com/python/cpython/issues/91227 that makes it so if I look for RetroArch before it's ready, it breaks the asyncio udp transport system. As a workaround, we don't look for RetroArch until the user asks for it with /sms * a few of the smaller suggestions from review * logic only looks at my locations instead of all the multiworld locations * some adjustments from pull request discussion and some unit tests * patch webhost changes from pull request discussion * zillion logic tests * better vblr test * test interaction of character rescue items with logic * move unit tests to new worlds folder * comment improvements * fix minor logic issue and add memory read timeout * capitalization in option display names Opa-Opa is a proper noun * redirect zz stdout to debug * fix option validation bug making unbeatable seeds * remove line that does nothing * attach logic cache to world Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Co-authored-by: Doug Hoskisson <doughoskisson@novuslabs.com>
		
			
				
	
	
		
			303 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			303 lines
		
	
	
		
			10 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
 | |
| 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
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     import ModuleUpdate
 | |
|     ModuleUpdate.update()
 | |
| 
 | |
| from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
 | |
|     is_windows, is_macos, is_linux
 | |
| 
 | |
| 
 | |
| 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)
 | |
| 
 | |
| 
 | |
| # 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')),
 | |
|     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')),
 | |
|     # ChecksFinder
 | |
|     Component('ChecksFinder Client', 'ChecksFinderClient'),
 | |
|     # Starcraft 2
 | |
|     Component('Starcraft 2 Client', 'Starcraft2Client'),
 | |
|     # Zillion
 | |
|     Component('Zillion Client', 'ZillionClient',
 | |
|               file_identifier=SuffixIdentifier('.apzl')),
 | |
|     # 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())
 |