From 4b8d7baac5c52d1c77c386027cc9309f28e121e7 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:18:30 +0200 Subject: [PATCH] feat: transcribe bios search orders into priority --- scripts/slot.py | 17 +++++++++++------ tests/test_slot.py | 36 ++++++++++++++++++++++++++++++++++-- wiki/advanced-usage.md | 9 +++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/scripts/slot.py b/scripts/slot.py index f9476aa5..bae36afa 100644 --- a/scripts/slot.py +++ b/scripts/slot.py @@ -19,8 +19,10 @@ exactly the file the core may load. Where the order is not declared the group is reported undecidable and every candidate is kept: picking one would be the arbitrary selection this exists to remove. -The scale is only meaningful inside one system's candidate set. Profiles that -cover the same system must rank on the same scale. +The scale is only meaningful inside one emulator's candidate set. When two +profiles rank the same file differently -- pcsx1 walks scph1001 first while +DuckStation prefers scph5501 -- the pack serves both cores, so neither order +may win: the file is marked conflicting and its group stays undecidable. """ from __future__ import annotations @@ -43,12 +45,16 @@ def build_slot_index(profiles: dict) -> dict[str, dict]: path = f.get("path") or "" for key in {path, name} - {""}: entry = index.setdefault( - key, {"rank": None, "regions": set(), "emulators": []} + key, + {"rank": None, "conflict": False, "regions": set(), + "emulators": []}, ) entry["regions"] |= {str(r) for r in (f.get("region") or [])} rank = f.get("priority") if rank is not None: current = entry["rank"] + if current is not None and current != rank: + entry["conflict"] = True entry["rank"] = ( rank if current is None else min(current, rank) ) @@ -96,9 +102,8 @@ def resolve_slot_drops( keep.add(destination) continue tier = tuple(sorted(entry["regions"])) - tiers.setdefault((group_id, tier), []).append( - (destination, entry["rank"]) - ) + rank = None if entry.get("conflict") else entry["rank"] + tiers.setdefault((group_id, tier), []).append((destination, rank)) for (group_id, tier), candidates in tiers.items(): if len(candidates) < 2: diff --git a/tests/test_slot.py b/tests/test_slot.py index e9a3494b..ca4a6d78 100644 --- a/tests/test_slot.py +++ b/tests/test_slot.py @@ -173,8 +173,12 @@ class TestRepoProfiles(unittest.TestCase): def test_lowest_priority_picks_the_reference_playstation_bios(self): """DuckStation ranks scph5501 at 5 and de-prioritizes by raising the - number, so the US slot must resolve to it and not to a PS2 image.""" - index = slot.build_slot_index(self.profiles) + number, so on its own scale the US slot resolves to it. The merged + index is a different story: pcsx1 disagrees, and the conflict test + below asserts that neither order wins there.""" + index = slot.build_slot_index( + {"duckstation": self.profiles["duckstation"]} + ) ds = self.profiles.get("duckstation") if not ds: self.skipTest("duckstation profile not present") @@ -192,6 +196,34 @@ class TestRepoProfiles(unittest.TestCase): kept = {n for n, _ in members} - drops self.assertEqual(kept, {"scph5501.bin"}) + def test_conflicting_cross_profile_orders_decide_nothing(self): + """pcsx1 walks scph1001 first; DuckStation prefers scph5501. The pack + serves both cores, so neither order may drop the other's first pick.""" + index = slot.build_slot_index(self.profiles) + for name in ("scph1001.bin", "scph5501.bin"): + entry = index.get(name) + if entry is None: + self.skipTest(f"{name} not in any profile") + if not index["scph1001.bin"].get("conflict"): + self.skipTest("no conflicting ranks declared yet") + drops, _u = slot.resolve_slot_drops( + {"psx": _pairs("scph1001.bin", "scph5501.bin", "scph7001.bin")}, + index, + ) + self.assertEqual(drops & {"scph1001.bin", "scph5501.bin"}, set()) + + def test_numero_search_order_decides_its_slot(self): + """numero is alone on ti-83 and ranks all three ROMs, so this is the + one slot the repo can currently decide end to end.""" + index = slot.build_slot_index(self.profiles) + if "ti83se.rom" not in index: + self.skipTest("numero profile not present") + drops, undecidable = slot.resolve_slot_drops( + {"ti-83": _pairs("ti83se.rom", "ti83plus.rom", "ti83.rom")}, index + ) + self.assertEqual(drops, {"ti83plus.rom", "ti83.rom"}) + self.assertEqual(undecidable, []) + def test_ties_leave_the_group_untouched(self): """Japanese PlayStation ties at 5, so nothing may be dropped there.""" index = slot.build_slot_index(self.profiles) diff --git a/wiki/advanced-usage.md b/wiki/advanced-usage.md index 357a34f4..70f5cbb6 100644 --- a/wiki/advanced-usage.md +++ b/wiki/advanced-usage.md @@ -136,6 +136,15 @@ Where no such list is declared, the group is left untouched and counted: 63 slot(s) with no declared order: every candidate kept ``` +Two rules keep a declared order from overreaching: + +- **Cores that disagree cancel out.** pcsx1 walks `scph1001` first while + DuckStation prefers `scph5501`; the pack serves both cores, so neither order + wins and both files stay. +- **A slot mixing unrelated roles stays open.** px68k's IPL ROMs are ranked, + but its character ROM shares the same system and carries no rank, so the + whole group is kept. Ordering an IPL against a font would be meaningless. + That number is the remaining work, not a failure. Picking a file without a declared order would be the arbitrary selection this exists to remove, and it could drop the one the core would have loaded.