Core: have webhost slot name links go through the launcher (#2779)
* Core: have webhost slot name links go through the launcher so that components can use them * fix query handling, remove debug prints, and change mousover text for new behavior * remove a missed debug and unused function * filter room id to suuid since that's what everything else uses * pass args to common client correctly * add GUI to select which client to open * remove args parsing and "require" components to parse it themselves * support for messenger since it was basically already done * use "proper" args argparsing and clean up uri handling * use a timer and auto launch text client if no component is found * change the timer to be a bit more appealing. also found a bug lmao * don't hold 5 hostage and capitalize URI ig --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
parent
a40744e6db
commit
430b71a092
|
@ -994,7 +994,7 @@ def get_base_parser(description: typing.Optional[str] = None):
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def run_as_textclient():
|
def run_as_textclient(*args):
|
||||||
class TextContext(CommonContext):
|
class TextContext(CommonContext):
|
||||||
# Text Mode to use !hint and such with games that have no text entry
|
# Text Mode to use !hint and such with games that have no text entry
|
||||||
tags = CommonContext.tags | {"TextOnly"}
|
tags = CommonContext.tags | {"TextOnly"}
|
||||||
|
@ -1033,7 +1033,7 @@ def run_as_textclient():
|
||||||
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
||||||
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
|
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
|
||||||
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(args if args else None) # this is necessary as long as CommonClient itself is launchable
|
||||||
|
|
||||||
if args.url:
|
if args.url:
|
||||||
url = urllib.parse.urlparse(args.url)
|
url = urllib.parse.urlparse(args.url)
|
||||||
|
|
98
Launcher.py
98
Launcher.py
|
@ -16,10 +16,11 @@ import multiprocessing
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Callable, Sequence, Union, Optional
|
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
import settings
|
import settings
|
||||||
|
@ -107,7 +108,81 @@ components.extend([
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]):
|
def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||||
|
url = urllib.parse.urlparse(path)
|
||||||
|
queries = urllib.parse.parse_qs(url.query)
|
||||||
|
launch_args = (path, *launch_args)
|
||||||
|
client_component = None
|
||||||
|
text_client_component = None
|
||||||
|
if "game" in queries:
|
||||||
|
game = queries["game"][0]
|
||||||
|
else: # TODO around 0.6.0 - this is for pre this change webhost uri's
|
||||||
|
game = "Archipelago"
|
||||||
|
for component in components:
|
||||||
|
if component.supports_uri and component.game_name == game:
|
||||||
|
client_component = component
|
||||||
|
elif component.display_name == "Text Client":
|
||||||
|
text_client_component = component
|
||||||
|
|
||||||
|
from kvui import App, Button, BoxLayout, Label, Clock, Window
|
||||||
|
|
||||||
|
class Popup(App):
|
||||||
|
timer_label: Label
|
||||||
|
remaining_time: Optional[int]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.title = "Connect to Multiworld"
|
||||||
|
self.icon = r"data/icon.png"
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
layout = BoxLayout(orientation="vertical")
|
||||||
|
|
||||||
|
if client_component is None:
|
||||||
|
self.remaining_time = 7
|
||||||
|
label_text = (f"A game client able to parse URIs was not detected for {game}.\n"
|
||||||
|
f"Launching Text Client in 7 seconds...")
|
||||||
|
self.timer_label = Label(text=label_text)
|
||||||
|
layout.add_widget(self.timer_label)
|
||||||
|
Clock.schedule_interval(self.update_label, 1)
|
||||||
|
else:
|
||||||
|
layout.add_widget(Label(text="Select client to open and connect with."))
|
||||||
|
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
||||||
|
|
||||||
|
text_client_button = Button(
|
||||||
|
text=text_client_component.display_name,
|
||||||
|
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||||
|
)
|
||||||
|
button_row.add_widget(text_client_button)
|
||||||
|
|
||||||
|
game_client_button = Button(
|
||||||
|
text=client_component.display_name,
|
||||||
|
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||||
|
)
|
||||||
|
button_row.add_widget(game_client_button)
|
||||||
|
|
||||||
|
layout.add_widget(button_row)
|
||||||
|
|
||||||
|
return layout
|
||||||
|
|
||||||
|
def update_label(self, dt):
|
||||||
|
if self.remaining_time > 1:
|
||||||
|
# countdown the timer and string replace the number
|
||||||
|
self.remaining_time -= 1
|
||||||
|
self.timer_label.text = self.timer_label.text.replace(
|
||||||
|
str(self.remaining_time + 1), str(self.remaining_time)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# our timer is finished so launch text client and close down
|
||||||
|
run_component(text_client_component, *launch_args)
|
||||||
|
Clock.unschedule(self.update_label)
|
||||||
|
App.get_running_app().stop()
|
||||||
|
Window.close()
|
||||||
|
|
||||||
|
Popup().run()
|
||||||
|
|
||||||
|
|
||||||
|
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
||||||
if path is None:
|
if path is None:
|
||||||
return None, None
|
return None, None
|
||||||
for component in components:
|
for component in components:
|
||||||
|
@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||||
elif not args:
|
elif not args:
|
||||||
args = {}
|
args = {}
|
||||||
|
|
||||||
if args.get("Patch|Game|Component", None) is not None:
|
path = args.get("Patch|Game|Component|url", None)
|
||||||
file, component = identify(args["Patch|Game|Component"])
|
if path is not None:
|
||||||
|
if path.startswith("archipelago://"):
|
||||||
|
handle_uri(path, args.get("args", ()))
|
||||||
|
return
|
||||||
|
file, component = identify(path)
|
||||||
if file:
|
if file:
|
||||||
args['file'] = file
|
args['file'] = file
|
||||||
if component:
|
if component:
|
||||||
args['component'] = component
|
args['component'] = component
|
||||||
if not component:
|
if not component:
|
||||||
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
logging.warning(f"Could not identify Component responsible for {path}")
|
||||||
|
|
||||||
if args["update_settings"]:
|
if args["update_settings"]:
|
||||||
update_settings()
|
update_settings()
|
||||||
if 'file' in args:
|
if "file" in args:
|
||||||
run_component(args["component"], args["file"], *args["args"])
|
run_component(args["component"], args["file"], *args["args"])
|
||||||
elif 'component' in args:
|
elif "component" in args:
|
||||||
run_component(args["component"], *args["args"])
|
run_component(args["component"], *args["args"])
|
||||||
elif not args["update_settings"]:
|
elif not args["update_settings"]:
|
||||||
run_gui()
|
run_gui()
|
||||||
|
@ -326,8 +405,9 @@ if __name__ == '__main__':
|
||||||
run_group = parser.add_argument_group("Run")
|
run_group = parser.add_argument_group("Run")
|
||||||
run_group.add_argument("--update_settings", action="store_true",
|
run_group.add_argument("--update_settings", action="store_true",
|
||||||
help="Update host.yaml and exit.")
|
help="Update host.yaml and exit.")
|
||||||
run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
|
run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?",
|
||||||
help="Pass either a patch file, a generated game or the name of a component to run.")
|
help="Pass either a patch file, a generated game, the component name to run, or a url to "
|
||||||
|
"connect with.")
|
||||||
run_group.add_argument("args", nargs="*",
|
run_group.add_argument("args", nargs="*",
|
||||||
help="Arguments to pass to component.")
|
help="Arguments to pass to component.")
|
||||||
main(parser.parse_args())
|
main(parser.parse_args())
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ patch.player_id }}</td>
|
<td>{{ patch.player_id }}</td>
|
||||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
<td data-tooltip="Connect via Game Client"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}?game={{ patch.game }}&room={{ room.id | suuid }}">{{ patch.player_name }}</a></td>
|
||||||
<td>{{ patch.game }}</td>
|
<td>{{ patch.game }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if patch.data %}
|
{% if patch.data %}
|
||||||
|
|
|
@ -228,8 +228,8 @@ Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{a
|
||||||
|
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
||||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
||||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
|
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0";
|
||||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
|
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
// See: https://stackoverflow.com/a/51614652/2287576
|
// See: https://stackoverflow.com/a/51614652/2287576
|
||||||
|
|
|
@ -26,10 +26,13 @@ class Component:
|
||||||
cli: bool
|
cli: bool
|
||||||
func: Optional[Callable]
|
func: Optional[Callable]
|
||||||
file_identifier: Optional[Callable[[str], bool]]
|
file_identifier: Optional[Callable[[str], bool]]
|
||||||
|
game_name: Optional[str]
|
||||||
|
supports_uri: Optional[bool]
|
||||||
|
|
||||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||||
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None):
|
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
||||||
|
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
self.script_name = script_name
|
self.script_name = script_name
|
||||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||||
|
@ -45,6 +48,8 @@ class Component:
|
||||||
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
|
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
|
||||||
self.func = func
|
self.func = func
|
||||||
self.file_identifier = file_identifier
|
self.file_identifier = file_identifier
|
||||||
|
self.game_name = game_name
|
||||||
|
self.supports_uri = supports_uri
|
||||||
|
|
||||||
def handles_file(self, path: str):
|
def handles_file(self, path: str):
|
||||||
return self.file_identifier(path) if self.file_identifier else False
|
return self.file_identifier(path) if self.file_identifier else False
|
||||||
|
@ -56,10 +61,10 @@ class Component:
|
||||||
processes = weakref.WeakSet()
|
processes = weakref.WeakSet()
|
||||||
|
|
||||||
|
|
||||||
def launch_subprocess(func: Callable, name: str = None):
|
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()):
|
||||||
global processes
|
global processes
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
process = multiprocessing.Process(target=func, name=name)
|
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||||
process.start()
|
process.start()
|
||||||
processes.add(process)
|
processes.add(process)
|
||||||
|
|
||||||
|
@ -78,9 +83,9 @@ class SuffixIdentifier:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def launch_textclient():
|
def launch_textclient(*args):
|
||||||
import CommonClient
|
import CommonClient
|
||||||
launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
|
launch_subprocess(CommonClient.run_as_textclient, "TextClient", args)
|
||||||
|
|
||||||
|
|
||||||
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
||||||
|
|
|
@ -19,7 +19,7 @@ from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shu
|
||||||
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
||||||
|
|
||||||
components.append(
|
components.append(
|
||||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game)#, game_name="The Messenger", supports_uri=True)
|
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import argparse
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
|
@ -17,7 +18,7 @@ from Utils import is_windows, messagebox, tuplize_version
|
||||||
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
|
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
|
||||||
|
|
||||||
|
|
||||||
def launch_game(url: Optional[str] = None) -> None:
|
def launch_game(*args) -> None:
|
||||||
"""Check the game installation, then launch it"""
|
"""Check the game installation, then launch it"""
|
||||||
def courier_installed() -> bool:
|
def courier_installed() -> bool:
|
||||||
"""Check if Courier is installed"""
|
"""Check if Courier is installed"""
|
||||||
|
@ -150,15 +151,19 @@ def launch_game(url: Optional[str] = None) -> None:
|
||||||
install_mod()
|
install_mod()
|
||||||
elif should_update is None:
|
elif should_update is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Messenger Client Launcher")
|
||||||
|
parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.")
|
||||||
|
args = parser.parse_args(args)
|
||||||
if not is_windows:
|
if not is_windows:
|
||||||
if url:
|
if args.url:
|
||||||
open_file(f"steam://rungameid/764790//{url}/")
|
open_file(f"steam://rungameid/764790//{args.url}/")
|
||||||
else:
|
else:
|
||||||
open_file("steam://rungameid/764790")
|
open_file("steam://rungameid/764790")
|
||||||
else:
|
else:
|
||||||
os.chdir(game_folder)
|
os.chdir(game_folder)
|
||||||
if url:
|
if args.url:
|
||||||
subprocess.Popen([MessengerWorld.settings.game_path, str(url)])
|
subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)])
|
||||||
else:
|
else:
|
||||||
subprocess.Popen(MessengerWorld.settings.game_path)
|
subprocess.Popen(MessengerWorld.settings.game_path)
|
||||||
os.chdir(working_directory)
|
os.chdir(working_directory)
|
||||||
|
|
Loading…
Reference in New Issue