diff --git a/README.md b/README.md
index 0df5cf0f0..c5db8e4a7 100644
--- a/README.md
+++ b/README.md
@@ -99,24 +99,24 @@ Full list with per-file details: **[https://abdess.github.io/retrobios/](https:/
| Platform | On its BIOS list | Files its emulators load | Checked by |
|----------|-----------------:|-------------------------:|------------|
-| AmberELEC | 414/434 | 4,751/4,935 | MD5 hash |
-| Batocera | 353/353 | 14,171/15,291 | MD5 hash |
-| BizHawk | 118/118 | - | SHA1 hash |
-| EmuDeck | 168/168 | 8,422/8,525 | MD5 hash |
-| EmuELEC | 544/572 | 4,786/10,215 | MD5 hash |
-| KNULLI | 290/290 | 9,126/15,349 | MD5 hash |
-| Lakka | 532/532 | 7,910/7,996 | file presence |
-| MiSTer FPGA | 81/81 | - | MD5 hash |
-| MinUI | 0/0 | 78/79 | file presence |
-| MyMinUI | 0/0 | 356/367 | file presence |
-| NextUI | 0/0 | 343/350 | file presence |
-| ROCKNIX | 38/38 | 7,421/9,134 | MD5 hash |
-| Recalbox | 351/351 | 3,125/3,344 | MD5 hash |
-| RetroArch | 526/526 | 7,919/7,987 | file presence |
-| RetroBat | 348/348 | 5,133/9,658 | MD5 hash |
-| RetroDECK | 2,007/2,008 | 6,703/6,991 | MD5 hash |
-| RetroPie * | 532/532 | 8,120/12,775 | file presence |
-| RomM | 383/383 | 83/873 | MD5 hash |
+| AmberELEC | 414/434 | 4,751/4,935 | MD5 |
+| Batocera | 353/353 | 14,171/15,291 | MD5 |
+| BizHawk | 118/118 | - | SHA-1 |
+| EmuDeck | 168/168 | 8,422/8,525 | MD5 |
+| EmuELEC | 544/572 | 4,786/10,215 | MD5 |
+| KNULLI | 290/290 | 9,126/15,349 | MD5 |
+| Lakka | 532/532 | 7,910/7,996 | name only |
+| MiSTer FPGA | 81/81 | - | MD5 |
+| MinUI | 0/0 | 78/79 | name only |
+| MyMinUI | 0/0 | 356/367 | name only |
+| NextUI | 0/0 | 343/350 | name only |
+| ROCKNIX | 38/38 | 7,421/9,134 | MD5 |
+| Recalbox | 351/351 | 3,125/3,344 | MD5 |
+| RetroArch | 526/526 | 7,919/7,987 | name only |
+| RetroBat | 348/348 | 5,133/9,658 | MD5 |
+| RetroDECK | 2,007/2,008 | 6,703/6,991 | MD5 |
+| RetroPie * | 532/532 | 8,120/12,775 | name only |
+| RomM | 383/383 | 83/873 | size, then any hash |
Each fraction is what the pack has over what is needed, counting required and optional files alike since both ship. The first column is the BIOS list the platform publishes. The second counts files its emulators load that this list never mentions, found by reading their source code, and it is routinely several times larger. A short fraction means files are still missing, and they are named in the [gap analysis](https://abdess.github.io/retrobios/gaps/).
That second number is a floor, not a ceiling: an emulator that accepts any file handed to it names none in its code, so nothing there can be counted.
@@ -203,4 +203,4 @@ The scripts and tooling are released under the [MIT License](LICENSE).
The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed.
The reasoning, and where it is weakest, is in [the project's legal position](https://abdess.github.io/retrobios/wiki/legal/).
-*Auto-generated on 2026-11-03T17:50:12Z*
+*Auto-generated on 2026-11-05T10:43:18Z*
diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py
index aba4e9c9f..1d32b975e 100644
--- a/scripts/generate_readme.py
+++ b/scripts/generate_readme.py
@@ -34,6 +34,7 @@ from common import (
unique_emulator_profiles,
write_if_changed,
)
+from nativemode import check_label
import release_record
from siteguides import install_command
from verify import verify_platform
@@ -737,15 +738,9 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
]
)
- mode_labels = {
- "md5": "MD5 hash",
- "sha1": "SHA1 hash",
- "existence": "file presence",
- }
-
for name, cov in sorted(coverages.items(), key=lambda x: x[1]["platform"]):
display = f"{cov['platform']} *" if name in archived else cov["platform"]
- checked = mode_labels.get(cov["mode"], cov["mode"])
+ checked = check_label(cov["mode"], cov["config"].get("hash_rule"))
core_total = (
cov["core_present"] + cov["core_missing"] + cov["core_unsourceable"]
)
diff --git a/scripts/generate_site.py b/scripts/generate_site.py
index 2a763163b..461cadd0a 100644
--- a/scripts/generate_site.py
+++ b/scripts/generate_site.py
@@ -64,7 +64,7 @@ from common import (
from artifacts import ArtifactLockBusy, artifact_lock
from artifacts import json_text as _json_text
from cross_reference import FileConsumers, held_copy
-from nativemode import digest_algorithm, reads_file_contents
+from nativemode import check_label, digest_algorithm, reads_file_contents
@@ -3194,8 +3194,6 @@ def generate_platform_index(coverages: dict, registry: dict | None = None) -> st
SETUP_TABLE_PAGE = "wiki/verification-modes.md"
SETUP_TABLE_ANCHOR = "every-setup-at-a-glance"
SETUP_TABLE_LINK = f"{SETUP_TABLE_PAGE}#{SETUP_TABLE_ANCHOR}"
-# How a setup checks a file, in the words of a reader.
-_HOW_IT_CHECKS = {"existence": "name only", "md5": "MD5", "sha1": "SHA-1"}
def setup_table(coverages: dict, registry: dict | None = None) -> list[str]:
@@ -3220,7 +3218,7 @@ def setup_table(coverages: dict, registry: dict | None = None) -> list[str]:
lines.append(
f"| [{cov['platform']}](../platforms/{name}.md)"
f"{' (archived)' if status == 'archived' else ''} | {listed} | {zipped} | "
- f"{_HOW_IT_CHECKS.get(cov['mode'], cov['mode'])} |"
+ f"{check_label(cov['mode'], cov['config'].get('hash_rule'))} |"
)
lines += [
"",
@@ -4292,9 +4290,8 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
return lines
-# How a platform checks a file, in words.
-_CHECK_WORDS = {"existence": "name only", "md5": "MD5", "sha1": "SHA-1", "sha256": "SHA-256",
- "crc32": "CRC32"}
+# A hash a platform's list writes, in words.
+_HASH_WORDS = {"md5": "MD5", "sha1": "SHA-1", "sha256": "SHA-256", "crc32": "CRC32"}
def generate_platform_page(
@@ -4374,7 +4371,7 @@ def generate_platform_page(
*_stat(f"{cov['present']}/{cov['total']}", f"Coverage ({pct})"),
*_stat(str(cov["verified"]), "Verified"),
*_stat(str(cov["missing"]), "Missing"),
- *_stat(f'{_CHECK_WORDS.get(mode, mode)}',
+ *_stat(f'{check_label(mode, config.get("hash_rule"))}',
"Checked by"),
"",
"",
@@ -4384,7 +4381,7 @@ def generate_platform_page(
# The hash the list writes, when it is not the one the platform checks:
# RetroArch checks presence and its System.dat writes sha1.
if hash_type and hash_type != mode:
- lines.append(f"| Hashes in the list | {_CHECK_WORDS.get(hash_type, hash_type)} |")
+ lines.append(f"| Hashes in the list | {_HASH_WORDS.get(hash_type, hash_type)} |")
if version:
lines.append(f"| Version | {version} |")
# base_destination is only the root the platform's own list writes its
@@ -10409,12 +10406,6 @@ def generate_gap_analysis(
"|----------|-----------------:|-------------------------:|------------|",
])
- mode_labels = {
- "md5": "MD5 hash",
- "sha1": "SHA1 hash",
- "existence": "file presence",
- }
-
for pname, cov in sorted(coverages.items(), key=lambda x: x[1]["platform"]):
display = cov["platform"]
core_total = (
@@ -10424,7 +10415,7 @@ def generate_gap_analysis(
f"| [{display}](platforms/{pname}.md) "
f"| {_ratio_cell(cov['present'], cov['total'], _missing_required_firmware(cov))} "
f"| {_ratio_cell(cov['core_present'], core_total)} "
- f"| {mode_labels.get(cov['mode'], cov['mode'])} |"
+ f"| {check_label(cov['mode'], cov['config'].get('hash_rule'))} |"
)
lines.extend([
"",
diff --git a/scripts/nativemode.py b/scripts/nativemode.py
index eae8015df..24cd56549 100644
--- a/scripts/nativemode.py
+++ b/scripts/nativemode.py
@@ -40,6 +40,18 @@ SIZE_AND_ANY_HASH = "size_and_any_hash"
HASH_RULES = (SIZE_AND_ANY_HASH,)
+#: How a platform checks a file, in a reader's words. Every table and badge
+#: that says it reads this, so a page cannot call RomM's rule an MD5 check.
+_CHECK_LABELS = {"existence": "name only", "md5": "MD5", "sha1": "SHA-1"}
+
+
+def check_label(mode: str | None, hash_rule: str | None = None) -> str:
+ """The words for what a platform's own check compares."""
+ if hash_rule == SIZE_AND_ANY_HASH:
+ return "size, then any hash"
+ return _CHECK_LABELS[normalize(mode)]
+
+
def normalize(mode: str | None) -> str:
"""Return a declared mode, falling back to the schema default."""
return mode if mode in MODES else DEFAULT_MODE
diff --git a/tests/test_site_r2_reference.py b/tests/test_site_r2_reference.py
index cd6f9aaee..4a12df33e 100644
--- a/tests/test_site_r2_reference.py
+++ b/tests/test_site_r2_reference.py
@@ -201,6 +201,22 @@ class PlatformReferenceInWords(unittest.TestCase):
self.assertIn("[r repository at commit aaaaaaaaaaaa](https://github.com/o/r/tree/"
+ "a" * 40 + ")", page)
+ def test_every_check_label_reads_the_hash_rule(self):
+ """RomM's badge and table rows said MD5 beside a sentence saying it
+ accepts the size and any one hash."""
+ details = [{"name": "f.bin", "status": "ok", "system": "sony-playstation"}]
+ cov = {"platform": "RomM", "details": details, "total": 1, "verified": 1,
+ "present": 1, "missing": 0, "untested": 0, "mode": "md5",
+ "core_present": 0, "core_missing": 0, "core_unsourceable": 0,
+ "config": {"hash_type": "md5", "hash_rule": "size_and_any_hash",
+ "systems": {"sony-playstation": {"files": [{"name": "f.bin"}]}},
+ "source": "https://github.com/o/r/tree/" + "a" * 40}}
+ page = generate_site.generate_platform_page("romm", cov, {})
+ self.assertIn(">size, then any hash", page)
+ self.assertNotIn(">MD5", page)
+ table = "\n".join(generate_site.setup_table({"romm": cov}, {}))
+ self.assertIn("| size, then any hash |", table)
+
def test_systems_by_name_with_the_id_in_small_text(self):
systems = {"sony-playstation": {"files": [{"name": "f.bin"}]}}
page = self._page("md5", "md5", systems)