docs: plain copy on the newcomer pages

The Start here pages ran 460 to 2,100 words and the first command of
Get your files sat 254 words down. Each page now opens on its action,
with short sentences and one fact per block. The facts a newcomer does
not act on moved one level down: the legal analysis to wiki/legal.md,
the project terms to wiki/reference-terms.md, the annotated run and the
installer failures to the installer page, collection figures to a new
Explore index.

tests/test_site_copy.py holds the budgets on the generated pages, and
the testing guide states them with their reasons.

Four statements the repository contradicts are rewritten: prboom.wad
is compiled into PrBoom (emulators/prboom.yml), Batocera reports a
mismatch as UNTESTED (batocera-systems checkBios), 13 hle_fallback
entries are required, and the gaps headline splits missing firmware
from game data and unread entries.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 14:23:54 +02:00
1 parent 5d868ee19b
commit 70d761682c
24 files changed
+1746 -1259

No files matched your search

+9 -5
View File
@@ -92,6 +92,10 @@ def compute_coverage(
"verified": ok,
"untested": untested,
"missing": missing,
"missing_not_firmware": {
category: len(dests)
for category, dests in (result.get("missing_not_firmware") or {}).items()
},
"present": present,
"percentage": pct,
"core_present": core_present,
@@ -116,7 +120,7 @@ _REPO = Path(__file__).resolve().parent.parent
# stores it here and the second reads it back, instead of verifying every
# platform twice (about a minute of the site build).
COVERAGE_CACHE_DIR = _REPO / "tmp" / "coverage-cache"
_COVERAGE_CACHE_FORMAT = "1"
_COVERAGE_CACHE_FORMAT = "2"
def _coverage_inputs(db_path: Path, platforms_dir: str, emulators_dir: str) -> str:
@@ -293,8 +297,8 @@ def extract_notes(platforms_dir: str) -> list[str]:
display = config.get("platform", key)
if config.get("base_destination") == "" and display in folders:
notes.append(
f"The {display} pack already carries its own `{own_root(config)}/` folder,"
f" so it extracts into `{folders[display]}` rather than into the BIOS folder."
f"The {display} pack holds its own `{own_root(config)}/` folder."
f" Unzip it into `{folders[display]}`."
)
return notes
@@ -810,8 +814,8 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
" backup, archival, and interoperability with emulation software."
" [NOTICE](NOTICE) sets out their status and how to ask for a file"
" to be removed.",
f"The reasoning, and where it is weakest, is in the"
f" [FAQ]({SITE_URL}wiki/faq/#is-this-legal).",
f"The reasoning, and where it is weakest, is in"
f" [the project's legal position]({SITE_URL}wiki/legal/).",
"",
f"*Auto-generated on {ts}*",
]
+282 -240
View File
@@ -82,6 +82,7 @@ from siteguides import (
GUIDES,
Guide,
INSTALL_BASE,
TERMINALS,
install_command,
platform_args,
terminal_tabs,
@@ -414,105 +415,92 @@ def generate_home(
) -> str:
"""The page someone lands on knowing nothing: what a BIOS file is,
whether they need one, and the three steps to get the right ones.
Figures about the collection come last."""
The figures about the collection, the cards to the deeper pages and the
call for help sat below the steps and doubled the page: they are on the
Explore index, one tab further."""
registry = registry or {}
total_files = db.get("total_files", 0)
total_size = db.get("total_size", 0)
emulator_count = len(unique_emulator_profiles(profiles))
names = sorted((cov["platform"] for cov in coverages.values()), key=str.casefold)
named = [n for n in ("RetroArch", "Batocera", "EmuDeck") if n in names]
others = len(names) - len(named)
works_with = ", ".join(named) + (f" and {others} more" if others else "")
lines = [
"# BIOS files for your emulators, in the right folder",
"# BIOS files for your emulators",
"",
"Many consoles start their games with the help of a small program built "
"into the machine, called a BIOS or firmware. An emulator needs a copy of "
"that file to start the games of those consoles. RetroBIOS gives you the "
"right files for the setup you already have, in the folder where your "
"emulators look for them.",
"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 "
"in the right folder.",
"",
"[Get your files](which-pack.md){ .md-button .md-button--primary } "
"[What is a BIOS file?](wiki/what-are-bios-files.md){ .md-button }",
"[What is a BIOS?](wiki/what-are-bios-files.md){ .md-button }",
"",
f"Free, with no account. It works with {', '.join(names[:-1])} and {names[-1]}.",
f"Free. No account. Works with {works_with}.",
"",
"<!-- image: bios-chain -->",
"## Do I need it?",
"",
"## Do I need this?",
"- **Yes**, if your emulator says a BIOS is missing.",
"- **Yes** for PlayStation 1 and 2, Saturn, Dreamcast and Neo Geo.",
"- **Usually not** for NES, Super Nintendo, Mega Drive and Game Boy.",
"",
"- **Yes**, if a game or an emulator says a BIOS file is missing.",
"- **Usually yes** for PlayStation, PlayStation 2, Saturn, Dreamcast, Neo "
"Geo and many home computers. Details: [the files each system "
"needs](systems/index.md).",
"- **Usually not** for NES, Super Nintendo and Mega Drive cartridges: those "
"consoles start games without a BIOS. Their add-ons do need one, such as "
"the Famicom Disk System and the Mega-CD.",
"Details: [Every console, one by one](systems/index.md)",
"",
"## Three steps",
"",
'<div class="grid cards" markdown>',
"",
"- **1. Find your setup**",
"",
" Pick your device, or the system you play on. Its page shows the folder "
"where the files go.",
"",
" [Pick your setup](which-pack.md#pick-your-setup)",
"",
"- **2. Get the files**",
"",
" One command downloads only what is missing and checks each file. You "
"can also download a ZIP pack and extract it yourself.",
"",
" [First install, step by step](wiki/first-install.md)",
"",
"- **3. Start a game**",
"",
" Your emulator finds the files by itself. If a game still does not "
"start, the help page goes through the usual causes.",
"",
" [When a game does not start](wiki/troubleshooting.md)",
"",
"</div>",
"1. **Pick your device.** Its page shows your BIOS folder.",
"2. **Run one command**, or download a ZIP file.",
"3. **Start a game.**",
"",
"## Pick your device",
"",
'<div class="grid cards" markdown>',
"",
]
for group, (label, description) in DEVICE_GROUPS.items():
for group, (label, _description) in DEVICE_GROUPS.items():
members = _group_members(coverages, registry, group)
if not members:
continue
lines += [
f"- **{label}**",
"",
f" {description}",
"",
" " + ", ".join(
f"[{coverages[name]['platform']}](platforms/{name}.md)" for name in members
),
"",
f" [See where the files go](which-pack.md#{GROUP_ANCHORS[group]})",
"",
]
lines += [
"</div>",
"",
"## Is this safe?",
"## Is it safe?",
"",
"The installer writes only inside your BIOS folder, checks every file "
"before saving it, keeps any file of yours it replaces in a "
"retrobios-backup folder beside it, and never touches games or saves. "
"[How the command is checked](wiki/faq.md#is-piping-the-installer-into-a-shell-safe).",
"- It writes only in your BIOS folder.",
"- It checks every file before saving it.",
"- It keeps any file it replaces, in a `retrobios-backup` folder.",
"- It never touches your games or saves.",
"",
"Laws on copying console firmware differ by country. "
"[The project's position](wiki/faq.md#is-this-legal) says where it stands "
"and how to ask for a file to be removed.",
"[How the command is checked](wiki/faq.md#is-piping-the-installer-into-a-shell-safe). "
"[Is it legal?](wiki/faq.md#is-this-legal)",
"",
"## Go further",
*_next_block([
"[Get your files](which-pack.md)",
"[What is a BIOS?](wiki/what-are-bios-files.md)",
]),
f'<div class="rb-timestamp">Generated on {_timestamp()}.</div>',
]
return "\n".join(lines) + "\n"
def generate_explore_index(db: dict, coverages: dict, profiles: dict) -> str:
"""The Explore tab opens here: where to look for one console, one
emulator or one installer option, and the figures of the collection."""
total_files = db.get("total_files", 0)
total_size = db.get("total_size", 0)
emulator_count = len(unique_emulator_profiles(profiles))
lines = [
"# Explore",
"",
"These pages go into detail, for when you want to know more than where the "
"files go.",
"One console, one emulator, or one installer option: each has its page, "
"with the files it needs and the code that reads them.",
"",
'<div class="grid cards" markdown>',
"",
@@ -520,13 +508,19 @@ def generate_home(
"",
" What each console and computer needs, file by file.",
"",
" Details: [Browse the systems](systems/index.md)",
" [Browse the systems](systems/index.md)",
"",
"- **Emulators**",
"",
" What each emulator reads, with the source code that reads it.",
"",
" Details: [Browse the emulators](emulators/index.md)",
" [Browse the emulators](emulators/index.md)",
"",
"- **Installer options**",
"",
" Fewer files, one region, another folder, a check without download.",
"",
" [All installer options](wiki/installer.md)",
"",
"- **How files are checked**",
"",
@@ -536,12 +530,14 @@ def generate_home(
"",
"- **Data and API**",
"",
" Every file and hash as JSON, CSV and SQLite, for your own tools.",
" Every file and hash as JSON, CSV and SQLite.",
"",
" Reference: [Data and API](data.md)",
"",
"</div>",
"",
"## The collection",
"",
'<div class="rb-stats" markdown>',
"",
*_stat(f"{total_files:,}", COLLECTION_FILES_LABEL),
@@ -554,14 +550,9 @@ def generate_home(
"",
"## Help the project",
"",
"Files, emulator profiles and corrections are welcome. "
"For contributors: [How to contribute](contributing.md).",
"A file you hold, an emulator profile or a correction fills a gap for "
"every setup at once. For contributors: [How to contribute](contributing.md).",
"",
*_next_block([
"[Get your files](which-pack.md)",
"[What BIOS files are](wiki/what-are-bios-files.md)",
]),
f'<div class="rb-timestamp">Generated on {_timestamp()}.</div>',
]
return "\n".join(lines) + "\n"
@@ -1429,6 +1420,7 @@ def _browser_title(relative: Path, title: str) -> str:
"systems/index.md": "Systems",
"emulators/index.md": "Emulators",
"wiki/index.md": "Reference",
"explore.md": "Explore",
}
if key in index_titles:
return index_titles[key]
@@ -1470,10 +1462,11 @@ def _page_description(markdown: str, relative: Path, title: str) -> str:
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.",
"data.md": "Versioned RetroBIOS JSON API, CSV exports, SQLite snapshot, schemas, checksums and data interpretation guidance.",
"cross-reference.md": "Cross-reference from retrogaming platforms to emulator cores, systems, upstream projects and profiled firmware files.",
"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.",
"exports.md": "Each platform's own BIOS list (System.dat, batocera-systems, es_bios.xml and the rest) with the hashes and files the emulator source code shows, offered to its maintainers.",
"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.",
}
if key in specific:
@@ -1875,7 +1868,7 @@ def page_level(relative: Path) -> str:
return "start"
if relative.parts[0] == "platforms":
return "setup"
if relative.parts[0] in ("systems", "emulators"):
if relative.parts[0] in ("systems", "emulators") or path == "explore.md":
return "explore"
if path in _REFERENCE_PAGES:
return "reference"
@@ -1978,8 +1971,7 @@ def generate_platform_index(coverages: dict, registry: dict | None = None) -> st
lines = [
"# Your setup",
"",
"Pick the system you play on. Its page says where the BIOS files go, "
"gives the command that installs them and links its pack.",
"Pick what you play on. Its page gives the folder, the command and the ZIP.",
"",
]
for group, (label, description) in DEVICE_GROUPS.items():
@@ -2314,19 +2306,24 @@ def _core_folders_section(name: str, config: dict) -> list[str]:
def _check_sentence(display: str, mode: str, config: dict, no_list: bool) -> str:
"""What the frontend itself checks, from the mode its code uses."""
if no_list:
return (f"{display} publishes no BIOS list, so it does not check files "
"itself. Each emulator core looks for its own files when a game starts.")
return (f"{display} has no BIOS list, so it checks nothing itself. Each "
"emulator looks for its own files when a game starts.")
if config.get("hash_rule") == "size_and_any_hash":
return (f"{display} itself compares the size and a hash (MD5, SHA1 or "
"CRC32) of each file with its list, so a file with other contents "
"does not count there.")
algorithm = digest_algorithm(mode)
if algorithm:
return (f"{display} itself compares the {algorithm.upper()} of each file "
"with its list, so a file with other contents does not count there.")
return (f"{display} itself only checks that each file is there, by its name. "
"The check above also reads the contents, so it finds a file that has "
"the right name and other contents.")
return (f"{display} checks each file's size and fingerprint too. It "
"reports a file with the wrong size or contents.")
if digest_algorithm(mode):
return (f"{display} checks each file's fingerprint too. It reports a file "
"with the wrong contents.")
return (f"{display} only checks that a file with the right name is there. "
"`--check` also checks what is inside.")
def _folder_rows(rows: tuple[tuple[str, str], ...], indent: str = "") -> list[str]:
return [
f"{indent}| Where | BIOS folder |",
f"{indent}|-------|-------------|",
*(f"{indent}| {label} | {path} |" for label, path in rows),
]
def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list: bool) -> list[str]:
@@ -2334,15 +2331,9 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
guide = GUIDES[name]
entry = registry.get(name, {})
extract_to = entry.get("extract_to")
lines = [
"## Where the files go",
"",
"| Where | BIOS folder |",
"|-------|-------------|",
*(f"| {label} | {path} |" for label, path in guide.folders),
]
lines = ["## Where the files go", "", *_folder_rows(guide.folders)]
if extract_to:
lines.append(f"| Extract the pack into | `{extract_to}` |")
lines.append(f"| Unzip the pack into | `{extract_to}` |")
lines.append("")
for note in guide.notes:
lines += [note, ""]
@@ -2350,35 +2341,38 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
for line in (note, "")]
if name == "retroarch":
lines += [
"Handheld systems that start RetroArch for their games keep its files "
"in the folder below. The command finds it by itself.",
'??? note "Other builds: Microsoft Store, Mac Steam, handhelds"',
"",
"| System | BIOS folder |",
"|--------|-------------|",
*(f"| {system} | `{folder}` |" for system, folder in _retroarch_handhelds(registry)),
*_folder_rows(guide.more_folders, " "),
*(f" | {system} | `{folder}` |" for system, folder in _retroarch_handhelds(registry)),
"",
" LaunchBox and other launchers use the folder RetroArch reads.",
"",
]
elif guide.more_folders:
lines += [
'??? note "Other layouts"',
"",
*_folder_rows(guide.more_folders, " "),
"",
]
lines += [
*(["<!-- image: minui-card -->", ""] if guide.on_card else []),
"## Install",
"",
f"The command finds the {display} BIOS folder, downloads the files that "
"are missing or differ, checks each one, and resumes where it stopped.",
"The command finds the folder, downloads what is missing, and checks "
"each file.",
"",
]
if guide.on_card:
lines += [
f"Put the {display} SD card in a computer. On Windows, note its drive "
"letter (for example **E:**). Then run the line with the root of the "
"card after `--dest`.",
"Put the SD card in a computer. On Windows, note its letter, like "
"**E:**. Put the card's path after `--dest`.",
"",
]
lines += [
*terminal_tabs(guide.run, platform_args(name, guide)),
*trust_box("../"),
"To install fewer systems, add `--system` with the system ids listed in "
"the reference below.",
"",
"## Download",
"",
]
@@ -2386,21 +2380,26 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
pack = release_record.pack_for(name, record)
if pack and pack.get("assets"):
parts = len(pack["assets"])
bullets = [
f"{_fmt_size(pack['download_size'])} to download, "
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.")
target = f"`{extract_to}`" if extract_to else "your BIOS folder"
bullets.append(f"Unzip {'them all' if parts > 1 else 'it'} into {target}.")
if guide.download_hint:
bullets.append(guide.download_hint)
lines += [
_pack_buttons(name, f"Download {display} Pack"),
"",
f"{_fmt_size(pack['download_size'])} to download"
+ (f" in {parts} parts" if parts > 1 else "")
+ f", {_fmt_size(pack['extracted_size'])} once extracted. "
+ (f"Extract it into `{extract_to}`. " if extract_to else "")
+ ("Every part is needed: extract them all into the same folder."
if parts > 1 else ""),
*(f"- {b}" for b in bullets),
"",
]
elif record.get("tag"):
lines += [
f"The {record['tag']} release has no {display} pack yet. The command "
"above installs the same files.",
f"No {display} pack in release {record['tag']} yet. The command above "
"installs the same files.",
"",
]
else:
@@ -2409,19 +2408,16 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
lines += [
"## How to check",
"",
"The same command with `--check` reads the BIOS folder, checks every file "
"and writes nothing. It ends on a line such as "
"`5865/5865 present (5865 verified, 0 wrong hash)`.",
"Add `--check` to the command. It changes nothing.",
"",
*terminal_tabs(guide.run, check_args, steps=False),
"All good when you see `0 wrong hash`.",
"",
_check_sentence(display, cov["mode"], cov["config"], no_list),
"",
*_next_block([
"[First install, step by step](../wiki/first-install.md), with what "
"each step prints",
"[First install, step by step](../wiki/first-install.md)",
"[When a game does not start](../wiki/troubleshooting.md)",
"[Every file by system](#reference-every-file-by-system), further down "
"this page",
]),
]
return lines
@@ -2470,9 +2466,7 @@ def generate_platform_page(
[
'!!! warning "Archived platform"',
"",
" The configuration is kept and packs are still built, but "
"upstream is no longer scraped on a schedule, so the file list "
"reflects the last sync rather than today's upstream.",
f" {display}'s list is kept as last read. Its pack is still built.",
"",
]
)
@@ -2490,9 +2484,11 @@ def generate_platform_page(
"",
level_label("reference"),
"",
"What follows is for checking the work: the counts, where the list was "
"read from, and each file with its hashes.",
"For checking the work: the counts, where the list comes from, and "
"every file with its hashes.",
"",
*(line for note in (GUIDES[name].reference_notes if name in GUIDES else ())
for line in (note, "")),
]
if no_list:
# MinUI and its forks hand each core a folder and name no file: a
@@ -3061,7 +3057,7 @@ def generate_provenance_page(
"not by catalog membership. Plenty of files this project ships are "
"outside any catalog by nature: composites a core assembles for itself "
"(the MiSTer X68000 `boot.rom` is `cgrom.dat` joined to `iplrom.dat`), "
"modern console firmware updates, game data such as `prboom.wad`, and "
"modern console firmware updates, game data such as Diablo's `diabdat.mpq`, and "
"arcade sets tracked by MAME driver source instead.",
"",
"When the two views disagree, this project follows the code, because "
@@ -4251,14 +4247,16 @@ def _render_metadata_rows(profile: dict) -> list[str]:
("resources_directory", "Resources directory"),
("rom_path", "ROM path"),
("game_count", "Game count"),
("verification", "Checked by"),
("verification", "Checks the file by"),
("analysis_date", "Analysis date"),
("analysis_commit", "Analysis commit"),
]:
val = profile.get(field)
if val is None or val == "" or isinstance(val, (dict, list)):
continue
if isinstance(val, str) and val.startswith("http"):
if field == "bios_size" and isinstance(val, int):
lines.append(f"| {label} | {_fmt_size(val)} ({val:,} bytes) |")
elif isinstance(val, str) and val.startswith("http"):
lines.append(f"| {label} | [{val}]({val}) |")
else:
lines.append(f"| {label} | {val} |")
@@ -5008,9 +5006,10 @@ def _render_problem_files(
if p["status"] == "missing":
entry = missing_files.setdefault(fname, {
"name": fname, "required": p["required"],
"platforms": [], "reason": p["reason"],
"platforms": [], "reason": p["reason"], "categories": set(),
})
entry["platforms"].append(p["platform"])
entry["categories"].add(p.get("category", "bios"))
if p["required"]:
entry["required"] = True
elif p["status"] == "untested":
@@ -5038,7 +5037,7 @@ def _render_problem_files(
"## Problem files",
"",
f"{len(missing_files)} missing, {len(untested_files)} untested, "
f"{len(mismatch_files)} hash mismatch.",
f"{len(mismatch_files)} discrepancy.",
"",
])
@@ -5047,14 +5046,22 @@ def _render_problem_files(
f'### Missing <span class="rb-badge rb-badge-danger">'
f"{_count(len(missing_files), 'file')}</span>",
"",
"| File | Required | Platforms |",
"|------|----------|-----------|",
"Required is the platform's own flag. Category says what the "
"platform's emulators do with the file: `bios` is firmware they "
"load, `game_data` comes from a copy of the game, `unread` is "
"opened by none of them. Only a missing `bios` entry stops a "
"console from starting.",
"",
"| File | Category | Required | Platforms |",
"|------|----------|----------|-----------|",
])
for fname in sorted(missing_files):
f = missing_files[fname]
req = "yes" if f["required"] else "no"
plats = ", ".join(sorted(set(f["platforms"])))
lines.append(f"| `{fname}` | {req} | {plats} |")
cats = f["categories"]
category = "bios" if "bios" in cats else ", ".join(sorted(cats))
lines.append(f"| `{fname}` | {category} | {req} | {plats} |")
lines.append("")
if untested_files:
@@ -5062,7 +5069,7 @@ def _render_problem_files(
f'### Untested <span class="rb-badge rb-badge-warning">'
f"{_count(len(untested_files), 'file')}</span>",
"",
"Present but hash not verified. Declared is what the platform "
"Present, but not the expected hash. Declared is what the platform "
"asks for, the collection copy is what the path holds.",
"",
"| File | Platforms | Reason | Declared | Collection copy |",
@@ -5081,10 +5088,11 @@ def _render_problem_files(
if mismatch_files:
lines.extend([
f'### Hash mismatch <span class="rb-badge rb-badge-warning">'
f'### Discrepancy <span class="rb-badge rb-badge-warning">'
f"{_count(len(mismatch_files), 'file')}</span>",
"",
"Platform says OK but emulator validation disagrees.",
"The platform's own check passes, but the emulator's size or "
"hash check fails. Each row names the check that fails.",
"",
"| File | Platforms | Discrepancy | Accepted dump in the collection |",
"|------|----------|-------------|-------------------|",
@@ -5118,25 +5126,44 @@ def _render_problem_files(
return lines
def _platform_lists_sentence(total: int, verified: int, untested: int, missing: int) -> str:
def _platform_lists_sentence(
total: int, verified: int, untested: int, missing: int,
not_firmware: dict[str, int] | None = None,
) -> str:
"""The platforms' own lists in one sentence, every count said.
Four tiles opened the page: "Verified (100%)" for 5,437 of 5,438 and
"Missing 0", above a table of red cells, on the page about what is
missing. The gaps are in the files the emulators load, and the sentence
says so.
says so. The missing count then read as missing firmware, while every
entry behind it was game data or a file no emulator opens: the split by
category is said with it.
"""
def _is(count: int) -> str:
return "is" if count == 1 else "are"
not_firmware = {k: v for k, v in (not_firmware or {}).items() if v}
parts = [f"{verified:,} {_is(verified)} held and pass the platform's own check"]
if untested:
parts.append(f"{untested:,} {_is(untested)} held but not verified "
"(listed under Untested below)")
parts.append(f"{missing:,} {_is(missing)} missing")
listed = ", ".join(parts[:-1]) + f" and {parts[-1]}"
split = ""
if missing and not_firmware:
labels = {"game_data": "game data",
"unread": "opened by none of the platform's emulators"}
firmware = missing - sum(not_firmware.values())
pieces = [f"{firmware:,} firmware"] + [
f"{count:,} {labels.get(category, category)}"
for category, count in sorted(not_firmware.items())
]
split = (
f" Of the missing, {', '.join(pieces[:-1])} and {pieces[-1]}: "
"only missing firmware stops a console from starting."
)
return (
f"Of the {total:,} files on the platforms' own BIOS lists, {listed}. "
f"Of the {total:,} files on the platforms' own BIOS lists, {listed}.{split} "
"The files the emulators load beyond those lists are counted per "
"platform in the table below."
)
@@ -5169,6 +5196,7 @@ def generate_gap_analysis(
total_untested = 0
total_missing_verify = 0
total_files_verify = 0
not_firmware_totals: dict[str, int] = {}
platform_problems: list[dict] = []
settled_problems: list[dict] = []
@@ -5177,6 +5205,8 @@ def generate_gap_analysis(
total_untested += cov["untested"]
total_missing_verify += cov["missing"]
total_files_verify += cov["total"]
for category, count in (cov.get("missing_not_firmware") or {}).items():
not_firmware_totals[category] = not_firmware_totals.get(category, 0) + count
for d in cov["details"]:
if d.get("settled_by") and d["status"] != "ok":
@@ -5195,6 +5225,7 @@ def generate_gap_analysis(
"name": d["name"],
"status": d["status"],
"required": d.get("required", True),
"category": d.get("category", "bios"),
"reason": d.get("reason", ""),
"discrepancy": d.get("discrepancy", ""),
"system": d.get("system", ""),
@@ -5205,15 +5236,24 @@ def generate_gap_analysis(
lines = [
"# Gap analysis",
"",
"What each platform and its emulators still lack, and where the "
"collection's copies come from.",
"What each pack still cannot give you, 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 "
"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 "
"every platform at once.",
"",
"[Download gaps CSV](downloads/gaps.csv){ .md-button } "
"[Open gaps API](api/v1/gaps.json){ .md-button } "
"[All data exports](data.md){ .md-button }",
"",
_platform_lists_sentence(
total_files_verify, total_verified, total_untested, total_missing_verify
total_files_verify, total_verified, total_untested, total_missing_verify,
not_firmware_totals,
),
"",
]
@@ -5259,7 +5299,7 @@ def generate_gap_analysis(
"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 "
"source code, and it is often several times larger. A short "
"fraction is flagged and named in the sections below. 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. "
@@ -5267,7 +5307,7 @@ def generate_gap_analysis(
"from its source code "
"([how each one works](wiki/verification-modes.md)).",
"",
"## Corroboration against emulator source",
"## How much of each list the emulator code confirms { #corroboration-against-emulator-source }",
"",
"| Platform | On its BIOS list | Documented in a profile | Content the code checks |",
"|----------|-----------------:|------------------------:|------------------------:|",
@@ -5402,7 +5442,7 @@ def generate_gap_analysis(
)
lines.extend([
"## Core complement",
"## Files emulators load that no platform lists { #core-complement }",
"",
f"Files loaded by emulators but not declared by any platform. "
f"{total_undeclared:,} files across {_count(len(emulator_gaps), 'emulator')}, "
@@ -5416,7 +5456,7 @@ def generate_gap_analysis(
"Whatever is not in the collection is an acquisition target, named "
"per emulator below.",
"",
"### Provenance",
"### Where the files come from",
"",
"| Source | Count | Description |",
"|--------|------:|-------------|",
@@ -5785,14 +5825,17 @@ def generate_cross_reference(
})
lines = [
"# Cross-reference",
"# Cores by platform",
"",
f"Platform > Core > Systems > Upstream emulator. "
f"{total_cores} cores across {len(coverages)} platforms, "
f"tracing back to {total_upstreams} upstream projects.",
"Every core each platform runs, the systems it emulates, and the project "
"it comes from. A core built for RetroArch is often a port of another "
"emulator, and the two can differ in the files they read and the checks "
"they make, so each core names its upstream project. "
f"{total_cores} cores across {len(coverages)} platforms come from "
f"{total_upstreams} upstream projects.",
"",
"The libretro core is a port of the upstream emulator. "
"Files, features, and validation may differ between the two.",
"The files a platform's emulators load and its own list does not name are "
"counted on [What the packs do not hold](gaps.md).",
"",
"[Download cross-reference CSV](downloads/cross-reference.csv){ .md-button } "
"[Open emulator API](api/v1/emulators.json){ .md-button } "
@@ -6095,58 +6138,49 @@ def _group_keys(registry: dict, group: str, display: dict[str, str]) -> list[str
)
def _own_root_folders(registry: dict) -> dict[str, str]:
"""Platforms whose pack holds its own top folder, with where it unzips.
The same rule as extract_notes: a list written from an empty base
destination carries its root in every path."""
folders = {}
for key, entry in registry.items():
config = load_platform_config(key, "platforms")
if config.get("base_destination") == "" and entry.get("extract_to"):
folders[key] = entry["extract_to"]
return folders
def generate_which_pack() -> str:
"""The device picker: every registered platform, grouped by the device it
runs on, with its BIOS folder, its pack and its page."""
runs on, with its BIOS folder, its pack and its page.
The command comes first: it finds the setup by itself, and the tables
below it serve the reader who downloads a ZIP."""
registry = load_platform_registry("platforms")
record = release_record.load_record()
display = {key: load_platform_config(key, "platforms").get("platform", key) for key in registry}
check_sh, check_ps = install_command("sh", "--check"), install_command("powershell", "--check")
card = install_command("sh", "--platform retroarch --dest /path/to/sdcard/retroarch/cores/system")
own_root = _own_root_folders(registry)
lines = [
"# Get your files",
"",
"Each setup keeps its BIOS files in a folder of its own. Pick yours below "
"to see where that folder is. Then get the files in one of two ways:",
"Two ways to fill your BIOS folder.",
"",
"- **One command**, the easiest. It finds your setup and its BIOS folder, "
"downloads only the files that are missing, and checks each one before "
"saving it.",
"- **A ZIP pack.** You download the pack of your setup and extract it into "
"the BIOS folder yourself.",
"",
"<!-- image: install-flow -->",
"",
"## Pick your setup",
"",
'<div class="grid cards" markdown>',
"",
]
for group, (label, description) in DEVICE_GROUPS.items():
members = _group_keys(registry, group, display)
if not members:
continue
lines += [
f"- **{label}**",
"",
f" {description}",
"",
f" [{', '.join(display[k] for k in members)}](#{GROUP_ANCHORS[group]})",
"",
]
lines += [
"</div>",
"- **[One command](#quick-install)**: finds your folder and checks each "
"file.",
"- **[A ZIP file](#manual-download)**: download it, unzip it into your "
"BIOS folder.",
"",
"## One command { #quick-install }",
"",
"Run this line in a terminal. It finds your setup by itself. On a system "
"that starts into a game menu, such as Batocera, the line runs on the "
"device: the page of your setup gives the exact command.",
"A **terminal** is a window where you type commands.",
"",
*terminal_tabs(("windows", "macos", "linux", "steamdeck", "android"), {}),
"On Batocera, Recalbox and other game systems, run it on the device. "
"Your setup's page shows how.",
"",
*trust_box(""),
"At a terminal, the installer shows the setup it found and asks what to "
"install:",
"It then asks what to install. Press **Enter** for everything.",
"",
"```",
" Enter) install everything",
@@ -6155,30 +6189,38 @@ def generate_which_pack() -> str:
" q) quit",
"```",
"",
"Press **Enter** to install everything. Press **c** to pick only some "
"consoles. The **i** line appears when the installer finds emulator cores "
"installed, and keeps only the files they read.",
"**Check your folder later.** Add `--check`. It changes nothing.",
"",
"To see what is already in your BIOS folder, add `--check`. It reads the "
"folder, checks every file and writes nothing:",
*terminal_tabs(("windows", "macos", "linux", "steamdeck", "android"),
dict.fromkeys(TERMINALS, "--check"), steps=False),
'??? note "Fill an SD card from a computer"',
"",
"```sh",
check_sh,
"```",
" Name your setup and the BIOS folder on the card. For RetroArch on a "
"Nintendo Switch:",
"",
"```powershell",
check_ps,
"```",
" ```sh",
" " + install_command(
"sh", "--platform retroarch --dest /path/to/sdcard/retroarch/cores/system"),
" ```",
"",
"A memory card from another device can be filled from a computer: name the "
"setup with `--platform` and the BIOS folder on the card with `--dest`. "
"For RetroArch on a Nintendo Switch:",
"Details: [All installer options](wiki/installer.md).",
"",
"```sh",
card,
"```",
"## Pick your setup",
"",
'<div class="grid cards" markdown>',
"",
]
for group, (label, _description) in DEVICE_GROUPS.items():
members = _group_keys(registry, group, display)
if not members:
continue
lines += [
f"- **{label}**",
"",
f" [{', '.join(display[k] for k in members)}](#{GROUP_ANCHORS[group]})",
"",
]
lines += ["</div>", ""]
for group, (label, description) in DEVICE_GROUPS.items():
members = _group_keys(registry, group, display)
if not members:
@@ -6188,60 +6230,57 @@ def generate_which_pack() -> str:
"",
description,
"",
"| Setup | What it is | Runs on | BIOS folder | Pack |",
"|-------|-----------|---------|-------------|------|",
"| Setup | Runs on | BIOS folder | ZIP |",
"|-------|---------|-------------|-----|",
]
for key in members:
guide = GUIDES[key]
folder = (f"Unzip into `{own_root[key]}`" if key in own_root
else _folder_cell(key, guide))
lines.append(
f"| [{display[key]}](platforms/{key}.md) | {guide.summary} | "
f"{_runs_on(registry[key], guide)} | {_folder_cell(key, guide)} | "
f"| [{display[key]}](platforms/{key}.md) | "
f"{_runs_on(registry[key], guide)} | {folder} | "
f"{_pack_cell(key, record)} |"
)
for setup, summary, uses, runs_on, folder in ALSO_RUNS.get(group, ()):
for setup, _summary, uses, runs_on, folder in ALSO_RUNS.get(group, ()):
lines.append(
f"| {setup} | {summary} It uses the [{display[uses]}](platforms/{uses}.md) "
f"files. | {runs_on} | {folder} | {_pack_cell(uses, record)} |"
f"| {setup} | {runs_on} | {folder}, same as "
f"[{display[uses]}](platforms/{uses}.md) | {_pack_cell(uses, record)} |"
)
lines.append("")
if group == "handheld":
lines += [
"Many other handheld systems start RetroArch for their games. On "
"them, the RetroArch files go in the folder below, and the command "
"finds it by itself.",
'??? note "Other handhelds that run RetroArch"',
"",
"| System | Runs | BIOS folder |",
"|--------|------|-------------|",
*(f"| {name} | [RetroArch](platforms/retroarch.md) | `{folder}` |"
" They use the RetroArch files, in the folder below. The command "
"finds it.",
"",
" | System | Runs | BIOS folder |",
" |--------|------|-------------|",
*(f" | {name} | [RetroArch](platforms/retroarch.md) | `{folder}` |"
for name, folder in _retroarch_handhelds(registry)),
"",
]
lines += [
"## Download a pack { #manual-download }",
"## Download a ZIP { #manual-download }",
"",
f"Each pack in the tables above comes from the [latest release]({RELEASE_URL}). "
"Download the pack of your setup and extract it into its BIOS folder. "
"This site can describe files newer than that release: a release is "
"published by hand, once its checks pass.",
"1. Find your setup in the tables above.",
"2. Download its ZIP. A big one comes in parts, like "
"`Pack.part1of2.zip` and `Pack.part2of2.zip`. Download every part.",
"3. Unzip every part into the folder named in the table.",
"",
"A pack over 2 GB comes in several parts (`PackName.part1of2.zip`, "
"`PackName.part2of2.zip`), and every part is needed. Each part is an "
"ordinary ZIP: extract them all into the same folder.",
"A row that says **Unzip into** has a ZIP that holds its own top folder. "
"Unzip it there, not into the BIOS folder.",
"",
*[line for note in extract_notes("platforms") for line in (note, "")],
"## Fewer files",
f"The ZIPs come from the [latest release]({RELEASE_URL}). The command may "
"install newer files.",
"",
"Each pack holds everything its setup runs: the BIOS list the setup "
"publishes, and every file its emulators load. The command can install "
"less: press **c** when it asks, or name the consoles with `--system`.",
"## Only some consoles?",
"",
"Details: [All installer options](wiki/installer.md#choosing-what-to-install). "
"Reference: [Build your own packs](wiki/advanced-usage.md).",
"Run the command and press **c** when it asks.",
"",
*_next_block([
"[First install, step by step](wiki/first-install.md)",
"The page of your setup, from the tables above: where the files go and "
"how to check them",
"[When a game does not start](wiki/troubleshooting.md)",
]),
]
@@ -6349,6 +6388,7 @@ def generate_mkdocs_nav(
]},
{"Your setup": _platform_nav(coverages, registry or {})},
{"Explore": [
"explore.md",
{"Systems": _system_nav(manufacturers)},
{"Emulators": _emulator_nav(profiles)},
{"All installer options": "wiki/installer.md"},
@@ -6357,7 +6397,7 @@ def generate_mkdocs_nav(
"wiki/index.md",
{"How each platform checks files": "wiki/verification-modes.md"},
{"What the packs do not hold": "gaps.md"},
{"Cross-reference": "cross-reference.md"},
{"Cores by platform": "cross-reference.md"},
{"Dump provenance": "provenance.md"},
{"Updated platform files": "exports.md"},
{"Data and API": "data.md"},
@@ -6502,6 +6542,8 @@ def _generate(args: argparse.Namespace) -> None:
)
)
write_if_changed(str(docs / "explore.md"), generate_explore_index(db, coverages, profiles))
stats = compute_stats(db, coverages, profiles)
stats["composition"] = compute_composition(db)
write_if_changed(str(docs / "stats.json"), generate_stats(stats))
+148 -124
View File
@@ -21,25 +21,23 @@ INSTALL_BASE = "https://raw.githubusercontent.com/Abdess/retrobios/main"
DEVICE_GROUPS: dict[str, tuple[str, str]] = {
"computer": (
"Computers and Steam Deck",
"An emulator app on a Windows, Mac or Linux computer, or on a Steam Deck.",
"An emulator app on a computer or a Steam Deck.",
),
"gaming-os": (
"Game systems for PCs and small boards",
"A system that starts straight into a game menu, installed on a PC, a "
"Raspberry Pi or another single-board computer.",
"A system that starts straight into a game menu, on a PC or a Raspberry Pi.",
),
"handheld": (
"Handhelds and TV boxes",
"A handheld console or a TV box, and the system it starts when you turn it on.",
"The system your handheld or TV box starts.",
),
"fpga": (
"MiSTer FPGA",
"A MiSTer board, which recreates console hardware in a chip instead of "
"emulating it in software.",
"A board that recreates old consoles in hardware, not in software.",
),
"server": (
"Self-hosted game library",
"A RomM server, which plays your games in a web browser.",
"A RomM server. It plays games in your web browser.",
),
}
@@ -72,43 +70,43 @@ ALSO_RUNS: dict[str, tuple[tuple[str, str, str, str, str], ...]] = {
TERMINALS: dict[str, tuple[str, str, str]] = {
"windows": (
"Windows",
"Open the Start menu, type **PowerShell** and open it. Paste the line "
"and press Enter.",
"Open the Start menu. Type **PowerShell** and open it. Paste the line. "
"Press Enter.",
"powershell",
),
"macos": (
"macOS",
"Open **Terminal**, in Applications then Utilities. Paste the line and "
"press Return.",
"Open **Terminal**, in Applications, then Utilities. Paste the line. "
"Press Return.",
"sh",
),
"linux": (
"Linux",
"Open a terminal. Paste the line and press Enter.",
"Open a terminal. Paste the line. Press Enter.",
"sh",
),
"steamdeck": (
"Steam Deck",
"Switch to Desktop Mode and open **Konsole**, the terminal. Paste the "
"line with Ctrl+Shift+V and press Enter.",
"Switch to Desktop Mode. Open **Konsole**. Paste with Ctrl+Shift+V. "
"Press Enter.",
"sh",
),
"android": (
"Android (Termux)",
"Install **Termux**, then run `pkg install python` and "
"`termux-setup-storage` once. Paste the line and press Enter.",
"Install **Termux**, a terminal app. Run `pkg install python` and "
"`termux-setup-storage` once. Then paste the line. Press Enter.",
"sh",
),
"device": (
"On the device",
"Open a terminal on the device, or connect to it from a computer on the "
"same network with SSH. Paste the line and press Enter.",
"Open a terminal on the device. Or connect from a computer with SSH, a "
"terminal over the network. Paste the line. Press Enter.",
"sh",
),
"server": (
"On the server",
"Open a terminal on the machine that holds the RomM library. Give "
"`--dest` the `bios` folder of that library.",
"Open a terminal on the machine that holds your RomM library. Put the "
"path of its `bios` folder after `--dest`.",
"sh",
),
}
@@ -119,7 +117,14 @@ CARD_TERMINALS = ("macos", "linux", "windows")
@dataclass(frozen=True)
class Guide:
"""What a platform page says before its file reference."""
"""What a platform page says before its file reference.
`folders` are the common cases, `more_folders` the rarer builds shown
folded under them. `notes` stay in the guide, one plain fact each.
`reference_notes` carry the source citations and the details a reader
checking the work wants, and open the Reference part of the page.
`download_hint` is one more bullet under the pack download.
"""
summary: str
folders: tuple[tuple[str, str], ...]
@@ -128,79 +133,82 @@ class Guide:
dest: str = ""
on_card: bool = False
extra_args: dict[str, str] = field(default_factory=dict)
more_folders: tuple[tuple[str, str], ...] = ()
reference_notes: tuple[str, ...] = ()
download_hint: str = ""
NETWORK_HINT = "From a computer, use the network folder above."
GUIDES: dict[str, Guide] = {
"retroarch": Guide(
summary="A multi-system emulator. It loads a different core for each console.",
summary="An app that runs many consoles. It uses a different **core**, "
"a small emulator, for each one.",
folders=(
("Windows, installer or portable build", "`system\\` next to `retroarch.exe`"),
("Windows, Microsoft Store build",
"`%LOCALAPPDATA%\\Packages\\1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_*\\LocalState\\system\\`"),
("Windows", "`system\\` next to `retroarch.exe`"),
("macOS", "`~/Documents/RetroArch/system/`"),
("macOS, Steam or portable build", "`~/Library/Application Support/RetroArch/system/`"),
("Linux", "`~/.config/retroarch/system/`"),
("Linux Flatpak and Steam Deck", "`~/.var/app/org.libretro.RetroArch/config/retroarch/system/`"),
("Steam Deck and Linux Flatpak", "`~/.var/app/org.libretro.RetroArch/config/retroarch/system/`"),
("Android", "`RetroArch/system/` on the internal storage"),
("Nintendo Switch", "`retroarch/cores/system/` on the SD card"),
),
more_folders=(
("Windows, Microsoft Store build",
"`%LOCALAPPDATA%\\Packages\\1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_*\\LocalState\\system\\`"),
("macOS, Steam or portable build", "`~/Library/Application Support/RetroArch/system/`"),
),
run=("windows", "macos", "linux", "steamdeck", "android"),
notes=(
"RetroArch shows the folder it reads in **Settings > Directory > "
"System/BIOS**. The same value is the `system_directory` line of "
"`retroarch.cfg`.",
"**Not sure?** In RetroArch, open **Settings > Directory > "
"System/BIOS**. It shows the folder.",
),
reference_notes=(
"RetroArch reads its BIOS folder from the `system_directory` line of "
"`retroarch.cfg`, the value **Settings > Directory > System/BIOS** "
"shows.",
"On Windows, `retroarch.cfg` is read next to `retroarch.exe` first, "
"then from `%APPDATA%\\RetroArch\\`. A `system_directory` left at its "
"default is the `system` folder next to `retroarch.exe` (`:\\system` "
"in RetroArch's `frontend/drivers/platform_win32.c`, where `:` stands "
"for the folder of the program).",
"LaunchBox and other launchers that start RetroArch use the folder "
"RetroArch itself reads.",
),
),
"batocera": Guide(
summary="A system that starts straight into a game menu. It runs on PCs, "
"Raspberry Pi boards and handhelds.",
summary="A system that starts straight into a game menu.",
folders=(
("On Batocera", "`/userdata/bios/`"),
("From Windows, over the network", "`\\\\BATOCERA\\share\\bios\\`"),
("From macOS or Linux, over the network", "`smb://batocera/share/bios/`"),
("From Windows, over your home network", "`\\\\BATOCERA\\share\\bios\\`"),
("From a Mac or Linux, over your home network", "`smb://batocera/share/bios/`"),
),
run=("device",),
notes=(
"From a computer on the same network, the network folder is the "
"easiest way in: download the pack and extract it there.",
),
download_hint=NETWORK_HINT,
),
"recalbox": Guide(
summary="A system that starts straight into a game menu and sets up its "
"emulators itself.",
"emulators.",
folders=(
("On Recalbox", "`/recalbox/share/bios/`"),
("From Windows, over the network", "`\\\\RECALBOX\\share\\bios\\`"),
("From Windows, over your home network", "`\\\\RECALBOX\\share\\bios\\`"),
),
run=("device",),
notes=(
"From a computer on the same network, download the pack and extract "
"it into the network folder.",
),
download_hint=NETWORK_HINT,
),
"retrobat": Guide(
summary="A Windows frontend built on EmulationStation. It comes with "
"RetroArch and standalone emulators.",
summary="A game menu for Windows. It comes with RetroArch and other "
"emulators.",
folders=(
("Inside the RetroBat folder", "`bios\\`"),
("Default install", "`C:\\RetroBat\\bios\\`"),
),
run=("windows",),
notes=(
notes=("The command finds RetroBat wherever you installed it.",),
reference_notes=(
"RetroBat records its own folder each time it starts, and the "
"installer reads it there, wherever RetroBat was installed.",
"installer reads it there.",
),
),
"emudeck": Guide(
summary="Installs and sets up many emulators, and adds each game to the "
"Steam library.",
summary="Installs your emulators and adds your games to Steam.",
folders=(
("Steam Deck and Linux", "`~/Emulation/bios/`"),
("Windows, by default", "`C:\\Emulation\\bios\\`"),
@@ -208,22 +216,24 @@ GUIDES: dict[str, Guide] = {
),
run=("steamdeck", "linux", "windows", "macos"),
notes=(
"The `bios` folder sits inside the Emulation folder chosen in "
"EmuDeck. When EmuDeck was set up on an SD card, that folder is on "
"the card. The installer reads the location from EmuDeck's settings "
"The `bios` folder is inside the Emulation folder you chose in "
"EmuDeck. If you chose the SD card, it is on the card.",
),
reference_notes=(
"The installer reads the Emulation folder from EmuDeck's settings "
"(`emulationPath` in `~/.config/EmuDeck/settings.sh`).",
),
),
"lakka": Guide(
summary="A system built around RetroArch. It starts straight into the "
"RetroArch menu.",
summary="A system that starts straight into RetroArch.",
folders=(("On Lakka", "`/storage/system/`"),),
run=("device",),
notes=("The folder is reachable over SSH and over the network share.",),
notes=("You can also reach the folder from a computer, over your home network.",),
reference_notes=("The folder is reachable over SSH and over the network share.",),
),
"retrodeck": Guide(
summary="One app with its emulators included, installed from Flathub "
"or Discover.",
summary="One app with its emulators inside. Install it from Flathub or "
"Discover.",
folders=(
("Steam Deck and Linux", "`~/retrodeck/bios/`"),
("Installed on a microSD card", "for example `/run/media/mmcblk0p1/retrodeck/bios/`"),
@@ -231,101 +241,111 @@ GUIDES: dict[str, Guide] = {
run=("steamdeck", "linux"),
),
"romm": Guide(
summary="A web-based game library. It plays games in the browser "
"through EmulatorJS.",
folders=(
("In the RomM library", "`bios/`, one subfolder per platform"),
summary="A game library on your own server. It plays games in the web "
"browser.",
folders=(("In the RomM library", "`bios/`, one subfolder per platform"),),
more_folders=(
("With `firmware: \"{platform}/bios\"` in `config.yml`",
"`<platform>/bios/` for each platform"),
),
run=("server",),
dest="/path/to/library/bios",
notes=("BIOS files can also be managed in RomM's web interface.",),
notes=("You can also add BIOS files in RomM's web page.",),
),
"rocknix": Guide(
summary="A Linux system for ARM and x86 handhelds, among them the "
"RG35XX (H700) and the RG552 (RK3399).",
summary="A system for handhelds.",
folders=(("On ROCKNIX", "`/storage/roms/bios/`"),),
run=("device",),
notes=("The folder is reachable over SSH and over the network share.",),
notes=("You can also reach the folder from a computer, over your home network.",),
reference_notes=(
"ROCKNIX runs on ARM and x86 handhelds, among them the RG35XX (H700) "
"and the RG552 (RK3399). The folder is reachable over SSH and over "
"the network share.",
),
),
"amberelec": Guide(
summary="A Linux system for Anbernic handhelds: RG351P, RG351V, "
"RG351MP, RG353, RG503 and RG552.",
summary="A system for Anbernic handhelds.",
folders=(("On AmberELEC", "`/storage/roms/`"),),
run=("device",),
notes=(
"The AmberELEC list names files under `/storage/roms`: `bios/` for "
"most, `neogeo/` for the rest. The pack holds those folders, so it "
"is extracted into `/storage/roms/` itself.",
reference_notes=(
"AmberELEC runs on the RG351P, RG351V, RG351MP, RG353, RG503 and "
"RG552. Its list names files under `/storage/roms`: `bios/` for most, "
"`neogeo/` for the rest. The pack holds those folders.",
),
),
"emuelec": Guide(
summary="A Linux system for Amlogic TV boxes, the Odroid Go Advance, "
"GameForce and RK356x devices.",
summary="A system for TV boxes and some handhelds.",
folders=(("On EmuELEC", "`/storage/roms/`"),),
run=("device",),
notes=(
"The EmuELEC list names files under `/storage/roms`: `bios/` for "
"most, `neogeo/` and `ports/` for the rest. The pack holds those "
"folders, so it is extracted into `/storage/roms/` itself.",
reference_notes=(
"EmuELEC runs on Amlogic TV boxes, the Odroid Go Advance, GameForce "
"and RK356x devices. Its list names files under `/storage/roms`: "
"`bios/` for most, `neogeo/` and `ports/` for the rest. The pack "
"holds those folders.",
),
),
"knulli": Guide(
summary="A system for handhelds based on Batocera, among them the "
"RG35XX family (H700) and the TrimUI Smart Pro.",
summary="A system for handhelds, based on Batocera.",
folders=(("On KNULLI", "`/userdata/bios/`"),),
run=("device",),
reference_notes=(
"KNULLI runs on handhelds among them the RG35XX family (H700) and the "
"TrimUI Smart Pro.",
),
),
"retropie": Guide(
summary="Scripts that install RetroArch and standalone emulators on "
"Raspberry Pi OS.",
summary="Installs RetroArch and other emulators on a Raspberry Pi.",
folders=(("On the Raspberry Pi", "`~/RetroPie/BIOS/`"),),
run=("device",),
notes=(
"Use the RetroPie pack, not the RetroArch one. RetroPie also "
"installs standalone emulators (openMSX, PCSX2, XRoar, Amiberry and "
"others) whose files only the RetroPie pack carries.",
"A stock RetroPie 4.8 image ships Python 3.7, older than the "
"installer needs. On it, download the RetroPie pack and extract it "
"into `~/RetroPie/BIOS/`.",
"Use the RetroPie pack, not the RetroArch one. On RetroPie 4.8, "
"Python is too old for the command: use the pack there.",
),
reference_notes=(
"RetroPie also installs standalone emulators (openMSX, PCSX2, XRoar, "
"Amiberry and others) whose files only the RetroPie pack carries.",
"RetroPie is archived in this project: its configuration is kept and "
"its pack is still built, but its upstream list is no longer read "
"again on a schedule.",
),
),
"bizhawk": Guide(
summary="A multi-system emulator with rerecording and debugging tools, "
"hosted by TASVideos.",
summary="An emulator app for many consoles, from TASVideos.",
folders=(
("Inside the BizHawk folder", "`Firmware\\`"),
("Default place the installer looks", "`%USERPROFILE%\\BizHawk\\Firmware\\`"),
),
run=("windows", "linux"),
reference_notes=("BizHawk carries rerecording and debugging tools.",),
),
"misterfpga": Guide(
summary="FPGA cores that recreate console hardware on a DE10-Nano board.",
summary="A board that recreates old consoles in hardware, not in software.",
folders=(("On the MiSTer SD card", "`/media/fat/games/`, one subfolder per core"),),
run=("device",),
notes=(
"Each file goes in the subfolder of its core, for example "
"`/media/fat/games/3DO/boot.rom`. Only the files the MiSTer BIOS "
"database lists with a download address are in the pack. The others "
"come with the MiSTer distribution and install themselves.",
"Each file goes in its console's folder, like "
"`/media/fat/games/3DO/boot.rom`.",
),
reference_notes=(
"MiSTer runs FPGA cores on a DE10-Nano board. Only the files the "
"MiSTer BIOS database lists with a download address are in the pack. "
"The others come with the MiSTer distribution and install themselves.",
),
),
"minui": Guide(
summary="A frontend for handhelds that runs from the SD card.",
summary="A simple game menu for handhelds. It runs from the SD card.",
folders=(("On the SD card", "`Bios/<TAG>/`, one folder per console"),),
run=CARD_TERMINALS,
on_card=True,
notes=(
"Each console has its own folder, named after the tag in its Roms "
"folder. `Roms/Sony PlayStation (PS)/` uses `Bios/PS/`.",
),
reference_notes=(
"MinUI gives each console its own folder, named after the tag in "
"parentheses of the Roms folder the game sits in. A game in "
"`Roms/Sony PlayStation (PS)/` runs with `Bios/PS/` as its BIOS "
"folder, one in `Roms/Game Boy (GB)/` with `Bios/GB/` (MinUI "
"`workspace/all/minarch/minarch.c:2943`). The pack holds the `Bios/` "
"folder: extract it at the root of the card.",
"`Roms/Game Boy (GB)/` runs with `Bios/GB/` as its BIOS folder (MinUI "
"`workspace/all/minarch/minarch.c:2943`).",
"Without `--platform`, the installer reads the release name MinUI "
"writes in `.system/version.txt` on the card. A console started by a "
"standalone program instead of a libretro core (DraStic for NDS, "
@@ -333,29 +353,33 @@ GUIDES: dict[str, Guide] = {
),
),
"nextui": Guide(
summary="A frontend for handhelds derived from MinUI. It runs from the SD card.",
summary="A game menu for handhelds, based on MinUI. It runs from the SD card.",
folders=(("On the SD card", "`Bios/<TAG>/`, one folder per console"),),
run=CARD_TERMINALS,
on_card=True,
notes=(
"Each console has its own folder, named after the tag in its Roms "
"folder. `Roms/Sony PlayStation (PS)/` uses `Bios/PS/`.",
),
reference_notes=(
"NextUI gives each console its own folder, named after the tag in "
"parentheses of the Roms folder the game sits in: `Bios/PS/` for "
"`Roms/Sony PlayStation (PS)/` (NextUI "
"`workspace/all/minarch/ma_core.c:76`). The pack holds the `Bios/` "
"folder: extract it at the root of the card.",
"parentheses of the Roms folder the game sits in (NextUI "
"`workspace/all/minarch/ma_core.c:76`).",
),
),
"myminui": Guide(
summary="A frontend for handhelds derived from MinUI. It runs from the SD card.",
summary="A game menu for handhelds, based on MinUI. It runs from the SD card.",
folders=(("On the SD card", "`Bios/<TAG>/`, one folder per console"),),
run=CARD_TERMINALS,
on_card=True,
notes=(
"Each console has its own folder, named after the tag in its Roms "
"folder. `Roms/Sony PlayStation (PS)/` uses `Bios/PS/`.",
),
reference_notes=(
"MyMinUI gives each console its own folder, named after the tag in "
"parentheses of the Roms folder the game sits in: `Bios/PS/` for "
"`Roms/Sony PlayStation (PS)/` (MyMinUI "
"`workspace/all/minarch/minarch.c:3532`). The pack holds the `Bios/` "
"folder: extract it at the root of the card.",
"parentheses of the Roms folder the game sits in (MyMinUI "
"`workspace/all/minarch/minarch.c:3532`).",
),
),
}
@@ -409,23 +433,23 @@ def platform_args(name: str, guide: Guide) -> dict[str, str]:
return args
TRUST_NOTE = (
"The line downloads a small script from this project on GitHub. The script "
"checks the installer against a fingerprint it carries, then the installer "
"downloads the files your setup needs and checks each one before saving it. "
"It writes only inside your BIOS folder. A BIOS file whose contents differ "
"from the expected one is replaced with a checked copy, and yours is kept in "
"a retrobios-backup folder beside it. Files it does not know are never moved "
"or deleted. Games and saves are never touched."
TRUST_ITEMS = (
"It checks each file before saving it.",
"It writes only in your BIOS folder.",
"It never touches games or saves.",
"It never moves or deletes files it does not know.",
"It keeps any file it replaces in a `retrobios-backup` folder, beside your "
"BIOS folder. The same command with `--restore` puts it back.",
)
def trust_box(prefix: str, indent: str = "") -> list[str]:
"""What the command does, under every command on a guide page."""
"""Is it safe, under every command on a guide page: five plain facts."""
return [
f'{indent}??? note "What this command does"',
f'{indent}??? note "Is it safe?"',
"",
f"{indent} {TRUST_NOTE} "
f"[Read the script first]({prefix}wiki/faq.md#is-piping-the-installer-into-a-shell-safe).",
*(f"{indent} - {item}" for item in TRUST_ITEMS),
"",
f"{indent} [Read the script first]({prefix}wiki/faq.md#is-piping-the-installer-into-a-shell-safe)",
"",
]