diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index faa0e90ba..7b9bf179f 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -55,6 +55,7 @@ from launchers import ( catalogued_model_files, declared_destinations, held_record, + name_settings, set_by, setting_readers, suffixed_stem, @@ -1199,27 +1200,30 @@ def launcher_reads_not_held( 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 "") - setting_entries = [ - entry - for key in readers - for entry in profiles[key].get("files") or [] - if passes_to and set_by(entry, passes_to) - ] - if any(holder._entry_held(entry) for entry in setting_entries): - continue - # A placeholder read from the setting takes the held dump the - # catalogues name after the model the launcher's name spells. - by_model = { - name - for name in names - for entry in setting_entries - if "<" in str(entry.get("name", "")) - and any( - name_match_size_ok(entry, record_.get("size")) - for record_ in catalogued_model_files(name, db) + settings = name_settings(record, names) + + def by_setting(name: str) -> bool: + """Whether the setting the name is written to reads a held file. + + A placeholder read from the setting takes the held dump the + catalogues name after the model the launcher's name spells. + """ + entries = [ + entry + for key in readers + for entry in profiles[key].get("files") or [] + if settings[name] and set_by(entry, settings[name]) + ] + return any(holder._entry_held(entry) for entry in entries) or any( + "<" in str(entry.get("name", "")) + and any( + name_match_size_ok(entry, found.get("size")) + for found in catalogued_model_files(name, db) + ) + for entry in entries ) - } + + by_model = {name for name in names if by_setting(name)} if record.get("kind") == "names_searched" and any( (found := holder.held(name)) and (found[1] or folds) for name in names ): diff --git a/scripts/launchers.py b/scripts/launchers.py index 9dcd9bbca..8eef3fd28 100644 --- a/scripts/launchers.py +++ b/scripts/launchers.py @@ -144,6 +144,26 @@ def catalogued_model_files(name: str, db: dict) -> list[dict]: return [record for *_key, record in sorted(found, key=lambda f: (f[0], f[1]))] +def name_settings(record: dict, names: list[str]) -> dict[str, str]: + """The setting each name of a record is written to. + + A record naming several files of one config spells passes_to as + describe_keys does (shell_launchers.py), "atari800.cfg ROM_OS_A_PAL, + ROM_5200_A, ...", one key per name and in the order of the names. When + the counts differ, each name keeps the whole description. + """ + passes_to = str(record.get("passes_to") or "") + head, _, rest = passes_to.partition(" ") + keys = [ + key.strip() + for key in re.sub(r"\[[^\]]*\]", ",", rest).split(",") + if key.strip() + ] + if len(names) > 1 and len(keys) == len(names) and all(" " not in k for k in keys): + return {name: f"{head} {key}" for name, key in zip(names, keys)} + return {name: passes_to for name in names} + + def setting_readers(passes_to: str, profiles: dict, keys: Iterable[str]) -> list[str]: """The profiles with an entry read from the setting a launcher writes.""" if not passes_to: @@ -263,17 +283,22 @@ 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 ""), } + settings = name_settings(record, names) if kind == "names_searched": # Each directory in turn, every name in it. depth = len(directories) if anchored else 1 ordered = tuple(paths_of(name)[i] for i in range(depth) for name in names) - places.append(LauncherPlace(paths=ordered, **common)) + places.append(LauncherPlace( + paths=ordered, passes_to=str(record.get("passes_to") or ""), **common + )) continue # A fixed name is the one file passed, and a core option offers # each value as the file the user picks: one place per name. - places.extend(LauncherPlace(paths=paths_of(name), **common) for name in names) + places.extend( + LauncherPlace(paths=paths_of(name), passes_to=settings[name], **common) + for name in names + ) return places diff --git a/tests/test_launcher_layer.py b/tests/test_launcher_layer.py index f82a55997..e58f39b23 100644 --- a/tests/test_launcher_layer.py +++ b/tests/test_launcher_layer.py @@ -289,6 +289,18 @@ class LauncherLayerTest(unittest.TestCase): self.assertEqual(added["kick.A500.rom"]["md5"], _md5(CONTENT["core.bin"])) self.assertIsNone(suffixed_stem("scph1001.bin")) + def test_each_name_of_a_config_takes_its_own_key(self) -> None: + """AmberELEC's atari800.cfg names eight ROMs under eight keys, and + one record describes them in order: 5200a.rom is ROM_5200_A's file, + not every key's.""" + config = {"base_destination": "bios", "launchers": [{ + "emulator": "e", "build": "libretro", "kind": "fixed_name", + "names": ["a.rom", "b.rom"], "search_dir": "/storage/roms/bios", + "passes_to": "e.cfg ROM_A, ROM_B", "source_ref": "e.cfg:1", + }]} + places = {p.paths[0]: p.passes_to for p in launcher_places(config)} + self.assertEqual(places, {"a.rom": "e.cfg ROM_A", "b.rom": "e.cfg ROM_B"}) + def test_a_core_option_names_the_dump_of_a_model(self) -> None: """RetroDECK sets LRPS2's pcsx2_bios to scph10000.bin. LRPS2's entry is a placeholder for every BIOS its scan finds, and the catalogues