diff --git a/scripts/manifestselect.py b/scripts/manifestselect.py index 5b4a88ad..fad640c7 100644 --- a/scripts/manifestselect.py +++ b/scripts/manifestselect.py @@ -19,7 +19,7 @@ from dataclasses import dataclass import region as region_mod from common import _norm_system_id, sanitize_pack_path -from packextras import _extra_system_ids, _kept, extra_region_groups +from packextras import _extra_system_ids, _kept, extra_region_groups, system_spellings def _dest(entry: dict) -> str: @@ -58,11 +58,14 @@ class SelectionIndex: owned_by: dict[str, set[str]] = {} extra_groups: dict[str, set[str]] = {} + spellings = system_spellings(pack_systems) for extra in extras: dest = _dest(extra) if not dest: continue - extra_groups.setdefault(dest, set()).update(extra_region_groups(extra)) + extra_groups.setdefault(dest, set()).update( + extra_region_groups(extra, spellings) + ) # A profile system the platform does not have is no system the # pack's --system accepts: it would offer what the pack refuses. for sys_id in _extra_system_ids(extra): diff --git a/scripts/packextras.py b/scripts/packextras.py index 3a36f010..cdb4df03 100644 --- a/scripts/packextras.py +++ b/scripts/packextras.py @@ -6,7 +6,9 @@ this module builds so the report and the pack withdraw the same files.""" from __future__ import annotations -from common import PROFILE_IDENTITY_FIELDS +from collections.abc import Iterable + +from common import PROFILE_IDENTITY_FIELDS, _norm_system_id from common import archive_members from common import declared_root, expand_platform_declared_names, platform_declarations from common import load_emulator_profiles @@ -948,6 +950,7 @@ def platform_region_groups( absent=absent, ) held = {id(extra) for extra in extras} + spellings = system_spellings(systems) for extra in _kept(extras + absent, required_only): dest = sanitize_pack_path(extra.get("destination", extra.get("name", ""))) if not dest: @@ -965,7 +968,7 @@ def platform_region_groups( # looked up by name its scph50006 took DuckStation's [japan], which # describes DuckStation's own PS1-on-PS2 choice. competes = not extra.get("agnostic_scan") or extra.get("region") - for group_id in extra_region_groups(extra) if competes else (): + for group_id in extra_region_groups(extra, spellings) if competes else (): groups.setdefault(group_id, []).append((dest, name)) if extras_out is not None and id(extra) in held: extras_out.append(extra) @@ -974,8 +977,31 @@ def platform_region_groups( return groups, extra_dests -def extra_region_groups(extra: dict) -> list[str]: - """The region groups a core extra competes in.""" +def system_spellings(systems: Iterable[str]) -> dict[str, str]: + """A platform's own spelling of each system, keyed the way ids compare. + + RetroDECK files the Master System as sega-master-system, the profiles as + sega-mastersystem. Grouped under both, the declared bios/bios_J.sms and + the core's bios_J.sms were reduced apart and a --region us pack kept the + Japanese BIOS. + """ + spellings: dict[str, str] = {} + for sys_id in sorted(systems): + spellings.setdefault(_norm_system_id(sys_id), sys_id) + return spellings + + +def extra_region_groups( + extra: dict, spellings: dict[str, str] | None = None +) -> list[str]: + """The region groups a core extra competes in. + + A group named after a system takes the platform's spelling of it, so the + extra competes beside the files the platform declares for that system. + """ + def system_group(sys_id: str) -> str: + return (spellings or {}).get(_norm_system_id(sys_id), sys_id) + emulator = extra.get("source_emulator", "") variant = extra.get("variant_group") if variant: @@ -986,10 +1012,10 @@ def extra_region_groups(extra: dict) -> list[str]: return [f"{emulator}:variant:{variant}"] explicit = extra.get("source_system") if explicit: - return [str(explicit)] + return [system_group(str(explicit))] systems = [str(s) for s in extra.get("source_systems", []) if s] if len(systems) == 1: - return systems + return [system_group(systems[0])] # No system names the machine this file is chosen for. Filed under every # system of a 30-system profile, CLK's American MSX ROM made the # Enterprise group drop its German ROM. Alone, it drops nothing and is diff --git a/tests/test_e2e.py b/tests/test_e2e.py index e32edb9b..5d29b65e 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -6251,6 +6251,44 @@ struct BurnDriver BurnDrvneogeo = { ("declared.bin", "declared.bin")], ) + def test_a_system_spelled_two_ways_is_one_group(self): + """RetroDECK files the Master System as sega-master-system, the + profiles as sega-mastersystem. Two groups reduced apart, and a + --region us pack kept the declared Japanese BIOS.""" + import hashlib # noqa: PLC0415 + + from packextras import platform_region_groups # noqa: PLC0415 + + files, by_name = {}, {} + for name in ("bios_U.sms", "bios_J.sms"): + payload = name.encode() + path = os.path.join(self.bios_dir, name) + with open(path, "wb") as handle: + handle.write(payload) + sha1 = hashlib.sha1(payload).hexdigest() + files[sha1] = {"path": path, "name": name, "size": len(payload), + "md5": hashlib.md5(payload).hexdigest()} + by_name[name] = [sha1] + db = {"files": files, "indexes": {"by_name": by_name, "by_md5": {}, + "by_crc32": {}, "by_path_suffix": {}}} + profiles = {"smscore": { + "emulator": "SMS Core", "type": "libretro", + "systems": ["sega-mastersystem"], + "files": [{"name": "bios_U.sms", "region": ["north-america"]}, + {"name": "bios_J.sms", "region": ["japan"]}], + }} + config = {"platform": "P", "cores": ["smscore"], "base_destination": "system", + "systems": {"sega-master-system": {"files": [ + {"name": "bios_J.sms", "destination": "bios/bios_J.sms"}]}}} + groups, _ = platform_region_groups( + config, config["systems"], self.emulators_dir, db, "system", profiles, + ) + self.assertNotIn("sega-mastersystem", groups) + self.assertEqual( + sorted(dest for dest, _ in groups["sega-master-system"]), + ["bios/bios_J.sms", "bios_J.sms", "bios_U.sms"], + ) + def test_slot_narrowed_pack_gets_its_own_name(self): names = { os.path.basename(self._region_pack()), diff --git a/tests/test_region_groups.py b/tests/test_region_groups.py index 2e6c6f9f..691bdb2c 100644 --- a/tests/test_region_groups.py +++ b/tests/test_region_groups.py @@ -14,7 +14,7 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO_ROOT / "scripts")) -from packextras import extra_region_groups # noqa: E402 +from packextras import extra_region_groups, system_spellings # noqa: E402 class TheGroupFollowsWhatTheProfileSays(unittest.TestCase): @@ -63,5 +63,29 @@ class TheEnterpriseKeepsItsRom(unittest.TestCase): self.assertNotIn("bios/brd.rom", drops) +class TheGroupTakesThePlatformSpelling(unittest.TestCase): + def test_a_profile_spelling_maps_to_the_platform_one(self): + spellings = system_spellings(["sega-master-system", "sony-playstation"]) + self.assertEqual( + extra_region_groups({"source_system": "sega-mastersystem"}, spellings), + ["sega-master-system"], + ) + + def test_a_system_the_platform_lacks_keeps_its_name(self): + spellings = system_spellings(["sony-playstation"]) + self.assertEqual( + extra_region_groups({"source_systems": ["sega-saturn"]}, spellings), + ["sega-saturn"], + ) + + def test_file_and_variant_groups_are_untouched(self): + spellings = system_spellings(["sega-megacd"]) + self.assertEqual( + extra_region_groups({"source_emulator": "picodrive", "variant_group": "mcd", + "source_system": "sega-megacd"}, spellings), + ["picodrive:variant:mcd"], + ) + + if __name__ == "__main__": unittest.main()