diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index af7525a77..ac1843322 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -50,7 +50,8 @@ from common import ( select_emulator_profiles, ) from hashing import parse_hash_list -from launchers import LauncherLayer, declared_destinations, held_record +from launchers import LauncherLayer, declared_destinations, held_record, set_by +from launchers import _directories as launcher_directories from launchers import _names as launcher_names from validation import ( case_folded_slots, @@ -1151,6 +1152,16 @@ def launcher_reads_not_held( a copy is reported with `held_as`, since the launcher does not open it under the name it asks for. *emulator* keeps the records whose emulator name designates that profile. + + Three records ask for nothing the collection lacks. A search keeps the + first name it finds, so one held name fills it: RetroBat's Amiberry + launcher returns the first of kick34005.A500, kick34005.A500.rom and + amiga-os-130.rom that exists (Amiberry.Generator.cs:674-699). A copy + whose source lies outside the BIOS directory brings a file the platform + installs itself, from its own package or written by its own script + (RetroPie's lr-vecx copies $md_inst/system.bin). A file handed to the + setting a reader's profile entry is read from is that entry's file, + which the pack places under the launcher's name. """ holder = _NameHolder(db, profiles, data_dir_registry) every_profile = set(profiles) @@ -1169,6 +1180,20 @@ def launcher_reads_not_held( if emulator and emulator not in readers: continue folds = bool(record.get("case_insensitive") or config.get("case_insensitive_fs")) + base = str(config.get("base_destination") or "bios") + if record.get("kind") == "copies" and not launcher_directories(record, base): + continue + passes_to = str(record.get("passes_to") or "") + if passes_to and any( + set_by(entry, passes_to) and holder._entry_held(entry) + for key in readers + for entry in profiles[key].get("files") or [] + ): + continue + if record.get("kind") == "names_searched" and any( + (found := holder.held(name)) and (found[1] or folds) for name in names + ): + continue reader = { "emulator": str(record.get("emulator", "")), "build": str(record.get("build", "")), diff --git a/scripts/launchers.py b/scripts/launchers.py index 1f9ce1d56..2b2a2014b 100644 --- a/scripts/launchers.py +++ b/scripts/launchers.py @@ -65,6 +65,7 @@ class LauncherPlace: anchored: bool conditional: bool source_ref: str + passes_to: str = "" def fold(self, name: str) -> str: return name.lower() if self.case_insensitive else name @@ -87,6 +88,24 @@ class LauncherPlace: ) +def setting_tokens(passes_to: str) -> set[str]: + """The words of a record's ``passes_to``, as an emulator setting is spelled. + + "command line -romfile4p" hands the file to sdltrs's romfile4p option, + "settings.bml NeoGeoPocket/Firmware/BIOS.World" to an ares setting. + """ + return { + word.strip("()\"',;").lstrip("-") + for word in passes_to.split() + } - {""} + + +def set_by(entry: dict, passes_to: str) -> bool: + """Whether a launcher hands its file to the setting a profile entry is read from.""" + key = str(entry.get("config_key") or "") + return bool(key) and key in setting_tokens(passes_to) + + 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("/") @@ -189,6 +208,7 @@ def launcher_places(config: dict) -> list[LauncherPlace]: "anchored": anchored, "conditional": bool(record.get("depends_on_option")), "source_ref": str(record.get("source_ref", "")), + "passes_to": str(record.get("passes_to") or ""), } if kind == "names_searched": # Each directory in turn, every name in it. @@ -461,6 +481,10 @@ class LauncherLayer: names = own & basenames if names: return _Candidate(entry, names, False) + # RetroPie runs sdl2trs with -romfile4p $biosdir/level4p.rom: the + # file under that name is the one the romfile4p slot reads. + if set_by(entry, place.passes_to): + return _Candidate(entry, basenames, False) content = { place.fold(path.rsplit("/", 1)[-1]) for path in place.paths diff --git a/tests/test_launcher_gaps.py b/tests/test_launcher_gaps.py index b68d19770..28528f710 100644 --- a/tests/test_launcher_gaps.py +++ b/tests/test_launcher_gaps.py @@ -66,8 +66,13 @@ LAUNCHERS = [ _record("diremu", "passes_path", []), {"emulator": "pkg", "build": "standalone", "kind": "package", "names": [], "source_ref": "pkg.sh:1"}, - _record("missemu", "names_searched", ["held.bin", "absent.bin"]), + _record("missemu", "names_searched", ["absent.bin"]), _record("otheremu", "copies", ["absent.bin"]), + _record("altemu", "names_searched", ["alt.bin", "held.bin", "late.bin"]), + _record("pkgemu", "copies", ["shipped.bin"], search_dir="$md_inst"), + {"emulator": "pkgemu", "build": "standalone", "kind": "copies", + "names": ["written.ini"], "source_ref": "pkg.sh:2"}, + _record("setemu", "fixed_name", ["setname.bin"], passes_to="command line -romslot"), _record("caseemu", "fixed_name", ["mixed.rom"]), _record("foldemu", "fixed_name", ["MIXED.rom"], case_insensitive=True), _record("labelemu", "core_option", ["labelled.rom (CDZ)"]), @@ -128,6 +133,10 @@ class LauncherGapTest(unittest.TestCase): "emulator": "AliasEmu", "type": "standalone", "systems": ["test-sys"], "files": [{"name": "core.bin", "aliases": ["alias.bin"], "required": True}], }, + "setemu": { + "emulator": "SetEmu", "type": "standalone", "systems": ["test-sys"], + "files": [{"name": "core.bin", "config_key": "romslot", "required": True}], + }, "missemu": { "emulator": "MissEmu", "type": "standalone", "systems": ["test-sys"], "files": [{"name": "absent.bin", "required": True, "md5": "0" * 32}], @@ -167,6 +176,19 @@ class LauncherGapTest(unittest.TestCase): reported = {r["emulators"][0] for r in self._rows().values()} self.assertFalse(reported & {"tplemu", "diremu", "pkg"}) + def test_a_search_with_one_held_name_reads_nothing_missing(self) -> None: + """The launcher keeps the first name it finds, so the held one fills it.""" + names = {name for _, name in self._rows()} + self.assertFalse(names & {"alt.bin", "late.bin"}) + + def test_a_copy_from_the_platform_package_is_not_owed(self) -> None: + """RetroPie copies lr-vecx's system.bin from $md_inst and writes mame.ini.""" + names = {name for _, name in self._rows()} + self.assertFalse(names & {"shipped.bin", "written.ini"}) + + def test_a_name_handed_to_a_held_entry_setting_is_held(self) -> None: + self.assertNotIn("setname.bin", {name for _, name in self._rows()}) + def test_a_label_after_the_name_is_dropped(self) -> None: self.assertEqual(launcher_file_names(LAUNCHERS[-1]), ["labelled.rom"]) self.assertIn(("gaptest", "labelled.rom"), self._rows()) @@ -225,7 +247,7 @@ class LauncherGapTest(unittest.TestCase): names = {r["name"] for r in report[LAUNCHER_READS_KEY]} self.assertEqual(names, {"absent.bin", "mixed.rom", "labelled.rom"}) # The emulator rows keep their own shape and counts. - self.assertEqual(set(report) - {LAUNCHER_READS_KEY}, {"aliasemu", "missemu"}) + self.assertEqual(set(report) - {LAUNCHER_READS_KEY}, {"aliasemu", "missemu", "setemu"}) def test_verify_counts_and_exit_code_are_unchanged(self) -> None: """A launcher name is reported, not judged: nothing else moves.""" diff --git a/tests/test_launcher_layer.py b/tests/test_launcher_layer.py index a7f99c230..ed06eeeb7 100644 --- a/tests/test_launcher_layer.py +++ b/tests/test_launcher_layer.py @@ -252,6 +252,27 @@ class LauncherLayerTest(unittest.TestCase): self.assertNotIn("core.bin", {u["name"] for u in self._report()}) self.assertNotIn("core.bin", self._pack_names()) + def test_a_file_handed_to_an_entry_setting_takes_the_entry_content(self) -> None: + """RetroPie runs sdl2trs with -romfile4p $biosdir/level4p.rom and + EmuDeck writes a No-Intro name into ares's NeoGeoPocket firmware + setting: neither name is the core's, and the held file of the entry + that setting reads went in under no name at all.""" + profile = { + "emulator": "FixEmu", "type": "standalone", "systems": ["test-sys"], + "files": [{"name": "core.bin", "required": True, "config_key": "romslot"}, + {"name": "other.bin", "required": True, "config_key": "otherslot"}], + } + with open(os.path.join(self.emulators_dir, "fixemu.yml"), "w") as fh: + yaml.dump(profile, fh) + self.config["launchers"][2]["names"] = ["launcher.rom"] + self.config["launchers"][2]["passes_to"] = "command line -romslot" + _emulator_profiles_cache.clear() + self.profiles = load_emulator_profiles(self.emulators_dir) + added = {u["path"]: u for u in self._report() if u.get("launcher")} + self.assertEqual(added["launcher.rom"]["launcher_of"], "core.bin") + self.assertEqual(added["launcher.rom"]["md5"], _md5(CONTENT["core.bin"])) + self.assertIn("launcher.rom", 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."""