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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_as_textclient():
 | 
			
		||||
def run_as_textclient(*args):
 | 
			
		||||
    class TextContext(CommonContext):
 | 
			
		||||
        # Text Mode to use !hint and such with games that have no text entry
 | 
			
		||||
        tags = CommonContext.tags | {"TextOnly"}
 | 
			
		||||
| 
						 | 
				
			
			@ -1033,7 +1033,7 @@ def run_as_textclient():
 | 
			
		|||
    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("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:
 | 
			
		||||
        url = urllib.parse.urlparse(args.url)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										98
									
								
								Launcher.py
								
								
								
								
							
							
						
						
									
										98
									
								
								Launcher.py
								
								
								
								
							| 
						 | 
				
			
			@ -16,10 +16,11 @@ import multiprocessing
 | 
			
		|||
import shlex
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
import urllib.parse
 | 
			
		||||
import webbrowser
 | 
			
		||||
from os.path import isfile
 | 
			
		||||
from shutil import which
 | 
			
		||||
from typing import Callable, Sequence, Union, Optional
 | 
			
		||||
from typing import Callable, Optional, Sequence, Tuple, Union
 | 
			
		||||
 | 
			
		||||
import Utils
 | 
			
		||||
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:
 | 
			
		||||
        return None, None
 | 
			
		||||
    for component in components:
 | 
			
		||||
| 
						 | 
				
			
			@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
 | 
			
		|||
    elif not args:
 | 
			
		||||
        args = {}
 | 
			
		||||
 | 
			
		||||
    if args.get("Patch|Game|Component", None) is not None:
 | 
			
		||||
        file, component = identify(args["Patch|Game|Component"])
 | 
			
		||||
    path = args.get("Patch|Game|Component|url", None)
 | 
			
		||||
    if path is not None:
 | 
			
		||||
        if path.startswith("archipelago://"):
 | 
			
		||||
            handle_uri(path, args.get("args", ()))
 | 
			
		||||
            return
 | 
			
		||||
        file, component = identify(path)
 | 
			
		||||
        if file:
 | 
			
		||||
            args['file'] = file
 | 
			
		||||
        if component:
 | 
			
		||||
            args['component'] = 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"]:
 | 
			
		||||
        update_settings()
 | 
			
		||||
    if 'file' in args:
 | 
			
		||||
    if "file" in args:
 | 
			
		||||
        run_component(args["component"], args["file"], *args["args"])
 | 
			
		||||
    elif 'component' in args:
 | 
			
		||||
    elif "component" in args:
 | 
			
		||||
        run_component(args["component"], *args["args"])
 | 
			
		||||
    elif not args["update_settings"]:
 | 
			
		||||
        run_gui()
 | 
			
		||||
| 
						 | 
				
			
			@ -326,8 +405,9 @@ if __name__ == '__main__':
 | 
			
		|||
    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("Patch|Game|Component|url", type=str, nargs="?",
 | 
			
		||||
                           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="*",
 | 
			
		||||
                           help="Arguments to pass to component.")
 | 
			
		||||
    main(parser.parse_args())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,7 @@
 | 
			
		|||
            {% for patch in room.seed.slots|list|sort(attribute="player_id") %}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <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>
 | 
			
		||||
                        {% 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"; ValueName: "URL Protocol"; ValueData: "";
 | 
			
		||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
 | 
			
		||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
 | 
			
		||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0";
 | 
			
		||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
 | 
			
		||||
 | 
			
		||||
[Code]
 | 
			
		||||
// See: https://stackoverflow.com/a/51614652/2287576
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,10 +26,13 @@ class Component:
 | 
			
		|||
    cli: bool
 | 
			
		||||
    func: Optional[Callable]
 | 
			
		||||
    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,
 | 
			
		||||
                 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.script_name = script_name
 | 
			
		||||
        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)
 | 
			
		||||
        self.func = func
 | 
			
		||||
        self.file_identifier = file_identifier
 | 
			
		||||
        self.game_name = game_name
 | 
			
		||||
        self.supports_uri = supports_uri
 | 
			
		||||
 | 
			
		||||
    def handles_file(self, path: str):
 | 
			
		||||
        return self.file_identifier(path) if self.file_identifier else False
 | 
			
		||||
| 
						 | 
				
			
			@ -56,10 +61,10 @@ class Component:
 | 
			
		|||
processes = weakref.WeakSet()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def launch_subprocess(func: Callable, name: str = None):
 | 
			
		||||
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()):
 | 
			
		||||
    global processes
 | 
			
		||||
    import multiprocessing
 | 
			
		||||
    process = multiprocessing.Process(target=func, name=name)
 | 
			
		||||
    process = multiprocessing.Process(target=func, name=name, args=args)
 | 
			
		||||
    process.start()
 | 
			
		||||
    processes.add(process)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -78,9 +83,9 @@ class SuffixIdentifier:
 | 
			
		|||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def launch_textclient():
 | 
			
		||||
def launch_textclient(*args):
 | 
			
		||||
    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]]:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shu
 | 
			
		|||
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
 | 
			
		||||
 | 
			
		||||
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 logging
 | 
			
		||||
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"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def launch_game(url: Optional[str] = None) -> None:
 | 
			
		||||
def launch_game(*args) -> None:
 | 
			
		||||
    """Check the game installation, then launch it"""
 | 
			
		||||
    def courier_installed() -> bool:
 | 
			
		||||
        """Check if Courier is installed"""
 | 
			
		||||
| 
						 | 
				
			
			@ -150,15 +151,19 @@ def launch_game(url: Optional[str] = None) -> None:
 | 
			
		|||
                install_mod()
 | 
			
		||||
            elif should_update is None:
 | 
			
		||||
                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 url:
 | 
			
		||||
            open_file(f"steam://rungameid/764790//{url}/")
 | 
			
		||||
        if args.url:
 | 
			
		||||
            open_file(f"steam://rungameid/764790//{args.url}/")
 | 
			
		||||
        else:
 | 
			
		||||
            open_file("steam://rungameid/764790")
 | 
			
		||||
    else:
 | 
			
		||||
        os.chdir(game_folder)
 | 
			
		||||
        if url:
 | 
			
		||||
            subprocess.Popen([MessengerWorld.settings.game_path, str(url)])
 | 
			
		||||
        if args.url:
 | 
			
		||||
            subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)])
 | 
			
		||||
        else:
 | 
			
		||||
            subprocess.Popen(MessengerWorld.settings.game_path)
 | 
			
		||||
        os.chdir(working_directory)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue