mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
minarch hands every core SDCARD_PATH/Bios/<TAG> as its system directory and opens no file in it. The scrapers read each pak's launch.sh (core, tag, devices), each device's core makefile (the project a library is built from: MyMinUI's dosbox_libretro.so is dosbox-pure) and the minarch statement, at a pinned commit. The installer recognises the card from .system/version.txt and refuses a folder inside it.
436 lines
18 KiB
Python
436 lines
18 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}
|
|
if "any" in seen:
|
|
return list(order)
|
|
return [name for name in order if name in seen]
|
|
|
|
|
|
def _card_section(platform_name: str, display: str) -> str:
|
|
"""The one-line install for a frontend whose files live on its SD card.
|
|
|
|
The installer runs on the computer the card is mounted on, and the card
|
|
root is named: no folder on that computer is one the frontend reads.
|
|
"""
|
|
return (
|
|
" Option A: Automatic (recommended)\n"
|
|
" ---------------------------------\n"
|
|
" With the SD card mounted on a computer, name its root with --dest.\n"
|
|
" Linux and macOS: run this in a terminal:\n\n"
|
|
f" curl -fsSL {INSTALL_SH} | sh -s -- --platform {platform_name} --dest /path/to/card\n\n"
|
|
" Windows: run this in PowerShell (E: being the card):\n\n"
|
|
f" &([scriptblock]::Create((irm {INSTALL_PS1}))) --platform {platform_name} --dest E:\\\n\n"
|
|
f" The installer writes each file where {display} hands it to the core\n"
|
|
" and checks every file.\n\n"
|
|
)
|
|
|
|
|
|
def _card_manual(display: str) -> str:
|
|
"""Where the files of a per-tag Bios/ layout go, copied by hand."""
|
|
return (
|
|
" Option B: Manual\n"
|
|
" ----------------\n"
|
|
" 1. Put the SD card in your PC\n"
|
|
" 2. Extract all files from this archive directly into the root of the\n"
|
|
" card: the archive holds the Bios/ folder, with one subfolder per\n"
|
|
" launch tag (Bios/PS/, Bios/GBA/, ...)\n"
|
|
" 3. Overwrite if asked\n\n"
|
|
f" {display} hands each core the folder named after the tag in\n"
|
|
" parentheses of its Roms folder (\"Sony PlayStation (PS)\" reads\n"
|
|
" Bios/PS/). A Roms folder can be renamed as long as the tag stays.\n\n"
|
|
)
|
|
|
|
|
|
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,
|
|
launch_folders: int = 0,
|
|
) -> str:
|
|
"""Build a personalized step-by-step README for each platform pack.
|
|
|
|
``launch_folders`` counts the folders a platform that hands each core
|
|
its own system directory gives them, where it names no system.
|
|
"""
|
|
narrowings = narrowings or []
|
|
sep = "=" * 50
|
|
scope = (
|
|
f"{launch_folders} launch folders" if launch_folders and not num_systems
|
|
else f"{num_systems} systems"
|
|
)
|
|
header = (
|
|
f"{sep}\n"
|
|
f" RETROBIOS - {platform_display} BIOS Pack\n"
|
|
f" {total_files} files for {scope}\n"
|
|
f"{sep}\n\n"
|
|
)
|
|
|
|
detect = list(detect or [])
|
|
bios_paths = [str(rule.get("bios_path", "")) for rule in detect]
|
|
if any(rule.get("method") == "card_marker" for rule in detect):
|
|
guide = (
|
|
"INSTALLATION GUIDE\n\n"
|
|
+ _card_section(platform_name, platform_display)
|
|
+ _card_manual(platform_display)
|
|
)
|
|
else:
|
|
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
|