From 602945faf0524f331972faa3ec74f168c5577154 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:47:37 +0100 Subject: [PATCH] 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. --- .github/workflows/deploy-site.yml | 26 +++++ docs_assets/logos/android.svg | 1 + docs_assets/logos/iphone-and-ipad.svg | 1 + docs_assets/logos/linux-handhelds.svg | 1 + docs_assets/logos/minui.webp | Bin 0 -> 828 bytes docs_assets/logos/myminui.webp | Bin 0 -> 1034 bytes docs_assets/logos/sources.json | 35 ++++++ install.ps1 | 2 +- install.py | 6 +- install.sh | 2 +- install/launchers/index.json | 16 +-- platforms/_guides.yml | 16 ++- platforms/_registry.yml | 7 ++ scripts/fetch_site_images.py | 41 +++++-- scripts/generate_installer_doc.py | 3 +- scripts/generate_site.py | 7 +- scripts/indexnow.py | 84 ++++++++++++++ scripts/site_markdown.py | 74 ++++++++++-- scripts/siteagents.py | 14 ++- scripts/siteguides.py | 13 ++- tests/fixtures/tui/colour-120x40.txt | 2 +- tests/fixtures/tui/colour-40x12.txt | 2 +- tests/fixtures/tui/colour-40x15.txt | 2 +- tests/fixtures/tui/colour-80x24.txt | 2 +- tests/fixtures/tui/plan-in-place-120x40.txt | 2 +- tests/fixtures/tui/plan-in-place-40x12.txt | 2 +- tests/fixtures/tui/plan-in-place-40x15.txt | 2 +- tests/fixtures/tui/plan-in-place-80x24.txt | 2 +- tests/fixtures/tui/route-evmapy.txt | 6 +- tests/fixtures/tui/route-gptokeyb.txt | 6 +- tests/fixtures/tui/route-gptokeyb_muxterm.txt | 6 +- tests/fixtures/tui/route-keys.txt | 6 +- tests/fixtures/tui/route-miyoo.txt | 6 +- tests/fixtures/tui/route-muxterm.txt | 6 +- tests/test_doc_pages.py | 2 +- tests/test_install.py | 4 +- tests/test_install_tui.py | 4 +- tests/test_site_components.py | 14 ++- tests/test_site_entry.py | 2 +- tests/test_site_freshness.py | 108 ++++++++++++++++++ tests/test_site_integration.py | 17 ++- wiki/adding-a-platform.md | 2 + wiki/faq.md | 22 ++-- wiki/first-install.md | 16 +-- wiki/installer.md | 4 +- wiki/legal.md | 5 +- wiki/measurements.md | 7 +- wiki/testing-guide.md | 2 +- wiki/what-are-bios-files.md | 8 +- 49 files changed, 501 insertions(+), 117 deletions(-) create mode 100644 docs_assets/logos/android.svg create mode 100644 docs_assets/logos/iphone-and-ipad.svg create mode 100644 docs_assets/logos/linux-handhelds.svg create mode 100644 docs_assets/logos/minui.webp create mode 100644 docs_assets/logos/myminui.webp create mode 100644 scripts/indexnow.py create mode 100644 tests/test_site_freshness.py diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 0f4721b23..b20dfaf7a 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -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 diff --git a/docs_assets/logos/android.svg b/docs_assets/logos/android.svg new file mode 100644 index 000000000..9955b7a0e --- /dev/null +++ b/docs_assets/logos/android.svg @@ -0,0 +1 @@ +Android \ No newline at end of file diff --git a/docs_assets/logos/iphone-and-ipad.svg b/docs_assets/logos/iphone-and-ipad.svg new file mode 100644 index 000000000..8fcb382e6 --- /dev/null +++ b/docs_assets/logos/iphone-and-ipad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs_assets/logos/linux-handhelds.svg b/docs_assets/logos/linux-handhelds.svg new file mode 100644 index 000000000..1b789a53a --- /dev/null +++ b/docs_assets/logos/linux-handhelds.svg @@ -0,0 +1 @@ +Linux \ No newline at end of file diff --git a/docs_assets/logos/minui.webp b/docs_assets/logos/minui.webp new file mode 100644 index 0000000000000000000000000000000000000000..eb9f3a63889652bc3c64eee866b509add60c39c1 GIT binary patch literal 828 zcmV-C1H=4MNk&FA0{{S5MM6+kP&gnc0{{S!7yz9CDo_AW06r}cheDyD53H^L3;|+V zKn1i9fIpJ|8VweaR2rkl0S(5iTtzvZ|J4`cNl;7k0uZ1 zeV`wRA0QvqKe_)D{jmQT)Ia8L^DE|Zfd;2f_5AeVL{(wDWe!zPhexf8% z>a$ivNZPm-DadSKl$3ut6o{e<*9kZv05R(lMc0QuG|==^5JY2qVCqO~py$5I%*@32 zHG9+~bK+kmYEwA;{oSdNFO&5yrWk+#0RI2bZ{Ydaw;DdUL9aN~t_cluj z*tsHmgXSaK>6fiMCIzQ1=i?Wk-~8PWcS1!+635}-R(AcqKaP)?=sEk<^P$_%t5Es} z_pL^i_}ViK`J#M4R1kBvXA-&W!|CI91+yRg-$LxOH8>SBNDBw;Bu$?MIpYb%^UK6r z3&zsH7#~kh(7^jJr7xK)`d%A*{?ETX;Qe|t)m7w*u*!8&Z_qsC5D)>c||4exO3Tbk&X@P8#U~Dve8ew{mb)xAU?v#?U?4ZSIsylt$4HHzbUX zc<=ht#Y!F-M7i~}uha-vZkn?8V=bYTICj#ChPmN zf|Ej4W{0lV-n@b5OQpChn#yBR+$hpzWT1a&yhe>;377H-gCZY*V_Aqy+fxrHIF&ID zg0Pawhg6_+E^nsVZ);XuHhD12_syJ7xBC&Qih!-h@D>S!_ zKeg@9x5R6^=BZgWPu?T?;Fvl6%U(@Y_BGWE5nEx8^FQbXzJ{>#$d`sDKYgtq_vb;l zgkP(DWEeYaVe#AOuXbh%*um6JrBvHC2+RUfGd%(?0=CNGuYTYHmllkCS4Bf({#4vR G0002qTAQE% literal 0 HcmV?d00001 diff --git a/docs_assets/logos/myminui.webp b/docs_assets/logos/myminui.webp new file mode 100644 index 0000000000000000000000000000000000000000..7052a4140391ba930304e53037a1a446ac9b7124 GIT binary patch literal 1034 zcmV+l1oit;Nk&Ej1ONb6MM6+kP&gp=0{{S!7yz9CDo_AW06sAmi9(_wAr@+Fm>>fL zw15ia4$*(1{w;O6cWc+omOoAWk^G|eGuNohm^`Fa6 z>3d)w#6O;YWc;`PP4Mmf|K10-znXu?|8V#Q{y+Tx{O9&>-2d^v|Nf}@n?GF;?1qOz z`TD|vXZoYe_JC)p&IkU=EuH&7+R9Th%K5eEcQynlK3K10Sxd#lI@ zo^zP-wec1o$T94Wo!?I;DfQB=Lu~~h0RH~&uQ5;vzauKpJG|OnZ^cqu1=F?P=2G&d zHR{IIK~>4=FnTIWsZp{Qe`rdzE!s$fx^Cy zyM;`|!Kq)C$T4DXllljP_2~&8A`;-e0Is5Q(6yTlLU5#VXH>e54@M)!;o=^8m%>WB zwnAfNErLX^(2c8HQc6!?5?G14j>oC29xvuc?M_`940!ezp}%YNZtgHI4%L;69A11l z-~LiXDk@am7wJv2RoL;J_#z`3wUCuLS(Hs?9D&a5KS+Zpr?^a8D_)Mc`M;ToPyXIf zP#=w=ey`nU^p5*vd@Wrg6*U3-_iLsIOf!Ek{ufQfAbk@x_EW(xi>YN;t(mS= zRx{4`ojO*qll1mE#@drV{ApdQc?~>)(?J|Z%`KVV2imjOcqP2s688o_EH>Pnz>lGqo`VTY)vRN)0G=q_t7X>vYDZkmMS{$3Qok&B;(VgC7)N3sW?|JM zrNx>OY2rSG5V*7ysX!7&-)@pDph(V4`)`u8S~0|&^R0H!5R8?2dDM7ubhasU1yd+67WX5*T(LCX_6;SABEu;@sjuI5m0FI{WGY$4^Jr91!t E0EC(jDF6Tf literal 0 HcmV?d00001 diff --git a/docs_assets/logos/sources.json b/docs_assets/logos/sources.json index a0a9debe8..30c36e6bc 100644 --- a/docs_assets/logos/sources.json +++ b/docs_assets/logos/sources.json @@ -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", diff --git a/install.ps1 b/install.ps1 index 0b44aa3b4..d04bc71e8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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 } diff --git a/install.py b/install.py index a7a272964..81e14d197 100644 --- a/install.py +++ b/install.py @@ -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"(? 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. diff --git a/install.sh b/install.sh index 1e47b7f91..2928245a0 100755 --- a/install.sh +++ b/install.sh @@ -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() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 8db6e297a..532115760 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -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 }, { diff --git a/platforms/_guides.yml b/platforms/_guides.yml index 7e7a2adb4..7a4fcb3c1 100644 --- a/platforms/_guides.yml +++ b/platforms/_guides.yml @@ -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//`, one folder per console' diff --git a/platforms/_registry.yml b/platforms/_registry.yml index 699cf7369..35eaa4b1d 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -1069,6 +1069,10 @@ platforms: # The card root: minarch hands each core SDCARD_PATH/Bios/ # (workspace/all/minarch/minarch.c:2943). extract_to: / + # 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/ # (workspace/all/minarch/minarch.c:3532). extract_to: / + # 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. diff --git a/scripts/fetch_site_images.py b/scripts/fetch_site_images.py index f698289e9..468ebaef8 100644 --- a/scripts/fetch_site_images.py +++ b/scripts/fetch_site_images.py @@ -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({ diff --git a/scripts/generate_installer_doc.py b/scripts/generate_installer_doc.py index bd391ccce..8414de2e0 100644 --- a/scripts/generate_installer_doc.py +++ b/scripts/generate_installer_doc.py @@ -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)), diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 739c9625f..8bc0c8303 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -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. diff --git a/scripts/indexnow.py b/scripts/indexnow.py new file mode 100644 index 000000000..d06be8f65 --- /dev/null +++ b/scripts/indexnow.py @@ -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= 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()) diff --git a/scripts/site_markdown.py b/scripts/site_markdown.py index db9e5b143..4f99d03cd 100644 --- a/scripts/site_markdown.py +++ b/scripts/site_markdown.py @@ -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 `/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") diff --git a/scripts/siteagents.py b/scripts/siteagents.py index 81d008d3c..60b366943 100644 --- a/scripts/siteagents.py +++ b/scripts/siteagents.py @@ -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") diff --git a/scripts/siteguides.py b/scripts/siteguides.py index 762cf5f0f..f50cc8b85 100644 --- a/scripts/siteguides.py +++ b/scripts/siteguides.py @@ -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) diff --git a/tests/fixtures/tui/colour-120x40.txt b/tests/fixtures/tui/colour-120x40.txt index c4227fd0e..d32e6cc18 100644 --- a/tests/fixtures/tui/colour-120x40.txt +++ b/tests/fixtures/tui/colour-120x40.txt @@ -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. diff --git a/tests/fixtures/tui/colour-40x12.txt b/tests/fixtures/tui/colour-40x12.txt index 76e3a768a..ff7783db6 100644 --- a/tests/fixtures/tui/colour-40x12.txt +++ b/tests/fixtures/tui/colour-40x12.txt @@ -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 diff --git a/tests/fixtures/tui/colour-40x15.txt b/tests/fixtures/tui/colour-40x15.txt index 3d4f54e39..080003c4a 100644 --- a/tests/fixtures/tui/colour-40x15.txt +++ b/tests/fixtures/tui/colour-40x15.txt @@ -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 diff --git a/tests/fixtures/tui/colour-80x24.txt b/tests/fixtures/tui/colour-80x24.txt index 121db8b0c..5a7109179 100644 --- a/tests/fixtures/tui/colour-80x24.txt +++ b/tests/fixtures/tui/colour-80x24.txt @@ -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. diff --git a/tests/fixtures/tui/plan-in-place-120x40.txt b/tests/fixtures/tui/plan-in-place-120x40.txt index 2869a8ecc..0e5b41f91 100644 --- a/tests/fixtures/tui/plan-in-place-120x40.txt +++ b/tests/fixtures/tui/plan-in-place-120x40.txt @@ -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. diff --git a/tests/fixtures/tui/plan-in-place-40x12.txt b/tests/fixtures/tui/plan-in-place-40x12.txt index 8c1faea1c..83ce5207e 100644 --- a/tests/fixtures/tui/plan-in-place-40x12.txt +++ b/tests/fixtures/tui/plan-in-place-40x12.txt @@ -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 diff --git a/tests/fixtures/tui/plan-in-place-40x15.txt b/tests/fixtures/tui/plan-in-place-40x15.txt index 3f07f6881..f5f2d9033 100644 --- a/tests/fixtures/tui/plan-in-place-40x15.txt +++ b/tests/fixtures/tui/plan-in-place-40x15.txt @@ -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 diff --git a/tests/fixtures/tui/plan-in-place-80x24.txt b/tests/fixtures/tui/plan-in-place-80x24.txt index 58631365c..da0702005 100644 --- a/tests/fixtures/tui/plan-in-place-80x24.txt +++ b/tests/fixtures/tui/plan-in-place-80x24.txt @@ -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. diff --git a/tests/fixtures/tui/route-evmapy.txt b/tests/fixtures/tui/route-evmapy.txt index 4ecf7723d..18bc3d791 100644 --- a/tests/fixtures/tui/route-evmapy.txt +++ b/tests/fixtures/tui/route-evmapy.txt @@ -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. diff --git a/tests/fixtures/tui/route-gptokeyb.txt b/tests/fixtures/tui/route-gptokeyb.txt index 0c2ec38af..cce15c7bf 100644 --- a/tests/fixtures/tui/route-gptokeyb.txt +++ b/tests/fixtures/tui/route-gptokeyb.txt @@ -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. diff --git a/tests/fixtures/tui/route-gptokeyb_muxterm.txt b/tests/fixtures/tui/route-gptokeyb_muxterm.txt index 5a52cc315..359cccd65 100644 --- a/tests/fixtures/tui/route-gptokeyb_muxterm.txt +++ b/tests/fixtures/tui/route-gptokeyb_muxterm.txt @@ -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. diff --git a/tests/fixtures/tui/route-keys.txt b/tests/fixtures/tui/route-keys.txt index 912da09d8..1c811c4b0 100644 --- a/tests/fixtures/tui/route-keys.txt +++ b/tests/fixtures/tui/route-keys.txt @@ -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. diff --git a/tests/fixtures/tui/route-miyoo.txt b/tests/fixtures/tui/route-miyoo.txt index 715bb7e85..5b8879086 100644 --- a/tests/fixtures/tui/route-miyoo.txt +++ b/tests/fixtures/tui/route-miyoo.txt @@ -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. diff --git a/tests/fixtures/tui/route-muxterm.txt b/tests/fixtures/tui/route-muxterm.txt index 0d132306f..83f8c5511 100644 --- a/tests/fixtures/tui/route-muxterm.txt +++ b/tests/fixtures/tui/route-muxterm.txt @@ -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. diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index 0b025c755..fd05ee777 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -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), []) diff --git a/tests/test_install.py b/tests/test_install.py index 7fac30ff3..049899d73 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -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() diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 610c70221..6ef4f2147 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -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() diff --git a/tests/test_site_components.py b/tests/test_site_components.py index 1968db1c5..cef9c0b13 100644 --- a/tests/test_site_components.py +++ b/tests/test_site_components.py @@ -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"]*>.*>MM") + 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("]*>.*?>MM") + 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") diff --git a/tests/test_site_entry.py b/tests/test_site_entry.py index 0a4913b10..c258663fd 100644 --- a/tests/test_site_entry.py +++ b/tests/test_site_entry.py @@ -607,7 +607,7 @@ class YourSetupIndexInPlainWords(unittest.TestCase): class HomeCopy(unittest.TestCase): def test_dreamcast_and_the_check_link(self): home = re.sub(r"]*>", "", _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. diff --git a/tests/test_site_freshness.py b/tests/test_site_freshness.py new file mode 100644 index 000000000..f74d07daa --- /dev/null +++ b/tests/test_site_freshness.py @@ -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 = ('

Title

' + '

Words of the page.

') + + +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() diff --git a/tests/test_site_integration.py b/tests/test_site_integration.py index 8b98448eb..b0904d34c 100644 --- a/tests/test_site_integration.py +++ b/tests/test_site_integration.py @@ -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): diff --git a/wiki/adding-a-platform.md b/wiki/adding-a-platform.md index 6dc34aeb8..930e75f79 100644 --- a/wiki/adding-a-platform.md +++ b/wiki/adding-a-platform.md @@ -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 diff --git a/wiki/faq.md b/wiki/faq.md index e8aa98554..9ea6f3ac1 100644 --- a/wiki/faq.md +++ b/wiki/faq.md @@ -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: @@ -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 } diff --git a/wiki/first-install.md b/wiki/first-install.md index dedd8285d..6c9ee71e0 100644 --- a/wiki/first-install.md +++ b/wiki/first-install.md @@ -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 diff --git a/wiki/installer.md b/wiki/installer.md index 689e379ca..04c58e8b9 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -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. ``` -`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 diff --git a/wiki/legal.md b/wiki/legal.md index 7cd0dd89e..dd234aed3 100644 --- a/wiki/legal.md +++ b/wiki/legal.md @@ -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. diff --git a/wiki/measurements.md b/wiki/measurements.md index ae5d0114d..6277748c2 100644 --- a/wiki/measurements.md +++ b/wiki/measurements.md @@ -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: diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index 23197370a..df3807e0b 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -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 diff --git a/wiki/what-are-bios-files.md b/wiki/what-are-bios-files.md index 15e26a961..392fc400d 100644 --- a/wiki/what-are-bios-files.md +++ b/wiki/what-are-bios-files.md @@ -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?