diff --git a/platforms/emudeck.yml b/platforms/emudeck.yml index e2db9d80..5ef9d2eb 100644 --- a/platforms/emudeck.yml +++ b/platforms/emudeck.yml @@ -66,6 +66,14 @@ standalone_cores: - xenia - xenianative - yuzu +dual_mode_cores: +- flycast +- mame +- melonds +- mgba +- mupen64plus_next +- ppsspp +- scummvm systems: sony-playstation: files: diff --git a/scripts/scraper/emudeck_scraper.py b/scripts/scraper/emudeck_scraper.py index 75381a5b..4f72adef 100644 --- a/scripts/scraper/emudeck_scraper.py +++ b/scripts/scraper/emudeck_scraper.py @@ -231,6 +231,25 @@ def launcher_revision(url: str) -> str: return found.group(1) +RETROARCH_SCRIPT = "functions/EmuScripts/emuDeckRetroArch.sh" + + +def installed_libretro_cores(script: str) -> list[str]: + """The cores RetroArch_installCores downloads, without the _libretro suffix.""" + body = re.search(r"RetroArch_installCores\(\)\s*\{(.*?)\n\}", script, re.S) + array = re.search(r"RAcores=\((.*?)\)", body.group(1), re.S) if body else None + if array is None: + raise RuntimeError(f"{RETROARCH_SCRIPT}: no RAcores list in RetroArch_installCores") + cores = [ + word.removesuffix("_libretro") + for word in array.group(1).replace("\\", " ").split() + if word.endswith("_libretro") + ] + if not cores: + raise RuntimeError(f"{RETROARCH_SCRIPT}: RAcores names no core") + return cores + + class Scraper(BaseScraper): """Scraper for EmuDeck checkBIOS.sh and CSV cheat sheets.""" @@ -451,6 +470,9 @@ class Scraper(BaseScraper): version = fetch_github_latest_version("dragoonDorise/EmuDeck") cores = self._fetch_installed_emulators() + standalone = [core for core in cores if core != "retroarch"] + tree = RepoTree.github(EMUDECK_REPO, launcher_revision(self.checkbios_url)) + libretro = set(installed_libretro_cores(tree.read(RETROARCH_SCRIPT))) return { "platform": "EmuDeck", @@ -469,12 +491,14 @@ class Scraper(BaseScraper): # Dolphin, Cemu, xemu, ARMSX2 and Supermodel ran with the libretro # layout and lost their standalone-only files. A profile with no # standalone build keeps its layout (runs_standalone). - "standalone_cores": [core for core in cores if core != "retroarch"], + "standalone_cores": standalone, + # RetroArch_installCores downloads a libretro build of some of + # them (flycast_libretro reads system/dc/ beside the standalone + # Flycast's flycast/bios/): both layouts are packed. + "dual_mode_cores": [core for core in standalone if core in libretro], "systems": systems, # Read at the commit checkBIOS.sh comes from. - "launchers": extract_launchers( - RepoTree.github(EMUDECK_REPO, launcher_revision(self.checkbios_url)) - ), + "launchers": extract_launchers(tree), } def _fetch_installed_emulators(self) -> list[str]: diff --git a/tests/test_standalone_links.py b/tests/test_standalone_links.py index 6062eb2d..6b2efa45 100644 --- a/tests/test_standalone_links.py +++ b/tests/test_standalone_links.py @@ -166,6 +166,20 @@ class Layout(unittest.TestCase): libretro = next(view for view, standalone in views if not standalone) self.assertIs(libretro, PROFILE) + def test_dual_mode_gets_both_layouts(self) -> None: + # The libretro build still reads dc/, the standalone build the + # linked folder: neither loses a file to the other. + cfg = {**config(link("emu", "$biosdir/emu", "$home/.local/share/emu is a symlink to it"), + base="BIOS"), "dual_mode_cores": ["emu"]} + views = dict((standalone, view) for view, standalone in + platform_layouts("emu", PROFILE, standalone_set(cfg))) + self.assertEqual( + [f["path"] for f in views[False]["files"]], ["dc/boot.bin", "dc/set.zip", "dc/shared.bin"] + ) + self.assertEqual( + [f.get("standalone_path") for f in views[True]["files"]], ["emu/boot.bin", "emu/set.zip", None] + ) + def test_union_of_platforms_with_two_folders_keeps_the_profile_layout(self) -> None: union = standalone_set(config(self.LINKED, base="BIOS")) | standalone_set(config( link("emu", "${biosPath}/emu/bios", "symlink ${biosPath}/emu/bios -> ${HOME}/.local/share/emu"), @@ -220,7 +234,39 @@ class FlycastOnEachPlatform(unittest.TestCase): ) + def test_emudeck_keeps_the_libretro_layout_beside_the_linked_one(self) -> None: + # EmuDeck's RetroArch_installCores downloads flycast_libretro, which + # reads system/dc/, next to the standalone Flycast in flycast/bios/. + cfg = load_platform_config("emudeck", str(REPO_ROOT / "platforms")) + self.assertIn("flycast", cfg.get("dual_mode_cores") or []) + views = { + standalone: view + for view, standalone in platform_layouts("flycast", self.profile, standalone_set(cfg)) + } + self.assertEqual(set(views), {False, True}) + for entry in views[False]["files"]: + self.assertTrue(entry["path"].startswith("dc/"), entry["path"]) + for entry in views[True]["files"]: + self.assertTrue(entry["standalone_path"].startswith("flycast/bios/")) + + class Scrapers(unittest.TestCase): + def test_emudeck_installed_libretro_cores(self) -> None: + from scraper.emudeck_scraper import installed_libretro_cores + + script = textwrap.dedent("""\ + RetroArch_installCores(){ + \tRAcores=( + \t\t\t81_libretro \\ + \t\t\tflycast_libretro \\ + \t\t\tmesen-s_libretro \\ + \t\t\t) + } + """) + self.assertEqual(installed_libretro_cores(script), ["81", "flycast", "mesen-s"]) + with self.assertRaises(RuntimeError): + installed_libretro_cores("RetroArch_installCores(){\n}\n") + """The records each platform's scraper writes for these directories.""" def test_rocknix_patched_data_directory(self) -> None: