33 Commits

Author SHA1 Message Date
github-actions[bot] 61ca8efc57 regenerate database and docs 2026-03-18 11:54:27 +00:00
Abdessamad Derraz 7653d5d108 feat: add 19 BIOS files, fix cross_reference resolution
New files: OpenTyrian data (11), Cave Story (2), SeaBIOS,
VGA BIOS, OpenSBI, Cromwell, xbox_hdd, Sega CD Model 2 (3),
NGP Color BIOS, Pentagon 128p-1.rom, X1 font, BK TERAK.
cross_reference.py: basename + case-insensitive lookup.
2026-03-18 12:50:55 +01:00
Abdessamad Derraz 76064605c0 fix: move zip_contents resolution after name-based lookup 2026-03-18 12:12:42 +01:00
Abdessamad Derraz 08f68e792d refactor: centralize hash logic, fix circular imports and perf bottlenecks 2026-03-18 11:51:12 +01:00
Abdessamad Derraz becd0efb33 fix: relative links in readme, commit pending changes 2026-03-18 11:28:58 +01:00
Abdessamad Derraz 09ebaa9316 fix: retropie logo from github avatar, remove *.png gitignore 2026-03-18 11:26:46 +01:00
Abdessamad Derraz 81278bd2e4 fix: system icons (systematic theme), retropie logo 2026-03-18 11:25:14 +01:00
Abdessamad Derraz a52ab19cf8 fix: full hashes, list format for system files 2026-03-18 11:15:11 +01:00
Abdessamad Derraz 300e5d7439 fix: redesign home page UX, fix broken retropie logo 2026-03-18 11:09:36 +01:00
Abdessamad Derraz 54c0f1d27e refactor: review fixes, DRY coverage, filter test nav
- Extract compute_coverage to common.py (was duplicated)
- Filter test cores from nav and emulator index
- Use absolute URL for README download links
- Consistent page titles with site name suffix
- Safer mkdocs.yml nav rewrite with regex
- Build all_platform_names once in gap analysis
2026-03-18 11:05:13 +01:00
Abdessamad Derraz e218763500 feat: add emulator logos to profiles and site 2026-03-18 10:57:00 +01:00
Abdessamad Derraz 6885681c65 feat: add platform logos to registry and site 2026-03-18 10:55:47 +01:00
Abdessamad Derraz 21a50c992f feat: slim readme + ci site deployment
README: 11141 -> 43 lines. Details on the MkDocs site.
generate_readme.py: 444 -> 164 lines. Slim coverage table only.
build.yml: adds mkdocs-material install, generate_site.py, gh-deploy.
Adds pages: write permission for GitHub Pages deployment.
2026-03-18 10:44:13 +01:00
Abdessamad Derraz 32e4f6e580 fix: review fixes for generate_site.py 2026-03-18 10:39:23 +01:00
Abdessamad Derraz 0b1ed3cb1a feat: add gap analysis page + platform tracking 2026-03-18 10:31:02 +01:00
Abdessamad Derraz 883e153a62 fix: clean platform/emulator page layout 2026-03-18 10:27:08 +01:00
Abdessamad Derraz b15b062782 feat: add mkdocs site generator, 332 pages
generate_site.py reads database.json + platforms/*.yml + emulators/*.yml
and produces a complete MkDocs Material documentation site:
- Home: stats, downloads, coverage dashboard
- 7 platform pages with per-file verification status
- 60 system pages grouped by manufacturer with cross-references
- 260 emulator pages with source code analysis
- Contributing guide

mkdocs.yml with Material theme, system fonts, auto dark mode.
Generated docs/ in .gitignore (built in CI only).
2026-03-18 10:22:00 +01:00
Abdessamad Derraz dd9e59c8e3 chore: remove nested bios/Philips/CD-i/bios/ duplicate + empty TOS ghosts
Nested bios/ directory inside CD-i was an agent artifact.
tos100uk.img and tos206us.img were 0-byte ghost files from failed git show.
2026-03-18 08:25:56 +01:00
Abdessamad Derraz 3de4bf8190 refactor: extract _fetch_raw to BaseScraper (DRY)
Identical _fetch_raw() implementation (URL fetch + cache + error handling)
was duplicated in 4 scrapers. Moved to BaseScraper.__init__ with url param.

Each scraper now passes url to super().__init__() and inherits _fetch_raw().
Eliminates ~48 lines of duplicated code.

DRY audit now clean: resolve logic in common.py, scraper CLI in base_scraper,
_fetch_raw in BaseScraper. Remaining duplications are justified (different
list_platforms semantics, context-specific hash computation).
2026-03-18 08:22:21 +01:00
Abdessamad Derraz 2466fc4a97 refactor: extract scraper_cli() to base_scraper.py (DRY)
Shared CLI boilerplate for all scrapers: argparse, dry-run, json, yaml output.
4 scrapers (libretro, batocera, retrobat, emudeck) reduced from ~58 lines
main() each to 3 lines calling scraper_cli().

~220 lines of duplicated boilerplate eliminated.
recalbox + coreinfo keep custom main() (extra flags: --full, --compare-db).
2026-03-18 08:17:14 +01:00
Abdessamad Derraz 00700609d8 refactor: extract resolve_local_file to common.py (DRY)
Single source of truth for file resolution logic:
- common.py:resolve_local_file() = 80 lines (core resolution)
- verify.py:resolve_to_local_path() = 3 lines (thin wrapper)
- generate_pack.py:resolve_file() = 20 lines (adds storage tiers + release assets)

Before: 103 + 73 = 176 lines of duplicated logic with subtle divergences
After: 80 lines shared + 23 lines wrappers = 103 lines total (-41%)

Resolution chain: SHA1 -> MD5 multi-hash -> truncated MD5 ->
zipped_file index -> name existence -> name composite -> name fallback
-> (pack only) release assets
2026-03-18 08:11:10 +01:00
Abdessamad Derraz 0c367ca7c6 feat: restore deleted TOS UK images, regenerate database
tos102uk.img, tos104uk.img, tos106uk.img were accidentally deleted
by a background agent. Restored from git history.
2026-03-18 07:47:25 +01:00
Abdessamad Derraz 8c18638cd2 regenerate database after merge 2026-03-18 07:31:46 +01:00
Abdessamad Derraz 7b1c6a723e refactor: review fixes - resolve coherence + cleanup
1. fetch_large_file moved to last resort (avoids HTTP before name lookup)
2. fetch_large_file receives first MD5 only (not comma-separated string)
3. verify.py MD5 lookup now splits comma-separated + lowercases (matches generate_pack)
4. seen_destinations simplified to set (stored hash was dead data)
5. Variable suffix shadowing renamed to file_ext
2026-03-18 07:18:40 +01:00
Abdessamad Derraz 7ae995fb32 fix: resolve_file multi-MD5 + md5_composite for Recalbox packs
Three fixes in resolve_file():
- Split comma-separated MD5 lists (Recalbox uses multi-hash)
- Add md5_composite check in name fallback (matches verify.py logic)
- Use ".zip" in basename instead of endswith for variant files

Recalbox pack: 346/346 verified (was 332/346 with 14 wrong hash)
Batocera pack: 359/359 verified (was 304/359 with 55 inner missing)
All 5 platforms now produce 0 untested, 0 missing packs.
2026-03-18 07:18:40 +01:00
Abdessamad Derraz a1dc6fa4ef fix: resolve_file prefers primary over variants for name fallback
When resolving by name with no MD5 (existence check), prefer files
NOT in .variants/ directory. Fixes naomi2.zip resolving to the
Recalbox variant (15 files) instead of the primary (21 files).

Also applies to hash_mismatch fallback path.
2026-03-18 07:18:40 +01:00
github-actions[bot] b0dad7dcf3 regenerate database and docs 2026-03-18 05:29:59 +00:00
Abdessamad Derraz 046fb276b0 fix: case-insensitive MD5 lookup in resolve_file
Recalbox uses uppercase MD5 hashes (6E3735FF...) but database index
is lowercase. Added .lower() to MD5 lookups in resolve_file().

Fixes scph101.bin wrong variant in Recalbox pack (was picking
.variants/ copy instead of primary due to MD5 case mismatch).
2026-03-18 06:27:35 +01:00
Abdessamad Derraz 040ea9f217 fix: resolve_file skips MD5 lookup for zipped_file entries
Same guard as verify.py: when zipped_file is set, the md5 is for the
inner ROM, not the container ZIP. Direct MD5 lookup resolved to the
standalone ROM file instead of the ZIP parent.

Fixes: ep64.zip/ep128.zip (Enterprise) written as raw ROM data instead
of ZIP archives in Batocera pack. Also fixes any other zipped_file entry
where the inner ROM MD5 matched a standalone file in the database.

Also: update Dinothawr.zip SHA1 in retroarch.yml to match actual file.
2026-03-18 06:27:35 +01:00
Abdessamad Derraz 84ab0ea6d3 fix: revert verify dedup (breaks counts), optimize pack generation
verify.py: removed destination dedup - verify counts ALL platform
entries (398 for RetroArch). Pack deduplicates at generation (395).
The delta (3 files: c52/g7400/jopac.bin) is correct behavior.

generate_pack.py: skip build_zip_contents_index() when no zipped_file
entries exist. RetroArch pack: 20s -> 11s (no ZIP scan needed).
2026-03-18 06:27:35 +01:00
github-actions[bot] 06ea19ee20 regenerate database and docs 2026-03-18 04:48:32 +00:00
Abdessamad Derraz 97a25b17ff fix: CI restores large files before DB regen, fix coverage numbers
build.yml now downloads large-files release assets and copies them
to their bios/ paths (matched via .gitignore) before regenerating
the database. This fixes Batocera showing 675/680 instead of 679/680
on the remote (PS3UPDAT.PUP, dsi_nand.bin, PSVUPDAT.PUP were missing).

Local: Batocera 679/680, all others 100%.
2026-03-18 05:46:16 +01:00
Abdessamad Derraz 4faae161b4 feat: implement --include-extras with hybrid core detection
generate_pack.py now merges Tier 2 emulator files into platform packs:
- Auto-detects cores from platform YAML "core:" fields (31 for RetroArch)
- Also reads manual "emulators:" list from _registry.yml (for Batocera etc)
- Union of both sources = complete emulator coverage per platform
- Files already in platform pack are skipped (Tier 1 wins)

Results with --include-extras:
  RetroArch: 395 -> 654 files (+259 emulator extras)
  Batocera:  359 -> 632 files (+273 emulator extras)

Pack naming: BIOS_Pack.zip (normal) vs Complete_Pack.zip (with extras)
2026-03-18 05:39:13 +01:00
76 changed files with 2146 additions and 12049 deletions
+28 -2
View File
@@ -13,6 +13,7 @@ on:
permissions:
contents: write
pages: write
concurrency:
group: build
@@ -30,10 +31,35 @@ jobs:
- run: pip install pyyaml
- name: Regenerate database and docs
- name: Restore large files from release
run: |
python scripts/generate_db.py --bios-dir bios --output database.json
mkdir -p .cache/large
gh release download large-files -D .cache/large/ 2>/dev/null || true
# Copy large files to their bios/ paths so generate_db sees them
for f in .cache/large/*; do
[ -f "$f" ] || continue
name=$(basename "$f")
# Match against .gitignore entries to find target path
target=$(grep "$name" .gitignore | head -1)
if [ -n "$target" ] && [ ! -f "$target" ]; then
mkdir -p "$(dirname "$target")"
cp "$f" "$target"
echo "Restored: $target"
fi
done
env:
GH_TOKEN: ${{ github.token }}
- name: Regenerate database, readme, and site
run: |
pip install mkdocs-material
python scripts/generate_db.py --force --bios-dir bios --output database.json
python scripts/generate_readme.py --db database.json --platforms-dir platforms
python scripts/generate_site.py
- name: Deploy site to GitHub Pages
if: github.ref == 'refs/heads/main'
run: mkdocs gh-deploy --force --clean
- name: Commit if changed
id: commit
+9
View File
@@ -6,10 +6,19 @@ __pycache__/
*.pyc
.cache/
dist/
site/
*.tmp
*.log
node_modules/
# Generated site pages (built in CI)
docs/index.md
docs/platforms/
docs/systems/
docs/emulators/
docs/contributing.md
docs/gaps.md
# Large files stored as GitHub Release assets (> 50MB)
bios/Arcade/Arcade/Firmware.19.0.0.zip
bios/Arcade/Arcade/maclc3.zip
+10 -47
View File
@@ -1,51 +1,14 @@
# Contributing BIOS Files
# Contributing to RetroBIOS
Thank you for helping expand the BIOS collection!
## Add a BIOS file
## How to Contribute
1. Fork this repository
2. Place the file in `bios/Manufacturer/Console/filename`
3. Variants (alternate hashes): `bios/Manufacturer/Console/.variants/`
4. Create a Pull Request - checksums are verified automatically
1. **Fork** this repository
2. **Add** your BIOS file to the correct directory under `bios/Manufacturer/Console/`
3. **Create a Pull Request**
## File conventions
## File Placement
Place files in the correct manufacturer/console directory:
```
bios/
├── Sony/
│ └── PlayStation/
│ └── scph5501.bin
├── Nintendo/
│ └── Game Boy Advance/
│ └── gba_bios.bin
└── Sega/
└── Dreamcast/
└── dc_boot.bin
```
## Verification
All submitted BIOS files are automatically verified against known checksums:
1. **Hash verification** - SHA1/MD5 checked against known databases
2. **Size verification** - File size matches expected value
3. **Platform reference** - File must be referenced in at least one platform config
4. **Duplicate detection** - Existing files are flagged to avoid duplication
## What We Accept
- **Verified BIOS dumps** with matching checksums from known databases
- **System firmware** required by emulators
- **New variants** of existing BIOS files (different regions, versions)
## What We Don't Accept
- Game ROMs or ISOs
- Modified/patched BIOS files
- Files without verifiable checksums
- Executable files (.exe, .bat, .sh)
## Questions?
Open an [Issue](../../issues) if you're unsure about a file.
- Files >50 MB go in GitHub release assets (`large-files` release)
- RPG Maker and ScummVM directories are excluded from deduplication
- See the [documentation site](https://abdess.github.io/retrobios/) for full details
+26 -11090
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+412 -62
View File
@@ -1,7 +1,7 @@
{
"generated_at": "2026-03-17T19:10:45Z",
"total_files": 5352,
"total_size": 3712113129,
"generated_at": "2026-03-18T11:53:21Z",
"total_files": 5376,
"total_size": 4896113929,
"files": {
"520d3d1b5897800af47f92efd2444a26b7a7dead": {
"path": "bios/3DO Company/3DO/3do_arcade_saot.bin",
@@ -1092,6 +1092,15 @@
"sha256": "90dc3258512e46ecf4ea8dfd0a8ebdb805bb001f2edfd6b882a1924da8ac305a",
"crc32": "b28f7112"
},
"ac4b78d53c7a97da2451ca35498395d8dd1e3024": {
"path": "bios/Arcade/Arcade/Firmware.19.0.0.zip",
"name": "Firmware.19.0.0.zip",
"size": 338076508,
"sha1": "ac4b78d53c7a97da2451ca35498395d8dd1e3024",
"md5": "72d6c73306c7f0b76723f989e7e1bdd1",
"sha256": "2f3791655e6c1b56f07a309b69ce8ea35d8412695599bbb6d4b0e29d1b044b66",
"crc32": "77228c84"
},
"5426d52e17e0ff9195fabbb42f704342e556d08e": {
"path": "bios/Arcade/Arcade/acpsx.zip",
"name": "acpsx.zip",
@@ -1515,14 +1524,14 @@
"sha256": "58e88cedb31918e2b9deeaddb80df45b8484cc1c4f10792520360a2585ad5014",
"crc32": "23ac17be"
},
"682f0d2475a3d30333c01d34ecf90b8b81d31997": {
"70c980f94d8d204cad3c18281cfb180592b6a6ee": {
"path": "bios/Arcade/Arcade/fdsbios.zip",
"name": "fdsbios.zip",
"size": 5724,
"sha1": "682f0d2475a3d30333c01d34ecf90b8b81d31997",
"md5": "e865dd6c266acb5e34a9012aac0d2862",
"sha256": "348746eaa86d1ec47f3cc59fe272bdae5ff5d6c6e29e8b2bd50a4a7fba7bbeb3",
"crc32": "32413056"
"size": 5788,
"sha1": "70c980f94d8d204cad3c18281cfb180592b6a6ee",
"md5": "7f780042388d83e7113556756d044d77",
"sha256": "1a49b157e41f1cf589e0e267d8f931f4793109cf76f162bedf85db9267e7acf1",
"crc32": "8ff1b99d"
},
"9ec50f79c5fd6eaddd88542aa18b6c5bb81a9ed4": {
"path": "bios/Arcade/Arcade/galgbios.zip",
@@ -1641,6 +1650,15 @@
"sha256": "9d057f11a2c4cfa7c637016fbca20c125c86a137ab4ca1957079f5cf0e73000c",
"crc32": "065d69d0"
},
"add40c002084e8e25768671877b2aa603aaf5cb1": {
"path": "bios/Arcade/Arcade/maclc3.zip",
"name": "maclc3.zip",
"size": 189428461,
"sha1": "add40c002084e8e25768671877b2aa603aaf5cb1",
"md5": "aff722788800df5b22d5a07cf8e558ee",
"sha256": "e663e456e88f475b3cacc06e75f50605e700789aa327b6648c627a560762a5d6",
"crc32": "81f21918"
},
"4e0202f8430cb4842184df7b5418e32620156c7b": {
"path": "bios/Arcade/Arcade/macsbios.zip",
"name": "macsbios.zip",
@@ -13269,6 +13287,15 @@
"sha256": "c93f3e7d3342f747f6c55a3b5ebb8c1840d3d118e80dc5d274f645dc2b393688",
"crc32": "26c6e8a0"
},
"273a9933b68a290c5aedcd6d69faa7b1d22c0344": {
"path": "bios/Elektronika/BK/TERAK.ROM",
"name": "TERAK.ROM",
"size": 128,
"sha1": "273a9933b68a290c5aedcd6d69faa7b1d22c0344",
"md5": "8cd30d86b57f9236ab749165b240a2cc",
"sha256": "a8cf685bd1c60ed722b956ae0d203da546c4249fe89639c44f2f294a4651ab34",
"crc32": "fd654b8e"
},
"61d0987b906146e21b94f265d5b51b4938c986a9": {
"path": "bios/Enterprise/64-128/basic20.rom",
"name": "basic20.rom",
@@ -16851,6 +16878,15 @@
"sha256": "3d5fae186cb31b5a6331155e532ef63e493afd85d7af8576fb52c95d0ce0362c",
"crc32": "1dbb7b59"
},
"4e1c2c2ee308ca4591542b3ca48653f65fae6e0f": {
"path": "bios/Microsoft/Xbox/cromwell_1024.bin",
"name": "cromwell_1024.bin",
"size": 1048576,
"sha1": "4e1c2c2ee308ca4591542b3ca48653f65fae6e0f",
"md5": "04e9565c5eb34c71c8f5f8b9f6524406",
"sha256": "b5c2cbdf297cd66ada8ef6fe4224c7c437cabc83d7c40c8e44af4941a72339c5",
"crc32": "5ae5278a"
},
"5d270675b54eb8071b480e42d22a3015ac211cef": {
"path": "bios/Microsoft/Xbox/mcpx_1.0.bin",
"name": "mcpx_1.0.bin",
@@ -16860,6 +16896,15 @@
"sha256": "e99e3a772bf5f5d262786aee895664eb96136196e37732fe66e14ae062f20335",
"crc32": "0b07d1f1"
},
"9da5f9ecfb1c9c32efa616f0300d02e8f702244d": {
"path": "bios/Microsoft/Xbox/xbox_hdd.qcow2",
"name": "xbox_hdd.qcow2",
"size": 1638400,
"sha1": "9da5f9ecfb1c9c32efa616f0300d02e8f702244d",
"md5": "cf15087fd5fd7593e293faaa7281cff8",
"sha256": "c5ec3e0fbc4f31d36c13211ded7f070f3b5f04e99a367fbe5d8c2d8c6e4f925d",
"crc32": "150b160b"
},
"ff3e7eaf715fe5612e46fc984d686ed3b115baef": {
"path": "bios/Microsoft/Xbox/xemu_eeprom.bin",
"name": "xemu_eeprom.bin",
@@ -18552,6 +18597,15 @@
"sha256": "1d7c771de535e6e07182fc9820ac32e76c6f0b5bcefd256a861fdfab144761ec",
"crc32": "df558b58"
},
"b48f44194fe918aaaec5298861479512b581d661": {
"path": "bios/Nintendo/Nintendo DS/dsi_nand.bin",
"name": "dsi_nand.bin",
"size": 251658304,
"sha1": "b48f44194fe918aaaec5298861479512b581d661",
"md5": "dfafb1908da8f527df7a372e649b50be",
"sha256": "f57d9bf00529bec35d58404faff029a193fd2ccda0a83403ec4e6cc32626721b",
"crc32": "416bf51a"
},
"3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3": {
"path": "bios/Nintendo/Nintendo DS/dsi_sd_card.bin",
"name": "dsi_sd_card.bin",
@@ -19101,6 +19155,150 @@
"sha256": "0ed991887342fba9e4b71668ff4c14ed93c2b3a19b4874dd0282404a1c442094",
"crc32": "aa727c5d"
},
"91d75a87872cbb88964bead92e0cbf8b72e836b6": {
"path": "bios/Other/NXEngine/Doukutsu.exe",
"name": "Doukutsu.exe",
"size": 1478656,
"sha1": "91d75a87872cbb88964bead92e0cbf8b72e836b6",
"md5": "f20bb7bc1b97453161e63964f24a2785",
"sha256": "8a7a63b24bb21557fb597697bdf09248c1ab7e3298cacfa1166d764dd81e7fc3",
"crc32": "0c0644ba"
},
"19555d2f5f72a66d6beddb4acc5ca00d634ac9c4": {
"path": "bios/Other/NXEngine/data/npc.tbl",
"name": "npc.tbl",
"size": 8664,
"sha1": "19555d2f5f72a66d6beddb4acc5ca00d634ac9c4",
"md5": "2c8dff849954bbca9c4a54087a5c06a9",
"sha256": "75b38bb4ab57b50bfc6c3ba77fe670a2eb67301f425aefe96bb1ed87c2b9e2db",
"crc32": "16b45e6c"
},
"58807f32c413a0c9db6deb0365f2fac9518e7c86": {
"path": "bios/Other/OpenTyrian/cubetxt1.dat",
"name": "cubetxt1.dat",
"size": 36169,
"sha1": "58807f32c413a0c9db6deb0365f2fac9518e7c86",
"md5": "3f2476d9ff29a06cf9fb85d52302222a",
"sha256": "2a0bc26ac4a8cdcbe7a145c9b1868a87ecb4335e9787f2f1e5713c965372df2b",
"crc32": "84e948df"
},
"e2b158dbf0c1a547e05f6e087f0ecc573a6c4f9a": {
"path": "bios/Other/OpenTyrian/levels1.dat",
"name": "levels1.dat",
"size": 9359,
"sha1": "e2b158dbf0c1a547e05f6e087f0ecc573a6c4f9a",
"md5": "013776dd1c00c1718906fac14493b69f",
"sha256": "e82164c96abb994450d416a3ab0e5fe6479fd4db831cd0f203a60dfe4aa78dcb",
"crc32": "67112fd6"
},
"b77313a9f92bc67f5c577699dcb95ca613e6948f": {
"path": "bios/Other/OpenTyrian/music.mus",
"name": "music.mus",
"size": 153482,
"sha1": "b77313a9f92bc67f5c577699dcb95ca613e6948f",
"md5": "bdb982d1ee185f1f713eadcd80931a66",
"sha256": "b06a509bd8576f63c61859e3a2c59482c4035d79f21725f9ec10479377044a74",
"crc32": "53e70005"
},
"29af4cd642e0b29dc8120b5d92f130eaf4e860c9": {
"path": "bios/Other/OpenTyrian/palette.dat",
"name": "palette.dat",
"size": 17664,
"sha1": "29af4cd642e0b29dc8120b5d92f130eaf4e860c9",
"md5": "847d6c6bf3d856422b7b27067049ed85",
"sha256": "6f0be74cb1b0026e46029d98d9ade41b8283bad849208543330b717fe5c365fc",
"crc32": "6c014b30"
},
"bd63566da5c24d1f50ffdcba99c654463f129d5c": {
"path": "bios/Other/OpenTyrian/tyrian.cdt",
"name": "tyrian.cdt",
"size": 1125,
"sha1": "bd63566da5c24d1f50ffdcba99c654463f129d5c",
"md5": "4490010ccebf217b86bb4337ac494773",
"sha256": "deb2c35297d89a1b2801ae21132d242121d9dee2a794a84b58dbd212f11f04fe",
"crc32": "6f6e39ce"
},
"f935e494b090b8bd2e86be06624a84946a5a947d": {
"path": "bios/Other/OpenTyrian/tyrian.hdt",
"name": "tyrian.hdt",
"size": 153657,
"sha1": "f935e494b090b8bd2e86be06624a84946a5a947d",
"md5": "0547aa08a6cfcdefa16f5b9813664ec6",
"sha256": "c816055863eb91d28ac10b4acc1e312f433deff553f08926e698c9a9ba68daa7",
"crc32": "c655ae91"
},
"0b216d9712f23e8faec041880b81773d93f646a2": {
"path": "bios/Other/OpenTyrian/tyrian.pic",
"name": "tyrian.pic",
"size": 365969,
"sha1": "0b216d9712f23e8faec041880b81773d93f646a2",
"md5": "a16de4ee30a67e2727c162dcd97ba25f",
"sha256": "5bda2ba46511f835e402188f1608b4398ae36303edfaef14304ad08b55bb5684",
"crc32": "278cba37"
},
"9978439cd9a52fde3d65c3b223dfe96ca437fef8": {
"path": "bios/Other/OpenTyrian/tyrian.shp",
"name": "tyrian.shp",
"size": 443871,
"sha1": "9978439cd9a52fde3d65c3b223dfe96ca437fef8",
"md5": "b79281a3c801b48af96cc57f510e2d83",
"sha256": "b9602b6dd42c1bf4716bdccbe5ab40495fcd064fcc55fd0f8d4436d640c4b450",
"crc32": "65906b5f"
},
"af896c25e6efeeb6dfd4cc7345362b4b29a85324": {
"path": "bios/Other/OpenTyrian/tyrian.snd",
"name": "tyrian.snd",
"size": 264512,
"sha1": "af896c25e6efeeb6dfd4cc7345362b4b29a85324",
"md5": "07fe095d8cc120b8293e7f776dfee90e",
"sha256": "0f5c37f97f0992026f1736f178bccc64746bc931daf23adbdc82553c140c80b8",
"crc32": "60dd487f"
},
"39825b5d69a07232d91886da68d217465a74695c": {
"path": "bios/Other/OpenTyrian/tyrian1.lvl",
"name": "tyrian1.lvl",
"size": 538262,
"sha1": "39825b5d69a07232d91886da68d217465a74695c",
"md5": "a1e73e0586ce715ab7daaedfe9b98595",
"sha256": "b41e532214fa8cea36e37fed6512b357c6ac9c8c3267899ceb4a258fcb1bf0b9",
"crc32": "06ce2efe"
},
"63ff6b55caeda529f69983a342618c84cba5addf": {
"path": "bios/Other/OpenTyrian/voices.snd",
"name": "voices.snd",
"size": 132767,
"sha1": "63ff6b55caeda529f69983a342618c84cba5addf",
"md5": "bdfeb89707fac1dd319a17e43328de2b",
"sha256": "c709ca1e74be6bd4b7917d258d81824d33c38a1a9a0287ec693b6459f58fc27a",
"crc32": "d23be573"
},
"cb1bd2cf5f89741900061955ac1a3b7cbd7a1ce9": {
"path": "bios/Other/QEMU/bios.bin",
"name": "bios.bin",
"size": 131072,
"sha1": "cb1bd2cf5f89741900061955ac1a3b7cbd7a1ce9",
"md5": "8bef06d1aa74c9ff45b268a18efcc954",
"sha256": "3dfd946d0c03ab0e022f84f10c3eb5f1dd507761f73e7d8067511ba35a10f776",
"crc32": "e7e3ac4c"
},
"214f09a25012e8702783d3ab9a22796071de5374": {
"path": "bios/Other/QEMU/vgabios.bin",
"name": "vgabios.bin",
"size": 38912,
"sha1": "214f09a25012e8702783d3ab9a22796071de5374",
"md5": "eb49484ba96ce09cdf4e60da747eceb1",
"sha256": "a7ea86bb06a58ff969fd0942e73b8eae00cb58e4c90bccbd900f6a3a01f54fbb",
"crc32": "e8256af7"
},
"d459d59b4d603d4cf733dd0fe34b7951f7c8165b": {
"path": "bios/Other/RVVM/fw_payload.bin",
"name": "fw_payload.bin",
"size": 2105672,
"sha1": "d459d59b4d603d4cf733dd0fe34b7951f7c8165b",
"md5": "14ee98e77ec06638bfef782a2ab8a063",
"sha256": "94468fe79c4fc51980c645cc964e00bdf21b4b68fd2306cfa13c4cd1ac9af9f4",
"crc32": "c068031c"
},
"cea669f6d740f29ca248d2e8837a4b4f86fbe75a": {
"path": "bios/Palm/Palm/bootloader-dbvz.rom",
"name": "bootloader-dbvz.rom",
@@ -19164,15 +19362,6 @@
"sha256": "c68f56d7cbcd4b621dd39804aa6bd50950df2946b970f81fd8bd87dae21c0813",
"crc32": "7bad9043"
},
"55068f5253956601a2eddd9c68efb6659ea27ac7": {
"path": "bios/Philips/CD-i/bios/cdibios.zip",
"name": "cdibios.zip",
"size": 468682,
"sha1": "55068f5253956601a2eddd9c68efb6659ea27ac7",
"md5": "80efc8294a76783c92e9f7b5a6b6c11b",
"sha256": "038fecfcca4fbb6ec17b59dded2322f2dd8d4564b88d2ccdc8d2400eb32164dd",
"crc32": "58926027"
},
"5d0b1b55b0d0958a5c9069c3219d4da5a87a6b93": {
"path": "bios/Philips/CD-i/cdimono1.zip",
"name": "cdimono1.zip",
@@ -45129,6 +45318,15 @@
"sha256": "ad1b2eb9300efd3d9476dad742c72e7436d9a9f67c951d537868553fa0c80843",
"crc32": "a61ca7c7"
},
"1c1a0d8c9f4c446ccd7470516b215ddca5052fb2": {
"path": "bios/Sharp/X1/FNT0808.X1",
"name": "FNT0808.X1",
"size": 2048,
"sha1": "1c1a0d8c9f4c446ccd7470516b215ddca5052fb2",
"md5": "851e4a5936f17d13f8c39a980cf00d77",
"sha256": "42139ed610747190c8ca9046c799907b52582c0d642b5f52f1548d9c1d4a86f8",
"crc32": "e3995a57"
},
"c4db9a6e99873808c8022afd1c50fef556a8b44d": {
"path": "bios/Sharp/X1/IPLROM.X1",
"name": "IPLROM.X1",
@@ -45156,15 +45354,6 @@
"sha256": "e6295a523008688421991b651ab61de392b28c67bec9efbf040b0004e4b70a2a",
"crc32": "e70011d3"
},
"1c1a0d8c9f4c446ccd7470516b215ddca5052fb2": {
"path": "bios/Sharp/X1/iplrom.x1t",
"name": "iplrom.x1t",
"size": 2048,
"sha1": "1c1a0d8c9f4c446ccd7470516b215ddca5052fb2",
"md5": "851e4a5936f17d13f8c39a980cf00d77",
"sha256": "42139ed610747190c8ca9046c799907b52582c0d642b5f52f1548d9c1d4a86f8",
"crc32": "e3995a57"
},
"76c18deb168ad0ffd7886a130a9e74e915070782": {
"path": "bios/Sharp/X68000/.variants/config",
"name": "config",
@@ -46695,6 +46884,15 @@
"sha256": "41de2047af8382988bfd568035ff26eec5f1cabc3efe773680546aa00a82857d",
"crc32": "2c3bcd32"
},
"093f8698b54b78dcb701de2043f82639de51d63b": {
"path": "bios/Sony/PlayStation 3/PS3UPDAT.PUP",
"name": "PS3UPDAT.PUP",
"size": 206126236,
"sha1": "093f8698b54b78dcb701de2043f82639de51d63b",
"md5": "05fe32f5dc8c78acbcd84d36ee7fdc5b",
"sha256": "69070a95780f59fc9e0d82bcf53eb9b28fd4ed4a7d54d0a40045f80422fd98d6",
"crc32": "24bdb2db"
},
"6bf1ae9fb01915966b715836253592cbf588b406": {
"path": "bios/Sony/PlayStation Portable/Roboto-Condensed.ttf",
"name": "Roboto-Condensed.ttf",
@@ -47586,6 +47784,24 @@
"sha256": "f372d306a7f2711125bea29aa75cbcb1eb19724e9648478fba07b8d655b37344",
"crc32": "c0c3a1fe"
},
"ed3a4cb264fff283209f10ae58c96c6090fed187": {
"path": "bios/Sony/PlayStation Vita/PSP2UPDAT.PUP",
"name": "PSP2UPDAT.PUP",
"size": 56778752,
"sha1": "ed3a4cb264fff283209f10ae58c96c6090fed187",
"md5": "59dcf059d3328fb67be7e51f8aa33418",
"sha256": "c3c03fc7363dd573d90e5157629bf11551f434b283cc898d9ffc71dd716b791c",
"crc32": "082ecf86"
},
"cc72dfcc964577cc29112ef368c28f55277c237c": {
"path": "bios/Sony/PlayStation Vita/PSVUPDAT.PUP",
"name": "PSVUPDAT.PUP",
"size": 133834240,
"sha1": "cc72dfcc964577cc29112ef368c28f55277c237c",
"md5": "f2c7b12fe85496ec88a0391b514d6e3b",
"sha256": "6ef6dc8da6db026f28647713e473486d770087a605c52a8d751bfca7478386cf",
"crc32": "39075d41"
},
"b184f1c1febf66c8168fcae0b8aa37a5754f79db": {
"path": "bios/Synertek/SYM-1/SYM.ROM",
"name": "SYM.ROM",
@@ -48295,6 +48511,7 @@
"fcb298d97792b9e9bdd3296cc6be10b6": "eb2a867578a05bbf8741e9fe7204301062df0cb8",
"ddb8aacffffffa608ddbb4a6d6dda5ec": "0b6519209766ed883e3fca4c61bf866804c89004",
"6c6c0c726cbf15e81785eb7592fdb51c": "de463b0577dfd1027bf7de523ff67a0fff861cdb",
"72d6c73306c7f0b76723f989e7e1bdd1": "ac4b78d53c7a97da2451ca35498395d8dd1e3024",
"fcb631bf18a56f2d5b077fa846bab4a6": "5426d52e17e0ff9195fabbb42f704342e556d08e",
"3f348c88af99a40fbd11fa435f28c69d": "e18c5e9ca21654dfd724aa54e625b386e6ffb2c5",
"c266fc58905af1e246dffadc84301042": "beaf97c4a0e0792b8db65648f9dabb6a54ae0549",
@@ -48342,7 +48559,7 @@
"d32987cdfcbd9f82abbfa93a297aa13f": "17d45f259fec8ef784526fe205c1b0722c5a9a00",
"547f3d12aed389058ca06148f1cca0ed": "b6ff66dcb5547bd91760d239ddf428a655631c53",
"1028615bcac4c31634a3364ce5c04044": "48d1712d1b1cdfeeeb43c6287c17b0b6309cfaab",
"e865dd6c266acb5e34a9012aac0d2862": "682f0d2475a3d30333c01d34ecf90b8b81d31997",
"7f780042388d83e7113556756d044d77": "70c980f94d8d204cad3c18281cfb180592b6a6ee",
"a832488251f892f961a9afe125320adf": "9ec50f79c5fd6eaddd88542aa18b6c5bb81a9ed4",
"c45fdc9ceb6908110656940a86b72c56": "6fdec00172233095cebb085b26214648d1093ecb",
"af5512431a3c23d8bb93057d17cff470": "975fbbbec0b578a495a8da9e1a5965d94d6d52e1",
@@ -48356,6 +48573,7 @@
"7b51d463324b6bf26e86c4afb7316a3a": "8cf0aa7f9dca4d77485e605fb0e2173a734633bf",
"7a14456d6e8afaf540f2368fead25f26": "78c8e1c3c033b65758b7e53a9346b44d037fea7f",
"d048a9ff941041de45c26474a0da40aa": "65a2f2cee74c316d5f40b68deda66787609df353",
"aff722788800df5b22d5a07cf8e558ee": "add40c002084e8e25768671877b2aa603aaf5cb1",
"34530e248d96e7171af19155af315378": "4e0202f8430cb4842184df7b5418e32620156c7b",
"b48fb4fb35dc348f4904a318dbf9a712": "697551fcf9557ae33e31096b118a0c6769700a2e",
"6bb005f55ba39bc5f6b330b663da1a58": "c9ee16e26e03496195a7bff151efbdd89da01204",
@@ -49648,6 +49866,7 @@
"5015228eeeb238e65da8edcd1b6dfac7": "28eefbb63047b26e4aec104aeeca74e2f9d0276c",
"5737f972e8638831ab71e9139abae052": "6386e58bc1bba5e76baec9e8a1ca4b99dc3c573f",
"95f8c41c6abf7640e35a6a03cecebd01": "4e83a94ae5155bbea14d7331a5a8db82457bd5ae",
"8cd30d86b57f9236ab749165b240a2cc": "273a9933b68a290c5aedcd6d69faa7b1d22c0344",
"8e18edce4a7acb2c33cc0ab18f988482": "61d0987b906146e21b94f265d5b51b4938c986a9",
"e972fe42b398c9ff1d93ff014786aec6": "03bbb386cf530e804363acdfc1d13e64cf28af2e",
"6af0402906944fd134004b85097c8524": "f34f0c330b44dbf2548329bea954d5991dec30ca",
@@ -50046,7 +50265,9 @@
"248514aba82a0ec7fe2a9106862b05cd": "e7905d16d2ccd57a013c122dc432106cd59ef52c",
"a0452dbf5ace7d2e49d0a8029efed09a": "829c00c3114f25b3dae5157c0a238b52a3ac37db",
"39cee882148a87f93cb440b99dde3ceb": "3944392c954cfb176d4210544e88353b3c5d36b1",
"04e9565c5eb34c71c8f5f8b9f6524406": "4e1c2c2ee308ca4591542b3ca48653f65fae6e0f",
"d49c52a4102f6df7bcf8d0617ac475ed": "5d270675b54eb8071b480e42d22a3015ac211cef",
"cf15087fd5fd7593e293faaa7281cff8": "9da5f9ecfb1c9c32efa616f0300d02e8f702244d",
"3e647719d47e4a3992fc8bc5a7e56fce": "ff3e7eaf715fe5612e46fc984d686ed3b115baef",
"9f770275393b8627cf9d24e5c56d2ab9": "008cf0f5cd5e2000b9f2ebf5e4ee84097e6aef74",
"424f1d6bf93259bf255afa7d1dc9f721": "3ca4a3b8d8a7f08492e684064c6fa362e914c1af",
@@ -50235,6 +50456,7 @@
"bfd8292fbf0a251647a23c5cb310a97a": "f1ad917e0affaeb8d2114c7ecd02b9f938c3cbd9",
"94bc5094607c5e6598d50472c52f27f2": "1cf9e67c2c703bb9961bbcdd39cd2c7e319a803b",
"8daa89fd280b3e5ec79fbab73ad6684e": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616",
"dfafb1908da8f527df7a372e649b50be": "b48f44194fe918aaaec5298861479512b581d661",
"b6d81b360a5672d80c27430f39153e2c": "3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3",
"74f23348012d7b3e1cc216c47192ffeb": "3773f52559d5ac4fc6d8aefe35bce58730ae8181",
"e45033d9b0fa6b0de071292bba7c9d13": "cfe072921ee3fb93f688743f8beef89043c3e9ad",
@@ -50296,6 +50518,22 @@
"0a814078410353744e2947a8e9342e4e": "35f92a0477a88f5cf564971125047ffcfa02ec10",
"82a22231d402cd3284c698ba16a51d1d": "d8ce5b1405b6428969493efeb6f3aa2027c41bdc",
"9a432244d9ee4a49e8ddcde64af94e05": "86fc8dc0932f983efa199e31ae05a4424772f959",
"f20bb7bc1b97453161e63964f24a2785": "91d75a87872cbb88964bead92e0cbf8b72e836b6",
"2c8dff849954bbca9c4a54087a5c06a9": "19555d2f5f72a66d6beddb4acc5ca00d634ac9c4",
"3f2476d9ff29a06cf9fb85d52302222a": "58807f32c413a0c9db6deb0365f2fac9518e7c86",
"013776dd1c00c1718906fac14493b69f": "e2b158dbf0c1a547e05f6e087f0ecc573a6c4f9a",
"bdb982d1ee185f1f713eadcd80931a66": "b77313a9f92bc67f5c577699dcb95ca613e6948f",
"847d6c6bf3d856422b7b27067049ed85": "29af4cd642e0b29dc8120b5d92f130eaf4e860c9",
"4490010ccebf217b86bb4337ac494773": "bd63566da5c24d1f50ffdcba99c654463f129d5c",
"0547aa08a6cfcdefa16f5b9813664ec6": "f935e494b090b8bd2e86be06624a84946a5a947d",
"a16de4ee30a67e2727c162dcd97ba25f": "0b216d9712f23e8faec041880b81773d93f646a2",
"b79281a3c801b48af96cc57f510e2d83": "9978439cd9a52fde3d65c3b223dfe96ca437fef8",
"07fe095d8cc120b8293e7f776dfee90e": "af896c25e6efeeb6dfd4cc7345362b4b29a85324",
"a1e73e0586ce715ab7daaedfe9b98595": "39825b5d69a07232d91886da68d217465a74695c",
"bdfeb89707fac1dd319a17e43328de2b": "63ff6b55caeda529f69983a342618c84cba5addf",
"8bef06d1aa74c9ff45b268a18efcc954": "cb1bd2cf5f89741900061955ac1a3b7cbd7a1ce9",
"eb49484ba96ce09cdf4e60da747eceb1": "214f09a25012e8702783d3ab9a22796071de5374",
"14ee98e77ec06638bfef782a2ab8a063": "d459d59b4d603d4cf733dd0fe34b7951f7c8165b",
"9da101cd2317830649a31f8fa46debec": "cea669f6d740f29ca248d2e8837a4b4f86fbe75a",
"abed11421f47bbf3f654af618e0c6a8a": "a368b40751dd017163c9c1a615d6f3506b7dcbdf",
"83cb1d1c76e568b916dc2e7c0bf669f6": "cc4898ee8cae4669fc19e184c5b560c770e731b3",
@@ -50303,7 +50541,6 @@
"c2fae3ff41cc4f94be0fdaed1523ea99": "858c62e21d3a42d2e70641d001a46ad44e923614",
"4d8f5238df9a374ce3640262773ba885": "e6714b3d5fdc7023348435a77a016b763e0992b1",
"56683e58930b2b554e6594fe04eda238": "e1d30b1d6a23aaaa765102590dc3ffff19c0b09f",
"80efc8294a76783c92e9f7b5a6b6c11b": "55068f5253956601a2eddd9c68efb6659ea27ac7",
"c59f92647701428bc453976740eb75cf": "5d0b1b55b0d0958a5c9069c3219d4da5a87a6b93",
"97aa5f47030cd9fdb679d4fafbb0e332": "9492247203b71c12d88fad0a5437376941c7870a",
"f1071cdb0b6b10dde94d3bc8a6146387": "a6120aed50831c9c0d95dbdf707820f601d9452e",
@@ -53188,10 +53425,10 @@
"3602382c1a370fb3064fcadeeea809e4": "1ec11e6639ab20b1bf1a69a5e5222909284c042b",
"1474da2b8fbbb37abce8e7ab5cf9024c": "fec7527ecbf79b1ac697137f770bb8715fe8a652",
"e634c906c23e62d5d3cd63581e5748ff": "2ca428b70ed1746834d129c11fb8e60a56317cff",
"851e4a5936f17d13f8c39a980cf00d77": "1c1a0d8c9f4c446ccd7470516b215ddca5052fb2",
"59074727a953fe965109b7dbe3298e30": "c4db9a6e99873808c8022afd1c50fef556a8b44d",
"56c28adcf1f3a2f87cf3d57c378013f5": "44620f57a25f0bcac2b57ca2b0f1ebad3bf305d3",
"eeeea1cd29c6e0e8b094790ae969bfa7": "d3395e9aeb5b8bbba7654dd471bcd8af228ee69a",
"851e4a5936f17d13f8c39a980cf00d77": "1c1a0d8c9f4c446ccd7470516b215ddca5052fb2",
"51b55ee3807901c015fdb93616858b8b": "76c18deb168ad0ffd7886a130a9e74e915070782",
"d407317a52f8425a6753232064d14700": "77be2f6f28897f99b73d4c47bf7cd47e999fd7cd",
"cb0a5cfcf7247a7eab74bb2716260269": "8d72c5b4d63bb14c5dbdac495244d659aa1498b6",
@@ -53362,6 +53599,7 @@
"5dea62f70439682a6cee16ba3823d11e": "6eddec30056cde7c664a0cf508dcad29353a12bb",
"de6da198a7359d1200c3eeb6df9c7eda": "40ecf6138c99a0aba775ef93240b295025a45500",
"44552702b05697a14ccbe2ca22ee7139": "47d2ec4b342649e4c391043ab915d4435f9d180d",
"05fe32f5dc8c78acbcd84d36ee7fdc5b": "093f8698b54b78dcb701de2043f82639de51d63b",
"55caa30ec34ef081ded15615db54eafe": "6bf1ae9fb01915966b715836253592cbf588b406",
"a062688b08c70a42ff2a0acff6c46d93": "08325554623568bb9babadc10213bfc0b1151766",
"ad0542e2956a8dddf52357f28a8a7d9c": "9d6c9874c1d6a0c57a1345f211154fe1e494b55a",
@@ -53461,6 +53699,8 @@
"331d6806a56d7370515d94a66616eca6": "78632d0fe9dd77bf9a2264f192fae6f0af03a71c",
"c96bb62586bf81dd6237c417f8cf3bb8": "18985a2079c7570c13cf39e0d001eef87538cd15",
"8b5f60b56c3da8365b973dba570c53a5": "3ae832c9800fcaa007eccfc48f24242967c111f8",
"59dcf059d3328fb67be7e51f8aa33418": "ed3a4cb264fff283209f10ae58c96c6090fed187",
"f2c7b12fe85496ec88a0391b514d6e3b": "cc72dfcc964577cc29112ef368c28f55277c237c",
"e59fdf56762c480ba4dfe1b3ec5fb86d": "b184f1c1febf66c8168fcae0b8aa37a5754f79db",
"1d33d70f35b33873fc75941d95ad1ffa": "567c5b5054552a2771eafa7966844a146f0dde96",
"b81dc552536796d234c08587bac7be43": "f2fa8d8e940f1d91a1b1624013df5dca0bb1ee44",
@@ -53743,7 +53983,8 @@
"1365b42d35d80feac9050caea8d6bd9d374fd1d2"
],
"maclc3.zip": [
"f454095619834dbfb9e8de0111bb3ee5fc21622d"
"f454095619834dbfb9e8de0111bb3ee5fc21622d",
"add40c002084e8e25768671877b2aa603aaf5cb1"
],
"macos3.img": [
"0546dd0fa34f4e6d913e4254ddb5350e1e42800c"
@@ -53802,8 +54043,7 @@
],
"cdibios.zip": [
"e7d2a0dad62d6f75bc10f48a376da0a99b764571",
"16072afaa65d1b059346616ac5b5a600c63ff1d1",
"55068f5253956601a2eddd9c68efb6659ea27ac7"
"16072afaa65d1b059346616ac5b5a600c63ff1d1"
],
"coh1000a.zip": [
"f8526dcec63402d2533d8180e217fa03a6322c34",
@@ -53817,7 +54057,7 @@
],
"fdsbios.zip": [
"c25686d24c7205473741f948f8a9df9906823145",
"682f0d2475a3d30333c01d34ecf90b8b81d31997"
"70c980f94d8d204cad3c18281cfb180592b6a6ee"
],
"hng64.zip": [
"fb0c36d69f66f4b10a895aa708ae37f826755257",
@@ -53888,6 +54128,9 @@
"de463b0577dfd1027bf7de523ff67a0fff861cdb",
"12516c82f52a8abb252fba754f95b7952c295e6f"
],
"Firmware.19.0.0.zip": [
"ac4b78d53c7a97da2451ca35498395d8dd1e3024"
],
"acpsx.zip": [
"5426d52e17e0ff9195fabbb42f704342e556d08e"
],
@@ -57681,6 +57924,9 @@
"MONIT10.ROM": [
"4e83a94ae5155bbea14d7331a5a8db82457bd5ae"
],
"TERAK.ROM": [
"273a9933b68a290c5aedcd6d69faa7b1d22c0344"
],
"basic20.rom": [
"61d0987b906146e21b94f265d5b51b4938c986a9"
],
@@ -58448,9 +58694,15 @@
"Complex_4627.bin": [
"3944392c954cfb176d4210544e88353b3c5d36b1"
],
"cromwell_1024.bin": [
"4e1c2c2ee308ca4591542b3ca48653f65fae6e0f"
],
"mcpx_1.0.bin": [
"5d270675b54eb8071b480e42d22a3015ac211cef"
],
"xbox_hdd.qcow2": [
"9da5f9ecfb1c9c32efa616f0300d02e8f702244d"
],
"xemu_eeprom.bin": [
"ff3e7eaf715fe5612e46fc984d686ed3b115baef"
],
@@ -58971,6 +59223,9 @@
"dsi_firmware.bin": [
"d2a5af338f09c5cbdd5d7628db5b9c075c69b616"
],
"dsi_nand.bin": [
"b48f44194fe918aaaec5298861479512b581d661"
],
"dsi_sd_card.bin": [
"3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3"
],
@@ -59150,6 +59405,54 @@
"telmon24.rom": [
"86fc8dc0932f983efa199e31ae05a4424772f959"
],
"Doukutsu.exe": [
"91d75a87872cbb88964bead92e0cbf8b72e836b6"
],
"npc.tbl": [
"19555d2f5f72a66d6beddb4acc5ca00d634ac9c4"
],
"cubetxt1.dat": [
"58807f32c413a0c9db6deb0365f2fac9518e7c86"
],
"levels1.dat": [
"e2b158dbf0c1a547e05f6e087f0ecc573a6c4f9a"
],
"music.mus": [
"b77313a9f92bc67f5c577699dcb95ca613e6948f"
],
"palette.dat": [
"29af4cd642e0b29dc8120b5d92f130eaf4e860c9"
],
"tyrian.cdt": [
"bd63566da5c24d1f50ffdcba99c654463f129d5c"
],
"tyrian.hdt": [
"f935e494b090b8bd2e86be06624a84946a5a947d"
],
"tyrian.pic": [
"0b216d9712f23e8faec041880b81773d93f646a2"
],
"tyrian.shp": [
"9978439cd9a52fde3d65c3b223dfe96ca437fef8"
],
"tyrian.snd": [
"af896c25e6efeeb6dfd4cc7345362b4b29a85324"
],
"tyrian1.lvl": [
"39825b5d69a07232d91886da68d217465a74695c"
],
"voices.snd": [
"63ff6b55caeda529f69983a342618c84cba5addf"
],
"bios.bin": [
"cb1bd2cf5f89741900061955ac1a3b7cbd7a1ce9"
],
"vgabios.bin": [
"214f09a25012e8702783d3ab9a22796071de5374"
],
"fw_payload.bin": [
"d459d59b4d603d4cf733dd0fe34b7951f7c8165b"
],
"bootloader-dbvz.rom": [
"cea669f6d740f29ca248d2e8837a4b4f86fbe75a"
],
@@ -67006,6 +67309,9 @@
"mz80kj.zip": [
"2ca428b70ed1746834d129c11fb8e60a56317cff"
],
"FNT0808.X1": [
"1c1a0d8c9f4c446ccd7470516b215ddca5052fb2"
],
"IPLROM.X1": [
"c4db9a6e99873808c8022afd1c50fef556a8b44d"
],
@@ -67015,9 +67321,6 @@
"iplrom.x1": [
"d3395e9aeb5b8bbba7654dd471bcd8af228ee69a"
],
"iplrom.x1t": [
"1c1a0d8c9f4c446ccd7470516b215ddca5052fb2"
],
"cgrom.dat": [
"8d72c5b4d63bb14c5dbdac495244d659aa1498b6"
],
@@ -67507,6 +67810,9 @@
"rom1.bin": [
"47d2ec4b342649e4c391043ab915d4435f9d180d"
],
"PS3UPDAT.PUP": [
"093f8698b54b78dcb701de2043f82639de51d63b"
],
"Roboto-Condensed.ttf": [
"6bf1ae9fb01915966b715836253592cbf588b406"
],
@@ -67799,7 +68105,11 @@
"18985a2079c7570c13cf39e0d001eef87538cd15"
],
"PSP2UPDAT.PUP": [
"3ae832c9800fcaa007eccfc48f24242967c111f8"
"3ae832c9800fcaa007eccfc48f24242967c111f8",
"ed3a4cb264fff283209f10ae58c96c6090fed187"
],
"PSVUPDAT.PUP": [
"cc72dfcc964577cc29112ef368c28f55277c237c"
],
"coco.zip": [
"567c5b5054552a2771eafa7966844a146f0dde96",
@@ -67981,6 +68291,15 @@
"disk2-16boot.rom": [
"d4181c9f046aafc3fb326b381baac809d9e38d16"
],
"tos102uk.img": [
"87900a40a890fdf03bd08be6c60cc645855cbce5"
],
"tos104uk.img": [
"9526ef63b9cb1d2a7109e278547ae78a5c1db6c6"
],
"tos106uk.img": [
"06f9ea322e74b682df0396acfaee8cb4d9c90cad"
],
"Complex_4627v1.03.bin": [
"3944392c954cfb176d4210544e88353b3c5d36b1"
],
@@ -68395,6 +68714,9 @@
"007-Ocean01.jpg": [
"e5741010ac7f941cdb3c01865a1eda016a479dfd"
],
"[BIOS] SNK Neo Geo Pocket Color (Japan) (En,Ja).ngc": [
"edc13192054a59be49c6d55f83b70e2510968e86"
],
"emi_actorlights.fragment": [
"41a739a72a43ee22d7d696d3b8a077af029a3ede"
],
@@ -68452,6 +68774,31 @@
"bios_J.sms": [
"a8c1b39a2e41137835eda6a5de6d46dd9fadbaf2"
],
"eu_mcd2_9306.bin": [
"7063192ae9f6b696c5b81bc8f0a9fe6f0c400e58"
],
"sega-mega-cd:9b562ebf2d095bf1dabadbc1881f519a": [
"7063192ae9f6b696c5b81bc8f0a9fe6f0c400e58"
],
"jp_mcd2_921222.bin": [
"d203cfe22c03ae479dd8ca33840cf8d9776eb3ff"
],
"sega-mega-cd:683a8a9e273662561172468dfa2858eb": [
"d203cfe22c03ae479dd8ca33840cf8d9776eb3ff"
],
"us_scd2_9306.bin": [
"5a8c4b91d3034c1448aac4b5dc9a6484fce51636"
],
"sega-mega-cd:310a9081d2edf2d316ab38813136725e": [
"5a8c4b91d3034c1448aac4b5dc9a6484fce51636"
],
"iplrom.x1t": [
"1c1a0d8c9f4c446ccd7470516b215ddca5052fb2",
"44620f57a25f0bcac2b57ca2b0f1ebad3bf305d3"
],
"128p-1.rom": [
"80080644289ed93d71a1103992a154cc9802b2fa"
],
"SCPH-70004_BIOS_V12_EUR_200.BIN": [
"434bc0b4eb4827da0773ec0795aadc5162569a07"
],
@@ -68473,18 +68820,9 @@
"tos100uk.img": [
"9a6e4c88533a9eaa4d55cdc040e47443e0226eb2"
],
"tos102uk.img": [
"87900a40a890fdf03bd08be6c60cc645855cbce5"
],
"tos104uk.img": [
"9526ef63b9cb1d2a7109e278547ae78a5c1db6c6"
],
"tos106de.img": [
"3b8cf5ffa41b252eb67f8824f94608fa4005d6dd"
],
"tos106uk.img": [
"06f9ea322e74b682df0396acfaee8cb4d9c90cad"
],
"tos206us.img": [
"ee58768bdfc602c9b14942ce5481e97dd24e7c83"
],
@@ -68968,15 +69306,6 @@
"sega-mega-cd:e66fa1dc5820d254611fdcdba0662372": [
"f891e0ea651e2232af0c5c4cb46a0cae2ee8f356"
],
"sega-mega-cd:683a8a9e273662561172468dfa2858eb": [
"d203cfe22c03ae479dd8ca33840cf8d9776eb3ff"
],
"sega-mega-cd:310a9081d2edf2d316ab38813136725e": [
"5a8c4b91d3034c1448aac4b5dc9a6484fce51636"
],
"sega-mega-cd:9b562ebf2d095bf1dabadbc1881f519a": [
"7063192ae9f6b696c5b81bc8f0a9fe6f0c400e58"
],
"sega-mega-cd:854b9150240a198070150e4566ae1290": [
"5adb6c3af218c60868e6b723ec47e36bbdf5e6f0"
],
@@ -69026,9 +69355,6 @@
"flash.bin": [
"94d44d7f9529ec1642ba3771ed3c5f756d5bc872"
],
"128p-1.rom": [
"80080644289ed93d71a1103992a154cc9802b2fa"
],
"plus3e-3.rom": [
"65f031caa8148a5493afe42c41f4929deab26b4e"
],
@@ -69264,6 +69590,7 @@
"ad37f2de": "eb2a867578a05bbf8741e9fe7204301062df0cb8",
"b0e03aa6": "0b6519209766ed883e3fca4c61bf866804c89004",
"b28f7112": "de463b0577dfd1027bf7de523ff67a0fff861cdb",
"77228c84": "ac4b78d53c7a97da2451ca35498395d8dd1e3024",
"9c9601ca": "5426d52e17e0ff9195fabbb42f704342e556d08e",
"2c87c283": "e18c5e9ca21654dfd724aa54e625b386e6ffb2c5",
"da9beacc": "beaf97c4a0e0792b8db65648f9dabb6a54ae0549",
@@ -69311,7 +69638,7 @@
"d2ea94b0": "17d45f259fec8ef784526fe205c1b0722c5a9a00",
"17516536": "b6ff66dcb5547bd91760d239ddf428a655631c53",
"23ac17be": "48d1712d1b1cdfeeeb43c6287c17b0b6309cfaab",
"32413056": "682f0d2475a3d30333c01d34ecf90b8b81d31997",
"8ff1b99d": "70c980f94d8d204cad3c18281cfb180592b6a6ee",
"3890ead5": "9ec50f79c5fd6eaddd88542aa18b6c5bb81a9ed4",
"e6264ee1": "6fdec00172233095cebb085b26214648d1093ecb",
"5be16858": "975fbbbec0b578a495a8da9e1a5965d94d6d52e1",
@@ -69325,6 +69652,7 @@
"ce2a2c77": "8cf0aa7f9dca4d77485e605fb0e2173a734633bf",
"7325a518": "78c8e1c3c033b65758b7e53a9346b44d037fea7f",
"065d69d0": "65a2f2cee74c316d5f40b68deda66787609df353",
"81f21918": "add40c002084e8e25768671877b2aa603aaf5cb1",
"946c6bb8": "4e0202f8430cb4842184df7b5418e32620156c7b",
"bfce92a3": "697551fcf9557ae33e31096b118a0c6769700a2e",
"7c57bff1": "c9ee16e26e03496195a7bff151efbdd89da01204",
@@ -70617,6 +70945,7 @@
"ed8a43ae": "28eefbb63047b26e4aec104aeeca74e2f9d0276c",
"b90a52e8": "6386e58bc1bba5e76baec9e8a1ca4b99dc3c573f",
"26c6e8a0": "4e83a94ae5155bbea14d7331a5a8db82457bd5ae",
"fd654b8e": "273a9933b68a290c5aedcd6d69faa7b1d22c0344",
"1228de34": "61d0987b906146e21b94f265d5b51b4938c986a9",
"55f96251": "03bbb386cf530e804363acdfc1d13e64cf28af2e",
"6999d6a3": "f34f0c330b44dbf2548329bea954d5991dec30ca",
@@ -71015,7 +71344,9 @@
"95db2959": "e7905d16d2ccd57a013c122dc432106cd59ef52c",
"8205795e": "829c00c3114f25b3dae5157c0a238b52a3ac37db",
"1dbb7b59": "3944392c954cfb176d4210544e88353b3c5d36b1",
"5ae5278a": "4e1c2c2ee308ca4591542b3ca48653f65fae6e0f",
"0b07d1f1": "5d270675b54eb8071b480e42d22a3015ac211cef",
"150b160b": "9da5f9ecfb1c9c32efa616f0300d02e8f702244d",
"151aca4c": "ff3e7eaf715fe5612e46fc984d686ed3b115baef",
"31cc5649": "008cf0f5cd5e2000b9f2ebf5e4ee84097e6aef74",
"5c01c82b": "3ca4a3b8d8a7f08492e684064c6fa362e914c1af",
@@ -71204,6 +71535,7 @@
"7389b815": "f1ad917e0affaeb8d2114c7ecd02b9f938c3cbd9",
"62efc03d": "1cf9e67c2c703bb9961bbcdd39cd2c7e319a803b",
"df558b58": "d2a5af338f09c5cbdd5d7628db5b9c075c69b616",
"416bf51a": "b48f44194fe918aaaec5298861479512b581d661",
"a738ea1c": "3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3",
"6f6d289b": "3773f52559d5ac4fc6d8aefe35bce58730ae8181",
"945f9dc9": "cfe072921ee3fb93f688743f8beef89043c3e9ad",
@@ -71265,6 +71597,22 @@
"94358dc6": "35f92a0477a88f5cf564971125047ffcfa02ec10",
"29e86dbc": "d8ce5b1405b6428969493efeb6f3aa2027c41bdc",
"aa727c5d": "86fc8dc0932f983efa199e31ae05a4424772f959",
"0c0644ba": "91d75a87872cbb88964bead92e0cbf8b72e836b6",
"16b45e6c": "19555d2f5f72a66d6beddb4acc5ca00d634ac9c4",
"84e948df": "58807f32c413a0c9db6deb0365f2fac9518e7c86",
"67112fd6": "e2b158dbf0c1a547e05f6e087f0ecc573a6c4f9a",
"53e70005": "b77313a9f92bc67f5c577699dcb95ca613e6948f",
"6c014b30": "29af4cd642e0b29dc8120b5d92f130eaf4e860c9",
"6f6e39ce": "bd63566da5c24d1f50ffdcba99c654463f129d5c",
"c655ae91": "f935e494b090b8bd2e86be06624a84946a5a947d",
"278cba37": "0b216d9712f23e8faec041880b81773d93f646a2",
"65906b5f": "9978439cd9a52fde3d65c3b223dfe96ca437fef8",
"60dd487f": "af896c25e6efeeb6dfd4cc7345362b4b29a85324",
"06ce2efe": "39825b5d69a07232d91886da68d217465a74695c",
"d23be573": "63ff6b55caeda529f69983a342618c84cba5addf",
"e7e3ac4c": "cb1bd2cf5f89741900061955ac1a3b7cbd7a1ce9",
"e8256af7": "214f09a25012e8702783d3ab9a22796071de5374",
"c068031c": "d459d59b4d603d4cf733dd0fe34b7951f7c8165b",
"a975efe4": "cea669f6d740f29ca248d2e8837a4b4f86fbe75a",
"9261a5aa": "a368b40751dd017163c9c1a615d6f3506b7dcbdf",
"6481a088": "cc4898ee8cae4669fc19e184c5b560c770e731b3",
@@ -71272,7 +71620,6 @@
"25314c62": "858c62e21d3a42d2e70641d001a46ad44e923614",
"59205298": "e6714b3d5fdc7023348435a77a016b763e0992b1",
"7bad9043": "e1d30b1d6a23aaaa765102590dc3ffff19c0b09f",
"58926027": "55068f5253956601a2eddd9c68efb6659ea27ac7",
"0a67ff2c": "5d0b1b55b0d0958a5c9069c3219d4da5a87a6b93",
"4eab5eda": "9492247203b71c12d88fad0a5437376941c7870a",
"a318e8d6": "a6120aed50831c9c0d95dbdf707820f601d9452e",
@@ -74157,10 +74504,10 @@
"8a887ad3": "1ec11e6639ab20b1bf1a69a5e5222909284c042b",
"17878e56": "fec7527ecbf79b1ac697137f770bb8715fe8a652",
"a61ca7c7": "2ca428b70ed1746834d129c11fb8e60a56317cff",
"e3995a57": "1c1a0d8c9f4c446ccd7470516b215ddca5052fb2",
"7b28d9de": "c4db9a6e99873808c8022afd1c50fef556a8b44d",
"2e8b767c": "44620f57a25f0bcac2b57ca2b0f1ebad3bf305d3",
"e70011d3": "d3395e9aeb5b8bbba7654dd471bcd8af228ee69a",
"e3995a57": "1c1a0d8c9f4c446ccd7470516b215ddca5052fb2",
"72628c06": "76c18deb168ad0ffd7886a130a9e74e915070782",
"5dd9a0c5": "77be2f6f28897f99b73d4c47bf7cd47e999fd7cd",
"9f3195f1": "8d72c5b4d63bb14c5dbdac495244d659aa1498b6",
@@ -74331,6 +74678,7 @@
"0b805686": "6eddec30056cde7c664a0cf508dcad29353a12bb",
"a3e573a0": "40ecf6138c99a0aba775ef93240b295025a45500",
"2c3bcd32": "47d2ec4b342649e4c391043ab915d4435f9d180d",
"24bdb2db": "093f8698b54b78dcb701de2043f82639de51d63b",
"72d2fd97": "6bf1ae9fb01915966b715836253592cbf588b406",
"c48bf84b": "08325554623568bb9babadc10213bfc0b1151766",
"2b629346": "9d6c9874c1d6a0c57a1345f211154fe1e494b55a",
@@ -74430,6 +74778,8 @@
"616f5e40": "78632d0fe9dd77bf9a2264f192fae6f0af03a71c",
"92ddefae": "18985a2079c7570c13cf39e0d001eef87538cd15",
"c0c3a1fe": "3ae832c9800fcaa007eccfc48f24242967c111f8",
"082ecf86": "ed3a4cb264fff283209f10ae58c96c6090fed187",
"39075d41": "cc72dfcc964577cc29112ef368c28f55277c237c",
"44295096": "b184f1c1febf66c8168fcae0b8aa37a5754f79db",
"31c53421": "567c5b5054552a2771eafa7966844a146f0dde96",
"d13aefe2": "f2fa8d8e940f1d91a1b1624013df5dca0bb1ee44",
+1
View File
@@ -1,6 +1,7 @@
emulator: bsnes
type: libretro
source: "https://github.com/libretro/bsnes-libretro"
logo: "https://raw.githubusercontent.com/bsnes-emu/bsnes/master/bsnes/target-bsnes/resource/bsnes.svg"
profiled_date: "2026-03-18"
core_version: "115"
display_name: "Nintendo - SNES / SFC (bsnes)"
+1
View File
@@ -1,6 +1,7 @@
emulator: Cemu
type: standalone
source: "https://github.com/cemu-project/Cemu"
logo: "https://raw.githubusercontent.com/cemu-project/Cemu/main/dist/linux/info.cemu.Cemu.png"
profiled_date: "2026-03-18"
core_version: "2.6"
display_name: "Cemu (Wii U)"
+1
View File
@@ -1,6 +1,7 @@
emulator: Citra / Lime3DS / Azahar
type: standalone + libretro
source: "https://github.com/azahar-emu/azahar"
logo: "https://raw.githubusercontent.com/wheremyfoodat/citra/master/dist/citra.svg"
profiled_date: "2026-03-18"
core_version: "Git"
display_name: "Nintendo - 3DS (Citra)"
+1
View File
@@ -1,6 +1,7 @@
emulator: Dolphin
type: standalone + libretro
source: "https://github.com/dolphin-emu/dolphin"
logo: "https://raw.githubusercontent.com/dolphin-emu/dolphin/master/Data/dolphin-emu.svg"
profiled_date: "2026-03-18"
core_version: "Git"
display_name: "Nintendo - GameCube / Wii (Dolphin)"
+1
View File
@@ -1,6 +1,7 @@
emulator: DOSBox Pure
type: libretro
source: "https://github.com/libretro/dosbox-pure"
logo: "https://raw.githubusercontent.com/schellingb/dosbox-pure/main/images/logo.png"
profiled_date: "2026-03-18"
core_version: "0.9.9"
display_name: "DOS (DOSBox-Pure)"
+1
View File
@@ -1,6 +1,7 @@
emulator: DuckStation
type: standalone
source: "https://github.com/stenzek/duckstation"
logo: "https://raw.githubusercontent.com/stenzek/duckstation/master/data/resources/images/duck.png"
profiled_date: "2026-03-18"
core_version: "v0.1"
display_name: "Sony - PlayStation (DuckStation)"
+1
View File
@@ -1,6 +1,7 @@
emulator: FinalBurn Neo
type: libretro
source: "https://github.com/libretro/FBNeo"
logo: "https://raw.githubusercontent.com/finalburnneo/FBNeo/master/projectfiles/xcode/Emulator/Assets.xcassets/AppIcon.appiconset/icon_512.png"
profiled_date: "2026-03-18"
core_version: "v1.0.0.03"
display_name: "Arcade (FinalBurn Neo)"
+1
View File
@@ -1,6 +1,7 @@
emulator: Flycast
type: standalone + libretro
source: "https://github.com/flyinghead/flycast"
logo: "https://raw.githubusercontent.com/flyinghead/flycast/master/shell/linux/flycast.png"
profiled_date: "2026-03-18"
core_version: "Git"
display_name: "Sega - Dreamcast/Naomi (Flycast)"
+1
View File
@@ -1,6 +1,7 @@
emulator: Hatari
type: libretro
source: "https://github.com/libretro/hatari"
logo: "https://raw.githubusercontent.com/hatari/hatari/main/share/icons/hicolor/scalable/apps/hatari.svg"
profiled_date: "2026-03-18"
core_version: "1.8"
display_name: "Atari - ST/STE/TT/Falcon (Hatari)"
+1
View File
@@ -1,6 +1,7 @@
emulator: HBMAME (Homebrew MAME)
type: libretro
source: "https://github.com/libretro/hbmame-libretro"
logo: "https://raw.githubusercontent.com/mamedev/mame/master/docs/source/images/MAMElogo.svg"
profiled_date: "2026-03-18"
core_version: "Git"
display_name: "Arcade (HBMAME)"
+1
View File
@@ -1,6 +1,7 @@
emulator: MAME 2003-Plus
type: libretro
source: "https://github.com/libretro/mame2003-plus-libretro"
logo: "https://raw.githubusercontent.com/mamedev/mame/master/docs/source/images/MAMElogo.svg"
profiled_date: "2026-03-18"
core_version: "2003-Plus"
display_name: "Arcade (MAME 2003-Plus)"
+1
View File
@@ -1,6 +1,7 @@
emulator: MAME 2010
type: libretro
source: "https://github.com/libretro/mame2010-libretro"
logo: "https://raw.githubusercontent.com/mamedev/mame/master/docs/source/images/MAMElogo.svg"
profiled_date: "2026-03-18"
core_version: "0.139"
display_name: "Arcade (MAME 2010)"
+1
View File
@@ -1,6 +1,7 @@
emulator: MAME 2016
type: libretro
source: "https://github.com/libretro/mame2016-libretro"
logo: "https://raw.githubusercontent.com/mamedev/mame/master/docs/source/images/MAMElogo.svg"
profiled_date: "2026-03-18"
core_version: "0.174"
display_name: "Arcade (MAME 2016)"
+1
View File
@@ -1,6 +1,7 @@
emulator: MelonDS
type: standalone + libretro
source: "https://github.com/melonDS-emu/melonDS"
logo: "https://raw.githubusercontent.com/melonDS-emu/melonDS/master/res/melon.svg"
profiled_date: "2026-03-18"
core_version: "Git"
display_name: "Nintendo - DS (melonDS)"
+1
View File
@@ -1,6 +1,7 @@
emulator: Mesen
type: libretro
source: "https://github.com/libretro/Mesen"
logo: "https://raw.githubusercontent.com/SourMesen/Mesen2/master/UI/Assets/Mesen.svg"
profiled_date: "2026-03-18"
core_version: "0.9.9"
display_name: "Nintendo - NES / Famicom (Mesen)"
+1
View File
@@ -1,6 +1,7 @@
emulator: mGBA
type: libretro
source: "https://github.com/libretro/mgba"
logo: "https://raw.githubusercontent.com/mgba-emu/mgba/master/res/mgba-256.png"
profiled_date: "2026-03-18"
core_version: "0.10-dev"
display_name: "Nintendo - Game Boy Advance (mGBA)"
+1
View File
@@ -1,6 +1,7 @@
emulator: Nestopia UE
type: libretro
source: "https://github.com/libretro/nestopia"
logo: "https://raw.githubusercontent.com/0ldsk00l/nestopia/master/icons/svg/nestopia.svg"
profiled_date: "2026-03-18"
core_version: "1.53.1"
display_name: "Nintendo - NES / Famicom (Nestopia)"
+1
View File
@@ -5,6 +5,7 @@
emulator: PCSX2
type: standalone
source: "https://github.com/PCSX2/pcsx2"
logo: "https://raw.githubusercontent.com/PCSX2/pcsx2/master/pcsx2-qt/resources/icons/PCSX2logo.svg"
profiled_date: "2026-03-18"
core_version: "Git"
display_name: "Sony - PlayStation 2 (LRPS2)"
+1
View File
@@ -5,6 +5,7 @@
emulator: PPSSPP
type: standalone
source: "https://github.com/hrydgard/ppsspp"
logo: "https://raw.githubusercontent.com/hrydgard/ppsspp/master/icons/icon-512.svg"
profiled_date: "2026-03-18"
core_version: "Git"
display_name: "Sony - PlayStation Portable (PPSSPP)"
+1
View File
@@ -5,6 +5,7 @@
emulator: RPCS3
type: standalone
source: "https://github.com/RPCS3/rpcs3"
logo: "https://raw.githubusercontent.com/RPCS3/rpcs3/master/rpcs3/rpcs3.svg"
profiled_date: "2026-03-18"
core_version: "0.0.35"
display_name: "RPCS3 (PS3)"
+1
View File
@@ -1,6 +1,7 @@
emulator: ScummVM
type: libretro
source: "https://github.com/libretro/scummvm"
logo: "https://raw.githubusercontent.com/scummvm/scummvm/master/icons/scummvm.svg"
profiled_date: "2026-03-18"
core_version: "2.8.0git"
display_name: "ScummVM"
+1
View File
@@ -1,6 +1,7 @@
emulator: snes9x
type: libretro
source: "https://github.com/libretro/snes9x"
logo: "https://raw.githubusercontent.com/snes9xgit/snes9x/master/gtk/data/snes9x.svg"
profiled_date: "2026-03-18"
core_version: "1.61"
display_name: "Nintendo - SNES / SFC (Snes9x)"
+1
View File
@@ -12,6 +12,7 @@ cores:
- vice_xcbm5x0
- vice_xscpu64
source: "https://github.com/libretro/vice-libretro"
logo: "https://raw.githubusercontent.com/VICE-Team/svn-mirror/main/vice/data/common/vice-logo-black.svg"
profiled_date: "2026-03-18"
core_version: "3.9"
display_name: "Commodore - C64 (VICE x64, fast)"
+1
View File
@@ -5,6 +5,7 @@
emulator: Vita3K
type: standalone
source: "https://github.com/Vita3K/Vita3K"
logo: "https://raw.githubusercontent.com/Vita3K/Vita3K/master/data/image/icon.png"
profiled_date: "2026-03-18"
core_version: "0.2.1"
display_name: "Vita3K (PS Vita)"
+1
View File
@@ -1,6 +1,7 @@
emulator: Xemu
type: standalone
source: "https://github.com/xemu-project/xemu"
logo: "https://raw.githubusercontent.com/xemu-project/xemu/master/data/xemu_64x64.png"
profiled_date: "2026-03-18"
core_version: "0.8.x"
display_name: "xemu (Xbox)"
+302
View File
@@ -0,0 +1,302 @@
site_name: RetroBIOS
site_url: https://abdess.github.io/retrobios/
repo_url: https://github.com/Abdess/retrobios
repo_name: Abdess/retrobios
theme:
name: material
palette:
- media: (prefers-color-scheme)
toggle:
icon: material/brightness-auto
name: Switch to light mode
- media: '(prefers-color-scheme: light)'
scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
- media: '(prefers-color-scheme: dark)'
scheme: slate
toggle:
icon: material/brightness-4
name: Switch to auto
font: false
features:
- navigation.tabs
- navigation.sections
- navigation.top
- search.suggest
- search.highlight
- content.tabs.link
- toc.follow
markdown_extensions:
- tables
- admonition
- attr_list
- toc:
permalink: true
plugins:
- search
nav:
- Home: index.md
- Platforms:
- Overview: platforms/index.md
- Batocera: platforms/batocera.md
- EmuDeck: platforms/emudeck.md
- Lakka: platforms/lakka.md
- Recalbox: platforms/recalbox.md
- RetroArch: platforms/retroarch.md
- RetroBat: platforms/retrobat.md
- RetroPie: platforms/retropie.md
- Systems:
- Overview: systems/index.md
- 3DO Company: systems/3do-company.md
- APF: systems/apf.md
- Acorn: systems/acorn.md
- Amstrad: systems/amstrad.md
- Apple: systems/apple.md
- Arcade: systems/arcade.md
- Atari: systems/atari.md
- Bally: systems/bally.md
- Bandai: systems/bandai.md
- Bit Corporation: systems/bit-corporation.md
- Camputers: systems/camputers.md
- Casio: systems/casio.md
- Coleco: systems/coleco.md
- Commodore: systems/commodore.md
- DOS: systems/dos.md
- Dinothawr: systems/dinothawr.md
- Dragon: systems/dragon.md
- EACA: systems/eaca.md
- Elektronika: systems/elektronika.md
- Enterprise: systems/enterprise.md
- Entex: systems/entex.md
- Epoch: systems/epoch.md
- Fairchild: systems/fairchild.md
- Fujitsu: systems/fujitsu.md
- Funtech: systems/funtech.md
- GCE: systems/gce.md
- Galaksija: systems/galaksija.md
- GamePark: systems/gamepark.md
- Grundy: systems/grundy.md
- Hartung: systems/hartung.md
- Id Software: systems/id-software.md
- Infocom: systems/infocom.md
- Java: systems/java.md
- Magnavox: systems/magnavox.md
- Mattel: systems/mattel.md
- Microsoft: systems/microsoft.md
- NEC: systems/nec.md
- Nintendo: systems/nintendo.md
- Nokia: systems/nokia.md
- Oric: systems/oric.md
- Palm: systems/palm.md
- Philips: systems/philips.md
- Pioneer: systems/pioneer.md
- RPG Maker: systems/rpg-maker.md
- SNK: systems/snk.md
- ScummVM: systems/scummvm.md
- Sega: systems/sega.md
- Sharp: systems/sharp.md
- Sinclair: systems/sinclair.md
- Sony: systems/sony.md
- Synertek: systems/synertek.md
- Tandy: systems/tandy.md
- Texas Instruments: systems/texas-instruments.md
- Tiger: systems/tiger.md
- Tomy: systems/tomy.md
- VTech: systems/vtech.md
- Videoton: systems/videoton.md
- Vircon: systems/vircon.md
- ZC: systems/zc.md
- xrick: systems/xrick.md
- Emulators:
- Overview: emulators/index.md
- '2048': emulators/2048.md
- 3DEngine: emulators/3dengine.md
- EightyOne: emulators/81.md
- a5200: emulators/a5200.md
- amiarcadia: emulators/amiarcadia.md
- Anarch: emulators/anarch.md
- AppleWin: emulators/applewin.md
- Ardens: emulators/ardens.md
- Arduous: emulators/arduous.md
- Atari800: emulators/atari800.md
- b2: emulators/b2.md
- Beetle Lynx (Mednafen Lynx): emulators/beetle_lynx.md
- Beetle NGP (Mednafen Neo Geo Pocket): emulators/beetle_ngp.md
- Beetle PCE (Mednafen PCE): emulators/beetle_pce.md
- Beetle PC-FX (Mednafen): emulators/beetle_pcfx.md
- Beetle PSX (Mednafen PSX): emulators/beetle_psx.md
- Beetle Saturn (Mednafen): emulators/beetle_saturn.md
- Beetle VB (Mednafen Virtual Boy): emulators/beetle_vb.md
- Beetle WonderSwan (Mednafen WonderSwan): emulators/beetle_wswan.md
- BennuGD: emulators/bennugd.md
- bk-emulator: emulators/bk.md
- BlastEm: emulators/blastem.md
- blueMSX: emulators/bluemsx.md
- bnes: emulators/bnes.md
- boom3: emulators/boom3.md
- Boytacean: emulators/boytacean.md
- bsnes: emulators/bsnes.md
- Cannonball: emulators/cannonball.md
- Caprice32: emulators/cap32.md
- Cemu: emulators/cemu.md
- ChaiLove: emulators/chailove.md
- Citra / Lime3DS / Azahar: emulators/citra.md
- ClownMDEmu: emulators/clownmdemu.md
- Craft: emulators/craft.md
- CrocoDS: emulators/crocods.md
- Cruzes: emulators/cruzes.md
- Daphne: emulators/daphne.md
- DeSmuME: emulators/desmume.md
- DICE: emulators/dice.md
- Dinothawr: emulators/dinothawr.md
- DirectXBox: emulators/directxbox.md
- Dolphin: emulators/dolphin.md
- Dolphin Launcher: emulators/dolphin_launcher.md
- DOSBox-core: emulators/dosbox_core.md
- DOSBox Pure: emulators/dosbox_pure.md
- DoubleCherryGB: emulators/doublecherrygb.md
- doukutsu-rs: emulators/doukutsu_rs.md
- DuckStation: emulators/duckstation.md
- EasyRPG Player: emulators/easyrpg.md
- ECWolf: emulators/ecwolf.md
- EmuSCV: emulators/emuscv.md
- emux (CHIP-8): emulators/emux_chip8.md
- ep128emu-core: emulators/ep128emu.md
- FAKE-08: emulators/fake08.md
- FinalBurn Neo: emulators/fbneo.md
- FCEUmm: emulators/fceumm.md
- FFmpeg: emulators/ffmpeg.md
- fixGB: emulators/fixgb.md
- Flycast: emulators/flycast.md
- fMSX: emulators/fmsx.md
- FreeChaF: emulators/freechaf.md
- FreeIntv: emulators/freeintv.md
- FreeIntv (Touchscreen Overlay): emulators/freeintv_ts_overlay.md
- FreeJ2ME: emulators/freej2me.md
- Frodo: emulators/frodo.md
- Fuse: emulators/fuse.md
- galaksija: emulators/galaksija.md
- GAM4980: emulators/gam4980.md
- Gambatte: emulators/gambatte.md
- Gearcoleco: emulators/gearcoleco.md
- Geargrafx: emulators/geargrafx.md
- Gearlynx: emulators/gearlynx.md
- Gearsystem: emulators/gearsystem.md
- Genesis Plus GX: emulators/genesis_plus_gx.md
- Geolith: emulators/geolith.md
- Game Music Emu: emulators/gme.md
- Gong: emulators/gong.md
- gpSP: emulators/gpsp.md
- Game & Watch: emulators/gw.md
- Handy: emulators/handy.md
- Hatari: emulators/hatari.md
- HBMAME (Homebrew MAME): emulators/hbmame.md
- Holani: emulators/holani.md
- Image Viewer: emulators/imageviewer.md
- Ishiiruka: emulators/ishiiruka.md
- JAXE: emulators/jaxe.md
- JollyCV: emulators/jollycv.md
- Jump 'n Bump: emulators/jumpnbump.md
- Kronos: emulators/kronos.md
- LowRes NX: emulators/lowresnx.md
- Lutro: emulators/lutro.md
- M2000: emulators/m2000.md
- MAME 2003-Plus: emulators/mame2003_plus.md
- MAME 2010: emulators/mame2010.md
- MAME 2016: emulators/mame2016.md
- MCSoftserve: emulators/mcsoftserve.md
- MelonDS: emulators/melonds.md
- Mesen: emulators/mesen.md
- Meteor GBA: emulators/meteor.md
- mGBA: emulators/mgba.md
- Mini vMac: emulators/minivmac.md
- mkxp-z: emulators/mkxp_z.md
- MojoZork: emulators/mojozork.md
- Moonlight: emulators/moonlight.md
- mpv: emulators/mpv.md
- Mr.Boom: emulators/mrboom.md
- Mu: emulators/mu.md
- Mupen64Plus-Next: emulators/mupen64plus.md
- NeoCD: emulators/neocd.md
- Nestopia UE: emulators/nestopia.md
- NooDS: emulators/noods.md
- NP2kai: emulators/np2kai.md
- Numero: emulators/numero.md
- NXEngine: emulators/nxengine.md
- O2EM: emulators/o2em.md
- Oberon: emulators/oberon.md
- ONScripter: emulators/onscripter.md
- ONScripter Yuri: emulators/onsyuri.md
- OpenLara: emulators/openlara.md
- OpenTyrian: emulators/opentyrian.md
- Opera (4DO): emulators/opera.md
- Panda3DS: emulators/panda3ds.md
- Pascal Pong: emulators/pascal_pong.md
- PCem: emulators/pcem.md
- PCSX2: emulators/pcsx2.md
- PCSX-ReARMed: emulators/pcsx_rearmed.md
- PD777: emulators/pd777.md
- PicoDrive: emulators/picodrive.md
- Play!: emulators/play.md
- PocketCDG: emulators/pocketcdg.md
- PokeMini: emulators/pokemini.md
- PPSSPP: emulators/ppsspp.md
- PrBoom: emulators/prboom.md
- ProSystem: emulators/prosystem.md
- PUAE (P-UAE): emulators/puae.md
- PuzzleScript: emulators/puzzlescript.md
- px68k: emulators/px68k.md
- QEMU: emulators/qemu.md
- QUASI88: emulators/quasi88.md
- RACE (Neo Geo Pocket): emulators/race.md
- Redbook: emulators/redbook.md
- REminiscence: emulators/reminiscence.md
- RemoteJoy: emulators/remotejoy.md
- Retro8: emulators/retro8.md
- RetroDream: emulators/retrodream.md
- ROM Cleaner: emulators/romcleaner.md
- RPCS3: emulators/rpcs3.md
- Rustation: emulators/rustation.md
- RVVM: emulators/rvvm.md
- SAME CDi: emulators/same_cdi.md
- SameBoy: emulators/sameboy.md
- ScummVM: emulators/scummvm.md
- SDLPAL: emulators/sdlpal.md
- SimCoupe: emulators/simcp.md
- SMS Plus GX: emulators/smsplus.md
- snes9x: emulators/snes9x.md
- SquirrelJME: emulators/squirreljme.md
- Stella: emulators/stella.md
- Stone Soup: emulators/stonesoup.md
- Super Bros War: emulators/superbroswar.md
- Syobon Action: emulators/syobonaction.md
- TamaLIBretro: emulators/tamalibretro.md
- TempGBA: emulators/tempgba.md
- TGB Dual: emulators/tgbdual.md
- Theodore: emulators/theodore.md
- The Powder Toy: emulators/thepowdertoy.md
- TIC-80: emulators/tic80.md
- TyrQuake: emulators/tyrquake.md
- MicroW8: emulators/uw8.md
- UXN: emulators/uxn.md
- uzem: emulators/uzem.md
- VaporSpec: emulators/vaporspec.md
- VBA-Next: emulators/vba_next.md
- vecx: emulators/vecx.md
- VeMUlator: emulators/vemulator.md
- VICE: emulators/vice.md
- Vircon32: emulators/vircon32.md
- Virtual Jaguar: emulators/virtualjaguar.md
- VirtualXT: emulators/virtualxt.md
- Vita3K: emulators/vita3k.md
- vitaQuakeII: emulators/vitaquake2.md
- vitaQuakeIII: emulators/vitaquake3.md
- WASM-4: emulators/wasm4.md
- X Millennium: emulators/x1.md
- x64sdl: emulators/x64sdl.md
- Xemu: emulators/xemu.md
- XRick: emulators/xrick.md
- Gap Analysis: gaps.md
- Contributing: contributing.md
+10
View File
@@ -10,6 +10,7 @@ platforms:
retroarch:
config: retroarch.yml
status: active
logo: "https://raw.githubusercontent.com/libretro/RetroArch/master/media/retroarch-vector_invader-only.svg"
scraper: libretro
source_url: "https://raw.githubusercontent.com/libretro/libretro-database/master/dat/System.dat"
source_format: clrmamepro_dat
@@ -20,33 +21,40 @@ platforms:
batocera:
config: batocera.yml
status: active
logo: "https://raw.githubusercontent.com/batocera-linux/batocera-emulationstation/master/resources/splash_batocera.svg"
scraper: batocera
source_url: "https://raw.githubusercontent.com/batocera-linux/batocera.linux/master/package/batocera/core/batocera-scripts/scripts/batocera-systems"
source_format: python_dict
hash_type: md5
schedule: weekly
emulators: [flycast, dolphin, pcsx2, duckstation, rpcs3, ppsspp, beetle_psx, beetle_saturn, genesis_plus_gx, picodrive, fbneo, puae, hatari, fuse, opera, bluemsx, fmsx, np2kai, quasi88]
recalbox:
config: recalbox.yml
status: active
logo: "https://raw.githubusercontent.com/homarr-labs/dashboard-icons/main/svg/recalbox.svg"
scraper: recalbox
source_url: "https://gitlab.com/recalbox/recalbox/-/raw/master/board/recalbox/fsoverlay/recalbox/share_init/system/.emulationstation/es_bios.xml"
source_format: xml
hash_type: md5
schedule: monthly
emulators: [flycast, dolphin, pcsx2, beetle_psx, beetle_saturn, genesis_plus_gx, picodrive, fbneo, puae, hatari, opera, bluemsx, fmsx]
retrobat:
config: retrobat.yml
status: active
logo: "https://raw.githubusercontent.com/RetroBat-Official/retrobat/main/system/resources/retrobat_logo_notext.png"
scraper: retrobat
source_url: "https://raw.githubusercontent.com/RetroBat-Official/emulatorlauncher/master/batocera-systems/Resources/batocera-systems.json"
source_format: json
hash_type: md5
schedule: weekly
emulators: [duckstation, pcsx2, dolphin, rpcs3, ppsspp, cemu, xemu, flycast, beetle_psx, beetle_saturn, genesis_plus_gx, puae, opera]
emudeck:
config: emudeck.yml
status: active
logo: "https://raw.githubusercontent.com/dragoonDorise/EmuDeck/main/icons/EmuDeck.png"
scraper: emudeck
source_url: "https://raw.githubusercontent.com/dragoonDorise/EmuDeck/main/functions/checkBIOS.sh"
source_wiki: "https://raw.githubusercontent.com/EmuDeck/emudeck.github.io/main/docs/tables/"
@@ -60,6 +68,7 @@ platforms:
lakka:
config: lakka.yml
status: active
logo: "https://raw.githubusercontent.com/libretro/retroarch-assets/master/src/xmb/flatui/lakka.svg"
scraper: libretro
inherits_from: retroarch
schedule: weekly
@@ -67,5 +76,6 @@ platforms:
retropie:
config: retropie.yml
status: archived # Last release: v4.8 (March 2022) - no update in 4 years
logo: "https://avatars.githubusercontent.com/u/11378204"
scraper: null
schedule: null
+2 -3
View File
@@ -458,9 +458,8 @@ systems:
- name: Dinothawr.zip
destination: Dinothawr.zip
required: true
sha1: 693d8bb4d992c645e6413a57195acf4eca2f5a2e
md5: a2e891e330d146c4046c2b622fc31462
crc32: 683ed4ad
sha1: eadb966430454b40a17387acc7302ff1683cc9f2
md5: b8e3f8e88dc8164f8b4b60aa2add9107
size: 5763199
dos:
files:
+110 -4
View File
@@ -45,12 +45,16 @@ def load_database(db_path: str) -> dict:
return json.load(f)
def md5sum(filepath: str | Path) -> str:
"""Compute MD5 of a file - matches Batocera's md5sum()."""
def md5sum(source: str | Path | object) -> str:
"""Compute MD5 of a file path or file-like object - matches Batocera's md5sum()."""
h = hashlib.md5()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
if hasattr(source, "read"):
for chunk in iter(lambda: source.read(65536), b""):
h.update(chunk)
else:
with open(source, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
@@ -117,6 +121,108 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -
return config
def resolve_local_file(
file_entry: dict,
db: dict,
zip_contents: dict | None = None,
) -> tuple[str | None, str]:
"""Resolve a BIOS file to its local path using database.json.
Single source of truth for file resolution, used by both verify.py
and generate_pack.py. Does NOT handle storage tiers (external/user_provided)
or release assets - callers handle those.
Returns (local_path, status) where status is one of:
exact, zip_exact, hash_mismatch, not_found.
"""
sha1 = file_entry.get("sha1")
md5_raw = file_entry.get("md5", "")
name = file_entry.get("name", "")
zipped_file = file_entry.get("zipped_file")
md5_list = [m.strip().lower() for m in md5_raw.split(",") if m.strip()] if md5_raw else []
files_db = db.get("files", {})
by_md5 = db.get("indexes", {}).get("by_md5", {})
by_name = db.get("indexes", {}).get("by_name", {})
# 1. SHA1 exact match
if sha1 and sha1 in files_db:
path = files_db[sha1]["path"]
if os.path.exists(path):
return path, "exact"
# 2. MD5 direct lookup (skip for zipped_file: md5 is inner ROM, not container)
if md5_list and not zipped_file:
for md5_candidate in md5_list:
sha1_match = by_md5.get(md5_candidate)
if sha1_match and sha1_match in files_db:
path = files_db[sha1_match]["path"]
if os.path.exists(path):
return path, "md5_exact"
if len(md5_candidate) < 32:
for db_md5, db_sha1 in by_md5.items():
if db_md5.startswith(md5_candidate) and db_sha1 in files_db:
path = files_db[db_sha1]["path"]
if os.path.exists(path):
return path, "md5_exact"
# 3. No MD5 = any file with that name (existence check)
if not md5_list:
candidates = []
for match_sha1 in by_name.get(name, []):
if match_sha1 in files_db:
path = files_db[match_sha1]["path"]
if os.path.exists(path):
candidates.append(path)
if candidates:
if zipped_file:
candidates = [p for p in candidates if ".zip" in os.path.basename(p)]
primary = [p for p in candidates if "/.variants/" not in p]
if primary or candidates:
return (primary[0] if primary else candidates[0]), "exact"
# 5. Name fallback with md5_composite + direct MD5 per candidate
md5_set = set(md5_list)
candidates = []
for match_sha1 in by_name.get(name, []):
if match_sha1 in files_db:
entry = files_db[match_sha1]
path = entry["path"]
if os.path.exists(path):
candidates.append((path, entry.get("md5", "")))
if candidates:
if zipped_file:
candidates = [(p, m) for p, m in candidates if ".zip" in os.path.basename(p)]
if md5_set:
for path, db_md5 in candidates:
if ".zip" in os.path.basename(path):
try:
composite = md5_composite(path).lower()
if composite in md5_set:
return path, "exact"
except (zipfile.BadZipFile, OSError):
pass
if db_md5.lower() in md5_set:
return path, "exact"
primary = [p for p, _ in candidates if "/.variants/" not in p]
return (primary[0] if primary else candidates[0][0]), "hash_mismatch"
# 6. zipped_file content match via pre-built index (last resort:
# matches inner ROM MD5 across ALL ZIPs in the repo, so only use
# when name-based resolution failed entirely)
if zipped_file and md5_list and zip_contents:
for md5_candidate in md5_list:
if md5_candidate in zip_contents:
zip_sha1 = zip_contents[md5_candidate]
if zip_sha1 in files_db:
path = files_db[zip_sha1]["path"]
if os.path.exists(path):
return path, "zip_exact"
return None, "not_found"
def safe_extract_zip(zip_path: str, dest_dir: str) -> None:
"""Extract a ZIP file safely, preventing zip-slip path traversal."""
dest = os.path.realpath(dest_dir)
+20 -5
View File
@@ -25,7 +25,7 @@ except ImportError:
sys.exit(1)
sys.path.insert(0, os.path.dirname(__file__))
from common import load_database
from common import load_database, load_platform_config
DEFAULT_EMULATORS_DIR = "emulators"
DEFAULT_PLATFORMS_DIR = "platforms"
@@ -52,8 +52,7 @@ def load_platform_files(platforms_dir: str) -> dict[str, set[str]]:
for f in sorted(Path(platforms_dir).glob("*.yml")):
if f.name.startswith("_"):
continue
with open(f) as fh:
config = yaml.safe_load(fh) or {}
config = load_platform_config(f.stem, platforms_dir)
for sys_id, system in config.get("systems", {}).items():
for fe in system.get("files", []):
name = fe.get("name", "")
@@ -62,6 +61,22 @@ def load_platform_files(platforms_dir: str) -> dict[str, set[str]]:
return declared
def _find_in_repo(fname: str, by_name: dict[str, list], by_name_lower: dict[str, str]) -> bool:
if fname in by_name:
return True
basename = fname.rsplit("/", 1)[-1] if "/" in fname else None
if basename and basename in by_name:
return True
key = fname.lower()
if key in by_name_lower:
return True
if basename:
key = basename.lower()
if key in by_name_lower:
return True
return False
def cross_reference(
profiles: dict[str, dict],
declared: dict[str, set[str]],
@@ -73,13 +88,13 @@ def cross_reference(
and coverage stats.
"""
by_name = db.get("indexes", {}).get("by_name", {})
by_name_lower = {k.lower(): k for k in by_name}
report = {}
for emu_name, profile in profiles.items():
emu_files = profile.get("files", [])
systems = profile.get("systems", [])
# Collect all platform-declared files for this emulator's systems
platform_names = set()
for sys_id in systems:
platform_names.update(declared.get(sys_id, set()))
@@ -92,7 +107,7 @@ def cross_reference(
continue
in_platform = fname in platform_names
in_repo = fname in by_name
in_repo = _find_in_repo(fname, by_name, by_name_lower)
entry = {
"name": fname,
+4 -11
View File
@@ -13,7 +13,6 @@ Usage:
from __future__ import annotations
import argparse
import hashlib
import json
import os
import sys
@@ -23,7 +22,7 @@ import zipfile
from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from common import safe_extract_zip
from common import compute_hashes, safe_extract_zip
GITHUB_API = "https://api.github.com"
REPO = "Abdess/retrobios"
@@ -135,21 +134,15 @@ def verify_files(platform: str, dest_dir: str, release: dict):
found = False
for local_file in dest.rglob(name):
if local_file.is_file():
h = hashlib.sha1()
with open(local_file, "rb") as f:
while True:
chunk = f.read(65536)
if not chunk:
break
h.update(chunk)
local_sha1 = compute_hashes(local_file)["sha1"]
if h.hexdigest() == sha1:
if local_sha1 == sha1:
verified += 1
found = True
break
else:
mismatched += 1
print(f" MISMATCH: {name} (expected {sha1[:12]}..., got {h.hexdigest()[:12]}...)")
print(f" MISMATCH: {name} (expected {sha1[:12]}..., got {local_sha1[:12]}...)")
found = True
break
+135 -78
View File
@@ -24,7 +24,7 @@ import zipfile
from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from common import load_database, load_platform_config
from common import compute_hashes, load_database, load_platform_config, md5_composite, resolve_local_file
try:
import yaml
@@ -44,17 +44,12 @@ MAX_ENTRY_SIZE = 512 * 1024 * 1024 # 512MB
def _verify_file_hash(path: str, expected_sha1: str = "",
expected_md5: str = "") -> bool:
"""Compute and compare hash of a local file."""
if not expected_sha1 and not expected_md5:
return True
h = hashlib.sha1() if expected_sha1 else hashlib.md5()
with open(path, "rb") as f:
while True:
chunk = f.read(65536)
if not chunk:
break
h.update(chunk)
return h.hexdigest() == (expected_sha1 or expected_md5)
hashes = compute_hashes(path)
if expected_sha1:
return hashes["sha1"] == expected_sha1
return hashes["md5"] == expected_md5
def fetch_large_file(name: str, dest_dir: str = ".cache/large",
@@ -100,10 +95,10 @@ def _sanitize_path(raw: str) -> str:
def resolve_file(file_entry: dict, db: dict, bios_dir: str,
zip_contents: dict | None = None) -> tuple[str | None, str]:
"""Resolve a BIOS file to its local path using database.json.
"""Resolve a BIOS file with storage tiers and release asset fallback.
Returns (local_path, status) where status is one of:
exact, zip_exact, hash_mismatch, external, user_provided, not_found.
Wraps common.resolve_local_file() with pack-specific logic for
storage tiers (external/user_provided) and large file release assets.
"""
storage = file_entry.get("storage", "embedded")
if storage == "user_provided":
@@ -111,61 +106,20 @@ def resolve_file(file_entry: dict, db: dict, bios_dir: str,
if storage == "external":
return None, "external"
sha1 = file_entry.get("sha1")
md5 = file_entry.get("md5")
path, status = resolve_local_file(file_entry, db, zip_contents)
if path:
return path, status
# Last resort: large files from GitHub release assets
name = file_entry.get("name", "")
zipped_file = file_entry.get("zipped_file")
if sha1 and sha1 in db.get("files", {}):
local_path = db["files"][sha1]["path"]
if os.path.exists(local_path):
return local_path, "exact"
if md5:
sha1_from_md5 = db.get("indexes", {}).get("by_md5", {}).get(md5)
if sha1_from_md5 and sha1_from_md5 in db["files"]:
local_path = db["files"][sha1_from_md5]["path"]
if os.path.exists(local_path):
return local_path, "exact"
# Truncated MD5 match (batocera-systems bug: 29 chars instead of 32)
if len(md5) < 32:
for db_md5, db_sha1 in db.get("indexes", {}).get("by_md5", {}).items():
if db_md5.startswith(md5) and db_sha1 in db["files"]:
local_path = db["files"][db_sha1]["path"]
if os.path.exists(local_path):
return local_path, "exact"
if zipped_file and md5 and zip_contents:
if md5 in zip_contents:
zip_sha1 = zip_contents[md5]
if zip_sha1 in db["files"]:
local_path = db["files"][zip_sha1]["path"]
if os.path.exists(local_path):
return local_path, "zip_exact"
# Release assets override local files (authoritative large files)
cached = fetch_large_file(name, expected_sha1=sha1 or "", expected_md5=md5 or "")
sha1 = file_entry.get("sha1")
md5_raw = file_entry.get("md5", "")
md5_list = [m.strip().lower() for m in md5_raw.split(",") if m.strip()] if md5_raw else []
first_md5 = md5_list[0] if md5_list else ""
cached = fetch_large_file(name, expected_sha1=sha1 or "", expected_md5=first_md5)
if cached:
return cached, "release_asset"
# No MD5 specified = any local file with that name is acceptable
if not md5:
name_matches = db.get("indexes", {}).get("by_name", {}).get(name, [])
for match_sha1 in name_matches:
if match_sha1 in db["files"]:
local_path = db["files"][match_sha1]["path"]
if os.path.exists(local_path):
return local_path, "exact"
# Name fallback (hash mismatch)
name_matches = db.get("indexes", {}).get("by_name", {}).get(name, [])
for match_sha1 in name_matches:
if match_sha1 in db["files"]:
local_path = db["files"][match_sha1]["path"]
if os.path.exists(local_path):
return local_path, "hash_mismatch"
return None, "not_found"
@@ -235,27 +189,100 @@ def download_external(file_entry: dict, dest_path: str) -> bool:
return True
def _load_emulator_extras(
platform_name: str,
platforms_dir: str,
emulators_dir: str,
seen: dict,
base_dest: str,
config: dict | None = None,
) -> list[dict]:
"""Load extra files from emulator profiles not already in the platform pack.
Collects emulators from two sources:
1. Auto-detected from platform config "core:" fields per system
2. Manual "emulators:" list in _registry.yml
"""
emu_names = set()
# Source 1: auto-detect from platform config core: fields
if config:
for system in config.get("systems", {}).values():
core = system.get("core", "")
if core:
emu_names.add(core)
# Source 2: manual list from _registry.yml
registry_path = os.path.join(platforms_dir, "_registry.yml")
if os.path.exists(registry_path):
with open(registry_path) as f:
registry = yaml.safe_load(f) or {}
platform_cfg = registry.get("platforms", {}).get(platform_name, {})
for name in platform_cfg.get("emulators", []):
emu_names.add(name)
if not emu_names:
return []
extras = []
emu_dir = Path(emulators_dir)
for emu_name in emu_names:
emu_path = emu_dir / f"{emu_name}.yml"
if not emu_path.exists():
continue
with open(emu_path) as f:
profile = yaml.safe_load(f) or {}
# Follow alias
if profile.get("alias_of"):
parent = emu_dir / f"{profile['alias_of']}.yml"
if parent.exists():
with open(parent) as f:
profile = yaml.safe_load(f) or {}
for fe in profile.get("files", []):
name = fe.get("name", "")
if not name or name.startswith("<"):
continue
dest = fe.get("destination", name)
full_dest = f"{base_dest}/{dest}" if base_dest else dest
if full_dest in seen:
continue
extras.append({
"name": name,
"sha1": fe.get("sha1"),
"md5": fe.get("md5"),
"destination": dest,
"required": fe.get("required", False),
"source_emulator": emu_name,
})
return extras
def generate_pack(
platform_name: str,
platforms_dir: str,
db_path: str,
db: dict,
bios_dir: str,
output_dir: str,
include_extras: bool = False,
emulators_dir: str = "emulators",
zip_contents: dict | None = None,
) -> str | None:
"""Generate a ZIP pack for a platform.
Returns the path to the generated ZIP, or None on failure.
"""
config = load_platform_config(platform_name, platforms_dir)
db = load_database(db_path)
zip_contents = build_zip_contents_index(db)
if zip_contents is None:
zip_contents = {}
verification_mode = config.get("verification_mode", "existence")
platform_display = config.get("platform", platform_name)
base_dest = config.get("base_destination", "")
zip_name = f"{platform_display.replace(' ', '_')}_BIOS_Pack.zip"
suffix = "Complete_Pack" if include_extras else "BIOS_Pack"
zip_name = f"{platform_display.replace(' ', '_')}_{suffix}.zip"
zip_path = os.path.join(output_dir, zip_name)
os.makedirs(output_dir, exist_ok=True)
@@ -263,7 +290,7 @@ def generate_pack(
missing_files = []
untested_files = []
user_provided = []
seen_destinations = {}
seen_destinations = set()
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for sys_id, system in sorted(config.get("systems", {}).items()):
@@ -279,7 +306,7 @@ def generate_pack(
dedup_key = full_dest
if dedup_key in seen_destinations:
continue
seen_destinations[dedup_key] = file_entry.get("sha1") or file_entry.get("md5") or ""
seen_destinations.add(dedup_key)
storage = file_entry.get("storage", "embedded")
@@ -295,8 +322,8 @@ def generate_pack(
local_path, status = resolve_file(file_entry, db, bios_dir, zip_contents)
if status == "external":
suffix = os.path.splitext(file_entry["name"])[1] or ""
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
file_ext = os.path.splitext(file_entry["name"])[1] or ""
with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp:
tmp_path = tmp.name
try:
@@ -329,6 +356,30 @@ def generate_pack(
zf.write(local_path, full_dest)
total_files += 1
# Tier 2: emulator extras
extra_count = 0
if include_extras:
extras = _load_emulator_extras(
platform_name, platforms_dir, emulators_dir,
seen_destinations, base_dest, config=config,
)
for fe in extras:
dest = _sanitize_path(fe.get("destination", fe["name"]))
if not dest:
continue
full_dest = f"{base_dest}/{dest}" if base_dest else dest
if full_dest in seen_destinations:
continue
local_path, status = resolve_file(fe, db, bios_dir, zip_contents)
if status in ("not_found", "external", "user_provided"):
continue
zf.write(local_path, full_dest)
seen_destinations.add(full_dest)
extra_count += 1
total_files += 1
if missing_files:
print(f" Missing ({len(missing_files)}): {', '.join(missing_files[:10])}")
if len(missing_files) > 10:
@@ -342,13 +393,12 @@ def generate_pack(
if user_provided:
print(f" User-provided ({len(user_provided)}): {', '.join(user_provided)}")
extras_msg = f" + {extra_count} emulator extras" if extra_count else ""
if verification_mode == "existence":
# RetroArch-family: only existence matters
print(f" Generated {zip_path}: {total_files} files ({total_files} present, {len(missing_files)} missing) [verification: existence]")
print(f" Generated {zip_path}: {total_files} files ({total_files - extra_count} platform{extras_msg}, {len(missing_files)} missing) [verification: existence]")
else:
# Batocera-family: hash verification matters
verified = total_files - len(untested_files)
print(f" Generated {zip_path}: {total_files} files ({verified} verified, {len(untested_files)} untested, {len(missing_files)} missing) [verification: {verification_mode}]")
print(f" Generated {zip_path}: {total_files} files ({verified} verified{extras_msg}, {len(untested_files)} untested, {len(missing_files)} missing) [verification: {verification_mode}]")
return zip_path
@@ -407,6 +457,9 @@ def main():
parser.error("Specify --platform or --all")
return
db = load_database(args.db)
zip_contents = build_zip_contents_index(db)
groups = _group_identical_platforms(platforms, args.platforms_dir)
for group_platforms, representative in groups:
@@ -418,7 +471,11 @@ def main():
print(f"\nGenerating pack for {representative}...")
try:
zip_path = generate_pack(representative, args.platforms_dir, args.db, args.bios_dir, args.output_dir)
zip_path = generate_pack(
representative, args.platforms_dir, db, args.bios_dir, args.output_dir,
include_extras=args.include_extras, emulators_dir=args.emulators_dir,
zip_contents=zip_contents,
)
if zip_path and len(group_platforms) > 1:
# Rename ZIP to include all platform names
names = [load_platform_config(p, args.platforms_dir).get("platform", p) for p in group_platforms]
+96 -379
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
"""Generate README.md and CONTRIBUTING.md from database.json and platform configs.
"""Generate slim README.md from database.json and platform configs.
Detailed documentation lives on the MkDocs site (abdess.github.io/retrobios/).
This script produces a concise landing page with download links and coverage.
Usage:
python scripts/generate_readme.py [--db database.json] [--platforms-dir platforms/]
@@ -16,428 +19,142 @@ from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from common import load_database, load_platform_config
from verify import verify_platform
try:
import yaml
except ImportError:
print("Error: PyYAML required (pip install pyyaml)", file=sys.stderr)
sys.exit(1)
def load_platform_configs(platforms_dir: str) -> dict:
"""Load all platform configs with inheritance resolved."""
configs = {}
for f in sorted(Path(platforms_dir).glob("*.yml")):
if f.name.startswith("_"):
continue
try:
config = load_platform_config(f.stem, platforms_dir)
if config:
configs[f.stem] = config
except (yaml.YAMLError, OSError) as e:
print(f"Warning: {f.name}: {e}", file=sys.stderr)
return configs
def compute_coverage(config: dict, db: dict, **kwargs) -> dict:
"""Compute BIOS coverage by delegating to verify.py's platform-aware logic."""
sys.path.insert(0, os.path.dirname(__file__))
from verify import verify_platform
def compute_coverage(platform_name: str, platforms_dir: str, db: dict) -> dict:
config = load_platform_config(platform_name, platforms_dir)
result = verify_platform(config, db)
present = result["ok"] + result["untested"]
pct = (present / result["total"] * 100) if result["total"] > 0 else 0
return {
"platform": config.get("platform", platform_name),
"total": result["total"],
"verified": result["ok"],
"untested": result["untested"],
"missing": result["missing"],
"present": present,
"missing": [d["name"] for d in result["details"] if d["status"] == "missing"],
"percentage": pct,
"verification_mode": result["verification_mode"],
"mode": config.get("verification_mode", "existence"),
"details": result["details"],
"config": config,
}
def status_badge(pct: float, platform: str = "") -> str:
"""Generate a shields.io badge URL for platform coverage."""
if pct >= 90:
color = "brightgreen"
elif pct >= 70:
color = "yellow"
else:
color = "red"
label = platform.replace(" ", "%20") if platform else "coverage"
return f"![{platform} {pct:.0f}%](https://img.shields.io/badge/{label}-{pct:.0f}%25-{color})"
SITE_URL = "https://abdess.github.io/retrobios/"
RELEASE_URL = "../../releases/latest"
def status_emoji(pct: float) -> str:
if pct >= 90:
return "🟢"
elif pct >= 70:
return "🟡"
else:
return "🔴"
def _rel_link(path: str) -> str:
"""Build a relative link to a file in the repo."""
encoded = path.replace(" ", "%20").replace("(", "%28").replace(")", "%29")
return encoded
def generate_readme(db: dict, configs: dict) -> str:
"""Generate README.md content."""
generated_at = db.get("generated_at", "unknown")
def generate_readme(db: dict, platforms_dir: str) -> str:
total_files = db.get("total_files", 0)
total_size_mb = db.get("total_size", 0) / (1024 * 1024)
total_size = db.get("total_size", 0)
size_mb = total_size / (1024 * 1024)
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
systems = {}
for sha1, entry in db.get("files", {}).items():
path = entry.get("path", "")
parts = path.split("/")
# Skip .variants/ files from main listing (shown under their canonical file)
if ".variants/" in path:
continue
if len(parts) >= 3:
system = f"{parts[1]}/{parts[2]}"
elif len(parts) >= 2:
system = parts[1]
else:
system = "Other"
systems.setdefault(system, []).append(entry)
platform_names = sorted(
p.stem for p in Path(platforms_dir).glob("*.yml")
if not p.name.startswith("_")
)
lines = []
lines.append("# Retrogaming BIOS & Firmware Collection")
lines.append("")
lines.append("Complete, verified collection of BIOS, firmware, and system files "
"for retrogaming emulators - RetroArch, Batocera, Recalbox, Lakka, "
"RetroPie, and more. Every file checked against official checksums "
"from [libretro System.dat](https://github.com/libretro/libretro-database), "
"[batocera-systems](https://github.com/batocera-linux/batocera.linux), "
"and [Recalbox es_bios.xml](https://gitlab.com/recalbox/recalbox).")
lines.append("")
lines.append(f"> **{total_files}** files | **{total_size_mb:.1f} MB** | "
f"Last updated: {generated_at}")
lines.append(">")
lines.append("> PlayStation, PS2, Nintendo DS, Game Boy, GBA, Dreamcast, Saturn, "
"Neo Geo, Mega CD, PC Engine, MSX, Amiga, Atari ST, ZX Spectrum, "
"Arcade (MAME/FBNeo), and 50+ systems.")
lines.append("")
lines.append("## Quick Start")
lines.append("")
lines.append("### Download a complete pack")
lines.append("")
lines.append("Go to [Releases](../../releases) and download the ZIP for your platform.")
lines.append("")
lines.append("### Using the download tool")
lines.append("")
lines.append("```bash")
lines.append("# List available platforms")
lines.append("python scripts/download.py --list")
lines.append("")
lines.append("# Download BIOS pack for RetroArch")
lines.append("python scripts/download.py retroarch ~/RetroArch/system/")
lines.append("")
lines.append("# Verify existing BIOS files")
lines.append("python scripts/download.py --verify retroarch ~/RetroArch/system/")
lines.append("```")
lines.append("")
lines.append("### Generate a pack locally (any platform)")
lines.append("")
lines.append("Some platforms are archived and not included in automated releases. "
"You can generate any pack locally - including archived ones:")
lines.append("")
lines.append("```bash")
lines.append("git clone https://github.com/Abdess/retrobios.git")
lines.append("cd retrobios")
lines.append("pip install pyyaml")
lines.append("")
lines.append("# Generate for a specific platform")
lines.append("python scripts/generate_pack.py --platform retropie --output-dir ~/Downloads/")
lines.append("")
lines.append("# Generate for ALL platforms (including archived)")
lines.append("python scripts/generate_pack.py --all --include-archived --output-dir ~/Downloads/")
lines.append("```")
lines.append("")
coverages = {}
for name in platform_names:
try:
coverages[name] = compute_coverage(name, platforms_dir, db)
except FileNotFoundError:
pass
registry = {}
registry_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "platforms", "_registry.yml")
if os.path.exists(registry_path):
with open(registry_path) as f:
registry = (yaml.safe_load(f) or {}).get("platforms", {})
emulator_count = sum(
1 for f in Path("emulators").glob("*.yml")
) if Path("emulators").exists() else 0
if configs:
lines.append("## Platform Coverage")
lines.append("")
lines.append("| Platform | Coverage | Status | Verification | Details |")
lines.append("|----------|----------|--------|--------------|---------|")
lines = [
"# Retrogaming BIOS & Firmware Collection",
"",
"Complete, verified collection of BIOS, firmware, and system files for retrogaming emulators.",
"",
f"> **{total_files}** files | **{size_mb:.1f} MB** | **{len(coverages)}** platforms | **{emulator_count}** emulator profiles",
"",
"## Download",
"",
"| Platform | Files | Verification | Pack |",
"|----------|-------|-------------|------|",
]
for name, config in sorted(configs.items()):
platform_display = config.get("platform", name)
platform_status = registry.get(name, {}).get("status", "active")
coverage = compute_coverage(config, db)
badge = status_badge(coverage["percentage"], platform_display)
emoji = status_emoji(coverage["percentage"])
mode = coverage["verification_mode"]
for name, cov in sorted(coverages.items(), key=lambda x: x[1]["platform"]):
lines.append(
f"| {cov['platform']} | {cov['total']} | {cov['mode']} | "
f"[Download]({RELEASE_URL}) |"
)
if platform_status == "archived":
badge = f"![{platform_display}](https://img.shields.io/badge/{platform_display.replace(' ', '%20')}-archived-lightgrey)"
emoji = "📦"
lines.extend([
"",
"## Coverage",
"",
"| Platform | Coverage | Verified | Untested | Missing |",
"|----------|----------|----------|----------|---------|",
])
if mode == "existence":
detail = f"{coverage['verified']} present"
if coverage['missing']:
detail += f", {len(coverage['missing'])} missing"
else:
parts = []
if coverage['verified']:
parts.append(f"{coverage['verified']} verified")
if coverage['untested']:
parts.append(f"{coverage['untested']} untested")
if coverage['missing']:
parts.append(f"{len(coverage['missing'])} missing")
detail = ", ".join(parts) if parts else "0 files"
for name, cov in sorted(coverages.items(), key=lambda x: x[1]["platform"]):
pct = f"{cov['percentage']:.1f}%"
lines.append(
f"| {cov['platform']} | {cov['present']}/{cov['total']} ({pct}) | "
f"{cov['verified']} | {cov['untested']} | {cov['missing']} |"
)
if platform_status == "archived":
detail += " *(archived - generate manually)*"
lines.extend([
"",
"## Documentation",
"",
f"Full file listings, platform coverage, emulator profiles, and gap analysis: **[{SITE_URL}]({SITE_URL})**",
"",
"## Contributing",
"",
"See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.",
"",
"## License",
"",
"This repository provides BIOS files for personal backup and archival purposes.",
"",
f"*Auto-generated on {ts}*",
])
lines.append(
f"| {platform_display} | "
f"{coverage['present']}/{coverage['total']} ({coverage['percentage']:.1f}%) | "
f"{badge} {emoji} | "
f"{mode} | "
f"{detail} |"
)
lines.append("")
DATA_PACK_MARKERS = {"RPG Maker", "ScummVM"}
bios_systems = {}
data_packs = {}
for system_name, files in systems.items():
if any(marker in system_name for marker in DATA_PACK_MARKERS):
data_packs[system_name] = files
else:
bios_systems[system_name] = files
lines.append("## Systems")
lines.append("")
lines.append("| System | Files | Size |")
lines.append("|--------|-------|------|")
for system_name, files in sorted(bios_systems.items()):
total_size = sum(f.get("size", 0) for f in files)
if total_size > 1024 * 1024:
size_str = f"{total_size / (1024*1024):.1f} MB"
elif total_size > 1024:
size_str = f"{total_size / 1024:.1f} KB"
else:
size_str = f"{total_size} B"
lines.append(f"| {system_name} | {len(files)} | {size_str} |")
lines.append("")
if data_packs:
lines.append("## Data Packs")
lines.append("")
lines.append("These are large asset packs required by specific cores. "
"They are included in the repository but not listed individually.")
lines.append("")
lines.append("| Pack | Files | Size |")
lines.append("|------|-------|------|")
for pack_name, files in sorted(data_packs.items()):
total_size = sum(f.get("size", 0) for f in files)
size_str = f"{total_size / (1024*1024):.1f} MB" if total_size > 1024*1024 else f"{total_size / 1024:.1f} KB"
# Link to the manufacturer/system directory
first_path = files[0].get("path", "") if files else ""
parts = first_path.split("/")
pack_path = "/".join(parts[:3]) if len(parts) >= 3 else first_path
lines.append(f"| [{pack_name}]({_rel_link(pack_path)}) | {len(files)} | {size_str} |")
lines.append("")
platform_names = {}
by_name_idx = db.get("indexes", {}).get("by_name", {})
files_db = db.get("files", {})
for cfg_name, cfg in configs.items():
plat_display = cfg.get("platform", cfg_name)
for sys_id, system in cfg.get("systems", {}).items():
for fe in system.get("files", []):
fe_name = fe.get("name", "")
fe_dest = fe.get("destination", fe_name)
fe_sha1 = fe.get("sha1")
fe_md5 = fe.get("md5", "").split(",")[0].strip() if fe.get("md5") else ""
# Find matching SHA1
matched_sha1 = None
if fe_sha1 and fe_sha1 in files_db:
matched_sha1 = fe_sha1
elif fe_md5:
matched_sha1 = db.get("indexes", {}).get("by_md5", {}).get(fe_md5.lower())
if not matched_sha1:
matched_sha1 = db.get("indexes", {}).get("by_md5", {}).get(fe_md5)
if not matched_sha1 and fe_name in by_name_idx:
matched_sha1 = by_name_idx[fe_name][0]
if matched_sha1:
if matched_sha1 not in platform_names:
platform_names[matched_sha1] = []
dest_name = fe_dest.split("/")[-1] if "/" in fe_dest else fe_dest
if dest_name != files_db.get(matched_sha1, {}).get("name", ""):
entry = (plat_display, dest_name)
if entry not in platform_names[matched_sha1]:
platform_names[matched_sha1].append(entry)
variants_map = {}
for sha1, entry in files_db.items():
if ".variants/" not in entry.get("path", ""):
continue
vname = entry["name"]
# Strip the .sha1short suffix to get the original filename
parts = vname.rsplit(".", 1)
if len(parts) == 2 and len(parts[1]) == 8 and all(c in "0123456789abcdef" for c in parts[1]):
base_name = parts[0]
else:
base_name = vname
variants_map.setdefault(base_name, []).append(entry)
lines.append("## BIOS File Listing")
lines.append("")
for system_name, files in sorted(bios_systems.items()):
lines.append(f"### {system_name}")
lines.append("")
for entry in sorted(files, key=lambda x: x["name"]):
name = entry["name"]
path = entry.get("path", "")
size = entry.get("size", 0)
sha1 = entry.get("sha1", "")
dl_link = _rel_link(path)
lines.append(f"- **[{name}]({dl_link})** ({size:,} bytes)")
lines.append(f" - SHA1: `{sha1 or 'N/A'}`")
lines.append(f" - MD5: `{entry.get('md5', 'N/A')}`")
lines.append(f" - CRC32: `{entry.get('crc32', 'N/A')}`")
if sha1:
alt_names = []
for alias_name, alias_sha1s in by_name_idx.items():
if sha1 in alias_sha1s and alias_name != name:
alt_names.append(alias_name)
if alt_names:
lines.append(f" - Also known as: {', '.join(f'`{a}`' for a in sorted(alt_names))}")
if sha1 and sha1 in platform_names and platform_names[sha1]:
plat_refs = [f"{plat}: `{dest}`" for plat, dest in platform_names[sha1]]
lines.append(f" - Platform names: {', '.join(plat_refs)}")
if name in variants_map:
vlist = variants_map[name]
lines.append(f" - **Variants** ({len(vlist)} alternate versions):")
for v in sorted(vlist, key=lambda x: x["name"]):
vlink = _rel_link(v["path"])
lines.append(f" - [{v['name']}]({vlink}) ({v['size']:,} bytes) "
f"- SHA1: `{v['sha1']}`, MD5: `{v['md5']}`")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## Contributing")
lines.append("")
lines.append("See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on submitting BIOS files.")
lines.append("")
lines.append("## License")
lines.append("")
lines.append("This repository provides BIOS files for personal backup and archival purposes.")
lines.append("")
lines.append(f"*Auto-generated on {generated_at}*")
lines.append("")
return "\n".join(lines)
return "\n".join(lines) + "\n"
def generate_contributing() -> str:
"""Generate CONTRIBUTING.md content."""
return """# Contributing BIOS Files
return """# Contributing to RetroBIOS
Thank you for helping expand the BIOS collection!
## Add a BIOS file
## How to Contribute
1. Fork this repository
2. Place the file in `bios/Manufacturer/Console/filename`
3. Variants (alternate hashes): `bios/Manufacturer/Console/.variants/`
4. Create a Pull Request - checksums are verified automatically
1. **Fork** this repository
2. **Add** your BIOS file to the correct directory under `bios/Manufacturer/Console/`
3. **Create a Pull Request**
## File conventions
## File Placement
Place files in the correct manufacturer/console directory:
```
bios/
├── Sony/
│ └── PlayStation/
│ └── scph5501.bin
├── Nintendo/
│ └── Game Boy Advance/
│ └── gba_bios.bin
└── Sega/
└── Dreamcast/
└── dc_boot.bin
```
## Verification
All submitted BIOS files are automatically verified against known checksums:
1. **Hash verification** - SHA1/MD5 checked against known databases
2. **Size verification** - File size matches expected value
3. **Platform reference** - File must be referenced in at least one platform config
4. **Duplicate detection** - Existing files are flagged to avoid duplication
## What We Accept
- **Verified BIOS dumps** with matching checksums from known databases
- **System firmware** required by emulators
- **New variants** of existing BIOS files (different regions, versions)
## What We Don't Accept
- Game ROMs or ISOs
- Modified/patched BIOS files
- Files without verifiable checksums
- Executable files (.exe, .bat, .sh)
## Questions?
Open an [Issue](../../issues) if you're unsure about a file.
- Files >50 MB go in GitHub release assets (`large-files` release)
- RPG Maker and ScummVM directories are excluded from deduplication
- See the [documentation site](https://abdess.github.io/retrobios/) for full details
"""
def main():
parser = argparse.ArgumentParser(description="Generate README.md and CONTRIBUTING.md")
parser.add_argument("--db", default="database.json", help="Path to database.json")
parser.add_argument("--platforms-dir", default="platforms", help="Platforms config directory")
parser.add_argument("--output-dir", default=".", help="Output directory for README/CONTRIBUTING")
parser = argparse.ArgumentParser(description="Generate slim README.md")
parser.add_argument("--db", default="database.json")
parser.add_argument("--platforms-dir", default="platforms")
args = parser.parse_args()
if not os.path.exists(args.db):
print(f"Error: {args.db} not found. Run generate_db.py first.", file=sys.stderr)
sys.exit(1)
db = load_database(args.db)
configs = load_platform_configs(args.platforms_dir) if os.path.isdir(args.platforms_dir) else {}
readme = generate_readme(db, configs)
readme_path = os.path.join(args.output_dir, "README.md")
with open(readme_path, "w") as f:
readme = generate_readme(db, args.platforms_dir)
with open("README.md", "w") as f:
f.write(readme)
print(f"Generated {readme_path}")
print(f"Generated ./README.md")
contributing = generate_contributing()
contributing_path = os.path.join(args.output_dir, "CONTRIBUTING.md")
with open(contributing_path, "w") as f:
with open("CONTRIBUTING.md", "w") as f:
f.write(contributing)
print(f"Generated {contributing_path}")
print(f"Generated ./CONTRIBUTING.md")
if __name__ == "__main__":
+813
View File
@@ -0,0 +1,813 @@
#!/usr/bin/env python3
"""Generate MkDocs site pages from database.json, platform configs, and emulator profiles.
Reads the same data sources as verify.py and generate_pack.py to produce
a complete documentation site. Zero manual content.
Usage:
python scripts/generate_site.py
python scripts/generate_site.py --db database.json --platforms-dir platforms
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path
try:
import yaml
except ImportError:
print("Error: PyYAML required (pip install pyyaml)", file=sys.stderr)
sys.exit(1)
sys.path.insert(0, os.path.dirname(__file__))
from common import load_database, load_platform_config
from verify import verify_platform
DOCS_DIR = "docs"
SITE_NAME = "RetroBIOS"
REPO_URL = "https://github.com/Abdess/retrobios"
RELEASE_URL = f"{REPO_URL}/releases/latest"
GENERATED_DIRS = ["platforms", "systems", "emulators"]
SYSTEM_ICON_BASE = "https://raw.githubusercontent.com/libretro/retroarch-assets/master/xmb/systematic/png"
def _timestamp() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _fmt_size(size: int) -> str:
if size >= 1024 * 1024 * 1024:
return f"{size / (1024**3):.1f} GB"
if size >= 1024 * 1024:
return f"{size / (1024**2):.1f} MB"
if size >= 1024:
return f"{size / 1024:.1f} KB"
return f"{size} B"
def _pct(n: int, total: int) -> str:
if total == 0:
return "0%"
return f"{n / total * 100:.1f}%"
def _status_icon(pct: float) -> str:
if pct >= 100:
return "OK"
if pct >= 95:
return "~OK"
return "partial"
def compute_coverage(platform_name: str, platforms_dir: str, db: dict) -> dict:
config = load_platform_config(platform_name, platforms_dir)
result = verify_platform(config, db)
present = result["ok"] + result["untested"]
pct = (present / result["total"] * 100) if result["total"] > 0 else 0
return {
"platform": config.get("platform", platform_name),
"total": result["total"],
"verified": result["ok"],
"untested": result["untested"],
"missing": result["missing"],
"present": present,
"percentage": pct,
"mode": config.get("verification_mode", "existence"),
"details": result["details"],
"config": config,
}
# ---------------------------------------------------------------------------
# Load emulator profiles
# ---------------------------------------------------------------------------
def _load_emulator_profiles(emulators_dir: str) -> dict[str, dict]:
profiles = {}
emu_path = Path(emulators_dir)
if not emu_path.exists():
return profiles
for f in sorted(emu_path.glob("*.yml")):
with open(f) as fh:
profile = yaml.safe_load(fh) or {}
profiles[f.stem] = profile
return profiles
# ---------------------------------------------------------------------------
# Home page
# ---------------------------------------------------------------------------
def generate_home(db: dict, coverages: dict, emulator_count: int,
registry: dict | None = None) -> str:
total_files = db.get("total_files", 0)
total_size = db.get("total_size", 0)
ts = _timestamp()
lines = [
f"# {SITE_NAME}",
"",
"Complete BIOS and firmware collection for retrogaming emulators.",
"",
"---",
"",
f"**{total_files:,}** files across **{len(coverages)}** platforms, "
f"backed by **{emulator_count}** emulator source code profiles.",
"",
]
# Single unified table: platform + coverage + download
lines.extend([
"## Platforms",
"",
"| | Platform | Coverage | Verified | Download |",
"|---|----------|----------|----------|----------|",
])
for name, cov in sorted(coverages.items(), key=lambda x: x[1]["platform"]):
display = cov["platform"]
pct = _pct(cov["present"], cov["total"])
logo_url = (registry or {}).get(name, {}).get("logo", "")
logo_md = f"![{display}]({logo_url}){{ width=20 loading=lazy }}" if logo_url else ""
lines.append(
f"| {logo_md} | [{display}](platforms/{name}.md) | "
f"{cov['present']}/{cov['total']} ({pct}) | "
f"{cov['verified']} | "
f"[Pack]({RELEASE_URL}) |"
)
# Quick links
lines.extend([
"",
"---",
"",
f"[Systems](systems/){{ .md-button }} "
f"[Emulators](emulators/){{ .md-button }} "
f"[Gap Analysis](gaps/){{ .md-button }} "
f"[Contributing](contributing/){{ .md-button .md-button--primary }}",
"",
f"*{_fmt_size(total_size)} total. Generated on {ts}.*",
])
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# Platform pages
# ---------------------------------------------------------------------------
def generate_platform_index(coverages: dict) -> str:
lines = [
f"# Platforms - {SITE_NAME}",
"",
"| Platform | Coverage | Verification | Status |",
"|----------|----------|-------------|--------|",
]
for name, cov in sorted(coverages.items(), key=lambda x: x[1]["platform"]):
display = cov["platform"]
pct = _pct(cov["present"], cov["total"])
plat_status = cov["config"].get("status", "active")
status = "archived" if plat_status == "archived" else _status_icon(cov["percentage"])
lines.append(
f"| [{display}]({name}.md) | "
f"{cov['present']}/{cov['total']} ({pct}) | "
f"{cov['mode']} | {status} |"
)
return "\n".join(lines) + "\n"
def generate_platform_page(name: str, cov: dict, registry: dict | None = None) -> str:
config = cov["config"]
display = cov["platform"]
mode = cov["mode"]
pct = _pct(cov["present"], cov["total"])
logo_url = (registry or {}).get(name, {}).get("logo", "")
logo_md = f"![{display}]({logo_url}){{ width=48 align=right }}\n\n" if logo_url else ""
lines = [
f"# {display} - {SITE_NAME}",
"",
logo_md + f"**Verification mode:** {mode}",
f"**Coverage:** {cov['present']}/{cov['total']} ({pct})",
f"**Verified:** {cov['verified']} | **Untested:** {cov['untested']} | **Missing:** {cov['missing']}",
"",
f"[Download {display} Pack]({RELEASE_URL}){{ .md-button }}",
"",
]
# Group details by system
by_system: dict[str, list] = {}
for d in cov["details"]:
sys_id = d.get("system", "unknown")
by_system.setdefault(sys_id, []).append(d)
for sys_id, files in sorted(by_system.items()):
ok_count = sum(1 for f in files if f["status"] == "ok")
total = len(files)
lines.append(f"## {sys_id}")
lines.append(f"")
lines.append(f"{ok_count}/{total} verified")
lines.append("")
# Only show table if there are non-OK entries, otherwise just list filenames
non_ok = [f for f in files if f["status"] != "ok"]
if non_ok:
lines.append("| File | Status | Detail |")
lines.append("|------|--------|--------|")
for f in sorted(non_ok, key=lambda x: x["name"]):
status = f["status"]
detail = ""
if status == "untested":
reason = f.get("reason", "")
expected = f.get("expected_md5", "")
actual = f.get("actual_md5", "")
detail = reason or (f"expected `{expected[:12]}...` got `{actual[:12]}...`" if expected and actual else "")
status_display = "Untested"
elif status == "missing":
status_display = "Missing"
detail = f"Expected: `{f.get('expected_md5', 'unknown')}`"
else:
status_display = status
lines.append(f"| `{f['name']}` | {status_display} | {detail} |")
lines.append("")
ok_files = [f for f in files if f["status"] == "ok"]
if ok_files:
unique_names = sorted(set(f["name"] for f in ok_files))
names = ", ".join(f"`{n}`" for n in unique_names)
lines.append(f"Files: {names}")
lines.append("")
lines.append(f"*Generated on {_timestamp()}*")
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# System pages
# ---------------------------------------------------------------------------
def _group_by_manufacturer(db: dict) -> dict[str, dict[str, list]]:
"""Group files by manufacturer -> console -> files."""
manufacturers: dict[str, dict[str, list]] = {}
for sha1, entry in db.get("files", {}).items():
path = entry.get("path", "")
parts = path.split("/")
if len(parts) < 3 or parts[0] != "bios":
continue
manufacturer = parts[1]
console = parts[2]
manufacturers.setdefault(manufacturer, {}).setdefault(console, []).append(entry)
return manufacturers
def generate_systems_index(manufacturers: dict) -> str:
lines = [
f"# Systems - {SITE_NAME}",
"",
"| Manufacturer | Consoles | Files |",
"|-------------|----------|-------|",
]
for mfr in sorted(manufacturers.keys()):
consoles = manufacturers[mfr]
file_count = sum(len(files) for files in consoles.values())
slug = mfr.lower().replace(" ", "-")
lines.append(f"| [{mfr}]({slug}.md) | {len(consoles)} | {file_count} |")
return "\n".join(lines) + "\n"
def generate_system_page(
manufacturer: str,
consoles: dict[str, list],
platform_files: dict[str, set],
emulator_files: dict[str, set],
) -> str:
slug = manufacturer.lower().replace(" ", "-")
lines = [
f"# {manufacturer} - {SITE_NAME}",
"",
]
for console_name in sorted(consoles.keys()):
files = consoles[console_name]
icon_name = f"{manufacturer} - {console_name}".replace("/", " ")
icon_url = f"{SYSTEM_ICON_BASE}/{icon_name.replace(' ', '%20')}.png"
lines.append(f"## ![{console_name}]({icon_url}){{ width=24 }} {console_name}")
lines.append("")
# Separate main files from variants
main_files = [f for f in files if "/.variants/" not in f["path"]]
variant_files = [f for f in files if "/.variants/" in f["path"]]
for f in sorted(main_files, key=lambda x: x["name"]):
name = f["name"]
sha1_full = f.get("sha1", "unknown")
md5_full = f.get("md5", "unknown")
size = _fmt_size(f.get("size", 0))
# Cross-reference: which platforms declare this file
plats = sorted(p for p, names in platform_files.items() if name in names)
# Cross-reference: which emulators load this file
emus = sorted(e for e, names in emulator_files.items() if name in names)
lines.append(f"**`{name}`** ({size})")
lines.append("")
lines.append(f"- SHA1: `{sha1_full}`")
lines.append(f"- MD5: `{md5_full}`")
if plats:
lines.append(f"- Platforms: {', '.join(plats)}")
if emus:
lines.append(f"- Emulators: {', '.join(emus)}")
lines.append("")
if variant_files:
lines.append("**Variants:**")
lines.append("")
for v in sorted(variant_files, key=lambda x: x["name"]):
vname = v["name"]
vmd5 = v.get("md5", "unknown")
lines.append(f"- `{vname}` MD5: `{vmd5}`")
lines.append("")
lines.append(f"*Generated on {_timestamp()}*")
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# Emulator pages
# ---------------------------------------------------------------------------
def generate_emulators_index(profiles: dict) -> str:
lines = [
f"# Emulators - {SITE_NAME}",
"",
"| Engine | Type | Systems | Files |",
"|--------|------|---------|-------|",
]
unique = {k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test")}
test_cores = {k: v for k, v in profiles.items() if v.get("type") == "test"}
aliases = {k: v for k, v in profiles.items() if v.get("type") == "alias"}
for name in sorted(unique.keys()):
p = unique[name]
emu_name = p.get("emulator", name)
emu_type = p.get("type", "unknown")
systems = p.get("systems", [])
files = p.get("files", [])
sys_str = ", ".join(systems[:3])
if len(systems) > 3:
sys_str += f" +{len(systems)-3}"
lines.append(
f"| [{emu_name}]({name}.md) | {emu_type} | "
f"{sys_str} | {len(files)} |"
)
if aliases:
lines.extend(["", "## Aliases", ""])
lines.append("| Core | Points to |")
lines.append("|------|-----------|")
for name in sorted(aliases.keys()):
parent = aliases[name].get("alias_of", "unknown")
lines.append(f"| {name} | [{parent}]({parent}.md) |")
return "\n".join(lines) + "\n"
def generate_emulator_page(name: str, profile: dict, db: dict,
platform_files: dict | None = None) -> str:
if profile.get("type") == "alias":
parent = profile.get("alias_of", "unknown")
return (
f"# {name} - {SITE_NAME}\n\n"
f"This core uses the same firmware as **{parent}**.\n\n"
f"See [{parent}]({parent}.md) for details.\n"
)
emu_name = profile.get("emulator", name)
emu_type = profile.get("type", "unknown")
source = profile.get("source", "")
version = profile.get("core_version", "unknown")
display = profile.get("display_name", emu_name)
profiled = profile.get("profiled_date", "unknown")
systems = profile.get("systems", [])
cores = profile.get("cores", [name])
files = profile.get("files", [])
logo_url = profile.get("logo", "")
logo_md = f"![{emu_name}]({logo_url}){{ width=48 align=right }}\n\n" if logo_url else ""
lines = [
f"# {emu_name} - {SITE_NAME}",
"",
logo_md + f"| | |",
f"|---|---|",
f"| Type | {emu_type} |",
]
if source:
lines.append(f"| Source | [{source}]({source}) |")
lines.append(f"| Version | {version} |")
lines.append(f"| Profiled | {profiled} |")
if cores:
lines.append(f"| Cores | {', '.join(str(c) for c in cores)} |")
if systems:
lines.append(f"| Systems | {', '.join(str(s) for s in systems)} |")
lines.append("")
if not files:
lines.append("No BIOS or firmware files required. This core is self-contained.")
note = profile.get("note", profile.get("notes", ""))
if note:
lines.extend(["", str(note)])
else:
by_name = db.get("indexes", {}).get("by_name", {})
in_repo_count = sum(1 for f in files if f.get("name", "") in by_name)
missing_count = len(files) - in_repo_count
lines.append(f"**{len(files)} files** ({in_repo_count} in repo, {missing_count} missing)")
lines.append("")
# Check which platforms declare each file
show_platforms = platform_files is not None
if show_platforms:
lines.append("| File | Required | In Repo | Platforms | Source Ref |")
lines.append("|------|----------|---------|-----------|-----------|")
else:
lines.append("| File | Required | In Repo | Source Ref |")
lines.append("|------|----------|---------|-----------|")
for f in files:
fname = f.get("name", "")
required = "yes" if f.get("required") else "no"
in_repo = "yes" if fname in by_name else "no"
source_ref = f.get("source_ref", "")
if show_platforms:
plats = [p for p, names in platform_files.items() if fname in names]
plat_str = ", ".join(sorted(plats)) if plats else "-"
lines.append(f"| `{fname}` | {required} | {in_repo} | {plat_str} | {source_ref} |")
else:
lines.append(f"| `{fname}` | {required} | {in_repo} | {source_ref} |")
lines.extend(["", f"*Generated on {_timestamp()}*"])
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# Contributing page
# ---------------------------------------------------------------------------
def generate_gap_analysis(
profiles: dict,
coverages: dict,
db: dict,
) -> str:
"""Generate a global gap analysis page showing all missing/undeclared files."""
by_name = db.get("indexes", {}).get("by_name", {})
platform_files = _build_platform_file_index(coverages)
lines = [
f"# Gap Analysis - {SITE_NAME}",
"",
"Files that emulators load but platforms don't declare, and their availability.",
"",
]
# Global stats
total_undeclared = 0
total_in_repo = 0
total_missing = 0
# Build global set of all platform-declared filenames (once)
all_platform_names = set()
for pfiles in platform_files.values():
all_platform_names.update(pfiles)
emulator_gaps = []
for emu_name, profile in sorted(profiles.items()):
if profile.get("type") == "alias":
continue
files = profile.get("files", [])
if not files:
continue
undeclared = []
for f in files:
fname = f.get("name", "")
if not fname or fname.startswith("<"):
continue
if fname not in all_platform_names:
in_repo = fname in by_name
undeclared.append({
"name": fname,
"required": f.get("required", False),
"in_repo": in_repo,
"source_ref": f.get("source_ref", ""),
})
total_undeclared += 1
if in_repo:
total_in_repo += 1
else:
total_missing += 1
if undeclared:
emulator_gaps.append((emu_name, profile.get("emulator", emu_name), undeclared))
lines.extend([
"## Summary",
"",
f"| Metric | Count |",
f"|--------|-------|",
f"| Total undeclared files | {total_undeclared} |",
f"| Already in repo | {total_in_repo} |",
f"| Missing from repo | {total_missing} |",
f"| Emulators with gaps | {len(emulator_gaps)} |",
"",
])
# Per-emulator breakdown
lines.extend([
"## Per Emulator",
"",
"| Emulator | Undeclared | In Repo | Missing |",
"|----------|-----------|---------|---------|",
])
for emu_name, display, gaps in sorted(emulator_gaps, key=lambda x: -len(x[2])):
in_repo = sum(1 for g in gaps if g["in_repo"])
missing = len(gaps) - in_repo
lines.append(f"| [{display}](emulators/{emu_name}.md) | {len(gaps)} | {in_repo} | {missing} |")
# Missing files detail (not in repo)
all_missing = set()
missing_details = []
for emu_name, display, gaps in emulator_gaps:
for g in gaps:
if not g["in_repo"] and g["name"] not in all_missing:
all_missing.add(g["name"])
missing_details.append({
"name": g["name"],
"emulator": display,
"required": g["required"],
"source_ref": g["source_ref"],
})
if missing_details:
lines.extend([
"",
f"## Missing Files ({len(missing_details)} unique)",
"",
"Files loaded by emulators but not available in the repository.",
"",
"| File | Emulator | Required | Source |",
"|------|----------|----------|--------|",
])
for m in sorted(missing_details, key=lambda x: x["name"]):
req = "yes" if m["required"] else "no"
lines.append(f"| `{m['name']}` | {m['emulator']} | {req} | {m['source_ref']} |")
lines.extend(["", f"*Generated on {_timestamp()}*"])
return "\n".join(lines) + "\n"
def generate_contributing() -> str:
return """# Contributing - RetroBIOS
## Add a BIOS file
1. Fork this repository
2. Place the file in `bios/Manufacturer/Console/filename`
3. Variants (alternate hashes for the same file): place in `bios/Manufacturer/Console/.variants/`
4. Create a Pull Request - hashes are verified automatically
## Add a platform
1. Create a scraper in `scripts/scraper/` (inherit `BaseScraper`)
2. Read the platform's upstream source code to understand its BIOS check logic
3. Add entry to `platforms/_registry.yml`
4. Generate the platform YAML config
5. Test: `python scripts/verify.py --platform <name>`
## Add an emulator profile
1. Clone the emulator's source code
2. Search for BIOS/firmware loading (grep for `bios`, `rom`, `firmware`, `fopen`)
3. Document every file the emulator loads with source code references
4. Write YAML to `emulators/<name>.yml`
5. Test: `python scripts/cross_reference.py --emulator <name>`
## File conventions
- `bios/Manufacturer/Console/filename` for canonical files
- `bios/Manufacturer/Console/.variants/filename.sha1prefix` for alternate versions
- Files >50 MB go in GitHub release assets (`large-files` release)
- RPG Maker and ScummVM directories are excluded from deduplication
## PR validation
The CI automatically:
- Computes SHA1/MD5/CRC32 of new files
- Checks against known hashes in platform configs
- Reports coverage impact
"""
# ---------------------------------------------------------------------------
# Build cross-reference indexes
# ---------------------------------------------------------------------------
def _build_platform_file_index(coverages: dict) -> dict[str, set]:
"""Map platform_name -> set of declared file names."""
index = {}
for name, cov in coverages.items():
names = set()
config = cov["config"]
for system in config.get("systems", {}).values():
for fe in system.get("files", []):
names.add(fe.get("name", ""))
index[name] = names
return index
def _build_emulator_file_index(profiles: dict) -> dict[str, set]:
"""Map emulator_name -> set of file names it loads."""
index = {}
for name, profile in profiles.items():
if profile.get("type") == "alias":
continue
names = {f.get("name", "") for f in profile.get("files", [])}
index[name] = names
return index
# ---------------------------------------------------------------------------
# mkdocs.yml nav generator
# ---------------------------------------------------------------------------
def generate_mkdocs_nav(
coverages: dict,
manufacturers: dict,
profiles: dict,
) -> list:
"""Generate the nav section for mkdocs.yml."""
platform_nav = [{"Overview": "platforms/index.md"}]
for name in sorted(coverages.keys(), key=lambda x: coverages[x]["platform"]):
display = coverages[name]["platform"]
platform_nav.append({display: f"platforms/{name}.md"})
system_nav = [{"Overview": "systems/index.md"}]
for mfr in sorted(manufacturers.keys()):
slug = mfr.lower().replace(" ", "-")
system_nav.append({mfr: f"systems/{slug}.md"})
unique_profiles = {k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test")}
emu_nav = [{"Overview": "emulators/index.md"}]
for name in sorted(unique_profiles.keys()):
display = unique_profiles[name].get("emulator", name)
emu_nav.append({display: f"emulators/{name}.md"})
return [
{"Home": "index.md"},
{"Platforms": platform_nav},
{"Systems": system_nav},
{"Emulators": emu_nav},
{"Gap Analysis": "gaps.md"},
{"Contributing": "contributing.md"},
]
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="Generate MkDocs site from project data")
parser.add_argument("--db", default="database.json")
parser.add_argument("--platforms-dir", default="platforms")
parser.add_argument("--emulators-dir", default="emulators")
parser.add_argument("--docs-dir", default=DOCS_DIR)
args = parser.parse_args()
db = load_database(args.db)
docs = Path(args.docs_dir)
# Clean generated dirs (preserve docs/superpowers/)
for d in GENERATED_DIRS:
target = docs / d
if target.exists():
shutil.rmtree(target)
# Ensure output dirs
for d in GENERATED_DIRS:
(docs / d).mkdir(parents=True, exist_ok=True)
# Load registry for platform metadata (logos, etc.)
registry_path = Path(args.platforms_dir) / "_registry.yml"
registry = {}
if registry_path.exists():
with open(registry_path) as f:
registry = (yaml.safe_load(f) or {}).get("platforms", {})
# Load platform configs
platform_names = [
p.stem for p in Path(args.platforms_dir).glob("*.yml")
if not p.name.startswith("_")
]
print("Computing platform coverage...")
coverages = {}
for name in sorted(platform_names):
try:
cov = compute_coverage(name, args.platforms_dir, db)
coverages[name] = cov
print(f" {cov['platform']}: {cov['present']}/{cov['total']} ({_pct(cov['present'], cov['total'])})")
except FileNotFoundError as e:
print(f" {name}: skipped ({e})", file=sys.stderr)
# Load emulator profiles
print("Loading emulator profiles...")
profiles = _load_emulator_profiles(args.emulators_dir)
unique_count = sum(1 for p in profiles.values() if p.get("type") != "alias")
print(f" {len(profiles)} profiles ({unique_count} unique, {len(profiles) - unique_count} aliases)")
# Build cross-reference indexes
platform_files = _build_platform_file_index(coverages)
emulator_files = _build_emulator_file_index(profiles)
# Generate home
print("Generating home page...")
(docs / "index.md").write_text(generate_home(db, coverages, unique_count, registry))
# Generate platform pages
print("Generating platform pages...")
(docs / "platforms" / "index.md").write_text(generate_platform_index(coverages))
for name, cov in coverages.items():
(docs / "platforms" / f"{name}.md").write_text(generate_platform_page(name, cov, registry))
# Generate system pages
print("Generating system pages...")
manufacturers = _group_by_manufacturer(db)
(docs / "systems" / "index.md").write_text(generate_systems_index(manufacturers))
for mfr, consoles in manufacturers.items():
slug = mfr.lower().replace(" ", "-")
page = generate_system_page(mfr, consoles, platform_files, emulator_files)
(docs / "systems" / f"{slug}.md").write_text(page)
# Generate emulator pages
print("Generating emulator pages...")
(docs / "emulators" / "index.md").write_text(generate_emulators_index(profiles))
for name, profile in profiles.items():
page = generate_emulator_page(name, profile, db, platform_files)
(docs / "emulators" / f"{name}.md").write_text(page)
# Generate gap analysis page
print("Generating gap analysis page...")
(docs / "gaps.md").write_text(
generate_gap_analysis(profiles, coverages, db)
)
# Generate contributing
print("Generating contributing page...")
(docs / "contributing.md").write_text(generate_contributing())
# Update mkdocs.yml nav section only (avoid yaml.dump round-trip mangling quotes)
print("Updating mkdocs.yml nav...")
nav = generate_mkdocs_nav(coverages, manufacturers, profiles)
nav_yaml = yaml.dump({"nav": nav}, default_flow_style=False, sort_keys=False, allow_unicode=True)
with open("mkdocs.yml") as f:
content = f.read()
# Replace nav section (everything from \nnav: to the next top-level key or EOF)
import re
if "\nnav:" in content:
content = re.sub(r'\nnav:.*', '\n' + nav_yaml.rstrip(), content, count=1, flags=re.DOTALL)
else:
content += "\n" + nav_yaml
with open("mkdocs.yml", "w") as f:
f.write(content)
total_pages = (
1 # home
+ 1 + len(coverages) # platform index + detail
+ 1 + len(manufacturers) # system index + detail
+ 1 + len(profiles) # emulator index + detail
+ 1 # gap analysis
+ 1 # contributing
)
print(f"\nGenerated {total_pages} pages in {args.docs_dir}/")
if __name__ == "__main__":
main()
+76
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import sys
import urllib.request
import urllib.error
from abc import ABC, abstractmethod
@@ -48,6 +49,24 @@ class ChangeSet:
class BaseScraper(ABC):
"""Abstract base class for platform BIOS requirement scrapers."""
def __init__(self, url: str = ""):
self.url = url
self._raw_data: str | None = None
def _fetch_raw(self) -> str:
"""Fetch raw content from source URL. Cached after first call."""
if self._raw_data is not None:
return self._raw_data
if not self.url:
raise ValueError("No source URL configured")
try:
req = urllib.request.Request(self.url, headers={"User-Agent": "retrobios-scraper/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
self._raw_data = resp.read().decode("utf-8")
return self._raw_data
except urllib.error.URLError as e:
raise ConnectionError(f"Failed to fetch {self.url}: {e}") from e
@abstractmethod
def fetch_requirements(self) -> list[BiosRequirement]:
"""Fetch current BIOS requirements from the platform source."""
@@ -135,6 +154,63 @@ def fetch_github_latest_version(repo: str) -> str | None:
return None
def scraper_cli(scraper_class: type, description: str = "Scrape BIOS requirements") -> None:
"""Shared CLI entry point for all scrapers. Eliminates main() boilerplate."""
import argparse
parser = argparse.ArgumentParser(description=description)
parser.add_argument("--dry-run", action="store_true", help="Show scraped data")
parser.add_argument("--output", "-o", help="Output YAML file")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
scraper = scraper_class()
try:
reqs = scraper.fetch_requirements()
except (ConnectionError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if args.dry_run:
by_system: dict[str, list] = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
for system, files in sorted(by_system.items()):
req_count = sum(1 for f in files if f.required)
opt_count = len(files) - req_count
print(f" {system}: {req_count} required, {opt_count} optional")
print(f"\nTotal: {len(reqs)} BIOS entries across {len(by_system)} systems")
return
if args.json:
data = [{"name": r.name, "system": r.system, "sha1": r.sha1, "md5": r.md5,
"size": r.size, "required": r.required} for r in reqs]
print(json.dumps(data, indent=2))
return
if args.output:
# Generate platform YAML
import yaml
config = {"systems": {}}
for req in reqs:
sys_id = req.system
config["systems"].setdefault(sys_id, {"files": []})
entry = {"name": req.name, "destination": req.destination or req.name, "required": req.required}
if req.sha1:
entry["sha1"] = req.sha1
if req.md5:
entry["md5"] = req.md5
if req.zipped_file:
entry["zipped_file"] = req.zipped_file
config["systems"][sys_id]["files"].append(entry)
with open(args.output, "w") as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
print(f"Written {len(reqs)} entries to {args.output}")
return
print(f"Scraped {len(reqs)} requirements. Use --dry-run, --json, or --output.")
def fetch_github_latest_tag(repo: str, prefix: str = "") -> str | None:
"""Fetch the most recent matching tag from a GitHub repo."""
url = f"https://api.github.com/repos/{repo}/tags?per_page=50"
+8 -88
View File
@@ -89,20 +89,8 @@ class Scraper(BaseScraper):
"""Scraper for batocera-systems Python dict."""
def __init__(self, url: str = SOURCE_URL):
self.url = url
self._raw_data: str | None = None
super().__init__(url=url)
def _fetch_raw(self) -> str:
if self._raw_data is not None:
return self._raw_data
try:
req = urllib.request.Request(self.url, headers={"User-Agent": "retrobios-scraper/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
self._raw_data = resp.read().decode("utf-8")
return self._raw_data
except urllib.error.URLError as e:
raise ConnectionError(f"Failed to fetch {self.url}: {e}") from e
def _extract_systems_dict(self, raw: str) -> dict:
"""Extract and parse the 'systems' dict from the Python source via ast.literal_eval."""
@@ -223,28 +211,12 @@ class Scraper(BaseScraper):
systems[req.system]["files"].append(entry)
# Sort numerically since API returns by commit date, not version
import json as _json
tag = fetch_github_latest_tag("batocera-linux/batocera.linux", prefix="batocera-")
batocera_version = ""
try:
_url = "https://api.github.com/repos/batocera-linux/batocera.linux/tags?per_page=50"
_req = urllib.request.Request(_url, headers={
"User-Agent": "retrobios-scraper/1.0",
"Accept": "application/vnd.github.v3+json",
})
with urllib.request.urlopen(_req, timeout=15) as _resp:
_tags = _json.loads(_resp.read())
_versions = []
for _t in _tags:
_name = _t["name"]
if _name.startswith("batocera-"):
_num = _name.replace("batocera-", "")
if _num.isdigit():
_versions.append(int(_num))
if _versions:
batocera_version = str(max(_versions))
except (ConnectionError, ValueError, OSError):
pass
if tag:
num = tag.removeprefix("batocera-")
if num.isdigit():
batocera_version = num
return {
"platform": "Batocera",
@@ -259,60 +231,8 @@ class Scraper(BaseScraper):
def main():
"""CLI entry point for testing."""
import argparse
import json
parser = argparse.ArgumentParser(description="Scrape batocera-systems")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--json", action="store_true")
parser.add_argument("--output", "-o")
args = parser.parse_args()
scraper = Scraper()
try:
reqs = scraper.fetch_requirements()
except (ConnectionError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if args.dry_run:
by_system = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
for system, files in sorted(by_system.items()):
print(f"\n{system} ({len(files)} files):")
for f in files:
hash_info = f.md5[:12] if f.md5 else "no-hash"
print(f" {f.name} ({hash_info}...)")
print(f"\nTotal: {len(reqs)} BIOS files across {len(by_system)} systems")
return
if args.json:
config = scraper.generate_platform_yaml()
print(json.dumps(config, indent=2))
return
if args.output:
try:
import yaml
except ImportError:
print("Error: PyYAML required", file=sys.stderr)
sys.exit(1)
config = scraper.generate_platform_yaml()
with open(args.output, "w") as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
print(f"Written to {args.output}")
else:
reqs = scraper.fetch_requirements()
by_system = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
print(f"Scraped {len(reqs)} BIOS files across {len(by_system)} systems")
from scripts.scraper.base_scraper import scraper_cli
scraper_cli(Scraper, "Scrape batocera BIOS requirements")
if __name__ == "__main__":
+2 -52
View File
@@ -403,58 +403,8 @@ class Scraper(BaseScraper):
def main():
import argparse
import json
parser = argparse.ArgumentParser(description="Scrape EmuDeck BIOS requirements")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--json", action="store_true")
parser.add_argument("--output", "-o")
args = parser.parse_args()
scraper = Scraper()
try:
reqs = scraper.fetch_requirements()
except (ConnectionError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if args.dry_run:
by_system: dict[str, list[BiosRequirement]] = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
for system, files in sorted(by_system.items()):
print(f"\n{system} ({len(files)} files):")
for f in files:
hash_info = f.md5[:12] if f.md5 else "no-hash"
print(f" {f.name} ({hash_info}...)")
print(f"\nTotal: {len(reqs)} BIOS entries across {len(by_system)} systems")
return
if args.json:
config = scraper.generate_platform_yaml()
print(json.dumps(config, indent=2))
return
if args.output:
try:
import yaml
except ImportError:
print("Error: PyYAML required", file=sys.stderr)
sys.exit(1)
config = scraper.generate_platform_yaml()
with open(args.output, "w") as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
print(f"Written to {args.output}")
else:
by_system = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
print(f"Scraped {len(reqs)} BIOS entries across {len(by_system)} systems")
from scripts.scraper.base_scraper import scraper_cli
scraper_cli(Scraper, "Scrape emudeck BIOS requirements")
if __name__ == "__main__":
+3 -68
View File
@@ -88,21 +88,8 @@ class Scraper(BaseScraper):
"""Scraper for libretro System.dat."""
def __init__(self, url: str = SOURCE_URL):
self.url = url
self._raw_data: str | None = None
super().__init__(url=url)
def _fetch_raw(self) -> str:
"""Fetch raw DAT content from source URL."""
if self._raw_data is not None:
return self._raw_data
try:
req = urllib.request.Request(self.url, headers={"User-Agent": "retrobios-scraper/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
self._raw_data = resp.read().decode("utf-8")
return self._raw_data
except urllib.error.URLError as e:
raise ConnectionError(f"Failed to fetch {self.url}: {e}") from e
def fetch_requirements(self) -> list[BiosRequirement]:
"""Parse System.dat and return BIOS requirements."""
@@ -263,60 +250,8 @@ class Scraper(BaseScraper):
def main():
"""CLI entry point for testing."""
import argparse
import json
parser = argparse.ArgumentParser(description="Scrape libretro System.dat")
parser.add_argument("--dry-run", action="store_true", help="Just show what would be scraped")
parser.add_argument("--output", "-o", help="Output YAML file")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
scraper = Scraper()
try:
reqs = scraper.fetch_requirements()
except (ConnectionError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if args.dry_run:
by_system = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
for system, files in sorted(by_system.items()):
print(f"\n{system} ({len(files)} files):")
for f in files:
hash_info = f.sha1[:12] if f.sha1 else f.md5[:12] if f.md5 else "no-hash"
print(f" {f.name} ({f.size or '?'} bytes, {hash_info}...)")
print(f"\nTotal: {len(reqs)} BIOS files across {len(by_system)} systems")
return
if args.json:
config = scraper.generate_platform_yaml()
print(json.dumps(config, indent=2))
return
if args.output:
try:
import yaml
except ImportError:
print("Error: PyYAML required (pip install pyyaml)", file=sys.stderr)
sys.exit(1)
config = scraper.generate_platform_yaml()
with open(args.output, "w") as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
print(f"Written to {args.output}")
else:
reqs = scraper.fetch_requirements()
by_system = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
print(f"Scraped {len(reqs)} BIOS files across {len(by_system)} systems")
from scripts.scraper.base_scraper import scraper_cli
scraper_cli(Scraper, "Scrape libretro BIOS requirements")
if __name__ == "__main__":
+1 -14
View File
@@ -86,20 +86,8 @@ class Scraper(BaseScraper):
"""Scraper for Recalbox es_bios.xml."""
def __init__(self, url: str = SOURCE_URL):
self.url = url
self._raw_data: str | None = None
super().__init__(url=url)
def _fetch_raw(self) -> str:
if self._raw_data is not None:
return self._raw_data
try:
req = urllib.request.Request(self.url, headers={"User-Agent": "retrobios-scraper/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
self._raw_data = resp.read().decode("utf-8")
return self._raw_data
except urllib.error.URLError as e:
raise ConnectionError(f"Failed to fetch {self.url}: {e}") from e
def fetch_requirements(self) -> list[BiosRequirement]:
"""Parse es_bios.xml and return BIOS requirements."""
@@ -274,7 +262,6 @@ def main():
print(json.dumps(config, indent=2))
return
reqs = scraper.fetch_requirements()
by_system = {}
for r in reqs:
by_system.setdefault(r.system, []).append(r)
+3 -65
View File
@@ -32,21 +32,9 @@ class Scraper(BaseScraper):
"""Scraper for RetroBat batocera-systems.json."""
def __init__(self, url: str = SOURCE_URL):
self.url = url
self._raw_data: str | None = None
super().__init__(url=url)
self._parsed: dict | None = None
def _fetch_raw(self) -> str:
if self._raw_data is not None:
return self._raw_data
try:
req = urllib.request.Request(self.url, headers={"User-Agent": "retrobios-scraper/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
self._raw_data = resp.read().decode("utf-8")
return self._raw_data
except urllib.error.URLError as e:
raise ConnectionError(f"Failed to fetch {self.url}: {e}") from e
def _parse_json(self) -> dict:
if self._parsed is not None:
@@ -158,58 +146,8 @@ class Scraper(BaseScraper):
def main():
"""CLI entry point for testing."""
import argparse
parser = argparse.ArgumentParser(description="Scrape RetroBat batocera-systems.json")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--json", action="store_true")
parser.add_argument("--output", "-o")
args = parser.parse_args()
scraper = Scraper()
try:
reqs = scraper.fetch_requirements()
except (ConnectionError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if args.dry_run:
by_system = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
for system, files in sorted(by_system.items()):
print(f"\n{system} ({len(files)} files):")
for f in files:
hash_info = f.md5[:12] if f.md5 else "no-hash"
print(f" {f.name} ({hash_info}...)")
print(f"\nTotal: {len(reqs)} BIOS files across {len(by_system)} systems")
return
if args.json:
config = scraper.generate_platform_yaml()
print(json.dumps(config, indent=2))
return
if args.output:
try:
import yaml
except ImportError:
print("Error: PyYAML required", file=sys.stderr)
sys.exit(1)
config = scraper.generate_platform_yaml()
with open(args.output, "w") as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
print(f"Written to {args.output}")
else:
by_system = {}
for req in reqs:
by_system.setdefault(req.system, []).append(req)
print(f"Scraped {len(reqs)} BIOS files across {len(by_system)} systems")
from scripts.scraper.base_scraper import scraper_cli
scraper_cli(Scraper, "Scrape retrobat BIOS requirements")
if __name__ == "__main__":
+51 -81
View File
@@ -28,7 +28,7 @@ except ImportError:
sys.exit(1)
sys.path.insert(0, os.path.dirname(__file__))
from common import load_platform_config, md5sum, md5_composite
from common import load_platform_config, md5sum, md5_composite, resolve_local_file
DEFAULT_DB = "database.json"
DEFAULT_PLATFORMS_DIR = "platforms"
@@ -54,14 +54,9 @@ def check_inside_zip(container: str, file_name: str, expected_md5: str) -> str:
return Status.OK
with archive.open(fname) as entry:
h = hashlib.md5()
while True:
block = entry.read(65536)
if not block:
break
h.update(block)
actual = md5sum(entry)
if h.hexdigest() == expected_md5:
if actual == expected_md5:
return Status.OK
else:
return Status.UNTESTED
@@ -71,75 +66,13 @@ def check_inside_zip(container: str, file_name: str, expected_md5: str) -> str:
return "error"
def resolve_to_local_path(file_entry: dict, db: dict) -> str | None:
"""Find the local file path for a BIOS entry using database.json.
Tries: SHA1 -> MD5 -> name index. Returns the first existing path found.
For zipped_file entries, the md5 refers to the inner ROM, not the ZIP
container, so MD5-based lookup is skipped to avoid resolving to a
standalone ROM file instead of the ZIP.
"""
sha1 = file_entry.get("sha1")
md5 = file_entry.get("md5")
name = file_entry.get("name", "")
has_zipped_file = bool(file_entry.get("zipped_file"))
files_db = db.get("files", {})
by_md5 = db.get("indexes", {}).get("by_md5", {})
by_name = db.get("indexes", {}).get("by_name", {})
if sha1 and sha1 in files_db:
path = files_db[sha1]["path"]
if os.path.exists(path):
return path
# Skip MD5 lookup for zipped_file entries: the md5 is for the inner ROM,
# not the container ZIP, so matching it would resolve to the wrong file.
if not has_zipped_file:
if md5 and md5 in by_md5:
sha1_match = by_md5[md5]
if sha1_match in files_db:
path = files_db[sha1_match]["path"]
if os.path.exists(path):
return path
# Truncated MD5 (batocera-systems bug: 29 chars instead of 32)
if md5 and len(md5) < 32:
for db_md5, db_sha1 in by_md5.items():
if db_md5.startswith(md5) and db_sha1 in files_db:
path = files_db[db_sha1]["path"]
if os.path.exists(path):
return path
if name in by_name:
# Prefer the candidate whose MD5 matches the expected hash
candidates = []
for match_sha1 in by_name[name]:
if match_sha1 in files_db:
entry = files_db[match_sha1]
path = entry["path"]
if os.path.exists(path):
candidates.append((path, entry.get("md5", "")))
if candidates:
if has_zipped_file:
candidates = [(p, m) for p, m in candidates if p.endswith(".zip")]
if md5 and not has_zipped_file:
md5_lower = md5.lower()
for path, db_md5 in candidates:
if db_md5.lower() == md5_lower:
return path
# Try composite MD5 for ZIP files (Recalbox uses Zip::Md5Composite)
for path, _ in candidates:
if ".zip" in os.path.basename(path):
try:
if md5_composite(path).lower() == md5_lower:
return path
except (zipfile.BadZipFile, OSError):
pass
if candidates:
primary = [p for p, _ in candidates if "/.variants/" not in p]
return primary[0] if primary else candidates[0][0]
return None
def resolve_to_local_path(
file_entry: dict,
db: dict,
zip_contents: dict | None = None,
) -> tuple[str | None, str]:
"""Find the local file path for a BIOS entry. Delegates to common.resolve_local_file."""
return resolve_local_file(file_entry, db, zip_contents)
def verify_entry_existence(file_entry: dict, local_path: str | None) -> dict:
@@ -150,7 +83,11 @@ def verify_entry_existence(file_entry: dict, local_path: str | None) -> dict:
return {"name": name, "status": Status.MISSING}
def verify_entry_md5(file_entry: dict, local_path: str | None) -> dict:
def verify_entry_md5(
file_entry: dict,
local_path: str | None,
resolve_status: str = "",
) -> dict:
"""MD5 verification - supports single MD5 (Batocera) and multi-MD5 (Recalbox)."""
name = file_entry.get("name", "")
expected_md5 = file_entry.get("md5", "")
@@ -190,6 +127,9 @@ def verify_entry_md5(file_entry: dict, local_path: str | None) -> dict:
if not md5_list:
return {"name": name, "status": Status.OK, "path": local_path}
if resolve_status == "md5_exact":
return {"name": name, "status": Status.OK, "path": local_path}
actual_md5 = md5sum(local_path)
# Case-insensitive - Recalbox uses uppercase MD5s
@@ -218,6 +158,26 @@ def verify_entry_md5(file_entry: dict, local_path: str | None) -> dict:
}
def _build_zip_contents_index(db: dict) -> dict:
"""Build index of {inner_rom_md5: zip_file_sha1} for ROMs inside ZIP files."""
index: dict[str, str] = {}
for sha1, entry in db.get("files", {}).items():
path = entry["path"]
if not path.endswith(".zip") or not os.path.exists(path):
continue
try:
with zipfile.ZipFile(path, "r") as zf:
for info in zf.infolist():
if info.is_dir() or info.file_size > 512 * 1024 * 1024:
continue
data = zf.read(info.filename)
inner_md5 = hashlib.md5(data).hexdigest()
index[inner_md5] = sha1
except (zipfile.BadZipFile, OSError):
continue
return index
def verify_platform(config: dict, db: dict) -> dict:
"""Verify all BIOS files for a platform using its verification_mode.
@@ -235,13 +195,23 @@ def verify_platform(config: dict, db: dict) -> dict:
mode = config.get("verification_mode", "existence")
platform = config.get("platform", "unknown")
verify_fn = verify_entry_existence if mode == "existence" else verify_entry_md5
has_zipped = any(
fe.get("zipped_file")
for sys in config.get("systems", {}).values()
for fe in sys.get("files", [])
)
zip_contents = _build_zip_contents_index(db) if has_zipped else {}
results = []
for sys_id, system in config.get("systems", {}).items():
for file_entry in system.get("files", []):
local_path = resolve_to_local_path(file_entry, db)
result = verify_fn(file_entry, local_path)
local_path, resolve_status = resolve_to_local_path(
file_entry, db, zip_contents,
)
if mode == "existence":
result = verify_entry_existence(file_entry, local_path)
else:
result = verify_entry_md5(file_entry, local_path, resolve_status)
result["system"] = sys_id
results.append(result)