diff --git a/platforms/retrodeck.yml b/platforms/retrodeck.yml index f59aa340..0d801412 100644 --- a/platforms/retrodeck.yml +++ b/platforms/retrodeck.yml @@ -44,6 +44,14 @@ standalone_cores: - vita3k - xemu - xroar +dual_mode_cores: +- azahar +- cemu +- dolphin +- mame +- melonds +- pcsx2 +- ppsspp systems: triforce: files: diff --git a/scripts/scraper/retrodeck_scraper.py b/scripts/scraper/retrodeck_scraper.py index 4b6797d9..a1e6ffe7 100644 --- a/scripts/scraper/retrodeck_scraper.py +++ b/scripts/scraper/retrodeck_scraper.py @@ -96,6 +96,25 @@ def es_systems_libretro_systems(text: str) -> dict[str, list[str]]: if match: found.setdefault(match.group(1), set()).add(folder) return {core: sorted(folders) for core, folders in sorted(found.items())} + + +def component_modes( + components: list[str], libretro_systems: dict[str, list[str]] +) -> tuple[list[str], list[str]]: + """(standalone_cores, dual_mode_cores) of RetroDECK's emulator components. + + Every component but retroarch is an emulator RetroDECK launches on its + own (its component_launcher.sh execs the emulator's binary). One whose name + is also a core an ES-DE command loads through RetroArch is offered both + ways: es_systems.xml lists "Azahar (Standalone)" and "Azahar" + (azahar_libretro.so) for n3ds. The core itself ships, RetroArch's cores + coming whole from the buildbot's nightly RetroArch_cores.7z + (retroarch/component_recipe.json:250-262 at components fcd0d729). + """ + standalone = [c for c in components if c != "retroarch"] + return standalone, [c for c in standalone if c in libretro_systems] + + NON_EMULATOR_COMPONENTS = { "framework", "es-de", @@ -533,6 +552,9 @@ class Scraper(BaseScraper): sys_entry["files"].append(requirement_entry(req)) version = fetch_github_latest_version("RetroDECK/RetroDECK") + # The ROM folders each libretro core runs for: its save folders. + libretro_systems = self._fetch_libretro_systems() + standalone, dual = component_modes(cores, libretro_systems) return { "platform": "RetroDECK", @@ -547,12 +569,10 @@ class Scraper(BaseScraper): "hash_type": "md5", "verification_mode": "md5", "cores": cores, - # Each component is one emulator RetroDECK ships and launches on - # its own, and retroarch is the one that runs cores. - "standalone_cores": [c for c in cores if c != "retroarch"], + "standalone_cores": standalone, + **({"dual_mode_cores": dual} if dual else {}), "systems": systems, - # The ROM folders each libretro core runs for: its save folders. - "libretro_systems": self._fetch_libretro_systems(), + "libretro_systems": libretro_systems, "launchers": extract_launchers(cores, self.component_file), } diff --git a/tests/test_scraper_contract.py b/tests/test_scraper_contract.py index 9330efc2..93ff1bff 100644 --- a/tests/test_scraper_contract.py +++ b/tests/test_scraper_contract.py @@ -472,6 +472,78 @@ class RetroDeckCoresAreItsComponents(unittest.TestCase): self.assertNotIn("cores", registry["platforms"]["retrodeck"]) +class RetroDeckComponentModes(unittest.TestCase): + """Every RetroDECK component was written standalone only: Azahar, whose + ES-DE system also loads azahar_libretro.so, never got its libretro + files, which the core reads below its save folder.""" + + # Commands as RetroDECK/ES-DE 15e1b236 writes them in + # resources/systems/linux/es_systems.xml (n3ds at 1379-1383, psx at + # 1843 and 1847). The commented-out DuckStation core is the fixture's own. + ES_SYSTEMS = """\ + + + n3ds%ROMPATH%/n3ds + %EMULATOR_AZAHAR% %ROM% + %EMULATOR_RETROARCH% -L %CORE_RETROARCH%/azahar_libretro.so %ROM% + %EMULATOR_RETROARCH% -L %CORE_RETROARCH%/citra_libretro.so %ROM% + + psx%ROMPATH%/psx + %EMULATOR_RETROARCH% -L %CORE_RETROARCH%/swanstation_libretro.so %ROM% + + %EMULATOR_DUCKSTATION% -batch %ROM% + + +""" + + def _config(self) -> dict: + from scraper import retrodeck_scraper as rd + + with tempfile.TemporaryDirectory() as tmp: + for component in ("azahar", "duckstation", "es-de", "retroarch"): + (Path(tmp) / component).mkdir() + (Path(tmp) / component / "component_manifest.json").write_text( + json.dumps({component: {"system": component}}) + ) + scraper = rd.Scraper(manifests_dir=tmp) + systems = rd.es_systems_libretro_systems(self.ES_SYSTEMS) + with mock.patch.object(rd, "fetch_github_latest_version", return_value="0.10.10b"), \ + mock.patch.object(rd, "extract_launchers", return_value=[]), \ + mock.patch.object(rd.Scraper, "_fetch_libretro_systems", return_value=systems): + return scraper.generate_platform_yaml() + + def test_a_component_es_de_also_runs_as_a_core_is_dual_mode(self): + config = self._config() + self.assertEqual(config["cores"], ["azahar", "duckstation", "retroarch"]) + self.assertEqual(config["standalone_cores"], ["azahar", "duckstation"]) + self.assertEqual(config["dual_mode_cores"], ["azahar"]) + + def test_a_commented_out_core_command_offers_nothing(self): + """duckstation_libretro.so sits in a comment: DuckStation runs standalone only.""" + config = self._config() + self.assertNotIn("duckstation", config["dual_mode_cores"]) + self.assertNotIn("duckstation", config["libretro_systems"]) + + def test_a_core_without_a_component_is_not_a_component(self): + """citra_libretro.so runs through RetroArch, and RetroDECK ships no citra build.""" + from scraper.retrodeck_scraper import component_modes + + standalone, dual = component_modes( + ["azahar", "retroarch"], {"azahar": ["n3ds"], "citra": ["n3ds"]} + ) + self.assertEqual((standalone, dual), (["azahar"], ["azahar"])) + + def test_the_platform_file_carries_the_modes(self): + import yaml + + root = Path(__file__).resolve().parents[1] + platform = yaml.safe_load((root / "platforms" / "retrodeck.yml").read_text()) + dual = set(platform.get("dual_mode_cores") or []) + self.assertIn("azahar", dual) + self.assertLessEqual(dual, set(platform["standalone_cores"])) + self.assertLessEqual(dual, set(platform["libretro_systems"])) + + class BizHawkRunsAresForN64Only(unittest.TestCase): """BizHawk embeds ares as Ares64, its N64 core. Mapping it to the ares profile claimed thirty systems and accused FirmwareDatabase.cs of a