mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-24 09:42:36 -05:00
Compare commits
33 Commits
58f3006d20
...
61ca8efc57
| Author | SHA1 | Date | |
|---|---|---|---|
| 61ca8efc57 | |||
| 7653d5d108 | |||
| 76064605c0 | |||
| 08f68e792d | |||
| becd0efb33 | |||
| 09ebaa9316 | |||
| 81278bd2e4 | |||
| a52ab19cf8 | |||
| 300e5d7439 | |||
| 54c0f1d27e | |||
| e218763500 | |||
| 6885681c65 | |||
| 21a50c992f | |||
| 32e4f6e580 | |||
| 0b1ed3cb1a | |||
| 883e153a62 | |||
| b15b062782 | |||
| dd9e59c8e3 | |||
| 3de4bf8190 | |||
| 2466fc4a97 | |||
| 00700609d8 | |||
| 0c367ca7c6 | |||
| 8c18638cd2 | |||
| 7b1c6a723e | |||
| 7ae995fb32 | |||
| a1dc6fa4ef | |||
| b0dad7dcf3 | |||
| 046fb276b0 | |||
| 040ea9f217 | |||
| 84ab0ea6d3 | |||
| 06ea19ee20 | |||
| 97a25b17ff | |||
| 4faae161b4 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
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.
Binary file not shown.
+412
-62
@@ -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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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,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,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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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,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
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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""
|
||||
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"}-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__":
|
||||
|
||||
@@ -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"{{ 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"{{ 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"## {{ 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"{{ 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()
|
||||
@@ -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"
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user