From b173a2a8519d4c655c3a64363d7cc579214954dc Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:03:17 +0200 Subject: [PATCH] fix: place libretro dolphin files where it reads --- emulators/dolphin.yml | 49 ++++++++++++++++------ emulators/ishiiruka.yml | 32 ++++++++++---- schemas/emulator.schema.json | 20 ++++++++- scripts/generate_pack.py | 65 ++++++++++++++-------------- scripts/packextras.py | 5 +-- scripts/packpaths.py | 25 +++++------ scripts/validation.py | 12 ++++-- scripts/verify.py | 9 ++-- tests/test_libretro_layout.py | 79 +++++++++++++++++++++++++++++++++++ 9 files changed, 215 insertions(+), 81 deletions(-) create mode 100644 tests/test_libretro_layout.py diff --git a/emulators/dolphin.yml b/emulators/dolphin.yml index 9db68f9d..873188bd 100644 --- a/emulators/dolphin.yml +++ b/emulators/dolphin.yml @@ -12,10 +12,13 @@ cores: - dolphin systems: [nintendo-gamecube, nintendo-wii, sega-triforce] -# File paths are relative to Dolphin User directory: -# Standalone: User/GC/ and User/Wii/ -# Libretro (RetroArch): system/dolphin-emu/GC/ and system/dolphin-emu/Wii/ -# Sys/ is checked as fallback when not found in User/. +# path: is where the libretro core reads a file, standalone_path: where the +# standalone app reads it under its User directory. +# Libretro: User is /User, Sys is /dolphin-emu/Sys +# (Source/Core/DolphinLibretro/Boot.cpp:248-254). The GC ROMs and +# segaboot.gcm fall back to Sys (Source/Core/Core/Config/MainSettings.cpp:761-768, +# Source/Core/Core/HW/DVD/AMMediaboard.cpp:500-513); the Wii and GBA files +# are read under User only. pack_structure: libretro: "dolphin-emu" @@ -32,7 +35,8 @@ files: # Checked in User/GC// then Sys/GC// # Regions: USA, EUR, JAP (legacy), JPN, DEV - name: "IPL.bin" - path: "GC/USA/IPL.bin" + path: "dolphin-emu/Sys/GC/USA/IPL.bin" + standalone_path: "GC/USA/IPL.bin" region: [north-america] size: 2097152 required: false @@ -41,7 +45,8 @@ files: source_ref: "Source/Core/Common/CommonPaths.h:139, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:108" - name: "IPL.bin" - path: "GC/EUR/IPL.bin" + path: "dolphin-emu/Sys/GC/EUR/IPL.bin" + standalone_path: "GC/EUR/IPL.bin" region: [europe] size: 2097152 required: false @@ -50,7 +55,8 @@ files: source_ref: "Source/Core/Core/Config/MainSettings.cpp:776-782" - name: "IPL.bin" - path: "GC/JAP/IPL.bin" + path: "dolphin-emu/Sys/GC/JAP/IPL.bin" + standalone_path: "GC/JAP/IPL.bin" region: [japan] size: 2097152 required: false @@ -63,7 +69,8 @@ files: # Dolphin ships free replacement ROMs, but real dumps improve accuracy. # Searched in: User/GC/ then Sys/GC/ - name: "dsp_rom.bin" - path: "GC/dsp_rom.bin" + path: "dolphin-emu/Sys/GC/dsp_rom.bin" + standalone_path: "GC/dsp_rom.bin" size: 8192 required: false hle_fallback: true @@ -74,7 +81,8 @@ files: source_ref: "Source/Core/Common/CommonPaths.h:136, Source/Core/Core/HW/DSPLLE/DSPLLE.cpp:84-117, Source/Core/Core/DSP/DSPCore.cpp:28-102" - name: "dsp_coef.bin" - path: "GC/dsp_coef.bin" + path: "dolphin-emu/Sys/GC/dsp_coef.bin" + standalone_path: "GC/dsp_coef.bin" size: 4096 required: false hle_fallback: true @@ -89,7 +97,8 @@ files: # If IPL dump is present, fonts are extracted from it instead (preferred). # Searched in: Sys/GC/ - name: "font_western.bin" - path: "GC/font_western.bin" + path: "dolphin-emu/Sys/GC/font_western.bin" + standalone_path: "GC/font_western.bin" size: 9589 required: false hle_fallback: true @@ -97,7 +106,8 @@ files: source_ref: "Source/Core/Common/CommonPaths.h:133, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:130,202-239" - name: "font_japanese.bin" - path: "GC/font_japanese.bin" + path: "dolphin-emu/Sys/GC/font_japanese.bin" + standalone_path: "GC/font_japanese.bin" size: 303693 required: false hle_fallback: true @@ -107,7 +117,8 @@ files: # -- Triforce firmware -- # Loaded from User/Triforce/ only, no Sys fallback. - name: "segaboot.gcm" - path: "Triforce/segaboot.gcm" + path: "dolphin-emu/Sys/Triforce/segaboot.gcm" + standalone_path: "Triforce/segaboot.gcm" system: sega-triforce required: true note: "SegaBoot, the Triforce system disc image. Read on every Triforce boot, min(size, 2 MiB) copied into the firmware buffer, nothing validated" @@ -118,6 +129,8 @@ files: # Loaded from User/GBA/gba_bios.bin (no Sys fallback). - name: "gba_bios.bin" path: "GBA/gba_bios.bin" + load_from: + libretro: save_dir required: false hle_fallback: true note: "GBA BIOS for GC-GBA link feature (uses integrated mGBA). Path configurable in settings" @@ -129,6 +142,8 @@ files: - name: "SYSCONF" path: "Wii/shared2/sys/SYSCONF" + load_from: + libretro: save_dir required: false hle_fallback: true note: "Wii system configuration. Auto-generated by Dolphin, can be imported from NAND backup" @@ -136,6 +151,8 @@ files: - name: "setting.txt" path: "Wii/title/00000001/00000002/data/setting.txt" + load_from: + libretro: save_dir size: 256 required: false hle_fallback: true @@ -161,6 +178,8 @@ files: # -- Wii SD Card Image -- - name: "WiiSD.raw" path: "Load/WiiSD.raw" + load_from: + libretro: save_dir required: false hle_fallback: true note: "Virtual SD card image for Wii homebrew. Auto-created by Dolphin, supports SD/SDHC up to 4GB" @@ -186,6 +205,8 @@ files: # -- NAND Certificates (auto-extracted) -- - name: "clientca.pem" path: "Wii/clientca.pem" + load_from: + libretro: save_dir required: false hle_fallback: true note: "SSL client certificate. Auto-extracted from IOS13 content during NAND import" @@ -193,6 +214,8 @@ files: - name: "clientcakey.pem" path: "Wii/clientcakey.pem" + load_from: + libretro: save_dir required: false hle_fallback: true note: "SSL client private key. Auto-extracted from IOS13 content during NAND import" @@ -200,6 +223,8 @@ files: - name: "rootca.pem" path: "Wii/rootca.pem" + load_from: + libretro: save_dir required: false hle_fallback: true note: "SSL root CA certificate. Auto-extracted from IOS13 content during NAND import" diff --git a/emulators/ishiiruka.yml b/emulators/ishiiruka.yml index 475a9053..6d1ba50c 100644 --- a/emulators/ishiiruka.yml +++ b/emulators/ishiiruka.yml @@ -13,7 +13,10 @@ systems: [nintendo-gamecube, nintendo-wii] # Ishiiruka is a performance-focused Dolphin fork (custom GPU backends, async shader compilation). # BIOS/firmware handling identical to Dolphin 5.0-era code. -# Libretro core uses "dolphin-emu" subdirectory, not "ishiiruka". +# Libretro: User is /User, Sys is /dolphin-emu/Sys +# (Source/Core/DolphinLibretro/Boot.cpp:43-49). path: is where the libretro +# core reads a file (the GC ROMs fall back to Sys), standalone_path: where +# the standalone app reads it under its User directory. # Core options use "ishiiruka_" prefix (ishiiruka_dsp_hle, ishiiruka_efb_scale, etc.) pack_structure: @@ -30,7 +33,8 @@ files: # Region-specific, placed in GC//IPL.bin # Checked in User/GC// then Sys/GC// - name: "IPL.bin" - path: "GC/USA/IPL.bin" + path: "dolphin-emu/Sys/GC/USA/IPL.bin" + standalone_path: "GC/USA/IPL.bin" region: [north-america] size: 2097152 required: false @@ -39,7 +43,8 @@ files: source_ref: "Source/Core/Core/ConfigManager.cpp:876-883, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:105" - name: "IPL.bin" - path: "GC/EUR/IPL.bin" + path: "dolphin-emu/Sys/GC/EUR/IPL.bin" + standalone_path: "GC/EUR/IPL.bin" region: [europe] size: 2097152 required: false @@ -48,7 +53,8 @@ files: source_ref: "Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:185" - name: "IPL.bin" - path: "GC/JAP/IPL.bin" + path: "dolphin-emu/Sys/GC/JAP/IPL.bin" + standalone_path: "GC/JAP/IPL.bin" region: [japan] size: 2097152 required: false @@ -60,7 +66,8 @@ files: # Used by DSP LLE for accurate audio. dsp_coef.bin also used by HLE for polyphase resampling. # Searched in: User/GC/ then Sys/GC/ - name: "dsp_rom.bin" - path: "GC/dsp_rom.bin" + path: "dolphin-emu/Sys/GC/dsp_rom.bin" + standalone_path: "GC/dsp_rom.bin" size: 8192 required: false hle_fallback: true @@ -71,7 +78,8 @@ files: source_ref: "Source/Core/Common/CommonPaths.h:112, Source/Core/Core/HW/DSPLLE/DSPLLE.cpp:142-150, Source/Core/Core/DSP/DSPCore.cpp:48,67, Source/Core/Core/DSP/DSPCore.cpp:38-105" - name: "dsp_coef.bin" - path: "GC/dsp_coef.bin" + path: "dolphin-emu/Sys/GC/dsp_coef.bin" + standalone_path: "GC/dsp_coef.bin" size: 4096 required: false hle_fallback: true @@ -86,7 +94,8 @@ files: # If IPL dump is present, fonts are extracted from it instead (preferred). # Loaded from: Sys/GC/ - name: "font_western.bin" - path: "GC/font_western.bin" + path: "dolphin-emu/Sys/GC/font_western.bin" + standalone_path: "GC/font_western.bin" size: 9589 required: false hle_fallback: true @@ -94,7 +103,8 @@ files: source_ref: "Source/Core/Common/CommonPaths.h:109, Source/Core/Core/HW/EXI/EXI_DeviceIPL.cpp:123,193-227" - name: "font_japanese.bin" - path: "GC/font_japanese.bin" + path: "dolphin-emu/Sys/GC/font_japanese.bin" + standalone_path: "GC/font_japanese.bin" size: 303693 required: false hle_fallback: true @@ -113,6 +123,8 @@ files: # Auto-generated/managed by Dolphin's Wii emulation. - name: "SYSCONF" path: "Wii/shared2/sys/SYSCONF" + load_from: + libretro: save_dir required: false hle_fallback: true note: "Wii system configuration. Auto-generated, can be imported from NAND backup" @@ -120,6 +132,8 @@ files: - name: "setting.txt" path: "Wii/title/00000001/00000002/data/setting.txt" + load_from: + libretro: save_dir size: 256 required: false hle_fallback: true @@ -129,6 +143,8 @@ files: # -- Wii SD Card Image -- - name: "sd.raw" path: "Wii/sd.raw" + load_from: + libretro: save_dir required: false hle_fallback: true note: "Virtual SD card image for Wii homebrew. Auto-created (128MB) if not present" diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index fbbc9ccd..b258787e 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -433,7 +433,25 @@ "type": "boolean" }, "load_from": { - "type": "string" + "description": "Where the emulator reads the file when not from the system directory; keyed by build when the libretro and standalone builds read it from different places.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "libretro": { + "type": "string" + }, + "standalone": { + "type": "string" + } + } + } + ] }, "priority": { "description": "Preference order among the BIOS a system accepts, lowest first. DuckStation's FindBIOSImageInDirectory keeps the image whose priority is lower; a search list the code walks in order maps onto it as 1, 2, 3.", diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 501bd742..78579542 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -1303,33 +1303,8 @@ def generate_emulator_pack( db, standalone, ) - # A tree only one build reads follows that build, like a file. - for dd in filter_files_by_mode( - profile.get("data_directories", []), standalone - ): - ref_key = dd.get("ref", "") - if not ref_key or not data_registry or ref_key not in data_registry: - if ref_key: - data_dir_notices.append(ref_key) - continue - entry = data_registry[ref_key] - local_cache = entry.get("local_cache", "") - if not local_cache or not os.path.isdir(local_cache): - data_dir_notices.append(ref_key) - continue - dd_dest = dd.get("destination", "") - if pack_structure: - mode_key = "standalone" if standalone else "libretro" - prefix = pack_structure.get(mode_key, "") - if prefix: - dd_dest = f"{prefix}/{dd_dest}" if dd_dest else prefix - total_files += _pack_data_tree( - zf, local_cache, dd_dest, seen_destinations, seen_parents, seen_lower - ) - if not files: print(f" No files needed for {profile.get('emulator', emu_name)}") - continue # Collect archives as atomic units archives: set[str] = set() @@ -1393,7 +1368,7 @@ def generate_emulator_pack( continue if fe.get("archive"): continue - dest = _resolve_destination(fe, pack_structure, standalone) + dest = _resolve_destination(fe, standalone) if not dest: continue if region_drops and dest in region_drops: @@ -1541,6 +1516,33 @@ def generate_emulator_pack( packed_at[dest.lower()] = local_path total_files += 1 + # The trees after the files, as in a platform pack: a dump the + # profile declares wins over the copy a tree bundles at the same + # path (Dolphin's real dsp_rom.bin over Sys/GC's free one). + # A tree only one build reads follows that build, like a file. + for dd in filter_files_by_mode( + profile.get("data_directories", []), standalone + ): + ref_key = dd.get("ref", "") + if not ref_key or not data_registry or ref_key not in data_registry: + if ref_key: + data_dir_notices.append(ref_key) + continue + entry = data_registry[ref_key] + local_cache = entry.get("local_cache", "") + if not local_cache or not os.path.isdir(local_cache): + data_dir_notices.append(ref_key) + continue + dd_dest = dd.get("destination", "") + if pack_structure: + mode_key = "standalone" if standalone else "libretro" + prefix = pack_structure.get(mode_key, "") + if prefix: + dd_dest = f"{prefix}/{dd_dest}" if dd_dest else prefix + total_files += _pack_data_tree( + zf, local_cache, dd_dest, seen_destinations, seen_parents, seen_lower + ) + # Remove empty ZIP (no files packed and no missing = nothing to ship) if total_files == 0 and not (missing_files or rejected_files or instruction_files or unsourceable_files): os.unlink(zip_path) @@ -1884,7 +1886,6 @@ def generate_md5_pack( for fe in system.get("files", []): plat_file_index.setdefault(fe.get("name", "").lower(), []).append(fe) - emu_pack_structure = None emu_display = "" if emulator_name and emulators_dir: profiles = load_emulator_profiles(emulators_dir, skip_aliases=False) @@ -1899,7 +1900,6 @@ def generate_md5_pack( print(f"Error: {exc}", file=sys.stderr) return None emu_display = profile.get("emulator", emulator_name) - emu_pack_structure = profile.get("pack_structure") for fe in profile.get("files", []): plat_file_index.setdefault(fe.get("name", "").lower(), []).append(fe) for alias in fe.get("aliases", []): @@ -1955,12 +1955,9 @@ def generate_md5_pack( if matched_fe: if emulator_name: - # Laid out as the emulator pack lays it out: a profile - # without pack_structure still names its path and its - # standalone_path, and --standalone has to reach them. - dest = _resolve_destination( - matched_fe, emu_pack_structure, standalone - ) + # Laid out as the emulator pack lays it out: its path and + # its standalone_path, which --standalone has to reach. + dest = _resolve_destination(matched_fe, standalone) else: dest = matched_fe.get("destination", matched_fe.get("name", name)) elif paths: diff --git a/scripts/packextras.py b/scripts/packextras.py index a8223fc1..476b0e6a 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -695,7 +695,7 @@ def _collect_emulator_extras( continue if file_mode == "libretro" and is_standalone: continue - if not read_from_system_dir(f): + if not read_from_system_dir(f, is_standalone): continue if is_standalone: raw = f.get("standalone_path") or f.get("path") or fname @@ -943,12 +943,11 @@ def emulator_region_drops( region_index = region_mod.build_region_index(dict(selected)) region_groups: dict[str, list[tuple[str, str]]] = {} for emu_name, profile in sorted(selected): - structure = profile.get("pack_structure") # Decided over what the pack keeps, as on the platform side: an # optional winner removed afterwards left its slot empty. entries = filter_files_by_mode(profile.get("files", []), standalone) for fe in _kept(entries, required_only): - dest = _resolve_destination(fe, structure, standalone) + dest = _resolve_destination(fe, standalone) if dest: group_id = _emulator_region_group(emu_name, profile, fe) region_groups.setdefault(group_id, []).append( diff --git a/scripts/packpaths.py b/scripts/packpaths.py index 2ea30571..8c6c0644 100644 --- a/scripts/packpaths.py +++ b/scripts/packpaths.py @@ -59,10 +59,16 @@ def _flat(arcname: str, prefix: str, flatten: bool) -> str: return arcname[len(prefix) + 1:] return arcname -def _resolve_destination( - file_entry: dict, pack_structure: dict | None, standalone: bool -) -> str: - """Resolve the ZIP destination path for a file entry.""" +def _resolve_destination(file_entry: dict, standalone: bool) -> str: + """Resolve the ZIP destination path for a file entry. + + ``path:`` is where the libretro build reads the file from the system + directory and ``standalone_path:`` where the standalone app reads it, so + a file's destination is written whole. ``pack_structure`` places a + profile's data directories and archives: prefixing files with it gave + PPSSPP/PPSSPP/ppge_atlas.zim, and the platform packs, which never read + it, put libretro Dolphin's IPL at the root of system/. + """ # 1. standalone_path override if standalone and file_entry.get("standalone_path"): rel = file_entry["standalone_path"] @@ -73,16 +79,7 @@ def _resolve_destination( else: rel = file_entry.get("name", "") - rel = sanitize_pack_path(rel) - - # Prepend pack_structure prefix - if pack_structure: - mode_key = "standalone" if standalone else "libretro" - prefix = pack_structure.get(mode_key, "") - if prefix: - rel = f"{prefix}/{rel}" - - return rel + return sanitize_pack_path(rel) def _archive_destination( diff --git a/scripts/validation.py b/scripts/validation.py index 6ae812bc..0dc0f320 100644 --- a/scripts/validation.py +++ b/scripts/validation.py @@ -448,14 +448,18 @@ def validate_cli_modes(args, mode_attrs: list[str]) -> None: raise SystemExit(f"Options are mutually exclusive: --{' --'.join(mode_attrs)}") -def read_from_system_dir(file_entry: dict) -> bool: +def read_from_system_dir(file_entry: dict, standalone: bool = False) -> bool: """Whether the emulator reads this entry from the system directory. A file loaded from the save, content or working directory is not a BIOS: no platform lists it, no pack carries it, and the truth model must not - ask a platform to declare it. + ask a platform to declare it. Two builds can read one file from two + places: libretro Dolphin keeps its User tree in the save directory, the + standalone app in its own folder, so ``load_from`` can be keyed by build. """ load_from = file_entry.get("load_from", "") + if isinstance(load_from, dict): + load_from = load_from.get("standalone" if standalone else "libretro", "") return not load_from or load_from == "system_dir" @@ -478,7 +482,9 @@ def outside_gap_scope(file_entry: dict, is_standalone: bool) -> bool: mode == "libretro" and is_standalone ): return True - return not read_from_system_dir(file_entry) or bool(file_entry.get("agnostic")) + return not read_from_system_dir(file_entry, is_standalone) or bool( + file_entry.get("agnostic") + ) def filter_files_by_mode(files: list[dict], standalone: bool) -> list[dict]: diff --git a/scripts/verify.py b/scripts/verify.py index f8d35ea9..2850a3f8 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -1512,11 +1512,10 @@ def verify_emulator( standalone, ) if region_drops: - structure = profile.get("pack_structure") files = [ fe for fe in files - if _resolve_destination(fe, structure, standalone) not in region_drops + if _resolve_destination(fe, standalone) not in region_drops ] # Check data directories (only notice if not cached) @@ -1705,10 +1704,8 @@ def verify_emulator( result["hle_fallback"] = hle result["ground_truth"] = build_ground_truth(name, validation_index) # The slot the emulator pack places this entry in: standalone_path - # under --standalone, the pack_structure prefix, the sanitised path. - dest = _resolve_destination( - file_entry, profile.get("pack_structure"), standalone - ) or name + # under --standalone, the sanitised path. + dest = _resolve_destination(file_entry, standalone) or name emu_results.setdefault(dest, []).append((result, required, hle)) for dest, alternatives in emu_results.items(): diff --git a/tests/test_libretro_layout.py b/tests/test_libretro_layout.py new file mode 100644 index 00000000..ff982984 --- /dev/null +++ b/tests/test_libretro_layout.py @@ -0,0 +1,79 @@ +"""A profile's path: is where the libretro core reads the file. + +pack_structure prefixed files as well as data directories: PPSSPP's +`PPSSPP/ppge_atlas.zim` became PPSSPP/PPSSPP/, while the platform packs, +which never read the field, put libretro Dolphin's IPL, DSP ROMs, fonts, +segaboot and Wii user files at the root of system/. libretro Dolphin reads +Sys from system/dolphin-emu/Sys and keeps User in the save directory. +""" + +from __future__ import annotations + +import os +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from packpaths import _resolve_destination # noqa: E402 +from validation import outside_gap_scope, read_from_system_dir # noqa: E402 + + +class PathIsTheDestination(unittest.TestCase): + def test_no_prefix_on_a_file(self): + entry = {"name": "ppge_atlas.zim", "path": "PPSSPP/ppge_atlas.zim"} + self.assertEqual(_resolve_destination(entry, False), "PPSSPP/ppge_atlas.zim") + dual = {"name": "IPL.bin", "path": "dolphin-emu/Sys/GC/USA/IPL.bin", + "standalone_path": "GC/USA/IPL.bin"} + self.assertEqual(_resolve_destination(dual, False), "dolphin-emu/Sys/GC/USA/IPL.bin") + self.assertEqual(_resolve_destination(dual, True), "GC/USA/IPL.bin") + + +class LoadFromPerBuild(unittest.TestCase): + ENTRY = {"name": "SYSCONF", "path": "Wii/shared2/sys/SYSCONF", + "load_from": {"libretro": "save_dir"}} + + def test_each_build_reads_its_own_place(self): + self.assertFalse(read_from_system_dir(self.ENTRY, False)) + self.assertTrue(read_from_system_dir(self.ENTRY, True)) + self.assertTrue(outside_gap_scope(self.ENTRY, False)) + self.assertFalse(outside_gap_scope(self.ENTRY, True)) + + def test_a_plain_value_holds_for_both(self): + entry = {"name": "x", "load_from": "save_dir"} + self.assertFalse(read_from_system_dir(entry, False)) + self.assertFalse(read_from_system_dir(entry, True)) + + +class RetroArchCarriesDolphinWhereItReads(unittest.TestCase): + """Reads the repository: skips when the database is absent.""" + + def test_no_dolphin_file_at_the_root(self): + if not (REPO_ROOT / "database.json").exists(): + self.skipTest("database.json is not built") + previous = os.getcwd() + os.chdir(REPO_ROOT) + self.addCleanup(os.chdir, previous) + from common import load_database, load_emulator_profiles, load_platform_config + from packextras import _collect_emulator_extras + + config = load_platform_config("retroarch") + extras = _collect_emulator_extras( + config, "emulators", load_database("database.json"), set(), + config.get("base_destination", ""), load_emulator_profiles("emulators"), + ) + dolphin = [ + e["destination"] for e in extras + if e.get("source_emulator") in ("Dolphin", "Ishiiruka") + ] + self.assertTrue(dolphin) + self.assertEqual( + [d for d in dolphin if not d.startswith("dolphin-emu/Sys/")], [] + ) + self.assertIn("dolphin-emu/Sys/Triforce/segaboot.gcm", dolphin) + + +if __name__ == "__main__": + unittest.main()