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
|
|
|
|
import itertools
|
2023-06-19 23:01:18 +00:00
|
|
|
import logging
|
2023-05-04 22:53:57 +00:00
|
|
|
import multiprocessing
|
2022-03-31 03:08:15 +00:00
|
|
|
import shlex
|
2022-08-27 00:28:46 +00:00
|
|
|
import subprocess
|
|
|
|
import sys
|
2023-04-15 23:57:52 +00:00
|
|
|
import webbrowser
|
2022-08-27 00:28:46 +00:00
|
|
|
from os.path import isfile
|
|
|
|
from shutil import which
|
2023-03-20 20:24:47 +00:00
|
|
|
from typing import Sequence, Union, Optional
|
|
|
|
|
2023-04-15 23:57:52 +00:00
|
|
|
import Utils
|
2023-07-05 20:39:35 +00:00
|
|
|
import settings
|
2023-04-17 00:35:54 +00:00
|
|
|
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
|
2022-08-27 00:28:46 +00:00
|
|
|
|
2022-09-04 21:43:03 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
import ModuleUpdate
|
|
|
|
ModuleUpdate.update()
|
2022-08-27 00:28:46 +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
|
|
|
|
|
|
|
|
|
|
|
def open_host_yaml():
|
2023-07-05 20:39:35 +00:00
|
|
|
file = settings.get_settings().filename
|
|
|
|
assert file, "host.yaml missing"
|
2022-03-31 03:08:15 +00:00
|
|
|
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:
|
|
|
|
webbrowser.open(file)
|
|
|
|
|
|
|
|
|
|
|
|
def open_patch():
|
2022-06-04 16:36:50 +00:00
|
|
|
suffixes = []
|
|
|
|
for c in components:
|
2023-10-02 18:52:00 +00:00
|
|
|
if c.type == Type.CLIENT and \
|
|
|
|
isinstance(c.file_identifier, SuffixIdentifier) and \
|
|
|
|
(c.script_name is None or isfile(get_exe(c)[-1])):
|
|
|
|
suffixes += c.file_identifier.suffixes
|
2022-03-31 03:08:15 +00:00
|
|
|
try:
|
2023-10-02 18:52:00 +00:00
|
|
|
filename = open_filename("Select patch", (("Patches", suffixes),))
|
2022-03-31 03:08:15 +00:00
|
|
|
except Exception as e:
|
2023-10-02 18:52:00 +00:00
|
|
|
messagebox("Error", str(e), error=True)
|
2022-03-31 03:08:15 +00:00
|
|
|
else:
|
2023-06-19 23:01:18 +00:00
|
|
|
file, component = identify(filename)
|
2022-03-31 03:08:15 +00:00
|
|
|
if file and component:
|
2023-10-02 18:52:00 +00:00
|
|
|
exe = get_exe(component)
|
|
|
|
if exe is None or not isfile(exe[-1]):
|
|
|
|
exe = get_exe("Launcher")
|
|
|
|
|
|
|
|
launch([*exe, file], component.cli)
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
|
2023-04-15 23:57:52 +00:00
|
|
|
def generate_yamls():
|
|
|
|
from Options import generate_yaml_templates
|
|
|
|
|
|
|
|
target = Utils.user_path("Players", "Templates")
|
|
|
|
generate_yaml_templates(target, False)
|
|
|
|
open_folder(target)
|
|
|
|
|
|
|
|
|
2022-03-31 03:08:15 +00:00
|
|
|
def browse_files():
|
2023-04-15 23:57:52 +00:00
|
|
|
open_folder(user_path())
|
|
|
|
|
|
|
|
|
|
|
|
def open_folder(folder_path):
|
2022-03-31 03:08:15 +00:00
|
|
|
if is_linux:
|
|
|
|
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
|
2023-04-15 23:57:52 +00:00
|
|
|
subprocess.Popen([exe, folder_path])
|
2022-03-31 03:08:15 +00:00
|
|
|
elif is_macos:
|
|
|
|
exe = which("open")
|
2023-04-15 23:57:52 +00:00
|
|
|
subprocess.Popen([exe, folder_path])
|
2022-03-31 03:08:15 +00:00
|
|
|
else:
|
2023-04-15 23:57:52 +00:00
|
|
|
webbrowser.open(folder_path)
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
|
2023-07-05 20:39:35 +00:00
|
|
|
def update_settings():
|
|
|
|
from settings import get_settings
|
|
|
|
get_settings().save()
|
|
|
|
|
|
|
|
|
2023-03-20 20:24:47 +00:00
|
|
|
components.extend([
|
2022-03-31 03:08:15 +00:00
|
|
|
# Functions
|
2023-04-17 00:35:54 +00:00
|
|
|
Component("Open host.yaml", func=open_host_yaml),
|
|
|
|
Component("Open Patch", func=open_patch),
|
2024-03-28 14:00:10 +00:00
|
|
|
Component("Generate Template Options", func=generate_yamls),
|
2023-04-17 00:35:54 +00:00
|
|
|
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
|
|
|
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
|
|
|
Component("Browse Files", func=browse_files),
|
2023-03-20 20:24:47 +00:00
|
|
|
])
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
def identify(path: Union[None, str]):
|
|
|
|
if path is None:
|
2023-06-19 23:01:18 +00:00
|
|
|
return None, None
|
2022-03-31 03:08:15 +00:00
|
|
|
for component in components:
|
|
|
|
if component.handles_file(path):
|
2023-10-02 18:52:00 +00:00
|
|
|
return path, component
|
2023-06-19 23:01:18 +00:00
|
|
|
elif path == component.display_name or path == component.script_name:
|
|
|
|
return None, component
|
|
|
|
return None, 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
|
2023-10-02 18:52:00 +00:00
|
|
|
if name.startswith("Archipelago"):
|
2022-03-31 03:08:15 +00:00
|
|
|
name = name[11:]
|
2023-10-02 18:52:00 +00:00
|
|
|
if name.endswith(".exe"):
|
2022-03-31 03:08:15 +00:00
|
|
|
name = name[:-4]
|
2023-10-02 18:52:00 +00:00
|
|
|
if name.endswith(".py"):
|
2022-03-31 03:08:15 +00:00
|
|
|
name = name[:-3]
|
|
|
|
if not name:
|
|
|
|
return None
|
|
|
|
for c in components:
|
2023-10-02 18:52:00 +00:00
|
|
|
if c.script_name == name or c.frozen_name == f"Archipelago{name}":
|
2022-03-31 03:08:15 +00:00
|
|
|
component = c
|
|
|
|
break
|
|
|
|
if not component:
|
|
|
|
return None
|
|
|
|
if is_frozen():
|
2023-10-02 18:52:00 +00:00
|
|
|
suffix = ".exe" if is_windows else ""
|
|
|
|
return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None
|
2022-03-31 03:08:15 +00:00
|
|
|
else:
|
2023-10-02 18:52:00 +00:00
|
|
|
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
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():
|
2024-03-03 05:32:58 +00:00
|
|
|
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
|
2023-04-30 16:10:58 +00:00
|
|
|
from kivy.uix.image import AsyncImage
|
2023-04-17 00:35:54 +00:00
|
|
|
from kivy.uix.relativelayout import RelativeLayout
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
class Launcher(App):
|
|
|
|
base_title: str = "Archipelago Launcher"
|
|
|
|
container: ContainerLayout
|
|
|
|
grid: GridLayout
|
|
|
|
|
2023-06-19 07:57:17 +00:00
|
|
|
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
|
|
|
|
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
|
|
|
|
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
|
|
|
|
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
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)
|
2024-03-03 05:32:58 +00:00
|
|
|
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
|
|
|
|
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
|
|
|
|
tool_layout = ScrollBox()
|
|
|
|
tool_layout.layout.orientation = "vertical"
|
|
|
|
self.grid.add_widget(tool_layout)
|
|
|
|
client_layout = ScrollBox()
|
|
|
|
client_layout.layout.orientation = "vertical"
|
|
|
|
self.grid.add_widget(client_layout)
|
|
|
|
|
|
|
|
def build_button(component: Component) -> Widget:
|
2023-04-17 00:35:54 +00:00
|
|
|
"""
|
|
|
|
Builds a button widget for a given component.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
component (Component): The component associated with the button.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
None. The button is added to the parent grid layout.
|
|
|
|
|
|
|
|
"""
|
2024-03-03 05:32:58 +00:00
|
|
|
button = Button(text=component.display_name, size_hint_y=None, height=40)
|
2023-04-17 00:35:54 +00:00
|
|
|
button.component = component
|
|
|
|
button.bind(on_release=self.component_action)
|
|
|
|
if component.icon != "icon":
|
2023-04-30 16:10:58 +00:00
|
|
|
image = AsyncImage(source=icon_paths[component.icon],
|
|
|
|
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
2024-03-03 05:32:58 +00:00
|
|
|
box_layout = RelativeLayout(size_hint_y=None, height=40)
|
2023-04-17 00:35:54 +00:00
|
|
|
box_layout.add_widget(button)
|
|
|
|
box_layout.add_widget(image)
|
2024-03-03 05:32:58 +00:00
|
|
|
return box_layout
|
|
|
|
return button
|
2023-04-17 00:35:54 +00:00
|
|
|
|
2022-03-31 03:08:15 +00:00
|
|
|
for (tool, client) in itertools.zip_longest(itertools.chain(
|
2023-06-19 07:57:17 +00:00
|
|
|
self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()):
|
2022-03-31 03:08:15 +00:00
|
|
|
# column 1
|
|
|
|
if tool:
|
2024-03-03 05:32:58 +00:00
|
|
|
tool_layout.layout.add_widget(build_button(tool[1]))
|
2022-03-31 03:08:15 +00:00
|
|
|
# column 2
|
|
|
|
if client:
|
2024-03-03 05:32:58 +00:00
|
|
|
client_layout.layout.add_widget(build_button(client[1]))
|
2022-03-31 03:08:15 +00:00
|
|
|
|
|
|
|
return self.container
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def component_action(button):
|
2023-06-19 07:57:17 +00:00
|
|
|
if button.component.func:
|
2022-03-31 03:08:15 +00:00
|
|
|
button.component.func()
|
|
|
|
else:
|
|
|
|
launch(get_exe(button.component), button.component.cli)
|
|
|
|
|
2023-06-27 07:30:54 +00:00
|
|
|
def _stop(self, *largs):
|
|
|
|
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
|
|
|
|
# Closing the window explicitly cleans it up.
|
|
|
|
self.root_window.close()
|
|
|
|
super()._stop(*largs)
|
|
|
|
|
2022-03-31 03:08:15 +00:00
|
|
|
Launcher().run()
|
|
|
|
|
|
|
|
|
2023-06-19 23:01:18 +00:00
|
|
|
def run_component(component: Component, *args):
|
|
|
|
if component.func:
|
|
|
|
component.func(*args)
|
|
|
|
elif component.script_name:
|
|
|
|
subprocess.run([*get_exe(component.script_name), *args])
|
|
|
|
else:
|
|
|
|
logging.warning(f"Component {component} does not appear to be executable.")
|
|
|
|
|
|
|
|
|
2022-03-31 03:08:15 +00:00
|
|
|
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:
|
2023-06-19 23:01:18 +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
|
2023-06-19 23:01:18 +00:00
|
|
|
if not component:
|
|
|
|
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
2022-03-31 03:08:15 +00:00
|
|
|
|
2023-07-05 20:39:35 +00:00
|
|
|
if args["update_settings"]:
|
|
|
|
update_settings()
|
2022-03-31 03:08:15 +00:00
|
|
|
if 'file' in args:
|
2023-06-19 23:01:18 +00:00
|
|
|
run_component(args["component"], args["file"], *args["args"])
|
2022-03-31 03:08:15 +00:00
|
|
|
elif 'component' in args:
|
2023-06-19 23:01:18 +00:00
|
|
|
run_component(args["component"], *args["args"])
|
2023-07-05 20:39:35 +00:00
|
|
|
elif not args["update_settings"]:
|
2022-03-31 03:08:15 +00:00
|
|
|
run_gui()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
init_logging('Launcher')
|
2023-06-25 00:24:43 +00:00
|
|
|
Utils.freeze_support()
|
|
|
|
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
2022-03-31 03:08:15 +00:00
|
|
|
parser = argparse.ArgumentParser(description='Archipelago Launcher')
|
2023-07-05 20:39:35 +00:00
|
|
|
run_group = parser.add_argument_group("Run")
|
|
|
|
run_group.add_argument("--update_settings", action="store_true",
|
|
|
|
help="Update host.yaml and exit.")
|
|
|
|
run_group.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.")
|
|
|
|
run_group.add_argument("args", nargs="*",
|
|
|
|
help="Arguments to pass to component.")
|
2022-03-31 03:08:15 +00:00
|
|
|
main(parser.parse_args())
|
2023-06-27 07:30:54 +00:00
|
|
|
|
|
|
|
from worlds.LauncherComponents import processes
|
|
|
|
for process in processes:
|
|
|
|
# we await all child processes to close before we tear down the process host
|
|
|
|
# this makes it feel like each one is its own program, as the Launcher is closed now
|
|
|
|
process.join()
|