diff --git a/scripts/truth.py b/scripts/truth.py index 3488136d9..302d6562e 100644 --- a/scripts/truth.py +++ b/scripts/truth.py @@ -113,7 +113,6 @@ _FILLED_FIELDS = ( "note", "aliases", "contents", - "region", ) _COPIED_FIELDS = ( "sha1", @@ -290,6 +289,27 @@ def _fold_into(existing: dict, file_entry: dict, emu_name: str) -> None: if file_entry.get(field) is not None and existing.get(field) is None: existing[field] = file_entry[field] _merge_priority(existing, file_entry.get("priority")) + _merge_region(existing, file_entry.get("region")) + + +def _merge_region(existing: dict, theirs: str | list | None) -> None: + """The regions every reader selects the file for, the pack's own rule. + + region.build_region_index takes the union of the tags and keeps a file + untagged as soon as one reader declares it without a region. The first + tag used to win: psxonpsp660.bin came out japan from pcsx_rearmed while + SwanStation tags it world and beetle_psx not at all. + """ + if existing.get("_region_void"): + return + if not theirs: + existing.pop("region", None) + existing["_region_void"] = True + return + ours = existing.get("region") or [] + ours = ours if isinstance(ours, list) else [ours] + added = [r for r in (theirs if isinstance(theirs, list) else [theirs]) if r not in ours] + existing["region"] = [*ours, *added] def _merge_hashes( @@ -373,6 +393,8 @@ def _new_truth_entry(file_entry: dict, emu_name: str, db: dict | None) -> dict: crc = entry.get("crc32") if isinstance(crc, str): entry["crc32"] = crc.removeprefix("0x").lower() + if not file_entry.get("region"): + entry["_region_void"] = True entry["_cores"] = {emu_name} entry["_required_by"] = {emu_name} if file_entry.get("required") else set() entry["_named_by"] = {emu_name} if file_entry.get("_opens_by_name", True) else set() @@ -814,6 +836,7 @@ def generate_platform_truth( for sys_id, sys_data in systems.items(): for fe in sys_data.get("files", []): fe.pop("_hash_by", None) + fe.pop("_region_void", None) fe["_cores"] = sorted(fe.get("_cores", set())) fe["_required_by"] = sorted(fe.get("_required_by", set())) fe["_named_by"] = sorted(fe.get("_named_by", set())) diff --git a/tests/test_truth_layout.py b/tests/test_truth_layout.py index 73f514d28..d644350aa 100644 --- a/tests/test_truth_layout.py +++ b/tests/test_truth_layout.py @@ -454,5 +454,38 @@ class TheModelIsAFunctionOfItsInputs(unittest.TestCase): ) +class RegionFollowsEveryReader(unittest.TestCase): + """The first core to tag a file set its region in the truth: the ST-V + BIOS kept beetle_saturn's Americas and lost mednafen_stv's Korea, and + psxonpsp660.bin read japan although beetle_psx declares it untagged. + The pack's region index takes the union and voids it on an untagged + reader, and the truth now says the same.""" + + @staticmethod + def _profile(name: str, region: list[str] | None) -> dict: + entry = {"name": "bios.bin", "system": "sega-stv", "md5": "a" * 32} + if region is not None: + entry["region"] = region + return {"emulator": name, "type": "libretro", "systems": ["sega-stv"], + "files": [entry]} + + def _region(self, profiles: dict) -> object: + config = {"cores": sorted(profiles), "systems": {"sega-stv": {"files": []}}} + truth = generate_platform_truth("p", config, {}, profiles) + (entry,) = truth["systems"]["sega-stv"]["files"] + self.assertNotIn("_region_void", entry) + return entry.get("region") + + def test_tags_of_every_reader_are_kept(self): + region = self._region({"a": self._profile("a", ["north-america"]), + "b": self._profile("b", ["south-korea", "north-america"])}) + self.assertEqual(region, ["north-america", "south-korea"]) + + def test_an_untagged_reader_leaves_no_region(self): + for order in (("a", "b"), ("b", "a")): + profiles = {"a": self._profile("a", ["japan"]), "b": self._profile("b", None)} + self.assertIsNone(self._region({k: profiles[k] for k in order})) + + if __name__ == "__main__": unittest.main()