feat: logos, build stamps and exact wording

- The installer says "Every file on the list is in place." where it
  said "Everything is in place.": a check covers the files the
  collection holds, and the required ones it lacks follow.
- The FAQ, first install, legal and home pages state only what the
  code shows (PlayStation 1 and Saturn run without a BIOS on some
  emulators, save files are never touched, records are added).
- MinUI and MyMinUI show their own boot logos, and the Android,
  Linux handheld and iPhone tiles show the Android robot, Tux and a
  plain device icon, each credited in sources.json.
- Each Markdown copy, llms.txt and llms-full.txt name the commit
  they were built from, version.json the current one, and the
  answering rules tell assistants to fetch a stale copy again.
- The deploy lists the pages whose words changed (digests.json) and
  posts them to IndexNow once the site is live.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 10:47:37 +01:00
1 parent f44ef8459e
commit 602945faf0
49 files changed
+501 -117

No files matched your search

+26
View File
@@ -28,6 +28,7 @@ on:
- "scripts/generate_readme.py"
- "scripts/generate_site.py"
- "scripts/hashing.py"
- "scripts/indexnow.py"
- "scripts/largefiles.py"
- "scripts/launchers.py"
- "scripts/manifestselect.py"
@@ -60,6 +61,7 @@ on:
- "scripts/siterender.py"
- "scripts/retired_files.py"
- "scripts/sitesources.py"
- "scripts/sitesteps.py"
- "scripts/sitestarlight.py"
- "scripts/sitewrite.py"
- "scripts/slot.py"
@@ -112,6 +114,8 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
outputs:
indexnow: ${{ steps.indexnow.outputs.payload }}
# Node's compile cache writes under /tmp on every node run, the build
# and the search gate alike.
env:
@@ -184,6 +188,14 @@ jobs:
- name: Require committed README and CONTRIBUTING to be current
run: git diff --exit-code -- README.md CONTRIBUTING.md
# The pages whose words this deploy changes, read against the
# digests the live site still serves. A failure here only loses the
# notice: search engines find the pages on their next visit.
- name: List the pages this deploy changes
id: indexnow
continue-on-error: true
run: python scripts/indexnow.py --dist website/dist --output "$GITHUB_OUTPUT"
- name: Setup Pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
@@ -202,3 +214,17 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
# IndexNow: Bing, Yandex, Seznam and Naver fetch the listed pages
# soon after, so an assistant searching through them reads this build.
- name: Tell search engines which pages changed
if: needs.build.outputs.indexnow != ''
continue-on-error: true
env:
PAYLOAD: ${{ needs.build.outputs.indexnow }}
run: |
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
-H 'Content-Type: application/json; charset=utf-8' \
--data "$PAYLOAD" https://api.indexnow.org/indexnow)
echo "IndexNow answered $code"
case "$code" in 200|202) ;; *) echo "::warning::IndexNow answered $code"; exit 1 ;; esac
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Android</title><path d="M18.44 5.559q-1.015 1.748-2.028 3.498-.055-.023-.111-.043a12.1 12.1 0 0 0-8.68.033C7.537 8.897 5.868 6.026 5.6 5.56a1 1 0 0 0-.141-.19 1.104 1.104 0 0 0-1.768 1.298c1.947 3.37-.096-.216 1.948 3.36.017.03-.495.263-1.393 1.017C2.9 12.176.452 14.772 0 18.99h24a11.7 11.7 0 0 0-.746-3.068 12.1 12.1 0 0 0-2.74-4.184 12 12 0 0 0-2.131-1.687c.66-1.122 1.312-2.256 1.965-3.385a1.11 1.11 0 0 0-.008-1.12 1.1 1.1 0 0 0-.852-.532c-.522-.054-.939.313-1.049.545m-.04 8.46c.395.593.324 1.331-.156 1.65-.48.32-1.188.1-1.582-.493s-.324-1.33.156-1.65c.473-.316 1.182-.11 1.582.494m-11.193-.492c.48.32.55 1.058.156 1.65-.394.593-1.103.815-1.584.495-.48-.32-.55-1.058-.156-1.65.4-.603 1.109-.811 1.584-.495"/></svg>

After

Width:  |  Height:  |  Size: 788 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><rect width="10" height="14" x="3" y="8" rx="2"/><path d="M5 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2h-2.4M8 18h.01"/></svg>

After

Width:  |  Height:  |  Size: 314 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Linux</title><path d="M12.504 0q-.232 0-.48.021c-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.4.4 0 0 0-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071s1.592-.536 2.257-1.306c.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.36.36 0 0 0-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139m.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105l-.004-.021-.004-.024a1.8 1.8 0 0 1-.15.706.95.95 0 0 1-.213.335 1 1 0 0 0-.088-.042c-.104-.045-.198-.064-.284-.133a1.3 1.3 0 0 0-.22-.066c.05-.06.146-.133.183-.198q.08-.193.088-.402v-.02a1.2 1.2 0 0 0-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 0 0-.205.334 1.2 1.2 0 0 0-.09.4v.019q.002.134.02.267c-.193-.067-.438-.135-.607-.202a2 2 0 0 1-.018-.2v-.02a1.8 1.8 0 0 1 .15-.768 1.08 1.08 0 0 1 .43-.533 1 1 0 0 1 .594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2q.018-.136.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.6.6 0 0 0-.166-.267.25.25 0 0 0-.183-.064h-.021c-.071.006-.13.04-.186.132a.55.55 0 0 0-.12.27 1 1 0 0 0-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268q.014.014.034.024c-.07.057-.117.07-.176.136a.3.3 0 0 1-.131.068 2.6 2.6 0 0 1-.275-.402 1.8 1.8 0 0 1-.155-.667 1.8 1.8 0 0 1 .08-.668 1.4 1.4 0 0 1 .283-.535c.128-.133.26-.2.418-.2m1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.57.57 0 0 1 .016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.1 1.1 0 0 1-.448-.067 4 4 0 0 1-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71q-.104-.403.193-.6c.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.7 2.7 0 0 0-.402-.533 1.5 1.5 0 0 0-.275-.333c.182 0 .338-.03.465-.067a.62.62 0 0 0 .314-.334c.108-.267 0-.697-.345-1.163s-.931-.995-1.788-1.521c-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.1 8.1 0 0 1 .647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335q.058.005.11.006c.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01q.08 0 .157.014c.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602q-.553-.1-.723-.4c-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336m7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8 8 0 0 1-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-Line truncated

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+35
View File
@@ -8,6 +8,13 @@
"owner": "AmberELEC",
"licence": "GitHub organisation avatar of AmberELEC"
},
{
"file": "android.svg",
"platform": "android",
"source": "https://raw.githubusercontent.com/simple-icons/simple-icons/7185d367013b99211c2728d5007833a922ec12c7/icons/android.svg",
"owner": "Google",
"licence": "Android robot, file from simple-icons, CC0-1.0"
},
{
"file": "batocera.svg",
"platform": "batocera",
@@ -36,6 +43,13 @@
"owner": "EmuELEC",
"licence": "GitHub organisation avatar of EmuELEC"
},
{
"file": "iphone-and-ipad.svg",
"platform": "iphone-and-ipad",
"source": "https://raw.githubusercontent.com/lucide-icons/lucide/a1b2ce5b7b6a09da2f2f61f682b299f69effb057/icons/tablet-smartphone.svg",
"owner": "Lucide",
"licence": "icon from lucide-icons, ISC"
},
{
"file": "knulli.webp",
"platform": "knulli",
@@ -50,6 +64,20 @@
"owner": "Lakka (libretro)",
"licence": "file from libretro/retroarch-assets, CC-BY-4.0"
},
{
"file": "linux-handhelds.svg",
"platform": "linux-handhelds",
"source": "https://raw.githubusercontent.com/simple-icons/simple-icons/521c96fd04b0ea93034db8715eda5a4de27a58bb/icons/linux.svg",
"owner": "Larry Ewing",
"licence": "Tux, file from simple-icons, CC0-1.0"
},
{
"file": "minui.webp",
"platform": "minui",
"source": "https://raw.githubusercontent.com/shauninman/MinUI/7c8b818e869302780c4083b8a9d6309b6d4a9ccd/skeleton/EXTRAS/Tools/m17/Bootlogo.pak/logo.bmp",
"owner": "Shaun Inman",
"licence": "MinUI boot logo, repository states no licence"
},
{
"file": "misterfpga.webp",
"platform": "misterfpga",
@@ -57,6 +85,13 @@
"owner": "MiSTer FPGA",
"licence": "GitHub organisation avatar of MiSTer-devel"
},
{
"file": "myminui.webp",
"platform": "myminui",
"source": "https://raw.githubusercontent.com/Turro75/MyMinUI/d5ab4f9f83280f4316cb070c529299cb81ccc473/workspace/h700/boot/bootlogo.png",
"owner": "Turro75",
"licence": "MyMinUI boot screen, repository states no licence"
},
{
"file": "nextui.webp",
"platform": "nextui",
+1 -1
View File
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
$defaultInstallSha256 = "bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50"
$defaultInstallSha256 = "4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+3 -3
View File
@@ -379,7 +379,7 @@ COLOUR_ROLES = {
# zero is no news and stays plain: "0 missing" is not a warning.
_STATUS_WORDS = tuple((re.compile(pattern), role) for pattern, role in (
(r"(?<!of )\b[1-9][\d,]* (?:files? )?(?:installed|in place|already in place"
r"|ok|copied|downloaded)\b|\bDone\b|\bEverything is in place\b|^ *ok\b", "success"),
r"|ok|copied|downloaded)\b|\bDone\b|\bEvery file on the list is in place\b|^ *ok\b", "success"),
(r"(?<!of )\b[1-9][\d,]* (?:files? )?(?:failed|refused)\b|\bFAILED\b|\bfailed:"
r"|\bFinished with errors\b|\bNothing was installed\b|\bSomething went wrong\b",
"failure"),
@@ -6312,7 +6312,7 @@ _INVOCATION = Invocation()
# What a run says when every file is in place, on the screen and in line
# mode: the site's "How to check" quotes it (scripts/generate_site.py).
EVERYTHING_IN_PLACE = "Everything is in place."
EVERYTHING_IN_PLACE = "Every file on the list is in place."
# The plan's two rows a beginner reads first: the README and the site name
# them from here, so they say what the screen shows.
TUI_INSTALL_ROW = "Install"
@@ -21351,7 +21351,7 @@ def _plan_lines(check: dict, dest: Path | None, state: TuiState) -> list:
else "")
if not need:
headline = (f"{_plural(data, 'data file')} to fetch{data_size}" if data
else "Everything is in place")
else EVERYTHING_IN_PLACE.rstrip("."))
elif lacking and not fetch and not copied:
# The data files too, when the pack holds none of them: "8252
# files" said nothing of the 2636 it cannot give either.
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
DEFAULT_INSTALL_SHA256="bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50"
DEFAULT_INSTALL_SHA256="4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1125268,
"sha256": "bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50",
"size": 1125299,
"sha256": "4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1178895,
"sha256": "814d80f123e59f5192631842d53a4808bf373599ced857f4622612ffbc25322b"
"size": 1178926,
"sha256": "e323c16d792945916f4352442ee7bcfcb4194f13e50d70150b05684adf2d0d88"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1125268,
"sha256": "bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50",
"size": 1125299,
"sha256": "4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1125268,
"sha256": "bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50",
"size": 1125299,
"sha256": "4b4833da4ae715233cbb075e0efe073b1ddfbe1635014470aa89e22af83dda8d",
"executable": false
},
{
+14 -2
View File
@@ -29,7 +29,7 @@ install_base: https://abdess.github.io/retrobios
trust_items:
- It checks each file before saving it.
- It writes in the BIOS folder. For some emulators it also adds files to the save folder.
- It never changes or deletes a game or a save.
- It never changes or deletes a game or save file.
- >-
It keeps any file it replaces in a `retrobios-backup` folder, next to the BIOS folder.
`--restore` puts it back.
@@ -73,14 +73,24 @@ device_groups:
# ayntec.com/products/ayn-odin-3: "Android 15".
text: Retroid, AYN Odin and other Android handhelds
link: platforms/retroarch.md#where-the-files-go
logo: https://raw.githubusercontent.com/simple-icons/simple-icons/7185d367013b99211c2728d5007833a922ec12c7/icons/android.svg
logo_owner: Google
logo_licence: Android robot, file from simple-icons, CC0-1.0
- name: Linux handhelds
text: ArkOS, muOS, Onion, spruceOS
link: platforms/linux-handhelds.md
logo: https://raw.githubusercontent.com/simple-icons/simple-icons/521c96fd04b0ea93034db8715eda5a4de27a58bb/icons/linux.svg
logo_owner: Larry Ewing
logo_licence: Tux, file from simple-icons, CC0-1.0
# No terminal runs there: the files are copied in, each app its own way
# (which-pack.md, section iPhone and iPad, each step cited).
- name: iPhone and iPad
text: RetroArch, Delta, Provenance
link: which-pack.md#iphone-and-ipad
# A phone and a tablet, no brand: Apple's guidelines allow no use of its logo.
logo: https://raw.githubusercontent.com/lucide-icons/lucide/a1b2ce5b7b6a09da2f2f61f682b299f69effb057/icons/tablet-smartphone.svg
logo_owner: Lucide
logo_licence: icon from lucide-icons, ISC
fpga:
label: MiSTer FPGA
description: >-
@@ -746,7 +756,7 @@ guides:
on the card. A console started by a standalone program instead of a libretro core
(DraStic for NDS, native PICO-8) is not covered.
logo_owner: Shaun Inman
logo_licence: GitHub avatar of shauninman, the author of MinUI
logo_licence: MinUI boot logo, repository states no licence
nextui:
summary: A game menu for TrimUI handhelds, based on MinUI, on the SD card.
# LoveRetro/NextUI README (5291344b) lines 37, 89-94.
@@ -771,6 +781,8 @@ guides:
summary: A game menu for Anbernic and Miyoo handhelds, based on MinUI, on the SD card.
# Turro75/MyMinUI README (72ed4f3d) lines 9-35.
runs_on: Anbernic, Miyoo handhelds
logo_owner: Turro75
logo_licence: MyMinUI boot screen, repository states no licence
folders:
- where: On the SD card
path: '`Bios/<TAG>/`, one folder per console'
+7
View File
@@ -1069,6 +1069,10 @@ platforms:
# The card root: minarch hands each core SDCARD_PATH/Bios/<TAG>
# (workspace/all/minarch/minarch.c:2943).
extract_to: <SD card>/
# The boot logo MinUI writes to the m17's screen, rows stored bottom up.
logo: https://raw.githubusercontent.com/shauninman/MinUI/7c8b818e869302780c4083b8a9d6309b6d4a9ccd/skeleton/EXTRAS/Tools/m17/Bootlogo.pak/logo.bmp
logo_flip: true
logo_trim: true
scraper: minui
# minarch hands each core a folder and names no file (workspace/all/minarch/minarch.c:2943):
# there is no BIOS list of its own to export corrected.
@@ -1124,6 +1128,9 @@ platforms:
# minarch hands each core SDCARD_PATH/Bios/<TAG>
# (workspace/all/minarch/minarch.c:3532).
extract_to: <SD card>/
# The boot screen MyMinUI shows on H700 handhelds.
logo: https://raw.githubusercontent.com/Turro75/MyMinUI/d5ab4f9f83280f4316cb070c529299cb81ccc473/workspace/h700/boot/bootlogo.png
logo_trim: true
scraper: myminui
# minarch hands each core a folder and names no file (workspace/all/minarch/minarch.c:3532):
# there is no BIOS list of its own to export corrected.
+33 -8
View File
@@ -52,14 +52,33 @@ def fetch(url: str) -> bytes:
return response.read()
def square_image(data: bytes, size: int, kind: str = "PNG") -> bytes:
def square_image(data: bytes, size: int, kind: str = "PNG", flip: bool = False,
trim: bool = False) -> bytes:
"""*flip* mirrors the image top to bottom, for a boot logo stored in the
row order a device's screen reads (MinUI's). *trim* cuts the plain
background around the mark, the corner pixel's colour, and keeps that
colour around it: a 640 x 480 boot screen became a dot."""
try:
from PIL import Image
from PIL import Image, ImageChops
except ImportError as exc:
raise SystemExit("Pillow is needed to normalise raster images: pip install Pillow") from exc
image = Image.open(io.BytesIO(data)).convert("RGBA")
if flip:
image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
background = (0, 0, 0, 0)
if trim:
background = image.getpixel((0, 0))
# Faint noise in the background is no part of the mark.
box = (ImageChops.difference(image, Image.new("RGBA", image.size, background))
.convert("L").point(lambda value: 255 if value > 48 else 0).getbbox())
if box:
image = image.crop(box)
side = round(max(image.size) * 1.2)
framed = Image.new("RGBA", (side, side), background)
framed.paste(image, ((side - image.width) // 2, (side - image.height) // 2))
image = framed
image.thumbnail((size, size), Image.LANCZOS)
tile = Image.new("RGBA", (size, size), (0, 0, 0, 0))
tile = Image.new("RGBA", (size, size), background)
tile.paste(image, ((size - image.width) // 2, (size - image.height) // 2), image)
out = io.BytesIO()
if kind == "WEBP":
@@ -130,14 +149,19 @@ def write_manifest(directory: Path, note: str, entries: list[dict]) -> None:
def fetch_logos() -> None:
from siteguides import GUIDES
from siteguides import GROUP_TILES, GUIDES
with open(ROOT / "platforms" / "_registry.yml", encoding="utf-8") as handle:
platforms = yaml.safe_load(handle)["platforms"]
LOGO_DIR.mkdir(parents=True, exist_ok=True)
entries = []
for name, entry in sorted(platforms.items()):
url = entry.get("logo")
# The tiles of a kind of device (Android handhelds) beside the setups
# carry a logo too, named after the tile.
sources = [(name, entry.get("logo"), entry, GUIDES.get(name))
for name, entry in sorted(platforms.items())]
sources += [(re.sub(r"[^a-z0-9]+", "-", tile.name.lower()).strip("-"), tile.logo, {}, tile)
for tiles in GROUP_TILES.values() for tile in tiles]
for name, url, entry, guide in sources:
if not url:
# No logo of its own: the tile shows initials, and an old copy
# would still be served.
@@ -149,13 +173,14 @@ def fetch_logos() -> None:
if is_svg(data):
file_name, payload = f"{name}.svg", minify_svg(shrink_embedded(safe_svg(data, url)))
else:
file_name, payload = f"{name}.webp", square_image(data, LOGO_SIZE, "WEBP")
file_name, payload = f"{name}.webp", square_image(
data, LOGO_SIZE, "WEBP", flip=bool(entry.get("logo_flip")),
trim=bool(entry.get("logo_trim")))
for stale in LOGO_DIR.glob(f"{name}.*"):
if stale.name != file_name:
stale.unlink()
(LOGO_DIR / file_name).write_bytes(payload)
# Who owns each mark: logo_owner and logo_licence in the guides.
guide = GUIDES.get(name)
owner = (guide and guide.logo_owner) or name
licence = (guide and guide.logo_licence) or "ownership not recorded"
entries.append({
+2 -1
View File
@@ -586,7 +586,8 @@ def sample_writers(box: Sandbox) -> dict[str, Callable[[str], str]]:
f"{list_date('retroarch')}:\n\n"
+ _fenced([_bulk_line(box)])),
"run-check": lambda _body: _fenced(
_lines_from(check_run(), "Checking existing files", " Everything is in place")),
_lines_from(check_run(), "Checking existing files",
" Every file on the list is in place")),
"check-refused": lambda _body: _fenced(_refused_lines(box.small("refused"))),
"from-run": lambda _body: _fenced(
_lines_from(box.small("from"), "Done.", count=2)),
+4 -3
View File
@@ -441,7 +441,8 @@ def _tiles(registry: dict, members: list[str], display: dict[str, str], prefix:
str(registry.get(name, {}).get("logo") or ""),
GUIDES[name].runs_on or _runs_on(registry.get(name, {}), GUIDES[name]))
for name in members),
*((tile.name, f"{prefix}{tile.link}", "", tile.text) for tile in GROUP_TILES.get(group, ())),
*((tile.name, f"{prefix}{tile.link}", tile.logo, tile.text)
for tile in GROUP_TILES.get(group, ())),
], eager=eager)
@@ -506,7 +507,7 @@ def generate_home(
"## Is it needed? { #do-i-need-it }",
"",
"- **Yes**, if the emulator says a BIOS is missing.",
"- **Yes** for PlayStation 1 and 2, Saturn and Neo Geo.",
"- **Yes** for PlayStation 2 and Neo Geo.",
"- **Better with it** for Dreamcast: some games need it.",
"- **Usually not** for NES, Super Nintendo, Mega Drive and Game Boy.",
"",
@@ -4201,7 +4202,7 @@ def check_pass_line() -> str:
no file is missing or wrong (install.py EVERYTHING_IN_PLACE), read from
the installer. Typed here, it kept "All files up to date." after the
installer said "Everything is in place."."""
return f"All good when it says `{_installer().EVERYTHING_IN_PLACE}`"
return f"Good result: `{_installer().EVERYTHING_IN_PLACE}`"
# Under "From your computer": a device with no network installs from the ZIP
# (`--from-pack`). The id is the one wiki/installer.md gives its section.
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""The pages a deploy changes, as the IndexNow list a search engine takes.
A search engine revisits a page on its own schedule, and an assistant that
searches through it reads the copy it last saw. IndexNow (Bing, Yandex,
Seznam, Naver) takes a list of changed addresses and fetches them soon
after. Google does not take it and reads the sitemap.
The built site carries `digests.json`, each page's SHA-256 before its build
line (scripts/site_markdown.py). This compares it with the one the live site
serves, before the deploy replaces it: a page whose digest differs, a new
page and a removed one are listed. The deploy job posts the list once the
new site is live (.github/workflows/deploy-site.yml).
python scripts/indexnow.py --dist website/dist # print the list
python scripts/indexnow.py --dist website/dist --output "$GITHUB_OUTPUT"
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from siteagents import INDEXNOW_FILE, INDEXNOW_KEY, SITE # noqa: E402
HOST = "abdess.github.io"
# What one IndexNow request accepts.
MAX_URLS = 10_000
USER_AGENT = "retrobios-indexnow"
def live_digests(url: str = f"{SITE}digests.json") -> dict[str, str]:
"""The digests the live site serves. None yet (the first deploy that
writes them) is an empty map: every page is new."""
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
try:
with urllib.request.urlopen(request, timeout=60) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
if exc.code == 404:
return {}
raise
def changed_urls(new: dict[str, str], old: dict[str, str]) -> list[str]:
"""Pages added, changed or removed between two digest maps."""
return sorted(url for url in new.keys() | old.keys() if new.get(url) != old.get(url))
def payload(urls: list[str]) -> dict:
if len(urls) > MAX_URLS:
raise SystemExit(f"error: {len(urls)} changed pages, one request takes {MAX_URLS}")
return {"host": HOST, "key": INDEXNOW_KEY, "keyLocation": f"{SITE}{INDEXNOW_FILE}",
"urlList": urls}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], allow_abbrev=False)
parser.add_argument("--dist", type=Path, required=True, help="the built site (website/dist)")
parser.add_argument("--output", type=Path,
help="append payload=<json> to this file (a GitHub step output)")
args = parser.parse_args()
new = json.loads((args.dist / "digests.json").read_text(encoding="utf-8"))
urls = changed_urls(new, live_digests())
print(f"{len(urls)} of {len(new)} pages changed", file=sys.stderr)
if not urls:
return 0
body = json.dumps(payload(urls), separators=(",", ":"))
if args.output:
with args.output.open("a", encoding="utf-8") as handle:
handle.write(f"payload={body}\n")
else:
print(body)
return 0
if __name__ == "__main__":
sys.exit(main())
+64 -10
View File
@@ -11,6 +11,13 @@ website/astro.config.mjs), and llms.txt says the convention.
`llms-full.txt` joins the Markdown of the pages a setup is done with:
the start pages, the guides and each setup's page.
An assistant often reads a copy fetched long before, by itself or by a
search engine. Each copy, llms.txt and llms-full.txt name the commit and
the date they were built from, and `version.json` names the current one,
so a stale copy can be told apart. `digests.json` gives each page's
SHA-256 before that line: the next deploy compares it with its own and
tells search engines which pages changed (scripts/indexnow.py).
Run by the Astro build once the pages are written (astro.config.mjs),
on the built site:
@@ -20,8 +27,11 @@ on the built site:
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
from html.parser import HTMLParser
from pathlib import Path
@@ -293,42 +303,86 @@ def page_markdown(html: str, page_url: str) -> str:
return converter.markdown()
def convert_site(dist: Path) -> int:
"""Write index.md beside every page's index.html, then llms-full.txt."""
ROOT = Path(__file__).resolve().parent.parent
def build_stamp() -> dict[str, str]:
"""The commit the site is built from and its date. CI names the commit
in GITHUB_SHA. The date is the commit's, so two builds of one commit
write the same bytes."""
commit = os.environ.get("GITHUB_SHA") or subprocess.run(
["git", "rev-parse", "HEAD"], cwd=ROOT, capture_output=True, text=True,
check=True).stdout.strip()
date = subprocess.run(["git", "log", "-1", "--format=%cI", commit], cwd=ROOT,
capture_output=True, text=True, check=True).stdout.strip()
return {"commit": commit, "date": date}
def stamp_line(stamp: dict[str, str]) -> str:
return (f"> Built from commit {stamp['commit'][:12]} on {stamp['date'][:10]}. "
f"The current build: {SITE}{BASE}version.json")
def stamped(text: str, stamp: dict[str, str]) -> str:
"""*text* with the build line under its first line, the title."""
first, _, rest = text.partition("\n")
return f"{first}\n\n{stamp_line(stamp)}\n{rest}"
def convert_site(dist: Path, stamp: dict[str, str] | None = None) -> int:
"""Write index.md beside every page's index.html, then llms-full.txt,
version.json and digests.json, and stamp llms.txt."""
stamp = stamp or build_stamp()
written = 0
digests: dict[str, str] = {}
for html_path in sorted(dist.rglob("index.html")):
relative = html_path.parent.relative_to(dist).as_posix()
page = "" if relative == "." else f"{relative}/"
url = f"{SITE}{BASE}{page}"
text = page_markdown(html_path.read_text(encoding="utf-8"), url)
(html_path.parent / "index.md").write_text(text, encoding="utf-8")
digests[url] = hashlib.sha256(text.encode("utf-8")).hexdigest()
(html_path.parent / "index.md").write_text(stamped(text, stamp), encoding="utf-8")
written += 1
write_full(dist)
write_full(dist, stamp)
llms = dist / "llms.txt"
if llms.is_file():
text = llms.read_text(encoding="utf-8")
digests[f"{SITE}{BASE}llms.txt"] = hashlib.sha256(text.encode("utf-8")).hexdigest()
# Under the summary, which llms.txt keeps as the quote after its title.
title, summary, rest = text.split("\n\n", 2)
line = stamp_line(stamp).removeprefix("> ")
llms.write_text(f"{title}\n\n{summary}\n\n{line}\n\n{rest}", encoding="utf-8")
(dist / "version.json").write_text(json.dumps(stamp, indent=1) + "\n", encoding="utf-8")
(dist / "digests.json").write_text(
json.dumps(dict(sorted(digests.items())), indent=0) + "\n", encoding="utf-8")
return written
def write_full(dist: Path) -> None:
def _unstamped(text: str, stamp: dict[str, str]) -> str:
"""A page's copy without its build line: llms-full.txt says it once."""
return text.replace(f"\n\n{stamp_line(stamp)}\n", "\n", 1)
def write_full(dist: Path, stamp: dict[str, str]) -> None:
"""llms-full.txt from the pages' Markdown copies."""
parts = ["# RetroBIOS: the guides in one file", "",
parts = ["# RetroBIOS: the guides in one file", "", stamp_line(stamp), "",
"> The pages a setup is done with, in reading order. Every page of the site also has "
"its own Markdown copy at `<page>/index.md`.", ""]
for page in FULL_PAGES:
md = dist / page / "index.md"
if md.is_file():
parts += [md.read_text(encoding="utf-8").rstrip(), "", "---", ""]
parts += [_unstamped(md.read_text(encoding="utf-8"), stamp).rstrip(), "", "---", ""]
# Each setup's own page (the setups the picker lists, not the parts a
# long page is cut into), up to its reference of every file: what to
# do and where, not the full lists, which its own copy keeps. All of
# them was 6.9 MB, past what an assistant reads at once.
import json # noqa: PLC0415
index = dist / "data" / "picker" / "index.json"
setups = [str(item["platform"]) for item in json.loads(index.read_text(encoding="utf-8"))] \
if index.is_file() else []
for setup in [*setups, "linux-handhelds"]:
md = dist / "platforms" / setup / "index.md"
if md.is_file():
text = md.read_text(encoding="utf-8")
text = _unstamped(md.read_text(encoding="utf-8"), stamp)
parts += [text.split("\n## Reference", 1)[0].rstrip(), "", "---", ""]
(dist / "llms-full.txt").write_text("\n".join(parts).rstrip() + "\n", encoding="utf-8")
+12 -2
View File
@@ -73,8 +73,8 @@ def llms_txt(platforms: list[tuple[str, str]]) -> str:
"code: which files an emulator reads, whether it starts without them, where each one "
"goes on every setup, their hashes, and an installer that puts them in place.",
"",
"Every claim about a file cites the line of emulator source code that reads it. The "
"files themselves are served by the GitHub repository "
"Each emulator's files are read from its code, and most cite the file and line that "
"read them. The files themselves are served by the GitHub repository "
"(https://github.com/Abdess/retrobios) and its releases.",
"",
"## Answering a user",
@@ -131,6 +131,9 @@ def llms_txt(platforms: list[tuple[str, str]]) -> str:
# folders and hashes change with each setup, and a guessed one is the
# answer that fails. The same rules end the skill.
ANSWER_RULES = (
"Each Markdown copy, llms.txt and llms-full.txt name the commit and the date they "
f"were built from. When a copy read earlier or found through a search engine names a "
f"commit other than {SITE}version.json, fetch it again: the site changes with each commit.",
"Answer from the pages and data, not from memory. Every page has a Markdown copy: add "
"`index.md` to its address (https://abdess.github.io/retrobios/which-pack/index.md). "
f"[llms-full.txt]({SITE}llms-full.txt) holds the guides and every setup's page in one file.",
@@ -247,9 +250,16 @@ def ard_manifest() -> dict:
}
# IndexNow: the key search engines read back from the site before they take
# a list of changed pages from it (scripts/indexnow.py). Public by design.
INDEXNOW_KEY = "23329654933fb67280e8e7ac749eb869"
INDEXNOW_FILE = "indexnow.txt"
def write(public: Path, platforms: list[tuple[str, str]],
write_if_changed: Callable[[str, str], object]) -> None:
write_if_changed(str(public / "llms.txt"), llms_txt(platforms))
write_if_changed(str(public / INDEXNOW_FILE), INDEXNOW_KEY + "\n")
(public / SKILL_PATH).parent.mkdir(parents=True, exist_ok=True)
write_if_changed(str(public / SKILL_PATH), skill_md())
write_if_changed(str(public / "ard.json"), json.dumps(ard_manifest(), indent=1) + "\n")
+11 -2
View File
@@ -81,6 +81,10 @@ class Tile:
name: str
text: str
link: str
# A logo naming the kind of device, credited like a setup's.
logo: str = ""
logo_owner: str = ""
logo_licence: str = ""
@dataclass(frozen=True)
@@ -129,6 +133,7 @@ _TOP_KEYS = {"install_base", "trust_items", "device_groups", "terminals", "guide
_GROUP_KEYS = {"label", "description", "anchor", "also_runs", "tiles"}
_ALSO_KEYS = ("setup", "summary", "uses", "runs_on", "folder")
_TILE_KEYS = ("name", "text", "link")
_TILE_LOGO_KEYS = ("logo", "logo_owner", "logo_licence")
_HANDHELD_KEYS = {"page", "title", "summary", "uses", "systems"}
_SYSTEM_KEYS = {"name", "folder", "folder_source", "devices", "devices_source", "linux_only"}
_SOURCE = re.compile(r"https://\S+")
@@ -342,8 +347,12 @@ def load_guides(path: Path = GUIDES_FILE, registry: dict | None = None) -> dict:
row_at = f"{at}.tiles[{i}]"
if not isinstance(row, dict):
raise GuideError(f"{row_at}: expected a mapping")
_unknown(row, _TILE_KEYS, row_at)
group_tiles.append(Tile(*(_text(row.get(k), f"{row_at}.{k}") for k in _TILE_KEYS)))
_unknown(row, _TILE_KEYS + _TILE_LOGO_KEYS, row_at)
if row.get("logo") and not all(row.get(k) for k in _TILE_LOGO_KEYS):
raise GuideError(f"{row_at}: a logo needs logo_owner and logo_licence")
group_tiles.append(Tile(
*(_text(row.get(k), f"{row_at}.{k}") for k in _TILE_KEYS),
*(_text(row[k], f"{row_at}.{k}") if row.get(k) else "" for k in _TILE_LOGO_KEYS)))
if group_tiles:
tiles[key] = tuple(group_tiles)
+1 -1
View File
@@ -370,7 +370,7 @@
=== plan-in-place
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+1 -1
View File
@@ -118,7 +118,7 @@
=== plan-in-place
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m
BIOS folder:
^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
+1 -1
View File
@@ -145,7 +145,7 @@
=== plan-in-place
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m
BIOS folder:
^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
+1 -1
View File
@@ -226,7 +226,7 @@
=== plan-in-place
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
^[[0;1m ^[[0;1;32mEvery file on the list is in place^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+1 -1
View File
@@ -1,6 +1,6 @@
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+1 -1
View File
@@ -1,6 +1,6 @@
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
+1 -1
View File
@@ -1,6 +1,6 @@
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
+1 -1
View File
@@ -1,6 +1,6 @@
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+3 -3
View File
@@ -458,7 +458,7 @@
=== plan-in-place 40x12
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -471,7 +471,7 @@
=== plan-in-place 40x15
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -487,7 +487,7 @@
=== plan-in-place 80x24
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+3 -3
View File
@@ -458,7 +458,7 @@
=== plan-in-place 40x12
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -471,7 +471,7 @@
=== plan-in-place 40x15
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -487,7 +487,7 @@
=== plan-in-place 80x24
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+3 -3
View File
@@ -458,7 +458,7 @@
=== plan-in-place 40x12
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -471,7 +471,7 @@
=== plan-in-place 40x15
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -487,7 +487,7 @@
=== plan-in-place 80x24
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+3 -3
View File
@@ -433,7 +433,7 @@
=== plan-in-place 40x12
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -446,7 +446,7 @@
=== plan-in-place 40x15
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -462,7 +462,7 @@
=== plan-in-place 80x24
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+3 -3
View File
@@ -487,7 +487,7 @@
=== plan-in-place 40x12
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -500,7 +500,7 @@
=== plan-in-place 40x15
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -516,7 +516,7 @@
=== plan-in-place 80x24
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+3 -3
View File
@@ -487,7 +487,7 @@
=== plan-in-place 40x12
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -500,7 +500,7 @@
=== plan-in-place 40x15
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
@@ -516,7 +516,7 @@
=== plan-in-place 80x24
RetroBIOS: RetroArch
Everything is in place
Every file on the list is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
+1 -1
View File
@@ -1665,7 +1665,7 @@ class InstallerPagesFollowTheCode(unittest.TestCase):
def test_a_message_the_installer_does_not_print_fails(self):
corpus = _literals()
self.assertEqual(unprinted_messages("It ends `Everything is in place.` here.\n", corpus), [])
self.assertEqual(unprinted_messages("It ends `Every file on the list is in place.` here.\n", corpus), [])
self.assertEqual(len(unprinted_messages("It says `All BIOS files are present.`\n", corpus)), 1)
self.assertEqual(unprinted_messages(
"`E:\\ holds 38 of these files: a run without --check copies them.`\n", corpus), [])
+2 -2
View File
@@ -5674,7 +5674,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest),
"--system", "sony-psp", "--check", "--no-input")
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertNotIn("Everything is in place", proc.stdout)
self.assertNotIn("Every file on the list is in place", proc.stdout)
found = re.search(r"(\d+) in place, (\d+) missing", proc.stdout)
self.assertIsNotNone(found, proc.stdout)
self.assertIn(f"\n {found.group(2)} data files to fetch\n", proc.stdout)
@@ -5949,7 +5949,7 @@ class StandaloneEmulatorsWithoutAPlatformAreServed(unittest.TestCase, _LoopbackR
if name not in named and not name.startswith("scph")], [])
proc = _install_run(self.env, "--no-input", "--check")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("Everything is in place.", proc.stdout)
self.assertIn("Every file on the list is in place.", proc.stdout)
def test_no_emulator_folder_keeps_the_hint(self):
self.duckstation.rmdir()
+2 -2
View File
@@ -13230,7 +13230,7 @@ class OneVocabularyOneCount(unittest.TestCase):
with self.subTest(**given):
text = self.run_line(listing, dest, [""], offline=True, **given)
self.assertIn("2 in place, 0 missing, 0 wrong copies", text)
self.assertIn("Everything is in place", text)
self.assertIn("Every file on the list is in place", text)
self.assertNotIn("up to date", text)
self.assertNotIn("0 files installed", text)
state = checked_state()
@@ -13239,7 +13239,7 @@ class OneVocabularyOneCount(unittest.TestCase):
"failed": []}
state.code = 0
_title, lines = install.tui_outcome(state)
self.assertEqual(lines[0], "Everything is in place.")
self.assertEqual(lines[0], "Every file on the list is in place.")
def test_check_names_a_wrong_copy_as_such(self):
listing, dest, _old = self.two_files()
+11 -3
View File
@@ -324,9 +324,17 @@ class TitleLogoOnAPlate(unittest.TestCase):
self.assertIn('height="40"', image)
def test_a_setup_with_no_logo_gets_its_initials(self):
# Every setup has a logo today: the fallback is read where it is
# written, the tile's and the title's.
import sitemarkup # noqa: PLC0415
self.assertRegex(sitemarkup._tile_mark("MyMinUI", ""), r"<svg[^>]*>.*>MM</text></svg>")
component = _read(REPO_ROOT / "website" / "src" / "components" / "PageTitle.astro")
self.assertIn("{initials(setup.name)}", component)
def test_myminui_shows_its_logo(self):
block = _title_through_heading(_built("platforms/myminui"))
self.assertNotIn("<img", block)
self.assertRegex(block, r"<svg class=\"rb-title-mark[^>]*>.*?>MM</text>")
self.assertIn('src="/retrobios/assets/logos/myminui.webp"', block)
class NoLevelBadgeOnATabRoot(unittest.TestCase):
@@ -616,7 +624,7 @@ class LogosAtTheirDisplaySize(unittest.TestCase):
self.assertEqual(sorted(p.name for p in LOGOS.glob("*.png")), [])
script = _read(REPO_ROOT / "scripts" / "fetch_site_images.py")
self.assertIn(f"LOGO_SIZE = {LOGO_PIXELS}", script)
self.assertIn('square_image(data, LOGO_SIZE, "WEBP")', script)
self.assertIn('square_image(\n data, LOGO_SIZE, "WEBP"', script)
def test_pages_point_at_the_copies(self):
html = _built("platforms/emudeck")
+1 -1
View File
@@ -607,7 +607,7 @@ class YourSetupIndexInPlainWords(unittest.TestCase):
class HomeCopy(unittest.TestCase):
def test_dreamcast_and_the_check_link(self):
home = re.sub(r"</?abbr[^>]*>", "", _page("index.md"))
self.assertIn("- **Yes** for PlayStation 1 and 2, Saturn and Neo Geo.", home)
self.assertIn("- **Yes** for PlayStation 2 and Neo Geo.", home)
self.assertIn("- **Better with it** for Dreamcast: some games need it.", home)
# A question and the link that answers it: no level label, which
# read as a broken crumb beside the Reference tab.
+108
View File
@@ -0,0 +1,108 @@
"""A copy an assistant read long ago can be told from the current one.
Each Markdown copy, llms.txt and llms-full.txt name the commit they were
built from, version.json names the current one, and the next deploy lists
the pages whose words changed for IndexNow."""
from __future__ import annotations
import json
import sys
import tempfile
import unittest
from pathlib import Path
import tests # noqa: F401
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
import indexnow # noqa: E402
import siteagents # noqa: E402
import site_markdown # noqa: E402
STAMP = {"commit": "0123456789abcdef0123456789abcdef01234567", "date": "2026-11-09T08:00:00+01:00"}
# The two parts a built Starlight page gives the copy: its title and its content.
PAGE = ('<html><body><h1 id="_top">Title</h1>'
'<div class="sl-markdown-content"><p>Words of the page.</p></div></body></html>')
def _site(root: Path, words: str = "Words of the page.") -> Path:
dist = root / "dist"
for page in ("", "wiki/faq/"):
(dist / page).mkdir(parents=True, exist_ok=True)
(dist / page / "index.html").write_text(PAGE.replace("Words of the page.", words),
encoding="utf-8")
(dist / "llms.txt").write_text("# RetroBIOS\n\n> The summary.\n\n## Start\n", encoding="utf-8")
return dist
class BuildStamp(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(dir=ROOT / "tmp")
self.dist = _site(Path(self.tmp.name))
site_markdown.convert_site(self.dist, STAMP)
def tearDown(self):
self.tmp.cleanup()
def test_each_copy_names_its_commit_under_its_title(self):
lines = (self.dist / "wiki" / "faq" / "index.md").read_text(encoding="utf-8").splitlines()
self.assertTrue(lines[0].startswith("# "))
self.assertEqual(lines[2], "> Built from commit 0123456789ab on 2026-11-09. "
"The current build: https://abdess.github.io/retrobios/version.json")
def test_version_json_names_the_build(self):
self.assertEqual(json.loads((self.dist / "version.json").read_text(encoding="utf-8")), STAMP)
def test_llms_txt_keeps_its_summary_after_the_title(self):
text = (self.dist / "llms.txt").read_text(encoding="utf-8")
self.assertTrue(text.startswith("# RetroBIOS\n\n> The summary.\n\nBuilt from commit 0123456789ab"))
def test_llms_full_says_the_build_once(self):
text = (self.dist / "llms-full.txt").read_text(encoding="utf-8")
self.assertEqual(text.count("Built from commit"), 1)
def test_digests_ignore_the_build_line(self):
first = json.loads((self.dist / "digests.json").read_text(encoding="utf-8"))
with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as other:
dist = _site(Path(other))
site_markdown.convert_site(dist, {"commit": "f" * 40, "date": "2026-11-10T00:00:00Z"})
second = json.loads((dist / "digests.json").read_text(encoding="utf-8"))
self.assertEqual(first, second)
self.assertIn("https://abdess.github.io/retrobios/llms.txt", first)
class IndexNowList(unittest.TestCase):
def test_added_changed_and_removed_pages_are_listed(self):
old = {"a": "1", "b": "2", "gone": "3"}
new = {"a": "1", "b": "9", "new": "4"}
self.assertEqual(indexnow.changed_urls(new, old), ["b", "gone", "new"])
def test_the_first_deploy_lists_every_page(self):
self.assertEqual(indexnow.changed_urls({"a": "1", "b": "2"}, {}), ["a", "b"])
def test_the_key_is_served_where_the_request_says(self):
body = indexnow.payload(["https://abdess.github.io/retrobios/"])
self.assertEqual(body["keyLocation"], "https://abdess.github.io/retrobios/indexnow.txt")
with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as public:
written: dict[str, str] = {}
siteagents.write(Path(public), [], lambda path, text: written.__setitem__(path, text))
self.assertEqual(written[str(Path(public) / "indexnow.txt")].strip(), body["key"])
def test_the_deploy_posts_the_list_after_the_site_is_live(self):
workflow = (ROOT / ".github" / "workflows" / "deploy-site.yml").read_text(encoding="utf-8")
listed = workflow.index("scripts/indexnow.py --dist website/dist")
deployed = workflow.index("actions/deploy-pages@")
posted = workflow.index("https://api.indexnow.org/indexnow")
self.assertLess(listed, deployed)
self.assertLess(deployed, posted)
class AnswerRule(unittest.TestCase):
def test_assistants_are_told_to_compare_the_commit(self):
self.assertTrue(any("version.json" in rule for rule in siteagents.ANSWER_RULES))
if __name__ == "__main__":
unittest.main()
+12 -5
View File
@@ -82,15 +82,22 @@ class PlatformLogos(unittest.TestCase):
self.assertTrue(self.registry["bizhawk"]["logo"].endswith(
"/src/BizHawk.Client.EmuHawk/images/logo.ico"))
def test_minui_has_no_logo_and_no_stale_copy(self):
self.assertNotIn("logo", self.registry["minui"])
self.assertEqual(list((REPO_ROOT / "docs_assets" / "logos").glob("minui.*")), [])
def test_minui_shows_its_boot_logo_not_its_author(self):
# The author's GitHub avatar is a face, not the project's mark.
self.assertIn("/Bootlogo.pak/", self.registry["minui"]["logo"])
self.assertTrue(self.registry["minui"]["logo_flip"])
def test_every_logo_copy_has_a_registry_logo(self):
def test_every_logo_copy_has_a_registry_or_tile_logo(self):
from siteguides import GROUP_TILES # noqa: PLC0415
tiles = {re.sub(r"[^a-z0-9]+", "-", tile.name.lower()).strip("-"): tile.logo
for group in GROUP_TILES.values() for tile in group}
sources = json.loads((REPO_ROOT / "docs_assets" / "logos" / "sources.json").read_text())
for entry in sources["images"]:
with self.subTest(platform=entry["platform"]):
self.assertEqual(self.registry[entry["platform"]].get("logo"), entry["source"])
logo = (self.registry[entry["platform"]].get("logo")
if entry["platform"] in self.registry else tiles.get(entry["platform"]))
self.assertEqual(logo, entry["source"])
class NodeCompileCache(unittest.TestCase):
+2
View File
@@ -295,6 +295,8 @@ that text belongs in a guide field.
```yaml
logo: https://... # SVG or PNG for UI/docs
logo_flip: true # a raster stored bottom row first (a boot logo)
logo_trim: true # cut the plain background around the mark
inherits_from: retroarch # the parent, for readers: the merge reads
# inherits: in the platform YAML
case_insensitive_fs: true # if the platform runs on case-insensitive filesystems
+10 -12
View File
@@ -9,9 +9,8 @@ terms: ios, iphone, ipad
## Are BIOS files needed? { #do-i-need-bios-files }
- **Yes** for PlayStation 2 and Neo Geo.
- **Most emulators** for PlayStation 1 and Saturn. Some start games without
it, with a built-in replacement.
- **Better with it** for Dreamcast. Some games need it.
- **Most emulators** for PlayStation 1 and Saturn.
- **Better with it** for Dreamcast.
- **Usually not** for NES or Super Nintendo.
[What is a BIOS?](what-are-bios-files.md)
@@ -36,8 +35,8 @@ curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --system sony-playsta
## Does the BIOS set the game's language?
Rarely. It comes from the game. Some follow the console's setting, like Flycast's **Language**
option.
It mostly comes from the game. Some follow the console's setting, like Flycast's
**Language** option.
## Zip or unzip? { #zip-or-unzip }
@@ -62,7 +61,7 @@ Game sets must match the emulator's version:
## Is the command safe? { #is-piping-the-installer-into-a-shell-safe }
- It stops if the installer does not match its expected fingerprint, and
- It stops if the installer does not match its fingerprint, and
checks each file.
- It fills the BIOS folder, some save folders and a games cache.
- It never touches games or save files.
@@ -79,13 +78,13 @@ and download their files.
## Why is there no ZIP for a setup? { #why-is-there-no-zip-for-my-setup }
ZIPs are made for the setups most people download. The command installs every
file the collection holds for each setup and checks each one. `--dry-run` shows the list first.
ZIPs are made for the setups most people download. The command installs each
setup's listed files and checks each one. `--dry-run` shows the list first.
## Fill an SD card from a computer?
The interface finds a muOS, Onion, spruceOS, MinUI, KNULLI, ROCKNIX, Lakka or
MiSTer card. Else name the setup and its BIOS folder. For RetroArch on a Switch,
MiSTer card. Else name its setup and BIOS folder. For RetroArch on a Switch,
with the card in drive `E:`, run:
<!-- os-tabs windows: --platform retroarch --dest E:\retroarch\cores\system -->
@@ -112,12 +111,11 @@ Fill it from a computer or point `--from-pack` at the ZIP: [Without a network](i
## Why is a file missing from the ZIP?
Some come with the emulator. Others are not in the collection yet:
[what is still missing](../gaps.md) names each one.
Some come with the emulator. Others are still missing.
## A game shows a black screen { #my-game-shows-a-black-screen }
Often a missing BIOS: [When a game does not start](troubleshooting.md).
Maybe a missing BIOS: [When a game does not start](troubleshooting.md).
## Can an owned console's BIOS be copied? { #copy-your-own-bios }
+8 -8
View File
@@ -5,7 +5,7 @@ order: 30
# First install, step by step
From an empty BIOS folder to every BIOS file the setup's list names.
From an empty BIOS folder to a checked one.
**Needed:** internet and some free space.
@@ -102,13 +102,12 @@ Esc or Ctrl+C stops, B on a handheld. Run again to resume.
## 5. Read the result
- **Done**: every file the collection holds for this setup is in place. The
screen names any file it does not hold.
- **Done**: every file on the list is in place.
- **Finished with errors**: run it again. It retries only those.
- **Finished with files missing**: run it again online.
- **Nothing installed**: the screen says why. See [If it stops](#if-it-stops).
For line-by-line questions (screen readers), add `--plain`.
Screen readers: add `--plain`.
## What the command does
@@ -123,16 +122,17 @@ Add `--check` to the same command. It changes nothing.
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --check
```
All good:
Result:
```
Everything is in place.
Every file on the list is in place.
```
Missing required files are named below it.
## Undo
Replaced files go to `retrobios-backup` beside the BIOS folder. The result
says how to restore them.
Replaced files go to `retrobios-backup`, and `--restore` puts them back.
## If it stops
+2 -2
View File
@@ -570,11 +570,11 @@ The same command with `--check` reads the folder and writes nothing:
Checking existing files...
18175 in place, 0 missing, 0 wrong copies
Everything is in place.
Every file on the list is in place.
```
<!-- /generated: run-check -->
`Everything is in place.` is said only when the data folders are complete
`Every file on the list is in place.` is said only when the data folders are complete
too. With the BIOS files in place and data files missing (PPSSPP's assets,
Dolphin's `Sys`), the line counts those instead (`169 data files to fetch`),
and `--check` exits with code 3. The files the collection does not hold are
+2 -3
View File
@@ -8,9 +8,8 @@ terms: license
It depends on the country. Nothing here is legal advice.
- US courts held that copying code to build a compatible emulator can be fair
use (*Sega v. Accolade*, *Sony v. Connectix*). EU law allows it under
conditions.
- US courts have held that building an emulator can be fair use. EU law allows
it under conditions.
- Sharing BIOS files is not settled law. The strongest case is old consoles no
longer sold. The weakest is encryption keys (Switch, 3DS, Wii U).
- The files are here for personal backup and preservation.
+3 -4
View File
@@ -53,10 +53,9 @@ file under the BIOS name: zeros, half the file, or another region's BIOS.
Each result is a full record in the
[`measurements/`](https://github.com/Abdess/retrobios/tree/main/measurements)
folder of this project. It names its method, the computer and the exact build
of the emulator. Records are added, not edited. A change to an existing record (scratch paths
removed on 2026-11-05 and 2026-11-06) is a commit of its own, visible in the
folder's history.
folder. It names its method, the computer and the exact build of the emulator.
Records are added. A change to one is a commit of its own.
The results so far ran on these computers:
+1 -1
View File
@@ -69,7 +69,7 @@ Every module in `tests/`, by the area it guards.
| Pack probe | the cores a platform runs for a system, staging grown to a fixed point, runs mapped to pack and manifest entries, the line and its schema, the probe lock per run, that no page shows its lines. One class builds a real pack narrowed to PlayStation and lays it out both ways, from the archive and with `install.py --dest`, and skips without the collection | `test_pack_probe` |
| Automation runner, disk cleanup, asset sync and bit-flip repair | fake jobs, git repositories with a bare origin and session worktrees, a committed file with one bit flipped, canned CI runs in place of `gh` | `test_autorun` |
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_cited_lines`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_pin_build`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_retired_files`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_agents`, `test_site_arcade_boards`, `test_site_availability`, `test_site_bootstrap`, `test_site_built_gate`, `test_site_components`, `test_site_console_folders`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_css`, `test_site_demo`, `test_site_density_cards`, `test_site_emulator_page`, `test_site_emulators`, `test_site_entry`, `test_site_entry_tables`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_inputs`, `test_site_insights`, `test_site_integration`, `test_site_nav`, `test_site_picker`, `test_site_platform_density`, `test_site_platform_hashes`, `test_site_privacy`, `test_site_pronouns`, `test_site_reference`, `test_site_render`, `test_site_search`, `test_site_steps`, `test_site_system_links`, `test_site_system_names`, `test_site_system_sections`, `test_site_systems`, `test_site_validation`, `test_site_weight`, `test_site_wiki`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_version_basis`, `test_workflow_paths` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_cited_lines`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_pin_build`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_retired_files`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_agents`, `test_site_arcade_boards`, `test_site_availability`, `test_site_bootstrap`, `test_site_built_gate`, `test_site_components`, `test_site_console_folders`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_css`, `test_site_demo`, `test_site_density_cards`, `test_site_emulator_page`, `test_site_emulators`, `test_site_entry`, `test_site_entry_tables`, `test_site_exports`, `test_site_freshness`, `test_site_funnel`, `test_site_images`, `test_site_inputs`, `test_site_insights`, `test_site_integration`, `test_site_nav`, `test_site_picker`, `test_site_platform_density`, `test_site_platform_hashes`, `test_site_privacy`, `test_site_pronouns`, `test_site_reference`, `test_site_render`, `test_site_search`, `test_site_steps`, `test_site_system_links`, `test_site_system_names`, `test_site_system_sections`, `test_site_systems`, `test_site_validation`, `test_site_weight`, `test_site_wiki`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_version_basis`, `test_workflow_paths` |
| Games on the card | small DATs, listxml and setup sources, a fake fetcher, fixture install lists, and the committed data against its schemas | `test_arcade_parents`, `test_game_catalogs`, `test_game_headers`, `test_game_regions`, `test_games_index`, `test_romfolders_card`, `test_romfolders_es` |
## Test architecture
+2 -6
View File
@@ -27,8 +27,7 @@ If step 3 fails, the screen says "BIOS missing" or stays black.
## Which consoles need one? { #do-i-need-one }
- **Yes:** PlayStation 2, Neo Geo, PC Engine CD and many home computers.
- **Most emulators:** PlayStation 1 and Saturn. Some start games without it,
with a built-in replacement. The emulator's page says which.
- **Most emulators:** PlayStation 1 and Saturn.
- **Better with it:** Dreamcast. Some games need it.
- **Usually not:** NES, Super Nintendo, Mega Drive, Game Boy and Game Boy
Advance.
@@ -50,13 +49,10 @@ another for Europe.
Some setups check each file's **hash**, a fingerprint of its contents.
Batocera, Recalbox and EmuDeck report a version they do not expect.
Where the collection holds a file, it is the version each setup expects.
[What is still missing](../gaps.md) names the rest.
## Can a game start without it?
Sometimes. Some emulators carry their own code in place of the BIOS. Some games
then glitch or do not start. The real file works best, so the packs include it.
then glitch or do not start.
## Is it legal?