diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp index fbc2dee..8547e44 100644 --- a/data/gtk/preferences.blp +++ b/data/gtk/preferences.blp @@ -226,6 +226,20 @@ template $PreferencesWindow : Adw.PreferencesWindow { } } + Adw.ExpanderRow retroarch_expander_row { + title: _("Retroarch"); + show-enable-switch: true; + + Adw.ActionRow retroarch_config_action_row { + title: _("Install Location"); + + Button retroarch_config_file_chooser_button { + icon-name: "folder-symbolic"; + valign: center; + } + } + } + Adw.ExpanderRow flatpak_expander_row { title: _("Flatpak"); show-enable-switch: true; diff --git a/data/hu.kramo.Cartridges.gschema.xml.in b/data/hu.kramo.Cartridges.gschema.xml.in index 4fffaad..9086a2c 100644 --- a/data/hu.kramo.Cartridges.gschema.xml.in +++ b/data/hu.kramo.Cartridges.gschema.xml.in @@ -64,6 +64,12 @@ "~/.config/legendary/" + + true + + + "~/.var/app/org.libretro.RetroArch/config/retroarch/" + true @@ -110,4 +116,4 @@ "[]" - \ No newline at end of file + diff --git a/flatpak/hu.kramo.Cartridges.Devel.json b/flatpak/hu.kramo.Cartridges.Devel.json index 1c2ecea..e74107f 100644 --- a/flatpak/hu.kramo.Cartridges.Devel.json +++ b/flatpak/hu.kramo.Cartridges.Devel.json @@ -17,6 +17,7 @@ "--filesystem=~/.var/app/com.heroicgameslauncher.hgl/config/heroic/:ro", "--filesystem=~/.var/app/com.usebottles.bottles/data/bottles/:ro", "--filesystem=~/.var/app/io.itch.itch/config/itch/:ro", + "--filesystem=~/.var/app/org.libretro.RetroArch/config/retroarch/:ro", "--filesystem=/var/lib/flatpak:ro" ], "cleanup" : [ diff --git a/src/importer/sources/retroarch_source.py b/src/importer/sources/retroarch_source.py new file mode 100644 index 0000000..b11cdcd --- /dev/null +++ b/src/importer/sources/retroarch_source.py @@ -0,0 +1,119 @@ +# retroarch_source.py +# +# Copyright 2023 Rilic +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from pathlib import Path +from time import time + +import json +import os +import logging +from json import JSONDecodeError + +from src import shared +from src.game import Game +from src.importer.sources.location import Location +from src.importer.sources.source import ( + SourceIterationResult, + SourceIterator, + Source +) + + +class RetroarchSourceIterator(SourceIterator): + source: "RetroarchSource" + + def generator_builder(self) -> SourceIterationResult: + playlist_files = [] + for file in os.listdir(self.source.config_location["playlists"]): + if file.endswith('.lpl'): + playlist_files.append(file) + + playlist_items = [] + for playlist_file in playlist_files: + open_file = open(str(self.source.config_location["playlists"]) + "/" + playlist_file) + try: + playlist_json = json.load(open_file) + except (JSONDecodeError, OSError, KeyError): + logging.warning("Cannot read playlist file: %s", str(playlist_file)) + continue + + for item in playlist_json["items"]: + # Select the core. Try the content's core first, then the playlist's + # default core. + core_path = item["core_path"] + if core_path == "DETECT": + default_core = playlist_json["default_core_path"] + if default_core: + core_path = default_core + else: + logging.warning("Cannot find core for: %s", str(item["path"])) + continue + + # Build game + game_title = item["label"].split("(", 1)[0] + values = { + "source": self.source.id, + "added": int(time()), + "name": game_title, + "game_id": self.source.game_id_format.format(game_id=item["crc32"][:8]), + "executable": self.source.executable_format.format( + rom_path = item["path"], + core_path = core_path, + ) + } + + game = Game(values) + additional_data = {} + + # Get boxart + boxart_image_name = item["label"].split(".", 1)[0] + ".png" + boxart_folder_name = playlist_file.split(".", 1)[0] + image_path = self.source.config_location["thumbnails"] / boxart_folder_name / "Named_Boxarts" / boxart_image_name + additional_data = {"local_image_path": image_path} + + yield(game, additional_data) + + +class RetroarchSource(Source): + args = ' -L "{core_path}" "{rom_path}"' + + name = "Retroarch" + available_on = {"linux"} + iterator_class = RetroarchSourceIterator + executable_format = 'retroarch' + args + + config_location = Location( + schema_key="retroarch-location", + candidates=( + shared.flatpak_dir / "org.libretro.RetroArch" / "config" / "retroarch", + shared.config_dir / "retroarch", + shared.home / ".config" / "retroarch", + ), + paths={ + "playlists": (True, "playlists"), + "thumbnails": (True, "thumbnails"), + }, + ) + + # Check if installation is flatpak'd. + # TODO: There's probably a MUCH better way of doing this. + # There's *is* a URI format, but it doesn't seem to work on the flatpak + # version of Retroarch. https://github.com/libretro/RetroArch/pull/13563 + if str(shared.flatpak_dir) in str(config_location["playlists"]): + executable_format = 'flatpak run org.libretro.RetroArch' + args diff --git a/src/main.py b/src/main.py index d0afd3e..ff6ac83 100644 --- a/src/main.py +++ b/src/main.py @@ -33,6 +33,7 @@ from src import shared from src.details_window import DetailsWindow from src.game import Game from src.importer.importer import Importer +from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.bottles_source import BottlesSource from src.importer.sources.flatpak_source import FlatpakSource from src.importer.sources.heroic_source import HeroicSource @@ -230,6 +231,9 @@ class CartridgesApplication(Adw.Application): if shared.schema.get_boolean("legendary"): importer.add_source(LegendarySource()) + if shared.schema.get_boolean("retroarch"): + importer.add_source(RetroarchSource()) + importer.run() def on_remove_game_action(self, *_args): diff --git a/src/preferences.py b/src/preferences.py index f090b4a..15bfbed 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -30,6 +30,7 @@ from src.importer.sources.flatpak_source import FlatpakSource from src.importer.sources.heroic_source import HeroicSource from src.importer.sources.itch_source import ItchSource from src.importer.sources.legendary_source import LegendarySource +from src.importer.sources.retroarch_source import RetroarchSource from src.importer.sources.location import UnresolvableLocationError from src.importer.sources.lutris_source import LutrisSource from src.importer.sources.source import Source @@ -82,6 +83,9 @@ class PreferencesWindow(Adw.PreferencesWindow): legendary_config_action_row = Gtk.Template.Child() legendary_config_file_chooser_button = Gtk.Template.Child() + retroarch_expander_row = Gtk.Template.Child() + retroarch_config_file_chooser_button = Gtk.Template.Child() + flatpak_expander_row = Gtk.Template.Child() flatpak_data_action_row = Gtk.Template.Child() flatpak_data_file_chooser_button = Gtk.Template.Child() @@ -138,6 +142,7 @@ class PreferencesWindow(Adw.PreferencesWindow): ItchSource, LegendarySource, LutrisSource, + RetroarchSource, SteamSource, ): source = source_class() @@ -385,3 +390,4 @@ class PreferencesWindow(Adw.PreferencesWindow): # Set the source row subtitles self.resolve_locations(source) self.update_source_action_row_paths(source) +