diff --git a/scripts/exporter/candidates.py b/scripts/exporter/candidates.py index ce89495a..8b8c2b66 100644 --- a/scripts/exporter/candidates.py +++ b/scripts/exporter/candidates.py @@ -242,6 +242,10 @@ class EntryCategories: and every entry of a platform with no core to examine (BizHawk, MiSTer), stays firmware. + A system that carries its own `cores:` list is settled by those cores: + the cores examined are the ones it runs, and only its own unprofiled + cores leave it open. Otherwise the platform's whole list applies. + `explain` returns the category with its evidence: the profile entries that read the file, or the cores of its system that were examined. """ @@ -275,6 +279,20 @@ class EntryCategories: for key in sorted(examined): for sid in profiles[key].get("systems") or []: self._by_system.setdefault(_norm_system_id(str(sid)), []).append(key) + # Where the platform says which cores run a system, those settle its + # entries: an unprofiled core of another system has no say. + self._system_runs: dict[str, tuple[list[str], list[str]]] = {} + for sys_id, system in (config.get("systems") or {}).items(): + names = system.get("cores") if isinstance(system, dict) else None + if names: + own = {**config, "cores": list(names)} + for key in ("standalone_cores", "dual_mode_cores"): + if key in config: + own[key] = [n for n in config.get(key) or [] if n in names] + self._system_runs[sys_id] = ( + sorted(resolve_platform_cores(own, profiles)), + sorted(unresolved_platform_cores(own, profiles)), + ) def _matching(self, key: str, kind: str, entry: dict) -> list[dict]: """The profile entries of `key` that name the platform entry.""" @@ -304,7 +322,17 @@ class EntryCategories: }) if self._launchers.proof(entry, destination) is not None: readers[BIOS].append({"emulator": "launcher", "proof": "launcher", "entries": []}) - system_cores = self._by_system.get(_norm_system_id(system), []) + if system in self._system_runs: + system_cores, unresolved = self._system_runs[system] + else: + system_cores = self._by_system.get(_norm_system_id(system), []) + unresolved = self._unresolved + # A reader that runs the entry's system comes first and says so: a + # core of another system may only share the file's name. + for found in readers.values(): + for reader in found: + reader["runs_system"] = reader["emulator"] in system_cores + found.sort(key=lambda r: not r["runs_system"]) if readers[BIOS]: return {"category": BIOS, "read_by": readers[BIOS]} if readers[GAME_DATA]: @@ -312,10 +340,10 @@ class EntryCategories: examined = {"cores_examined": system_cores} if self._guessed: return {"category": BIOS, "unsettled": "the platform declares no core list", **examined} - if self._unresolved: + if unresolved: return {"category": BIOS, "unsettled": "cores the platform names have no profile: " - + ", ".join(self._unresolved), **examined} + + ", ".join(unresolved), **examined} if not system_cores: return {"category": BIOS, "unsettled": "no examined core emulates this system", **examined} diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 740ccd40..4c42541f 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2064,7 +2064,7 @@ def _platform_data_links(name: str) -> str: def _category_evidence_md(evidence: dict) -> str: """What establishes a missing entry's category, as the page shows it.""" if evidence.get("read_by"): - readers = [] + groups: dict[bool, list[str]] = {True: [], False: []} for reader in evidence["read_by"]: refs = [ f"`{e['name']}` at `{e['source_ref']}`" if e.get("source_ref") else f"`{e['name']}`" @@ -2072,8 +2072,14 @@ def _category_evidence_md(evidence: dict) -> str: ] name = (_emulator_link(reader["emulator"], "../") if reader["emulator"] != "launcher" else "the platform's launcher") - readers.append(f"{name} ({', '.join(refs)})" if refs else f"{name} (by {reader['proof']})") - return "Read by " + ", ".join(readers) + groups[bool(reader.get("runs_system"))].append( + f"{name} ({', '.join(refs)})" if refs else f"{name} (by {reader['proof']})") + if not groups[True]: + return "Read by " + ", ".join(groups[False]) + text = "Read by " + ", ".join(groups[True]) + if groups[False]: + text += ", also named by " + ", ".join(groups[False]) + return text examined = evidence.get("cores_examined") or [] cores = ("cores examined: " + ", ".join(_emulator_link(c, "../") for c in examined) if examined else "no core examined") diff --git a/scripts/verify.py b/scripts/verify.py index b080f7f7..2b0430d0 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -1634,17 +1634,22 @@ _NOT_FIRMWARE = { def category_evidence_text(evidence: dict) -> str: """One line saying what establishes a missing entry's category.""" if evidence.get("read_by"): - readers = [] + groups: dict[bool, list[str]] = {True: [], False: []} for reader in evidence["read_by"]: refs = [ f"{e['name']} at {e['source_ref']}" if e.get("source_ref") else e["name"] for e in reader.get("entries", []) ] - readers.append( + groups[bool(reader.get("runs_system"))].append( f"{reader['emulator']} ({', '.join(refs)})" if refs else f"{reader['emulator']} (by {reader['proof']})" ) - return "read by " + ", ".join(readers) + if not groups[True]: + return "read by " + ", ".join(groups[False]) + text = "read by " + ", ".join(groups[True]) + if groups[False]: + text += ", also named by " + ", ".join(groups[False]) + return text examined = evidence.get("cores_examined") or [] cores = f"cores examined: {', '.join(examined)}" if examined else "no core examined" if evidence.get("unsettled"): diff --git a/tests/test_entry_categories.py b/tests/test_entry_categories.py index c14b5735..d6cbb324 100644 --- a/tests/test_entry_categories.py +++ b/tests/test_entry_categories.py @@ -82,7 +82,7 @@ class Evidence(unittest.TestCase): found = self.cats.explain({"name": "diabdat.mpq"}, "ports/diablo/diabdat.mpq", "diablo") self.assertEqual(found["category"], GAME_DATA) self.assertEqual(found["read_by"], [{ - "emulator": "devilutionx", "proof": "name", + "emulator": "devilutionx", "proof": "name", "runs_system": True, "entries": [{"name": "diabdat.mpq", "source_ref": "Source/init.cpp:120"}]}]) self.assertEqual(verify.category_evidence_text(found), "read by devilutionx (diabdat.mpq at Source/init.cpp:120)") @@ -91,6 +91,25 @@ class Evidence(unittest.TestCase): found = self.cats.explain({"name": "hardwareconfig.xml"}, "bios/Machines/MSX/hardwareconfig.xml", "microsoft-msx") self.assertEqual(found, {"category": UNREAD, "cores_examined": ["bluemsx"]}) + + def test_the_system_s_own_core_reads_first(self): + profiles = {**PROFILES, "other": { + "emulator": "Other", "type": "game", "cores": ["other"], "systems": ["elsewhere"], + "files": [{"name": "diabdat.mpq", "category": "game_data"}]}} + cats = EntryCategories(_config(["bluemsx", "devilutionx", "other"], []), profiles) + found = cats.explain({"name": "diabdat.mpq"}, "ports/diablo/diabdat.mpq", "diablo") + self.assertEqual([r["emulator"] for r in found["read_by"]], ["devilutionx", "other"]) + self.assertEqual(verify.category_evidence_text(found), + "read by devilutionx (diabdat.mpq at Source/init.cpp:120), " + "also named by other (diabdat.mpq)") + + def test_a_system_s_own_cores_settle_it(self): + config = _config(["bluemsx", "devilutionx", "mystery"], [], system="microsoft-msx") + config["systems"]["microsoft-msx"]["cores"] = ["bluemsx"] + found = EntryCategories(config, PROFILES).explain( + {"name": "hardwareconfig.xml"}, "bios/Machines/MSX/hardwareconfig.xml", + "microsoft-msx") + self.assertEqual(found, {"category": UNREAD, "cores_examined": ["bluemsx"]}) self.assertEqual(verify.category_evidence_text(found), "no reader found (cores examined: bluemsx)")