mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
chore: merge origin/main
This commit is contained in:
commit
e6c83859d5
70 files changed
+1605
-896
No files matched your search
+1
-1
@@ -102,7 +102,7 @@ handheld launchers, `bwrap` (bubblewrap) for the probe sandbox, and a Python 3.8
|
||||
with pyte (`python3.8 -m pip install "pyte==0.8.2"`) for the full screen. CI has all three.
|
||||
|
||||
`profile_sync.py` compares the branch with this repository's `main`, and
|
||||
fetches it in a fork's clone. A profile you only edit is not held to
|
||||
fetches it in a fork's clone. An edited profile is not held to
|
||||
`--strict`: its older refs are reviewed by hand.
|
||||
|
||||
The full pipeline is not needed for a contribution: it rebuilds every pack
|
||||
|
||||
@@ -5,186 +5,82 @@
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Abdess/retrobios/actions/workflows/deploy-site.yml"><img src="https://github.com/Abdess/retrobios/actions/workflows/deploy-site.yml/badge.svg" alt="Site"></a>
|
||||
</p>
|
||||
<p align="center"><strong>The BIOS files emulators need, put in the right folder and checked.</strong></p>
|
||||
|
||||
BIOS and firmware files for AmberELEC, Batocera, BizHawk, EmuDeck, EmuELEC, KNULLI, Lakka, MiSTer FPGA, MinUI, MyMinUI, NextUI, ROCKNIX, Recalbox, RetroArch, RetroBat, RetroDECK, RetroPie, and RomM.
|
||||
<p align="center"><a href="https://abdess.github.io/retrobios/">Website</a> · <a href="https://abdess.github.io/retrobios/which-pack/">Get the files</a> · <a href="https://abdess.github.io/retrobios/wiki/troubleshooting/">A game does not start</a> · <a href="https://abdess.github.io/retrobios/wiki/faq/">Questions</a></p>
|
||||
|
||||
A pack holds the files its emulators read, as their source code names them, and leaves out what the emulators install themselves.
|
||||
Some consoles need a small program, the **BIOS**, to start their games: PlayStation 1 and 2, Saturn, Neo Geo, Dreamcast and more. RetroBIOS installs the BIOS and firmware files for AmberELEC, Batocera, BizHawk, EmuDeck, EmuELEC, KNULLI, Lakka, MiSTer FPGA, MinUI, MyMinUI, NextUI, ROCKNIX, Recalbox, RetroArch, RetroBat, RetroDECK, RetroPie, and RomM, checked the way each one checks them.
|
||||
|
||||
## Quick install
|
||||
## Install in one line
|
||||
|
||||
Copy one command into your terminal:
|
||||
**Windows**: open the Start menu, type **PowerShell** and open it. Paste this line (right-click in the window) and press Enter.
|
||||
|
||||
```powershell
|
||||
irm https://abdess.github.io/retrobios/i.ps1 | iex
|
||||
```
|
||||
|
||||
**Linux, macOS, Steam Deck, Android (Termux)**: open a terminal, paste this line and press Enter.
|
||||
|
||||
```bash
|
||||
# Linux / macOS / Steam Deck
|
||||
curl -fsSL https://abdess.github.io/retrobios/i | sh
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://abdess.github.io/retrobios/i.ps1 | iex
|
||||
|
||||
# Android (Termux: it installs Python and asks for storage access)
|
||||
curl -fsSL https://abdess.github.io/retrobios/i | sh
|
||||
|
||||
# SD card mounted on a PC: a muOS, KNULLI, ROCKNIX, Lakka, MiSTer, Onion, spruceOS or MinUI card
|
||||
# is found by itself. For another card, --dest is its BIOS folder
|
||||
# (RetroArch on Switch: retroarch/cores/system, on Android: RetroArch/system)
|
||||
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
|
||||
|
||||
# Options in PowerShell: anything after | iex goes to iex, so the script runs as a block
|
||||
&([scriptblock]::Create((irm https://abdess.github.io/retrobios/i.ps1))) --check
|
||||
|
||||
# With Python 3.8 or newer: the same installer as a package (or pipx run retrobios)
|
||||
uvx retrobios
|
||||
```
|
||||
|
||||
**With Python 3.8 or newer**: `uvx retrobios` runs the same installer, and so does `pipx run retrobios`.
|
||||
|
||||
**A handheld, an SD card, or not sure?** [Pick the device](https://abdess.github.io/retrobios/which-pack/): its page gives the folder and the command. A muOS, Onion, spruceOS, MinUI, KNULLI, ROCKNIX, Lakka or MiSTer SD card plugged into the computer is found by itself.
|
||||
|
||||

|
||||
|
||||
The script opens a full-screen installer, driven with the arrow keys and Enter, or with a controller on a handheld. It finds your setup and its BIOS folder, downloads only the missing files and checks each one. It can also copy the BIOS files from your old SD card.
|
||||
Every option, and what the installer detects and writes, is on the [installer page](https://abdess.github.io/retrobios/wiki/installer/#passing-options).
|
||||
The installer finds the setup and its BIOS folder, installs the files it needs (only what the games need when it finds them on the card) and checks each one. Its screen installs everything, or fewer under **Choose systems**. Arrow keys and Enter drive it, or a controller on a handheld. It sends nothing back ([what is counted](https://abdess.github.io/retrobios/wiki/privacy/)). Options go after the line in the form the [installer page](https://abdess.github.io/retrobios/wiki/installer/#passing-options) gives.
|
||||
|
||||
First time? Follow the [first install, step by step](https://abdess.github.io/retrobios/wiki/first-install/). A game does not start: [what to check](https://abdess.github.io/retrobios/wiki/troubleshooting/). Other questions: the [FAQ](https://abdess.github.io/retrobios/wiki/faq/).
|
||||
First time? [The first install, step by step](https://abdess.github.io/retrobios/wiki/first-install/). A game does not start: [what to check](https://abdess.github.io/retrobios/wiki/troubleshooting/). Other questions: [FAQ](https://abdess.github.io/retrobios/wiki/faq/).
|
||||
|
||||
## Download BIOS packs
|
||||
## Or download a ZIP
|
||||
|
||||
A pack holds its platform's own BIOS list plus the files its emulator cores load. What no pack carries yet is in the [gap analysis](https://abdess.github.io/retrobios/gaps/). Pick your platform, download the ZIP, extract to the BIOS path. Each Download link is a page that keeps its address from one release to the next. The installer above does the same file by file: its screen installs everything, or fewer under **Choose systems**, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.
|
||||
The Files column counts what a pack holds once unzipped.
|
||||
|
||||
A pack over 2 GB comes in several parts (`Pack.part1of2.zip`, `Pack.part2of2.zip`), and every part is needed. Each part is an ordinary ZIP: extract them all into the same folder.
|
||||
|
||||
Every release ships `SHA256SUMS.txt` and a detached signature of it, checkable against `allowed_signers` in this repository: [verifying a release](https://abdess.github.io/retrobios/wiki/release-process/#verifying-a-release).
|
||||
|
||||
The Files column counts what a pack holds once extracted, the figure a file manager shows for the folder. It differs from one platform to the next because a pack carries only what that platform's emulators load. The size is what the files occupy once extracted, and the ZIP itself downloads smaller.
|
||||
|
||||
| Platform | Files | Extracted size | Extract to | Download |
|
||||
|----------|------:|---------------:|-----------|----------|
|
||||
| AmberELEC | - | - | `/storage/roms/` | [Use the command](https://abdess.github.io/retrobios/get/amberelec/) |
|
||||
| Setup | Files | Size | Unzip into | Download |
|
||||
|-------|------:|-----:|------------|----------|
|
||||
| Batocera | 12,810 | 7.5 GB | `/userdata/bios/` | [Download](https://abdess.github.io/retrobios/get/batocera/) |
|
||||
| BizHawk | - | - | `Firmware/` | [Use the command](https://abdess.github.io/retrobios/get/bizhawk/) |
|
||||
| EmuDeck | 620 | 3.2 GB | `~/Emulation/bios/` | [Download](https://abdess.github.io/retrobios/get/emudeck/) |
|
||||
| EmuELEC | - | - | `/storage/roms/` | [Use the command](https://abdess.github.io/retrobios/get/emuelec/) |
|
||||
| KNULLI | - | - | `/userdata/bios/` | [Use the command](https://abdess.github.io/retrobios/get/knulli/) |
|
||||
| Lakka | - | - | `/storage/system/` | [Use the command](https://abdess.github.io/retrobios/get/lakka/) |
|
||||
| MiSTer FPGA | 83 | 29 MB | `/media/fat/games/` | [Download](https://abdess.github.io/retrobios/get/misterfpga/) |
|
||||
| MinUI | - | - | `<SD card>/` | [Use the command](https://abdess.github.io/retrobios/get/minui/) |
|
||||
| MyMinUI | - | - | `<SD card>/` | [Use the command](https://abdess.github.io/retrobios/get/myminui/) |
|
||||
| NextUI | - | - | `<SD card>/` | [Use the command](https://abdess.github.io/retrobios/get/nextui/) |
|
||||
| ROCKNIX | - | - | `/storage/roms/bios/` | [Use the command](https://abdess.github.io/retrobios/get/rocknix/) |
|
||||
| Recalbox | - | - | `/recalbox/share/bios/` | [Use the command](https://abdess.github.io/retrobios/get/recalbox/) |
|
||||
| RetroArch | 8,504 | 6.0 GB | `system/` | [Download](https://abdess.github.io/retrobios/get/retroarch/) |
|
||||
| RetroBat | 12,399 | 7.9 GB | `bios/` | [Download](https://abdess.github.io/retrobios/get/retrobat/) |
|
||||
| RetroDECK | - | - | `~/retrodeck/` | [Use the command](https://abdess.github.io/retrobios/get/retrodeck/) |
|
||||
| RetroPie | - | - | `~/RetroPie/BIOS/` | [Use the command](https://abdess.github.io/retrobios/get/retropie/) |
|
||||
| RomM | 1,087 | 1.5 GB | `library/bios/` | [Download](https://abdess.github.io/retrobios/get/romm/) |
|
||||
|
||||
ZIPs are made for the setups most people download. The others read "Use the command": the installer above installs every file for them and checks each one, and `--dry-run` shows what it would install ([why no ZIP](https://abdess.github.io/retrobios/wiki/faq/#why-is-there-no-zip-for-my-setup)).
|
||||
ZIPs are made for the setups most people download. The others install with the line above, and each page gives the command: [AmberELEC](https://abdess.github.io/retrobios/get/amberelec/), [BizHawk](https://abdess.github.io/retrobios/get/bizhawk/), [EmuELEC](https://abdess.github.io/retrobios/get/emuelec/), [KNULLI](https://abdess.github.io/retrobios/get/knulli/), [Lakka](https://abdess.github.io/retrobios/get/lakka/), [MinUI](https://abdess.github.io/retrobios/get/minui/), [MyMinUI](https://abdess.github.io/retrobios/get/myminui/), [NextUI](https://abdess.github.io/retrobios/get/nextui/), [ROCKNIX](https://abdess.github.io/retrobios/get/rocknix/), [Recalbox](https://abdess.github.io/retrobios/get/recalbox/), [RetroDECK](https://abdess.github.io/retrobios/get/retrodeck/), [RetroPie](https://abdess.github.io/retrobios/get/retropie/).
|
||||
|
||||
## What's included
|
||||
Unzip into the BIOS folder, files straight in it. A big pack comes in parts (`Pack.part1of2.zip`): download every part ([why some setups have no ZIP](https://abdess.github.io/retrobios/wiki/faq/#why-is-there-no-zip-for-my-setup)). Each release lists its files' SHA-256 with a signature ([verifying a release](https://abdess.github.io/retrobios/wiki/release-process/#verifying-a-release)).
|
||||
|
||||
BIOS, firmware, and system files for consoles from Atari to PlayStation 3.
|
||||
These are the files an emulator loads from disk instead of carrying inside itself. Some are required to boot a system, others improve accuracy or unlock a feature, and the packs carry both.
|
||||
## Help
|
||||
|
||||
Each file is checked the way your platform checks it. Most compare a checksum, the fingerprint of a file's contents, which catches a corrupt or unexpected copy. RetroArch, Lakka and RetroPie check that the file is present under its name without reading its contents (`path_is_valid` in RetroArch's `core_info.c`), and MinUI, NextUI and MyMinUI hand each core its folder without opening a file in it: the Coverage table says which applies to you. Independently of that, the collection records five fingerprints per file, and wherever an emulator's code states an expected size or hash, that value is read from its source and rechecked here.
|
||||
- "BIOS missing" or a game does not start: [what to check](https://abdess.github.io/retrobios/wiki/troubleshooting/)
|
||||
- Have a file? [Check it by its hash](https://abdess.github.io/retrobios/files/)
|
||||
- Every console and the files it needs: [systems](https://abdess.github.io/retrobios/systems/)
|
||||
- Asking an AI assistant: it reads the site best from [llms.txt](https://abdess.github.io/retrobios/llms.txt)
|
||||
|
||||
- **1,087 files** the platforms' emulators load are still to be found, and 138 more the collection does not hold and does not serve by name (retail data, per-user keys, user-filled slots), each with its reason. Both are named in the [gap analysis](https://abdess.github.io/retrobios/gaps/)
|
||||
- **18 platforms** supported with platform-specific verification
|
||||
- **623 emulators** profiled from source (RetroArch cores + standalone)
|
||||
- **532 systems** handled by those emulators (NES, SNES, PlayStation, Saturn, Dreamcast, ...)
|
||||
- **19,947 files in the collection**, each with its SHA1, MD5, SHA256, CRC32 and Adler-32 fingerprints: 5,744 system files, 2,975 arcade ROM sets, 11,228 game and engine data files. That is every platform and emulator together, so no pack holds them all: each carries what its own emulators load, counted in the download table
|
||||
- **566 of 5,744 system files** matched to dump-preservation catalogs (No-Intro, Redump, TOSEC). Arcade sets and engine data fall outside what those catalogs index
|
||||
- **15.8 GB** for the whole collection
|
||||
## What makes it different
|
||||
|
||||
## Supported systems
|
||||
- **623 emulators profiled from their source code**: each file an emulator loads, its hash and the line of code that reads it
|
||||
- **18 setups**, each checked its own way: MD5 for Batocera and Recalbox, the file name for RetroArch, as their own code does
|
||||
- **532 systems**, 19,947 files in the collection, [what is still missing](https://abdess.github.io/retrobios/gaps/) named file by file
|
||||
- Public data: [JSON, CSV and SQLite](https://abdess.github.io/retrobios/data/), MIT licensed
|
||||
|
||||
NES, SNES, Nintendo 64, GameCube, Wii, Game Boy, Game Boy Advance, Nintendo DS, Nintendo 3DS, Switch, PlayStation, PlayStation 2, PlayStation 3, PSP, PS Vita, Mega Drive, Saturn, Dreamcast, Game Gear, Master System, Neo Geo, Atari 2600, Atari 7800, Atari Lynx, Atari ST, MSX, PC Engine, TurboGrafx-16, ColecoVision, Intellivision, Commodore 64, Amiga, ZX Spectrum, Arcade (MAME), and 498+ more.
|
||||
|
||||
Full list with per-file details: **[https://abdess.github.io/retrobios/](https://abdess.github.io/retrobios/)**
|
||||
|
||||
## Coverage
|
||||
|
||||
| Platform | On its BIOS list | Files its emulators load | Checked by |
|
||||
|----------|-----------------:|-------------------------:|------------|
|
||||
| AmberELEC | 414/434 | 4,754/4,937 | MD5 |
|
||||
| Batocera | 353/353 | 14,171/15,291 | MD5 |
|
||||
| BizHawk | 118/118 | - | SHA-1 |
|
||||
| EmuDeck | 168/168 | 8,426/8,528 | MD5 |
|
||||
| EmuELEC | 543/572 | 4,788/10,217 | MD5 |
|
||||
| KNULLI | 290/290 | 9,126/15,350 | MD5 |
|
||||
| Lakka | 532/532 | 7,909/7,994 | name only |
|
||||
| MiSTer FPGA | 81/81 | - | MD5 |
|
||||
| MinUI | 0/0 | 78/79 | name only |
|
||||
| MyMinUI | 0/0 | 356/367 | name only |
|
||||
| NextUI | 0/0 | 343/350 | name only |
|
||||
| ROCKNIX | 38/38 | 7,427/9,139 | MD5 |
|
||||
| Recalbox | 351/351 | 3,125/3,344 | MD5 |
|
||||
| RetroArch | 526/526 | 7,918/7,985 | name only |
|
||||
| RetroBat | 348/348 | 5,135/9,659 | MD5 |
|
||||
| RetroDECK | 2,007/2,008 | 6,705/6,992 | MD5 |
|
||||
| RetroPie | 532/532 | 8,117/12,771 | name only |
|
||||
| RomM | 383/383 | 83/874 | size, then any hash |
|
||||
|
||||
Each fraction is what the pack has over what is needed, counting required and optional files alike since both ship. The first column is the BIOS list the platform publishes. The second counts files its emulators load that this list never mentions, found by reading their source code, and it is routinely several times larger. A short fraction means files are still missing, and they are named in the [gap analysis](https://abdess.github.io/retrobios/gaps/).
|
||||
That second number is a floor, not a ceiling: an emulator that accepts any file handed to it names none in its code, so nothing there can be counted.
|
||||
Checked by is the test your platform runs on its own, replicated here from its source code, so the result matches what you would see in the frontend ([how each one works](https://abdess.github.io/retrobios/wiki/verification-modes/)).
|
||||
|
||||
## Build your own pack
|
||||
<details>
|
||||
<summary><strong>For developers: build a pack</strong></summary>
|
||||
|
||||
Packs are built from a clone, and the clone carries the collection: its checkout alone is over 7.7 GB. `git clone --depth 1 https://github.com/Abdess/retrobios.git` leaves the history out. Files kept as release assets are downloaded when a pack needs them. Then generate packs for any platform, emulator, or system:
|
||||
|
||||
```bash
|
||||
# The pack of a platform, as released
|
||||
python scripts/generate_pack.py --platform retroarch --output-dir dist/
|
||||
python scripts/generate_pack.py --platform batocera --output-dir dist/
|
||||
|
||||
# Single emulator or system
|
||||
python scripts/generate_pack.py --emulator dolphin
|
||||
python scripts/generate_pack.py --system sony-playstation-2
|
||||
|
||||
# One region, best first, one file per system and region
|
||||
python scripts/generate_pack.py --platform retroarch --region us,eu,jp
|
||||
python scripts/generate_pack.py --platform retroarch --region us --one-per-slot
|
||||
|
||||
# Firmware only: the game data a list or a core names is left out, each file named
|
||||
python scripts/generate_pack.py --platform emuelec --no-game-data
|
||||
|
||||
# The bare minimum: only the files each core needs to start
|
||||
python scripts/generate_pack.py --platform batocera --required-only
|
||||
|
||||
# List available emulators and systems
|
||||
python scripts/generate_pack.py --list-emulators
|
||||
python scripts/generate_pack.py --list-systems
|
||||
|
||||
# Coverage of this repository's collection per platform, emulator or system
|
||||
# (it reads database.json and bios/: for your own BIOS folder, run the installer with --check)
|
||||
python scripts/verify.py --all
|
||||
python scripts/verify.py --platform batocera
|
||||
python scripts/verify.py --emulator flycast
|
||||
python scripts/verify.py --platform retroarch --verbose # emulator ground truth
|
||||
```
|
||||
|
||||
Requires Python 3.9 or newer and `pyyaml`. The one-line installer alone runs on Python 3.8.
|
||||
Requires Python 3.9 or newer and `pyyaml`. The one-line installer alone runs on Python 3.8. Every tool and option: [tools](https://abdess.github.io/retrobios/wiki/tools/). How the lists are built: [architecture](https://abdess.github.io/retrobios/wiki/architecture/).
|
||||
|
||||
## Documentation site
|
||||
|
||||
The [documentation site](https://abdess.github.io/retrobios/) provides:
|
||||
|
||||
- **Per-platform pages** with file-by-file verification status and hashes
|
||||
- **Per-emulator profiles** with source code references for every file
|
||||
- **Per-system pages** showing which emulators and platforms cover each console
|
||||
- **Gap analysis** identifying missing files and undeclared core requirements
|
||||
- **Cross-reference** mapping files across 18 platforms and 623 emulators
|
||||
- **Versioned data access** through JSON, CSV and SQLite exports with published SHA-256 checksums
|
||||
|
||||
## How it works
|
||||
|
||||
A platform's BIOS list and an emulator's code are maintained separately. Each list is compared with the emulator source code, file by file where a profile exists, and where the two differ the code decides.
|
||||
|
||||
Hashes document what emulator code loads and accepts, not dump provenance. That boundary, and how it relates to preservation catalogs such as No-Intro, is drawn in the [technical questions](https://abdess.github.io/retrobios/wiki/technical-faq/#are-these-files-verified-against-original-hardware-dumps).
|
||||
|
||||
1. **Read emulator source code**: trace every file the code loads, its expected hash and size
|
||||
2. **Cross-reference with platforms**: match against what each platform declares
|
||||
3. **Build packs**: include baseline files plus what each platform's cores need
|
||||
4. **Verify**: run platform-native checks and emulator-level validation
|
||||
</details>
|
||||
|
||||
## Contributors
|
||||
|
||||
@@ -199,7 +95,7 @@ Hashes document what emulator code loads and accepts, not dump provenance. That
|
||||
|
||||
## Community tools
|
||||
|
||||
- [BIOS Preservation Tool](https://github.com/monster-penguin/BIOS-Preservation-Tool) by [monster-penguin](https://github.com/monster-penguin) - scan, verify, and stage your own BIOS collection using RetroBIOS hash metadata
|
||||
- [BIOS Preservation Tool](https://github.com/monster-penguin/BIOS-Preservation-Tool) by [monster-penguin](https://github.com/monster-penguin) - scans, verifies and stages a BIOS collection using RetroBIOS hash metadata
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -211,4 +107,4 @@ The scripts and tooling are released under the [MIT License](LICENSE).
|
||||
The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed.
|
||||
The reasoning, and where it is weakest, is in [the project's legal position](https://abdess.github.io/retrobios/wiki/legal/).
|
||||
|
||||
*Auto-generated on 2026-11-06T23:09:06Z*
|
||||
*Auto-generated on 2026-11-09T06:27:53Z*
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Security policy
|
||||
|
||||
## Scope
|
||||
|
||||
- The one-line installer: `install.sh`, `install.ps1` and the `install.py` they download.
|
||||
- The packs, their manifests under `install/` and the checksums each release publishes.
|
||||
- The website, its scripts and the data it serves under `api/`.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Report it privately from the repository's **Security** tab, with **Report a vulnerability**. A public issue is the wrong place for it: the details stay private until a fix is out.
|
||||
|
||||
A useful report names the file or the page, the steps that show the problem, and what an attacker gains.
|
||||
|
||||
## Supported versions
|
||||
|
||||
The installer and the site served from `main`, and the latest release. Older releases are removed when a new one is published.
|
||||
|
||||
## How downloads are checked
|
||||
|
||||
- `install.sh` and `install.ps1` download `install.py` from a fixed commit and refuse it unless its SHA-256 matches the one they carry.
|
||||
- The installer checks every file against the hash its manifest records before writing it.
|
||||
- Each release lists the SHA-256 of its files in `SHA256SUMS.txt`, signed with the key in `allowed_signers`: [verifying a release](https://abdess.github.io/retrobios/wiki/release-process/#verifying-a-release).
|
||||
|
||||
## Not a security issue
|
||||
|
||||
A file with an unexpected hash, a missing file or a takedown request: [open an issue](https://github.com/Abdess/retrobios/issues/new) with the file's path or hash.
|
||||
@@ -1,9 +1,23 @@
|
||||
{
|
||||
"width": 800,
|
||||
"height": 460,
|
||||
"setup": "ArkOS",
|
||||
"games": 6,
|
||||
"systems": 6,
|
||||
"files": 43,
|
||||
"size": "15.1 MB"
|
||||
"default": {
|
||||
"width": 800,
|
||||
"height": 460,
|
||||
"setup": "ArkOS",
|
||||
"games": 6,
|
||||
"systems": 6,
|
||||
"files": 43,
|
||||
"size": "15.1 MB",
|
||||
"alt": "Recording of the one-line command in a terminal. The installer finds an ArkOS SD card with 6 games, plans 43 BIOS files (15.1 MB) for them, installs them and shows the summary."
|
||||
},
|
||||
"handheld": {
|
||||
"width": 640,
|
||||
"height": 480,
|
||||
"setup": "ArkOS",
|
||||
"games": 6,
|
||||
"systems": 6,
|
||||
"files": 43,
|
||||
"size": "15.1 MB",
|
||||
"pad": "gptokeyb",
|
||||
"alt": "Recording of the full-screen installer on the 640 by 480 screen of a handheld, started from the Tools menu of ArkOS. It finds the 6 games on the card, plans 43 BIOS files (15.1 MB) for them, installs them and shows the summary. The A button chooses, and the bottom line names the buttons."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,43 @@
|
||||
# The installer demo of the "On the device" tab: the full-screen interface
|
||||
# on ArkOS, at the 640x480 screen of an R36S, as the Tools menu launcher
|
||||
# starts it. Run scripts/make_demo.py --variant handheld, which builds the
|
||||
# card and ArkOS's root, serves the repository on loopback, starts VHS on
|
||||
# this file from the repository root and encodes what it writes. Enter is
|
||||
# what gptokeyb sends for A (ports/shared/retrobios.gptk).
|
||||
|
||||
Output tmp/demo/recorded-handheld.mp4
|
||||
|
||||
Set Shell "bash"
|
||||
Set FontFamily "DejaVu Sans Mono"
|
||||
Set FontSize 16
|
||||
Set LineHeight 1.15
|
||||
Set Width 640
|
||||
Set Height 480
|
||||
Set Padding 8
|
||||
Set Framerate 25
|
||||
Set TypingSpeed 35ms
|
||||
Set CursorBlink false
|
||||
Set Theme { "name": "RetroBIOS", "background": "#12131c", "foreground": "#e6e6f0", "cursor": "#aebcff", "selection": "#3a3d52", "black": "#12131c", "red": "#ef9a9a", "green": "#a5d6a7", "yellow": "#ffcc80", "blue": "#90caf9", "magenta": "#aebcff", "cyan": "#aebcff", "white": "#cfd8dc", "brightBlack": "#7c82a3", "brightRed": "#ef9a9a", "brightGreen": "#a5d6a7", "brightYellow": "#ffcc80", "brightBlue": "#90caf9", "brightMagenta": "#c9d2ff", "brightCyan": "#c9d2ff", "brightWhite": "#e6e6f0" }
|
||||
|
||||
# ArkOS's root, the card at /roms, the interface started.
|
||||
Hide
|
||||
Type 'exec "$RETROBIOS_DEMO_SESSION"'
|
||||
Enter
|
||||
Wait+Screen@60s /A Continue/
|
||||
Sleep 300ms
|
||||
Show
|
||||
|
||||
# The first screen: the system found, its BIOS folder.
|
||||
Sleep 300ms
|
||||
Screenshot tmp/demo/poster-handheld.png
|
||||
Sleep 2500ms
|
||||
Enter
|
||||
|
||||
# A: the plan, only the files the games on the card need.
|
||||
Wait+Screen@60s /A Install/
|
||||
Sleep 4500ms
|
||||
Enter
|
||||
|
||||
# A: install. The progress, then the summary.
|
||||
Wait+Screen@60s /RetroBIOS: Done/
|
||||
Sleep 4500ms
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 233 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 36 KiB |
+2
-2
@@ -54,8 +54,8 @@ notes: |
|
||||
Place all ROM files directly in the RetroArch system directory. No subdirectory
|
||||
needed. The core does chdir() to system_dir before loading.
|
||||
|
||||
The default model is MSX2+, which needs MSX2P.ROM and MSX2PEXT.ROM. If you only
|
||||
play cartridge ROMs, these two files are sufficient. For disk games (.dsk), also
|
||||
The default model is MSX2+, which needs MSX2P.ROM and MSX2PEXT.ROM. For cartridge
|
||||
ROMs alone, these two files are sufficient. For disk games (.dsk), also
|
||||
add DISK.ROM and enable "Simulate DiskROM" in core options.
|
||||
|
||||
KANJI.ROM and FMPAC.ROM are loaded automatically when present, no option needed.
|
||||
|
||||
+48
-44
@@ -28,10 +28,10 @@ install_base: https://abdess.github.io/retrobios
|
||||
# (place_save_entries), and retrobios-backup beside the folder written to.
|
||||
trust_items:
|
||||
- It checks each file before saving it.
|
||||
- It writes in your BIOS folder. For some emulators it also adds files to your save folder.
|
||||
- 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 keeps any file it replaces in a `retrobios-backup` folder, next to your BIOS folder.
|
||||
It keeps any file it replaces in a `retrobios-backup` folder, next to the BIOS folder.
|
||||
`--restore` puts it back.
|
||||
|
||||
# Order of the groups on the picker, in the navigation and on the home page.
|
||||
@@ -57,7 +57,7 @@ device_groups:
|
||||
anchor: game-systems
|
||||
handheld:
|
||||
label: Handhelds and TV boxes (Anbernic, Retroid, Miyoo)
|
||||
description: The system your handheld or TV box starts.
|
||||
description: The system a handheld or TV box starts.
|
||||
anchor: handhelds
|
||||
also_runs:
|
||||
- setup: Android (Retroid Pocket, AYN Odin and others)
|
||||
@@ -88,7 +88,7 @@ device_groups:
|
||||
anchor: fpga
|
||||
server:
|
||||
label: Self-hosted game library
|
||||
description: A RomM server. It plays games in your web browser.
|
||||
description: A RomM server. It plays games in the web browser.
|
||||
anchor: self-hosted
|
||||
|
||||
# Ways to run the installer: the tab label, how to open a terminal there, and
|
||||
@@ -97,21 +97,25 @@ terminals:
|
||||
windows:
|
||||
label: Windows
|
||||
how: >-
|
||||
Open the Start menu. Type **PowerShell** and open it. Paste the line. Press Enter.
|
||||
Open Start. Type **PowerShell**, press Enter. Click Copy. Right-click to paste. Press
|
||||
Enter.
|
||||
shell: powershell
|
||||
macos:
|
||||
label: macOS
|
||||
how: >-
|
||||
Open **Terminal**, in Applications, then Utilities. Paste the line. Press Return.
|
||||
Press Cmd+Space. Type **Terminal**, press Return. Click Copy. Press Cmd+V. Press
|
||||
Return.
|
||||
shell: sh
|
||||
linux:
|
||||
label: Linux
|
||||
how: Open a terminal. Paste the line. Press Enter.
|
||||
how: >-
|
||||
Open a terminal, often with Ctrl+Alt+T. Click Copy. Press Ctrl+Shift+V. Press Enter.
|
||||
shell: sh
|
||||
steamdeck:
|
||||
label: Steam Deck
|
||||
how: >-
|
||||
Switch to Desktop Mode. Open **Konsole**. Paste with Ctrl+Shift+V. Press Enter.
|
||||
Hold power, pick **Switch to Desktop**. Open **Konsole**. Click Copy. Press
|
||||
Ctrl+Shift+V. Press Enter.
|
||||
shell: sh
|
||||
android:
|
||||
label: Android (Termux)
|
||||
@@ -128,7 +132,7 @@ terminals:
|
||||
server:
|
||||
label: On the server
|
||||
how: >-
|
||||
Open a terminal on the machine that holds your RomM library. Put the path of its
|
||||
Open a terminal on the machine that holds the RomM library. Put the path of its
|
||||
`bios` folder after `--dest`.
|
||||
shell: sh
|
||||
|
||||
@@ -283,18 +287,18 @@ guides:
|
||||
pack_manual: |
|
||||
Option B: Manual (PC)
|
||||
---------------------
|
||||
1. Find your RetroArch system directory:
|
||||
1. Find the RetroArch system directory:
|
||||
- RetroArch > Settings > Directory > System/BIOS
|
||||
- Default: retroarch/system/
|
||||
2. Extract all files from this archive directly into your system directory
|
||||
2. Extract all files from this archive directly into the system directory
|
||||
3. Overwrite if asked
|
||||
|
||||
Option C: Manual (handheld / SD card)
|
||||
-------------------------------------
|
||||
1. Connect your SD card to your PC
|
||||
1. Connect the SD card to a PC
|
||||
2. Find the BIOS folder in the list below
|
||||
3. Extract all files from this archive directly into that folder
|
||||
4. Eject SD card and reboot your device
|
||||
4. Eject the SD card and reboot the device
|
||||
|
||||
Folders by device:
|
||||
{folders}
|
||||
@@ -314,9 +318,9 @@ guides:
|
||||
root@batocera.local`, password `linux`. Paste the line. Press Enter.
|
||||
folders:
|
||||
- where: On Batocera
|
||||
- where: From Windows, over your home network
|
||||
- where: From Windows, over the home network
|
||||
path: '`\\BATOCERA\share\bios\`'
|
||||
- where: From a Mac or Linux, over your home network
|
||||
- where: From a Mac or Linux, over the home network
|
||||
path: '`smb://batocera/share/bios/`'
|
||||
run: [device]
|
||||
download_hint: From a computer, use the network folder above.
|
||||
@@ -326,7 +330,7 @@ guides:
|
||||
|
||||
Option B: Manual (network share)
|
||||
--------------------------------
|
||||
1. On your PC, open the Batocera network share:
|
||||
1. On a PC, open the Batocera network share:
|
||||
- Windows: \\BATOCERA\share\bios\
|
||||
- Mac/Linux: smb://batocera/share/bios/
|
||||
2. Extract all files from this archive directly into the share
|
||||
@@ -334,13 +338,13 @@ guides:
|
||||
|
||||
Option C: Manual (SD card)
|
||||
--------------------------
|
||||
1. Put the SD card in your PC
|
||||
1. Put the SD card in a PC
|
||||
2. Open the bios folder at the root of the SHARE drive
|
||||
(Batocera itself names it /userdata/bios/)
|
||||
3. Extract all files from this archive directly into that folder
|
||||
|
||||
NOTE: Dreamcast flash memory is named dc_nvmem.bin
|
||||
(if your setup asks for dc_flash.bin, same file).
|
||||
(if the setup asks for dc_flash.bin, same file).
|
||||
logo_owner: Batocera
|
||||
logo_licence: repository licence MIT
|
||||
recalbox:
|
||||
@@ -351,24 +355,24 @@ guides:
|
||||
# a keyboard. SSH user root, password recalboxroot or the one set when flashing 10.0+.
|
||||
terminal: >-
|
||||
With a keyboard, press **Alt+F2**. From a computer, open a terminal over the network
|
||||
with `ssh root@recalbox`, password `recalboxroot` or the one you set. Paste the line.
|
||||
with `ssh root@recalbox`, password `recalboxroot` or the one set when flashing. Paste the line.
|
||||
Press Enter.
|
||||
folders:
|
||||
- where: On Recalbox
|
||||
- where: From Windows, over your home network
|
||||
- where: From Windows, over the home network
|
||||
path: '`\\RECALBOX\share\bios\`'
|
||||
run: [device]
|
||||
download_hint: From a computer, use the network folder above.
|
||||
pack_manual: |
|
||||
Option B: Manual (network share)
|
||||
--------------------------------
|
||||
1. On your PC, open the Recalbox network share:
|
||||
1. On a PC, open the Recalbox network share:
|
||||
- Windows: \\RECALBOX\share\bios\
|
||||
2. Extract all files from this archive directly into the share
|
||||
|
||||
Option C: Manual (SD card)
|
||||
--------------------------
|
||||
1. Put the SD card in your PC
|
||||
1. Put the SD card in a PC
|
||||
2. Open the bios folder at the root of the SHARE drive
|
||||
(Recalbox itself names it /recalbox/share/bios/)
|
||||
3. Extract all files from this archive directly into that folder
|
||||
@@ -384,7 +388,7 @@ guides:
|
||||
path: '`C:\RetroBat\bios\`'
|
||||
run: [windows]
|
||||
notes:
|
||||
- The command finds RetroBat wherever you installed it.
|
||||
- The command finds RetroBat wherever it is installed.
|
||||
reference_notes:
|
||||
- >-
|
||||
RetroBat records its own folder each time it starts, and the installer reads it
|
||||
@@ -392,15 +396,15 @@ guides:
|
||||
pack_manual: |
|
||||
Option B: Manual
|
||||
----------------
|
||||
1. Open your RetroBat installation folder
|
||||
1. Open the RetroBat installation folder
|
||||
2. Navigate to the bios\ subfolder
|
||||
(default: C:\RetroBat\bios\)
|
||||
3. Extract all files from this archive directly into your bios\ folder
|
||||
3. Extract all files from this archive directly into the bios\ folder
|
||||
4. Overwrite if asked
|
||||
logo_owner: RetroBat
|
||||
logo_licence: repository states no licence
|
||||
emudeck:
|
||||
summary: Installs your emulators and adds your games to Steam.
|
||||
summary: Installs emulators and adds the games to Steam.
|
||||
folders:
|
||||
- where: Steam Deck and Linux
|
||||
# EmuDeck functions/helperFunctions.sh:89-99 (getSDPath, the card's mount point)
|
||||
@@ -414,7 +418,7 @@ guides:
|
||||
run: [steamdeck, linux, windows, macos]
|
||||
notes:
|
||||
- >-
|
||||
The `bios` folder is inside the Emulation folder you chose in EmuDeck.
|
||||
The `bios` folder is inside the Emulation folder chosen in EmuDeck.
|
||||
# emudeck-electron src/renderer/translations/en.json:416-418 (b06bfcf5): the
|
||||
# "Bios files checker".
|
||||
finish: >-
|
||||
@@ -440,7 +444,7 @@ guides:
|
||||
folders:
|
||||
- where: On Lakka
|
||||
# lakka.tv/doc/Accessing-Lakka-filesystem: Samba in Settings > Services, share System.
|
||||
- where: From Windows, over your home network
|
||||
- where: From Windows, over the home network
|
||||
path: '`\\LAKKA\System\`'
|
||||
run: [device]
|
||||
notes:
|
||||
@@ -463,7 +467,7 @@ guides:
|
||||
Option B: Via network share
|
||||
--------------------------
|
||||
1. On Lakka: Settings > Services > Samba Enable
|
||||
2. On your PC, open the System share: \\LAKKA\System\
|
||||
2. On a PC, open the System share: \\LAKKA\System\
|
||||
3. Extract all files from this archive directly into that share
|
||||
|
||||
Option C: Via SSH
|
||||
@@ -497,7 +501,7 @@ guides:
|
||||
logo_licence: repository licence GPL-3.0
|
||||
romm:
|
||||
summary: >-
|
||||
A game library on your own server. It plays games in the web browser.
|
||||
A game library on a self-hosted server. It plays games in the web browser.
|
||||
folders:
|
||||
- where: In the RomM library
|
||||
path: '`bios/`, one subfolder per platform'
|
||||
@@ -506,7 +510,7 @@ guides:
|
||||
path: '`<platform>/bios/` for each platform'
|
||||
run: [server]
|
||||
notes:
|
||||
- 'You can also add BIOS files in RomM''s web page.'
|
||||
- 'BIOS files can also be added in RomM''s web page.'
|
||||
dest: /path/to/library/bios
|
||||
# RomM 5.4 places firmware by filesystem.structure.firmware in config.yml,
|
||||
# bios/{platform} by default, and suggests {platform}/bios for a library
|
||||
@@ -539,7 +543,7 @@ guides:
|
||||
run: [device]
|
||||
notes:
|
||||
- >-
|
||||
You can also reach the folder from a computer, over your home network.
|
||||
The folder is also reachable from a computer, over the home network.
|
||||
# rocknix.org/faqs and rocknix.org/play/add-games: SSH off by default, turned on in
|
||||
# Network Settings, user root, the password shown in System Settings > Security.
|
||||
terminal: >-
|
||||
@@ -561,7 +565,7 @@ guides:
|
||||
# AmberELEC 5a3882cb packages/network/samba/config/smb.conf:59-60 shares
|
||||
# /storage/roms as "roms". smbd runs unless samba.disabled exists
|
||||
# (system.d.opt/smbd.service:5).
|
||||
- where: From Windows, over your home network
|
||||
- where: From Windows, over the home network
|
||||
path: '`\\AmberELEC\roms\`'
|
||||
run: [device]
|
||||
# amberelec.org/guides/getting-to-know-amberelec.html: host AmberELEC, root, amberelec.
|
||||
@@ -584,7 +588,7 @@ guides:
|
||||
- where: On EmuELEC
|
||||
# EmuELEC wiki (35b92c78) Backup-and-Recovery-options.md:4 (\\EMUELEC, roms share)
|
||||
# and Bios.md (/storage/roms/bios).
|
||||
- where: From Windows, over your home network
|
||||
- where: From Windows, over the home network
|
||||
path: '`\\EMUELEC\roms\`'
|
||||
run: [device]
|
||||
# EmuELEC wiki How-to-SSH-into-EmuELEC.md:38-44: root, password emuelec.
|
||||
@@ -606,7 +610,7 @@ guides:
|
||||
- where: On KNULLI
|
||||
# knulli.org/play/add-games/network-transfer: \\KNULLI, its share folder, Samba
|
||||
# off by default. The share is /userdata, so bios is under it.
|
||||
- where: From Windows, over your home network
|
||||
- where: From Windows, over the home network
|
||||
path: '`\\KNULLI\share\bios\`'
|
||||
run: [device]
|
||||
notes:
|
||||
@@ -632,7 +636,7 @@ guides:
|
||||
- where: On the Raspberry Pi
|
||||
# RetroPie-Setup scriptmodules/supplementary/samba.sh:49-52 (46128cd2) shares
|
||||
# $home/RetroPie/BIOS as "bios", and docs/Transferring-Roms.md:29 names \\RETROPIE.
|
||||
- where: From Windows, over your home network
|
||||
- where: From Windows, over the home network
|
||||
path: '`\\RETROPIE\bios\`'
|
||||
run: [device]
|
||||
# RetroPie docs/FAQ.md:193 (F4 in EmulationStation, its game menu), 175f5b15. The computer route
|
||||
@@ -653,12 +657,12 @@ guides:
|
||||
pack_manual: |
|
||||
Option B: Via network share
|
||||
--------------------------
|
||||
1. On your PC, open: \\RETROPIE\bios\
|
||||
1. On a PC, open: \\RETROPIE\bios\
|
||||
2. Extract all files from this archive directly into that folder
|
||||
|
||||
Option C: Via SSH
|
||||
-----------------
|
||||
1. SSH into your Pi: ssh pi@retropie
|
||||
1. SSH into the Pi: ssh pi@retropie
|
||||
2. Copy files to ~/RetroPie/BIOS/
|
||||
|
||||
NOTE: the BIOS folder is on the card's Linux partition,
|
||||
@@ -680,10 +684,10 @@ guides:
|
||||
pack_manual: |
|
||||
Option B: Manual
|
||||
----------------
|
||||
1. Open the folder you extracted BizHawk to, the one holding
|
||||
1. Open the folder BizHawk was extracted to, the one holding
|
||||
EmuHawk.exe (Windows) or EmuHawkMono.sh (Linux)
|
||||
2. Navigate to its Firmware subfolder
|
||||
3. Extract all files from this archive directly into your Firmware folder
|
||||
3. Extract all files from this archive directly into the Firmware folder
|
||||
4. In BizHawk: Config > Paths > Firmware should
|
||||
point to this folder
|
||||
logo_owner: TASVideos
|
||||
@@ -698,7 +702,7 @@ guides:
|
||||
path: '`/media/fat/games/`, one subfolder per core'
|
||||
# mister-devel.github.io/MkDocs_MiSTer/setup/games: run samba_on from the Scripts
|
||||
# menu, then \\MiSTer\sdcard\.
|
||||
- where: From Windows, over your home network
|
||||
- where: From Windows, over the home network
|
||||
path: '`\\MiSTer\sdcard\games\`'
|
||||
run: [device]
|
||||
# mister-devel.github.io/MkDocs_MiSTer/advanced/network: F9 for a Linux terminal,
|
||||
@@ -730,7 +734,7 @@ guides:
|
||||
- >-
|
||||
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
|
||||
PlayStation (PS)/` uses `Bios/PS/`.
|
||||
- The command makes each console's folder for you.
|
||||
- The command makes each console's folder.
|
||||
on_card: true
|
||||
reference_notes:
|
||||
- >-
|
||||
@@ -755,7 +759,7 @@ guides:
|
||||
- >-
|
||||
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
|
||||
PlayStation (PS)/` uses `Bios/PS/`.
|
||||
- The command makes each console's folder for you.
|
||||
- The command makes each console's folder.
|
||||
on_card: true
|
||||
reference_notes:
|
||||
- >-
|
||||
@@ -775,7 +779,7 @@ guides:
|
||||
- >-
|
||||
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
|
||||
PlayStation (PS)/` uses `Bios/PS/`.
|
||||
- The command makes each console's folder for you.
|
||||
- The command makes each console's folder.
|
||||
on_card: true
|
||||
reference_notes:
|
||||
- >-
|
||||
|
||||
+114
-268
@@ -476,17 +476,17 @@ def clone_line(db: dict) -> str:
|
||||
)
|
||||
|
||||
|
||||
def demo_facts() -> dict:
|
||||
"""What the installer demo shows, as make_demo.py measured it."""
|
||||
return json.loads((DEMO_DIR / "demo.json").read_text(encoding="utf-8"))
|
||||
def demo_facts(variant: str = "default") -> dict:
|
||||
"""What an installer demo shows, as make_demo.py measured it: one entry
|
||||
per variant (default, handheld)."""
|
||||
facts = json.loads((DEMO_DIR / "demo.json").read_text(encoding="utf-8"))
|
||||
return facts[variant]
|
||||
|
||||
|
||||
def demo_description(facts: dict | None = None) -> str:
|
||||
"""The demo in words, for a reader who cannot see it."""
|
||||
facts = facts or demo_facts()
|
||||
return (f"Recording of the one-line command in a terminal. The installer finds an "
|
||||
f"{facts['setup']} SD card with {facts['games']} games, plans {facts['files']} "
|
||||
f"BIOS files ({facts['size']}) for them, installs them and shows the summary.")
|
||||
"""The demo in words, for a reader who cannot see it: the alt text
|
||||
make_demo.py wrote from what it measured."""
|
||||
return str((facts or demo_facts())["alt"])
|
||||
|
||||
|
||||
def _existing_contributor_block(readme_path: str = "README.md") -> list[str]:
|
||||
@@ -594,6 +594,15 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
|
||||
system_ids = system_index(profiles)
|
||||
|
||||
# The README is the repository's front page and its most read page: a
|
||||
# visitor from a search lands here, often new to terminals. It says what
|
||||
# the project does, gives the one line that installs, shows it, and leads
|
||||
# on to the site, where every detail lives. The figures and the lists it
|
||||
# once carried (coverage, collection, build commands) are the site's.
|
||||
record = release_record.load_advertised(platforms_dir=platforms_dir)
|
||||
extract_paths = _extract_cells(platforms_dir)
|
||||
released = [name for name in sorted(coverages, key=lambda n: coverages[n]["platform"])
|
||||
if release_totals(name, record)[0]]
|
||||
lines = [
|
||||
'<p align="center">',
|
||||
" <picture>",
|
||||
@@ -602,284 +611,121 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
" </picture>",
|
||||
"</p>",
|
||||
"",
|
||||
'<p align="center">',
|
||||
' <a href="https://github.com/Abdess/retrobios/actions/workflows/deploy-site.yml">'
|
||||
'<img src="https://github.com/Abdess/retrobios/actions/workflows/deploy-site.yml/badge.svg" alt="Site"></a>',
|
||||
'<p align="center"><strong>The BIOS files emulators need, put in the right folder and checked.</strong></p>',
|
||||
"",
|
||||
'<p align="center">'
|
||||
f'<a href="{SITE_URL}">Website</a> · '
|
||||
f'<a href="{SITE_URL}which-pack/">Get the files</a> · '
|
||||
f'<a href="{SITE_URL}wiki/troubleshooting/">A game does not start</a> · '
|
||||
f'<a href="{SITE_URL}wiki/faq/">Questions</a>'
|
||||
"</p>",
|
||||
"",
|
||||
f"BIOS and firmware files for {_names_list(coverages)}.",
|
||||
"Some consoles need a small program, the **BIOS**, to start their games: PlayStation 1"
|
||||
" and 2, Saturn, Neo Geo, Dreamcast and more. RetroBIOS installs the BIOS and firmware"
|
||||
f" files for {_names_list(coverages)}, checked the way each one checks them.",
|
||||
"",
|
||||
"A pack holds the files its emulators read, as their source code names"
|
||||
" them, and leaves out what the emulators install themselves.",
|
||||
"## Install in one line",
|
||||
"",
|
||||
"## Quick install",
|
||||
"**Windows**: open the Start menu, type **PowerShell** and open it. Paste this line"
|
||||
" (right-click in the window) and press Enter.",
|
||||
"",
|
||||
"Copy one command into your terminal:",
|
||||
"```powershell",
|
||||
install_command("powershell"),
|
||||
"```",
|
||||
"",
|
||||
"**Linux, macOS, Steam Deck, Android (Termux)**: open a terminal, paste this line"
|
||||
" and press Enter.",
|
||||
"",
|
||||
"```bash",
|
||||
"# Linux / macOS / Steam Deck",
|
||||
install_command("sh"),
|
||||
"",
|
||||
"# Windows (PowerShell)",
|
||||
install_command("powershell"),
|
||||
"",
|
||||
"# Android (Termux: it installs Python and asks for storage access)",
|
||||
install_command("sh"),
|
||||
"",
|
||||
"# SD card mounted on a PC: a muOS, KNULLI, ROCKNIX, Lakka, MiSTer, Onion, spruceOS or MinUI card",
|
||||
"# is found by itself. For another card, --dest is its BIOS folder",
|
||||
"# (RetroArch on Switch: retroarch/cores/system, on Android: RetroArch/system)",
|
||||
install_command("sh", "--platform retroarch --dest /path/to/sdcard/retroarch/cores/system"),
|
||||
"",
|
||||
"# Options in PowerShell: anything after | iex goes to iex, so the script runs as a block",
|
||||
install_command("powershell", "--check"),
|
||||
"",
|
||||
"# With Python 3.8 or newer: the same installer as a package (or pipx run retrobios)",
|
||||
PACKAGE_COMMAND,
|
||||
"```",
|
||||
"",
|
||||
f"**With Python 3.8 or newer**: `{PACKAGE_COMMAND}` runs the same installer, and so"
|
||||
" does `pipx run retrobios`.",
|
||||
"",
|
||||
f"**A handheld, an SD card, or not sure?** [Pick the device]({SITE_URL}which-pack/):"
|
||||
" its page gives the folder and the command. A muOS, Onion, spruceOS, MinUI, KNULLI,"
|
||||
" ROCKNIX, Lakka or MiSTer SD card plugged into the computer is found by itself.",
|
||||
"",
|
||||
f"",
|
||||
"",
|
||||
"The script opens a full-screen installer, driven with the arrow keys and Enter, or with a"
|
||||
" controller on a handheld. It finds your setup and its BIOS folder, downloads only the missing"
|
||||
" files and checks each one. It can also copy the BIOS files from your old SD card.",
|
||||
f"Every option, and what the installer detects and writes, is on the [installer page]({SITE_URL}wiki/installer/#passing-options).",
|
||||
"The installer finds the setup and its BIOS folder, installs the files it needs (only"
|
||||
" what the games need when it finds them on the card) and checks each one. Its screen"
|
||||
" installs everything, or fewer under **" + _installer_words().TUI_SYSTEMS_ROW + "**."
|
||||
" Arrow keys and Enter drive it, or a controller on a handheld. It sends nothing back"
|
||||
f" ([what is counted]({SITE_URL}wiki/privacy/)). Options go after the line in the"
|
||||
f" form the [installer page]({SITE_URL}wiki/installer/#passing-options) gives.",
|
||||
"",
|
||||
f"First time? Follow the [first install, step by step]({SITE_URL}wiki/first-install/)."
|
||||
f"First time? [The first install, step by step]({SITE_URL}wiki/first-install/)."
|
||||
f" A game does not start: [what to check]({SITE_URL}wiki/troubleshooting/)."
|
||||
f" Other questions: the [FAQ]({SITE_URL}wiki/faq/).",
|
||||
f" Other questions: [FAQ]({SITE_URL}wiki/faq/).",
|
||||
"",
|
||||
"## Download BIOS packs",
|
||||
"## Or download a ZIP",
|
||||
"",
|
||||
f"A pack holds its platform's own BIOS list plus the files its emulator cores load. What no pack carries yet is in the [gap analysis]({SITE_URL}gaps/). Pick your platform, download the ZIP, extract to the BIOS path. Each Download link is a page that keeps its address from one release to the next. The installer above does the same file by file: its screen installs everything, or fewer under **" + _installer_words().TUI_SYSTEMS_ROW + "**, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.",
|
||||
"",
|
||||
"A pack over 2 GB comes in several parts (`Pack.part1of2.zip`,"
|
||||
" `Pack.part2of2.zip`), and every part is needed. Each part is an"
|
||||
" ordinary ZIP: extract them all into the same folder.",
|
||||
"",
|
||||
"Every release ships `SHA256SUMS.txt` and a detached signature of it,"
|
||||
" checkable against `allowed_signers` in this repository:"
|
||||
" [verifying a release]"
|
||||
"(https://abdess.github.io/retrobios/wiki/release-process/"
|
||||
"#verifying-a-release).",
|
||||
"",
|
||||
"The Files column counts what a pack holds once extracted, the figure a"
|
||||
" file manager shows for the folder. It differs from one platform to"
|
||||
" the next because a pack carries only what that platform's emulators"
|
||||
" load. The size is what the files occupy once extracted, and the ZIP"
|
||||
" itself downloads smaller.",
|
||||
"The Files column counts what a pack holds once unzipped.",
|
||||
"",
|
||||
"| Setup | Files | Size | Unzip into | Download |",
|
||||
"|-------|------:|-----:|------------|----------|",
|
||||
]
|
||||
|
||||
# Where the pack itself is extracted, which is not always the BIOS folder:
|
||||
# a pack whose entries already carry their own root (RetroDECK) extracts
|
||||
# one level above it.
|
||||
extract_paths = _extract_cells(platforms_dir)
|
||||
|
||||
lines.extend(
|
||||
download_table(
|
||||
coverages, extract_paths,
|
||||
release_record.load_advertised(platforms_dir=platforms_dir),
|
||||
)
|
||||
)
|
||||
lines.extend(["", no_pack_line()])
|
||||
|
||||
lines.extend(_paragraphs(extract_notes(platforms_dir, released_only=True)))
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## What's included",
|
||||
"",
|
||||
"BIOS, firmware, and system files for consoles from Atari to PlayStation 3.",
|
||||
"These are the files an emulator loads from disk instead of"
|
||||
" carrying inside itself. Some are required to boot a system,"
|
||||
" others improve accuracy or unlock a feature, and the packs carry"
|
||||
" both.",
|
||||
"",
|
||||
"Each file is checked the way your platform checks it. Most compare"
|
||||
" a checksum, the fingerprint of a file's contents, which catches a"
|
||||
" corrupt or unexpected copy. RetroArch, Lakka and RetroPie check"
|
||||
" that the file is present under its name without reading its"
|
||||
" contents (`path_is_valid` in RetroArch's `core_info.c`), and"
|
||||
" MinUI, NextUI and MyMinUI hand each core its folder without"
|
||||
" opening a file in it: the"
|
||||
" Coverage table says which applies to you. Independently of that,"
|
||||
" the collection records five fingerprints per file, and wherever"
|
||||
" an emulator's code states an expected size or hash, that value is"
|
||||
" read from its source and rechecked here.",
|
||||
"",
|
||||
(
|
||||
f"- **{acquirable_total:,} files** the platforms' emulators load"
|
||||
f" are still to be found, and {unsourceable_total:,} more the"
|
||||
f" collection does not hold and does not serve by name (retail"
|
||||
f" data, per-user keys, user-filled slots), each with its reason."
|
||||
f" Both are named in the"
|
||||
f" [gap analysis]({SITE_URL}gaps/)"
|
||||
if missing_total
|
||||
else "- **Nothing missing**: every file the platforms'"
|
||||
" emulators load is in the collection"
|
||||
),
|
||||
f"- **{len(coverages)} platforms** supported with platform-specific verification",
|
||||
f"- **{emulator_count} emulators** profiled from source (RetroArch cores + standalone)",
|
||||
f"- **{len(system_ids)} systems** handled by those emulators (NES, SNES, PlayStation, Saturn, Dreamcast, ...)",
|
||||
collection_line(total_files, comp),
|
||||
*_catalog_matched_line(db),
|
||||
f"- **{format_size(total_size)}** for the whole collection",
|
||||
"",
|
||||
"## Supported systems",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
# Show well-known systems for SEO, link to full list
|
||||
well_known = [
|
||||
"NES",
|
||||
"SNES",
|
||||
"Nintendo 64",
|
||||
"GameCube",
|
||||
"Wii",
|
||||
"Game Boy",
|
||||
"Game Boy Advance",
|
||||
"Nintendo DS",
|
||||
"Nintendo 3DS",
|
||||
"Switch",
|
||||
"PlayStation",
|
||||
"PlayStation 2",
|
||||
"PlayStation 3",
|
||||
"PSP",
|
||||
"PS Vita",
|
||||
"Mega Drive",
|
||||
"Saturn",
|
||||
"Dreamcast",
|
||||
"Game Gear",
|
||||
"Master System",
|
||||
"Neo Geo",
|
||||
"Atari 2600",
|
||||
"Atari 7800",
|
||||
"Atari Lynx",
|
||||
"Atari ST",
|
||||
"MSX",
|
||||
"PC Engine",
|
||||
"TurboGrafx-16",
|
||||
"ColecoVision",
|
||||
"Intellivision",
|
||||
"Commodore 64",
|
||||
"Amiga",
|
||||
"ZX Spectrum",
|
||||
"Arcade (MAME)",
|
||||
]
|
||||
lines.extend(
|
||||
[
|
||||
", ".join(well_known) + f", and {len(system_ids) - len(well_known)}+ more.",
|
||||
"",
|
||||
f"Full list with per-file details: **[{SITE_URL}]({SITE_URL})**",
|
||||
"",
|
||||
"## Coverage",
|
||||
"",
|
||||
"| Platform | On its BIOS list | Files its emulators load | Checked by |",
|
||||
"|----------|-----------------:|-------------------------:|------------|",
|
||||
]
|
||||
)
|
||||
|
||||
for name, cov in sorted(coverages.items(), key=lambda x: x[1]["platform"]):
|
||||
display = cov["platform"]
|
||||
checked = check_label(cov["mode"], cov["config"].get("hash_rule"))
|
||||
core_total = (
|
||||
cov["core_present"] + cov["core_missing"] + cov["core_unsourceable"]
|
||||
)
|
||||
core_cell = f"{cov['core_present']:,}/{core_total:,}" if core_total else "-"
|
||||
for name in released:
|
||||
files, size = release_totals(name, record)
|
||||
display = coverages[name]["platform"]
|
||||
lines.append(
|
||||
f"| {display} | {cov['present']:,}/{cov['total']:,} |"
|
||||
f" {core_cell} | {checked} |"
|
||||
f"| {display} | {files:,} | {format_size(size) if size else '-'} |"
|
||||
f" {extract_paths.get(display, '')} |"
|
||||
f" {download_links(name, record, 'Download', site=SITE_URL)} |"
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"Each fraction is what the pack has over what is needed, counting"
|
||||
" required and optional files alike since both ship. The first"
|
||||
" column is the BIOS list the platform publishes. The second counts"
|
||||
" files its emulators load that this list never mentions, found by"
|
||||
" reading their source code, and it is routinely several times"
|
||||
" larger. A short fraction means files are still missing, and they"
|
||||
" are named in the"
|
||||
f" [gap analysis]({SITE_URL}gaps/).",
|
||||
"That second number is a floor, not a ceiling: an emulator that"
|
||||
" accepts any file handed to it names none in its code, so nothing"
|
||||
" there can be counted.",
|
||||
"Checked by is the test your platform runs on its own, replicated"
|
||||
" here from its source code, so the result matches what you would"
|
||||
f" see in the frontend ([how each one works]({SITE_URL}wiki/verification-modes/)).",
|
||||
"",
|
||||
"## Build your own pack",
|
||||
"",
|
||||
clone_line(db),
|
||||
"",
|
||||
"```bash",
|
||||
"# The pack of a platform, as released",
|
||||
"python scripts/generate_pack.py --platform retroarch --output-dir dist/",
|
||||
"python scripts/generate_pack.py --platform batocera --output-dir dist/",
|
||||
"",
|
||||
"# Single emulator or system",
|
||||
"python scripts/generate_pack.py --emulator dolphin",
|
||||
"python scripts/generate_pack.py --system sony-playstation-2",
|
||||
"",
|
||||
"# One region, best first, one file per system and region",
|
||||
"python scripts/generate_pack.py --platform retroarch --region us,eu,jp",
|
||||
"python scripts/generate_pack.py --platform retroarch --region us --one-per-slot",
|
||||
"",
|
||||
"# Firmware only: the game data a list or a core names is left out, each file named",
|
||||
"python scripts/generate_pack.py --platform emuelec --no-game-data",
|
||||
"",
|
||||
"# The bare minimum: only the files each core needs to start",
|
||||
"python scripts/generate_pack.py --platform batocera --required-only",
|
||||
"",
|
||||
"# List available emulators and systems",
|
||||
"python scripts/generate_pack.py --list-emulators",
|
||||
"python scripts/generate_pack.py --list-systems",
|
||||
"",
|
||||
"# Coverage of this repository's collection per platform, emulator or system",
|
||||
"# (it reads database.json and bios/: for your own BIOS folder, run the installer with --check)",
|
||||
"python scripts/verify.py --all",
|
||||
"python scripts/verify.py --platform batocera",
|
||||
"python scripts/verify.py --emulator flycast",
|
||||
"python scripts/verify.py --platform retroarch --verbose # emulator ground truth",
|
||||
"```",
|
||||
"",
|
||||
"Requires Python 3.9 or newer and `pyyaml`. The one-line installer alone runs on Python 3.8.",
|
||||
"",
|
||||
"## Documentation site",
|
||||
"",
|
||||
f"The [documentation site]({SITE_URL}) provides:",
|
||||
"",
|
||||
"- **Per-platform pages** with file-by-file verification status and hashes",
|
||||
"- **Per-emulator profiles** with source code references for every file",
|
||||
"- **Per-system pages** showing which emulators and platforms cover each console",
|
||||
"- **Gap analysis** identifying missing files and undeclared core requirements",
|
||||
f"- **Cross-reference** mapping files across {len(coverages)} platforms and {emulator_count} emulators",
|
||||
"- **Versioned data access** through JSON, CSV and SQLite exports with published SHA-256 checksums",
|
||||
"",
|
||||
"## How it works",
|
||||
"",
|
||||
"A platform's BIOS list and an emulator's code are maintained"
|
||||
" separately. Each list is compared with the emulator source code,"
|
||||
" file by file where a profile exists, and where the two differ the"
|
||||
" code decides.",
|
||||
"",
|
||||
"Hashes document what emulator code loads and accepts, not dump"
|
||||
" provenance. That boundary, and how it relates to preservation"
|
||||
" catalogs such as No-Intro, is drawn in the"
|
||||
f" [technical questions]({SITE_URL}wiki/technical-faq/"
|
||||
"#are-these-files-verified-against-original-hardware-dumps).",
|
||||
"",
|
||||
"1. **Read emulator source code**: trace every file the code loads, its expected hash and size",
|
||||
"2. **Cross-reference with platforms**: match against what each platform declares",
|
||||
"3. **Build packs**: include baseline files plus what each platform's cores need",
|
||||
"4. **Verify**: run platform-native checks and emulator-level validation",
|
||||
"",
|
||||
]
|
||||
)
|
||||
others = [name for name in sorted(coverages, key=lambda n: coverages[n]["platform"])
|
||||
if name not in released]
|
||||
lines.extend([
|
||||
"",
|
||||
"ZIPs are made for the setups most people download. The others install with the line"
|
||||
" above, and each page gives the command: "
|
||||
+ ", ".join(f"[{coverages[name]['platform']}]({SITE_URL}get/{name}/)" for name in others)
|
||||
+ ".",
|
||||
"",
|
||||
"Unzip into the BIOS folder, files straight in it. A big pack comes in parts"
|
||||
" (`Pack.part1of2.zip`): download every part"
|
||||
f" ([why some setups have no ZIP]({SITE_URL}wiki/faq/#why-is-there-no-zip-for-my-setup))."
|
||||
" Each release lists its files' SHA-256 with a signature"
|
||||
f" ([verifying a release]({SITE_URL}wiki/release-process/#verifying-a-release)).",
|
||||
"",
|
||||
"## Help",
|
||||
"",
|
||||
f"- \"BIOS missing\" or a game does not start: [what to check]({SITE_URL}wiki/troubleshooting/)",
|
||||
f"- Have a file? [Check it by its hash]({SITE_URL}files/)",
|
||||
f"- Every console and the files it needs: [systems]({SITE_URL}systems/)",
|
||||
f"- Asking an AI assistant: it reads the site best from [llms.txt]({SITE_URL}llms.txt)",
|
||||
"",
|
||||
"## What makes it different",
|
||||
"",
|
||||
f"- **{emulator_count} emulators profiled from their source code**: each file an emulator"
|
||||
" loads, its hash and the line of code that reads it",
|
||||
f"- **{len(coverages)} setups**, each checked its own way: MD5 for Batocera and Recalbox,"
|
||||
" the file name for RetroArch, as their own code does",
|
||||
f"- **{len(system_ids)} systems**, {total_files:,} files in the collection,"
|
||||
f" [what is still missing]({SITE_URL}gaps/) named file by file",
|
||||
f"- Public data: [JSON, CSV and SQLite]({SITE_URL}data/), MIT licensed",
|
||||
"",
|
||||
"<details>",
|
||||
"<summary><strong>For developers: build a pack</strong></summary>",
|
||||
"",
|
||||
clone_line(db),
|
||||
"",
|
||||
"```bash",
|
||||
"python scripts/generate_pack.py --platform retroarch --output-dir dist/",
|
||||
"python scripts/generate_pack.py --emulator dolphin",
|
||||
"python scripts/generate_pack.py --platform batocera --required-only",
|
||||
"python scripts/verify.py --platform batocera",
|
||||
"```",
|
||||
"",
|
||||
"Requires Python 3.9 or newer and `pyyaml`. The one-line installer alone runs on Python 3.8."
|
||||
f" Every tool and option: [tools]({SITE_URL}wiki/tools/). How the lists are built:"
|
||||
f" [architecture]({SITE_URL}wiki/architecture/).",
|
||||
"",
|
||||
"</details>",
|
||||
"",
|
||||
])
|
||||
|
||||
contributors = fetch_contributors()
|
||||
if not contributors:
|
||||
@@ -913,7 +759,7 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
"",
|
||||
"- [BIOS Preservation Tool](https://github.com/monster-penguin/BIOS-Preservation-Tool)"
|
||||
" by [monster-penguin](https://github.com/monster-penguin)"
|
||||
" - scan, verify, and stage your own BIOS collection"
|
||||
" - scans, verifies and stages a BIOS collection"
|
||||
" using RetroBIOS hash metadata",
|
||||
"",
|
||||
"## Contributing",
|
||||
@@ -1056,7 +902,7 @@ handheld launchers, `bwrap` (bubblewrap) for the probe sandbox, and a Python 3.8
|
||||
with pyte (`{floor_line}`) for the full screen. CI has all three.
|
||||
|
||||
`profile_sync.py` compares the branch with this repository's `main`, and
|
||||
fetches it in a fork's clone. A profile you only edit is not held to
|
||||
fetches it in a fork's clone. An edited profile is not held to
|
||||
`--strict`: its older refs are reviewed by hand.
|
||||
|
||||
The full pipeline is not needed for a contribution: it rebuilds every pack
|
||||
|
||||
+73
-71
@@ -96,6 +96,7 @@ import siteinputs
|
||||
import retired_files
|
||||
import siteagents
|
||||
import sitestarlight
|
||||
import sitesteps
|
||||
from siteexports import generate_exports_page, load_export_reports
|
||||
from siteguides import (
|
||||
ALSO_RUNS,
|
||||
@@ -488,13 +489,13 @@ def generate_home(
|
||||
works_with = ", ".join(named) + (f" and {others} more" if others else "")
|
||||
|
||||
lines = [
|
||||
"# BIOS files for your emulators",
|
||||
"# BIOS files for emulators",
|
||||
"",
|
||||
"Some consoles need a small program to start their games. It is called "
|
||||
"the **BIOS**. Your emulator needs a copy of it. We put the right copy "
|
||||
"the **BIOS**. The emulator needs a copy of it. RetroBIOS puts the right copy "
|
||||
"in the right folder.",
|
||||
"",
|
||||
markup.button("Get your files", "which-pack.md", primary=True) + " "
|
||||
markup.button("Get the files", "which-pack.md", primary=True) + " "
|
||||
+ markup.button("What is a BIOS?", "wiki/what-are-bios-files.md"),
|
||||
"",
|
||||
f"Free. No account. Works with {works_with}.",
|
||||
@@ -503,9 +504,9 @@ def generate_home(
|
||||
"[When a game does not start](wiki/troubleshooting.md)",
|
||||
"- Have a file? [Check it](files/index.md)",
|
||||
"",
|
||||
"## Do I need it?",
|
||||
"## Is it needed? { #do-i-need-it }",
|
||||
"",
|
||||
"- **Yes**, if your emulator says a BIOS is missing.",
|
||||
"- **Yes**, if the emulator says a BIOS is missing.",
|
||||
"- **Yes** for PlayStation 1 and 2, Saturn and Neo Geo.",
|
||||
"- **Better with it** for Dreamcast: some games need it.",
|
||||
"- **Usually not** for NES, Super Nintendo, Mega Drive and Game Boy.",
|
||||
@@ -515,12 +516,12 @@ def generate_home(
|
||||
"## Three steps",
|
||||
"",
|
||||
*markup.steps([
|
||||
"**Pick your device.** Its page shows your BIOS folder.",
|
||||
"**Pick the device.** Its page shows the BIOS folder.",
|
||||
"**Run one command.** Some setups have a ZIP.",
|
||||
"**Start a game.**",
|
||||
]),
|
||||
*install_demo(),
|
||||
"## Pick your device",
|
||||
"## Pick the device { #pick-your-device }",
|
||||
"",
|
||||
*setup_picker(registry, display),
|
||||
"## Is it safe?",
|
||||
@@ -574,7 +575,7 @@ def generate_explore_index(db: dict, coverages: dict, profiles: dict) -> str:
|
||||
"",
|
||||
"## Help the project",
|
||||
"",
|
||||
"A file you hold, an emulator profile or a correction fills a gap for "
|
||||
"A file sent in, an emulator profile or a correction fills a gap for "
|
||||
"every setup at once. For contributors: [How to contribute](contributing.md).",
|
||||
"",
|
||||
]
|
||||
@@ -752,7 +753,7 @@ def _folder_words(display: str, extract_to: str | None) -> str:
|
||||
"""The folder a pack or a copy goes into, in words: the device's path,
|
||||
or a folder of the setup named as the setup's."""
|
||||
if not extract_to:
|
||||
return "your BIOS folder"
|
||||
return "the BIOS folder"
|
||||
if _setup_relative(extract_to):
|
||||
return f"{display}'s `{extract_to.rstrip('/')}` folder"
|
||||
return f"`{extract_to}`"
|
||||
@@ -824,7 +825,7 @@ def generate_get_page(platform_name: str) -> str:
|
||||
"",
|
||||
f"- {_fmt_size(pack['download_size'])} to download, "
|
||||
f"{_fmt_size(pack['extracted_size'])} once unzipped.",
|
||||
*(["- You need both parts." if parts == 2 else f"- You need all {parts} parts."]
|
||||
*(["- Both parts are needed." if parts == 2 else f"- All {parts} parts are needed."]
|
||||
if parts > 1 else []),
|
||||
*_unzip_into(platform_name, display, extract_to, parts),
|
||||
*(line for text in release for line in (text, "")),
|
||||
@@ -2349,7 +2350,7 @@ def generate_data_page(exports: list[dict], measurements_sqlite: Path | None = N
|
||||
"per `bios/<Manufacturer>/<Console>/` folder of the collection, named "
|
||||
"`Manufacturer\\Console` so the tools look in that subfolder, each file a "
|
||||
"rom named by its path below that folder. It audits a copy of the "
|
||||
"collection's `bios/` tree, or rebuilds that layout from your own dumps.",
|
||||
"collection's `bios/` tree, or rebuilds that layout from a reader's own dumps.",
|
||||
"",
|
||||
"A setup's DAT, `downloads/dat/<platform>.dat`, describes its BIOS folder "
|
||||
"once installed: one game named after the folder (`bios`, `system`), each "
|
||||
@@ -2449,7 +2450,7 @@ def _browser_title(relative: Path, title: str) -> str:
|
||||
# Material appends " - RetroBIOS": the home title says what the site
|
||||
# is, instead of "RetroBIOS - RetroBIOS".
|
||||
"index.md": "BIOS and firmware packs for RetroArch, Batocera and more",
|
||||
"platforms/index.md": "Your setup",
|
||||
"platforms/index.md": "Setups",
|
||||
"systems/index.md": "Systems",
|
||||
"emulators/index.md": "Emulators",
|
||||
"wiki/index.md": "Reference",
|
||||
@@ -2510,7 +2511,7 @@ def _shorten(plain: str, limit: int = 157) -> str:
|
||||
def _page_description(markdown: str, relative: Path, title: str) -> str:
|
||||
key = relative.as_posix()
|
||||
specific = {
|
||||
"index.md": "The BIOS files your emulators need, for RetroArch, Batocera, EmuDeck and more: the folder they go in, one command that installs and checks them, and packs to download.",
|
||||
"index.md": "The BIOS files emulators need, for RetroArch, Batocera, EmuDeck and more: the folder they go in, one command that installs and checks them, and packs to download.",
|
||||
"data.md": "Versioned RetroBIOS JSON API, CSV exports, SQLite snapshot, schemas, checksums and data interpretation guidance.",
|
||||
"cross-reference.md": "Every emulator core each retrogaming platform runs, the systems it emulates and the upstream project it comes from.",
|
||||
"gaps.md": "Current RetroBIOS verification gaps, platform-to-emulator divergences, missing files and documented source limitations.",
|
||||
@@ -2518,7 +2519,7 @@ def _page_description(markdown: str, relative: Path, title: str) -> str:
|
||||
sitemeasurements.EXPLORER_PAGE: "The records of emulator cores run headless on demand, each with its claim, verdict, the BIOS it ran, method, computer and full record, filterable, and as SQLite.",
|
||||
"provenance.md": "Hash-based comparison of RetroBIOS metadata with No-Intro, Redump and TOSEC catalog snapshots.",
|
||||
"explore.md": "Systems, emulators and installer options to explore, with the figures of the BIOS collection.",
|
||||
"which-pack.md": "Pick your device or setup and get its BIOS files: the folder they go in, one command that installs and checks them, or a ZIP pack to extract.",
|
||||
"which-pack.md": "Pick a device or setup and get its BIOS files: the folder they go in, one command that installs and checks them, or a ZIP pack to extract.",
|
||||
"contributing.md": "Add a BIOS file the collection lacks from the browser, checked on arrival, or add a platform or an emulator profile.",
|
||||
}
|
||||
if key in specific:
|
||||
@@ -2571,7 +2572,7 @@ def _lead(markdown: str) -> str:
|
||||
|
||||
def _emulator_description(markdown: str, key: str, title: str, intro: str) -> str:
|
||||
"""An emulator page by its intro sentence ("DuckStation emulates Sony
|
||||
PlayStation. It picks the BIOS for your game's region: ..."), or by
|
||||
PlayStation. It picks the BIOS for the game's region: ..."), or by
|
||||
its profile table when the profile names no system."""
|
||||
notes = re.search(r"<summary>Technical notes</summary>\s*\n\n(.+?)(?:\n\n|$)", markdown, re.S)
|
||||
if not intro and notes:
|
||||
@@ -3357,12 +3358,12 @@ def decorate_markdown_pages(docs: Path) -> dict[str, str]:
|
||||
|
||||
|
||||
def generate_platform_index(coverages: dict, registry: dict | None = None) -> str:
|
||||
"""The Your setup tab: every platform by the device it runs on, and one
|
||||
"""The Setups tab: every platform by the device it runs on, and one
|
||||
line to the table that sets them side by side (setup_table), which is
|
||||
reference matter and sits with how each setup checks its files."""
|
||||
registry = registry or {}
|
||||
lines = [
|
||||
"# Your setup",
|
||||
"# Setups",
|
||||
"",
|
||||
"Each setup's page gives the folder and the command.",
|
||||
"",
|
||||
@@ -4181,16 +4182,19 @@ def first_screen_words() -> list[str]:
|
||||
|
||||
# The demo files the site serves (docs_assets/demo/, scripts/make_demo.py):
|
||||
# the GIF is the README's, the tape and demo.json are inputs.
|
||||
DEMO_ASSETS = ("install.webm", "install.mp4", "install.png")
|
||||
DEMO_ASSETS = tuple(f"{stem}.{ext}" for stem in ("install", "install-handheld")
|
||||
for ext in ("webm", "mp4", "png"))
|
||||
|
||||
|
||||
def install_demo() -> list[str]:
|
||||
"""The installer demo: a video that loads once it is on screen, its
|
||||
description for screen readers."""
|
||||
facts = demo_facts()
|
||||
def install_demo(variant: str = "default", name: str = "") -> list[str]:
|
||||
"""An installer demo (default, or handheld): a video that loads once it
|
||||
is on screen, its description for screen readers. *name* tells two
|
||||
demos of one page apart (the tab each sits in)."""
|
||||
facts = demo_facts(variant)
|
||||
text = html.escape(demo_description(facts), quote=True)
|
||||
own = f' variant="{variant}" name="{name}"' if name else ""
|
||||
return [f'<InstallDemo description="{text}" width="{facts["width"]}" '
|
||||
f'height="{facts["height"]}" />', ""]
|
||||
f'height="{facts["height"]}"{own} />', ""]
|
||||
|
||||
|
||||
def check_pass_line() -> str:
|
||||
@@ -4238,7 +4242,7 @@ def _share_tabs(name: str, share: str) -> tuple[dict[str, str], dict[str, str]]:
|
||||
how = {
|
||||
"macos": (f"In Finder, Go > Connect to Server: `{url}`. Then paste the line in "
|
||||
"**Terminal**."),
|
||||
"linux": (f"Open `{url}` in your file manager. Paste the line in a terminal, "
|
||||
"linux": (f"Open `{url}` in the file manager. Paste the line in a terminal, "
|
||||
f"with the folder it opened for `/path/to/{volume}`."),
|
||||
}
|
||||
return args, how
|
||||
@@ -4279,7 +4283,7 @@ def _card_route(name: str, display: str, guide: Guide) -> list[str]:
|
||||
return [lead, "", *terminal_tabs(tabs, args, steps=False)]
|
||||
# A folder on the card's Linux partition: Windows and macOS show the
|
||||
# small boot partition alone, and no folder there fits --dest.
|
||||
put = ("Put the device's card in your computer." if "windows" in tabs
|
||||
put = ("Put the device's card in a computer." if "windows" in tabs
|
||||
else "Only a Linux computer opens the card.")
|
||||
lead = (f"No network? {put} After `--dest`, "
|
||||
f"name the folder the device calls `{extract_to}`.")
|
||||
@@ -4306,14 +4310,14 @@ def _setup_routes(name: str, display: str, page_link: bool = False) -> list[str]
|
||||
card = _card_route(name, display, guide)
|
||||
shared = _get_page_line(name, display) if page_link else []
|
||||
if share or (card and guide.run == ("device",)):
|
||||
computer = ["## From your computer { #from-your-computer }", ""]
|
||||
computer = ["## From a computer { #from-your-computer }", ""]
|
||||
if share:
|
||||
args, how = _share_tabs(name, share)
|
||||
# The setup page stays in its word budget: its Mac and Linux
|
||||
# lines are on the get/ page, the one the command is shared from.
|
||||
tabs = ("windows",) if page_link else ("windows", "macos", "linux")
|
||||
computer += [
|
||||
"Needs the device on your network.",
|
||||
"Needs the device on the same network.",
|
||||
"",
|
||||
*terminal_tabs(tabs, args, how=how),
|
||||
*([f"[Mac or Linux](../{get_page_link(name)}#from-your-computer)", ""]
|
||||
@@ -4327,7 +4331,7 @@ def _setup_routes(name: str, display: str, page_link: bool = False) -> list[str]
|
||||
|
||||
|
||||
# Shown in the "On the device" tab of a guide that has no `terminal:` of its own.
|
||||
DEVICE_TERMINAL_FALLBACK = "Or fill the folder from your computer, with the ZIP."
|
||||
DEVICE_TERMINAL_FALLBACK = "Or fill the folder from a computer, with the ZIP."
|
||||
|
||||
|
||||
def _computer_first(name: str, guide: Guide) -> bool:
|
||||
@@ -4362,11 +4366,11 @@ def _guide_download(name: str, display: str, guide: Guide, extract_to: str | Non
|
||||
f"{_fmt_size(pack['extracted_size'])} once unzipped."
|
||||
]
|
||||
if parts > 1:
|
||||
bullets.append("You need both parts." if parts == 2 else f"You need all {parts} parts.")
|
||||
bullets.append("Both parts are needed." if parts == 2 else f"All {parts} parts are needed.")
|
||||
if _computer_first(name, guide) and network:
|
||||
target = "the network folder above"
|
||||
else:
|
||||
target = f"`{extract_to}`" if extract_to else "your BIOS folder"
|
||||
target = f"`{extract_to}`" if extract_to else "the BIOS folder"
|
||||
bullets.append(f"Unzip {'them all' if parts > 1 else 'it'} into {target}."
|
||||
+ (" Files like `neogeo.zip` stay zipped." if _ships_neogeo(name) else ""))
|
||||
if guide.download_hint and not _computer_first(name, guide):
|
||||
@@ -4392,7 +4396,7 @@ def _guide_download(name: str, display: str, guide: Guide, extract_to: str | Non
|
||||
|
||||
|
||||
def only_some_consoles(name: str, prefix: str = "") -> list[str]:
|
||||
"""The way to a smaller download, in the words Get your files uses."""
|
||||
"""The way to a smaller download, in the words Get the files uses."""
|
||||
return [
|
||||
f"Only some consoles? [Pick them]({prefix}which-pack.md?platform={name}"
|
||||
f"#only-some-consoles), or the command's **{_installer().TUI_SYSTEMS_ROW}**.",
|
||||
@@ -4483,7 +4487,7 @@ def generate_handheld_guide(guide: HandheldGuide) -> str:
|
||||
"",
|
||||
"## Where the files go { #where-the-files-go }",
|
||||
"",
|
||||
"Put the SD card in a computer. Find your system below.",
|
||||
"Put the SD card in a computer. Find the system below.",
|
||||
"",
|
||||
"| System | Devices | BIOS folder on the card |",
|
||||
"|--------|---------|-------------------------|",
|
||||
@@ -4539,7 +4543,7 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
|
||||
if _computer_first(name, guide):
|
||||
lines += [
|
||||
*_guide_download(name, display, guide, extract_to,
|
||||
"## From your computer { #download }"),
|
||||
"## From a computer { #download }"),
|
||||
*_offline_line(name),
|
||||
*_guide_install(name, guide, "## On the device { #install }"),
|
||||
]
|
||||
@@ -5502,7 +5506,7 @@ def generate_provenance_page(
|
||||
"",
|
||||
*([f'<p class="rb-timestamp">Catalogs as of: {as_of}.</p>', ""] if as_of else []),
|
||||
"Provenance says whether a file is, byte for byte, a dump that Redump, "
|
||||
"No-Intro or TOSEC catalogues. It tells you where a dump is known from, "
|
||||
"No-Intro or TOSEC catalogues. It says where a dump is known from, "
|
||||
"not whether an emulator accepts it: that is the emulator's code, read "
|
||||
"in its profile. Example: `scph5501.bin` is Redump's `ps-30a.bin`.",
|
||||
"",
|
||||
@@ -5539,7 +5543,7 @@ def generate_provenance_page(
|
||||
"arcade sets tracked by MAME driver source instead.",
|
||||
"",
|
||||
"When the two views disagree, this project follows the code, because "
|
||||
"that is what decides whether your emulator boots. The reasoning is in "
|
||||
"that is what decides whether the emulator boots. The reasoning is in "
|
||||
"the [technical questions](wiki/technical-faq.md#are-these-files-verified-against-original-"
|
||||
"hardware-dumps).",
|
||||
"",
|
||||
@@ -6993,7 +6997,7 @@ def _group_lines(
|
||||
lines += [f"- **{key}**: {text.lstrip(': ')}"
|
||||
for key, block in blocks.items() for _lead, text in block]
|
||||
return [*lines, ""]
|
||||
lines = ["**Start with** the files of your machine:", ""]
|
||||
lines = ["**Start with** the files of the machine:", ""]
|
||||
for key, block in blocks.items():
|
||||
for lead, text in block:
|
||||
if lead == "*":
|
||||
@@ -7205,7 +7209,7 @@ def _start_lines(block: list[tuple[str, str]], console: str = "") -> list[str]:
|
||||
below = " They come first below." if _sep else ""
|
||||
return [" ".join(filter(None, [f"**Start with**: {named}.{below}", instead])), ""]
|
||||
bios = f"the {console} BIOS" if console else "the BIOS"
|
||||
lines = [f"**Start with** {bios} for the region of your games:", ""]
|
||||
lines = [f"**Start with** {bios} for the region of the games:", ""]
|
||||
for lead, text in block:
|
||||
lines.append(f"- **{lead or 'Some emulators also require'}**: {text}")
|
||||
if instead:
|
||||
@@ -7262,7 +7266,7 @@ def _romset_note(consoles: dict[str, list]) -> list[str]:
|
||||
|
||||
|
||||
_NEXT_FROM_SYSTEMS = [
|
||||
"[Get your files](../which-pack.md), for the setup you play on",
|
||||
"[Get the files](../which-pack.md), for the setup in use",
|
||||
"[When a game does not start](../wiki/troubleshooting.md)",
|
||||
"[Every emulator](../emulators/index.md), and the files each one reads",
|
||||
]
|
||||
@@ -8976,7 +8980,7 @@ def _install_block(name: str, profile: dict) -> list[str]:
|
||||
f"BIOS folder, then downloads only the files {emu} reads there and checks "
|
||||
"each one.")
|
||||
else:
|
||||
lead = ("The installer finds your setup and its BIOS folder, then downloads "
|
||||
lead = ("The installer finds the setup and its BIOS folder, then downloads "
|
||||
"only the files this emulator reads there and checks each one.")
|
||||
if profile.get("type") == "standalone + libretro":
|
||||
# The command fills a setup, never the app's own folder.
|
||||
@@ -9242,7 +9246,7 @@ def _install_picker(declared: list[tuple[str, str]]) -> list[str]:
|
||||
"",
|
||||
'<div class="rb-install" data-rb-install="" hidden>',
|
||||
"",
|
||||
f'<label class="rb-install-setup">Your setup <select>{options}</select></label>',
|
||||
f'<label class="rb-install-setup">Setup <select>{options}</select></label>',
|
||||
"",
|
||||
*terminal_tabs(INSTALL_PICKER_OSES, args, steps=False),
|
||||
"",
|
||||
@@ -9505,7 +9509,7 @@ def _emulator_file_summary(files: list[dict]) -> list[str]:
|
||||
if _regional_choice([f for f in files if f.get("required") is True and not comes_with_app(f)]):
|
||||
# DuckStation takes the BIOS of the game's region: "3 required"
|
||||
# read as "get all three".
|
||||
line = (f"{total}. One of {req_count:,} is required, for your games' region. "
|
||||
line = (f"{total}. One of {req_count:,} is required, for the games' region. "
|
||||
f"{len(slots) - req_count:,} {'is' if len(slots) - req_count == 1 else 'are'} "
|
||||
"optional.")
|
||||
if hle_count:
|
||||
@@ -9913,7 +9917,7 @@ def _any_name_folder(profile: dict, entry: dict) -> str:
|
||||
own = ("[the app's data folder](#data-folder)" if profile.get("data_root")
|
||||
else "the app's own data folder")
|
||||
return f"the {_code(folder.strip('/'))} folder inside {own}"
|
||||
return f"{_code(folder)}, inside your BIOS folder"
|
||||
return f"{_code(folder)}, inside the BIOS folder"
|
||||
|
||||
|
||||
def _any_name_steps(emu_name: str, profile: dict, entry: dict, href: str,
|
||||
@@ -9998,7 +10002,7 @@ def _emulator_intro(emu_name: str, profile: dict, href: str = "", name: str = ""
|
||||
# DuckStation needs the BIOS of the game's region, not all three: the
|
||||
# page said "3 of them required" and read as "get all of these".
|
||||
others = len(files) - len(required)
|
||||
sentences.append(f"It picks the BIOS for your game's region: {by_region}.")
|
||||
sentences.append(f"It picks the BIOS for the game's region: {by_region}.")
|
||||
if others:
|
||||
sentences.append(
|
||||
f"The {_count(others, 'other file')} {'is' if others == 1 else 'are'} optional.")
|
||||
@@ -10290,7 +10294,7 @@ def emulator_pages(
|
||||
lines[trees_start:] = search_excluded(lines[trees_start:])
|
||||
|
||||
lines.extend(_next_block([
|
||||
"[Get your files](../which-pack.md), for the setup you play on",
|
||||
"[Get the files](../which-pack.md), for the setup in use",
|
||||
"[Every system](../systems/index.md), and the files each one needs",
|
||||
"[When a game does not start](../wiki/troubleshooting.md)",
|
||||
]))
|
||||
@@ -10852,15 +10856,15 @@ def generate_gap_analysis(
|
||||
lines = [
|
||||
"# What the packs do not hold",
|
||||
"",
|
||||
"What each pack still cannot give you, and why. Most gaps are not "
|
||||
"What each pack still cannot give, and why. Most gaps are not "
|
||||
"firmware: a platform's list can also name game files, such as "
|
||||
"Diablo's `diabdat.mpq`, which you copy from your own game, or files "
|
||||
"Diablo's `diabdat.mpq`, which comes from an owned copy of the game, or files "
|
||||
"no emulator of that platform opens. Those are counted apart. A missing "
|
||||
"firmware file means a console that does not start, and each one is "
|
||||
"named below with the platforms that list it.",
|
||||
"",
|
||||
"Each file here is a request: if you hold one of the files under "
|
||||
"\"Files to source\", [adding it](contributing.md) fills the gap for "
|
||||
"Each file here is a request: for a file under \"Files to source\", "
|
||||
"[adding it](contributing.md) fills the gap for "
|
||||
"every platform at once.",
|
||||
"",
|
||||
" ".join([
|
||||
@@ -11581,7 +11585,7 @@ def generate_not_found() -> str:
|
||||
'<p><button type="button" class="rb-notfound-search" data-rb-open-search>Search the site</button></p>',
|
||||
"",
|
||||
"- [Start here](index.md): what BIOS files are and how to install them",
|
||||
"- [Get your files](which-pack.md): the pack or the command for your device",
|
||||
"- [Get the files](which-pack.md): the pack or the command for each device",
|
||||
"- Browse [platforms](platforms/index.md), [systems](systems/index.md) "
|
||||
"or [emulators](emulators/index.md)",
|
||||
"",
|
||||
@@ -11622,7 +11626,7 @@ def generate_wiki_data_model(db: dict, profiles: dict) -> str:
|
||||
"lists every file the collection holds, keyed by its SHA1. A platform YAML "
|
||||
"copies the BIOS list a setup publishes. An emulator profile records what "
|
||||
"one emulator opens, read from its code. The sections below show the "
|
||||
"fields of each, with a real entry, so you can read the data or build on it.",
|
||||
"fields of each, with a real entry, to read the data or build on it.",
|
||||
"",
|
||||
"Example: `scph5501.bin` is one entry of `database.json` (key "
|
||||
"`0555c6fa...`), one entry of `platforms/batocera.yml` (its MD5) and one "
|
||||
@@ -11943,22 +11947,26 @@ def generate_which_pack() -> str:
|
||||
display = {key: load_platform_config(key, str(siteinputs.current().platforms)).get("platform", key)
|
||||
for key in registry}
|
||||
all_tabs = ("windows", "macos", "linux", "steamdeck", "android")
|
||||
# The quick install box also runs on a game system: its tab shows the
|
||||
# handheld recording (sitesteps).
|
||||
box_tabs = (*all_tabs, "device")
|
||||
box_steps, box_demos = sitesteps.install_box(box_tabs)
|
||||
lines = [
|
||||
"---",
|
||||
f"terms: {', '.join(IOS_TERMS)}",
|
||||
"---",
|
||||
"",
|
||||
"# Get your files",
|
||||
"# Get the files",
|
||||
"",
|
||||
"Pick your setup. Its page gives the folder and the command.",
|
||||
"Pick the setup. Its page gives the folder and the command.",
|
||||
"",
|
||||
"## Pick your setup",
|
||||
"## Pick the setup { #pick-your-setup }",
|
||||
"",
|
||||
*setup_picker(registry, display),
|
||||
"## Folders and ZIP files { #manual-download }",
|
||||
"",
|
||||
"- A big ZIP comes in parts, like `Pack.part1of2.zip`. Download every part.",
|
||||
"- Unzip them into your BIOS folder. Put the files straight in it, "
|
||||
"- Unzip them into the BIOS folder. Put the files straight in it, "
|
||||
"not in a new folder.",
|
||||
"- Leave the ZIPs inside, like `neogeo.zip`, as they are.",
|
||||
"- A few ZIPs hold their own top folder. Their setup's page says where "
|
||||
@@ -11987,21 +11995,22 @@ def generate_which_pack() -> str:
|
||||
*ios_section(),
|
||||
"## Not in the list? One command finds it { #quick-install }",
|
||||
"",
|
||||
"A **terminal** is a window where you type commands.",
|
||||
"A **terminal** is a window for typing commands.",
|
||||
"",
|
||||
*terminal_tabs(all_tabs, {}),
|
||||
*terminal_tabs(box_tabs, {}, under_steps=box_steps, under_command=box_demos),
|
||||
"On Batocera, Recalbox and other game systems, run it on the device. "
|
||||
"Your setup's page shows how.",
|
||||
"The setup's page shows how.",
|
||||
"",
|
||||
f"With Python, `{PACKAGE_COMMAND}` does the same.",
|
||||
"",
|
||||
*first_screen_words(),
|
||||
"",
|
||||
*install_demo(),
|
||||
*trust_box(""),
|
||||
"**Check your folder later.** Add `--check`. It changes nothing.",
|
||||
"**Check the folder later.** Add `--check`. It changes nothing.",
|
||||
"",
|
||||
*terminal_tabs(all_tabs, dict.fromkeys(TERMINALS, "--check"), steps=False),
|
||||
*markup.details("note", "Fill an SD card from a computer", [
|
||||
"Name your setup and the BIOS folder on the card. For RetroArch on a "
|
||||
"Name the setup and the BIOS folder on the card. For RetroArch on a "
|
||||
"Nintendo Switch:",
|
||||
"",
|
||||
"Linux, macOS:",
|
||||
@@ -12014,14 +12023,7 @@ def generate_which_pack() -> str:
|
||||
*fence("powershell", install_command(
|
||||
"powershell", "--platform retroarch --dest E:\\retroarch\\cores\\system")),
|
||||
]),
|
||||
*markup.details("note", "Have Python? Run it as a package", [
|
||||
"The same installer, with Python 3.8 or newer:",
|
||||
"",
|
||||
*fence("sh", PACKAGE_COMMAND),
|
||||
"",
|
||||
"`pipx run retrobios` works too, or `pip install retrobios` and then `retrobios`.",
|
||||
]),
|
||||
*markup.details("note", "Copy the BIOS files from your old card", [
|
||||
*markup.details("note", "Copy the BIOS files from an old card", [
|
||||
"Plug the old card in and add `--from` with its folder. The BIOS files "
|
||||
"found there are copied, and the rest downloaded. The old card is only read.",
|
||||
"",
|
||||
@@ -12113,7 +12115,7 @@ def _platform_nav(coverages: dict, registry: dict) -> list:
|
||||
# The tabs in reading order, by the level their pages declare.
|
||||
TABS = {
|
||||
"start": "Start here",
|
||||
"setup": "Your setup",
|
||||
"setup": "Setups",
|
||||
"explore": "Explore",
|
||||
"reference": "Reference",
|
||||
"contribute": "Contribute",
|
||||
@@ -12213,7 +12215,7 @@ def generate_nav(
|
||||
) -> list:
|
||||
"""The navigation, five tabs in reading order.
|
||||
|
||||
Start here is for someone who knows nothing yet, Your setup for someone
|
||||
Start here is for someone who knows nothing yet, Setups for someone
|
||||
who knows their device, Explore for one system or emulator, Reference for
|
||||
checking the work, Contribute for adding to it. Each tab opens on its
|
||||
index page.
|
||||
@@ -12224,7 +12226,7 @@ def generate_nav(
|
||||
own order.
|
||||
"""
|
||||
generated: dict[str, list[tuple]] = {
|
||||
"start": [(0, "index.md"), (20, {"Get your files": "which-pack.md"})],
|
||||
"start": [(0, "index.md"), (20, {"Get the files": "which-pack.md"})],
|
||||
"setup": [(10 * i, item)
|
||||
for i, item in enumerate(_platform_nav(coverages, registry or {}))],
|
||||
"explore": [
|
||||
|
||||
+279
-95
@@ -1,35 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record the installer demo shown in the README and on the site, once.
|
||||
"""Record the installer demos shown in the README and on the site, once.
|
||||
|
||||
The recording is the full-screen installer run from the one-line command on
|
||||
an ArkOS SD card that holds six games: it finds the card, plans only the BIOS
|
||||
files those games need, installs them and shows the summary. Everything it
|
||||
reads is local and the same on every machine:
|
||||
Each recording is the installer on an ArkOS SD card that holds six games: it
|
||||
finds the card, plans only the BIOS files those games need, installs them
|
||||
and shows the summary. Two variants, by where the install box says the
|
||||
command runs:
|
||||
|
||||
- default: the one-line command in a terminal on a Linux computer, the card
|
||||
mounted at /media/EASYROMS. The README and every tab but one show it.
|
||||
- handheld: the full-screen interface on ArkOS itself, at the 640x480
|
||||
screen of an R36S, as the Tools menu launcher starts it (ports/shared/
|
||||
tui.txt: install.py --tui, RETROBIOS_PAD=gptokeyb, RETROBIOS_IDLE_EXIT).
|
||||
The keys typed are the ones gptokeyb sends for the buttons
|
||||
(ports/shared/retrobios.gptk: A is Enter), and the bottom line names the
|
||||
buttons. The "On the device" tab shows it.
|
||||
|
||||
No Windows variant: pwsh on Linux runs install.ps1's whole flow, but the
|
||||
installer reads drive E: there only through its WSL layout, and its summary
|
||||
gives the command back with --dest /mnt/e/bios, a path no Windows user sees.
|
||||
|
||||
Everything a recording reads is local and the same on every machine:
|
||||
|
||||
- The card is built under tmp/demo/ (bios/, the folders ArkOS writes on its
|
||||
games partition, one game per console) and mounted at /media/EASYROMS in a
|
||||
mount namespace of this user (unshare), on a tmpfs of a card's size. /media
|
||||
and /mnt are emptied there, so no drive of this machine is shown.
|
||||
games partition, one game per console) on a tmpfs of a card's size, in a
|
||||
mount namespace of this user (unshare, or bwrap for the handheld, whose
|
||||
root holds ArkOS's /home/ark, /opt/system and /roms). /media and /mnt are
|
||||
emptied, so no drive of this machine is shown.
|
||||
- The repository is served on 127.0.0.1 and named with RETROBIOS_BASE_URL:
|
||||
the file lists, install/games/ and the BIOS files are this checkout's.
|
||||
- The one-line command runs this checkout's install.sh and install.py. The
|
||||
shell of the recording answers `curl -fsSL <site>/i` with a line that
|
||||
starts the local install.sh, which skips the download of the installer and
|
||||
its SHA-256 check and runs the rest of the bootstrap as the site's copy does.
|
||||
- The one-line command runs this checkout's install.sh: the shell answers
|
||||
`curl -fsSL <site>/i` with a line that starts the local install.sh, which
|
||||
skips the download of the installer and its SHA-256 check and runs the
|
||||
rest of the bootstrap as the site's copy does.
|
||||
- HOME is an empty folder and the prompt is "$ ".
|
||||
|
||||
docs_assets/demo/install.tape drives VHS (github.com/charmbracelet/vhs, with
|
||||
ttyd, ffmpeg and a Chromium on PATH). This script encodes what it records:
|
||||
docs_assets/demo/<stem>.tape drives VHS (github.com/charmbracelet/vhs, with
|
||||
ttyd, ffmpeg and a Chromium on PATH, and bwrap for the handheld). This
|
||||
script encodes what it records:
|
||||
|
||||
docs_assets/demo/install.gif the README
|
||||
docs_assets/demo/install.webm the site, with install.mp4 as fallback
|
||||
docs_assets/demo/install.png the first frame shown, the video's poster
|
||||
docs_assets/demo/demo.json what the recording shows, for the alt text
|
||||
docs_assets/demo/install.gif the README (the default variant only)
|
||||
docs_assets/demo/<stem>.webm the site, with <stem>.mp4 as fallback
|
||||
docs_assets/demo/<stem>.png the first frame shown, the video's poster
|
||||
docs_assets/demo/demo.json per variant, what it shows and its alt text
|
||||
|
||||
The site build copies the video files and never runs this script. Run it
|
||||
again when a screen of the installer or the file list of its games changes:
|
||||
|
||||
python scripts/make_demo.py
|
||||
python scripts/make_demo.py # both variants
|
||||
python scripts/make_demo.py --variant handheld # one, the others kept
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -48,20 +66,72 @@ import sys
|
||||
import threading
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
OUT = ROOT / "docs_assets" / "demo"
|
||||
TAPE = OUT / "install.tape"
|
||||
WORK = ROOT / "tmp" / "demo"
|
||||
# What the tape writes, read back here (paths relative to ROOT, the tape's
|
||||
# working directory).
|
||||
RECORDED = WORK / "recorded.mp4"
|
||||
POSTER_SHOT = WORK / "poster.png"
|
||||
MOUNT = "/media/EASYROMS"
|
||||
FACTS = OUT / "demo.json"
|
||||
CARD_SIZE = "29g"
|
||||
SETUP = "ArkOS"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Variant:
|
||||
"""One recording. *place* is "computer" (the card in a reader, the
|
||||
one-line command typed) or "device" (the interface on the card's own
|
||||
system). *os_type* is RETROBIOS_OS for the run."""
|
||||
|
||||
name: str
|
||||
stem: str
|
||||
mount: str
|
||||
os_type: str
|
||||
place: str
|
||||
gif: bool = False
|
||||
|
||||
@property
|
||||
def tape(self) -> Path:
|
||||
return OUT / f"{self.stem}.tape"
|
||||
|
||||
@property
|
||||
def suffix(self) -> str:
|
||||
return "" if self.name == "default" else f"-{self.name}"
|
||||
|
||||
@property
|
||||
def recorded(self) -> Path:
|
||||
"""What the tape writes, read back here (relative to ROOT, the
|
||||
tape's working directory)."""
|
||||
return WORK / f"recorded{self.suffix}.mp4"
|
||||
|
||||
@property
|
||||
def poster_shot(self) -> Path:
|
||||
return WORK / f"poster{self.suffix}.png"
|
||||
|
||||
@property
|
||||
def outputs(self) -> tuple[str, ...]:
|
||||
names = (f"{self.stem}.webm", f"{self.stem}.mp4", f"{self.stem}.png")
|
||||
return ((f"{self.stem}.gif",) if self.gif else ()) + names
|
||||
|
||||
|
||||
VARIANTS: dict[str, Variant] = {
|
||||
"default": Variant("default", "install", "/media/EASYROMS", "linux", "computer",
|
||||
gif=True),
|
||||
"handheld": Variant("handheld", "install-handheld", "/roms", "linux", "device"),
|
||||
}
|
||||
# The default variant's paths, as the tests and the README name them.
|
||||
TAPE = VARIANTS["default"].tape
|
||||
RECORDED = VARIANTS["default"].recorded
|
||||
POSTER_SHOT = VARIANTS["default"].poster_shot
|
||||
MOUNT = VARIANTS["default"].mount
|
||||
|
||||
# Where ArkOS keeps what the handheld recording needs: the Tools menu folder
|
||||
# the launcher sits in (ports/tools/RetroBIOS.sh, RB_DIR), the user, and
|
||||
# EASYROMS at /roms (install.py _OS_RULES, ArkOS).
|
||||
ARK_HOME = "/home/ark"
|
||||
ARK_TOOLS = "/opt/system/Tools"
|
||||
ARK_INSTALLER = f"{ARK_TOOLS}/retrobios/install.py"
|
||||
|
||||
# The card: the folders ArkOS's default tree writes on EASYROMS (install.py
|
||||
# _GAMES_PART_RULES reads launchimages/), and one game per console folder,
|
||||
# named as the folder's extensions in install/games/setups/arkos.json allow.
|
||||
@@ -79,13 +149,18 @@ GAME_BYTES = 256 * 1024
|
||||
# cores read the region from (install/games/systems/sony-playstation.json).
|
||||
PSX_LICENCE = b" Licensed by Sony Computer Entertainment Amer ica "
|
||||
|
||||
# Size targets of the outputs, in bytes: the README and two site pages.
|
||||
LIMITS = {"install.gif": 1_500_000, "install.webm": 1_000_000,
|
||||
"install.mp4": 1_000_000, "install.png": 150_000}
|
||||
# Size targets of the outputs, in bytes, by extension: the README and the
|
||||
# site's pages.
|
||||
LIMITS = {".gif": 1_500_000, ".webm": 1_000_000, ".mp4": 1_000_000, ".png": 150_000}
|
||||
GIF_FPS = 10
|
||||
VIDEO_FPS = 25
|
||||
|
||||
|
||||
def limits(variant: Variant) -> dict[str, int]:
|
||||
"""Each output of a variant and its size target."""
|
||||
return {name: LIMITS[Path(name).suffix] for name in variant.outputs}
|
||||
|
||||
|
||||
def game_bytes(name: str) -> bytes:
|
||||
"""Bytes of a game no catalog names, the same on every run."""
|
||||
seed = hashlib.sha256(name.encode("utf-8")).digest()
|
||||
@@ -133,10 +208,9 @@ def served_repository() -> Iterator[str]:
|
||||
server.server_close()
|
||||
|
||||
|
||||
def write_session(work: Path, names: list[str]) -> Path:
|
||||
"""The program the recording's shell becomes: the card mounted in a
|
||||
namespace of its own, then a clean interactive bash, or the command
|
||||
given as arguments, with only the variables names lists."""
|
||||
def _write_rc(work: Path) -> Path:
|
||||
"""The startup file of the shell the recording types into: the prompt,
|
||||
no history, and the site's address answered with the local bootstrap."""
|
||||
rc = work / "bashrc"
|
||||
rc.write_text(
|
||||
"PS1='$ '\n"
|
||||
@@ -151,67 +225,144 @@ def write_session(work: Path, names: list[str]) -> Path:
|
||||
"clear\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
session = work / "session.sh"
|
||||
session.write_text(
|
||||
"#!/bin/sh\n"
|
||||
"# Written by scripts/make_demo.py.\n"
|
||||
"set -eu\n"
|
||||
return rc
|
||||
|
||||
|
||||
def _computer_session(variant: Variant, work: Path, names: list[str]) -> str:
|
||||
rc = _write_rc(work)
|
||||
interactive = f'bash --noprofile --rcfile "{rc}" -i'
|
||||
mount = variant.mount
|
||||
return (
|
||||
"inner='set -eu\n"
|
||||
"mount -t tmpfs -o mode=0755 demo /media\n"
|
||||
"mount -t tmpfs -o mode=0755 demo /mnt\n"
|
||||
f"mkdir {MOUNT}\n"
|
||||
f"mount -t tmpfs -o size={CARD_SIZE},mode=0755 EASYROMS {MOUNT}\n"
|
||||
f'cp -a "$RETROBIOS_DEMO_CARD/." {MOUNT}/\n'
|
||||
f"mkdir {mount}\n"
|
||||
f"mount -t tmpfs -o size={CARD_SIZE},mode=0755 EASYROMS {mount}\n"
|
||||
f'cp -a "$RETROBIOS_DEMO_CARD/." {mount}/\n'
|
||||
'cd "$HOME"\n'
|
||||
'exec unshare --user --map-user="$RETROBIOS_DEMO_UID" '
|
||||
'--map-group="$RETROBIOS_DEMO_GID" env -i '
|
||||
+ " ".join(f'{name}="${name}"' for name in names) + ' "$@"\'\n'
|
||||
'if [ "$#" -eq 0 ]; then\n'
|
||||
f' set -- bash --noprofile --rcfile "{rc}" -i\n'
|
||||
f" set -- {interactive}\n"
|
||||
"fi\n"
|
||||
'exec unshare --user --mount --map-root-user /bin/sh -c "$inner" sh "$@"\n',
|
||||
encoding="utf-8",
|
||||
'exec unshare --user --mount --map-root-user /bin/sh -c "$inner" sh "$@"\n'
|
||||
)
|
||||
|
||||
|
||||
def _device_session(names: list[str]) -> str:
|
||||
"""ArkOS's own root, made of this machine's /usr and /etc: the user and
|
||||
the Tools menu folder ArkOS is recognised by, EASYROMS at /roms, and
|
||||
the installer where the Tools menu launcher keeps it."""
|
||||
binds = " ".join(
|
||||
f"--symlink usr/{name} /{name}" for name in ("bin", "lib", "lib64", "sbin"))
|
||||
return (
|
||||
'if [ "$#" -eq 0 ]; then\n'
|
||||
f' set -- python3 {ARK_INSTALLER} --tui\n'
|
||||
"fi\n"
|
||||
"exec bwrap --unshare-user --unshare-pid --unshare-ipc --unshare-uts --hostname arkos "
|
||||
'--uid "$RETROBIOS_DEMO_UID" --gid "$RETROBIOS_DEMO_GID" '
|
||||
f"--ro-bind /usr /usr {binds} --ro-bind /etc /etc --proc /proc --dev /dev "
|
||||
f"--tmpfs /tmp --dir {ARK_HOME} --dir /opt/inttools --dir {ARK_TOOLS} "
|
||||
f'--ro-bind "$RETROBIOS_DEMO_INSTALLER" {ARK_INSTALLER} '
|
||||
f'--ro-bind "$RETROBIOS_DEMO_CARD" /card --size 31138512896 --tmpfs /roms '
|
||||
f"--chdir {ARK_HOME} "
|
||||
"/bin/sh -c 'cp -a /card/. /roms/ && exec env -i \"$@\"' sh "
|
||||
+ " ".join(f'{name}="${name}"' for name in names) + ' "$@"\n'
|
||||
)
|
||||
|
||||
|
||||
def write_session(work: Path, names: list[str], variant: Variant = VARIANTS["default"]) -> Path:
|
||||
"""The program the recording's shell becomes: the card mounted in a
|
||||
namespace of its own, then a clean interactive shell (or, on the
|
||||
device, the interface), or the command given as arguments, with only
|
||||
the variables names lists."""
|
||||
body = (_device_session(names) if variant.place == "device"
|
||||
else _computer_session(variant, work, names))
|
||||
session = work / f"session{variant.suffix}.sh"
|
||||
session.write_text("#!/bin/sh\n# Written by scripts/make_demo.py.\nset -eu\n" + body,
|
||||
encoding="utf-8")
|
||||
session.chmod(0o755)
|
||||
return session
|
||||
|
||||
|
||||
def session_env(base_url: str, work: Path, card: Path) -> dict[str, str]:
|
||||
def session_env(base_url: str, work: Path, card: Path,
|
||||
variant: Variant = VARIANTS["default"]) -> dict[str, str]:
|
||||
"""The variables of the run, and of nothing else: the namespace starts
|
||||
from an empty environment."""
|
||||
own = {
|
||||
"RETROBIOS_DEMO_CARD": str(card),
|
||||
"RETROBIOS_DEMO_UID": str(os.getuid()),
|
||||
"RETROBIOS_DEMO_GID": str(os.getgid()),
|
||||
}
|
||||
if variant.place == "device":
|
||||
return {
|
||||
**own,
|
||||
"HOME": ARK_HOME,
|
||||
"PATH": "/usr/local/bin:/usr/bin:/bin",
|
||||
"LANG": "C.UTF-8",
|
||||
# ArkOS's console (ports/tools/RetroBIOS.sh: console 1).
|
||||
"TERM": "linux",
|
||||
"RETROBIOS_OS": variant.os_type,
|
||||
"RETROBIOS_BASE_URL": base_url,
|
||||
# What ports/shared/tui.txt exports before the interface starts.
|
||||
"RETROBIOS_PAD": "gptokeyb",
|
||||
"RETROBIOS_IDLE_EXIT": "300",
|
||||
"RETROBIOS_DEMO_INSTALLER": str(ROOT / "install.py"),
|
||||
}
|
||||
home = work / "home"
|
||||
home.mkdir(parents=True, exist_ok=True)
|
||||
return {
|
||||
**own,
|
||||
"HOME": str(home),
|
||||
"PATH": "/usr/local/bin:/usr/bin:/bin",
|
||||
"LANG": "C.UTF-8",
|
||||
"TERM": "xterm-256color",
|
||||
"TMPDIR": str(work / "scratch"),
|
||||
"RETROBIOS_OS": "linux",
|
||||
"RETROBIOS_OS": variant.os_type,
|
||||
"RETROBIOS_BASE_URL": base_url,
|
||||
# What the downloaded bootstrap sets: commands given back are the
|
||||
# one-line command, not a path to a temporary copy.
|
||||
# What the downloaded bootstrap sets: commands given back are
|
||||
# the one-line command, not a path to a temporary copy.
|
||||
"RETROBIOS_BOOTSTRAP": "sh",
|
||||
"RETROBIOS_DEMO_BOOTSTRAP": str(ROOT / "install.sh"),
|
||||
"RETROBIOS_DEMO_CARD": str(card),
|
||||
"RETROBIOS_DEMO_UID": str(os.getuid()),
|
||||
"RETROBIOS_DEMO_GID": str(os.getgid()),
|
||||
}
|
||||
|
||||
|
||||
def format_size(size: int) -> str:
|
||||
"""The size as the installer writes it."""
|
||||
def _installer() -> object:
|
||||
spec = importlib.util.spec_from_file_location("retrobios_install", ROOT / "install.py")
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("install.py cannot be loaded")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return str(module.format_size(size))
|
||||
return module
|
||||
|
||||
|
||||
def measure(session: Path, env: dict[str, str]) -> dict:
|
||||
def format_size(size: int) -> str:
|
||||
"""The size as the installer writes it."""
|
||||
return str(_installer().format_size(size)) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def describe(variant: str, facts: dict) -> str:
|
||||
"""A recording in words, for a reader who cannot see it: its alt text."""
|
||||
plan = (f"plans {facts['files']} BIOS files ({facts['size']}) for them, installs them "
|
||||
"and shows the summary.")
|
||||
if variant == "handheld":
|
||||
return (f"Recording of the full-screen installer on the {facts['width']} by "
|
||||
f"{facts['height']} screen of a handheld, started from the Tools menu of "
|
||||
f"{facts['setup']}. It finds the {facts['games']} games on the card, {plan} The "
|
||||
"A button chooses, and the bottom line names the buttons.")
|
||||
return (f"Recording of the one-line command in a terminal. The installer finds an "
|
||||
f"{facts['setup']} SD card with {facts['games']} games, {plan}")
|
||||
|
||||
|
||||
def measure(session: Path, env: dict[str, str], variant: Variant = VARIANTS["default"]) -> dict:
|
||||
"""What the recording shows, read from the same run without the screen:
|
||||
the games read on the card and the files planned for them."""
|
||||
command = [str(session), sys.executable, str(ROOT / "install.py"), "--platform", "retroarch",
|
||||
"--dest", f"{MOUNT}/bios", "--for-games", "--dry-run", "--events"]
|
||||
installer = ARK_INSTALLER if variant.place == "device" else str(ROOT / "install.py")
|
||||
options = ([] if variant.place == "device" else
|
||||
["--platform", "retroarch", "--dest", f"{variant.mount}/bios", "--for-games"])
|
||||
command = [str(session), "python3" if variant.place == "device" else sys.executable,
|
||||
installer, *options, "--dry-run", "--events", "--no-input"]
|
||||
result = subprocess.run(command, env=env, capture_output=True, text=True, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"the dry run failed ({result.returncode}):\n{result.stderr}")
|
||||
@@ -219,8 +370,9 @@ def measure(session: Path, env: dict[str, str]) -> dict:
|
||||
games = next(e for e in events if e["event"] == "games")
|
||||
check = next(e for e in events if e["event"] == "check")
|
||||
size = {key.lower(): int(value) for key, value in
|
||||
re.findall(r"^Set (Width|Height) (\d+)$", TAPE.read_text(encoding="utf-8"), re.M)}
|
||||
return {
|
||||
re.findall(r"^Set (Width|Height) (\d+)$", variant.tape.read_text(encoding="utf-8"),
|
||||
re.M)}
|
||||
facts = {
|
||||
"width": size["width"],
|
||||
"height": size["height"],
|
||||
"setup": SETUP,
|
||||
@@ -229,61 +381,93 @@ def measure(session: Path, env: dict[str, str]) -> dict:
|
||||
"files": check["missing"],
|
||||
"size": format_size(check["bytes_needed"]),
|
||||
}
|
||||
if variant.name == "handheld":
|
||||
facts["pad"] = env["RETROBIOS_PAD"]
|
||||
facts["alt"] = describe(variant.name, facts)
|
||||
return facts
|
||||
|
||||
|
||||
def ffmpeg(*args: str) -> None:
|
||||
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", *args], check=True)
|
||||
|
||||
|
||||
def encode(recorded: Path, poster: Path, out: Path, scratch: Path) -> None:
|
||||
palette = scratch / "palette.png"
|
||||
ffmpeg("-i", str(recorded), "-vf", f"fps={GIF_FPS},palettegen=max_colors=48:stats_mode=full",
|
||||
str(palette))
|
||||
ffmpeg("-i", str(recorded), "-i", str(palette), "-lavfi",
|
||||
f"fps={GIF_FPS}[v];[v][1:v]paletteuse=dither=none:diff_mode=rectangle",
|
||||
"-loop", "0", str(out / "install.gif"))
|
||||
ffmpeg("-i", str(recorded), "-an", "-r", str(VIDEO_FPS), "-c:v", "libvpx-vp9",
|
||||
def encode(variant: Variant, out: Path, scratch: Path) -> None:
|
||||
recorded, stem = str(variant.recorded), variant.stem
|
||||
if variant.gif:
|
||||
palette = scratch / "palette.png"
|
||||
ffmpeg("-i", recorded, "-vf", f"fps={GIF_FPS},palettegen=max_colors=48:stats_mode=full",
|
||||
str(palette))
|
||||
ffmpeg("-i", recorded, "-i", str(palette), "-lavfi",
|
||||
f"fps={GIF_FPS}[v];[v][1:v]paletteuse=dither=none:diff_mode=rectangle",
|
||||
"-loop", "0", str(out / f"{stem}.gif"))
|
||||
ffmpeg("-i", recorded, "-an", "-r", str(VIDEO_FPS), "-c:v", "libvpx-vp9",
|
||||
"-b:v", "0", "-crf", "42", "-row-mt", "1", "-deadline", "good",
|
||||
"-pix_fmt", "yuv420p", str(out / "install.webm"))
|
||||
ffmpeg("-i", str(recorded), "-an", "-r", str(VIDEO_FPS), "-c:v", "libx264",
|
||||
"-pix_fmt", "yuv420p", str(out / f"{stem}.webm"))
|
||||
ffmpeg("-i", recorded, "-an", "-r", str(VIDEO_FPS), "-c:v", "libx264",
|
||||
"-preset", "veryslow", "-crf", "30", "-tune", "animation", "-pix_fmt", "yuv420p",
|
||||
"-movflags", "+faststart", str(out / "install.mp4"))
|
||||
ffmpeg("-i", str(poster), "-pred", "mixed", "-compression_level", "100",
|
||||
str(out / "install.png"))
|
||||
"-movflags", "+faststart", str(out / f"{stem}.mp4"))
|
||||
ffmpeg("-i", str(variant.poster_shot), "-pred", "mixed", "-compression_level", "100",
|
||||
str(out / f"{stem}.png"))
|
||||
|
||||
|
||||
def check_sizes(out: Path) -> list[str]:
|
||||
def check_sizes(out: Path, variant: Variant = VARIANTS["default"]) -> list[str]:
|
||||
return [f"{name}: {(out / name).stat().st_size:,} bytes, over {limit:,}"
|
||||
for name, limit in LIMITS.items() if (out / name).stat().st_size > limit]
|
||||
for name, limit in limits(variant).items() if (out / name).stat().st_size > limit]
|
||||
|
||||
|
||||
def load_facts() -> dict:
|
||||
"""demo.json, an empty mapping before the first recording."""
|
||||
if not FACTS.is_file():
|
||||
return {}
|
||||
return json.loads(FACTS.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def record(variant: Variant) -> dict:
|
||||
"""Record one variant into OUT and return what it shows."""
|
||||
card = WORK / "card"
|
||||
build_card(card)
|
||||
with served_repository() as base_url:
|
||||
env = session_env(base_url, WORK, card, variant)
|
||||
session = write_session(WORK, sorted(env), variant)
|
||||
facts = measure(session, env, variant)
|
||||
vhs_env = {**os.environ, **env, "PATH": os.environ.get("PATH", ""),
|
||||
"HOME": os.environ.get("HOME", ""),
|
||||
"RETROBIOS_DEMO_SESSION": str(session)}
|
||||
subprocess.run(["vhs", str(variant.tape.relative_to(ROOT))], cwd=ROOT, env=vhs_env,
|
||||
check=True)
|
||||
encode(variant, OUT, WORK / "scratch")
|
||||
return facts
|
||||
|
||||
|
||||
def needed_tools(variant: Variant) -> list[str]:
|
||||
tools = ["vhs", "ttyd", "ffmpeg"]
|
||||
return [*tools, "bwrap" if variant.place == "device" else "unshare"]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False,
|
||||
description="Record the installer demo (VHS).")
|
||||
parser.parse_args()
|
||||
missing = [tool for tool in ("vhs", "ttyd", "ffmpeg", "unshare") if shutil.which(tool) is None]
|
||||
description="Record the installer demos (VHS).")
|
||||
parser.add_argument("--variant", action="append", choices=sorted(VARIANTS),
|
||||
help="record this variant only (repeatable); every variant by default")
|
||||
args = parser.parse_args()
|
||||
chosen = [VARIANTS[name] for name in (args.variant or VARIANTS)]
|
||||
missing = sorted({tool for variant in chosen for tool in needed_tools(variant)
|
||||
if shutil.which(tool) is None})
|
||||
if missing:
|
||||
sys.exit(f"Missing on PATH: {', '.join(missing)} (VHS: github.com/charmbracelet/vhs)")
|
||||
if WORK.exists():
|
||||
shutil.rmtree(WORK)
|
||||
(WORK / "scratch").mkdir(parents=True)
|
||||
facts = load_facts()
|
||||
over: list[str] = []
|
||||
try:
|
||||
card = WORK / "card"
|
||||
build_card(card)
|
||||
with served_repository() as base_url:
|
||||
env = session_env(base_url, WORK, card)
|
||||
session = write_session(WORK, sorted(env))
|
||||
facts = measure(session, env)
|
||||
vhs_env = {**os.environ, **env, "PATH": os.environ.get("PATH", ""),
|
||||
"HOME": os.environ.get("HOME", ""),
|
||||
"RETROBIOS_DEMO_SESSION": str(session)}
|
||||
subprocess.run(["vhs", str(TAPE.relative_to(ROOT))], cwd=ROOT, env=vhs_env,
|
||||
check=True)
|
||||
encode(RECORDED, POSTER_SHOT, OUT, WORK / "scratch")
|
||||
(OUT / "demo.json").write_text(json.dumps(facts, indent=2) + "\n", encoding="utf-8")
|
||||
for name in (*LIMITS, "demo.json"):
|
||||
print(f"{(OUT / name).relative_to(ROOT)} {(OUT / name).stat().st_size:,} bytes")
|
||||
over = check_sizes(OUT)
|
||||
for variant in chosen:
|
||||
if WORK.exists():
|
||||
shutil.rmtree(WORK)
|
||||
(WORK / "scratch").mkdir(parents=True)
|
||||
facts[variant.name] = record(variant)
|
||||
for name in variant.outputs:
|
||||
print(f"{(OUT / name).relative_to(ROOT)} {(OUT / name).stat().st_size:,} bytes")
|
||||
over += check_sizes(OUT, variant)
|
||||
ordered = {name: facts[name] for name in VARIANTS if name in facts}
|
||||
FACTS.write_text(json.dumps(ordered, indent=2) + "\n", encoding="utf-8")
|
||||
if over:
|
||||
sys.exit("Over the size targets:\n " + "\n ".join(over))
|
||||
finally:
|
||||
|
||||
@@ -20,11 +20,15 @@ on the built site:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import run_cli # noqa: E402
|
||||
|
||||
SITE = "https://abdess.github.io"
|
||||
BASE = "/retrobios/"
|
||||
# Elements whose content is never words a reader needs.
|
||||
@@ -342,4 +346,4 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
run_cli(main)
|
||||
@@ -32,7 +32,7 @@ QUOTING = (f"A folder goes in single quotes, attached to its option. One that ho
|
||||
"command and refuses such a folder.")
|
||||
|
||||
START_PAGES = (
|
||||
("Which pack do I need", "which-pack/", "pick a device or setup, get the folder its BIOS files go in, "
|
||||
("Which pack is needed", "which-pack/", "pick a device or setup, get the folder its BIOS files go in, "
|
||||
"one command that installs and checks them, or a ZIP pack"),
|
||||
("First install", "wiki/first-install/", "the steps from nothing to a working BIOS folder"),
|
||||
("What are BIOS files", "wiki/what-are-bios-files/", "why emulators need them and where they come from"),
|
||||
@@ -57,7 +57,7 @@ MORE = (
|
||||
("Emulators", "emulators/", "one page per emulator"),
|
||||
("Systems", "systems/", "one page per console maker, every file with its hashes"),
|
||||
("Installer", "wiki/installer/", "every option of install.py"),
|
||||
("What we count", "wiki/privacy/", "the site's cookie-free counting"),
|
||||
("What is counted", "wiki/privacy/", "the site's cookie-free counting"),
|
||||
("Is it legal?", "wiki/legal/", "the project's position"),
|
||||
("Contributing", "contributing/", "add a missing file or an emulator profile"),
|
||||
)
|
||||
@@ -142,7 +142,7 @@ ANSWER_RULES = (
|
||||
f"up: {SITE}files/?hash=<value> (the page) or api/v1/lookup/<first two hex>.json (the data).",
|
||||
"Prefer the installer to a manual copy: it puts each file in its folder and checks its hash. "
|
||||
"Give the command the setup's page gives, with `--for-games --required-only` for the fewest files.",
|
||||
"Say where an answer comes from: link the page you read.",
|
||||
"Say where an answer comes from: link the source page.",
|
||||
"On a page of the site, the WebMCP tools answer from the data: setup_bios_files (folder and "
|
||||
"files of a setup), emulator_bios_files, lookup_bios_hash, device_setup_command, search_retrobios.",
|
||||
f"Whether sharing BIOS files is legal depends on the country: point to [Is it legal?]({SITE}wiki/legal/), "
|
||||
@@ -173,7 +173,7 @@ def skill_md() -> str:
|
||||
"",
|
||||
"## Steps",
|
||||
"",
|
||||
"1. Ask where the games are: on the device itself (you have its terminal or SSH), or on "
|
||||
"1. Ask where the games are: on the device itself (with its terminal or SSH), or on "
|
||||
"its card plugged into this computer.",
|
||||
f"2. Preview, nothing is written: `{SH} --for-games --required-only --dry-run` "
|
||||
f"(Windows PowerShell: `{PS} --for-games --required-only --dry-run`).",
|
||||
@@ -221,9 +221,9 @@ def ard_manifest() -> dict:
|
||||
"description": "An agent skill that installs the BIOS files a device needs, only "
|
||||
"what the games on its card need to start.",
|
||||
"representativeQueries": [
|
||||
"install the minimum BIOS files for the games on my R36S running dArkOS",
|
||||
"set up BIOS files for RetroArch on my Steam Deck",
|
||||
"my emulator says BIOS missing, fix it",
|
||||
"install the minimum BIOS files for the games on an R36S running dArkOS",
|
||||
"set up BIOS files for RetroArch on a Steam Deck",
|
||||
"the emulator says BIOS missing, fix it",
|
||||
],
|
||||
"capabilities": ["SetupBios"],
|
||||
"tags": ["bios", "firmware", "emulation", "retrogaming"],
|
||||
|
||||
@@ -397,8 +397,8 @@ def _emulator_names_in(emulators_dir: str) -> dict[str, str]:
|
||||
def _bios_chain_boxes() -> list[Box]:
|
||||
platform, _emulator = CHAIN_EXAMPLES[0]
|
||||
return [
|
||||
Box("Your device", ["A PC, a Steam Deck, a handheld or a TV box"], number="1"),
|
||||
Box("Setup", ["The menu you pick a game from"], number="2"),
|
||||
Box("The device", ["A PC, a Steam Deck, a handheld or a TV box"], number="1"),
|
||||
Box("Setup", ["The menu that picks a game"], number="2"),
|
||||
Box("Emulator", ["Starts the game and reads its BIOS"], number="3"),
|
||||
Box("BIOS folder", [f"{_display(platform)} reads", Mono(_folder(platform))],
|
||||
accent=True, number="4"),
|
||||
@@ -421,8 +421,8 @@ def _bios_chain_caption() -> str:
|
||||
|
||||
def _install_boxes() -> list[Box]:
|
||||
return [
|
||||
Box("Finds your setup", ["And the folder it reads"], number="1"),
|
||||
Box("Looks at your files", ["The ones already in the folder"], number="2"),
|
||||
Box("Finds the setup", ["And the folder it reads"], number="1"),
|
||||
Box("Looks at the files", ["The ones already in the folder"], number="2"),
|
||||
Box("Downloads the rest", ["Only what is missing or different"], number="3"),
|
||||
Box("Checks each file", ["Before saving it"], accent=True, number="4"),
|
||||
]
|
||||
@@ -604,7 +604,7 @@ def _figures() -> dict[str, Figure]:
|
||||
"install-flow": Figure(
|
||||
"install-flow",
|
||||
"What the command does",
|
||||
"The command finds your setup and its BIOS folder, looks at the files already "
|
||||
"The command finds the setup and its BIOS folder, looks at the files already "
|
||||
"there, downloads what is missing or different, then checks each file before "
|
||||
"saving it.",
|
||||
"Running the command again repeats the same steps and downloads only what changed.",
|
||||
@@ -904,7 +904,7 @@ GLANCE_GROUPS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
"--cores-dir")),
|
||||
("Look first", ("--check", "--dry-run", "--list-platforms", "--list-targets",
|
||||
"--list-systems", "--list-cores", "--list-emulators")),
|
||||
("From files you have", ("--from", "--from-pack", "--manifest", "--offline", "--restore")),
|
||||
("From files on hand", ("--from", "--from-pack", "--manifest", "--offline", "--restore")),
|
||||
("How the run talks", ("--tui", "--plain", "--no-input", "--yes", "--quiet", "--verbose",
|
||||
"--events", "--jobs", "--version", "--help")),
|
||||
)
|
||||
@@ -999,7 +999,7 @@ def prepare_page(markdown: str, page: Path) -> str:
|
||||
OG_SECTIONS: dict[str, tuple[str, str]] = {
|
||||
"home": ("BIOS and firmware packs", "Checked file by file, for {frontends}"),
|
||||
"start": ("Start here", "What BIOS files are, and how to install them with one command"),
|
||||
"setup": ("Your setup", "Where the files go on each device and frontend"),
|
||||
"setup": ("Setups", "Where the files go on each device and frontend"),
|
||||
"explore": ("Systems and emulators", "Which files each console and emulator reads"),
|
||||
"reference": ("Reference", "Verification modes, gaps, provenance and the data API"),
|
||||
"contribute": ("Contribute", "Adding files, emulator profiles and platforms"),
|
||||
|
||||
@@ -462,11 +462,14 @@ PLACES = ("device", "server")
|
||||
def terminal_tabs(
|
||||
tabs: tuple[str, ...], args: dict[str, str], indent: str = "", steps: bool = True,
|
||||
how: dict[str, str] | None = None,
|
||||
under_steps: dict[str, list[str]] | None = None,
|
||||
under_command: dict[str, list[str]] | None = None,
|
||||
) -> list[str]:
|
||||
"""Content tabs, one per place the command runs, each with how to open
|
||||
the terminal there. Tab labels are shared across the site, so the reader's
|
||||
choice carries from page to page. *how* replaces the shared text of a tab
|
||||
for one setup."""
|
||||
for one setup. *under_steps* and *under_command* add lines to a tab
|
||||
after its text and after its command (sitesteps.install_box)."""
|
||||
items = []
|
||||
for key in tabs:
|
||||
label, shared, shell = TERMINALS[key]
|
||||
@@ -474,7 +477,9 @@ def terminal_tabs(
|
||||
label = "" # one place, which the text around already names
|
||||
text = (how or {}).get(key) or shared
|
||||
body = [text, ""] if steps else []
|
||||
items.append((label, body + fence(shell, install_command(shell, args.get(key, "")))))
|
||||
body += (under_steps or {}).get(key, [])
|
||||
items.append((label, body + fence(shell, install_command(shell, args.get(key, "")))
|
||||
+ [""] + (under_command or {}).get(key, [])))
|
||||
return markup.tabs(items, indent)
|
||||
|
||||
|
||||
|
||||
@@ -651,8 +651,8 @@ def _sidebar_link(rel: str) -> str:
|
||||
PRUNED_TABS = ("explore",)
|
||||
|
||||
# Reading runs from one tab into the next only here: the last Start here
|
||||
# page leads to Your setup. Every other tab is read on its own.
|
||||
CHAINED_TABS = (("start-here", "your-setup"),)
|
||||
# page leads to Setups. Every other tab is read on its own.
|
||||
CHAINED_TABS = (("start-here", "setups"),)
|
||||
|
||||
|
||||
def topic_id(label: str) -> str:
|
||||
@@ -804,7 +804,7 @@ def sidebar(nav: list, present: set[str], anchors: dict[str, set[str]] | None =
|
||||
|
||||
def _reading(nav: list, present: set[str]) -> list[list[dict]]:
|
||||
"""The previous and next links, in runs. An overview is named after the
|
||||
group or tab it opens ("Your setup", not "Overview")."""
|
||||
group or tab it opens ("Setups", not "Overview")."""
|
||||
def walk(value: list, name: str) -> Iterator[dict]:
|
||||
for entry in value:
|
||||
if isinstance(entry, str):
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
"""What the install box of Get the files shows under each tab.
|
||||
|
||||
Two things per tab, both drawn from data:
|
||||
|
||||
- The steps that differ by system, opening a terminal and pasting the line,
|
||||
as an inline SVG: one card per step, a pictogram and a short caption,
|
||||
drawn with sitediagrams' primitives so it takes the theme's colours in
|
||||
light and dark. The text steps above it (platforms/_guides.yml,
|
||||
terminals) stay the accessible source. The SVG says the same in its title
|
||||
and desc. The pictograms are generic shapes (a menu, a search field, a
|
||||
window, keys, a mouse, the power symbol), no vendor's interface or logo.
|
||||
- The installer demo recorded for that place (scripts/make_demo.py): the
|
||||
full-screen interface on a handheld under "On the device", the terminal
|
||||
recording under the others but Windows. No Windows recording is
|
||||
published (make_demo.py says why), and a curl line recorded in a Linux
|
||||
terminal under the PowerShell command would contradict it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
import sitemarkup as markup
|
||||
from sitediagrams import (
|
||||
FONT,
|
||||
MONO_STYLE,
|
||||
NARROW,
|
||||
STROKE,
|
||||
TEXT_SIZE,
|
||||
WIDE,
|
||||
_badge,
|
||||
_badge_side,
|
||||
_line,
|
||||
_n,
|
||||
_rect,
|
||||
_text,
|
||||
wrap,
|
||||
)
|
||||
|
||||
KEY_SIZE = 11.5
|
||||
ICON_H = 44.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Step:
|
||||
"""One card: a pictogram (*icon* and its *word*, the text it shows:
|
||||
keys joined by "+", the word typed in a search field) and a caption."""
|
||||
|
||||
icon: str
|
||||
caption: str
|
||||
word: str = ""
|
||||
|
||||
|
||||
STEPS: dict[str, tuple[str, tuple[Step, ...]]] = {
|
||||
"windows": ("Windows: open PowerShell and paste the line", (
|
||||
Step("menu", "Open the Start menu"),
|
||||
Step("search", "Type PowerShell and press Enter", "PowerShell"),
|
||||
Step("copy", "Click Copy, the button by the line"),
|
||||
Step("mouse", "Right-click in the window to paste"),
|
||||
Step("keys", "Press Enter", "Enter"),
|
||||
)),
|
||||
"macos": ("macOS: open Terminal and paste the line", (
|
||||
Step("keys", "Press Cmd+Space", "Cmd+Space"),
|
||||
Step("search", "Type Terminal and press Return", "Terminal"),
|
||||
Step("copy", "Click Copy, the button by the line"),
|
||||
Step("keys", "Press Cmd+V to paste", "Cmd+V"),
|
||||
Step("keys", "Press Return", "Return"),
|
||||
)),
|
||||
"linux": ("Linux: open a terminal and paste the line", (
|
||||
Step("keys", "Open a terminal, often Ctrl+Alt+T", "Ctrl+Alt+T"),
|
||||
Step("copy", "Click Copy, the button by the line"),
|
||||
Step("keys", "Press Ctrl+Shift+V to paste", "Ctrl+Shift+V"),
|
||||
Step("keys", "Press Enter", "Enter"),
|
||||
)),
|
||||
"steamdeck": ("Steam Deck: open Konsole and paste the line", (
|
||||
Step("power", "Hold power, pick Switch to Desktop"),
|
||||
Step("window", "Open Konsole"),
|
||||
Step("copy", "Click Copy, the button by the line"),
|
||||
Step("keys", "Press Ctrl+Shift+V to paste", "Ctrl+Shift+V"),
|
||||
Step("keys", "Press Enter", "Enter"),
|
||||
)),
|
||||
}
|
||||
|
||||
# The recording each tab shows (docs_assets/demo/demo.json keys), "" for none.
|
||||
TAB_DEMOS = {"device": "handheld", "windows": ""}
|
||||
DEFAULT_DEMO = "default"
|
||||
|
||||
|
||||
def demo_variant(tab: str) -> str:
|
||||
return TAB_DEMOS.get(tab, DEFAULT_DEMO)
|
||||
|
||||
|
||||
def description(tab: str) -> str:
|
||||
"""The steps in words: the SVG's desc."""
|
||||
return " ".join(f"{index}. {step.caption}." for index, step in
|
||||
enumerate(STEPS[tab][1], start=1))
|
||||
|
||||
|
||||
# Pictograms, drawn in a box *w* wide and ICON_H high from (x, y).
|
||||
|
||||
|
||||
def _stroke(d: str, cls: str = "rb-d-line", width: float = 1.5) -> str:
|
||||
return f'<path d="{d}" fill="none" stroke="{STROKE}" stroke-width="{_n(width)}" class="{cls}"/>'
|
||||
|
||||
|
||||
def _key_width(label: str) -> float:
|
||||
return max(20.0, len(label) * KEY_SIZE * 0.62 + 10)
|
||||
|
||||
|
||||
def _keys(x: float, y: float, w: float, word: str) -> list[str]:
|
||||
labels = word.split("+")
|
||||
total = sum(_key_width(label) for label in labels) + 4 * (len(labels) - 1)
|
||||
left = x + (w - total) / 2
|
||||
out = []
|
||||
top = y + ICON_H / 2 - 12
|
||||
for label in labels:
|
||||
width = _key_width(label)
|
||||
out.append(_rect(left, top, width, 24, accent=True))
|
||||
out.append(
|
||||
f'<text x="{_n(left + width / 2)}" y="{_n(top + 16.5)}" font-size="{_n(KEY_SIZE)}" '
|
||||
f'text-anchor="middle" fill="currentColor" style="{MONO_STYLE}">{escape(label)}</text>')
|
||||
left += width + 4
|
||||
return out
|
||||
|
||||
|
||||
def _menu(x: float, y: float, w: float) -> list[str]:
|
||||
cx, top = x + w / 2, y + 4
|
||||
out = [_rect(cx - 22, top, 44, 36)]
|
||||
out += [_stroke(f"M{_n(cx - 14)} {_n(top + 10 + 8 * i)} H{_n(cx + 14)}") for i in range(3)]
|
||||
return out
|
||||
|
||||
|
||||
def _search(x: float, y: float, w: float, word: str) -> list[str]:
|
||||
width = min(w - 4, len(word) * KEY_SIZE * 0.62 + 34)
|
||||
left, top = x + (w - width) / 2, y + ICON_H / 2 - 12
|
||||
return [
|
||||
_rect(left, top, width, 24, accent=True),
|
||||
f'<circle cx="{_n(left + 11)}" cy="{_n(top + 11)}" r="5" fill="none" stroke="{STROKE}" '
|
||||
'stroke-width="1.5" class="rb-d-line"/>',
|
||||
_stroke(f"M{_n(left + 15)} {_n(top + 15)} L{_n(left + 19)} {_n(top + 19)}"),
|
||||
f'<text x="{_n(left + 25)}" y="{_n(top + 16.5)}" font-size="{_n(KEY_SIZE)}" '
|
||||
f'fill="currentColor" style="{MONO_STYLE}">{escape(word)}</text>',
|
||||
]
|
||||
|
||||
|
||||
def _window(x: float, y: float, w: float) -> list[str]:
|
||||
cx, top = x + w / 2, y + 2
|
||||
return [
|
||||
_rect(cx - 32, top, 64, 40),
|
||||
_stroke(f"M{_n(cx - 32)} {_n(top + 9)} H{_n(cx + 32)}"),
|
||||
f'<text x="{_n(cx - 24)}" y="{_n(top + 28)}" font-size="{_n(KEY_SIZE)}" '
|
||||
f'fill="currentColor" style="{MONO_STYLE}">>_</text>',
|
||||
]
|
||||
|
||||
|
||||
def _copy(x: float, y: float, w: float) -> list[str]:
|
||||
cx, top = x + w / 2, y + 6
|
||||
return [_rect(cx - 12, top, 18, 24), _rect(cx - 6, top + 7, 18, 24, accent=True)]
|
||||
|
||||
|
||||
def _mouse(x: float, y: float, w: float) -> list[str]:
|
||||
cx, top = x + w / 2, y + 2
|
||||
return [
|
||||
f'<rect x="{_n(cx - 13)}" y="{_n(top)}" width="26" height="40" rx="12" fill="none" '
|
||||
f'stroke="{STROKE}" stroke-width="1.5" class="rb-d-line"/>',
|
||||
# The right button, the one to press.
|
||||
f'<path d="M{_n(cx + 1)} {_n(top + 1)} A12 12 0 0 1 {_n(cx + 12)} {_n(top + 13)} '
|
||||
f'L{_n(cx + 1)} {_n(top + 13)} Z" fill="currentColor" class="rb-d-num"/>',
|
||||
_stroke(f"M{_n(cx)} {_n(top)} V{_n(top + 14)} M{_n(cx - 13)} {_n(top + 14)} "
|
||||
f"H{_n(cx + 13)}"),
|
||||
]
|
||||
|
||||
|
||||
def _power(x: float, y: float, w: float) -> list[str]:
|
||||
cx, cy = x + w / 2, y + ICON_H / 2 + 2
|
||||
return [
|
||||
_stroke(f"M{_n(cx - 9)} {_n(cy - 11)} A14 14 0 1 0 {_n(cx + 9)} {_n(cy - 11)}", width=2.5),
|
||||
_stroke(f"M{_n(cx)} {_n(cy - 18)} V{_n(cy - 2)}", width=2.5),
|
||||
]
|
||||
|
||||
|
||||
def _icon(step: Step, x: float, y: float, w: float) -> list[str]:
|
||||
if step.icon == "keys":
|
||||
return _keys(x, y, w, step.word)
|
||||
if step.icon == "search":
|
||||
return _search(x, y, w, step.word)
|
||||
return {"menu": _menu, "window": _window, "copy": _copy, "mouse": _mouse,
|
||||
"power": _power}[step.icon](x, y, w)
|
||||
|
||||
|
||||
def _wide(steps: tuple[Step, ...]) -> tuple[float, float, list[str]]:
|
||||
"""The cards in a row, joined by arrows."""
|
||||
gap = 14.0
|
||||
width = (WIDE - gap * (len(steps) - 1) - 2) / len(steps)
|
||||
captions = [wrap(step.caption, width - 12, TEXT_SIZE) for step in steps]
|
||||
height = 34 + ICON_H + 10 + max(len(lines) for lines in captions) * 18 + 8
|
||||
out: list[str] = []
|
||||
for index, (step, lines) in enumerate(zip(steps, captions)):
|
||||
x = 1 + index * (width + gap)
|
||||
out.append(_rect(x, 1, width, height))
|
||||
out += _badge(x + 8, 8, str(index + 1))
|
||||
out += _icon(step, x + 4, 30, width - 8)
|
||||
y = 30 + ICON_H + 24
|
||||
for line in lines:
|
||||
out.append(_text(x + width / 2, y, line, anchor="middle"))
|
||||
y += 18
|
||||
if index < len(steps) - 1:
|
||||
mid = 1 + height / 2
|
||||
out += _line([(x + width + 1, mid), (x + width + gap - 1, mid)])
|
||||
return WIDE, height + 2, out
|
||||
|
||||
|
||||
def _narrow(steps: tuple[Step, ...]) -> tuple[float, float, list[str]]:
|
||||
"""One card a row: number, pictogram, caption."""
|
||||
icon_w, row_gap = 112.0, 8.0
|
||||
text_x = 8 + _badge_side("9") + 6 + icon_w + 8
|
||||
out: list[str] = []
|
||||
y = 1.0
|
||||
for index, step in enumerate(steps):
|
||||
lines = wrap(step.caption, NARROW - text_x - 8, TEXT_SIZE)
|
||||
height = max(ICON_H + 12, len(lines) * 18 + 16)
|
||||
out.append(_rect(1, y, NARROW - 2, height))
|
||||
out += _badge(8, y + height / 2 - 10, str(index + 1))
|
||||
out += _icon(step, 8 + _badge_side("9") + 6, y + (height - ICON_H) / 2, icon_w)
|
||||
ty = y + height / 2 - (len(lines) - 1) * 9 + 5
|
||||
for line in lines:
|
||||
out.append(_text(text_x, ty, line))
|
||||
ty += 18
|
||||
y += height + row_gap
|
||||
return NARROW, y - row_gap + 1, out
|
||||
|
||||
|
||||
def _svg(tab: str, layout: str) -> str:
|
||||
title, steps = STEPS[tab]
|
||||
width, height, body = (_wide if layout == "wide" else _narrow)(steps)
|
||||
base = f"rb-steps-{tab}-{layout}"
|
||||
return (
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {_n(width)} {_n(height)}" '
|
||||
f'width="{_n(width)}" height="{_n(height)}" role="img" '
|
||||
f'aria-labelledby="{base}-t {base}-d" font-family="{FONT}" '
|
||||
f'class="rb-diagram rb-diagram-{layout}">'
|
||||
f'<title id="{base}-t">{escape(title)}</title>'
|
||||
f'<desc id="{base}-d">{escape(description(tab))}</desc>'
|
||||
+ "".join(body) + "</svg>"
|
||||
)
|
||||
|
||||
|
||||
def steps_figure(tab: str) -> list[str]:
|
||||
"""The figure of a tab's steps, both layouts, or nothing for a tab with
|
||||
no drawing."""
|
||||
if tab not in STEPS:
|
||||
return []
|
||||
return [f'<figure class="rb-figure rb-steps" {markup.search_exclude()}>',
|
||||
_svg(tab, "wide") + _svg(tab, "narrow"), "</figure>", ""]
|
||||
|
||||
|
||||
def tab_demo(tab: str) -> list[str]:
|
||||
"""The recording a tab shows, as generate_site writes the component."""
|
||||
from generate_site import install_demo # noqa: PLC0415
|
||||
|
||||
variant = demo_variant(tab)
|
||||
return install_demo(variant, tab) if variant else []
|
||||
|
||||
|
||||
def install_box(tabs: tuple[str, ...]) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
|
||||
"""What terminal_tabs puts under each tab's steps and under its command."""
|
||||
return ({tab: steps_figure(tab) for tab in tabs},
|
||||
{tab: tab_demo(tab) for tab in tabs})
|
||||
@@ -56,8 +56,8 @@ class ReadmeRegressions(unittest.TestCase):
|
||||
|
||||
def _quick_install(self) -> str:
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
return readme.split("## Quick install", 1)[1].split(
|
||||
"## Download BIOS packs", 1
|
||||
return readme.split("## Install in one line", 1)[1].split(
|
||||
"## Or download a ZIP", 1
|
||||
)[0]
|
||||
|
||||
def test_advertised_bootstrap_urls_are_the_shipped_ones(self):
|
||||
@@ -1121,7 +1121,10 @@ class CatalogRatioRegressions(unittest.TestCase):
|
||||
match = re.search(
|
||||
r"\*\*([\d,]+) of ([\d,]+) system files\*\* matched to", readme
|
||||
)
|
||||
self.assertIsNotNone(match, "README no longer states the catalog ratio")
|
||||
if match is None:
|
||||
# The README states no ratio since it leads to the site: a ratio
|
||||
# it states must still be the database's.
|
||||
return
|
||||
matched, total = (int(g.replace(",", "")) for g in match.groups())
|
||||
self.assertEqual(matched, count_catalog_matched(db))
|
||||
self.assertEqual(total, compute_composition(db)["systems"]["files"])
|
||||
|
||||
+10
-13
@@ -340,7 +340,7 @@ class FilesColumnParagraph(unittest.TestCase):
|
||||
paragraph = next(p for p in readme.split("\n\n") if "The Files column" in p)
|
||||
self.assertNotIn("\n", paragraph.strip())
|
||||
after = readme.split(paragraph, 1)[1].lstrip("\n")
|
||||
self.assertTrue(after.startswith("| Platform | Files |"))
|
||||
self.assertTrue(after.startswith("| Setup | Files |"))
|
||||
|
||||
|
||||
class PageOpenings(unittest.TestCase):
|
||||
@@ -769,14 +769,12 @@ class ReadmeInstallerOptions(unittest.TestCase):
|
||||
import generate_readme
|
||||
|
||||
source = inspect.getsource(generate_readme.generate_readme)
|
||||
self.assertIn('install_command("powershell", "--check")', source)
|
||||
self.assertIn("wiki/installer/#passing-options", source)
|
||||
self.assertIn("## Passing options", _page("installer.md"))
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
quick = _section(readme, "Quick install")
|
||||
from siteguides import INSTALL_PS1
|
||||
|
||||
self.assertIn(f"&([scriptblock]::Create((irm {INSTALL_PS1}))) --check", quick)
|
||||
quick = _section(readme, "Install in one line")
|
||||
# The README gives the bare line and sends options to the page that
|
||||
# writes their PowerShell form: an option after `| iex` reaches iex.
|
||||
self.assertIn("wiki/installer/#passing-options", quick)
|
||||
self.assertNotRegex(readme, r"(?m)^irm \S+ \| iex \S")
|
||||
|
||||
@@ -791,7 +789,7 @@ class PublishedNumbersShareOneFormat(unittest.TestCase):
|
||||
|
||||
readme_source = inspect.getsource(generate_readme.generate_readme)
|
||||
self.assertNotIn("size_mb", readme_source)
|
||||
self.assertIn("format_size(total_size)", readme_source)
|
||||
self.assertIn("format_size(size)", readme_source)
|
||||
self.assertIn("{total_files:,}", inspect.getsource(generate_site))
|
||||
self.assertEqual(generate_readme.format_size(16011 * 1024 * 1024), "15.6 GB")
|
||||
|
||||
@@ -1713,9 +1711,7 @@ class ReadmeKeepsItsPointers(unittest.TestCase):
|
||||
"""The README sent a reader to 'build your own pack below' for a bare
|
||||
minimum, and the block below never named --required-only."""
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
self.assertIn("For a bare minimum, build your own pack below.", readme)
|
||||
block = readme[readme.index("## Build your own pack"):]
|
||||
block = block[:block.index("\n## ", 1)]
|
||||
block = readme[readme.index("<summary><strong>For developers"):readme.index("</details>")]
|
||||
self.assertIn("--required-only", block)
|
||||
|
||||
def test_unsourceable_files_are_not_said_to_exist_nowhere(self):
|
||||
@@ -1725,7 +1721,8 @@ class ReadmeKeepsItsPointers(unittest.TestCase):
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
self.assertNotIn("sourced at all", readme)
|
||||
self.assertNotIn("nobody has made", readme)
|
||||
self.assertRegex(readme, r"more the collection does not hold and does not serve by name")
|
||||
# The README names no count: the gap analysis it links does, by reason.
|
||||
self.assertIn("gaps/", readme)
|
||||
|
||||
|
||||
class ArchitectureStatesTheArbitration(unittest.TestCase):
|
||||
@@ -2041,8 +2038,8 @@ class BuildYourOwnPackSaysWhatTheCloneCosts(unittest.TestCase):
|
||||
line = generate_readme.clone_line(db)
|
||||
self.assertIn("git clone --depth 1", line)
|
||||
self.assertRegex(line, r"over \d+(\.\d)? GB")
|
||||
section = _section((ROOT / "README.md").read_text(encoding="utf-8"),
|
||||
"Build your own pack")
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
section = readme[readme.index("<summary><strong>For developers"):readme.index("</details>")]
|
||||
self.assertIn("git clone --depth 1", section)
|
||||
|
||||
def test_advanced_usage_names_the_shallow_clone(self):
|
||||
|
||||
@@ -191,16 +191,17 @@ class DownloadLinksKeepTheirAddress(unittest.TestCase):
|
||||
from common import list_registered_platforms, list_release_pack_platforms
|
||||
|
||||
readme = _read("README.md")
|
||||
table = _section(readme, "## Download BIOS packs")
|
||||
table = _section(readme, "## Or download a ZIP")
|
||||
self.assertNotIn("/releases/download/", table)
|
||||
released = set(list_release_pack_platforms("platforms"))
|
||||
rows = [line for line in table.splitlines() if line.startswith("| ")]
|
||||
for key in list_registered_platforms("platforms"):
|
||||
with self.subTest(platform=key):
|
||||
# Every setup's lasting page is linked: a ZIP's from its row,
|
||||
# the others from the line that says they install by command.
|
||||
self.assertIn(f"]({SITE_URL}get/{key}/)", table)
|
||||
row = next(line for line in table.splitlines()
|
||||
if f"get/{key}/)" in line)
|
||||
if key not in released:
|
||||
self.assertIn("[Use the command]", row)
|
||||
in_rows = any(f"get/{key}/)" in row for row in rows)
|
||||
self.assertEqual(in_rows, key in released)
|
||||
|
||||
def test_the_download_page_rows_link_get_pages(self):
|
||||
page = _which_pack()
|
||||
@@ -258,7 +259,7 @@ class TheReadmeLeadsBeginnersToTheirPages(unittest.TestCase):
|
||||
def test_quick_install_links_the_start_pages(self):
|
||||
# The code block holds `#` comment lines, so the section runs to
|
||||
# the next level-two heading.
|
||||
section = _read("README.md").split("## Quick install", 1)[1].split("\n## ", 1)[0]
|
||||
section = _read("README.md").split("## Install in one line", 1)[1].split("\n## ", 1)[0]
|
||||
for page in ("first-install", "troubleshooting", "faq"):
|
||||
with self.subTest(page=page):
|
||||
self.assertIn(f"wiki/{page}/)", section)
|
||||
@@ -270,10 +271,10 @@ class EndUsersCheckTheirOwnFolder(unittest.TestCase):
|
||||
files points at the installer's --check, which hashes their folder."""
|
||||
|
||||
USER_SECTIONS = (
|
||||
("wiki/troubleshooting.md", "## 1. Check my BIOS folder { #checking-what-you-installed }"),
|
||||
("wiki/troubleshooting.md", "## 1. Check the BIOS folder { #checking-what-you-installed }"),
|
||||
("wiki/troubleshooting.md", "## 2. Fix the files"),
|
||||
("wiki/faq.md", "## My game shows a black screen"),
|
||||
("wiki/faq.md", "## How do I know which BIOS I need?"),
|
||||
("wiki/faq.md", "## A game shows a black screen { #my-game-shows-a-black-screen }"),
|
||||
("wiki/faq.md", "## How to tell which BIOS is needed? { #how-do-i-know-which-bios-i-need }"),
|
||||
)
|
||||
|
||||
def test_user_sections_never_send_to_verify(self):
|
||||
|
||||
@@ -284,13 +284,13 @@ class EverySurfaceSaysWhy(unittest.TestCase):
|
||||
|
||||
def test_the_readme_rows_read_use_the_command(self):
|
||||
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
|
||||
row = next(line for line in readme.splitlines() if line.startswith("| KNULLI |"))
|
||||
self.assertIn(f"[Use the command]({SITE}get/knulli/)", row)
|
||||
self.assertIn("ZIPs are made for the setups most people download.", readme)
|
||||
line = next(text for text in readme.split("\n\n") if "ZIPs are made for the setups most people download." in text)
|
||||
self.assertIn(f"[KNULLI]({SITE}get/knulli/)", line)
|
||||
self.assertIn("install with the line above", line)
|
||||
|
||||
def test_the_faq_answers_why(self):
|
||||
faq = (REPO_ROOT / "wiki" / "faq.md").read_text(encoding="utf-8")
|
||||
section = faq.split("## Why is there no ZIP for my setup?", 1)[1].split("\n## ", 1)[0]
|
||||
section = faq.split("## Why is there no ZIP for a setup?", 1)[1].split("\n## ", 1)[0]
|
||||
self.assertIn("most people download", section)
|
||||
self.assertIn("checks each one", section)
|
||||
self.assertIn("--dry-run", section)
|
||||
|
||||
@@ -124,7 +124,7 @@ class EveryPageHasAMarkdownCopy(unittest.TestCase):
|
||||
pages = sorted(DIST.rglob("index.html"))
|
||||
missing = [p for p in pages if not (p.parent / "index.md").is_file()]
|
||||
self.assertEqual(missing, [])
|
||||
self.assertIn("# Get your files", (DIST / "which-pack" / "index.md").read_text(encoding="utf-8"))
|
||||
self.assertIn("# Get the files", (DIST / "which-pack" / "index.md").read_text(encoding="utf-8"))
|
||||
self.assertTrue((DIST / "llms-full.txt").is_file())
|
||||
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ class GeneratedSurfaces(unittest.TestCase):
|
||||
|
||||
def test_the_readme_shows_the_short_form(self):
|
||||
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
|
||||
quick = readme.split("## Quick install", 1)[1].split("\n## ", 1)[0]
|
||||
quick = readme.split("## Install in one line", 1)[1].split("\n## ", 1)[0]
|
||||
self.assertIn(f"\n{install_command('sh')}\n", quick)
|
||||
self.assertIn(f"\n{install_command('powershell')}\n", quick)
|
||||
self.assertNotIn("raw.githubusercontent.com", quick)
|
||||
|
||||
@@ -183,7 +183,7 @@ class SectionTabsInTheHeader(unittest.TestCase):
|
||||
nav = re.search(r'<nav class="rb-tabs[^"]*"[^>]*>(.*?)</nav>', html, re.S)
|
||||
self.assertIsNotNone(nav)
|
||||
labels = [re.sub(r"<[^>]+>", "", a).strip() for a in re.findall(r"<a\b[^>]*>.*?</a>", nav.group(1), re.S)]
|
||||
self.assertEqual(labels, ["Start here", "Your setup", "Explore", "Reference", "Contribute"])
|
||||
self.assertEqual(labels, ["Start here", "Setups", "Explore", "Reference", "Contribute"])
|
||||
self.assertEqual(nav.group(1).count('aria-current="true"'), 1)
|
||||
self.assertRegex(nav.group(1), r'aria-current="true"[^>]*>\s*Start here')
|
||||
|
||||
@@ -198,7 +198,7 @@ class LevelBadgeOnlyWhereItAddsSomething(unittest.TestCase):
|
||||
def test_badge_follows_the_crumb(self):
|
||||
# A tab root: the header tab already says it.
|
||||
self.assertNotIn('class="rb-level ', _title_block(_built("platforms")))
|
||||
# Under the "Start here" or "Your setup" crumb the badge repeated it.
|
||||
# Under the "Start here" or "Setups" crumb the badge repeated it.
|
||||
for page in ("which-pack", "platforms/batocera"):
|
||||
with self.subTest(page=page):
|
||||
block = _title_block(_built(page))
|
||||
|
||||
@@ -157,7 +157,7 @@ class NavFromFrontMatter(unittest.TestCase):
|
||||
tabs = self._nav(WIKI)
|
||||
self.assertEqual(tabs["Explore"][0], "explore.md")
|
||||
self.assertEqual([next(iter(e)) for e in tabs["Explore"][1:]], ["Systems", "Emulators"])
|
||||
self.assertEqual(tabs["Your setup"][-1], {"All installer options": "wiki/installer.md"})
|
||||
self.assertEqual(tabs["Setups"][-1], {"All installer options": "wiki/installer.md"})
|
||||
start = [e if isinstance(e, str) else next(iter(e)) for e in tabs["Start here"]]
|
||||
self.assertEqual(start.index("Is it legal?"), start.index("Common questions") + 1)
|
||||
self.assertEqual(tabs["Reference"][0], "wiki/index.md")
|
||||
|
||||
+145
-40
@@ -1,9 +1,12 @@
|
||||
"""The installer demo of the README and the site (scripts/make_demo.py).
|
||||
"""The installer demos of the README and the site (scripts/make_demo.py).
|
||||
|
||||
The home page and Get your files score 98 to 100 on a phone: the video must
|
||||
The home page and Get your files score 98 to 100 on a phone: a video must
|
||||
cost nothing before it is on screen, keep still under reduced motion, and say
|
||||
in words what it shows. The files are committed, so their sizes are held
|
||||
here, and the words come from what the recording measured.
|
||||
here, and the words come from what each recording measured. Get your files
|
||||
shows one recording per tab of its install box: the handheld's under "On the
|
||||
device", none under Windows (no Windows recording is published), the
|
||||
terminal recording under the others.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -29,43 +32,90 @@ COMPONENT = ROOT / "website" / "src" / "components" / "InstallDemo.astro"
|
||||
DIST = ROOT / "website" / "dist"
|
||||
|
||||
|
||||
def _facts() -> dict:
|
||||
def _all_facts() -> dict:
|
||||
return json.loads((DEMO / "demo.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _facts(variant: str = "default") -> dict:
|
||||
return _all_facts()[variant]
|
||||
|
||||
|
||||
class TheFilesAreThereAndSmall(unittest.TestCase):
|
||||
def test_each_output_is_under_its_target(self):
|
||||
for name, limit in make_demo.LIMITS.items():
|
||||
with self.subTest(name=name):
|
||||
self.assertLessEqual((DEMO / name).stat().st_size, limit)
|
||||
for variant in make_demo.VARIANTS.values():
|
||||
for name, limit in make_demo.limits(variant).items():
|
||||
with self.subTest(name=name):
|
||||
self.assertLessEqual((DEMO / name).stat().st_size, limit)
|
||||
|
||||
def test_the_gif_and_the_poster_have_the_recorded_size(self):
|
||||
def test_only_the_readme_variant_has_a_gif(self):
|
||||
gifs = sorted(p.name for p in DEMO.glob("*.gif"))
|
||||
self.assertEqual(gifs, [Path(generate_readme.DEMO_GIF).name])
|
||||
self.assertEqual([v.name for v in make_demo.VARIANTS.values() if v.gif], ["default"])
|
||||
|
||||
def test_the_gif_and_the_posters_have_the_recorded_size(self):
|
||||
facts = _facts()
|
||||
head = (DEMO / "install.gif").read_bytes()[:10]
|
||||
self.assertEqual(head[:6], b"GIF89a")
|
||||
self.assertEqual(struct.unpack("<HH", head[6:10]), (facts["width"], facts["height"]))
|
||||
png = (DEMO / "install.png").read_bytes()[:24]
|
||||
self.assertEqual(png[:8], b"\x89PNG\r\n\x1a\n")
|
||||
self.assertEqual(struct.unpack(">II", png[16:24]), (facts["width"], facts["height"]))
|
||||
for variant in make_demo.VARIANTS.values():
|
||||
facts = _facts(variant.name)
|
||||
png = (DEMO / f"{variant.stem}.png").read_bytes()[:24]
|
||||
with self.subTest(variant=variant.name):
|
||||
self.assertEqual(png[:8], b"\x89PNG\r\n\x1a\n")
|
||||
self.assertEqual(struct.unpack(">II", png[16:24]),
|
||||
(facts["width"], facts["height"]))
|
||||
|
||||
def test_the_videos_are_webm_and_mp4(self):
|
||||
self.assertEqual((DEMO / "install.webm").read_bytes()[:4], b"\x1a\x45\xdf\xa3")
|
||||
self.assertEqual((DEMO / "install.mp4").read_bytes()[4:8], b"ftyp")
|
||||
for variant in make_demo.VARIANTS.values():
|
||||
with self.subTest(variant=variant.name):
|
||||
self.assertEqual((DEMO / f"{variant.stem}.webm").read_bytes()[:4],
|
||||
b"\x1a\x45\xdf\xa3")
|
||||
self.assertEqual((DEMO / f"{variant.stem}.mp4").read_bytes()[4:8], b"ftyp")
|
||||
|
||||
def test_demo_json_holds_each_variant_and_its_alt_text(self):
|
||||
facts = _all_facts()
|
||||
self.assertEqual(list(facts), list(make_demo.VARIANTS))
|
||||
for name, entry in facts.items():
|
||||
with self.subTest(variant=name):
|
||||
self.assertEqual(entry["alt"], make_demo.describe(name, entry))
|
||||
# Written into an MDX attribute: no quote of either kind.
|
||||
self.assertNotRegex(entry["alt"], "[\"']")
|
||||
|
||||
|
||||
class TheTapeAndTheDriverAgree(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tape = make_demo.TAPE.read_text(encoding="utf-8")
|
||||
|
||||
def test_the_tape_writes_what_the_driver_encodes(self):
|
||||
self.assertIn(f"Output {make_demo.RECORDED.relative_to(ROOT).as_posix()}\n", self.tape)
|
||||
self.assertIn(f"Screenshot {make_demo.POSTER_SHOT.relative_to(ROOT).as_posix()}\n",
|
||||
self.tape)
|
||||
def test_each_tape_writes_what_the_driver_encodes(self):
|
||||
for variant in make_demo.VARIANTS.values():
|
||||
tape = variant.tape.read_text(encoding="utf-8")
|
||||
with self.subTest(variant=variant.name):
|
||||
self.assertIn(f"Output {variant.recorded.relative_to(ROOT).as_posix()}\n", tape)
|
||||
self.assertIn(
|
||||
f"Screenshot {variant.poster_shot.relative_to(ROOT).as_posix()}\n", tape)
|
||||
|
||||
def test_the_size_in_demo_json_is_the_tapes(self):
|
||||
facts = _facts()
|
||||
self.assertIn(f"Set Width {facts['width']}\n", self.tape)
|
||||
self.assertIn(f"Set Height {facts['height']}\n", self.tape)
|
||||
def test_the_size_in_demo_json_is_each_tapes(self):
|
||||
for variant in make_demo.VARIANTS.values():
|
||||
facts, tape = _facts(variant.name), variant.tape.read_text(encoding="utf-8")
|
||||
with self.subTest(variant=variant.name):
|
||||
self.assertIn(f"Set Width {facts['width']}\n", tape)
|
||||
self.assertIn(f"Set Height {facts['height']}\n", tape)
|
||||
|
||||
def test_the_handheld_is_an_r36s_screen_driven_by_its_buttons(self):
|
||||
variant = make_demo.VARIANTS["handheld"]
|
||||
tape = variant.tape.read_text(encoding="utf-8")
|
||||
self.assertIn("Set Width 640\n", tape)
|
||||
self.assertIn("Set Height 480\n", tape)
|
||||
# Nothing typed but the hidden start of the session: the buttons
|
||||
# gptokeyb maps (ports/shared/retrobios.gptk: a = enter) do the rest.
|
||||
self.assertEqual(re.findall(r"^Type (.*)$", tape, re.M),
|
||||
["'exec \"$RETROBIOS_DEMO_SESSION\"'"])
|
||||
gptk = (ROOT / "ports" / "shared" / "retrobios.gptk").read_text(encoding="utf-8")
|
||||
self.assertRegex(gptk, r"(?m)^a = enter$")
|
||||
env = make_demo.session_env("http://127.0.0.1:1", ROOT / "tmp", ROOT / "tmp", variant)
|
||||
self.assertEqual(env["RETROBIOS_PAD"], "gptokeyb")
|
||||
self.assertEqual(env["RETROBIOS_OS"], "linux")
|
||||
self.assertEqual(variant.mount, "/roms")
|
||||
|
||||
def test_the_command_typed_is_the_one_the_pages_give(self):
|
||||
from siteguides import install_command # noqa: PLC0415
|
||||
@@ -84,8 +134,8 @@ class TheReadmeShowsTheGif(unittest.TestCase):
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
line = f""
|
||||
self.assertIn(line, readme)
|
||||
self.assertLess(readme.index("## Quick install"), readme.index(line))
|
||||
self.assertLess(readme.index(line), readme.index("## Download BIOS packs"))
|
||||
self.assertLess(readme.index("## Install in one line"), readme.index(line))
|
||||
self.assertLess(readme.index(line), readme.index("## Or download a ZIP"))
|
||||
|
||||
def test_the_description_names_what_the_recording_measured(self):
|
||||
facts = _facts()
|
||||
@@ -112,7 +162,13 @@ class TheVideoLoadsOnlyOnScreen(unittest.TestCase):
|
||||
self.assertRegex(self.markup, rf"\n\t\t{attribute}\n")
|
||||
|
||||
def test_webm_comes_before_mp4(self):
|
||||
self.assertLess(self.markup.index("install.webm"), self.markup.index("install.mp4"))
|
||||
self.assertLess(self.markup.index("asset('webm')"), self.markup.index("asset('mp4')"))
|
||||
|
||||
def test_each_variant_has_its_files_and_its_own_ids(self):
|
||||
self.assertIn("const stem = variant === 'default' ? 'install' : `install-${variant}`;",
|
||||
self.source)
|
||||
self.assertIn("const textId = name ? `rb-demo-text-${name}` : 'rb-demo-text';",
|
||||
self.source)
|
||||
|
||||
def test_reduced_motion_keeps_the_poster_with_a_button(self):
|
||||
self.assertIn("matchMedia('(prefers-reduced-motion: reduce)')", self.source)
|
||||
@@ -121,44 +177,93 @@ class TheVideoLoadsOnlyOnScreen(unittest.TestCase):
|
||||
self.markup)
|
||||
|
||||
def test_screen_readers_get_the_description(self):
|
||||
self.assertIn('aria-describedby="rb-demo-text"', self.markup)
|
||||
self.assertIn('<figcaption id="rb-demo-text" class="sr-only">{description}</figcaption>',
|
||||
self.assertIn("aria-describedby={textId}", self.markup)
|
||||
self.assertIn('<figcaption id={textId} class="sr-only">{description}</figcaption>',
|
||||
self.markup)
|
||||
|
||||
def test_the_component_is_registered(self):
|
||||
self.assertEqual(sitestarlight.COMPONENTS["InstallDemo"], "components/InstallDemo.astro")
|
||||
|
||||
|
||||
def _tab_bodies(markdown: str) -> dict[str, str]:
|
||||
"""Each tab of the quick install box of Get your files, by label."""
|
||||
box = markdown[markdown.index("{ #quick-install }"):]
|
||||
box = box[:box.index("</Tabs>")]
|
||||
return dict(re.findall(r'<TabItem label="([^"]+)">(.*?)</TabItem>', box, re.S))
|
||||
|
||||
|
||||
class TheTwoPagesShowIt(unittest.TestCase):
|
||||
def test_home_and_get_your_files(self):
|
||||
def test_home_shows_the_terminal_recording(self):
|
||||
import generate_site # noqa: PLC0415
|
||||
|
||||
tag = generate_site.install_demo()[0]
|
||||
self.assertIn(f'description="{generate_readme.demo_description()}"', tag)
|
||||
source = Path(generate_site.__file__).read_text(encoding="utf-8")
|
||||
for function in ("def generate_home(", "def generate_which_pack("):
|
||||
body = source[source.index(function):]
|
||||
body = body[:body.index("\ndef ", 1)]
|
||||
with self.subTest(function=function):
|
||||
self.assertIn("*install_demo(),", body)
|
||||
body = source[source.index("def generate_home("):]
|
||||
body = body[:body.index("\ndef ", 1)]
|
||||
self.assertIn("*install_demo(),", body)
|
||||
|
||||
def test_the_site_serves_the_video_not_the_gif(self):
|
||||
def test_each_tab_of_get_your_files_shows_its_variant(self):
|
||||
import generate_site # noqa: PLC0415
|
||||
|
||||
self.assertEqual(generate_site.DEMO_ASSETS, ("install.webm", "install.mp4", "install.png"))
|
||||
tabs = _tab_bodies(generate_site.generate_which_pack())
|
||||
expected = {"Windows": None, "macOS": "default", "Linux": "default",
|
||||
"Steam Deck": "default", "Android (Termux)": "default",
|
||||
"On the device": "handheld"}
|
||||
self.assertEqual(set(tabs), set(expected))
|
||||
for label, variant in expected.items():
|
||||
demos = re.findall(r"<InstallDemo [^>]*/>", tabs[label])
|
||||
with self.subTest(tab=label):
|
||||
if variant is None:
|
||||
self.assertEqual(demos, [])
|
||||
continue
|
||||
self.assertEqual(len(demos), 1)
|
||||
self.assertIn(f'description="{_facts(variant)["alt"]}"', demos[0])
|
||||
self.assertIn(f'width="{_facts(variant)["width"]}"', demos[0])
|
||||
self.assertIn(f'variant="{variant}"', demos[0])
|
||||
# The demo below the box moved into its tabs: one per tab, no other.
|
||||
page = generate_site.generate_which_pack()
|
||||
self.assertEqual(page.count("<InstallDemo "), len(expected) - 1)
|
||||
|
||||
def test_the_site_serves_the_videos_not_the_gif(self):
|
||||
import generate_site # noqa: PLC0415
|
||||
|
||||
expected = tuple(name for variant in make_demo.VARIANTS.values()
|
||||
for name in variant.outputs if not name.endswith(".gif"))
|
||||
self.assertEqual(sorted(generate_site.DEMO_ASSETS), sorted(expected))
|
||||
|
||||
def test_the_built_pages_fetch_nothing_of_it(self):
|
||||
if not (DIST / "index.html").is_file():
|
||||
self.skipTest("no built site")
|
||||
for page in ("index.html", "which-pack/index.html"):
|
||||
html = (DIST / page).read_text(encoding="utf-8")
|
||||
figure = re.search(r'<figure class="rb-demo[^"]*".*?</figure>', html, re.S)
|
||||
figures = re.findall(r'<figure class="rb-demo[^"]*".*?</figure>', html, re.S)
|
||||
with self.subTest(page=page):
|
||||
self.assertIsNotNone(figure)
|
||||
self.assertIn('preload="none"', figure.group(0))
|
||||
self.assertNotRegex(figure.group(0), r"\s(src|poster)=")
|
||||
for name in ("install.webm", "install.mp4", "install.png"):
|
||||
self.assertTrue((DIST / "assets" / "demo" / name).is_file())
|
||||
self.assertTrue(figures)
|
||||
for figure in figures:
|
||||
self.assertIn('preload="none"', figure)
|
||||
self.assertNotRegex(figure, r"\s(src|poster)=")
|
||||
for name in generate_site_assets():
|
||||
self.assertTrue((DIST / "assets" / "demo" / name).is_file())
|
||||
|
||||
def test_the_built_tabs_hold_their_variant(self):
|
||||
page = DIST / "which-pack" / "index.html"
|
||||
if not page.is_file():
|
||||
self.skipTest("no built site")
|
||||
html = page.read_text(encoding="utf-8")
|
||||
panel = r'<div[^>]*role="tabpanel"'
|
||||
panels = re.findall(rf"{panel}[^>]*>(.*?)(?={panel}|</starlight-tabs>)", html, re.S)
|
||||
variants = [re.findall(r'data-variant="([a-z]+)"', panel) for panel in panels]
|
||||
# The quick install box: Windows, macOS, Linux, Steam Deck, Android, device.
|
||||
self.assertIn([[], ["default"], ["default"], ["default"], ["default"], ["handheld"]],
|
||||
[variants[i:i + 6] for i in range(len(variants) - 5)])
|
||||
self.assertIn("assets/demo/install-handheld.png", html)
|
||||
|
||||
|
||||
def generate_site_assets() -> tuple[str, ...]:
|
||||
import generate_site # noqa: PLC0415
|
||||
|
||||
return generate_site.DEMO_ASSETS
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -258,7 +258,7 @@ class TheSetupListLooksLikeAList(unittest.TestCase):
|
||||
self.assertEqual(len(re.findall(r"--rb-select-arrow:", css)), 2)
|
||||
# The box's select sits in Markdown content, where the shared rule applies.
|
||||
box = "\n".join(gs._console_install_block("PlayStation Portable", PAIRS))
|
||||
self.assertIn('<label class="rb-install-setup">Your setup <select>', box)
|
||||
self.assertIn('<label class="rb-install-setup">Setup <select>', box)
|
||||
|
||||
def test_the_built_select_shows_the_arrow(self) -> None:
|
||||
if not (DIST / "systems").is_dir():
|
||||
|
||||
@@ -67,7 +67,7 @@ class TheIntroSaysWhatIsNeeded(unittest.TestCase):
|
||||
profile = _profile("duckstation")
|
||||
intro = gs._emulator_intro("DuckStation", profile)
|
||||
self.assertIn(
|
||||
"It picks the BIOS for your game's region: scph5500.bin (Japan), "
|
||||
"It picks the BIOS for the game's region: scph5500.bin (Japan), "
|
||||
"scph5501.bin (North America and Latin America) or scph5502.bin "
|
||||
"(Europe and Oceania).", intro)
|
||||
others = len(profile["files"]) - 3
|
||||
@@ -417,7 +417,7 @@ class AnAppThatReadsAnyBios(unittest.TestCase):
|
||||
# LRPS2 runs in RetroArch, whose BIOS folder the installer finds.
|
||||
page = _pages("lrps2")["lrps2"]
|
||||
self.assertIn("--emulator lrps2", page)
|
||||
self.assertIn("2. Put it in `pcsx2/bios/`, inside your BIOS folder.", page)
|
||||
self.assertIn("2. Put it in `pcsx2/bios/`, inside the BIOS folder.", page)
|
||||
self.assertIn("a Sony PlayStation 2 BIOS, any file name", page)
|
||||
|
||||
def test_only_found_by_content_with_one_bios(self):
|
||||
@@ -752,7 +752,7 @@ class SmallFixes(unittest.TestCase):
|
||||
|
||||
def test_region_choice_summary(self) -> None:
|
||||
lines = gs._emulator_file_summary(_profile("duckstation")["files"])
|
||||
self.assertIn("One of 3 is required, for your games' region.", lines[0])
|
||||
self.assertIn("One of 3 is required, for the games' region.", lines[0])
|
||||
self.assertNotIn("3 required", lines[0])
|
||||
|
||||
def test_no_emulator_part_says_where_it_sits(self) -> None:
|
||||
|
||||
+11
-11
@@ -190,7 +190,7 @@ class GetYourFilesOrder(unittest.TestCase):
|
||||
def test_choice_then_tables_then_picker_then_command(self):
|
||||
text = _page("which-pack.md")
|
||||
order = [text.index(mark) for mark in (
|
||||
"## Pick your setup", "| Setup | Runs on | BIOS folder | ZIP |", "<FilePicker",
|
||||
"## Pick the setup { #pick-your-setup }", "| Setup | Runs on | BIOS folder | ZIP |", "<FilePicker",
|
||||
"## Not in the list? One command finds it", "It asks **",
|
||||
"<summary>Is it safe?</summary>",
|
||||
)]
|
||||
@@ -221,7 +221,7 @@ class GetYourFilesOrder(unittest.TestCase):
|
||||
class GuideRoutes(unittest.TestCase):
|
||||
def test_a_device_system_opens_on_the_computer_route(self):
|
||||
text = _page("platforms/batocera.md")
|
||||
self.assertLess(text.index("## From your computer"), text.index("## On the device"))
|
||||
self.assertLess(text.index("## From a computer"), text.index("## On the device"))
|
||||
self.assertIn("{ #install }", "\n".join(generate_site._guide_install(
|
||||
"batocera", siteguides.GUIDES["batocera"], "## On the device { #install }")))
|
||||
self.assertIn("Unzip them all into the network folder above", text)
|
||||
@@ -347,7 +347,7 @@ class HomeLinksTheOtherTasks(unittest.TestCase):
|
||||
for target in ("/retrobios/wiki/troubleshooting/", "/retrobios/files/",
|
||||
"/retrobios/wiki/legal/"):
|
||||
self.assertIn(f"]({target})", home)
|
||||
self.assertLess(home.index("/retrobios/wiki/troubleshooting/"), home.index("## Do I need it?"))
|
||||
self.assertLess(home.index("/retrobios/wiki/troubleshooting/"), home.index("## Is it needed? { #do-i-need-it }"))
|
||||
|
||||
|
||||
GUIDE = siteguides.HANDHELD_GUIDE
|
||||
@@ -425,7 +425,7 @@ class HandheldWayIn(unittest.TestCase):
|
||||
def test_the_guide_is_in_the_handheld_group_of_the_navigation(self):
|
||||
coverages = {key: {"platform": _display(key)} for key in REGISTRY}
|
||||
nav = generate_site.generate_nav(coverages, {}, {}, REGISTRY)
|
||||
setup = next(tab["Your setup"] for tab in nav if "Your setup" in tab)
|
||||
setup = next(tab["Setups"] for tab in nav if "Setups" in tab)
|
||||
groups = {next(iter(item)): next(iter(item.values()))
|
||||
for item in setup if isinstance(item, dict)}
|
||||
label = siteguides.DEVICE_GROUPS["handheld"][0]
|
||||
@@ -526,7 +526,7 @@ class SafetyListIsLinked(unittest.TestCase):
|
||||
class OfflineLine(unittest.TestCase):
|
||||
def test_under_from_your_computer(self):
|
||||
text = _page("platforms/batocera.md")
|
||||
start = text.index("## From your computer")
|
||||
start = text.index("## From a computer")
|
||||
line = text.index("No internet on the device?")
|
||||
self.assertLess(start, line)
|
||||
self.assertLess(line, text.index("## On the device"))
|
||||
@@ -682,24 +682,24 @@ class NoZipNoUnzipText(unittest.TestCase):
|
||||
def test_the_minui_family_makes_each_folder(self):
|
||||
for name in ("minui", "myminui", "nextui"):
|
||||
with self.subTest(setup=name):
|
||||
self.assertIn("The command makes each console's folder for you.",
|
||||
self.assertIn("The command makes each console's folder.",
|
||||
_plain_page(f"platforms/{name}.md"))
|
||||
|
||||
def test_knulli_is_filled_over_the_network_with_the_samba_switch_cited(self):
|
||||
text = _guide_part(_plain_page("platforms/knulli.md"))
|
||||
self.assertIn("## From your computer { #from-your-computer }", text)
|
||||
self.assertIn("## From a computer { #from-your-computer }", text)
|
||||
self.assertIn("--platform knulli --dest \\\\KNULLI\\share\\bios\n", text)
|
||||
self.assertRegex(text, r"turn on \*\*SAMBA\*\* in \*\*System Settings > Services\*\*\s*"
|
||||
r"\(\[KNULLI docs\]\(https://github\.com/knulli-cfw/knulli\.org/blob/"
|
||||
r"[0-9a-f]{40}/docs/play/add-games/network-transfer\.md")
|
||||
# The computer route comes before the SSH one.
|
||||
self.assertLess(text.index("## From your computer"), text.index("## On the device"))
|
||||
self.assertLess(text.index("## From a computer"), text.index("## On the device"))
|
||||
|
||||
def test_the_elec_systems_say_they_need_the_network(self):
|
||||
for name, share in (("amberelec", "\\\\AmberELEC\\roms"), ("emuelec", "\\\\EMUELEC\\roms")):
|
||||
text = _guide_part(_plain_page(f"platforms/{name}.md"))
|
||||
with self.subTest(setup=name):
|
||||
self.assertIn("Needs the device on your network.", text)
|
||||
self.assertIn("Needs the device on the same network.", text)
|
||||
self.assertIn(f"--platform {name} --dest {share}\n", text)
|
||||
|
||||
def test_a_mac_and_a_linux_desktop_reach_the_share_too(self):
|
||||
@@ -935,14 +935,14 @@ class ARouteWithoutANetwork(unittest.TestCase):
|
||||
for page in (f"platforms/{name}.md", f"get/{name}.md"):
|
||||
text = _plain_page(page)
|
||||
with self.subTest(page=page):
|
||||
self.assertIn("## From your computer { #from-your-computer }", text)
|
||||
self.assertIn("## From a computer { #from-your-computer }", text)
|
||||
self.assertIn(CARD_FAQ, text)
|
||||
self.assertIn(f"the folder the device calls `{extract_to}`", text)
|
||||
self.assertIn(f"--platform {name} --dest /path/to/", text)
|
||||
|
||||
def test_rocknix_has_a_computer_route(self):
|
||||
text = _guide_part(_plain_page("platforms/rocknix.md"))
|
||||
self.assertLess(text.index("## From your computer"), text.index("## On the device"))
|
||||
self.assertLess(text.index("## From a computer"), text.index("## On the device"))
|
||||
|
||||
def test_retropie_card_is_read_from_linux_only(self):
|
||||
self.assertEqual(siteguides.GUIDES["retropie"].card_run, ("linux",))
|
||||
|
||||
@@ -56,14 +56,14 @@ class FiveTabs(unittest.TestCase):
|
||||
nav = generate_site.generate_nav({}, {}, {}, {})
|
||||
self.assertEqual(
|
||||
[next(iter(tab)) for tab in nav],
|
||||
["Start here", "Your setup", "Explore", "Reference", "Contribute"],
|
||||
["Start here", "Setups", "Explore", "Reference", "Contribute"],
|
||||
)
|
||||
|
||||
def test_each_tab_opens_on_an_index_or_a_first_page(self):
|
||||
nav = generate_site.generate_nav({}, {}, {}, {})
|
||||
tabs = {next(iter(tab)): next(iter(tab.values())) for tab in nav}
|
||||
self.assertEqual(tabs["Start here"][0], "index.md")
|
||||
self.assertEqual(tabs["Your setup"][0], "platforms/index.md")
|
||||
self.assertEqual(tabs["Setups"][0], "platforms/index.md")
|
||||
self.assertEqual(tabs["Reference"][0], "wiki/index.md")
|
||||
self.assertEqual(tabs["Contribute"][0], "contributing.md")
|
||||
|
||||
@@ -125,7 +125,7 @@ class EveryPlatformOnThePicker(unittest.TestCase):
|
||||
for key in _registered()
|
||||
}
|
||||
nav = generate_site.generate_nav(coverages, {}, {}, registry)
|
||||
setup = next(tab["Your setup"] for tab in nav if "Your setup" in tab)
|
||||
setup = next(tab["Setups"] for tab in nav if "Setups" in tab)
|
||||
groups = {next(iter(item)): next(iter(item.values()))
|
||||
for item in setup if isinstance(item, dict)}
|
||||
for key in coverages:
|
||||
@@ -278,7 +278,7 @@ class GuidePagesEndOnANextStep(unittest.TestCase):
|
||||
text = sitepages.page("platforms/batocera.md").read_text(encoding="utf-8")
|
||||
# Batocera runs on its own device: the computer route comes first.
|
||||
order = [text.index(h) for h in (
|
||||
"## Where the files go", "## From your computer", "## On the device",
|
||||
"## Where the files go", "## From a computer", "## On the device",
|
||||
"## How to check", "## Reference: every file by system",
|
||||
)]
|
||||
self.assertEqual(order, sorted(order))
|
||||
|
||||
@@ -210,7 +210,7 @@ class FaqInstallsFromWhatYouHold(unittest.TestCase):
|
||||
|
||||
def test_no_unzip_before_from_pack(self):
|
||||
section = " ".join(self._section("No internet on the device?").split())
|
||||
self.assertIn("point `--from-pack` at your ZIP:", section)
|
||||
self.assertIn("point `--from-pack` at the ZIP:", section)
|
||||
self.assertNotIn("unzip", section)
|
||||
install = (REPO_ROOT / "install.py").read_text(encoding="utf-8")
|
||||
self.assertIn("Install from a pack already downloaded: its ZIP, one of its parts",
|
||||
@@ -237,7 +237,7 @@ class FaqInstallsFromWhatYouHold(unittest.TestCase):
|
||||
word = rule.name.split()[0]
|
||||
with self.subTest(share=rule.name):
|
||||
self.assertIn(word, section)
|
||||
self.assertRegex(readme, rf"# SD card mounted on a PC: .*\b{word}\b")
|
||||
self.assertRegex(readme, rf"\b{word}\b[^.]*SD card plugged into the computer is found by itself")
|
||||
# The label the first screen gives a card found: "Onion SD card".
|
||||
self.assertEqual(install._card_word(("retroarch", Path("/x/BIOS"), "Onion")), "SD card")
|
||||
self.assertEqual(install._card_word(("batocera", Path("/x/bios"), "Batocera")),
|
||||
|
||||
@@ -118,7 +118,7 @@ class GroupsAreGroups(unittest.TestCase):
|
||||
self.groups = self.side["groups"]
|
||||
|
||||
def test_setup_groups_stay_groups_and_point_at_their_section(self):
|
||||
setup = {key: value for key, value in self.groups.items() if key.startswith("Your setup/")}
|
||||
setup = {key: value for key, value in self.groups.items() if key.startswith("Setups/")}
|
||||
self.assertTrue(setup)
|
||||
for key, group in setup.items():
|
||||
with self.subTest(group=key):
|
||||
@@ -206,7 +206,7 @@ class OneNextPerPage(unittest.TestCase):
|
||||
|
||||
def test_an_overview_is_named_after_what_it_opens(self):
|
||||
labels = {entry["link"]: entry["label"] for run in self.runs for entry in run}
|
||||
self.assertEqual(labels["/retrobios/platforms/"], "Your setup")
|
||||
self.assertEqual(labels["/retrobios/platforms/"], "Setups")
|
||||
self.assertEqual(labels["/retrobios/systems/"], "Systems")
|
||||
self.assertNotIn("Overview", labels.values())
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
"""The site speaks without personal pronouns.
|
||||
|
||||
Every page is written as an instruction or a plain statement: "Open the
|
||||
Start menu.", "The installer finds the BIOS folder." No I, we, you or their
|
||||
forms in the wiki sources, the pages the generators write, the README and
|
||||
CONTRIBUTING, llms.txt or the agent skill. Code, URLs, markup and anchor ids
|
||||
are not prose.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
|
||||
|
||||
import html
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DOCS = ROOT / "website" / "src" / "content" / "docs"
|
||||
|
||||
PRONOUN = re.compile(
|
||||
r"(?<![\w-])((?-i:I)|me|my|mine|myself|we|us|our|ours|ourselves"
|
||||
r"|you|your|yours|yourself|yourselves)(?![\w-])",
|
||||
re.I,
|
||||
)
|
||||
|
||||
FENCED = re.compile(r"^[ \t]*(```|~~~).*?^[ \t]*\1[^\n]*$", re.S | re.M)
|
||||
INLINE_CODE = re.compile(r"(`+)(?:(?!\n\n)[^`])*?\1")
|
||||
HTML_COMMENT = re.compile(r"<!--.*?-->", re.S)
|
||||
SCRIPT_STYLE = re.compile(r"<(script|style|pre|code)\b.*?</\1>", re.S | re.I)
|
||||
TAG = re.compile(r"</?[A-Za-z][^<>]*>")
|
||||
ENTITY = re.compile(r"&#(?:\d+|x[0-9a-fA-F]+);")
|
||||
URL = re.compile(r"(?:https?|ftp|smb)://\S+|\bwww\.\S+")
|
||||
LINK_TARGET = re.compile(r"\]\([^)\s]*(?:\s+\"[^\"]*\")?\)|^\[[^\]]+\]:\s*\S+.*$", re.M)
|
||||
ANCHOR = re.compile(r"\{[^{}\n]*#[^{}\n]*\}")
|
||||
PATHLIKE = re.compile(r"\S*[/\\]\S*")
|
||||
# File names, domains and dotted initials: baserom.us.z64, delete.me, A.I.R.
|
||||
DOTTED = re.compile(r"[\w-]+(?:\.[\w-]+)+\.?")
|
||||
# A list of single letters or digits (D, F, I, P), or of lowercase option
|
||||
# values in parentheses (euro, us, asia), names keys, not people.
|
||||
LETTERS = re.compile(r"(?<!\w)[A-Z0-9](?:,\s?[A-Z0-9#@^](?!\w))+")
|
||||
VALUES = re.compile(r"\((?:[a-z][\w-]*, )+[a-z][\w-]*\)")
|
||||
LETTER_HEADING = re.compile(r"^#+ [A-Z]\s*$", re.M)
|
||||
# Search keywords, written the way readers type a query. Pagefind matches
|
||||
# them, the page never shows them.
|
||||
TERMS = re.compile(r"^terms: .*$", re.M)
|
||||
# A capitalised "US" names the country and its region code.
|
||||
COUNTRY = re.compile(r"(?<![\w-])US(?![\w-])")
|
||||
|
||||
# Proper names that hold one of the words.
|
||||
NAMES: dict[str, str] = {
|
||||
r"I(?:'|''|'|')Max": "I'Max MACS, an arcade board maker",
|
||||
r"My (?:Old)?Boy!": "My Boy! and My OldBoy!, Android emulators",
|
||||
r"My Vision": "Nichibutsu My Vision, a console",
|
||||
r"On My iPhone": "the Files app location on iOS",
|
||||
r"Us vs Them": "a game title",
|
||||
r"Aliens Ate My Railway|Fell Apart On Me": "OpenTTD music track titles",
|
||||
r"\b(?:Model|Level|Series|Mk|MK|Ultima|Interface|Type) I\b(?!')":
|
||||
"a roman numeral in a machine or game name (Model I, Ultima I)",
|
||||
r"\bI(?= \([A-Z])": "a model letter, I (Palladium)",
|
||||
r"Java ME": "Java ME, the mobile Java platform",
|
||||
r"laseractive us(?=:)": "the LaserActive region key of geargrafx's BIOS list",
|
||||
}
|
||||
|
||||
# Program output quoted word for word. INSTALLER holds install.py's own
|
||||
# messages, which the pass over the installer's wording rewrites, and empties
|
||||
# this list with it. SOURCE holds what an emulator prints, quoted in its
|
||||
# profile from the emulator's code, which this project does not write.
|
||||
INSTALLER: dict[str, str] = {
|
||||
"Where do you play?": "title of the installer's first screen",
|
||||
"Copy the BIOS files from your old card or a folder": "installer menu row",
|
||||
"copy the BIOS files from your other card": "installer menu row on a two-card handheld",
|
||||
"if your files go there.": "installer hint for a card with no handheld system folder",
|
||||
"See [Files you already had]": (
|
||||
"link label in the generated options table of wiki/installer.md, "
|
||||
"kept by generate_installer_doc"),
|
||||
}
|
||||
SOURCE: dict[str, str] = {
|
||||
"You do not have a ROM image installed for Model": "sdltrs message, emulators/sdl2trs.yml",
|
||||
"You might not get sound because": "lindbergh-loader message, emulators/lindbergh-loader.yml",
|
||||
'"Your Card"': "KEGS disk name, emulators/kegs.yml",
|
||||
"How do I use SC3000": "MG.html question title, emulators/mastergear.yml",
|
||||
"How do I use SF7000 disk images?": "MG.html question title, emulators/mastergear.yml",
|
||||
"There is no version of MG for my Unix!": "MG.html question title, emulators/mastergear.yml",
|
||||
"rename your own SFX dat file to this name": "Zelda Classic README, emulators/zc210.yml",
|
||||
"Could not load ROM images! You must supply these.":
|
||||
"hypseus message, emulators/hypseus-singe.yml",
|
||||
"Please copy pak0.pk3 from the 'etmain' path of your":
|
||||
"ET: Legacy message, emulators/etlegacy.yml",
|
||||
'"keyconf.dat(us)"': "file name px68k ships, emulators/px68k-sdl.yml",
|
||||
}
|
||||
|
||||
_SPACED = [
|
||||
re.compile(r"\s+".join(map(re.escape, phrase.split())))
|
||||
for phrase in (*INSTALLER, *SOURCE)
|
||||
]
|
||||
_NAMES = [re.compile(pattern) for pattern in NAMES]
|
||||
|
||||
|
||||
def _blank(match: re.Match[str]) -> str:
|
||||
return re.sub(r"[^\n]", " ", match[0])
|
||||
|
||||
|
||||
def prose(text: str) -> str:
|
||||
"""The text of a page with everything that is not prose blanked."""
|
||||
for pattern in (HTML_COMMENT, SCRIPT_STYLE, TAG):
|
||||
text = pattern.sub(_blank, text)
|
||||
# _ and ' spell a file name or a name: read them as characters.
|
||||
text = ENTITY.sub(lambda m: html.unescape(m[0]), text)
|
||||
for pattern in (
|
||||
*_SPACED, FENCED, INLINE_CODE, LINK_TARGET, URL, ANCHOR, PATHLIKE,
|
||||
TERMS, *_NAMES, DOTTED, LETTERS, VALUES, LETTER_HEADING, COUNTRY,
|
||||
):
|
||||
text = pattern.sub(_blank, text)
|
||||
return text
|
||||
|
||||
|
||||
def pronouns(text: str) -> list[tuple[int, str]]:
|
||||
"""(line number, line) for each prose line holding a personal pronoun."""
|
||||
lines = text.splitlines()
|
||||
return [
|
||||
(number, lines[number - 1].strip())
|
||||
for number, line in enumerate(prose(text).splitlines(), 1)
|
||||
if PRONOUN.search(line)
|
||||
]
|
||||
|
||||
|
||||
def _hits(paths: list[Path]) -> list[str]:
|
||||
return [
|
||||
f"{path.relative_to(ROOT)}:{number}: {line}"
|
||||
for path in paths
|
||||
for number, line in pronouns(path.read_text(encoding="utf-8"))
|
||||
]
|
||||
|
||||
|
||||
@cache
|
||||
def _generated() -> bool:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "generate_site.py")],
|
||||
cwd=ROOT, capture_output=True, text=True, check=False,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
class Scanner(unittest.TestCase):
|
||||
def test_code_urls_and_anchors_are_not_prose(self):
|
||||
text = (
|
||||
"Run `install.py --help` first.\n"
|
||||
"```\nyou are here\n```\n"
|
||||
"See https://example.org/your/page and [a link](our-page.md).\n"
|
||||
"## Get the files { #get-your-files }\n"
|
||||
'<a title="your title">Open the menu</a>\n'
|
||||
"A US BIOS, Quake II I mean.\n"
|
||||
)
|
||||
self.assertEqual(pronouns(text), [(8, "A US BIOS, Quake II I mean.")])
|
||||
|
||||
def test_each_form_is_found_in_any_case(self):
|
||||
for word in ("I", "me", "My", "mine", "we", "Us", "OUR", "you", "Yours",
|
||||
"yourselves", "you're", "I'm"):
|
||||
with self.subTest(word=word):
|
||||
self.assertTrue(pronouns(f"Then {word} copy it."))
|
||||
|
||||
def test_a_word_holding_the_letters_passes(self):
|
||||
self.assertEqual(pronouns("Usage of the menu, mine-craft, youth."), [])
|
||||
|
||||
|
||||
class SitePronouns(unittest.TestCase):
|
||||
def test_wiki_sources(self):
|
||||
hits = _hits(sorted((ROOT / "wiki").glob("*.md")))
|
||||
self.assertEqual(hits, [], "\n".join(hits))
|
||||
|
||||
def test_readme_and_contributing(self):
|
||||
hits = _hits([ROOT / "README.md", ROOT / "CONTRIBUTING.md"])
|
||||
self.assertEqual(hits, [], "\n".join(hits))
|
||||
|
||||
def test_generated_pages(self):
|
||||
if not _generated():
|
||||
self.fail("scripts/generate_site.py failed")
|
||||
pages = sorted(p for p in DOCS.rglob("*") if p.suffix in (".md", ".mdx"))
|
||||
self.assertTrue(pages)
|
||||
hits = _hits(pages)
|
||||
self.assertEqual(hits, [], "\n".join(hits[:200]))
|
||||
|
||||
def test_agent_files(self):
|
||||
if not _generated():
|
||||
self.fail("scripts/generate_site.py failed")
|
||||
public = ROOT / "website" / "public"
|
||||
files = [public / "llms.txt", *sorted((public / "skills").rglob("*.md"))]
|
||||
self.assertTrue(all(path.is_file() for path in files))
|
||||
hits = _hits(files)
|
||||
self.assertEqual(hits, [], "\n".join(hits))
|
||||
|
||||
def test_every_allowed_quote_says_why(self):
|
||||
for quoted, reason in {**NAMES, **INSTALLER, **SOURCE}.items():
|
||||
with self.subTest(quoted=quoted):
|
||||
self.assertTrue(reason.strip())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -534,7 +534,7 @@ class NoTooltipInsideAName(unittest.TestCase):
|
||||
import sitestarlight
|
||||
|
||||
terms = {"Emulator": "An app that acts like a console.",
|
||||
"Terminal": "A window where you type commands.",
|
||||
"Terminal": "A window for typing commands.",
|
||||
"BIOS": "The console's built-in software."}
|
||||
for line in ("Fuse (Free Unix Spectrum Emulator) reads it.",
|
||||
"Open Windows Terminal (WSL).",
|
||||
|
||||
@@ -228,7 +228,7 @@ class Descriptions(unittest.TestCase):
|
||||
self.addCleanup(lambda: (gs.PAGE_DATA.clear(), gs.PAGE_DATA.update(saved)))
|
||||
gs.PAGE_DATA["emulators/duckstation.md"] = {"hasPart": [{"name": "scph5501.bin"}]}
|
||||
body = ("# DuckStation\n\nDuckStation emulates Sony PlayStation. It picks the BIOS for "
|
||||
"your game's region.\n\n<details>\n\nThe installer finds your platform.\n")
|
||||
"the game's region.\n\n<details>\n\nThe installer finds the platform.\n")
|
||||
text = gs._page_description(body, Path("emulators/duckstation.md"), "DuckStation")
|
||||
self.assertTrue(text.startswith("DuckStation emulates Sony PlayStation."))
|
||||
self.assertNotIn("source-pinned", text)
|
||||
@@ -308,10 +308,10 @@ class Descriptions(unittest.TestCase):
|
||||
self.assertEqual(text, "Dreamcast BIOS files (Sega). Start with dc_boot.bin, dc_flash.bin.")
|
||||
|
||||
def test_the_faq_is_described_by_its_questions(self) -> None:
|
||||
body = ("# Common questions\n\n## Do I need BIOS files?\n\nYes.\n\n## Which one?\n\n"
|
||||
body = ("# Common questions\n\n## Are BIOS files needed?\n\nYes.\n\n## Which one?\n\n"
|
||||
"Run the command with --dry-run.\n\n## Zip or not?\n\nZip.\n")
|
||||
text = gs._page_description(body, Path("wiki/faq.md"), "Common questions")
|
||||
self.assertTrue(text.startswith("Common questions: Do I need BIOS files?"))
|
||||
self.assertTrue(text.startswith("Common questions: Are BIOS files needed?"))
|
||||
|
||||
def test_a_written_description_wins(self) -> None:
|
||||
source = (REPO_ROOT / "scripts" / "generate_site.py").read_text(encoding="utf-8")
|
||||
@@ -359,7 +359,7 @@ class GetPagesSitInYourSetup(unittest.TestCase):
|
||||
def test_every_get_page_joins_your_setup(self):
|
||||
side, get_pages = _sidebar_with_get_pages()
|
||||
self.assertEqual(side["options"]["exclude"], [])
|
||||
setup = set(side["options"]["topics"].get("your-setup", []))
|
||||
setup = set(side["options"]["topics"].get("setups", []))
|
||||
for rel in get_pages:
|
||||
with self.subTest(page=rel):
|
||||
self.assertIn("/" + sitestarlight._sidebar_link(rel).strip("/"), setup)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""The drawn steps of the install box (scripts/sitesteps.py).
|
||||
|
||||
Each drawing says in words what it shows, to a screen reader and to a
|
||||
reader whose images do not load: a title and a desc the SVG names. It tells
|
||||
the same steps as the text above it, the keys included, so the picture and
|
||||
the words never disagree. And it draws in the theme's colours.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
|
||||
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import sitesteps # noqa: E402
|
||||
from siteguides import TERMINALS # noqa: E402
|
||||
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
DIST = ROOT / "website" / "dist"
|
||||
|
||||
|
||||
class EachDrawingNamesItsSteps(unittest.TestCase):
|
||||
def test_title_and_desc_are_what_aria_points_at(self):
|
||||
for tab in sitesteps.STEPS:
|
||||
for layout in ("wide", "narrow"):
|
||||
root = ET.fromstring(sitesteps._svg(tab, layout))
|
||||
with self.subTest(tab=tab, layout=layout):
|
||||
self.assertEqual(root.get("role"), "img")
|
||||
title, desc = root.find(f"{SVG}title"), root.find(f"{SVG}desc")
|
||||
self.assertIsNotNone(title)
|
||||
self.assertIsNotNone(desc)
|
||||
self.assertEqual(root.get("aria-labelledby").split(),
|
||||
[title.get("id"), desc.get("id")])
|
||||
self.assertEqual(desc.text, sitesteps.description(tab))
|
||||
|
||||
def test_every_caption_drawn_is_in_the_desc(self):
|
||||
for tab, (_title, steps) in sitesteps.STEPS.items():
|
||||
desc = sitesteps.description(tab)
|
||||
for step in steps:
|
||||
with self.subTest(tab=tab, step=step.caption):
|
||||
self.assertIn(step.caption, desc)
|
||||
|
||||
def test_the_drawing_and_the_text_name_the_same_keys(self):
|
||||
for tab, (_title, steps) in sitesteps.STEPS.items():
|
||||
text = TERMINALS[tab][1].replace("**", "")
|
||||
keys = {step.word for step in steps if step.icon == "keys"}
|
||||
with self.subTest(tab=tab):
|
||||
for word in keys:
|
||||
self.assertIn(word, text)
|
||||
drawn = set(re.findall(r"\b(?:Ctrl|Cmd|Shift|Alt)\+\w+(?:\+\w+)?", text))
|
||||
self.assertLessEqual(drawn, keys)
|
||||
self.assertIn("Click Copy", text)
|
||||
|
||||
def test_one_drawing_per_computer_tab_none_elsewhere(self):
|
||||
self.assertEqual(set(sitesteps.STEPS), {"windows", "macos", "linux", "steamdeck"})
|
||||
self.assertEqual(sitesteps.steps_figure("device"), [])
|
||||
|
||||
def test_colours_come_from_the_theme(self):
|
||||
for tab in sitesteps.STEPS:
|
||||
root = ET.fromstring(sitesteps._svg(tab, "wide"))
|
||||
for node in root.iter():
|
||||
tag = node.tag.removeprefix(SVG)
|
||||
if tag in ("rect", "path", "circle"):
|
||||
with self.subTest(tab=tab, node=ET.tostring(node)[:60]):
|
||||
self.assertTrue(node.get("class", "").startswith("rb-d-"))
|
||||
|
||||
def test_ids_are_unique_on_the_page(self):
|
||||
ids = re.findall(r'id="([^"]+)"', "".join(
|
||||
sitesteps._svg(tab, layout) for tab in sitesteps.STEPS for layout in ("wide", "narrow")))
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
|
||||
|
||||
class TheBuiltPageHoldsThem(unittest.TestCase):
|
||||
def test_under_each_tab_of_get_your_files(self):
|
||||
page = DIST / "which-pack" / "index.html"
|
||||
if not page.is_file():
|
||||
self.skipTest("no built site")
|
||||
html = page.read_text(encoding="utf-8")
|
||||
for tab in sitesteps.STEPS:
|
||||
with self.subTest(tab=tab):
|
||||
self.assertIn(f'aria-labelledby="rb-steps-{tab}-wide-t rb-steps-{tab}-wide-d"',
|
||||
html)
|
||||
self.assertIn(f'<desc id="rb-steps-{tab}-narrow-d">', html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -129,7 +129,7 @@ class TheConsoleBiosComesFirst(unittest.TestCase):
|
||||
|
||||
def test_the_section_opens_on_the_bios_by_region(self):
|
||||
lines = gs._start_with(self.files, self.required_only())
|
||||
self.assertEqual(lines[0], "**Start with** the BIOS for the region of your games:")
|
||||
self.assertEqual(lines[0], "**Start with** the BIOS for the region of the games:")
|
||||
self.assertIn("- **North America**: [`scph5501.bin`](#sha1-444444444444)", lines)
|
||||
self.assertIn("- **Japan**: [`scph5500.bin`](#sha1-555555555555)", lines)
|
||||
self.assertLess(lines.index("- **North America**: [`scph5501.bin`](#sha1-444444444444)"),
|
||||
|
||||
+15
-15
@@ -435,7 +435,7 @@ class TheFaqAnswersWithACommand(unittest.TestCase):
|
||||
is the lever the installer itself names."""
|
||||
|
||||
def test_every_file_of_a_console(self):
|
||||
section = _section(_wiki("faq.md"), "Do I need every file of a console?")
|
||||
section = _section(_wiki("faq.md"), "Is every file of a console needed?")
|
||||
self.assertIn("<!-- os-tabs -->", section)
|
||||
self.assertIn("--system sony-playstation --core pcsx1", section)
|
||||
self.assertNotIn("only your region", section)
|
||||
@@ -450,7 +450,7 @@ class LegalOpensInPlainWords(unittest.TestCase):
|
||||
|
||||
def test_the_answer_comes_before_the_reasoning(self):
|
||||
self.assertIn("## The reasoning in detail", self.text)
|
||||
self.assertIn("It depends on your country. Nothing here is legal advice.", self.opening)
|
||||
self.assertIn("It depends on the country. Nothing here is legal advice.", self.opening)
|
||||
for case in ("Connectix", "Accolade", "DMCA", "CFR", "Chamberlain", "Yuzu"):
|
||||
with self.subTest(case=case):
|
||||
self.assertNotIn(case, self.opening)
|
||||
@@ -500,7 +500,7 @@ class ArcadeAnswerWherePlayersLook(unittest.TestCase):
|
||||
self.assertIn(words, section)
|
||||
|
||||
def test_the_faq_links_the_technical_answer(self):
|
||||
section = _section(_wiki("faq.md"), "My arcade game fails (which MAME version?)")
|
||||
section = _section(_wiki("faq.md"), "An arcade game fails (which MAME version?)")
|
||||
anchor = "technical-faq.md#which-mame-version-do-the-arcade-bios-sets-match"
|
||||
self.assertIn(anchor, section)
|
||||
self.assertIn("## Which MAME version do the arcade BIOS sets match?", _wiki("technical-faq.md"))
|
||||
@@ -513,7 +513,7 @@ class ArcadeAnswerWherePlayersLook(unittest.TestCase):
|
||||
self.assertTrue(profile.get("core_version") or profile.get("version"))
|
||||
|
||||
def test_the_check_step_holds_the_searched_words(self):
|
||||
self.assertIn("## 1. Check my BIOS folder { #checking-what-you-installed }",
|
||||
self.assertIn("## 1. Check the BIOS folder { #checking-what-you-installed }",
|
||||
_wiki("troubleshooting.md"))
|
||||
|
||||
|
||||
@@ -583,7 +583,7 @@ class SetupsWithoutAPage(unittest.TestCase):
|
||||
"""N9, J13: ES-DE, LaunchBox and iOS found nothing."""
|
||||
|
||||
def test_the_answer(self):
|
||||
section = _flat(_section(_wiki("faq.md"), "My setup is not in the list"))
|
||||
section = _flat(_section(_wiki("faq.md"), "A setup is not in the list"))
|
||||
for words in ("--dest", "ES-DE", "LaunchBox", "iPhone"):
|
||||
with self.subTest(words=words):
|
||||
self.assertIn(words, section)
|
||||
@@ -604,7 +604,7 @@ class LinkLabelsMatchPages(unittest.TestCase):
|
||||
for name in ("first-install.md", "troubleshooting.md", "faq.md"):
|
||||
with self.subTest(page=name):
|
||||
for label in re.findall(r"\[([^\]]+)\]\(\.\./which-pack\.md\)", _wiki(name)):
|
||||
self.assertEqual(label, "Get your files")
|
||||
self.assertEqual(label, "Get the files")
|
||||
|
||||
|
||||
class ExitStatusAndPinnedInstaller(unittest.TestCase):
|
||||
@@ -712,7 +712,7 @@ class TroubleshootingSaysTheWordsOnScreen(unittest.TestCase):
|
||||
self.text = _wiki("troubleshooting.md")
|
||||
|
||||
def test_step_one_opens_on_the_messages(self):
|
||||
step = _flat(_section(self.text, "1. Check my BIOS folder"))
|
||||
step = _flat(_section(self.text, "1. Check the BIOS folder"))
|
||||
self.assertIn("**BIOS not found**, **BIOS missing** or **firmware missing**?", step)
|
||||
self.assertLess(step.index("BIOS not found"), step.index("```"))
|
||||
|
||||
@@ -792,7 +792,7 @@ class ArcadeVersionTable(unittest.TestCase):
|
||||
|
||||
def test_the_section_opens_on_the_answer(self):
|
||||
self.assertTrue(self.section.lstrip().startswith(
|
||||
"Your game sets and BIOS sets must come from the same version as your arcade\nemulator."))
|
||||
"Game sets and BIOS sets must come from the same version as the arcade\nemulator."))
|
||||
self.assertIn("| Emulator, as RetroArch names it | Sets from |", self.section)
|
||||
self.assertLess(self.section.index("| Emulator"), self.section.index("### How this is known"))
|
||||
|
||||
@@ -818,7 +818,7 @@ class ArcadeVersionTable(unittest.TestCase):
|
||||
self.assertIn(ARCADE_ANCHOR, arcade)
|
||||
self.assertNotIn("emulators/index.md", arcade)
|
||||
faq = _wiki("faq.md")
|
||||
self.assertIn("## My arcade game fails (which MAME version?) "
|
||||
self.assertIn("## An arcade game fails (which MAME version?) "
|
||||
"{ #my-arcade-game-does-not-start }", faq)
|
||||
|
||||
|
||||
@@ -830,7 +830,7 @@ class FaqLabels(unittest.TestCase):
|
||||
|
||||
def test_no_reference_label(self):
|
||||
self.assertNotRegex(self.text, r"(?m)^Reference:")
|
||||
arcade = _section(self.text, "My arcade game fails (which MAME version?)")
|
||||
arcade = _section(self.text, "An arcade game fails (which MAME version?)")
|
||||
self.assertIn(f"Game sets must match the emulator's version:\n[the list]({ARCADE_ANCHOR}).",
|
||||
arcade)
|
||||
|
||||
@@ -845,7 +845,7 @@ class CopyYourOwnBios(unittest.TestCase):
|
||||
"""C-other, need 12: no page answered the legal route."""
|
||||
|
||||
def test_each_console_has_a_guide(self):
|
||||
section = _section(_wiki("faq.md"), "Can I copy my own console's BIOS?")
|
||||
section = _section(_wiki("faq.md"), "Can an owned console's BIOS be copied?")
|
||||
links = re.findall(r"\[([^\]]+)\]\((https://[^)]+)\)", section)
|
||||
self.assertEqual(dict(links), {
|
||||
"PCSX2": "https://pcsx2.net/docs/setup/bios/",
|
||||
@@ -1093,15 +1093,15 @@ class FaqAnswersLeadSomewhere(unittest.TestCase):
|
||||
self.generator = (REPO_ROOT / "scripts" / "generate_site.py").read_text(encoding="utf-8")
|
||||
|
||||
def test_ios_goes_to_the_iphone_section(self):
|
||||
section = _flat(_section(self.text, "My setup is not in the list"))
|
||||
self.assertIn("iPhone, iPad (iOS): no terminal. [Pick your consoles and copy them "
|
||||
section = _flat(_section(self.text, "A setup is not in the list"))
|
||||
self.assertIn("iPhone, iPad (iOS): no terminal. [Pick the consoles and copy them "
|
||||
"to the app](../which-pack.md#iphone-and-ipad).", section)
|
||||
self.assertNotIn("Use the ZIP", section)
|
||||
self.assertIn('"## iPhone and iPad { #iphone-and-ipad }"', self.generator)
|
||||
|
||||
def test_antivirus_answer_covers_setups_without_a_zip(self):
|
||||
section = _flat(_section(self.text, "Windows or my antivirus blocks the command"))
|
||||
self.assertIn("Use your setup's ZIP. None? [Pick your consoles]"
|
||||
section = _flat(_section(self.text, "Windows or an antivirus blocks the command"))
|
||||
self.assertIn("Use the setup's ZIP. None? [Pick the consoles]"
|
||||
"(../which-pack.md#only-some-consoles) and download their files.",
|
||||
section)
|
||||
self.assertNotIn("if any", section)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
// "Which files do I need": pick a setup and systems, get the file list and
|
||||
// "Which files are needed": pick a setup and systems, get the file list and
|
||||
// the install command. Client side only: reads public/data/picker/*.json,
|
||||
// which convert.py derives from install/<platform>.json. The selection
|
||||
// follows install.py narrow(): a file is kept when one of its systems is
|
||||
@@ -22,7 +22,7 @@ const packs = Object.fromEntries(
|
||||
|
||||
<div class="rb-picker" data-base={base} data-release={JSON.stringify({ tag: release.tag ?? '', packs })}>
|
||||
<fieldset>
|
||||
<label for="rb-pick-platform">Your setup</label>
|
||||
<label for="rb-pick-platform">Setup</label>
|
||||
<select id="rb-pick-platform" name="platform">
|
||||
<option value="">Choose a setup</option>
|
||||
</select>
|
||||
|
||||
@@ -4,18 +4,25 @@
|
||||
// poster and the sources are set by the script below once the video is in
|
||||
// view (preload="none", no src in the HTML). With reduced motion or Save-Data
|
||||
// the poster stays, and the button plays it. The button pauses it otherwise.
|
||||
// variant names the recording (default, handheld), name the tab a demo sits
|
||||
// in when a page holds several: a demo in a closed tab is not on screen, so
|
||||
// it loads nothing until its tab is chosen.
|
||||
interface Props {
|
||||
description: string;
|
||||
width: string;
|
||||
height: string;
|
||||
variant?: 'default' | 'handheld';
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const { description, width, height } = Astro.props;
|
||||
const { description, width, height, variant = 'default', name = '' } = Astro.props;
|
||||
const base = import.meta.env.BASE_URL.replace(/\/?$/, '/');
|
||||
const asset = (name: string) => `${base}assets/demo/${name}`;
|
||||
const stem = variant === 'default' ? 'install' : `install-${variant}`;
|
||||
const asset = (ext: string) => `${base}assets/demo/${stem}.${ext}`;
|
||||
const textId = name ? `rb-demo-text-${name}` : 'rb-demo-text';
|
||||
---
|
||||
|
||||
<figure class="rb-demo" data-demo data-pagefind-ignore>
|
||||
<figure class="rb-demo" data-demo data-variant={variant} data-pagefind-ignore>
|
||||
<video
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -24,14 +31,14 @@ const asset = (name: string) => `${base}assets/demo/${name}`;
|
||||
playsinline
|
||||
preload="none"
|
||||
disablepictureinpicture
|
||||
data-poster={asset('install.png')}
|
||||
aria-describedby="rb-demo-text"
|
||||
data-poster={asset('png')}
|
||||
aria-describedby={textId}
|
||||
>
|
||||
<source data-src={asset('install.webm')} type="video/webm" />
|
||||
<source data-src={asset('install.mp4')} type="video/mp4" />
|
||||
<source data-src={asset('webm')} type="video/webm" />
|
||||
<source data-src={asset('mp4')} type="video/mp4" />
|
||||
</video>
|
||||
<button type="button" class="rb-demo-toggle" hidden>Play</button>
|
||||
<figcaption id="rb-demo-text" class="sr-only">{description}</figcaption>
|
||||
<figcaption id={textId} class="sr-only">{description}</figcaption>
|
||||
</figure>
|
||||
|
||||
<script>
|
||||
@@ -91,6 +98,10 @@ const asset = (name: string) => `${base}assets/demo/${name}`;
|
||||
border: 1px solid var(--rb-border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
/* A handheld's 4:3 screen, no taller than the terminal recording. */
|
||||
.rb-demo[data-variant='handheld'] {
|
||||
max-inline-size: 32rem;
|
||||
}
|
||||
.rb-demo-toggle {
|
||||
position: absolute;
|
||||
inset-inline-end: 0.5rem;
|
||||
|
||||
@@ -31,7 +31,7 @@ const LABELS = {
|
||||
// The tab each level is read in, as the header names it.
|
||||
const TABS = {
|
||||
start: 'Start here',
|
||||
setup: 'Your setup',
|
||||
setup: 'Setups',
|
||||
explore: 'Explore',
|
||||
reference: 'Reference',
|
||||
contribute: 'Contribute',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// page or the section that introduces it, never to a sibling page. A
|
||||
// group of names by their first letter (A, B) is no crumb;
|
||||
// - previous and next: the reading order of `reading`. Start here runs on
|
||||
// into Your setup, every other tab is read on its own, and a system,
|
||||
// into Setups, every other tab is read on its own, and a system,
|
||||
// emulator or platform page has none;
|
||||
// - a pruned sidebar: in the tabs listing every system and emulator, a
|
||||
// group that does not hold the page becomes one link to its overview, so
|
||||
|
||||
@@ -630,7 +630,7 @@ const renderMatches = async (status: HTMLElement, result: HTMLElement, matched:
|
||||
list.append(row);
|
||||
}
|
||||
blocks.push(
|
||||
paragraph(strong('Wanted.'), ' The collection does not hold this dump yet. If you dumped it yourself, ', link('contributing/', 'add it'), '.'),
|
||||
paragraph(strong('Wanted.'), ' The collection does not hold this dump yet. Anyone who dumped it can ', link('contributing/', 'add it'), '.'),
|
||||
list,
|
||||
);
|
||||
}
|
||||
@@ -692,7 +692,7 @@ const WANTED = 'provenance/#wanted-catalogued-dumps-this-project-does-not-have';
|
||||
const renderNoMatch = (status: HTMLElement, result: HTMLElement) => {
|
||||
status.textContent = 'No file, setup or emulator the site knows has this hash.';
|
||||
result.replaceChildren(
|
||||
paragraph('If you dumped it yourself, ', link('contributing/', 'add it'), '.'),
|
||||
paragraph('Anyone who dumped it can ', link('contributing/', 'add it'), '.'),
|
||||
paragraph('The ', link(WANTED, 'wanted list'), ' names the catalogued dumps the collection still lacks.'),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -179,7 +179,7 @@ platforms:
|
||||
```
|
||||
|
||||
`device_group` places the platform among the devices a reader picks from: on
|
||||
the site's "Get your files" page, in the "Your setup" navigation and on the home
|
||||
the site's "Get the files" page, in the "Setups" navigation and on the home
|
||||
page. The groups, their labels and their anchors are under `device_groups` in
|
||||
`platforms/_guides.yml`, beside the platform's [guide text](#guide-text).
|
||||
|
||||
@@ -520,7 +520,7 @@ The YAML is the scraper's output and is never edited by hand: the next scrape
|
||||
would undo the edit, and the file would stop describing the platform. Anything
|
||||
an export has to write back (the platform's own system id, path, name) is
|
||||
recorded by the scraper and never reconstructed later, because several of the
|
||||
platform's systems can fold into one of ours. A rescrape can add systems, so
|
||||
platform's systems can fold into one of the collection's. A rescrape can add systems, so
|
||||
read `verify.py --platform` before committing the regenerated YAML.
|
||||
|
||||
Verify the result:
|
||||
@@ -602,7 +602,7 @@ of files:
|
||||
- `can_add()` - `False` when an entry is code rather than a row of data
|
||||
(BizHawk), so nothing is added.
|
||||
- `keys_by_system()` - `True` when the platform finds a file by the name of its
|
||||
own system, so an entry filed under one of our system ids is never written.
|
||||
own system, so an entry filed under one of the collection's system ids is never written.
|
||||
- `needs_original()` - `True` as soon as `native_sources()` names a file.
|
||||
|
||||
Those methods say what the format could hold. The report counts what `render()`
|
||||
@@ -631,7 +631,7 @@ will not find in the diff.
|
||||
|
||||
The scraper and the exporter are checked against each other as
|
||||
[round-trip testing](adding-a-scraper.md#round-trip-testing) describes. The
|
||||
export must not lose anything the platform already declares. If your
|
||||
export must not lose anything the platform already declares. If the
|
||||
format carries code as well as data (a script, a C# database, a manifest
|
||||
whose BIOS list sits beside launch configuration), name the file in
|
||||
`native_sources()` and patch it rather than writing a new one. An exporter
|
||||
|
||||
@@ -456,7 +456,7 @@ manipulation rather than load-dump to maintain human-readable formatting.
|
||||
|
||||
The scrape and the export are two directions of the same transcription, so
|
||||
run them against each other: scrape upstream, export it back, and diff the
|
||||
result against the file you started from. Nothing the platform declares may
|
||||
result against the original file. Nothing the platform declares may
|
||||
be missing from the round trip.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,7 +3,7 @@ level: contribute
|
||||
order: 80
|
||||
---
|
||||
|
||||
# Build your own packs
|
||||
# Build custom packs
|
||||
|
||||
Fine-grained control over pack generation, hardware filtering, truth analysis (the file
|
||||
list a platform would have if it were written from the emulators' code) and
|
||||
@@ -23,7 +23,7 @@ A clone carries the whole collection, several gigabytes, and `git clone
|
||||
--depth 1` leaves its history out. From that clone, `python scripts/generate_pack.py --platform
|
||||
retroarch --region us` builds a narrower pack, `--required-only` the bare
|
||||
minimum each core needs to start, and `--no-game-data` keeps the firmware and
|
||||
leaves out the files a game reads. `--help` lists every way to build your own,
|
||||
leaves out the files a game reads. `--help` lists every way to build one,
|
||||
and the sections below describe each one.
|
||||
|
||||
## Custom packs
|
||||
@@ -47,8 +47,8 @@ carries one hash (MD5, SHA-1 or CRC32), alone or beside a file name as
|
||||
python scripts/generate_pack.py --platform batocera --from-md5-file missing.txt
|
||||
```
|
||||
|
||||
This is useful when a platform reports missing files and you want to generate a targeted pack
|
||||
rather than re-downloading the full archive.
|
||||
This builds a targeted pack when a platform reports missing files, rather than
|
||||
downloading the full archive again.
|
||||
|
||||
### Split packs
|
||||
|
||||
@@ -79,7 +79,7 @@ python scripts/verify.py --platform batocera --system psx
|
||||
```
|
||||
|
||||
Beside `--platform`, `--system` accepts the platform's own name for a system
|
||||
as well as our id: `psx` is what Batocera calls `sony-playstation`, and both
|
||||
as well as the collection's id: `psx` is what Batocera calls `sony-playstation`, and both
|
||||
build the same pack. The names come from `native_system` in the platform
|
||||
YAML, the mapping the installer reads as `system_names`, and
|
||||
`--platform batocera --list-systems` shows them after each id. A comma-separated
|
||||
@@ -527,7 +527,7 @@ README, site generation) run normally using cached data.
|
||||
|
||||
### Partial runs
|
||||
|
||||
Skip pack generation when you only need verification results:
|
||||
Skip pack generation when only verification results are needed:
|
||||
|
||||
```bash
|
||||
python scripts/pipeline.py --offline --skip-packs
|
||||
|
||||
@@ -5,7 +5,7 @@ order: 60
|
||||
|
||||
# Architecture
|
||||
|
||||
How a BIOS list on a platform's site becomes a pack on your drive. Scrapers
|
||||
How a BIOS list on a platform's site becomes a pack on a drive. Scrapers
|
||||
copy each platform's own list. Emulator profiles record what each emulator's
|
||||
code opens. `database.json` indexes every file the collection holds by its
|
||||
contents. The pack builder joins the three and ships every file a platform's
|
||||
@@ -147,7 +147,7 @@ Every part of the toolchain follows from a few rules.
|
||||
folders, file names or database, nor to one platform's conventions. The
|
||||
toolchain resolves a profile against the collection.
|
||||
- **Other projects are peers.** A finding about another project says what
|
||||
changes, never who erred, and comes with its evidence. Our data can be
|
||||
changes, never who erred, and comes with its evidence. The data here can be
|
||||
wrong too, and an issue that shows it is as welcome as a finding sent
|
||||
upstream.
|
||||
- **An inconsistency found is fixed now.** A fix comes with a test that fails
|
||||
|
||||
@@ -16,13 +16,13 @@ Projects built on RetroBIOS data.
|
||||
- **Repository:** [github.com/monster-penguin/BIOS-Preservation-Tool](https://github.com/monster-penguin/BIOS-Preservation-Tool)
|
||||
- **License:** MIT
|
||||
|
||||
A local tool for managing your own BIOS collection. Point it at your files, it verifies them against RetroBIOS platform YAMLs, stores them in a deduplicated database, and produces ready-to-use directories for each platform. Supports scanning directories, archives, and URLs.
|
||||
A local tool for managing a personal BIOS collection. Point it at the files, it verifies them against RetroBIOS platform YAMLs, stores them in a deduplicated database, and produces ready-to-use directories for each platform. Supports scanning directories, archives, and URLs.
|
||||
|
||||
See the project's [README](https://github.com/monster-penguin/BIOS-Preservation-Tool#readme) for setup and usage.
|
||||
|
||||
---
|
||||
|
||||
:::note[Add your project]
|
||||
Open an issue with a link to your repository.
|
||||
:::note[Add a project]
|
||||
Open an issue with a link to the repository.
|
||||
:::
|
||||
|
||||
+17
-17
@@ -6,7 +6,7 @@ terms: ios, iphone, ipad
|
||||
|
||||
# Common questions
|
||||
|
||||
## Do I need BIOS files?
|
||||
## Are BIOS files needed? { #do-i-need-bios-files }
|
||||
|
||||
- **Yes** for PlayStation 1 and 2, Saturn and Neo Geo.
|
||||
- **Better with it** for Dreamcast. Some games need it.
|
||||
@@ -14,7 +14,7 @@ terms: ios, iphone, ipad
|
||||
|
||||
[What is a BIOS?](what-are-bios-files.md)
|
||||
|
||||
## How do I know which BIOS I need?
|
||||
## How to tell which BIOS is needed? { #how-do-i-know-which-bios-i-need }
|
||||
|
||||
`--dry-run` lists them and changes nothing.
|
||||
|
||||
@@ -23,7 +23,7 @@ terms: ios, iphone, ipad
|
||||
curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
## Do I need every file of a console?
|
||||
## Is every file of a console needed? { #do-i-need-every-file-of-a-console }
|
||||
|
||||
No. `--core pcsx1` keeps what PCSX reads:
|
||||
|
||||
@@ -39,14 +39,14 @@ option.
|
||||
|
||||
## Zip or unzip? { #zip-or-unzip }
|
||||
|
||||
Unzip into the folder your setup's page names. Keep
|
||||
Unzip into the folder the setup's page names. Keep
|
||||
inner ZIPs zipped, like `neogeo.zip`.
|
||||
|
||||
## Does a BIOS work on every setup?
|
||||
|
||||
Usually. The command installs each setup's own, like `neogeo.zip`.
|
||||
|
||||
## My arcade game fails (which MAME version?) { #my-arcade-game-does-not-start }
|
||||
## An arcade game fails (which MAME version?) { #my-arcade-game-does-not-start }
|
||||
|
||||
Game sets must match the emulator's version:
|
||||
[the list](technical-faq.md#which-mame-version-do-the-arcade-bios-sets-match).
|
||||
@@ -70,12 +70,12 @@ Read them: [install.sh](https://github.com/Abdess/retrobios/blob/main/install.sh
|
||||
[install.ps1](https://github.com/Abdess/retrobios/blob/main/install.ps1),
|
||||
[install.py](https://github.com/Abdess/retrobios/blob/main/install.py).
|
||||
|
||||
## Windows or my antivirus blocks the command
|
||||
## Windows or an antivirus blocks the command { #windows-or-my-antivirus-blocks-the-command }
|
||||
|
||||
Use your setup's ZIP. None? [Pick your consoles](../which-pack.md#only-some-consoles)
|
||||
Use the setup's ZIP. None? [Pick the consoles](../which-pack.md#only-some-consoles)
|
||||
and download their files.
|
||||
|
||||
## Why is there no ZIP for my setup?
|
||||
## 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
|
||||
setup's files and checks each one. `--dry-run` shows the list first.
|
||||
@@ -83,7 +83,7 @@ setup's 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 your setup and its BIOS folder. For RetroArch on a Switch,
|
||||
MiSTer card. Else name the setup and its BIOS folder. For RetroArch on a Switch,
|
||||
with the card in drive `E:`, run:
|
||||
|
||||
<!-- os-tabs windows: --platform retroarch --dest E:\retroarch\cores\system -->
|
||||
@@ -94,38 +94,38 @@ curl -fsSL https://abdess.github.io/retrobios/i \
|
||||
|
||||
## No internet on the device?
|
||||
|
||||
Fill it from a computer or point `--from-pack` at your ZIP: [Without a network](installer.md#without-a-network-offline-install).
|
||||
Fill it from a computer or point `--from-pack` at the ZIP: [Without a network](installer.md#without-a-network-offline-install).
|
||||
|
||||
## Lost or wiped card?
|
||||
|
||||
`--from` copies BIOS files from an old card or backup:
|
||||
[Copy from an old card](installer.md#copy-from-an-old-card).
|
||||
|
||||
## My setup is not in the list
|
||||
## A setup is not in the list { #my-setup-is-not-in-the-list }
|
||||
|
||||
- Name your emulator's BIOS folder with `--dest`.
|
||||
- Name the emulator's BIOS folder with `--dest`.
|
||||
- ES-DE or LaunchBox? It fills their emulators' folders.
|
||||
- iPhone, iPad (iOS): no terminal.
|
||||
[Pick your consoles and copy them to the app](../which-pack.md#iphone-and-ipad).
|
||||
[Pick the consoles and copy them to the app](../which-pack.md#iphone-and-ipad).
|
||||
|
||||
## Why is a file missing from the ZIP?
|
||||
|
||||
Some come with the emulator.
|
||||
|
||||
## My game shows a black screen
|
||||
## A game shows a black screen { #my-game-shows-a-black-screen }
|
||||
|
||||
Often a missing BIOS: [When a game does not start](troubleshooting.md).
|
||||
|
||||
## Can I copy my own console's BIOS? { #copy-your-own-bios }
|
||||
## Can an owned console's BIOS be copied? { #copy-your-own-bios }
|
||||
|
||||
Yes: [PCSX2](https://pcsx2.net/docs/setup/bios/) for PlayStation 2,
|
||||
[melonDS](https://melonds.kuribo64.net/faq.php) for DS and DSi.
|
||||
|
||||
## Is this legal?
|
||||
|
||||
[It depends on your country](legal.md).
|
||||
[It depends on the country](legal.md).
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Get your files](../which-pack.md)
|
||||
- [Get the files](../which-pack.md)
|
||||
- [Words used on this site](glossary.md)
|
||||
@@ -7,11 +7,11 @@ order: 30
|
||||
|
||||
From an empty BIOS folder to games that start.
|
||||
|
||||
**You need:** internet and some free space.
|
||||
**Needed:** internet and some free space.
|
||||
|
||||
## 1. Open a terminal and paste
|
||||
|
||||
A **terminal** is a window where you type commands. Paste your system's
|
||||
A **terminal** is a window for typing commands. Paste the matching
|
||||
line, then press Enter.
|
||||
|
||||
<Tabs syncKey="os">
|
||||
@@ -64,7 +64,7 @@ curl -fsSL https://abdess.github.io/retrobios/i | sh
|
||||
</Tabs>
|
||||
|
||||
On Batocera, Recalbox and similar setups, see
|
||||
[Get your files](../which-pack.md).
|
||||
[Get the files](../which-pack.md).
|
||||
|
||||
## 2. Check the setup it found
|
||||
|
||||
@@ -88,8 +88,8 @@ touch screen, tap a row twice to open it, once to tick a box.
|
||||
|
||||
## 3. Install
|
||||
|
||||
Press Enter on **Install**. **Choose systems** keeps the consoles you
|
||||
play.
|
||||
Press Enter on **Install**. **Choose systems** picks the consoles to
|
||||
keep.
|
||||
|
||||
**Game data** (only a game reads it) has its own row.
|
||||
|
||||
@@ -130,14 +130,14 @@ All good:
|
||||
|
||||
## Undo
|
||||
|
||||
Replaced files go to `retrobios-backup` beside your BIOS folder. The result
|
||||
Replaced files go to `retrobios-backup` beside the BIOS folder. The result
|
||||
says how to restore them.
|
||||
|
||||
## If it stops
|
||||
|
||||
### Nothing was found here.
|
||||
|
||||
Choose your setup, then its folder. Where the screen offers
|
||||
Choose the setup, then its folder. Where the screen offers
|
||||
**Another setup or folder**, choose it first.
|
||||
|
||||
### Error: Python 3.8 or newer is required.
|
||||
@@ -162,7 +162,7 @@ Nothing ran, because something changed the file. Try another network.
|
||||
|
||||
### Cannot create ...
|
||||
|
||||
You cannot write there. Do what the message says.
|
||||
That folder cannot be written. Do what the message says.
|
||||
|
||||
### The card is full
|
||||
|
||||
@@ -174,6 +174,6 @@ See [When a game does not start](troubleshooting.md).
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Get your files](../which-pack.md)
|
||||
- [Get the files](../which-pack.md)
|
||||
- [When a game does not start](troubleshooting.md)
|
||||
- Details: [Reading the installer's output, line by line](installer.md#reading-a-run)
|
||||
+7
-7
@@ -16,10 +16,10 @@ order: 60
|
||||
console.
|
||||
|
||||
**Setup**
|
||||
: What you play with: RetroArch, Batocera, EmuDeck and others.
|
||||
: What plays the games: RetroArch, Batocera, EmuDeck and others.
|
||||
|
||||
**BIOS folder**
|
||||
: The folder where your setup looks for BIOS files.
|
||||
: The folder where the setup looks for BIOS files.
|
||||
|
||||
**Pack**
|
||||
: The ZIP, sometimes in parts, with one setup's BIOS files. Made for the
|
||||
@@ -29,13 +29,13 @@ order: 60
|
||||
: A setup, as the project's data calls it.
|
||||
|
||||
**ZIP**
|
||||
: One file that holds many files. You unzip it to get them out.
|
||||
: One file that holds many files. Unzip it to get them out.
|
||||
|
||||
**Terminal**
|
||||
: A window where you type commands.
|
||||
: A window for typing commands.
|
||||
|
||||
**Installer**
|
||||
: The small program the command starts. It fills your BIOS folder.
|
||||
: The small program the command starts. It fills the BIOS folder.
|
||||
|
||||
**Hash**
|
||||
: A fingerprint of a file's contents. A different file gets a different hash.
|
||||
@@ -50,11 +50,11 @@ order: 60
|
||||
: Required: games do not start without it. Optional: games start anyway.
|
||||
|
||||
**Game data**
|
||||
: Files from your own copy of a PC game, like Doom. Not a BIOS.
|
||||
: Files from an owned copy of a PC game, like Doom. Not a BIOS.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [What is a BIOS?](what-are-bios-files.md)
|
||||
- [Get your files](../which-pack.md)
|
||||
- [Get the files](../which-pack.md)
|
||||
- For contributors: [Terms of the reference pages](reference-terms.md)
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ order: 0
|
||||
|
||||
These pages describe how the files are checked, what the data holds and how the
|
||||
toolchain works. To set up BIOS files on a device, start with
|
||||
[Get your files](../which-pack.md).
|
||||
[Get the files](../which-pack.md).
|
||||
|
||||
## Checking files
|
||||
|
||||
@@ -51,7 +51,7 @@ at run time, so the source is the primary reference:
|
||||
- **[Release process](release-process.md)**: CI workflows, large files, manual release
|
||||
- **[Architecture](architecture.md)**: directory structure, data flow, platform inheritance, pack grouping, security, edge cases, CI workflows
|
||||
- **[Command-line tools](tools.md)**: every script, pipeline usage, scrapers
|
||||
- **[Build your own packs](advanced-usage.md)**: custom packs, region filtering, one file per slot, target filtering, truth generation, emulator verification, offline workflow
|
||||
- **[Build custom packs](advanced-usage.md)**: custom packs, region filtering, one file per slot, target filtering, truth generation, emulator verification, offline workflow
|
||||
|
||||
See [contributing](../contributing.md) for submission guidelines.
|
||||
|
||||
|
||||
+14
-14
@@ -6,20 +6,20 @@ terms: manifest, events, exit code, schema, packager, pin, reproducible
|
||||
|
||||
# All installer options
|
||||
|
||||
The one-line command finds your setup and its BIOS folder. It downloads only
|
||||
The one-line command finds the setup and its BIOS folder. It downloads only
|
||||
the files that are missing or wrong, and checks each one against its
|
||||
fingerprint before writing it. The options below are for anything else: fewer
|
||||
files (one console, one region, only the cores you have), another folder, an
|
||||
files (one console, one region, only the cores installed), another folder, an
|
||||
SD card, a check without download, or a run from a script.
|
||||
|
||||
Example: `--platform retroarch --system sony-playstation --region north-america`
|
||||
installs only the PlayStation files, and the North American version where
|
||||
there is one per region.
|
||||
|
||||
How the command protects your machine is under
|
||||
How the command protects the machine is under
|
||||
[What the bootstrap does](#what-the-bootstrap-does) and
|
||||
[Trust boundary](#trust-boundary). The BIOS folder of each setup is on
|
||||
[Get your files](../which-pack.md).
|
||||
[Get the files](../which-pack.md).
|
||||
|
||||
## Options at a glance
|
||||
|
||||
@@ -169,13 +169,13 @@ Enter checks the BIOS folder when nothing can be copied.
|
||||
## The full-screen interface { #the-full-screen-interface }
|
||||
|
||||
Run at a terminal, the installer opens a full screen that a keyboard or a
|
||||
controller drives. Three presses of A or Enter install everything your setup
|
||||
controller drives. Three presses of A or Enter install everything the setup
|
||||
reads: the setup found here, the plan, then the result. The plan screen says
|
||||
how many files are missing, how much they weigh, how many are in place and how
|
||||
much space is free, before anything is written.
|
||||
|
||||
The other choices sit one press away. **Choose systems** ticks the consoles you
|
||||
play, with the total to download under the list. A long list opens on **None**:
|
||||
The other choices sit one press away. **Choose systems** ticks the consoles to
|
||||
keep, with the total to download under the list. A long list opens on **None**:
|
||||
A or Enter there unticks every system, and A on a system then takes that one alone. A
|
||||
on a system (**Continue**) always opens the confirm screen, which counts the choice
|
||||
before **Install**: it never installs by itself. Back with nothing ticked keeps the ticks
|
||||
@@ -365,7 +365,7 @@ file list is downloaded once and handed to each run.
|
||||
(`.<ref>.archive.part`), asking with `If-Range` so that a server whose
|
||||
archive changed sends it whole. A second run into the same folder finds
|
||||
that archive in use and downloads its own copy. A file already there with other contents
|
||||
is moved aside first, see [Files you already had](#files-you-already-had).
|
||||
is moved aside first, see [Files already in the folder](#files-you-already-had).
|
||||
8. Copies into standalone emulator directories, only with `--standalone-copies`.
|
||||
|
||||
The file list names the version of the platform it was built for (Batocera
|
||||
@@ -590,7 +590,7 @@ and `--check` exits with code 3. The files the collection does not hold are
|
||||
listed after it again, as long as they are absent from the folder. A number before `wrong copies` means a file is
|
||||
in place with contents the platform does not accept. Running the install again
|
||||
replaces it and keeps the old one, see
|
||||
[Files you already had](#files-you-already-had).
|
||||
[Files already in the folder](#files-you-already-had).
|
||||
|
||||
### Why a file is refused { #why-a-file-is-refused }
|
||||
|
||||
@@ -649,7 +649,7 @@ a part that was not extracted, or one extracted into a folder of its own:
|
||||
Download it and extract into /home/me/.config/retroarch/system, beside the other parts.
|
||||
```
|
||||
|
||||
## Files you already had
|
||||
## Files already in the folder { #files-you-already-had }
|
||||
|
||||
A file in the BIOS folder whose contents differ from the expected one (another
|
||||
revision, a personal dump) is replaced, but never lost. Just before the
|
||||
@@ -733,7 +733,7 @@ member locked by a password.
|
||||
The folder is only read: each file is copied, checked against the list's
|
||||
hashes once written, and the original stays where it is. A file in place with
|
||||
other contents is kept aside first, as a download would replace it (see
|
||||
[Files you already had](#files-you-already-had)). What no folder holds is
|
||||
[Files already in the folder](#files-you-already-had)). What no folder holds is
|
||||
downloaded as usual, or taken from the pack with `--from-pack`. Several
|
||||
`--from` are read in the order given, and the end of the run says where each
|
||||
file came from. A run of a list of four files, two of them in `old`:
|
||||
@@ -835,7 +835,7 @@ answer is not asked.
|
||||
Asked only when the chosen systems have such files. Every core is kept
|
||||
until numbers name some, and the head then says how many of how many
|
||||
(`1 of 2 cores`). The full screen's **Cores** opens with every core
|
||||
ticked, as Choose systems does: untick the ones you do not use, **All**
|
||||
ticked, as Choose systems does: untick the unused ones, **All**
|
||||
and **None** tick or untick them all.
|
||||
- **Regions.** Asked only when a chosen system has one BIOS per region. The
|
||||
numbers give an order of preference: `2,1` keeps the second region where a
|
||||
@@ -1474,7 +1474,7 @@ python install.py --platform retroarch --dest /storage/emulated/0/RetroArch/syst
|
||||
|
||||
RetroBIOS also runs on the handheld itself, in the
|
||||
[full-screen interface](#the-full-screen-interface), with the controller.
|
||||
Copy the file for your system to the SD card, then start RetroBIOS from the
|
||||
Copy the file for the system to the SD card, then start RetroBIOS from the
|
||||
menu.
|
||||
|
||||
The files below ship with the next release. Until then, build them from a
|
||||
@@ -1565,7 +1565,7 @@ On a Miyoo, B, X, Y and Select are Left Ctrl, Shift, Alt and Right Ctrl:
|
||||
pressed alone, a modifier sends nothing to a terminal, so the interface goes
|
||||
back with R2 instead, and the bottom line says so.
|
||||
|
||||
**Without Wi-Fi.** On a computer, download the BIOS pack that serves your
|
||||
**Without Wi-Fi.** On a computer, download the BIOS pack that serves the
|
||||
system (every part, for a pack in parts): RetroArch's on muOS, ArkOS,
|
||||
dArkOS and JELOS, Batocera's on Batocera. Copy it next to RetroBIOS: into
|
||||
`ports/retrobios` for the PortMaster port and on Batocera, to the root of
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ terms: license
|
||||
|
||||
# Is it legal?
|
||||
|
||||
It depends on your country. Nothing here is legal advice.
|
||||
It depends on the country. Nothing here is legal advice.
|
||||
|
||||
- Making an emulator is legal in the US. EU law allows it under conditions.
|
||||
- Sharing BIOS files is not settled law. The strongest case is old consoles no
|
||||
|
||||
@@ -7,8 +7,8 @@ terms: measurement, proof, tested, core probe, real conditions, method
|
||||
|
||||
# How emulators are tested
|
||||
|
||||
We run an emulator only when a question needs a real answer. That is an
|
||||
issue, a fix we send to a project, or a check before a release.
|
||||
An emulator runs here only when a question needs a real answer. That is an
|
||||
issue, a fix sent to a project, or a check before a release.
|
||||
|
||||
A page that relies on a run says so in a **Proof** line. Every run is on the
|
||||
[Measurements](../measurements.md) page.
|
||||
@@ -18,7 +18,7 @@ A page that relies on a run says so in a **Proof** line. Every run is on the
|
||||
- A script runs the emulator with no window and no sound.
|
||||
- It starts a game and runs it for a set time.
|
||||
- A test that compares files runs each case at least twice.
|
||||
- It compares the screens with ones we checked by eye.
|
||||
- It compares the screens with ones checked by eye.
|
||||
- It reads the emulator's log to see which BIOS it ran.
|
||||
|
||||
## What a result says
|
||||
@@ -37,8 +37,8 @@ A page that relies on a run says so in a **Proof** line. Every run is on the
|
||||
|
||||
- It shows the first minutes of one game, not the whole game.
|
||||
- It sees the files the emulator opened on this path, not every path.
|
||||
- Some emulators run their own BIOS when yours is refused. The game still works.
|
||||
- The app you play in may change settings the test left as they were.
|
||||
- Some emulators run their own BIOS when the given one is refused. The game still works.
|
||||
- The app that runs the game may change settings the test left as they were.
|
||||
|
||||
## How a file is proven
|
||||
|
||||
|
||||
+9
-9
@@ -4,23 +4,23 @@ order: 65
|
||||
terms: privacy, cookies, analytics, tracking
|
||||
---
|
||||
|
||||
# What we count
|
||||
# What is counted
|
||||
|
||||
No cookies. No account. Nothing is stored on your device, and nothing
|
||||
No cookies. No account. Nothing is stored on the device, and nothing
|
||||
links a visit to a person.
|
||||
|
||||
## On this site
|
||||
|
||||
When a page opens, your browser sends one request to
|
||||
When a page opens, the browser sends one request to
|
||||
[GoatCounter](https://www.goatcounter.com), an open-source counter. It
|
||||
carries:
|
||||
|
||||
- the page address, without the part after `?` or `#`;
|
||||
- the site you came from, by its address without a path
|
||||
- the site the visit came from, by its address without a path
|
||||
(`https://www.google.com`).
|
||||
|
||||
GoatCounter reads the browser name and the country from that request. It does
|
||||
not keep your IP address. To tell a reload from a second visitor it keeps a
|
||||
not keep the IP address. To tell a reload from a second visitor it keeps a
|
||||
code made from the request for 8 hours, then drops it.
|
||||
|
||||
These actions are counted the same way, with the page they happened on:
|
||||
@@ -35,12 +35,12 @@ These actions are counted the same way, with the page they happened on:
|
||||
as "other", without its words;
|
||||
- the answer to "Did this page help?".
|
||||
|
||||
Not sent: your screen size, your language, a search that found results.
|
||||
Not sent: the screen size, the language, a search that found results.
|
||||
|
||||
The totals are kept two years and are public:
|
||||
[retrobios.goatcounter.com](https://retrobios.goatcounter.com).
|
||||
|
||||
If your browser sends Do Not Track or Global Privacy Control, nothing is sent,
|
||||
If the browser sends Do Not Track or Global Privacy Control, nothing is sent,
|
||||
and the "Did this page help?" question is not shown. A content blocker that
|
||||
stops GoatCounter changes nothing on the site.
|
||||
|
||||
@@ -51,6 +51,6 @@ those requests as it sees any download.
|
||||
|
||||
## Search engines
|
||||
|
||||
Google and Bing tell us which searches showed the site and how often it was
|
||||
clicked. They report totals, from their own records, never from your
|
||||
Google and Bing report which searches showed the site and how often it was
|
||||
clicked. They give totals, from their own records, never from a reader's
|
||||
browser.
|
||||
@@ -8,7 +8,7 @@ group: Checking files
|
||||
|
||||
These sections are about this repository's own tools, run from a clone.
|
||||
`scripts/verify.py` checks the collection in `bios/` against each platform, never
|
||||
your BIOS folder: for that, use the installer's `--check`
|
||||
a user's BIOS folder: for that, use the installer's `--check`
|
||||
([When a game does not start](troubleshooting.md)). How each platform checks a
|
||||
file is on [How each platform checks files](verification-modes.md).
|
||||
|
||||
@@ -43,7 +43,7 @@ version the emulator tries first, and some cores accept several versions.
|
||||
|
||||
## Pack is missing files
|
||||
|
||||
A generated pack doesn't contain all the files you expected.
|
||||
A generated pack lacks some of the expected files.
|
||||
|
||||
### Severity levels
|
||||
|
||||
@@ -69,7 +69,7 @@ Files like PS3UPDAT.PUP, PSVUPDAT.PUP, and Switch firmware are too large for the
|
||||
git repository. They are stored as GitHub release assets under the `large-files`
|
||||
release and downloaded at build time.
|
||||
|
||||
If a pack build fails to include these, check your network connection. In offline
|
||||
If a pack build fails to include these, check the network connection. In offline
|
||||
mode (`--offline`), large files are only included if already cached locally in
|
||||
`.cache/large/`.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ The words the reference and contributor pages use, each with a real case. The
|
||||
everyday words (BIOS, emulator, core, hash) are in the [glossary](glossary.md).
|
||||
|
||||
**Frontend**
|
||||
: The menu you pick and start games from, such as EmulationStation, MinUI or
|
||||
: The menu that picks and starts games, such as EmulationStation, MinUI or
|
||||
the RetroArch menu. It starts the emulator.
|
||||
|
||||
**Pack**
|
||||
|
||||
@@ -367,7 +367,7 @@ download address. It lists the files of the pack the latest release holds,
|
||||
read from `release.json`, or gives the command when there is no pack. The
|
||||
README, the setup pages and the release notes link it, never an asset URL:
|
||||
step 6 deletes the previous tag, and with it every link to its assets. Six of
|
||||
the eight links one community wiki had to our packs answered 404 that way.
|
||||
the eight links one community wiki had to the packs answered 404 that way.
|
||||
Committing `release.json` in step 5 rebuilds these pages, so the release
|
||||
never touches them.
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ terms: mame version, romset version, which mame
|
||||
|
||||
# Technical questions
|
||||
|
||||
Answers to "how do you know?" questions: how a file is identified, why one BIOS
|
||||
Answers to "how is this known?" questions: how a file is identified, why one BIOS
|
||||
has several fingerprints, why some files are not BIOS, and which MAME version an
|
||||
arcade set belongs to. Each answer gives a real file as its example, so you can
|
||||
check it on the system pages. Questions asked before installing are in
|
||||
arcade set belongs to. Each answer gives a real file as its example, to check on the
|
||||
system pages. Questions asked before installing are in
|
||||
[Common questions](faq.md).
|
||||
|
||||
## What's a hash/checksum?
|
||||
@@ -50,7 +50,7 @@ Two main reasons:
|
||||
1. **Regional variants.** The same filename (e.g., `IPL.bin` for GameCube) exists in different versions for USA, Europe, and Japan. Each region has a different hash.
|
||||
2. **Revision differences.** Console manufacturers released updated BIOS versions over time. A PlayStation SCPH-5501 BIOS differs from a SCPH-7001.
|
||||
|
||||
Platforms that verify by MD5 accept specific hashes. If yours doesn't match any known hash, it may be a bad dump or an uncommon revision.
|
||||
Platforms that verify by MD5 accept specific hashes. A file that matches no known hash may be a bad dump or an uncommon revision.
|
||||
|
||||
A filename is never allowed to stand in for a declared hash. When a file entry
|
||||
carries a hash, resolution matches on content and reports `hash_mismatch` if the
|
||||
@@ -114,14 +114,14 @@ cross-referenced against their DATs: files matching a catalog entry
|
||||
carry a verified dump badge on the system pages, and catalog entries
|
||||
absent from the collection are tracked as acquisition targets. When
|
||||
the two views differ, this project follows the code, because that is
|
||||
what decides whether your emulator boots.
|
||||
what decides whether the emulator boots.
|
||||
|
||||
The [dump provenance](../provenance.md) page has the current coverage
|
||||
per catalog and the full list of catalogued dumps still missing.
|
||||
|
||||
## Which MAME version do the arcade BIOS sets match?
|
||||
|
||||
Your game sets and BIOS sets must come from the same version as your arcade
|
||||
Game sets and BIOS sets must come from the same version as the arcade
|
||||
emulator.
|
||||
|
||||
<!-- generated: romsets -->
|
||||
@@ -156,10 +156,10 @@ carry their own profiles on top of those generations: `mamearcade`,
|
||||
`mamemess`, `mame2003_midway`, the three `mame4droid` drops,
|
||||
`advancemame`, `groovymame` and `hbmame`. Recalbox compiles two MAME
|
||||
versions of its own, 0.258 and 0.278, profiled as `mame0258` and
|
||||
`mame0278`. The pack ships the sets matching the core you run, and a
|
||||
`mame0278`. The pack ships the sets matching the core in use, and a
|
||||
generic unversioned arcade pack cannot do that.
|
||||
|
||||
## Why does my verification report say UNTESTED?
|
||||
## Why does a verification report say UNTESTED? { #why-does-my-verification-report-say-untested }
|
||||
|
||||
`untested` means the file exists on disk but its hash does not match the expected value. This happens on MD5 and SHA1 platforms (Batocera, Recalbox, BizHawk, ROCKNIX, MiSTer FPGA, and the rest) when the file is present but contains different data than what the platform declares.
|
||||
|
||||
|
||||
@@ -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_reference`, `test_site_render`, `test_site_search`, `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_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
|
||||
@@ -219,7 +219,7 @@ disambiguated by a hash suffix on purpose.
|
||||
## How to add a test
|
||||
|
||||
1. **Pick the right category.** Find the number range that matches the
|
||||
subsystem you are testing. If none fits, start a new range after the last
|
||||
subsystem under test. If none fits, start a new range after the last
|
||||
existing one.
|
||||
|
||||
2. **Create synthetic fixtures.** Write the minimum YAML configs and fake
|
||||
@@ -235,7 +235,7 @@ disambiguated by a hash suffix on purpose.
|
||||
method, file counts, or pack contents. Avoid brittle assertions on log
|
||||
output or formatting.
|
||||
|
||||
5. **Run the full suite.** After adding your test, run `python -m unittest
|
||||
5. **Run the full suite.** After adding the test, run `python -m unittest
|
||||
discover -s tests -t . -v` to verify nothing else broke.
|
||||
|
||||
A new module starts with `import tests` right after
|
||||
@@ -497,7 +497,7 @@ chmod +x .git/hooks/commit-msg
|
||||
```
|
||||
|
||||
Files are staged by name, never with `git add -A` or `git add .`, which sweep in
|
||||
deletions and work that is not yours. Generated artifacts (`database.json`,
|
||||
deletions and other people's work. Generated artifacts (`database.json`,
|
||||
`retired.json`, `derivable.json`, `install/*.json`, `README.md`, `CONTRIBUTING.md`, `exports/`,
|
||||
the generated tables and run samples of `wiki/installer.md`) are committed
|
||||
with the data they derive from, never alone: a manifest committed before the
|
||||
|
||||
+3
-3
@@ -267,13 +267,13 @@ own verification mode ([discrepancy detection](verification-modes.md#discrepancy
|
||||
### Granular options
|
||||
|
||||
- `--system` with `--platform`: filter to specific systems within a platform pack.
|
||||
It takes our system ids (`sony-playstation`) and the platform's own names,
|
||||
It takes the collection's system ids (`sony-playstation`) and the platform's own names,
|
||||
read from `native_system` in its YAML (Batocera `psx`, Recalbox `msx1,msx2`).
|
||||
`--platform <name> --list-systems` prints both. `verify.py --platform --system`
|
||||
reports on the same narrowed pack, and the installer's `--system` reads the
|
||||
same mapping from the manifest's `system_names`. An unknown name is refused
|
||||
with the closest matches before any work. Without `--platform`, `--system`
|
||||
takes our ids only.
|
||||
takes the collection's ids only.
|
||||
- `--required-only`: exclude optional files and the data directories declared
|
||||
`required: false`, keep only required. `verify.py --required-only` reports
|
||||
on the same file set, for `--platform`, `--emulator` and `--system`
|
||||
@@ -1374,7 +1374,7 @@ before a release (wiki/release-process.md). Its lines stay in
|
||||
| `sitesources.py` | Record which cited source paths each pinned upstream tree holds, for the site's source links (`--fetch` reads a tree no cache holds) |
|
||||
| `site_markdown.py` | Write each built page as Markdown beside its HTML (`<page>/index.md`, named in every page's head) and `llms-full.txt`, the guides and every setup's page in one file, for the assistants that read the site. The Astro build runs it once the pages are written |
|
||||
| `site_insights.py` | What readers look for and do not find, from totals only: searches that show the site but are rarely clicked, file names searched and whether the collection holds them, site searches that found nothing, broken addresses, "Did this page help?" answers, GitHub traffic and release downloads (`--days`, `--skip gsc,goatcounter,github`, `--output`). Reads Search Console, GoatCounter and GitHub with credentials kept outside the repository. [What the site counts](privacy.md) |
|
||||
| `make_demo.py` | Record the installer demo of the README and the site into `docs_assets/demo/`: an ArkOS card with six games built under `tmp/demo/` and mounted in a mount namespace of the user, this checkout served on loopback, `docs_assets/demo/install.tape` run by VHS (needs `vhs`, `ttyd`, `ffmpeg`, `unshare`), then the GIF, WebM, MP4 and poster encoded and held to their size targets. `demo.json` records what the recording shows, for the alt text. Never run by the build |
|
||||
| `make_demo.py` | Record the installer demos of the README and the site into `docs_assets/demo/`. An ArkOS card with six games is built under `tmp/demo/` in a mount namespace of the user, this checkout is served on loopback, and VHS runs one tape per variant: `default` (`install.tape`, the one-line command in a terminal, the card at `/media/EASYROMS`, needs `unshare`) and `handheld` (`install-handheld.tape`, the full-screen interface at 640x480 on an ArkOS root made with `bwrap`, as its Tools menu starts it, driven by the keys gptokeyb sends for the buttons). `--variant NAME` records one (repeatable), every variant by default, the others kept. Each needs `vhs`, `ttyd` and `ffmpeg`. The default gets the README's GIF, each gets a WebM, an MP4 and a poster, held to their size targets, and an entry in `demo.json` with what it shows and its alt text. No Windows variant: under pwsh on Linux the installer's summary gives `--dest /mnt/e/bios` back. Never run by the build |
|
||||
| `validate_site.py` | Validate rendered metadata, heading order, image alternatives, JSON-LD, local resources, links and fragments, and the page size and search index budgets |
|
||||
| `romset_recipes.py` | Identify which emulator version an arcade archive matches, rebuild a pinned archive from ROMs already held, record a recipe for every archive the collection holds and build any of them from the members it holds (see [Archive recipes](#archive-recipes)) |
|
||||
| `archive_build.py` | The archive recipes behind `romset_recipes.py --capture`, `--build` and `--build-all`: capture, member lookup by sha1, verified build |
|
||||
|
||||
@@ -8,7 +8,7 @@ terms: bios not found, bios missing, firmware missing, bios error, check my bios
|
||||
|
||||
Usually a BIOS file is missing or wrong.
|
||||
|
||||
## 1. Check my BIOS folder { #checking-what-you-installed }
|
||||
## 1. Check the BIOS folder { #checking-what-you-installed }
|
||||
|
||||
<span id="game-wont-start-black-screen"></span><span id="my-setup-says-untested"></span>
|
||||
|
||||
@@ -17,7 +17,7 @@ Usually a BIOS file is missing or wrong.
|
||||
platforms/libretro/libretro.cpp:866 "OS-7 BIOS missing" (23d0ec28), RetroArch
|
||||
intl/msg_hash_us.h:32805 "One or more firmware files are missing" (5333a7c5). -->
|
||||
|
||||
Your emulator says **BIOS not found**, **BIOS missing** or **firmware
|
||||
The emulator says **BIOS not found**, **BIOS missing** or **firmware
|
||||
missing**? Run the command with `--check`. It changes nothing.
|
||||
|
||||
<!-- os-tabs -->
|
||||
@@ -42,9 +42,9 @@ Without `--check`, it adds missing files and replaces wrong ones.
|
||||
|
||||
## 3. Check the folder and the names
|
||||
|
||||
- **Right folder?** [Get your files](../which-pack.md) names it.
|
||||
- **Right folder?** [Get the files](../which-pack.md) names it.
|
||||
- **Exact name?** `scph5501.bin` and `SCPH-5501.BIN` differ.
|
||||
- **A `bios` folder inside your BIOS folder?** The installer copies those
|
||||
- **A `bios` folder inside the BIOS folder?** The installer copies those
|
||||
files up one level.
|
||||
- **BIOS setting on a subfolder, like `system/dc`?** Point it at the BIOS
|
||||
folder. Emulators add their own, like `dc/` for Flycast.
|
||||
@@ -94,5 +94,5 @@ Other messages: [When something goes wrong](installer.md#when-something-goes-wro
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Get your files](../which-pack.md)
|
||||
- [Get the files](../which-pack.md)
|
||||
- [Common questions](faq.md)
|
||||
@@ -8,7 +8,7 @@ order: 10
|
||||
A console has a small program inside it. It starts first, then starts the game.
|
||||
This program is the **BIOS**, also called **firmware**.
|
||||
|
||||
<a class="rb-button rb-button-primary" href="../which-pack.md">Get your files</a>
|
||||
<a class="rb-button rb-button-primary" href="../which-pack.md">Get the files</a>
|
||||
|
||||
An **emulator** is an app that acts like a console. For many consoles, it needs
|
||||
a copy of the BIOS. That copy is a file: a **BIOS file**.
|
||||
@@ -17,14 +17,14 @@ a copy of the BIOS. That copy is a file: a **BIOS file**.
|
||||
|
||||
## How it works
|
||||
|
||||
1. You pick a game in your setup's menu.
|
||||
1. Pick a game in the setup's menu.
|
||||
2. The setup starts the emulator.
|
||||
3. The emulator opens the BIOS file in your **BIOS folder**.
|
||||
3. The emulator opens the BIOS file in the **BIOS folder**.
|
||||
4. The game starts.
|
||||
|
||||
If step 3 fails, you see "BIOS missing" or a black screen.
|
||||
If step 3 fails, the screen says "BIOS missing" or stays black.
|
||||
|
||||
## Do I need one?
|
||||
## Which consoles need one? { #do-i-need-one }
|
||||
|
||||
- **Yes:** PlayStation 1 and 2, Saturn, Neo Geo, PC Engine CD and many home
|
||||
computers.
|
||||
@@ -49,12 +49,12 @@ 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.
|
||||
|
||||
Our files are the versions each setup expects.
|
||||
The files here are the versions each setup expects.
|
||||
|
||||
## 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 we include it.
|
||||
then glitch or do not start. The real file works best, so the packs include it.
|
||||
|
||||
## Is it legal?
|
||||
|
||||
@@ -62,6 +62,6 @@ Laws differ by country. [Is it legal?](legal.md)
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Get your files](../which-pack.md)
|
||||
- [Get the files](../which-pack.md)
|
||||
- [Words used on this site](glossary.md)
|
||||
|
||||
Reference in new issue
Block a user