diff --git a/bios/Microsoft/MSX/SEGA - SF-7000/sf7000.rom b/bios/Sega/SF-7000/sf7000.rom similarity index 100% rename from bios/Microsoft/MSX/SEGA - SF-7000/sf7000.rom rename to bios/Sega/SF-7000/sf7000.rom diff --git a/emulators/drastic.yml b/emulators/drastic.yml index 3c489e8d..93465e8f 100644 --- a/emulators/drastic.yml +++ b/emulators/drastic.yml @@ -131,7 +131,6 @@ notes: | files: - name: nds_bios_arm9.bin - aliases: [NDS_Bios9.bin, bios9.bin, biosnds9.rom] path: system/nds_bios_arm9.bin system: nintendo-ds required: false @@ -154,7 +153,6 @@ files: source_ref: "drastic r2.5.2.2 initialize_memory 0x1c4b0-0x1c4d4, libdrastic_arm64.so 0x2b6f0-0x2b708, DraSticActivity.smali:4084-4090, drastic_bios_readme.txt:1-31" - name: nds_bios_arm7.bin - aliases: [NDS_Bios7.bin, bios7.bin, biosnds7.rom] path: system/nds_bios_arm7.bin system: nintendo-ds required: false @@ -177,7 +175,6 @@ files: source_ref: "drastic r2.5.2.2 initialize_memory 0x1c508-0x1c52c, libdrastic_arm64.so 0x2b740-0x2b758, DraSticActivity.smali:4078-4083" - name: nds_firmware.bin - aliases: [NDS_Lite_Firmware.bin, dsfirmware.bin, firmware.bin] path: system/nds_firmware.bin system: nintendo-ds required: false diff --git a/emulators/lrps2.yml b/emulators/lrps2.yml index 8aad8b6b..3b1aa8d5 100644 --- a/emulators/lrps2.yml +++ b/emulators/lrps2.yml @@ -8,8 +8,9 @@ core_classification: community_fork bios_mode: agnostic source: "https://github.com/libretro/ps2" upstream: "https://github.com/PCSX2/pcsx2" -profiled_date: "2026-03-25" -source_commit: "6eabf2242d451ee2e6d52b1e3f10efb77143ed05" +profiled_date: "2026-10-21" +source_commit: "6664c60294ee05fe5e8c7645ab7da546dae78ed2" +upstream_commit: "a81f1aa39fc56ef60dbc913637c74c49c1792033" core_version: "Git" display_name: "Sony - PlayStation 2 (LRPS2)" cores: [lrps2, pcsx2, "ps2"] @@ -46,7 +47,7 @@ files: min_size: 4194304 max_size: 8388608 validation: [size] - source_ref: "source pcsx2/ps2/BiosTools.cpp:33-34, 63-180, 249-274, 286-348" + source_ref: "source pcsx2/ps2/BiosTools.cpp:33-34, 63-180, 250-275, 287-349" note: > Scans pcsx2/bios/ for any file between 4 MB and 8 MB. Validates via romdir structure parsing (RESET + ROMVER entries). User selects BIOS via core option @@ -57,7 +58,7 @@ files: required: false max_size: 4194304 description: "DVD player ROM" - source_ref: "source pcsx2/ps2/BiosTools.cpp:189-229, 339" + source_ref: "source pcsx2/ps2/BiosTools.cpp:189-230, 340" note: > DVD player ROM. Tries .rom1 (appended) then .rom1 (extension replaced). Silently skipped if not found. @@ -67,7 +68,7 @@ files: required: false max_size: 524288 description: "Chinese ROM extension" - source_ref: "source pcsx2/ps2/BiosTools.cpp:189-229, 340" + source_ref: "source pcsx2/ps2/BiosTools.cpp:189-230, 341" note: > Chinese region ROM extension. Same naming convention as rom1. Only present on Chinese region consoles. @@ -78,7 +79,7 @@ files: hle_fallback: true size: 1024 description: "NVRAM / EEPROM data" - source_ref: "source pcsx2/CDVD/CDVD.cpp:196-227" + source_ref: "source pcsx2/CDVD/CDVD.cpp:194-226" note: > Console EEPROM data (language, timezone, region, iLink ID, OSD settings). Path derived from BIOS path with extension replaced to .nvm. @@ -90,7 +91,7 @@ files: hle_fallback: true size: 4 description: "Mechacon version" - source_ref: "source pcsx2/CDVD/CDVD.cpp:229-245" + source_ref: "source pcsx2/CDVD/CDVD.cpp:228-243" note: > Mechacon (disc drive controller) version as u32. Auto-created with default 0x00020603 if missing. @@ -99,7 +100,7 @@ files: path: "pcsx2/resources/cheats_ws.zip" required: false description: "widescreen patches archive" - source_ref: "source pcsx2/VMManager.cpp:383-396" + source_ref: "source pcsx2/VMManager.cpp:382-395" note: > ZIP archive of per-game widescreen (16:9) patches in pnach format. Only loaded when widescreen patches are enabled via core options. @@ -109,7 +110,7 @@ files: path: "pcsx2/resources/cheats_ni.zip" required: false description: "no-interlacing patches archive" - source_ref: "source pcsx2/VMManager.cpp:418-431" + source_ref: "source pcsx2/VMManager.cpp:417-430" note: > ZIP archive of per-game no-interlacing patches in pnach format. Only loaded when no-interlacing patches are enabled via core options. diff --git a/emulators/ymir.yml b/emulators/ymir.yml index 19fc4143..896d4519 100644 --- a/emulators/ymir.yml +++ b/emulators/ymir.yml @@ -59,14 +59,56 @@ files: - name: "sega_101.bin" system: sega-saturn required: true + mode: standalone size: 524288 validation: [size] - aliases: [mpr-17933.bin, saturn_bios.bin] description: "Saturn IPL ROM (BIOS). Any region accepted." note: "The libretro port reads sega_101.bin, mpr-17933.bin and saturn_bios.bin from its system directory (kBiosFilenames)." source_ref: standalone: "apps/ymir-sdl3/src/util/rom_loader.cpp:9-22, apps/ymir-sdl3/src/app/rom_manager.cpp:14-53, libs/ymir-core/include/ymir/sys/memory_defs.hpp:9" - libretro: "apps/ymir-libretro/src/libretro.cpp:305-309, apps/ymir-libretro/src/libretro.cpp:651-690, apps/ymir-libretro/src/libretro.cpp:692-703" + + # The libretro build reads three names in its system directory. The md5 of + # the first two is that of the image whose XXH128 is the kLocalBiosHashes + # row the port gives a region to. + - name: "sega_101.bin" + system: sega-saturn + region: [japan] + priority: 1 + required: true + mode: libretro + size: 524288 + validation: [size] + md5: "85ec9ca47d8f6807718151cbcca8b964" + description: "Saturn IPL ROM 1.01 (1994-12-28), Japan." + note: "First name read. The port gives this image the Japan region, so it is loaded when ymir_region is japan and kept or switched to for a disc whose area code is J. Any other 512 KiB file under this name is read with the region the IPL database gives it, or none." + source_ref: + libretro: "apps/ymir-libretro/src/libretro.cpp:305-309, apps/ymir-libretro/src/libretro.cpp:596-606, apps/ymir-libretro/src/libretro.cpp:627-648, apps/ymir-libretro/src/libretro.cpp:651-702, apps/ymir-libretro/src/libretro.cpp:1231-1238, apps/ymir-libretro/src/libretro.cpp:1290-1306" + + - name: "mpr-17933.bin" + system: sega-saturn + region: [north-america, europe, asia-ntsc, asia-pal, brazil, latin-america] + priority: 2 + required: true + mode: libretro + size: 524288 + validation: [size] + md5: "3240872c70984b6cbfda1586cab68dbe" + description: "Saturn IPL ROM 1.01a (1994-11-15), North America and Europe." + note: "Second name read. The port gives this image the US_EU region, loaded when ymir_region is north_america or europe and for a disc whose area code is any of T, U, B, A, E and L. Area K maps to the KR region, which neither image carries, so a Korean disc keeps the image already loaded." + source_ref: + libretro: "apps/ymir-libretro/src/libretro.cpp:305-309, apps/ymir-libretro/src/libretro.cpp:596-606, apps/ymir-libretro/src/libretro.cpp:627-648, apps/ymir-libretro/src/libretro.cpp:651-702, apps/ymir-libretro/src/libretro.cpp:1231-1238, apps/ymir-libretro/src/libretro.cpp:1290-1306, libs/ymir-core/include/ymir/media/media_defs.hpp:17-29" + + - name: "saturn_bios.bin" + system: sega-saturn + priority: 3 + required: false + mode: libretro + size: 524288 + validation: [size] + description: "Saturn IPL ROM, any revision" + note: "Third name read. No region comes with the name: the image gets the one the IPL database gives its hash, or none, and is loaded when it is the first candidate found or its region is the one asked for." + source_ref: + libretro: "apps/ymir-libretro/src/libretro.cpp:305-309, apps/ymir-libretro/src/libretro.cpp:651-702, apps/ymir-libretro/src/libretro.cpp:1231-1238" # IPL images named by the standalone XXH128 database. The md5 is that of # the image whose seeded XXH128 matches the database row. diff --git a/platforms/retropie.yml b/platforms/retropie.yml index b7849ea9..6909f888 100644 --- a/platforms/retropie.yml +++ b/platforms/retropie.yml @@ -506,6 +506,15 @@ launchers: kind: package names: [] source_ref: "scriptmodules/emulators/drastic.sh:12, 40" +- emulator: drastic + package: drastic + build: standalone + kind: passes_path + names: [] + search_dir: $md_conf_root/nds/drastic + passes_to: working directory of $md_inst/drastic.sh + fails_when_missing: false + source_ref: "scriptmodules/emulators/drastic.sh:48" - emulator: fbzx package: fbzx repo: "git https://github.com/rastersoft/fbzx :_get_branch_fbzx" diff --git a/scripts/generate_db.py b/scripts/generate_db.py index 855a6fd6..4eaddf74 100644 --- a/scripts/generate_db.py +++ b/scripts/generate_db.py @@ -614,9 +614,12 @@ def _collect_all_aliases(files: dict) -> dict: ["sgb2_bios.bin", "sgb2_boot.bin", "sgb2.boot.rom"], ["sgb1.program.rom", "SGB1.sfc/program.rom"], ["sgb2.program.rom", "SGB2.sfc/program.rom"], - # Nintendo DS - ["bios7.bin", "nds7.bin"], - ["bios9.bin", "nds9.bin"], + # Nintendo DS. DraStic opens nds_bios_arm9.bin, nds_bios_arm7.bin + # and nds_firmware.bin and checks their size only: the collection + # answers those names with its DS dumps. + ["NDS_Bios7.bin", "bios7.bin", "nds7.bin", "nds_bios_arm7.bin"], + ["NDS_Bios9.bin", "bios9.bin", "nds9.bin", "nds_bios_arm9.bin"], + ["NDS_Lite_Firmware.bin", "nds_firmware.bin"], ["dsi_sd_card.bin", "nds_sd_card.bin"], # MSX ["MSX.ROM", "MSX.rom", "Machines/Shared Roms/MSX.rom"], diff --git a/scripts/launchers.py b/scripts/launchers.py index e12b21f5..ae81146a 100644 --- a/scripts/launchers.py +++ b/scripts/launchers.py @@ -22,10 +22,23 @@ from dataclasses import dataclass from common import declared_root, name_match_size_ok, sanitize_pack_path -# The kinds whose names say which file the launcher opens. The others hand -# over a directory, copy a file the platform already names, or let the +# The kinds whose names say which file the launcher opens. A link of a named +# file is one too: ROCKNIX links /storage/roms/bios/nds_bios_arm9.bin into +# DraStic's system folder, so that is the path the emulator reads. The others +# hand over a directory, copy a file the platform already names, or let the # emulator choose. -PLACE_KINDS = ("names_searched", "fixed_name", "core_option") +PLACE_KINDS = ("names_searched", "fixed_name", "core_option", "links") + +# How each launcher extractor spells the BIOS directory in search_dir, as the +# platform's own scripts name it: RetroPie's scriptmodules ($biosdir, +# retropie_launchers.BIOSDIR), EmuDeck's EmuScripts (${biosPath}, +# emudeck_launchers.BIOS) and RetroDECK's framework ($bios_path, +# retrodeck_launchers.BIOS). +BIOS_ROOT_TOKENS = ("$biosdir", "${biosdir}", "${biosPath}", "$bios_path", "${bios_path}") + +# A path spelled from a home or filesystem root names the BIOS directory by +# its base name: /userdata/bios, ~/roms/bios, $HOME/RetroPie/BIOS. +_ROOTED = ("/", "~/", "$HOME/", "$home/", "${HOME}/") _LABELLED = re.compile(r"^([^\s()]+\.[A-Za-z0-9]+) \([^()]*\)$") _PLACEHOLDER = re.compile(r"\{(\w+)\}") @@ -68,19 +81,22 @@ class LauncherPlace: option or a system leaves that default in use elsewhere, and a core option offers a file the user may choose without ever choosing it. """ - return self.kind in ("names_searched", "fixed_name") and not self.conditional + return ( + self.kind in ("names_searched", "fixed_name", "links") + and not self.conditional + ) def _bios_relative(directory: str, base: str) -> str | None: """A launcher directory relative to the BIOS root, None when it is not one.""" directory = directory.strip().rstrip("/") - roots = {base, ""} + roots = {base, "", *BIOS_ROOT_TOKENS} head, _, rest = directory.partition("/") if directory in roots: return "" if head in roots: relative = rest - elif directory.startswith("/") and base and f"/{base}/" in f"{directory}/": + elif directory.startswith(_ROOTED) and base and f"/{base}/" in f"{directory}/": relative = f"{directory}/".split(f"/{base}/", 1)[1].rstrip("/") else: return None @@ -390,9 +406,18 @@ class LauncherLayer: if isinstance(filled, dict): added.append(filled) if place.stands_for_default: + # The emulator opens the launcher's file instead of its + # own name, so a copy under that name is read by nobody: + # EmuDeck passes Complex_4627v1.03.bin to xemu, which then + # never opens Complex_4627.bin. A held file sitting on one + # of the place's own paths is what fills it. + own = {place.fold(p) for p in place.paths} settled.update( id(report) for _candidate, report in matched if not report.get("in_repo") + or place.fold(sanitize_pack_path( + report.get("path") or report.get("name", "") + )) not in own ) return added, settled diff --git a/scripts/scraper/retropie_launchers.py b/scripts/scraper/retropie_launchers.py index 1d9e1df0..320ab841 100644 --- a/scripts/scraper/retropie_launchers.py +++ b/scripts/scraper/retropie_launchers.py @@ -48,6 +48,11 @@ OUTSIDE_BIOS: dict[str, tuple[str, ...]] = { "gngeopi": ("-B",), # gngeo -B/--biospath } +# Packages whose emulator reads its system files below the working directory +# the generated launch script enters with pushd, a folder outside $biosdir: +# DraStic opens system/ under it. Each must still pushd. +WORKING_DIRECTORY: tuple[str, ...] = ("drastic",) + _FUNCTION = re.compile(r"^function\s+([\w.+-]+)\s*\(\)\s*\{\s*$") _ASSIGN = re.compile(r"^(?:local\s+)?([A-Za-z_]\w*)=(.*)$") _DECLARE = re.compile(r"^local\s+([A-Za-z_]\w*(?:\s+[A-Za-z_]\w*)*)\s*$") @@ -518,6 +523,7 @@ class PackageReader: if links: self._links(links) self._outside_bios(events) + self._working_directory(events) return self.records @staticmethod @@ -821,6 +827,32 @@ class PackageReader: ) + def _working_directory(self, events: list[Event]) -> None: + """The folder a generated launch script enters before it runs the emulator.""" + if self.package.id not in WORKING_DIRECTORY: + return + found = False + for event in events: + if not event.heredoc or not event.words or event.words[0] != ["pushd"]: + continue + script = _redirect_target(event.opener) + if script is None or len(event.words) != 2 or len(event.words[1]) != 1: + raise LauncherPatternMissing( + f"{event.path}:{event.line}", f"pushd outside a launch script: {event.raw}" + ) + found = True + self.record( + "passes_path", [], [(event.path, event.line)], + search_dir=event.words[1][0].strip("\"'"), + passes_to=f"working directory of {script}", + fails_when_missing=False, depends_on_option=self._depends(event), + ) + if not found: + raise LauncherPatternMissing( + self.package.path, "the launch script no longer enters a working directory" + ) + + def _redirect_target(words: list[list[str]]) -> str | None: """The file a `cat > FILE << EOF` or `cat << EOF >> FILE` line writes.""" flat = [w[0] for w in words if w] diff --git a/tests/test_db_aliases.py b/tests/test_db_aliases.py index 3d2af69f..1ffe4fe2 100644 --- a/tests/test_db_aliases.py +++ b/tests/test_db_aliases.py @@ -153,5 +153,43 @@ class AnUnreadableSourceStopsTheRun(unittest.TestCase): self.assertIn("broken.yml", str(caught.exception)) +class DrasticNamesTheDSDumps(unittest.TestCase): + """DraStic opens nds_bios_arm9.bin, nds_bios_arm7.bin and nds_firmware.bin + and checks their size only. Its profile listed nine other names none of + its binaries contains, which were the only way the collection answered + its own three names.""" + + def _aliases(self, files: dict) -> dict: + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp: + previous = os.getcwd() + os.chdir(tmp) + try: + Path("emulators").mkdir() + return generate_db._collect_all_aliases(files) + finally: + os.chdir(previous) + + def test_the_collection_answers_drastic_names(self): + files = { + "a9": {"name": "NDS_Bios9.bin", "path": "bios/Nintendo/DS/NDS_Bios9.bin", "md5": "m9"}, + "a7": {"name": "NDS_Bios7.bin", "path": "bios/Nintendo/DS/NDS_Bios7.bin", "md5": "m7"}, + "fw": {"name": "NDS_Lite_Firmware.bin", + "path": "bios/Nintendo/DS/NDS_Lite_Firmware.bin", "md5": "mf"}, + } + aliases = self._aliases(files) + named = {sha: {a["name"] for a in entries} for sha, entries in aliases.items()} + self.assertIn("nds_bios_arm9.bin", named["a9"]) + self.assertIn("nds_bios_arm7.bin", named["a7"]) + self.assertIn("nds_firmware.bin", named["fw"]) + + def test_the_profile_carries_no_name_drastic_never_opens(self): + import yaml + + profile = yaml.safe_load((REPO_ROOT / "emulators" / "drastic.yml").read_text()) + self.assertEqual( + [e["name"] for e in profile["files"] if e.get("aliases")], [], + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_launcher_layer.py b/tests/test_launcher_layer.py index 4231a0c8..bb3069a8 100644 --- a/tests/test_launcher_layer.py +++ b/tests/test_launcher_layer.py @@ -250,6 +250,50 @@ class LauncherLayerTest(unittest.TestCase): self.assertNotIn("core.bin", {u["name"] for u in self._report()}) self.assertNotIn("core.bin", self._pack_names()) + def test_the_default_name_a_launcher_replaces_is_not_packed(self) -> None: + """EmuDeck passes Complex_4627v1.03.bin to xemu, which then never + opens Complex_4627.bin: the pack carried both, one read by nobody.""" + report = {u["path"]: u for u in self._report()} + self.assertIn("sub/fix.bin", report) + self.assertNotIn("core.bin", report) + self.assertNotIn("core.bin", self._pack_names()) + + def test_a_link_of_a_named_file_is_a_place(self) -> None: + """ROCKNIX links /storage/roms/bios/nds_bios_arm9.bin into DraStic's + system folder (start_drastic.sh:37-41): the file goes where the + link reads it, not under the emulator's own folder.""" + self.config["launchers"][2].update( + kind="links", names=["fix.bin"], search_dir="/storage/roms/bios", + ) + places = [p for p in launcher_places(self.config) if p.kind == "links"] + self.assertEqual([p.paths for p in places], [("fix.bin",)]) + self.assertTrue(places[0].stands_for_default) + report = {u["path"]: u for u in self._report()} + self.assertIn("fix.bin", report) + self.assertNotIn("core.bin", report) + + def test_a_link_of_a_directory_is_no_place(self) -> None: + self.config["launchers"][2].update( + kind="links", names=[], search_dir="/userdata/bios/sub", + ) + self.assertFalse([p for p in launcher_places(self.config) if p.kind == "links"]) + + def test_the_platform_spelling_of_the_bios_folder_is_a_root(self) -> None: + """RetroPie names $biosdir, EmuDeck ${biosPath}, RetroDECK $bios_path: + read as no folder at all, every place of those platforms was dropped.""" + for spelling in ("$biosdir", "${biosPath}", "$bios_path", "$HOME/RetroPie/bios"): + self.config["launchers"][2]["search_dir"] = f"{spelling}/sub" + with self.subTest(spelling=spelling): + self.assertEqual(launcher_places(self.config)[2].paths, ("sub/sub/fix.bin",)) + + def test_every_extractor_spelling_of_the_bios_folder_is_known(self) -> None: + import launchers + from scraper import emudeck_launchers, retrodeck_launchers, retropie_launchers + + for token in (emudeck_launchers.BIOS, retrodeck_launchers.BIOS, + retropie_launchers.BIOSDIR): + self.assertIn(token, launchers.BIOS_ROOT_TOKENS) + def test_no_place_is_filled_twice(self) -> None: added = [u["path"] for u in self._report() if u.get("launcher")] self.assertEqual(sorted(added), ["e.bin", "sub/fix.bin"]) diff --git a/tests/test_launchers_retropie.py b/tests/test_launchers_retropie.py index a4280c73..56babfbf 100644 --- a/tests/test_launchers_retropie.py +++ b/tests/test_launchers_retropie.py @@ -269,6 +269,47 @@ class CommandLines(unittest.TestCase): rl.extract_launchers(files) +DRASTIC = module("drastic", """ + function configure_drastic() { + mkRomDir "nds" + + addEmulator 1 "$md_id" "nds" "$md_inst/drastic.sh %ROM%" + addSystem "nds" + + [[ "$md_mode" == "remove" ]] && return + + isPlatform "kms" && ! isPlatform "x11" && setBackend "$md_id" "x11" + + cat > "$md_inst/drastic.sh" << _EOF_ + #!/bin/bash + pushd "$md_conf_root/nds/drastic" + $md_inst/drastic "\\$1" + popd + _EOF_ + chmod +x "$md_inst/drastic.sh" + } +""", 36) + + +class WorkingDirectory(unittest.TestCase): + def test_drastic_reads_below_the_folder_its_script_enters(self): + found = records({f"{EMULATORS}/drastic.sh": DRASTIC}, "drastic") + entered = [r for r in found if r["kind"] == "passes_path"] + self.assertEqual( + [(r["search_dir"], r["passes_to"], r["source_ref"]) for r in entered], + [( + "$md_conf_root/nds/drastic", + "working directory of $md_inst/drastic.sh", + f"{EMULATORS}/drastic.sh:48", + )], + ) + + def test_a_launch_script_that_no_longer_enters_a_folder_raises(self): + files = {f"{EMULATORS}/drastic.sh": DRASTIC.replace('pushd "', 'echo "')} + with self.assertRaises(rl.LauncherPatternMissing): + rl.extract_launchers(files) + + class LaunchScripts(unittest.TestCase): def test_fs_uae_refuses_to_start_without_kick13(self): files = {f"{EMULATORS}/fs-uae.sh": FS_UAE, f"{EMULATORS}/fs-uae/fs-uae.sh": FS_UAE_LAUNCHER} diff --git a/tests/test_region.py b/tests/test_region.py index 8fe967f9..6669680a 100644 --- a/tests/test_region.py +++ b/tests/test_region.py @@ -1347,5 +1347,37 @@ class ARefusedRegionalFileHoldsNoSlot(unittest.TestCase): self.assertIn("rg_jp.bin", report["pack_destinations"]) +class YmirLibretroRegions(unittest.TestCase): + """ymir's libretro port gives sega_101.bin the Japan region and + mpr-17933.bin US_EU (apps/ymir-libretro/src/libretro.cpp:596-606) and + loads the candidate a region asks for (627-648, 1290-1306). One entry for + all three names put no region on any of them.""" + + def test_each_name_carries_the_region_the_port_gives_it(self): + import yaml + + profile = yaml.safe_load( + (Path(__file__).resolve().parents[1] / "emulators" / "ymir.yml").read_text() + ) + libretro = { + entry["name"]: entry for entry in profile["files"] + if entry.get("mode") == "libretro" + and entry["name"] in ("sega_101.bin", "mpr-17933.bin", "saturn_bios.bin") + } + self.assertEqual(sorted(libretro), ["mpr-17933.bin", "saturn_bios.bin", "sega_101.bin"]) + self.assertEqual(libretro["sega_101.bin"]["region"], ["japan"]) + self.assertTrue( + {"north-america", "europe"} <= set(libretro["mpr-17933.bin"]["region"]) + ) + self.assertNotIn("japan", libretro["mpr-17933.bin"]["region"]) + self.assertNotIn("region", libretro["saturn_bios.bin"]) + self.assertEqual( + [libretro[n]["priority"] for n in ("sega_101.bin", "mpr-17933.bin", "saturn_bios.bin")], + [1, 2, 3], + ) + self.assertFalse(any(e.get("aliases") for e in profile["files"] + if e["name"] == "sega_101.bin")) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_sega_filing.py b/tests/test_sega_filing.py new file mode 100644 index 00000000..0fd10af1 --- /dev/null +++ b/tests/test_sega_filing.py @@ -0,0 +1,49 @@ +"""Sega ROMs live under bios/Sega. + +The SF-7000 disk unit ROM, which only the sega-sf7000 entries read, sat in +bios/Microsoft/MSX because blueMSX keeps its machine folders together. The +collection files a dump under the maker of the machine it belongs to, and +the blueMSX layout is a destination the pack writes, not a storage rule. +Reads the repository. +""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + + +class SegaFilesStayUnderSega(unittest.TestCase): + def test_no_sega_only_file_under_msx(self): + folder = REPO_ROOT / "bios" / "Microsoft" / "MSX" + if not folder.is_dir(): + self.skipTest("bios/Microsoft/MSX is absent") + from common import load_emulator_profiles + + systems: dict[str, set[str]] = {} + for profile in load_emulator_profiles(str(REPO_ROOT / "emulators")).values(): + for entry in profile.get("files") or []: + if not isinstance(entry, dict) or not entry.get("system"): + continue + for name in [entry.get("name", ""), *(entry.get("aliases") or [])]: + key = Path(str(name)).name.lower() + systems.setdefault(key, set()).add(str(entry["system"])) + misfiled = sorted( + str(path.relative_to(folder)) + for path in folder.rglob("*") + if path.is_file() + and systems.get(path.name.lower()) + and all(s.startswith("sega-") for s in systems[path.name.lower()]) + ) + self.assertEqual(misfiled, []) + + def test_sf7000_rom_is_filed_under_sega(self): + self.assertTrue((REPO_ROOT / "bios" / "Sega" / "SF-7000" / "sf7000.rom").is_file()) + + +if __name__ == "__main__": + unittest.main() diff --git a/wiki/adding-a-platform.md b/wiki/adding-a-platform.md index 166bac8f..d4d1349a 100644 --- a/wiki/adding-a-platform.md +++ b/wiki/adding-a-platform.md @@ -326,6 +326,10 @@ Each package also gets one `kind: package` record, and that record is what maps a package to the profile of the build it compiles: `repo` names the project (`minivmac` builds github.com/minivmac/minivmac, Mini vMac 37, not the libretro port), so the mapping never rests on stripping `lr-` from a name. +DraStic reads its BIOS below the folder its generated launch script enters +(`pushd "$md_conf_root/nds/drastic"`, `drastic.sh:48`), outside `$biosdir`: +that folder is a `passes_path` record, and the scrape stops if the script no +longer enters one. RetroDECK's records come from each component's `component_prepare.sh`, `component_launcher.sh` and `component_functions.sh`, and from the presets they copy, with `$bios_path` as the framework names it. In both, every line @@ -337,17 +341,25 @@ stops with `LauncherPatternMissing`. cross-reference both of them consume. A `names_searched` list is one place: the names are tried in order and one held file fills it, so a core file the launcher would never open once another name of the list is there is not -reported missing. A `fixed_name` or `core_option` name is a place of its own. +reported missing. A `fixed_name` or `core_option` name is a place of its own, +and so is a `links` name: ROCKNIX links `/storage/roms/bios/nds_bios_arm9.bin` +into DraStic's system folder, so that path is the one DraStic reads. Only a record that applies without condition settles the core's own name: a core option the user may never set, or a record tied to one system, leaves -the core's default in use elsewhere. +the core's default in use elsewhere. Settled, the core's own name is neither +reported nor packed, held or not: EmuDeck passes `Complex_4627v1.03.bin` to +xemu, which then never opens `Complex_4627.bin`. A place that no declared file and no packed core file fills receives, under the launcher's name, a file the core accepts: the file the collection knows under that name when the core takes its content, otherwise the copy held for the core entry read under that name. Records that only hand over a directory -(`passes_path`, `scans_dir`) or copy a file the platform already names -(`links`, `copies`) place nothing, and a `search_dir` the code describes -rather than names ("directory of the Chihiro BIOS found") is skipped. +(`passes_path`, `scans_dir`, a `links` record with no names) or copy a file +the platform already names (`copies`) place nothing, and a `search_dir` the +code describes rather than names ("directory of the Chihiro BIOS found") is +skipped. `$biosdir`, `${biosPath}` and `$bios_path` are the BIOS directory as +RetroPie, EmuDeck and RetroDECK spell it, and a path from a root +(`/userdata/bios`, `$HOME/RetroPie/BIOS/dc`) is placed by the platform's +`base_destination`. ### Inheritance diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 275dda99..52c0d41e 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -38,7 +38,7 @@ Every module in `tests/`, by the area it guards. | Area | Fixtures | Modules | |------|----------|---------| | Core regression suites | synthetic | `test_e2e`, `test_audit_regressions`, `test_tool_messages` | -| File resolution and identity | synthetic + collection | `test_adler32_resolution`, `test_agnostic_scan`, `test_agnostic_scan_mode`, `test_agnostic_scope`, `test_agnostic_substitute`, `test_archive_choice`, `test_bios_companions`, `test_bundled_identity`, `test_casefold_cache`, `test_db_aliases`, `test_declared_names`, `test_derivations`, `test_directory_entries`, `test_extra_identity`, `test_foreign_tree`, `test_foreign_tree_names`, `test_frontend_digest`, `test_game_data_homonyms`, `test_hashing`, `test_identity`, `test_md5_pack_context`, `test_name_tiebreak`, `test_nec_filing`, `test_owner_resolution`, `test_path_suffix`, `test_resolution_evidence`, `test_shipped_content`, `test_twin_declarations`, `test_validated_choice`, `test_variant_choice`, `test_variant_identity`, `test_zipped_member` | +| File resolution and identity | synthetic + collection | `test_adler32_resolution`, `test_agnostic_scan`, `test_agnostic_scan_mode`, `test_agnostic_scope`, `test_agnostic_substitute`, `test_archive_choice`, `test_bios_companions`, `test_bundled_identity`, `test_casefold_cache`, `test_db_aliases`, `test_declared_names`, `test_derivations`, `test_directory_entries`, `test_extra_identity`, `test_foreign_tree`, `test_foreign_tree_names`, `test_frontend_digest`, `test_game_data_homonyms`, `test_hashing`, `test_identity`, `test_md5_pack_context`, `test_name_tiebreak`, `test_nec_filing`, `test_owner_resolution`, `test_path_suffix`, `test_resolution_evidence`, `test_sega_filing`, `test_shipped_content`, `test_twin_declarations`, `test_validated_choice`, `test_variant_choice`, `test_variant_identity`, `test_zipped_member` | | Verification, cross-reference and profile checks | synthetic + profiles | `test_crypto_aes`, `test_crypto_unique_data`, `test_crypto_verify`, `test_case_insensitive_platforms`, `test_checks_have_values`, `test_core_file_fills_declaration`, `test_core_size_refusal`, `test_cross_reference_sources`, `test_file_consumers`, `test_fileless_audit`, `test_gap_settlement`, `test_hidden_extras`, `test_launcher_aliases`, `test_launcher_layer`, `test_native_mode`, `test_no_case_collisions`, `test_path_conflict_status`, `test_path_null_copies`, `test_profile_checks`, `test_profile_repositories`, `test_romm_hash_rule`, `test_shared_groups`, `test_standalone_mode`, `test_undeclared_dedup`, `test_unplaced_extras`, `test_validation_scope`, `test_verify_all_dir`, `test_verify_emulator_evidence`, `test_verify_listing`, `test_verify_pack_choice`, `test_verify_release_asset` | | Packs and manifests | synthetic + real packs | `test_artifact_lock`, `test_atomic_artifacts`, `test_batocera_cores`, `test_deterministic_zip`, `test_emulator_pack_counts`, `test_finalize_pack_names`, `test_large_file_cache`, `test_libretro_layout`, `test_manifest_entry`, `test_pack_conformance_dirs`, `test_pack_core_extras`, `test_pack_counts`, `test_pack_destinations`, `test_pack_failures`, `test_pack_file_names`, `test_pack_grouping`, `test_pack_integrity`, `test_pack_member_identity`, `test_pack_readme`, `test_platform_inheritance`, `test_platform_keys`, `test_required_only_archives`, `test_required_only_twins`, `test_split_extras`, `test_split_names`, `test_split_pack`, `test_system_pack_extras`, `test_system_pack_parity`, `test_torrentzip`, `test_which_pack` | | Slots and regions | synthetic + profiles | `test_region`, `test_region_agnostic_scan`, `test_region_groups`, `test_region_held`, `test_region_platform_index`, `test_region_required_only`, `test_slot`, `test_slots`, `test_slots_shipped_file` |