From c2c998adcd5408e235b57a3fff9ab0acb78c0be2 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sun, 13 Aug 2023 18:13:17 +0200 Subject: [PATCH 1/6] Created source subclass, improved RetroArch exec Steam RetroArch still not working on my machine. --- src/importer/sources/flatpak_source.py | 4 +-- src/importer/sources/legendary_source.py | 8 +++-- src/importer/sources/retroarch_source.py | 45 +++++++++++++++++++----- src/importer/sources/source.py | 23 +++++++++--- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/importer/sources/flatpak_source.py b/src/importer/sources/flatpak_source.py index ee4ebbe..b4af7c4 100644 --- a/src/importer/sources/flatpak_source.py +++ b/src/importer/sources/flatpak_source.py @@ -26,7 +26,7 @@ from gi.repository import GLib, Gtk from src import shared from src.game import Game from src.importer.sources.location import Location, LocationSubPath -from src.importer.sources.source import Source, SourceIterable +from src.importer.sources.source import ExecutableFormatSource, SourceIterable class FlatpakSourceIterable(SourceIterable): @@ -116,7 +116,7 @@ class FlatpakLocations(NamedTuple): data: Location -class FlatpakSource(Source): +class FlatpakSource(ExecutableFormatSource): """Generic Flatpak source""" source_id = "flatpak" diff --git a/src/importer/sources/legendary_source.py b/src/importer/sources/legendary_source.py index 03bbdd2..6c46ada 100644 --- a/src/importer/sources/legendary_source.py +++ b/src/importer/sources/legendary_source.py @@ -26,7 +26,11 @@ from typing import NamedTuple from src import shared from src.game import Game from src.importer.sources.location import Location, LocationSubPath -from src.importer.sources.source import Source, SourceIterationResult, SourceIterable +from src.importer.sources.source import ( + ExecutableFormatSource, + SourceIterationResult, + SourceIterable, +) class LegendarySourceIterable(SourceIterable): @@ -93,7 +97,7 @@ class LegendaryLocations(NamedTuple): config: Location -class LegendarySource(Source): +class LegendarySource(ExecutableFormatSource): source_id = "legendary" name = _("Legendary") executable_format = "legendary launch {app_name}" diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 96b16e1..b5dc4fb 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -26,6 +26,8 @@ from pathlib import Path from time import time from typing import NamedTuple +from urllib.parse import quote + from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game @@ -103,7 +105,7 @@ class RetroarchSourceIterable(SourceIterable): "added": added_time, "name": item["label"], "game_id": self.source.game_id_format.format(game_id=game_id), - "executable": self.source.executable_format.format( + "executable": self.source.make_executable( rom_path=item["path"], core_path=core_path, ), @@ -168,14 +170,6 @@ class RetroarchSource(Source): ) ) - @property - def executable_format(self): - self.locations.config.resolve() - is_flatpak = self.locations.config.root.is_relative_to(shared.flatpak_dir) - base = "flatpak run org.libretro.RetroArch" if is_flatpak else "retroarch" - args = '-L "{core_path}" "{rom_path}"' - return f"{base} {args}" - def __init__(self) -> None: super().__init__() try: @@ -214,3 +208,36 @@ class RetroarchSource(Source): return Path(f"{library_path}/steamapps/common/RetroArch") # Not found raise ValueError("RetroArch not found in Steam library") + + def make_executable(self, rom_path: Path, core_path: Path) -> str: + """ + Generate an executable command from the rom path and core path, + depending on the source's location. + + The format depends on RetroArch's installation method, + detected from the source config location + + :param Path rom_path: the game's rom path + :param Path core_path: the game's core path + :return str: an executable command + """ + + self.locations.config.resolve() + args = f'-L "{core_path}" "{rom_path}"' + + # Steam RetroArch + # (Must check before Flatpak, because Steam itself can be installed as one) + if self.locations.config.root.parent.parent.name == "steamapps": + uri = "steam://run/1118310//" + quote(args) + "/" + return f"xdg-open {uri}" + + # Flatpak RetroArch + if self.locations.config.root.is_relative_to(shared.flatpak_dir): + return f"flatpak run org.libretro.RetroArch {args}" + + # TODO executable override for non-sandboxed sources + + # Linux native RetroArch + return f"retroarch {args}" + + # TODO implement for windows (needs override) diff --git a/src/importer/sources/source.py b/src/importer/sources/source.py index 26b2a84..e434d14 100644 --- a/src/importer/sources/source.py +++ b/src/importer/sources/source.py @@ -75,10 +75,12 @@ class Source(Iterable): def is_available(self): return sys.platform in self.available_on - @property @abstractmethod - def executable_format(self) -> str: - """The executable format used to construct game executables""" + def make_executable(self, *args, **kwargs) -> str: + """ + Create a game executable command. + Should be implemented by child classes. + """ def __iter__(self) -> Generator[SourceIterationResult, None, None]: """ @@ -93,8 +95,21 @@ class Source(Iterable): return iter(self.iterable_class(self)) +class ExecutableFormatSource(Source): + """Source class that uses a simple executable format to start games""" + + @property + @abstractmethod + def executable_format(self) -> str: + """The executable format used to construct game executables""" + + def make_executable(self, *args, **kwargs) -> str: + """Use the executable format to""" + return self.executable_format.format(args, kwargs) + + # pylint: disable=abstract-method -class URLExecutableSource(Source): +class URLExecutableSource(ExecutableFormatSource): """Source class that use custom URLs to start games""" url_format: str From 86ac95641ca6717666aaf54e2760ff4d84928b36 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sun, 13 Aug 2023 19:18:54 +0200 Subject: [PATCH 2/6] Work on Retroarch Steam executable --- src/importer/sources/retroarch_source.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index b5dc4fb..7556f86 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -26,8 +26,6 @@ from pathlib import Path from time import time from typing import NamedTuple -from urllib.parse import quote - from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game @@ -223,13 +221,20 @@ class RetroarchSource(Source): """ self.locations.config.resolve() - args = f'-L "{core_path}" "{rom_path}"' + args = f"-L '{core_path}' '{rom_path}'" # Steam RetroArch # (Must check before Flatpak, because Steam itself can be installed as one) if self.locations.config.root.parent.parent.name == "steamapps": - uri = "steam://run/1118310//" + quote(args) + "/" - return f"xdg-open {uri}" + # TODO fix the RetroArch Steam arguments + # It seems that the space after "-L" is parsed as the value for that arg. + # The URI protocol is proprietary, a community doc is available: + # https://developer.valvesoftware.com/wiki/Steam_browser_protocol + # ... But it doesn't sepcify HOW the args should be formatted. + # Space delimited? Quoted individually? URL-Encoded? + # I don't know. It no workie :D + uri = f"steam://run/1118310//{args}/" + return f'xdg-open "{uri}"' # Flatpak RetroArch if self.locations.config.root.is_relative_to(shared.flatpak_dir): From 1c2c844f898de2c6f008ecaf5ed1030f4d136293 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Tue, 15 Aug 2023 02:52:17 +0200 Subject: [PATCH 3/6] Disabled Steam RetroArch candidate --- src/importer/sources/retroarch_source.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 7556f86..260dae7 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -170,6 +170,11 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() + # TODO enable when RetroArch Steam's executable issue is resolved + # self.add_steam_location_candidate() + + def add_steam_location_candidate(self) -> None: + """Add the Steam RetroAcrh location to the config candidates""" try: self.locations.config.candidates.append(self.get_steam_location()) except (OSError, KeyError, UnresolvableLocationError): @@ -225,16 +230,10 @@ class RetroarchSource(Source): # Steam RetroArch # (Must check before Flatpak, because Steam itself can be installed as one) - if self.locations.config.root.parent.parent.name == "steamapps": - # TODO fix the RetroArch Steam arguments - # It seems that the space after "-L" is parsed as the value for that arg. - # The URI protocol is proprietary, a community doc is available: - # https://developer.valvesoftware.com/wiki/Steam_browser_protocol - # ... But it doesn't sepcify HOW the args should be formatted. - # Space delimited? Quoted individually? URL-Encoded? - # I don't know. It no workie :D - uri = f"steam://run/1118310//{args}/" - return f'xdg-open "{uri}"' + # TODO enable if/when we can pass args with steam://run + # if self.locations.config.root.parent.parent.name == "steamapps": + # uri = f"steam://run/1118310//{args}/" + # return f'xdg-open "{uri}"' # Flatpak RetroArch if self.locations.config.root.is_relative_to(shared.flatpak_dir): From f8037e25427008a396032b8510db0b808f6913f6 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Wed, 16 Aug 2023 02:34:59 +0200 Subject: [PATCH 4/6] Improved Steam RetroArch command generation - Verified that libretro core is selected - Verified that rom is selected - Ensure proper quoting --- src/importer/sources/retroarch_source.py | 27 ++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 260dae7..426b4ee 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -23,8 +23,10 @@ import re from hashlib import md5 from json import JSONDecodeError from pathlib import Path +from shlex import quote as shell_quote from time import time from typing import NamedTuple +from urllib.parse import quote as url_quote from src import shared from src.errors.friendly_error import FriendlyError @@ -104,8 +106,8 @@ class RetroarchSourceIterable(SourceIterable): "name": item["label"], "game_id": self.source.game_id_format.format(game_id=game_id), "executable": self.source.make_executable( - rom_path=item["path"], core_path=core_path, + rom_path=item["path"], ), } @@ -170,8 +172,7 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() - # TODO enable when RetroArch Steam's executable issue is resolved - # self.add_steam_location_candidate() + self.add_steam_location_candidate() def add_steam_location_candidate(self) -> None: """Add the Steam RetroAcrh location to the config candidates""" @@ -212,7 +213,7 @@ class RetroarchSource(Source): # Not found raise ValueError("RetroArch not found in Steam library") - def make_executable(self, rom_path: Path, core_path: Path) -> str: + def make_executable(self, core_path: Path, rom_path: Path) -> str: """ Generate an executable command from the rom path and core path, depending on the source's location. @@ -226,22 +227,26 @@ class RetroarchSource(Source): """ self.locations.config.resolve() - args = f"-L '{core_path}' '{rom_path}'" + args = ("-L", core_path, rom_path) # Steam RetroArch # (Must check before Flatpak, because Steam itself can be installed as one) - # TODO enable if/when we can pass args with steam://run - # if self.locations.config.root.parent.parent.name == "steamapps": - # uri = f"steam://run/1118310//{args}/" - # return f'xdg-open "{uri}"' + if self.locations.config.root.parent.parent.name == "steamapps": + # steam://run exepects args to be url-encoded and separated by spaces. + args = map(lambda s: url_quote(str(s), safe=""), args) + args_str = " ".join(args) + uri = f"steam://run/1118310//{args_str}/" + return f"xdg-open {shell_quote(uri)}" # Flatpak RetroArch + args = map(lambda s: shell_quote(str(s)), args) + args_str = " ".join(args) if self.locations.config.root.is_relative_to(shared.flatpak_dir): - return f"flatpak run org.libretro.RetroArch {args}" + return f"flatpak run org.libretro.RetroArch {args_str}" # TODO executable override for non-sandboxed sources # Linux native RetroArch - return f"retroarch {args}" + return f"retroarch {args_str}" # TODO implement for windows (needs override) From 195f7dbb7eb4ad2a9c8b685e59aa9d8693890a96 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 17 Aug 2023 14:07:04 +0200 Subject: [PATCH 5/6] Disabled Steam RetroArch to merge improvements to main --- src/importer/sources/retroarch_source.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 426b4ee..9eb3ee6 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -172,7 +172,8 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() - self.add_steam_location_candidate() + # TODO enable when we get the Steam RetroArch games work + # self.add_steam_location_candidate() def add_steam_location_candidate(self) -> None: """Add the Steam RetroAcrh location to the config candidates""" @@ -231,12 +232,13 @@ class RetroarchSource(Source): # Steam RetroArch # (Must check before Flatpak, because Steam itself can be installed as one) - if self.locations.config.root.parent.parent.name == "steamapps": - # steam://run exepects args to be url-encoded and separated by spaces. - args = map(lambda s: url_quote(str(s), safe=""), args) - args_str = " ".join(args) - uri = f"steam://run/1118310//{args_str}/" - return f"xdg-open {shell_quote(uri)}" + # TODO enable when we get Steam RetroArch executable to work + # if self.locations.config.root.parent.parent.name == "steamapps": + # # steam://run exepects args to be url-encoded and separated by spaces. + # args = map(lambda s: url_quote(str(s), safe=""), args) + # args_str = " ".join(args) + # uri = f"steam://run/1118310//{args_str}/" + # return f"xdg-open {shell_quote(uri)}" # Flatpak RetroArch args = map(lambda s: shell_quote(str(s)), args) From 37f838e4a2005a48f0887c74da3d2306fd81188c Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Thu, 17 Aug 2023 14:13:55 +0200 Subject: [PATCH 6/6] Added back locations --- src/importer/sources/retroarch_source.py | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py index 796c7ee..7e6beea 100644 --- a/src/importer/sources/retroarch_source.py +++ b/src/importer/sources/retroarch_source.py @@ -151,6 +151,31 @@ class RetroarchSource(Source): def __init__(self) -> None: super().__init__() + self.locations = RetroarchLocations( + Location( + schema_key="retroarch-location", + candidates=[ + shared.flatpak_dir + / "org.libretro.RetroArch" + / "config" + / "retroarch", + shared.config_dir / "retroarch", + shared.home / ".config" / "retroarch", + # TODO: Windows support, waiting for executable path setting improvement + # Path("C:\\RetroArch-Win64"), + # Path("C:\\RetroArch-Win32"), + # TODO: UWP support (URL handler - https://github.com/libretro/RetroArch/pull/13563) + # shared.local_appdata_dir + # / "Packages" + # / "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_8ngdn9a6dx1ma" + # / "LocalState", + ], + paths={ + "retroarch.cfg": LocationSubPath("retroarch.cfg"), + }, + invalid_subtitle=Location.CONFIG_INVALID_SUBTITLE, + ) + ) # TODO enable when we get the Steam RetroArch games work # self.add_steam_location_candidate()