mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
The pack README's one-line install was written per platform: ROCKNIX and MiSTer had none, EmuDeck's Windows edition was told to open Konsole, and RomM users were sent to library/bios/ whatever their firmware template. The automatic section now follows each platform's install.detect systems, the EmuDeck and RomM manual steps name every layout, and the download page lists EmuDeck under Windows and macOS.
383 lines
16 KiB
Python
383 lines
16 KiB
Python
"""The notes shipped inside a pack.
|
|
|
|
A pack has to explain itself offline: what it holds, where each file
|
|
goes, and which ones the user has to rename."""
|
|
|
|
from __future__ import annotations
|
|
|
|
INSTALL_SH = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh"
|
|
INSTALL_PS1 = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1"
|
|
|
|
def _detect_systems(detect: list[dict]) -> list[str]:
|
|
"""The operating systems the registry's detection rules cover, in order."""
|
|
order = ("linux", "darwin", "windows")
|
|
seen = {str(rule.get("os", "")) for rule in detect}
|
|
return [name for name in order if name in seen]
|
|
|
|
|
|
def _automatic_section(
|
|
display: str, detect: list[dict], standalone_copies: list[dict] | None,
|
|
) -> str:
|
|
"""The one-line install, for every system the registry detects the
|
|
platform on. Written per platform by hand, it left ROCKNIX and MiSTer
|
|
without it and told EmuDeck's Windows edition to open Konsole."""
|
|
systems = _detect_systems(detect)
|
|
if not systems:
|
|
return ""
|
|
# A platform detected on Linux only is a system of its own (Batocera,
|
|
# ROCKNIX, MiSTer) or runs on one machine (RetroDECK, a RomM server):
|
|
# the command runs there.
|
|
on_device = systems == ["linux"]
|
|
lines = [
|
|
" Option A: Automatic (recommended)",
|
|
" ---------------------------------",
|
|
]
|
|
posix = [name for name in systems if name != "windows"]
|
|
if posix:
|
|
if on_device:
|
|
where = f"On the machine running {display} (its terminal, or SSH), run:"
|
|
else:
|
|
labels = {"linux": "Linux", "darwin": "macOS"}
|
|
where = f"{' and '.join(labels[name] for name in posix)}: run this in a terminal:"
|
|
lines += [f" {where}", "", f" curl -fsSL {INSTALL_SH} | sh", ""]
|
|
if "windows" in systems:
|
|
lines += [" Windows: run this in PowerShell:", "", f" irm {INSTALL_PS1} | iex", ""]
|
|
lines += [
|
|
f" The installer finds the {display} BIOS folder on this machine and",
|
|
" installs there, checking every file. --dest names another folder.",
|
|
"",
|
|
]
|
|
# On a desktop: an emulator folder under the home of a console's own
|
|
# system is not one its frontend reads.
|
|
if standalone_copies and not on_device:
|
|
lines += [
|
|
" Standalone emulators read some files from folders of their own",
|
|
" (Switch and 3DS keys, PlayStation and GameCube BIOS). To copy them",
|
|
" into the emulator folders the installer finds, add --standalone-copies:",
|
|
"",
|
|
]
|
|
if posix:
|
|
lines.append(f" curl -fsSL {INSTALL_SH} | sh -s -- --standalone-copies")
|
|
if "windows" in systems:
|
|
lines.append(f" &([scriptblock]::Create((irm {INSTALL_PS1}))) --standalone-copies")
|
|
lines.append("")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def _generic_manual(bios_paths: list[str]) -> str:
|
|
"""Where the files go, from the registry's paths, for a platform with no
|
|
manual guide of its own."""
|
|
targets = [path for path in bios_paths if path]
|
|
where = " or ".join(f"{path}/" for path in dict.fromkeys(targets)) or "your BIOS directory"
|
|
return (
|
|
" Option B: Manual\n"
|
|
" ----------------\n"
|
|
f" 1. Extract all files from this archive directly into {where}\n"
|
|
" 2. Overwrite if asked\n\n"
|
|
)
|
|
|
|
|
|
# What a manual install needs on each platform: its shares, cards and folders.
|
|
MANUAL_GUIDES: dict[str, str] = {
|
|
"retroarch": (
|
|
" Option B: Manual (PC)\n"
|
|
" ---------------------\n"
|
|
" 1. Find your RetroArch system directory:\n"
|
|
" - RetroArch > Settings > Directory > System/BIOS\n"
|
|
" - Default: retroarch/system/\n"
|
|
" 2. Extract all files from this archive directly into your system directory\n"
|
|
" 3. Overwrite if asked\n\n"
|
|
" Option C: Manual (handheld / SD card)\n"
|
|
" -------------------------------------\n"
|
|
" Anbernic, Retroid, Miyoo, Trimui, etc.:\n"
|
|
" 1. Connect your SD card to your PC\n"
|
|
" 2. Find the BIOS folder (usually BIOS/ or system/)\n"
|
|
" 3. Extract all files from this archive directly into that folder\n"
|
|
" 4. Eject SD card and reboot your device\n\n"
|
|
" Common paths by device:\n"
|
|
" Anbernic (ArkOS/JELOS): BIOS/\n"
|
|
" Retroid (RetroArch): RetroArch/system/\n"
|
|
" Miyoo Mini (Onion OS): BIOS/\n"
|
|
" Steam Deck (RetroArch): ~/.config/retroarch/system/\n\n"
|
|
),
|
|
"batocera": (
|
|
" The terminal is F1 from the Batocera menu, or SSH.\n\n"
|
|
" Option B: Manual (network share)\n"
|
|
" --------------------------------\n"
|
|
" 1. On your PC, open the Batocera network share:\n"
|
|
" - Windows: \\\\BATOCERA\\share\\bios\\\n"
|
|
" - Mac/Linux: smb://batocera/share/bios/\n"
|
|
" 2. Extract all files from this archive directly into the share\n"
|
|
" 3. Overwrite if asked\n\n"
|
|
" Option C: Manual (SD card)\n"
|
|
" --------------------------\n"
|
|
" 1. Put the SD card in your PC\n"
|
|
" 2. Navigate to /userdata/bios/ on the SHARE partition\n"
|
|
" 3. Extract all files from this archive directly into that folder\n\n"
|
|
" NOTE: Dreamcast flash memory is named dc_nvmem.bin\n"
|
|
" (if your setup asks for dc_flash.bin, same file).\n\n"
|
|
),
|
|
"recalbox": (
|
|
" Option B: Manual (network share)\n"
|
|
" --------------------------------\n"
|
|
" 1. On your PC, open the Recalbox network share:\n"
|
|
" - Windows: \\\\RECALBOX\\share\\bios\\\n"
|
|
" - Mac/Linux: smb://recalbox/share/bios/\n"
|
|
" 2. Extract all files from this archive directly into the share\n\n"
|
|
" Option C: Manual (SD card)\n"
|
|
" --------------------------\n"
|
|
" 1. Put the SD card in your PC\n"
|
|
" 2. Navigate to /recalbox/share/bios/\n"
|
|
" 3. Extract all files from this archive directly into that folder\n\n"
|
|
),
|
|
"emudeck": (
|
|
" Option B: Manual\n"
|
|
" ----------------\n"
|
|
" Extract all files from this archive directly into EmuDeck's bios\n"
|
|
" folder, inside the Emulation folder chosen in EmuDeck:\n"
|
|
" Steam Deck, Linux and macOS: ~/Emulation/bios/ by default\n"
|
|
" Windows: C:\\Emulation\\bios\\ by default\n\n"
|
|
),
|
|
"retrodeck": (
|
|
" Option B: Manual\n"
|
|
" ----------------\n"
|
|
" 1. Open Dolphin file manager\n"
|
|
" 2. Show hidden files (Ctrl+H)\n"
|
|
" 3. Navigate to ~/retrodeck/\n"
|
|
" 4. Extract all files from this archive directly into ~/retrodeck/\n"
|
|
" (the archive already holds the bios/ and roms/ folders)\n\n"
|
|
" NOTE: RetroDECK uses its own BIOS checker. After\n"
|
|
" copying, open RetroDECK > Tools > BIOS Checker to\n"
|
|
" verify everything is detected.\n\n"
|
|
),
|
|
"retrobat": (
|
|
" Option B: Manual\n"
|
|
" ----------------\n"
|
|
" 1. Open your RetroBat installation folder\n"
|
|
" 2. Navigate to the bios\\ subfolder\n"
|
|
" (default: C:\\RetroBat\\bios\\)\n"
|
|
" 3. Extract all files from this archive directly into your bios\\ folder\n"
|
|
" 4. Overwrite if asked\n\n"
|
|
),
|
|
"bizhawk": (
|
|
" Option B: Manual\n"
|
|
" ----------------\n"
|
|
" 1. Open the folder you extracted BizHawk to, the one holding\n"
|
|
" EmuHawk.exe (Windows) or EmuHawkMono.sh (Linux)\n"
|
|
" 2. Navigate to its Firmware subfolder\n"
|
|
" 3. Extract all files from this archive directly into your Firmware folder\n"
|
|
" 4. In BizHawk: Config > Paths > Firmware should\n"
|
|
" point to this folder\n\n"
|
|
),
|
|
# RomM 5.4 places firmware by filesystem.structure.firmware in config.yml,
|
|
# bios/{platform} by default, and suggests {platform}/bios for a library
|
|
# laid out as {platform}/roms (rommapp/romm 68f71259,
|
|
# backend/config/config_manager.py:58,101-109,1105-1124).
|
|
"romm": (
|
|
" Option B: Manual\n"
|
|
" ----------------\n"
|
|
" The archive holds one folder per RomM platform (psx/, gba/, ...).\n"
|
|
" Where they go depends on filesystem.structure.firmware in RomM's\n"
|
|
" config.yml, read relative to the library folder:\n\n"
|
|
" - Not set, or \"bios/{platform}\" (the default): extract all files\n"
|
|
" from this archive directly into library/bios/\n"
|
|
" - \"{platform}/bios\" (the layout RomM suggests for a library kept as\n"
|
|
" {platform}/roms): each folder goes under its platform, psx/ into\n"
|
|
" library/psx/bios/, gba/ into library/gba/bios/, and so on.\n"
|
|
" The installer reads config.yml and does this itself. Run from\n"
|
|
" the host of a container, --romm-firmware \"{platform}/bios\" says it.\n\n"
|
|
" Then restart the RomM service to detect new files.\n\n"
|
|
),
|
|
"retropie": (
|
|
" Option B: Via network share\n"
|
|
" --------------------------\n"
|
|
" 1. On your PC, open: \\\\RETROPIE\\bios\\\n"
|
|
" 2. Extract all files from this archive directly into that folder\n\n"
|
|
" Option C: Via SSH\n"
|
|
" -----------------\n"
|
|
" 1. SSH into your Pi: ssh pi@retropie\n"
|
|
" 2. Copy files to ~/RetroPie/BIOS/\n\n"
|
|
" NOTE: the BIOS folder is on the card's Linux partition,\n"
|
|
" which Windows and macOS cannot open: copying through the\n"
|
|
" card works from a Linux computer only\n"
|
|
" (<rootfs>/home/pi/RetroPie/BIOS/).\n\n"
|
|
),
|
|
"lakka": (
|
|
" Lakka reads its BIOS files from /storage/system/.\n\n"
|
|
" Option B: Via network share\n"
|
|
" --------------------------\n"
|
|
" 1. On Lakka: Settings > Services > Samba Enable\n"
|
|
" 2. On your PC, open the System share: \\\\LAKKA\\System\\\n"
|
|
" (macOS and Linux: smb://lakka/System)\n"
|
|
" 3. Extract all files from this archive directly into that share\n\n"
|
|
" Option C: Via SSH\n"
|
|
" -----------------\n"
|
|
" 1. On Lakka: Settings > Services > SSH Enable\n"
|
|
" 2. Copy the files to /storage/system/ (scp -r * root@lakka:/storage/system/)\n\n"
|
|
),
|
|
}
|
|
|
|
|
|
def _build_readme(
|
|
platform_name: str,
|
|
platform_display: str,
|
|
base_dest: str,
|
|
total_files: int,
|
|
num_systems: int,
|
|
source: str = "full",
|
|
contributors: list[dict] | None = None,
|
|
regions: list[str] | None = None,
|
|
fallback_systems: list[str] | None = None,
|
|
one_per_slot: bool = False,
|
|
undecidable_slots: list[str] | None = None,
|
|
narrowings: list[tuple[str, str]] | None = None,
|
|
system_filter: list[str] | None = None,
|
|
detect: list[dict] | None = None,
|
|
standalone_copies: list[dict] | None = None,
|
|
) -> str:
|
|
"""Build a personalized step-by-step README for each platform pack."""
|
|
narrowings = narrowings or []
|
|
sep = "=" * 50
|
|
header = (
|
|
f"{sep}\n"
|
|
f" RETROBIOS - {platform_display} BIOS Pack\n"
|
|
f" {total_files} files for {num_systems} systems\n"
|
|
f"{sep}\n\n"
|
|
)
|
|
|
|
detect = list(detect or [])
|
|
bios_paths = [str(rule.get("bios_path", "")) for rule in detect]
|
|
guide = (
|
|
"INSTALLATION GUIDE\n\n"
|
|
+ _automatic_section(platform_display, detect, standalone_copies)
|
|
+ MANUAL_GUIDES.get(platform_name, _generic_manual(bios_paths))
|
|
)
|
|
|
|
if regions:
|
|
region_help = (
|
|
" - Wrong region? This pack was filtered. Only the\n"
|
|
" best-matching BIOS was kept per system. Use the\n"
|
|
" unfiltered pack to play imports.\n"
|
|
)
|
|
else:
|
|
region_help = (
|
|
" - Wrong region? Some systems have regional BIOS\n"
|
|
" variants (USA/EUR/JAP). All are included.\n"
|
|
)
|
|
footer = (
|
|
"TROUBLESHOOTING\n\n"
|
|
" - Core says BIOS missing? Check the exact filename\n"
|
|
" and make sure it's in the right subfolder.\n"
|
|
f"{region_help}"
|
|
" - Need help? https://github.com/Abdess/retrobios/issues\n\n"
|
|
f"{sep}\n"
|
|
f" https://github.com/Abdess/retrobios\n"
|
|
f"{sep}\n"
|
|
)
|
|
|
|
source_info = ""
|
|
if source == "platform":
|
|
source_info = (
|
|
"PACK TYPE: Platform Only\n\n"
|
|
f" This pack contains only files declared by {platform_display}.\n"
|
|
" Core extras from emulator profiles are not included.\n"
|
|
" Use the Full pack for maximum coverage.\n\n"
|
|
)
|
|
elif source == "truth":
|
|
source_info = (
|
|
"PACK TYPE: Ground Truth\n\n"
|
|
" This pack contains files that emulators actually load,\n"
|
|
" based on source code analysis of emulator profiles.\n"
|
|
" Independent of platform scraper accuracy.\n\n"
|
|
)
|
|
|
|
region_info = ""
|
|
if regions:
|
|
pretty = ", ".join(
|
|
" ".join(w.title() for w in slug.split("-")) for slug in regions
|
|
)
|
|
region_info = (
|
|
"PACK TYPE: Region Filtered\n\n"
|
|
f" Region priority: {pretty}\n\n"
|
|
" Only the best-matching BIOS was kept for each system.\n"
|
|
)
|
|
if fallback_systems:
|
|
listed = "\n".join(f" {_group_label(s)}" for s in fallback_systems)
|
|
region_info += (
|
|
"\n These systems have no BIOS in those regions, so all\n"
|
|
" of theirs were kept:\n"
|
|
f"{listed}\n"
|
|
)
|
|
region_info += (
|
|
"\n This shrinks the pack. It does not change how cores pick\n"
|
|
" a BIOS: most already select per region from fixed filename\n"
|
|
" lists driven by the game's region. Loading imports from\n"
|
|
" another region may need the unfiltered pack.\n\n"
|
|
)
|
|
|
|
slot_info = ""
|
|
if one_per_slot:
|
|
slot_info = (
|
|
" Where a core declares which BIOS it prefers, only that one was\n"
|
|
" kept for each system and region. Systems whose cores declare no\n"
|
|
" order keep all of theirs.\n"
|
|
)
|
|
if undecidable_slots:
|
|
slot_info += (
|
|
f" {len(undecidable_slots)} slot(s) had no declared order.\n"
|
|
)
|
|
slot_info += "\n"
|
|
|
|
narrowed = ""
|
|
labels = [label for _tag, label in narrowings]
|
|
if system_filter:
|
|
labels.append(f"systems {', '.join(system_filter)}")
|
|
if labels:
|
|
listed = "".join(f" {label}\n" for label in labels)
|
|
narrowed = (
|
|
"PACK TYPE: Narrowed\n\n"
|
|
" This pack holds fewer files than the full one:\n"
|
|
f"{listed}"
|
|
"\n The unfiltered pack is the one to use when in doubt.\n\n"
|
|
)
|
|
|
|
credits = ""
|
|
if contributors:
|
|
credits = "\nCONTRIBUTORS\n\n"
|
|
for cb in contributors:
|
|
username = cb.get("username", "")
|
|
credits += f" @{username}\n"
|
|
credits += "\n"
|
|
|
|
return (
|
|
header + narrowed + source_info + region_info + slot_info
|
|
+ guide + credits + footer
|
|
)
|
|
|
|
def _build_agnostic_rename_readme(
|
|
destination: str,
|
|
original: str,
|
|
alternatives: list[str],
|
|
) -> str:
|
|
"""Build a README explaining an agnostic file rename."""
|
|
lines = [
|
|
"This file was renamed for compatibility:",
|
|
f" {destination} <- {original}",
|
|
"",
|
|
]
|
|
if alternatives:
|
|
lines.append("All variants included in this pack:")
|
|
for alt in sorted(alternatives):
|
|
lines.append(f" {alt}")
|
|
lines.append("")
|
|
lines.append(f"To use a different variant, rename it to: {destination}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def _group_label(group_id: str) -> str:
|
|
"""A region group as a reader names it: the system, or one core's slot."""
|
|
emulator, kind, rest = (group_id.split(":", 2) + ["", ""])[:3]
|
|
if kind == "variant" and rest:
|
|
return f"{rest} ({emulator})"
|
|
return group_id
|