chore: merge main into Sony pages

This commit is contained in:
Abdessamad Derraz committed 2026-11-04 05:11:58 +01:00
commit 0bd206da4d
410 files changed
+146998 -134303

No files matched your search

+6
View File
@@ -25,3 +25,9 @@ os.environ.setdefault(
(Path(__file__).resolve().parent.parent / "tmp" / "tests").mkdir(
parents=True, exist_ok=True
)
# The installer's cache (the games index, what reading a card found) is the
# suite's own: a run never reads nor fills the one of the person running it.
os.environ.setdefault(
"RETROBIOS_CACHE",
str(Path(__file__).resolve().parent.parent / "tmp" / "tests" / "cache"),
)
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0"?>
<!DOCTYPE mame [
<!ELEMENT mame (machine+)>
]>
<mame build="0.000 (fixture)">
<machine name="neogeo" sourcefile="neogeo/neogeo.cpp" isbios="yes">
<description>Neo-Geo</description>
<rom name="sp-s2.sp1" size="131072" crc="9036d879" sha1="4f5ed7105b7128794654ce82b51723e16e389543"/>
<device_ref name="ym2610"/>
</machine>
<machine name="naomi" sourcefile="sega/naomi.cpp" isbios="yes">
<description>Naomi</description>
<rom name="epr-21576h.ic27" size="2097152" crc="d4895685"/>
</machine>
<machine name="naomigd" sourcefile="sega/naomi.cpp" isbios="yes" romof="naomi">
<description>Naomi GD-ROM</description>
<rom name="epr-21576h.ic27" merge="epr-21576h.ic27" size="2097152" crc="d4895685"/>
</machine>
<machine name="mslug" sourcefile="neogeo/neogeo.cpp" romof="neogeo">
<description>Metal Slug</description>
<rom name="201-p1.p1" size="2097152" crc="08d8daa5"/>
<device_ref name="ym2610"/>
<device_ref name="m68705p3"/>
</machine>
<machine name="mslugh" sourcefile="neogeo/neogeo.cpp" cloneof="mslug" romof="mslug">
<description>Metal Slug (clone)</description>
<rom name="201-p1h.p1" size="2097152" crc="11111111"/>
<device_ref name="ym2610"/>
</machine>
<machine name="ikaruga" sourcefile="sega/naomi.cpp" romof="naomigd">
<description>Ikaruga</description>
<rom name="317-0323-jpn.pic" size="7665" crc="22222222"/>
<disk name="gdl-0010" sha1="0000000000000000000000000000000000000000"/>
</machine>
<machine name="orphan" sourcefile="misc/x.cpp" romof="gonebios">
<description>Orphan</description>
<rom name="o.bin" size="16" crc="33333333"/>
</machine>
<machine name="pacman" sourcefile="namco/pacman.cpp">
<description>Pac-Man</description>
<rom name="pacman.6e" size="4096" crc="c1e6ab10"/>
<device_ref name="ym2610"/>
</machine>
<machine name="nodumponly" sourcefile="misc/x.cpp">
<description>Nothing dumped</description>
<rom name="u1" size="16" status="nodump"/>
<disk name="d" status="nodump"/>
</machine>
<machine name="ym2610" sourcefile="sound/ym2610.cpp" isdevice="yes" runnable="no">
<description>YM2610</description>
</machine>
<machine name="m68705p3" sourcefile="cpu/m6805.cpp" isdevice="yes" runnable="no">
<description>M68705P3</description>
<rom name="m68705p3.bin" size="2048" crc="44444444"/>
</machine>
</mame>
+99
View File
@@ -0,0 +1,99 @@
// Fixture in the shape of flycast core/hw/naomi/naomi_roms.cpp
const BIOS_t BIOS[] =
{
{
"naomi",
{
{ 1, "epr-21576h.ic27", 0x000000, 0x200000, 0xd4895685 },
}
},
{
"naomi-jp-d",
{
{ 0, "epr-21576d.ic27", 0x000000, 0x200000, 0x3b2afa7b },
},
"naomi"
},
{
"awbios",
{
/* "commented", { 9, "x" } */
{ 1, "bios0.ic23", 0x000000, 0x020000, 0x719b2b0b },
}
},
{ nullptr }
};
const Game Games[] =
{
// "not a game", nullptr,
{
"gram2000",
nullptr,
"Giant Gram 2000",
0x0b000000,
0x7f805c3f,
nullptr,
M1,
ROT0,
{
{ "epr-23377.ic11", 0x0000000, 0x0400000, 0x4ca3149c },
},
nullptr,
&giant_gram_inputs,
},
{
"kofnw",
nullptr,
"The King of Fighters Neowave",
0x08000000,
0,
"awbios",
AW,
ROT0,
{
{ "ax2201p01.ic12", 0x0000000, 0x0800000, 0xb1fff0c8 },
},
},
{
"jpgame",
"gram2000",
"Japan only, a brace } in a string",
0x08000000,
0,
"naomi-jp-d",
M1,
ROT0,
{
{ "x.ic11", 0x0000000, 0x0800000, 0x00000001 },
},
},
{
"ikaruga",
nullptr,
"Ikaruga",
0x8000000,
0,
nullptr,
GD,
ROT270,
{
{ "317-0323-jpn.pic", 0, 0x4000 },
},
"ikaruga",
},
{
"bad",
nullptr,
"Unknown BIOS name",
0x8000000,
0,
"nosuchbios",
M1,
ROT0,
{
{ "b.ic11", 0, 0x4000 },
},
},
{ nullptr }
};
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<!DOCTYPE mame [
<!ELEMENT mame (game+)>
]>
<mame build="0.78 (fixture)">
<game name="kof98" sourcefile="neogeo.c" romof="neogeo">
<description>KOF 98</description>
<rom name="242-p1.bin" size="2097152" crc="61ac868a"/>
</game>
<game runnable="no" name="neogeo">
<description>Neo-Geo</description>
<rom name="neo-geo.rom" size="131072" crc="9036d879"/>
</game>
<game runnable="no" name="psarc95x">
<description>A root no game names</description>
<rom name="x.bin" size="16" crc="55555555"/>
</game>
<game name="brvblade" sourcefile="zn.c" romof="psarc95">
<description>Brave Blade</description>
<rom name="b.bin" size="16" crc="66666666"/>
</game>
</mame>
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<datafile><header><name>Ignored - System</name><version>1</version></header>
<game name="X (USA)"><rom name="x.bin" size="1" crc="00000001" sha1="6666666666666666666666666666666666666666"/></game>
</datafile>
@@ -0,0 +1,27 @@
<?xml version="1.0"?>
<datafile>
<header>
<name>Nintendo - Super Nintendo Entertainment System</name>
<version>20260707-000000</version>
</header>
<game name="Zeta Quest (USA)">
<description>Zeta Quest (USA)</description>
<rom name="Zeta Quest (USA).sfc" size="1048576" crc="0000BEEF" md5="00000000000000000000000000000000" sha1="1111111111111111111111111111111111111111"/>
</game>
<game name="Alpha Run (Japan)">
<description>Alpha Run (Japan)</description>
<rom name="Alpha Run (Japan).sfc" size="524288" crc="0000CAFE" sha1="2222222222222222222222222222222222222222"/>
</game>
<game name="Both Worlds (USA, Europe) (Rev 1)">
<description>Both Worlds (USA, Europe) (Rev 1)</description>
<rom name="Both Worlds (USA, Europe) (Rev 1).sfc" size="524288" crc="0000CAFE" sha1="3333333333333333333333333333333333333333"/>
</game>
<game name="Everywhere (World)">
<description>Everywhere (World)</description>
<rom name="Everywhere (World).sfc" size="262144" crc="12345678" sha1="4444444444444444444444444444444444444444"/>
</game>
<game name="Untagged Demo">
<description>Untagged Demo</description>
<rom name="Untagged Demo.sfc" size="131072" crc="abcdef01" sha1="5555555555555555555555555555555555555555"/>
</game>
</datafile>
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<!DOCTYPE datafile PUBLIC "-//Logiqx//DTD ROM Management Datafile//EN" "http://www.logiqx.com/Dats/datafile.dtd">
<datafile>
<header>
<name>Sega - Mega CD &amp; Sega CD</name>
<version>2026-10-23 10-16-53</version>
</header>
<game name="Sonic CD (Europe)">
<serial>4407</serial>
<rom name="Sonic CD (Europe).cue" size="1000" crc="99999999" sha1="7777777777777777777777777777777777777777"/>
<rom name="Sonic CD (Europe) (Track 01).bin" size="129590496" crc="0060ac74" sha1="1ab058c8228af31a50def1b01f1f33961f2f23d7"/>
<rom name="Sonic CD (Europe) (Track 02).bin" size="1000" crc="88888888" sha1="8888888888888888888888888888888888888888"/>
</game>
<game name="Sonic CD (USA)">
<serial>4407, T-0000</serial>
<rom name="Sonic CD (USA).cue" size="1000" crc="99999990" sha1="9999999999999999999999999999999999999999"/>
<rom name="Sonic CD (USA) (Track 01).bin" size="129590496" crc="0060ac75" sha1="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"/>
</game>
<game name="Fresh Cleaner (Japan)">
<serial>M-20001</serial>
<rom name="Fresh Cleaner (Japan).cue" size="100" crc="11111110" sha1="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"/>
<rom name="Fresh Cleaner (Japan) (Track 1).bin" size="6644400" crc="020ca333" sha1="ba09041ae84fc5798a0c88b688ac0c7b9d42b873"/>
</game>
</datafile>
+9
View File
@@ -0,0 +1,9 @@
emulator: FDS Core
type: libretro
source: https://github.com/example/fdscore
source_commit: 2222222222222222222222222222222222222222
systems: [nintendo-nes, nintendo-fds]
files:
- name: disksys.rom
system: nintendo-fds
required: true
+18
View File
@@ -0,0 +1,18 @@
emulator: PSX Core
type: libretro
source: https://github.com/example/psxcore
source_commit: 1111111111111111111111111111111111111111
systems: [sony-playstation]
files:
- name: scph5500.bin
region: [japan]
required: false
- name: scph5501.bin
aliases: [scph7003.bin]
region: [north-america]
required: false
- name: scph5502.bin
region: [europe]
required: false
- name: openbios.bin
required: false
+8
View File
@@ -0,0 +1,8 @@
extras:
- system: nintendo-fds
profile: fdscore
trigger:
extensions: [.fds]
files: [disksys.rom]
required: true
source_ref: "example/fdscore@22222222 fds.c:5"
+18
View File
@@ -0,0 +1,18 @@
systems:
nintendo-fds:
readers:
fds-image:
location: {offset: 0, length: 14, encoding: ascii}
decode:
kind: exact-prefix
values:
- {value: "FDS\x1a", label: fds}
otherwise: unknown
sega-dreamcast:
readers:
dc-product-id:
location: {offset: 64, length: 10, encoding: ascii}
fields:
disk_type: {offset: 37, length: 6, encoding: ascii, mil_cd: MIL CD}
decode:
kind: serial
+20
View File
@@ -0,0 +1,20 @@
cases:
psx-override-bios:
title: Region-free override BIOS
reason: An option swaps in a region-free image for every region.
systems:
sony-playstation:
psxcore:
routes:
south-korea: japan
default_region: north-america
region_free: [openbios.bin]
source_ref: "example/psxcore@11111111 core.c:10-20"
keep_all_regions:
- case: psx-override-bios
condition: option
option: psxcore_override
default: disabled
keeps: region-free-files
files: [openbios.bin]
source_ref: "example/psxcore@11111111 core.c:30"
@@ -0,0 +1,3 @@
snes:
emulator: libretro
core: pocketsnes
@@ -0,0 +1 @@
configgen-defaults-bcm2835.yml
@@ -0,0 +1,9 @@
default:
emulator:
core:
snes:
emulator: libretro
core: snes9x
mame:
emulator: libretro
core: mame078plus
+25
View File
@@ -0,0 +1,25 @@
snes:
name: Super Nintendo
hardware: console
extensions: [smc, SFC, zip]
emulators:
libretro:
snes9x: { requireAnyOf: [X] }
pocketsnes: { requireAnyOf: [Y] }
mame:
name: MAME
hardware: arcade
extensions: [zip, 7z]
emulators:
libretro:
mame078plus: { requireAnyOf: [Z] }
nogame:
name: Has no emulator
extensions: [txt]
elsewhere:
name: Outside the roms root
path: /userdata/library
extensions: [lnk]
emulators:
flatpak:
flatpak: { requireAnyOf: [W] }
@@ -0,0 +1,4 @@
SYSTEM_NAME="3do"
SYSTEM_HARDWARE="console"
SYSTEM_PATH="/storage/roms/3do"
SYSTEM_EXTENSION=".iso .bin .chd"
@@ -0,0 +1,4 @@
SYSTEM_NAME="psx"
SYSTEM_HARDWARE="console"
SYSTEM_PATH="/storage/roms/psx"
SYSTEM_EXTENSION=".cue .CUE .chd"
@@ -0,0 +1,3 @@
SYSTEM_NAME="tools"
SYSTEM_PATH="/storage/.config/modules"
SYSTEM_EXTENSION=".sh"
+23
View File
@@ -0,0 +1,23 @@
PKG_NAME="emulators"
makeinstall_target() {
add_system_dir /storage/roms/bios
add_emu_core 3do retroarch opera true
add_es_system 3do
case ${DEVICE} in
RK3326)
add_emu_core psx retroarch pcsx_rearmed true
add_emu_core psx retroarch swanstation false
;;
*)
add_emu_core psx retroarch swanstation true
add_emu_core psx retroarch pcsx_rearmed false
;;
esac
add_es_system psx
add_emu_core tools ignore ignore true
add_es_system tools
}
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<systemList>
<system>
<name>model2</name>
<path>%ROMPATH%/model2/roms</path>
<extension>.zip .ZIP</extension>
<command label="Model 2 (Standalone)">%EMULATOR_MODEL-2% %BASENAME% & exit</command>
</system>
</systemList>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<systemList>
<system>
<name>psx</name>
<path>%ROMPATH%/psx</path>
<extension>.cue .CUE .chd .CHD</extension>
<command label="Beetle PSX">%EMULATOR_RETROARCH% -L %CORE_RETROARCH%/mednafen_psx_libretro.so %ROM%</command>
<command label="DuckStation (Standalone)">%EMULATOR_DUCKSTATION% -batch %ROM%</command>
</system>
<system>
<name>model2</name>
<path>%ROMPATH%/model2</path>
<extension>.zip</extension>
<command label="MAME">%EMULATOR_RETROARCH% -L %CORE_RETROARCH%/mame_libretro.so %ROM%</command>
</system>
</systemList>
+5
View File
@@ -0,0 +1,5 @@
ESDE_setDefaultEmulators(){
mkdir -p "$gamelists"
ESDE_setEmu 'DuckStation (Standalone)' psx
ESDE_setEmu 'Missing Label' model2
}
+31
View File
@@ -0,0 +1,31 @@
{
"systems": [
{
"name": "psx",
"path": "/storage/roms/psx",
"extensions": [".cue", ".CUE"],
"emulators": [
{"name": "libretro", "cores": [{"name": "pcsx_rearmed"}, {"name": "duckstation", "default": true}]}
]
},
{
"name": "imageviewer",
"path": "/storage/roms/screenshots",
"platform": "ignore",
"command": "ignore",
"extensions": [".png"]
},
{
"name": "pico8",
"path": "/storage/roms/pico-8",
"extensions": [".p8"],
"emulators": [{"name": "Pico-8", "cores": [{"name": "Pico-8", "default": true}]}]
},
{
"name": "pico8",
"path": "/storage/roms/pico",
"extensions": [".md"],
"emulators": [{"name": "libretro", "cores": [{"name": "genesis_plus_gx", "default": true}]}]
}
]
}
@@ -0,0 +1,12 @@
[system]
name = "mame"
roms.folder = "%ROOT%/mame"
[properties]
type = arcade
[core.0]
priority = 1
emulator = "libretro"
core = "mame2003_plus"
extensions = ".zip!"
@@ -0,0 +1,25 @@
[system]
name = "psx"
roms.folder = "%ROOT%/psx"
[properties]
type = console
[core.0]
priority = 1
emulator = "libretro"
core = "pcsx_rearmed"
extensions = ".cue .chd"
[core.1]
priority = 1
emulator = "libretro"
core = "swanstation"
extensions = ".cue .pbp"
[core.2]
priority = 2
priority.rpi1 = 0
emulator = "libretro"
core = "mednafen_psx"
extensions = ".cue"
+8
View File
@@ -0,0 +1,8 @@
psx_exts=".cue .chd"
psx_fullname="PlayStation"
nes_exts=".nes .zip"
nes_fullname="Nintendo Entertainment System"
vectrex_exts=".vec"
vectrex_fullname="Vectrex"
@@ -0,0 +1,7 @@
rp_module_id="pcsx2"
rp_module_section="exp"
rp_module_flags="!all x86"
function configure_pcsx2() {
addEmulator 1 "$md_id" "psx" "pcsx2 %ROM%"
}
@@ -0,0 +1,9 @@
rp_module_id="lr-fceumm"
rp_module_section="main"
function configure_lr-fceumm() {
local system
for system in nes fds; do
addEmulator 1 "$md_id" "$system" "$md_inst/fceumm_libretro.so"
done
}
@@ -0,0 +1,7 @@
rp_module_id="lr-pcsx-rearmed"
rp_module_section="main"
rp_module_flags="!x86"
function configure_lr-pcsx-rearmed() {
addEmulator 1 "$md_id" "psx" "$md_inst/pcsx_rearmed_libretro.so"
}
@@ -0,0 +1,6 @@
rp_module_id="lr-swanstation"
rp_module_section="opt"
function configure_lr-swanstation() {
addEmulator 1 "$md_id" "psx" "$md_inst/swanstation_libretro.so"
}
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<systemList>
<system>
<name>psx</name>
<path>/storage/roms/psx</path>
<extension>.cue .CUE .chd</extension>
<command>runemu.py %ROM%</command>
<platform>psx</platform>
<emulators>
<emulator name="retroarch">
<cores>
<core>pcsx_rearmed</core>
<core default="true">duckstation</core>
</cores>
</emulator>
</emulators>
</system>
<!-- <system><name>commented</name><path>/storage/roms/commented</path> --no-gui </system> -->
<system>
<name>advision</name>
<path>/storage/roms/advision/</path>
<extension>.bin .zip</extension>
<command>retroarch -L /cores/mess_libretro.so %ROM% 2>&1 > /dev/null</command>
<platform>advision</platform>
</system>
<system>
<name>tools</name>
<path>/storage/.config/modules</path>
<extension>.sh</extension>
<command>ignore</command>
<platform>ignore</platform>
</system>
<system>
<name>mame</name>
<path>/storage/roms/mame</path>
<extension>.zip</extension>
<command>runemu.py %ROM%</command>
<platform>arcade</platform>
<emulators>
<emulator name="retroarch">
<cores>
<core>mame2003_plus</core>
</cores>
</emulator>
</emulators>
</system>
</systemList>
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<systemList>
<system>
<name>psx</name>
<path>/storage/roms/psx</path>
<extension>.cue .CUE .chd</extension>
<command>runemu.py %ROM%</command>
<platform>psx</platform>
<emulators>
<emulator name="retroarch">
<cores>
<core>pcsx_rearmed</core>
<core default="true">swanstation</core>
</cores>
</emulator>
</emulators>
</system>
<!-- <system><name>commented</name><path>/storage/roms/commented</path> --no-gui </system> -->
<system>
<name>advision</name>
<path>/storage/roms/advision/</path>
<extension>.bin .zip</extension>
<command>retroarch -L /cores/mess_libretro.so %ROM% 2>&1 > /dev/null</command>
<platform>advision</platform>
</system>
<system>
<name>tools</name>
<path>/storage/.config/modules</path>
<extension>.sh</extension>
<command>ignore</command>
<platform>ignore</platform>
</system>
<system>
<name>mame</name>
<path>/storage/roms/mame</path>
<extension>.zip</extension>
<command>runemu.py %ROM%</command>
<platform>arcade</platform>
<emulators>
<emulator name="retroarch">
<cores>
<core>mame2003_plus</core>
</cores>
</emulator>
</emulators>
</system>
</systemList>
+64
View File
@@ -0,0 +1,64 @@
{
"onion": {
"static/packages/Emu/Nintendo - GBA (gpSP)/Emu/GBA/config.json": "{\n \"label\": \"GBA\",\n \"launch\": \"launch.sh\",\n \"rompath\": \"../../Roms/GBA\",\n \"extlist\": \"gba|GBA|zip|7z\"\n}\n",
"static/packages/Emu/Nintendo - GBA (gpSP)/Emu/GBA/launch.sh": "#!/bin/sh\ndefault_core=gpsp\ncore=\"$default_core\"\nretroarch -L .retroarch/cores/${default_core}_libretro.so \"$1\"\n",
"static/packages/RApp/Nintendo - GBA (VBA-M)/RApp/GBA_VBAM/config.json": "{\n \"label\": \"VBA-M\",\n \"rompath\": \"../../Roms/GBA\",\n \"extlist\": \"gba\"\n}\n",
"static/packages/RApp/Nintendo - GBA (VBA-M)/RApp/GBA_VBAM/launch.sh": "retroarch -L .retroarch/cores/vbam_libretro.so \"$1\"\n",
"static/packages/RApp/NEC - PC-98 (Neko Project II)/RApp/NP2/config.json": "{\n \"rompath\": \"/mnt/SDCARD/Roms/PCNINETYEIGHT\",\n \"extlist\": \"d98|hdi\"\n}\n",
"static/packages/RApp/NEC - PC-98 (Neko Project II)/RApp/NP2/launch.sh": "retroarch -L .retroarch/cores/nekop2_libretro.so \"$1\"\n",
"static/packages/App/Clock/App/Clock/config.json": "{\"label\": \"Clock\"}\n"
},
"spruce": {
"Emu/PS/config.json": "{\n \"label\": \"PlayStation\",\n \"alternativeFolderNames\": [\"psx\", \"PS1\"],\n \"extlist\": \"bin|cue|CHD\",\n \"default_emulator\": \"pcsx_rearmed\",\n \"ignoreList\": [\"neogeo.zip\"]\n}\n",
"Emu/-CUSTOM-SYSTEM-/config.json": "{\"label\": \"template\"}\n",
"Emu/PS/launch.sh": "echo\n"
},
"crossmix": {
"Emus/PS/config.json": "{\n \"rompath\": \"../../Roms/PS\",\n \"extlist\": \"cue|chd\",\n \"launchlist\": [\n {\"name\": \"--- libretro ---\", \"launch\": \"\"},\n {\"name\": \"PCSX ReARMed\", \"launch\": \"pcsx.sh\"}\n ]\n}\n",
"Emus/PS/pcsx.sh": "RA_CORE=\"$HOME/cores/pcsx_rearmed_libretro.so\"\n",
"Emus/_BootLogo/config.json": "{\"rompath\": \"../../Apps/x\"}\n"
},
"koriki": {
"base/.simplemenu/section_groups/apps and games.ini": "[CONSOLES]\nexecs = none\n\n[PS]\nexecs = /mnt/SDCARD/.simplemenu/launchers/pcsx_libretro.sh,/mnt/SDCARD/.simplemenu/launchers/pcsx_standalone.sh\nromDirs = /mnt/SDCARD/Roms/PS/\nromExts = .cue,.CHD,.pbp\n",
"base/.simplemenu/launchers/pcsx_libretro.sh": "retroarch -L /mnt/SDCARD/RetroArch/.retroarch/cores/pcsx_rearmed_libretro.so \"$1\"\n"
},
"allium": {
"consoles.toml": "[[consoles]]\nname = \"Arcade\"\ncores = [\n \"mame2003_plus\",\n \"fbneo\",\n]\npatterns = [\"ARCADE\", \"mame\"]\n\n[[consoles]]\nname = \"Game Boy Advance\"\ncores = [\"gpsp\"]\npatterns = [\"GBA\"]\nextensions = [\"gba\"]\n\n[[consoles]]\nname = \"Native\"\ncores = [\"native\"]\nextensions = [\"pak\"]\n",
"cores.toml": "[cores.native]\npath = \"/mnt/SDCARD/.allium/cores/pak/launch.sh\"\nname = \"Native\"\n\n[cores.\"dosbox_pure_0.9.7\"]\nretroarch = \"dosbox_pure_0.9.7\"\n\n[cores.mame2003_plus]\nretroarch = \"mame2003_plus\"\nname = \"MAME 2003+\"\n\n[cores.gpsp]\nretroarch = \"gpsp\"\n"
},
"minui": {
"skeleton/BASE/Roms/Sony PlayStation (PS)/.keep": "",
"skeleton/SYSTEM/rg35xx/paks/Emus/PS.pak/launch.sh": "#!/bin/sh\nEMU_EXE=pcsx_rearmed\nminarch.elf \"$CORES_PATH/${EMU_EXE}_libretro.so\" \"$1\"\n",
"skeleton/SYSTEM/miyoomini/paks/Emus/PS.pak/launch.sh": "#!/bin/sh\nEMU_EXE=pcsx_rearmed\nminarch.elf \"$CORES_PATH/${EMU_EXE}_libretro.so\" \"$1\"\n",
"skeleton/SYSTEM/tg5040/paks/Emus/PS.pak/launch.sh": "#!/bin/sh\nEMU_EXE=swanstation\nminarch.elf \"$CORES_PATH/${EMU_EXE}_libretro.so\" \"$1\"\n",
"skeleton/EXTRAS/Emus/miyoomini/A2600.pak/launch.sh": "#!/bin/sh\nEMU_EXE=stella2014\nminarch.elf \"$CORES_PATH/${EMU_EXE}_libretro.so\" \"$1\"\n"
},
"plumos": {
"systems.json": "{\n \"systems\": [\n {\n \"id\": \"nes\",\n \"enabled\": true,\n \"directory_aliases\": [\n {\"name\": \"nes\", \"priority\": 20},\n {\"name\": \"FC\", \"priority\": 10, \"shared\": true}\n ],\n \"extensions\": [\"nes\", \"zip\"],\n \"default_launch_profile\": \"retroarch:quicknes\"\n },\n {\n \"id\": \"fds\",\n \"enabled\": false,\n \"directory_aliases\": [\n {\"name\": \"FC\", \"priority\": 10, \"shared\": true},\n {\"name\": \"fds\", \"priority\": 20}\n ],\n \"extensions\": [\"fds\"],\n \"default_launch_profile\": \"standalone:fceux\"\n }\n ]\n}\n"
},
"muos": {
"core.json": "{\n \"Sony PlayStation\": {\n \"catalogue\": \"Sony PlayStation\",\n \"default\": \"psx rearmed\",\n \"friendly\": [\"ps\", \"psx\", \"sonyplaystation\"],\n \"libretro\": {\"psx rearmed\": {\"core\": \"pcsx_rearmed_libretro.so\"}}\n },\n \"Nintendo DS\": {\n \"catalogue\": \"Nintendo DS\",\n \"default\": \"drastic\",\n \"friendly\": [\"nds\", \"nintendods\"],\n \"libretro\": {}\n }\n}\n"
},
"gmenu2x": {
"board/miyoo/roms/GB/a.gb": "",
"board/miyoo/roms/GBA/b.gba": "",
"board/miyoo/roms/autorun.inf": ""
},
"gpsp": {
"miyoo/opkg_assets/gpsp.lnk": "title=gpSP\nexec=/mnt/emus/gpsp/gpsp\nselectordir=/mnt/roms/GBA\nselectorfilter=.gba,.zip\n"
},
"dist": {
"games/SNES/boot.rom": "",
"games/mame/.keep": "",
"games/readme.txt": ""
},
"romm": {
"index.ts": "const _EJS_CORES_MAP: Record<string, string[]> = {\n \"3do\": [\"opera\"],\n arcade: [\n \"mame2003_plus\",\n \"fbneo\",\n ],\n};\n\nconst _EJS_NIGHTLY_CORES_MAP: Record<string, string[]> = {\n \"3ds\": [\"azahar\"],\n};\n",
"known_bios_files.json": "{\"3do:panafz1.bin\": {}, \"64dd:IPL.n64\": {}}\n"
},
"bizhawk": {
"PathEntryCollection.cs": "\t\tprivate static readonly string COMBINED_SYSIDS_GB = string.Join(\"_\", VSystemID.Raw.GB, VSystemID.Raw.GBC, VSystemID.Raw.SGB);\n\t\t\tCommonEntriesFor(VSystemID.Raw.Sega32X, basePath: Path.Combine(\".\", \"32X\")),\n\t\t\tCommonEntriesFor(COMBINED_SYSIDS_GB, basePath: Path.Combine(\".\", \"Gameboy\")),\n\t\t\tCommonEntriesFor(VSystemID.Raw.NES, basePath: Path.Combine(\".\", \"NES\")),\n",
"RomLoader.cs": "\t\t\tpublic static readonly IReadOnlyCollection<string> GB = new[] { \"gb\", \"gbc\", \"sgb\" };\n\t\t\tpublic static readonly IReadOnlyCollection<string> NES = new[] { \"nes\", \"fds\", /*\"x\"*/ \"unf\" };\n",
"Config.cs": "\t\tpublic static readonly IReadOnlyList<(string[] AppliesTo, string[] CoreNames)> CorePickerUIData = new List<(string[], string[])>\n\t\t{\n\t\t\t([ VSystemID.Raw.GB, VSystemID.Raw.GBC ],\n\t\t\t\t[ CoreNames.Gambatte, CoreNames.Sameboy ]),\n\t\t\t([ VSystemID.Raw.NES ],\n\t\t\t\t[ CoreNames.QuickNes, CoreNames.NesHawk ]),\n\t\t};\n"
}
}
+100 -18
View File
@@ -373,6 +373,7 @@
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
^[[0;7;35m > Close^[[0m
@@ -404,20 +405,19 @@
^[[0;1;35mA/Enter^[[0m Close ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help
=== systems
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
Filter (/ to type)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
^[[0;7;35m > [ ] nintendo-famicom-disk (ファミコン) in place^[[0m
All
None
Filter (/ to type)
^[[0;7;35m > [x] nintendo-gba (gba) 1 file, 16.0 KB^[[0m
[x] sony-playstation 3 files, 1.0 MB
^[[0;2m [ ] nintendo-famicom-disk (ファミコン) in place^[[0m
^[[0;32mDone^[[0m
@@ -448,7 +448,7 @@
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
^[[0;1;35mA/Enter^[[0m Continue ^[[0;2mB/Esc^[[0m Back ^[[0;2mX/Space^[[0m Tick ^[[0;2m?^[[0m Help
^[[0;1;35mA/Enter^[[0m Tick ^[[0;2mB/Esc^[[0m Done ^[[0;2m?^[[0m Help
=== options
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
@@ -498,8 +498,8 @@
^[[0;7;35m > [1] North America^[[0m
[ ] Japan
None
^[[0;32mDone^[[0m
@@ -534,14 +534,15 @@
=== cores
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
Every core ticked keeps every core's files. Untick the cores you do not use: the platform's own
files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and a core with no
extra file reads only those.
^[[0;7;35m > All^[[0m
^[[0;7;35m > Filter (/ to type)^[[0m
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
^[[0;32mDone^[[0m
@@ -570,8 +571,7 @@
^[[0;1;35mA/Enter^[[0m Tick all ^[[0;2mB/Esc^[[0m Back ^[[0;2mX/Space^[[0m Tick ^[[0;2m?^[[0m Help
^[[0;1;35mA/Enter^[[0m Filter ^[[0;2mB/Esc^[[0m Done ^[[0;2m?^[[0m Help
=== preview
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
@@ -742,6 +742,7 @@
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
--restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -773,7 +774,6 @@
^[[0;1;35mA/Enter^[[0m Exit
@@ -900,3 +900,85 @@
Any key closes this help
=== plan-games
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
^[[0;1;35mA/Enter^[[0m Install ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help
=== games
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
^[[0;1;35mA/Enter^[[0m Back
+48 -22
View File
@@ -122,25 +122,25 @@
BIOS folder:
^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
^[[0;7;35m > Close^[[0m
Options
^[[0;1;35mA^[[0m Close ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help
=== systems
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
^[[0;7;35m > [ ] nintendo-famicom-disk in place^[[0m
All
None
Filter (/ to type)
^[[0;7;35m > [x] nintendo-gba (gba) 16.0 KB^[[0m
[x] sony-playstation 1.0 MB
^[[0;2m [ ] nintendo-famicom-disk in place^[[0m
^[[0;2m 1-3 of 3^[[0m
nintendo-famicom-disk (ファミコン): in
place
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
^[[0;1;35mA^[[0m Continue ^[[0;2mB^[[0m Back ^[[0;2mX^[[0m Tick ^[[0;2m?^[[0m Help
^[[0;1;35mA^[[0m Tick ^[[0;2mB^[[0m Done ^[[0;2m?^[[0m Help
=== options
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
@@ -165,21 +165,21 @@
^[[0;7;35m > [1] North America^[[0m
[ ] Japan
^[[0;2m 1-2 of 2^[[0m
^[[0;1;35mA^[[0m Tick ^[[0;2mB^[[0m Done ^[[0;2m<>^[[0m Order ^[[0;2m?^[[0m Help
=== cores
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
^[[0;7;35m > All^[[0m
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
^[[0;1;35mA^[[0m Tick all ^[[0;2mB^[[0m Back ^[[0;2mX^[[0m Tick ^[[0;2m?^[[0m Help
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
^[[0;7;35m > Filter (/ to type)^[[0m
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
^[[0;2m 1-2 of 2^[[0m
^[[0;1;35mA^[[0m Filter ^[[0;2mB^[[0m Done ^[[0;2m?^[[0m Help
=== preview
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
@@ -239,11 +239,11 @@
BIOS folder:
^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
^[[0;2m 1-8 of 16^[[0m
^[[0;2m 1-8 of 17^[[0m
^[[0;1;35mA^[[0m Exit ^[[0;2m^v^[[0m Scroll
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
@@ -284,3 +284,29 @@
BIOS folder:
^[[0;1m/home/user/.config/retroarch/system^[[0m
Any key closes this help
=== plan-games
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
BIOS folder:
^[[0;1m~/.config/retroarch/system^[[0m
^[[0;7;35m > Install^[[0m
For the games on this card: yes 412
Only what starts: no
Show the games
^[[0;2m 1-4 of 8^[[0m
^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help
=== games
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m
412 games in /media/user/SDCARD/Roms,
398 recognised by their content.
sony-playstation (PS): 400 games, 390
recognised, every region
nintendo-gba (GBA): 12 games, 8
recognised, Europe only
^[[0;2m 1-5 of 9^[[0m
^[[0;1;35mA^[[0m Back ^[[0;2m^v^[[0m Scroll
+56 -24
View File
@@ -149,31 +149,31 @@
BIOS folder:
^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
^[[0;7;35m > Close^[[0m
Options
^[[0;1;35mA^[[0m Close ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help
=== systems
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
Filter (/ to type)
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
^[[0;7;35m > [ ] nintendo-famicom-disk in place^[[0m
All
None
Filter (/ to type)
^[[0;7;35m > [x] nintendo-gba (gba) 16.0 KB^[[0m
[x] sony-playstation 1.0 MB
^[[0;2m [ ] nintendo-famicom-disk in place^[[0m
^[[0;32mDone^[[0m
nintendo-famicom-disk (ファミコン): in
place
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
^[[0;1;35mA^[[0m Continue ^[[0;2mB^[[0m Back ^[[0;2mX^[[0m Tick ^[[0;2m?^[[0m Help
^[[0;1;35mA^[[0m Tick ^[[0;2mB^[[0m Done ^[[0;2m?^[[0m Help
=== options
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
@@ -201,27 +201,27 @@
^[[0;7;35m > [1] North America^[[0m
[ ] Japan
None
^[[0;32mDone^[[0m
^[[0;1;35mA^[[0m Tick ^[[0;2mB^[[0m Done ^[[0;2m<>^[[0m Order ^[[0;2m?^[[0m Help
=== cores
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
^[[0;7;35m > All^[[0m
^[[0;7;35m > Filter (/ to type)^[[0m
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
^[[0;32mDone^[[0m
^[[0;1;35mA^[[0m Tick all ^[[0;2mB^[[0m Back ^[[0;2mX^[[0m Tick ^[[0;2m?^[[0m Help
^[[0;1;35mA^[[0m Filter ^[[0;2mB^[[0m Done ^[[0;2m?^[[0m Help
=== preview
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
@@ -293,14 +293,14 @@
BIOS folder:
^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
--restore .../2026-10-23_1200
^[[0;2m Shortened to fit this window.^[[0m
Same choice without questions:
^[[0;2m 1-11 of 16^[[0m
^[[0;2m 1-11 of 17^[[0m
^[[0;1;35mA^[[0m Exit ^[[0;2m^v^[[0m Scroll
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
@@ -350,3 +350,35 @@
Any key closes this help
=== plan-games
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
BIOS folder:
^[[0;1m~/.config/retroarch/system^[[0m
For the 412 games on this card.
^[[0;7;35m > Install^[[0m
For the games on this card: yes 412
Only what starts: no
Show the games
Choose systems
Copy from your old card or a folder
Options
Quit
^[[0;1;35mA^[[0m Install ^[[0;2mB^[[0m Back ^[[0;2mY^[[0m Options ^[[0;2m?^[[0m Help
=== games
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m
412 games in /media/user/SDCARD/Roms,
398 recognised by their content.
sony-playstation (PS): 400 games, 390
recognised, every region
nintendo-gba (GBA): 12 games, 8
recognised, Europe only
Every BIOS of their system kept, so
that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
^[[0;1;35mA^[[0m Back
+68 -18
View File
@@ -229,6 +229,7 @@
^[[0;1m ^[[0;1;32mEverything is in place^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
^[[0;32m5 in place^[[0m, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
^[[0;7;35m > Close^[[0m
@@ -246,18 +247,17 @@
^[[0;1;35mA/Enter^[[0m Close ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help
=== systems
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Choose systems^[[0m
Filter (/ to type)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
^[[0;7;35m > [ ] nintendo-famicom-disk (ファミコン) in place^[[0m
All
None
Filter (/ to type)
^[[0;7;35m > [x] nintendo-gba (gba) 1 file, 16.0 KB^[[0m
[x] sony-playstation 3 files, 1.0 MB
^[[0;2m [ ] nintendo-famicom-disk (ファミコン) in place^[[0m
^[[0;32mDone^[[0m
@@ -272,7 +272,7 @@
^[[0;1m ^[[0;1;35m2^[[0;1m of ^[[0;1;35m3^[[0;1m systems: ^[[0;1;35m4^[[0;1m files, ^[[0;1;35m1.0 MB^[[0m
^[[0;1;35mA/Enter^[[0m Continue ^[[0;2mB/Esc^[[0m Back ^[[0;2mX/Space^[[0m Tick ^[[0;2m?^[[0m Help
^[[0;1;35mA/Enter^[[0m Tick ^[[0;2mB/Esc^[[0m Done ^[[0;2m?^[[0m Help
=== options
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Options^[[0m
@@ -307,8 +307,8 @@
^[[0;7;35m > [1] North America^[[0m
[ ] Japan
None
^[[0;32mDone^[[0m
@@ -326,14 +326,15 @@
=== cores
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Cores^[[0m
Every core ticked keeps every core's files. Untick the cores you do not use:
the platform's own files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and
a core with no extra file reads only those.
^[[0;7;35m > All^[[0m
^[[0;7;35m > Filter (/ to type)^[[0m
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
^[[0;32mDone^[[0m
@@ -346,8 +347,7 @@
^[[0;1;35mA/Enter^[[0m Tick all ^[[0;2mB/Esc^[[0m Back ^[[0;2mX/Space^[[0m Tick ^[[0;2m?^[[0m Help
^[[0;1;35mA/Enter^[[0m Filter ^[[0;2mB/Esc^[[0m Done ^[[0;2m?^[[0m Help
=== preview
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Preview^[[0m
@@ -454,6 +454,7 @@
^[[0;1m ^[[0;1;32m3 installed^[[0;1m, ^[[0;1;31m1 failed^[[0;1m.^[[0m
BIOS folder: ^[[0;1m/home/user/.config/retroarch/system^[[0m
^[[0;1;31mfailed:^[[0m scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
@@ -471,7 +472,6 @@
^[[0;1;35mA/Enter^[[0m Exit
=== error
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: ^[[0;1;31mSomething went wrong^[[0m
@@ -548,3 +548,53 @@
Any key closes this help
=== plan-games
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: RetroArch^[[0m
^[[0;1m ^[[0;1;35m4^[[0;1m files to download, ^[[0;1;35m1.0 MB^[[0m
BIOS folder: ^[[0;1m~/.config/retroarch/system^[[0m
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
^[[0;32m1 in place^[[0m, ^[[0;33m3 missing^[[0m, ^[[0;33m1 wrong copy^[[0m
The ^[[0;33m1 wrong copy^[[0m is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
^[[0;7;35m > Install^[[0m
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
^[[0;1;35mA/Enter^[[0m Install ^[[0;2mB/Esc^[[0m Back ^[[0;2mY/o^[[0m Options ^[[0;2m?^[[0m Help
=== games
^[[0;1m ^[[0;1;35mRetroBIOS^[[0;1m: Games on this card^[[0m
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
^[[0;1;35mA/Enter^[[0m Back
+8 -8
View File
@@ -1,13 +1,14 @@
RetroBIOS: Cores
Every core ticked keeps every core's files. Untick the cores you do not use: the platform's own
files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and a core with no
extra file reads only those.
> All
> Filter (/ to type)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
Done
@@ -36,5 +37,4 @@
A/Enter Tick all B/Esc Back X/Space Tick ? Help
A/Enter Filter B/Esc Done ? Help
+10 -10
View File
@@ -1,12 +1,12 @@
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
> All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
A Tick all B Back X Tick ? Help
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> Filter (/ to type)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
1-2 of 2
A Filter B Done ? Help
+10 -10
View File
@@ -1,15 +1,15 @@
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> All
> Filter (/ to type)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
Done
A Tick all B Back X Tick ? Help
A Filter B Done ? Help
+8 -8
View File
@@ -1,13 +1,14 @@
RetroBIOS: Cores
Every core ticked keeps every core's files. Untick the cores you do not use:
the platform's own files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and
a core with no extra file reads only those.
> All
> Filter (/ to type)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
Done
@@ -20,5 +21,4 @@
A/Enter Tick all B/Esc Back X/Space Tick ? Help
A/Enter Filter B/Esc Done ? Help
+40
View File
@@ -0,0 +1,40 @@
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
A/Enter Back
+12
View File
@@ -0,0 +1,12 @@
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms,
398 recognised by their content.
sony-playstation (PS): 400 games, 390
recognised, every region
nintendo-gba (GBA): 12 games, 8
recognised, Europe only
1-5 of 9
A Back ^v Scroll
+15
View File
@@ -0,0 +1,15 @@
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms,
398 recognised by their content.
sony-playstation (PS): 400 games, 390
recognised, every region
nintendo-gba (GBA): 12 games, 8
recognised, Europe only
Every BIOS of their system kept, so
that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
A Back
+24
View File
@@ -0,0 +1,24 @@
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
A/Enter Back
+40
View File
@@ -0,0 +1,40 @@
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder: ~/.config/retroarch/system
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
1 in place, 3 missing, 1 wrong copy
The 1 wrong copy is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Install
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
A/Enter Install B/Esc Back Y/o Options ? Help
+12
View File
@@ -0,0 +1,12 @@
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
1-4 of 8
A Install B Back Y Options ? Help
+15
View File
@@ -0,0 +1,15 @@
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
For the 412 games on this card.
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
Choose systems
Copy from your old card or a folder
Options
Quit
A Install B Back Y Options ? Help
+24
View File
@@ -0,0 +1,24 @@
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder: ~/.config/retroarch/system
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
1 in place, 3 missing, 1 wrong copy
The 1 wrong copy is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Install
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
A/Enter Install B/Esc Back Y/o Options ? Help
+1 -1
View File
@@ -3,6 +3,7 @@
Everything is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Close
@@ -34,7 +35,6 @@
A/Enter Close B/Esc Back Y/o Options ? Help
+2 -2
View File
@@ -4,9 +4,9 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close B Back Y Options ? Help
+2 -2
View File
@@ -4,12 +4,12 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close B Back Y Options ? Help
+1 -1
View File
@@ -3,6 +3,7 @@
Everything is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Close
@@ -20,5 +21,4 @@
A/Enter Close B/Esc Back Y/o Options ? Help
+2 -2
View File
@@ -5,8 +5,8 @@
> [1] North America
[ ] Japan
None
Done
+1 -1
View File
@@ -8,5 +8,5 @@
> [1] North America
[ ] Japan
1-2 of 2
A Tick B Done <> Order ? Help
+2 -2
View File
@@ -8,8 +8,8 @@
> [1] North America
[ ] Japan
None
Done
A Tick B Done <> Order ? Help
+2 -2
View File
@@ -6,8 +6,8 @@
> [1] North America
[ ] Japan
None
Done
+1 -1
View File
@@ -3,6 +3,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
--restore /home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
@@ -34,7 +35,6 @@
A/Enter Exit
+2 -2
View File
@@ -4,9 +4,9 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
1-8 of 16
1-8 of 17
A Exit ^v Scroll
+2 -2
View File
@@ -4,12 +4,12 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
--restore .../2026-10-23_1200
Shortened to fit this window.
Same choice without questions:
1-11 of 16
1-11 of 17
A Exit ^v Scroll
+1 -1
View File
@@ -3,6 +3,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
@@ -20,5 +21,4 @@
A/Enter Exit
+141 -62
View File
@@ -462,11 +462,11 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close B Back Y Options
=== plan-in-place 40x15
RetroBIOS: RetroArch
@@ -475,14 +475,14 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close B Back Y Options
=== plan-in-place 80x24
RetroBIOS: RetroArch
@@ -490,6 +490,7 @@
Everything is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Close
@@ -507,47 +508,46 @@
A Close B Back Y Options
=== systems 40x12
RetroBIOS: Choose systems
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
1-3 of 3
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== systems 40x15
RetroBIOS: Choose systems
Filter (with a keyboard)
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
Done
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== systems 80x24
RetroBIOS: Choose systems
Filter (with a keyboard)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
> [ ] nintendo-famicom-disk (ファミコン) in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
[ ] nintendo-famicom-disk (ファミコン) in place
Done
@@ -562,7 +562,7 @@
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== options 40x12
RetroBIOS: Options
@@ -628,7 +628,7 @@
> [1] North America
[ ] Japan
1-2 of 2
A Tick B Done <> Order
=== regions 40x15
RetroBIOS: Preferred regions
@@ -641,8 +641,8 @@
> [1] North America
[ ] Japan
None
Done
A Tick B Done <> Order
@@ -655,8 +655,8 @@
> [1] North America
[ ] Japan
None
Done
@@ -674,43 +674,44 @@
=== cores 40x12
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
> All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
A Tick all B Back X Tick
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
1-2 of 2
A Filter B Done
=== cores 40x15
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> All
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
A Tick all B Back X Tick
A Filter B Done
=== cores 80x24
RetroBIOS: Cores
Every core ticked keeps every core's files. Untick the cores you do not use:
the platform's own files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and
a core with no extra file reads only those.
> All
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
@@ -723,8 +724,7 @@
A Tick all B Back X Tick
A Filter B Done
=== preview 80x24
RetroBIOS: Preview
@@ -890,11 +890,11 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
A Exit
=== result 40x15
RetroBIOS: Finished with errors
@@ -903,6 +903,7 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
@@ -910,7 +911,6 @@
A Exit
=== result 80x24
RetroBIOS: Finished with errors
@@ -918,6 +918,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
@@ -935,7 +936,6 @@
A Exit
=== error 40x12
RetroBIOS: Something went wrong
@@ -1054,3 +1054,82 @@
Any key closes this help
=== plan-games 40x12
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
1-4 of 8
A Install B Back Y Options
=== plan-games 40x15
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
For the 412 games on this card.
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
Choose systems
Copy from your old card or a folder
Options
Quit
A Install B Back Y Options
=== plan-games 80x24
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder: ~/.config/retroarch/system
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
1 in place, 3 missing, 1 wrong copy
The 1 wrong copy is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Install
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
A Install B Back Y Options
=== games 80x24
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
A Back
+141 -62
View File
@@ -462,11 +462,11 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close B Back Y Options
=== plan-in-place 40x15
RetroBIOS: RetroArch
@@ -475,14 +475,14 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close B Back Y Options
=== plan-in-place 80x24
RetroBIOS: RetroArch
@@ -490,6 +490,7 @@
Everything is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Close
@@ -507,47 +508,46 @@
A Close B Back Y Options
=== systems 40x12
RetroBIOS: Choose systems
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
1-3 of 3
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== systems 40x15
RetroBIOS: Choose systems
Filter (with a keyboard)
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
Done
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== systems 80x24
RetroBIOS: Choose systems
Filter (with a keyboard)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
> [ ] nintendo-famicom-disk (ファミコン) in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
[ ] nintendo-famicom-disk (ファミコン) in place
Done
@@ -562,7 +562,7 @@
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== options 40x12
RetroBIOS: Options
@@ -628,7 +628,7 @@
> [1] North America
[ ] Japan
1-2 of 2
A Tick B Done <> Order
=== regions 40x15
RetroBIOS: Preferred regions
@@ -641,8 +641,8 @@
> [1] North America
[ ] Japan
None
Done
A Tick B Done <> Order
@@ -655,8 +655,8 @@
> [1] North America
[ ] Japan
None
Done
@@ -674,43 +674,44 @@
=== cores 40x12
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
> All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
A Tick all B Back X Tick
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
1-2 of 2
A Filter B Done
=== cores 40x15
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> All
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
A Tick all B Back X Tick
A Filter B Done
=== cores 80x24
RetroBIOS: Cores
Every core ticked keeps every core's files. Untick the cores you do not use:
the platform's own files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and
a core with no extra file reads only those.
> All
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
@@ -723,8 +724,7 @@
A Tick all B Back X Tick
A Filter B Done
=== preview 80x24
RetroBIOS: Preview
@@ -890,11 +890,11 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
A Exit
=== result 40x15
RetroBIOS: Finished with errors
@@ -903,6 +903,7 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
@@ -910,7 +911,6 @@
A Exit
=== result 80x24
RetroBIOS: Finished with errors
@@ -918,6 +918,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
@@ -935,7 +936,6 @@
A Exit
=== error 40x12
RetroBIOS: Something went wrong
@@ -1054,3 +1054,82 @@
Any key closes this help
=== plan-games 40x12
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
1-4 of 8
A Install B Back Y Options
=== plan-games 40x15
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
For the 412 games on this card.
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
Choose systems
Copy from your old card or a folder
Options
Quit
A Install B Back Y Options
=== plan-games 80x24
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder: ~/.config/retroarch/system
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
1 in place, 3 missing, 1 wrong copy
The 1 wrong copy is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Install
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
A Install B Back Y Options
=== games 80x24
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
A Back
+141 -62
View File
@@ -462,11 +462,11 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close B Back Y Options
=== plan-in-place 40x15
RetroBIOS: RetroArch
@@ -475,14 +475,14 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close B Back Y Options
=== plan-in-place 80x24
RetroBIOS: RetroArch
@@ -490,6 +490,7 @@
Everything is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Close
@@ -507,47 +508,46 @@
A Close B Back Y Options
=== systems 40x12
RetroBIOS: Choose systems
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
1-3 of 3
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== systems 40x15
RetroBIOS: Choose systems
Filter (with a keyboard)
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
Done
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== systems 80x24
RetroBIOS: Choose systems
Filter (with a keyboard)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
> [ ] nintendo-famicom-disk (ファミコン) in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
[ ] nintendo-famicom-disk (ファミコン) in place
Done
@@ -562,7 +562,7 @@
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick
A Tick B Done
=== options 40x12
RetroBIOS: Options
@@ -628,7 +628,7 @@
> [1] North America
[ ] Japan
1-2 of 2
A Tick B Done <> Order
=== regions 40x15
RetroBIOS: Preferred regions
@@ -641,8 +641,8 @@
> [1] North America
[ ] Japan
None
Done
A Tick B Done <> Order
@@ -655,8 +655,8 @@
> [1] North America
[ ] Japan
None
Done
@@ -674,43 +674,44 @@
=== cores 40x12
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
> All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
A Tick all B Back X Tick
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
1-2 of 2
A Filter B Done
=== cores 40x15
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> All
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
A Tick all B Back X Tick
A Filter B Done
=== cores 80x24
RetroBIOS: Cores
Every core ticked keeps every core's files. Untick the cores you do not use:
the platform's own files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and
a core with no extra file reads only those.
> All
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
@@ -723,8 +724,7 @@
A Tick all B Back X Tick
A Filter B Done
=== preview 80x24
RetroBIOS: Preview
@@ -890,11 +890,11 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
A Exit
=== result 40x15
RetroBIOS: Finished with errors
@@ -903,6 +903,7 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
@@ -910,7 +911,6 @@
A Exit
=== result 80x24
RetroBIOS: Finished with errors
@@ -918,6 +918,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
@@ -935,7 +936,6 @@
A Exit
=== error 40x12
RetroBIOS: Something went wrong
@@ -1054,3 +1054,82 @@
Any key closes this help
=== plan-games 40x12
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
1-4 of 8
A Install B Back Y Options
=== plan-games 40x15
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
For the 412 games on this card.
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
Choose systems
Copy from your old card or a folder
Options
Quit
A Install B Back Y Options
=== plan-games 80x24
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder: ~/.config/retroarch/system
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
1 in place, 3 missing, 1 wrong copy
The 1 wrong copy is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Install
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
A Install B Back Y Options
=== games 80x24
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
A Back
+172 -64
View File
@@ -437,11 +437,11 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
Enter Close Esc Back o Options
=== plan-in-place 40x15
RetroBIOS: RetroArch
@@ -450,14 +450,14 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
Enter Close Esc Back o Options
=== plan-in-place 80x24
RetroBIOS: RetroArch
@@ -465,6 +465,7 @@
Everything is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Close
@@ -482,47 +483,46 @@
Enter Close Esc Back o Options ? Help
=== systems 40x12
RetroBIOS: Choose systems
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (/ to type)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
1-3 of 3
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
Enter Continue Esc Back Space Tick
Enter Tick Esc Done ? Help
=== systems 40x15
RetroBIOS: Choose systems
Filter (/ to type)
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (/ to type)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
Done
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
Enter Continue Esc Back Space Tick
Enter Tick Esc Done ? Help
=== systems 80x24
RetroBIOS: Choose systems
Filter (/ to type)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
> [ ] nintendo-famicom-disk (ファミコン) in place
All
None
Filter (/ to type)
> [x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
[ ] nintendo-famicom-disk (ファミコン) in place
Done
@@ -537,7 +537,7 @@
2 of 3 systems: 4 files, 1.0 MB
Enter Continue Esc Back Space Tick ? Help
Enter Tick Esc Done ? Help
=== options 40x12
RetroBIOS: Options
@@ -603,7 +603,7 @@
> [1] North America
[ ] Japan
1-2 of 2
Enter Tick Esc Done <> Order ? Help
=== regions 40x15
RetroBIOS: Preferred regions
@@ -616,8 +616,8 @@
> [1] North America
[ ] Japan
None
Done
Enter Tick Esc Done <> Order ? Help
@@ -630,8 +630,8 @@
> [1] North America
[ ] Japan
None
Done
@@ -649,43 +649,44 @@
=== cores 40x12
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
> All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
Enter Tick all Esc Back Space Tick
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> Filter (/ to type)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
1-2 of 2
Enter Filter Esc Done ? Help
=== cores 40x15
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> All
> Filter (/ to type)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
Done
Enter Tick all Esc Back Space Tick
Enter Filter Esc Done ? Help
=== cores 80x24
RetroBIOS: Cores
Every core ticked keeps every core's files. Untick the cores you do not use:
the platform's own files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and
a core with no extra file reads only those.
> All
> Filter (/ to type)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (/ to type)
[ ] mesen
[x] pcsx_rearmed
Done
@@ -698,8 +699,7 @@
Enter Tick all Esc Back Space Tick ? Help
Enter Filter Esc Done ? Help
=== preview 40x12
RetroBIOS: Preview
@@ -923,11 +923,11 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
1-8 of 16
1-8 of 17
Enter Exit ^v Scroll
=== result 40x15
RetroBIOS: Finished with errors
@@ -936,14 +936,14 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
--restore .../2026-10-23_1200
Shortened to fit this window.
Same choice without questions:
1-11 of 16
1-11 of 17
Enter Exit ^v Scroll
=== result 80x24
RetroBIOS: Finished with errors
@@ -951,6 +951,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
To put them back: python3 install.py \
@@ -968,7 +969,6 @@
Enter Exit
=== error 40x12
RetroBIOS: Something went wrong
@@ -1132,3 +1132,111 @@
Any key closes this help
=== plan-games 40x12
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
1-4 of 8
Enter Install Esc Back o Options
=== plan-games 40x15
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
For the 412 games on this card.
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
Choose systems
Copy from your old card or a folder
Options
Quit
Enter Install Esc Back o Options
=== plan-games 80x24
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder: ~/.config/retroarch/system
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
1 in place, 3 missing, 1 wrong copy
The 1 wrong copy is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Install
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
Enter Install Esc Back o Options ? Help
=== games 40x12
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms,
398 recognised by their content.
sony-playstation (PS): 400 games, 390
recognised, every region
nintendo-gba (GBA): 12 games, 8
recognised, Europe only
1-5 of 9
Enter Back ^v Scroll
=== games 40x15
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms,
398 recognised by their content.
sony-playstation (PS): 400 games, 390
recognised, every region
nintendo-gba (GBA): 12 games, 8
recognised, Europe only
Every BIOS of their system kept, so
that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
Enter Back
=== games 80x24
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
Enter Back
+141 -62
View File
@@ -491,11 +491,11 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close R2 Back L2 Options
=== plan-in-place 40x15
RetroBIOS: RetroArch
@@ -504,14 +504,14 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
A Close R2 Back L2 Options
=== plan-in-place 80x24
RetroBIOS: RetroArch
@@ -519,6 +519,7 @@
Everything is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Close
@@ -536,47 +537,46 @@
A Close R2 Back L2 Options
=== systems 40x12
RetroBIOS: Choose systems
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
1-3 of 3
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
Start Continue A Tick R2 Back
A Tick R2 Done
=== systems 40x15
RetroBIOS: Choose systems
Filter (with a keyboard)
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
Done
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
Start Continue A Tick R2 Back
A Tick R2 Done
=== systems 80x24
RetroBIOS: Choose systems
Filter (with a keyboard)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
> [ ] nintendo-famicom-disk (ファミコン) in place
All
None
Filter (with a keyboard)
> [x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
[ ] nintendo-famicom-disk (ファミコン) in place
Done
@@ -591,7 +591,7 @@
2 of 3 systems: 4 files, 1.0 MB
Start Continue A Tick R2 Back
A Tick R2 Done
=== options 40x12
RetroBIOS: Options
@@ -657,7 +657,7 @@
> [1] North America
[ ] Japan
1-2 of 2
A Tick R2 Done <> Order
=== regions 40x15
RetroBIOS: Preferred regions
@@ -670,8 +670,8 @@
> [1] North America
[ ] Japan
None
Done
A Tick R2 Done <> Order
@@ -684,8 +684,8 @@
> [1] North America
[ ] Japan
None
Done
@@ -703,43 +703,44 @@
=== cores 40x12
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
> All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
A Tick all R2 Back
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
1-2 of 2
A Filter R2 Done
=== cores 40x15
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> All
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
A Tick all R2 Back
A Filter R2 Done
=== cores 80x24
RetroBIOS: Cores
Every core ticked keeps every core's files. Untick the cores you do not use:
the platform's own files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and
a core with no extra file reads only those.
> All
> Filter (with a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (with a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
@@ -752,8 +753,7 @@
A Tick all R2 Back
A Filter R2 Done
=== preview 80x24
RetroBIOS: Preview
@@ -919,11 +919,11 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
A Exit
=== result 40x15
RetroBIOS: Finished with errors
@@ -932,6 +932,7 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
@@ -939,7 +940,6 @@
A Exit
=== result 80x24
RetroBIOS: Finished with errors
@@ -947,6 +947,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
@@ -964,7 +965,6 @@
A Exit
=== error 40x12
RetroBIOS: Something went wrong
@@ -1128,3 +1128,82 @@
Any key closes this help
=== plan-games 40x12
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
1-4 of 8
A Install R2 Back L2 Options
=== plan-games 40x15
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
For the 412 games on this card.
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
Choose systems
Copy from your old card or a folder
Options
Quit
A Install R2 Back L2 Options
=== plan-games 80x24
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder: ~/.config/retroarch/system
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
1 in place, 3 missing, 1 wrong copy
The 1 wrong copy is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Install
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
A Install R2 Back L2 Options
=== games 80x24
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
A Back
+170 -62
View File
@@ -491,11 +491,11 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
Start Close B Back
=== plan-in-place 40x15
RetroBIOS: RetroArch
@@ -504,14 +504,14 @@
BIOS folder:
~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced,
the old one kept in a backup folder.
Free space: 50.0 GB
> Close
Options
Start Close B Back
=== plan-in-place 80x24
RetroBIOS: RetroArch
@@ -519,6 +519,7 @@
Everything is in place
BIOS folder: ~/.config/retroarch/system
5 in place, 0 missing, 0 wrong copies
1 of the 0 wrong copies is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Close
@@ -536,47 +537,46 @@
Start Close B Back
=== systems 40x12
RetroBIOS: Choose systems
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (Select shows a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
1-3 of 3
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
Start Continue B Back Y Tick
Start Tick B Done
=== systems 40x15
RetroBIOS: Choose systems
Filter (Select shows a keyboard)
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (Select shows a keyboard)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
Done
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
Start Continue B Back Y Tick
Start Tick B Done
=== systems 80x24
RetroBIOS: Choose systems
Filter (Select shows a keyboard)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
> [ ] nintendo-famicom-disk (ファミコン) in place
All
None
Filter (Select shows a keyboard)
> [x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
[ ] nintendo-famicom-disk (ファミコン) in place
Done
@@ -591,7 +591,7 @@
2 of 3 systems: 4 files, 1.0 MB
Start Continue B Back Y Tick
Start Tick B Done
=== options 40x12
RetroBIOS: Options
@@ -657,7 +657,7 @@
> [1] North America
[ ] Japan
1-2 of 2
Start Tick B Done <> Order
=== regions 40x15
RetroBIOS: Preferred regions
@@ -670,8 +670,8 @@
> [1] North America
[ ] Japan
None
Done
Start Tick B Done <> Order
@@ -684,8 +684,8 @@
> [1] North America
[ ] Japan
None
Done
@@ -703,43 +703,44 @@
=== cores 40x12
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
> All
None
Filter (Select shows a keyboard)
[ ] mesen
[x] pcsx_rearmed
Start Tick all B Back Y Tick
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> Filter (Select shows a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
1-2 of 2
Start Filter B Done
=== cores 40x15
RetroBIOS: Cores
Every core ticked keeps every core's
files. Untick the cores you do not
use: the platform's own files are
always kept.
Untick the cores you do not use: the
platform's own files are always
kept, and a core with no extra file
reads only those.
> All
> Filter (Select shows a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (Select shows a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
Start Tick all B Back Y Tick
Start Filter B Done
=== cores 80x24
RetroBIOS: Cores
Every core ticked keeps every core's files. Untick the cores you do not use:
the platform's own files are always kept.
Untick the cores you do not use: the platform's own files are always kept, and
a core with no extra file reads only those.
> All
> Filter (Select shows a keyboard)
[ ] mesen 1 file, 8.0 KB
[x] pcsx_rearmed 1 file, 4 B
All
None
Filter (Select shows a keyboard)
[ ] mesen
[x] pcsx_rearmed
Done
@@ -752,8 +753,7 @@
Start Tick all B Back Y Tick
Start Filter B Done
=== preview 40x12
RetroBIOS: Preview
@@ -977,11 +977,11 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
Start Exit
=== result 40x15
RetroBIOS: Finished with errors
@@ -990,6 +990,7 @@
BIOS folder:
/home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/
retrobios-backup/2026-10-23_1200
@@ -997,7 +998,6 @@
Start Exit
=== result 80x24
RetroBIOS: Finished with errors
@@ -1005,6 +1005,7 @@
3 installed, 1 failed.
BIOS folder: /home/user/.config/retroarch/system
failed: scph5500.bin (HTTP 404)
Run it again to retry only those.
The files it replaced are kept in
/home/user/.config/retroarch/retrobios-backup/2026-10-23_1200
Details: /tmp/retrobios-x/install.log
@@ -1022,7 +1023,6 @@
Start Exit
=== error 40x12
RetroBIOS: Something went wrong
@@ -1186,3 +1186,111 @@
Any key closes this help
=== plan-games 40x12
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
1-4 of 8
Start Install B Back
=== plan-games 40x15
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder:
~/.config/retroarch/system
For the 412 games on this card.
> Install
For the games on this card: yes 412
Only what starts: no
Show the games
Choose systems
Copy from your old card or a folder
Options
Quit
Start Install B Back
=== plan-games 80x24
RetroBIOS: RetroArch
4 files to download, 1.0 MB
BIOS folder: ~/.config/retroarch/system
For the 412 games on this card.
One region where the games prove it: nintendo-gba Europe.
2 games get every BIOS of their system: Show the games.
1 in place, 3 missing, 1 wrong copy
The 1 wrong copy is replaced, the old one kept in a backup folder.
Free space: 50.0 GB
> Install
For the games on this card: yes 412 games
Only what starts: no
Show the games
Choose systems
Copy the BIOS files from your old card or a folder
Options
Quit
Start Install B Back
=== games 40x12
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms,
398 recognised by their content.
sony-playstation (PS): 400 games, 390
recognised, every region
nintendo-gba (GBA): 12 games, 8
recognised, Europe only
1-5 of 9
Start Back ^v Scroll
=== games 40x15
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms,
398 recognised by their content.
sony-playstation (PS): 400 games, 390
recognised, every region
nintendo-gba (GBA): 12 games, 8
recognised, Europe only
Every BIOS of their system kept, so
that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
Start Back
=== games 80x24
RetroBIOS: Games on this card
412 games in /media/user/SDCARD/Roms, 398 recognised by their content.
sony-playstation (PS): 400 games, 390 recognised, every region
nintendo-gba (GBA): 12 games, 8 recognised, Europe only
Every BIOS of their system kept, so that they start:
GBA/demo.gba: not recognised
GBA/hack.gba: not recognised
Start Back
+6 -6
View File
@@ -1,12 +1,12 @@
RetroBIOS: Choose systems
Filter (/ to type)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
> [ ] nintendo-famicom-disk (ファミコン) in place
All
None
Filter (/ to type)
> [x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
[ ] nintendo-famicom-disk (ファミコン) in place
Done
@@ -37,4 +37,4 @@
2 of 3 systems: 4 files, 1.0 MB
A/Enter Continue B/Esc Back X/Space Tick ? Help
A/Enter Tick B/Esc Done ? Help
+7 -7
View File
@@ -1,12 +1,12 @@
RetroBIOS: Choose systems
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (/ to type)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
1-3 of 3
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick ? Help
A Tick B Done ? Help
+8 -8
View File
@@ -1,15 +1,15 @@
RetroBIOS: Choose systems
Filter (/ to type)
[x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
> [ ] nintendo-famicom-disk in place
All
None
Filter (/ to type)
> [x] nintendo-gba (gba) 16.0 KB
[x] sony-playstation 1.0 MB
[ ] nintendo-famicom-disk in place
Done
nintendo-famicom-disk (ファミコン): in
place
2 of 3 systems: 4 files, 1.0 MB
A Continue B Back X Tick ? Help
A Tick B Done ? Help
+6 -6
View File
@@ -1,12 +1,12 @@
RetroBIOS: Choose systems
Filter (/ to type)
[x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
> [ ] nintendo-famicom-disk (ファミコン) in place
All
None
Filter (/ to type)
> [x] nintendo-gba (gba) 1 file, 16.0 KB
[x] sony-playstation 3 files, 1.0 MB
[ ] nintendo-famicom-disk (ファミコン) in place
Done
@@ -21,4 +21,4 @@
2 of 3 systems: 4 files, 1.0 MB
A/Enter Continue B/Esc Back X/Space Tick ? Help
A/Enter Tick B/Esc Done ? Help
+33
View File
@@ -0,0 +1,33 @@
"""The guard a test module turns on for itself and the Python it starts
(sitecustomize). module_guard() gives the setUpModule and tearDownModule
pair: the module fails at its end when anything tried to leave the loopback."""
from __future__ import annotations
import os
from pathlib import Path
from typing import Callable
from .sitecustomize import disable, enable, refused
__all__ = ["module_guard", "enable", "disable", "refused"]
def module_guard(name: str) -> tuple[Callable[[], None], Callable[[], None]]:
log = Path(__file__).resolve().parents[2] / "tmp" / "tests" / f"netlog-{name}-{os.getpid()}"
state: dict = {}
def set_up() -> None:
log.parent.mkdir(parents=True, exist_ok=True)
state["before"] = enable(str(log))
def tear_down() -> None:
disable(state.pop("before", {}))
tried = refused(str(log))
log.unlink(missing_ok=True)
if tried:
raise AssertionError(
f"{len(tried)} connections off the loopback during {name} (pid host): "
+ ", ".join(tried[:10]))
return set_up, tear_down
+135
View File
@@ -0,0 +1,135 @@
"""The machine running the installer tests, kept out of them.
Python imports this module at start in every process a test starts with
tests/netguard on PYTHONPATH (enable), and the test module
calls guard() for its own process. Two things the host has never reach a
test: a connection off the loopback, which is refused (OSError, as a network
that is down) and written to RETROBIOS_TEST_NETLOG, and the cards and drives
mounted under /media, /run/media, /mnt and /Volumes, which read as folders
no drive is mounted on. A test module passed on a machine with no network
and no card, and failed with an AmberELEC card in the reader, while 261
connections to GitHub left the module in a run (code_audit_24 P3-13).
"""
from __future__ import annotations
import errno
import os
import socket
import sys
LOOPBACK = frozenset({"127.0.0.1", "::1", "localhost", "0.0.0.0", ""})
MOUNT_ROOTS = ("/media", "/run/media", "/mnt", "/Volumes")
NETLOG = "RETROBIOS_TEST_NETLOG"
# What guard() replaced, for unguard().
_SAVED: dict = {}
def _refuse(host: object) -> None:
where = os.environ.get(NETLOG)
# A name under .invalid never resolves (RFC 2606): a test names one to
# fail on purpose, refused here before its DNS query leaves.
if where and not str(host).endswith(".invalid"):
with open(where, "a", encoding="utf-8") as log:
log.write(f"{os.getpid()} {host} {' '.join(sys.argv)[:300]}\n")
raise OSError(errno.ENETUNREACH, f"a test makes no request to {host}")
def guard() -> None:
"""Refuse every connection off the loopback, hide the host's mounts."""
if _SAVED:
return
_SAVED.update(connect=socket.socket.__dict__.get("connect"),
connect_ex=socket.socket.__dict__.get("connect_ex"),
getaddrinfo=socket.getaddrinfo, ismount=os.path.ismount)
connect = socket.socket.connect
connect_ex = socket.socket.connect_ex
getaddrinfo = socket.getaddrinfo
def host_of(address: object) -> object:
return address[0] if isinstance(address, tuple) and address else None
def checked(sock: socket.socket, address: object) -> None:
if sock.family in (socket.AF_INET, socket.AF_INET6) and \
host_of(address) not in LOOPBACK:
_refuse(host_of(address))
def guarded_connect(self: socket.socket, address: object) -> None:
checked(self, address)
return connect(self, address)
def guarded_connect_ex(self: socket.socket, address: object) -> int:
checked(self, address)
return connect_ex(self, address)
def guarded_getaddrinfo(host: object, *args: object, **kwargs: object) -> list:
if host is not None and str(host) not in LOOPBACK:
_refuse(host)
return getaddrinfo(host, *args, **kwargs)
socket.socket.connect = guarded_connect # type: ignore[method-assign]
socket.socket.connect_ex = guarded_connect_ex # type: ignore[method-assign]
socket.getaddrinfo = guarded_getaddrinfo # type: ignore[assignment]
ismount = os.path.ismount
def guarded_ismount(path: object) -> bool:
text = os.fspath(path) if not isinstance(path, str) else path
if isinstance(text, bytes):
text = text.decode(errors="replace")
if any(text == root or text.startswith(root + "/") for root in MOUNT_ROOTS):
return False
return ismount(path)
os.path.ismount = guarded_ismount # type: ignore[assignment]
def unguard() -> None:
"""The process as it was before guard()."""
if not _SAVED:
return
for name in ("connect", "connect_ex"):
if _SAVED[name] is None:
delattr(socket.socket, name)
else:
setattr(socket.socket, name, _SAVED[name])
socket.getaddrinfo = _SAVED["getaddrinfo"]
os.path.ismount = _SAVED["ismount"]
_SAVED.clear()
def enable(log: str) -> dict:
"""Guard this process and every Python it starts, the refusals written
to log. Returns the variables it changed, for disable()."""
before = {name: os.environ.get(name) for name in ("PYTHONPATH", NETLOG)}
here = os.path.dirname(os.path.abspath(__file__))
paths = [here, *[p for p in os.environ.get("PYTHONPATH", "").split(os.pathsep) if p]]
os.environ["PYTHONPATH"] = os.pathsep.join(dict.fromkeys(paths))
os.environ[NETLOG] = log
with open(log, "w", encoding="utf-8"):
pass
guard()
return before
def disable(before: dict) -> None:
"""This process and the Python it starts as before enable()."""
unguard()
for name, value in before.items():
if value is None:
os.environ.pop(name, None)
else:
os.environ[name] = value
def refused(log: str) -> list[str]:
"""The connections refused since enable(log), one line each."""
try:
with open(log, encoding="utf-8") as text:
return [line.strip() for line in text if line.strip()]
except OSError:
return []
if os.environ.get(NETLOG):
guard()
+406
View File
@@ -0,0 +1,406 @@
"""Arcade parent index: game set to BIOS sets and device sets, per core."""
from __future__ import annotations
import io
import json
import tempfile
import unittest
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TMP_ROOT = ROOT / "tmp" / "tests"
TMP_ROOT.mkdir(parents=True, exist_ok=True)
FIXTURES = ROOT / "tests" / "fixtures" / "arcade_parents"
COMMITTED = ROOT / "games" / "arcade"
from scripts.scraper import arcade_parent_importer as api # noqa: E402
try:
from jsonschema import Draft202012Validator
except ImportError: # validate_schemas.py is the only tool that needs it
Draft202012Validator = None
COMMIT = "a" * 40
def _sets(name: str, chunk: int | None = None) -> dict:
data = (FIXTURES / name).read_bytes()
saved = api.LISTXML_CHUNK
if chunk:
api.LISTXML_CHUNK = chunk
try:
return api.read_sets([lambda: io.BytesIO(data)])
finally:
api.LISTXML_CHUNK = saved
class ListxmlIndex(unittest.TestCase):
def setUp(self) -> None:
self.index = api.build_index(_sets("listxml.xml"), "isbios")
def test_clone_reaches_bios_through_its_parent(self) -> None:
self.assertEqual(self.index["bios"]["neogeo"], ["mslug", "mslugh"])
def test_every_bios_on_the_chain_is_listed(self) -> None:
self.assertIn("ikaruga", self.index["bios"]["naomigd"])
self.assertIn("ikaruga", self.index["bios"]["naomi"])
def test_device_with_roms_only(self) -> None:
self.assertEqual(self.index["devices"], {"m68705p3": ["mslug"]})
def test_chd_and_missing_parent(self) -> None:
self.assertEqual(self.index["chd"], ["ikaruga"])
self.assertEqual(self.index["missing_parents"], {"orphan": "gonebios"})
def test_sets_without_a_dump_are_not_games(self) -> None:
counts = self.index["counts"]
# mslug, mslugh, ikaruga, orphan, pacman
self.assertEqual(counts["games"], 5)
self.assertEqual(counts["games_with_bios"], 3)
self.assertEqual(counts["bios_sets"], 3)
def test_games_without_a_bios_and_the_bios_sets_are_named(self) -> None:
# A set on a card is then a game that needs nothing, a BIOS set,
# or a name the DAT does not hold: only the last falls back.
self.assertEqual(self.index["no_bios"], ["pacman"])
self.assertEqual(len(self.index["bios_sets"]), self.index["counts"]["bios_sets"])
self.assertIn("neogeo", self.index["bios_sets"])
self.assertNotIn("orphan", self.index["no_bios"])
def test_small_window_reads_the_same(self) -> None:
small = api.build_index(_sets("listxml.xml", chunk=64), "isbios")
self.assertEqual(api.dump(small), api.dump(self.index))
class RunnableMarker(unittest.TestCase):
def test_runnable_no_marks_the_bios(self) -> None:
index = api.build_index(_sets("runnable_no.dat"), "runnable")
self.assertEqual(index["bios"], {"neogeo": ["kof98"]})
self.assertEqual(index["missing_parents"], {"brvblade": "psarc95"})
self.assertEqual(index["counts"]["bios_sets"], 2)
def test_isbios_marker_ignores_runnable(self) -> None:
index = api.build_index(_sets("runnable_no.dat"), "isbios")
self.assertEqual(index["bios"], {})
class FlycastTable(unittest.TestCase):
def setUp(self) -> None:
self.index = api.flycast_index((FIXTURES / "naomi_roms.cpp").read_text())
def test_null_bios_is_naomi_and_filename_names_the_archive(self) -> None:
self.assertEqual(self.index["bios"]["naomi"], ["gram2000", "ikaruga", "jpgame"])
self.assertEqual(self.index["bios"]["awbios"], ["kofnw"])
self.assertNotIn("naomi-jp-d", self.index["bios"])
def test_gdrom_game_needs_a_chd(self) -> None:
self.assertEqual(self.index["chd"], ["ikaruga"])
def test_unknown_bios_is_reported(self) -> None:
self.assertEqual(self.index["missing_parents"], {"bad": "nosuchbios"})
class OfflineImport(unittest.TestCase):
"""import_profile reads the cache only, and two runs write the same bytes."""
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory(dir=TMP_ROOT)
self.cache = Path(self.temp.name)
dat = self.cache / "repo" / "libretro" / "mame2010-libretro" / COMMIT / "metadata"
dat.mkdir(parents=True)
(dat / "mame2010.xml").write_bytes((FIXTURES / "listxml.xml").read_bytes())
with zipfile.ZipFile(self.cache / "mame0000lx.zip", "w") as archive:
archive.write(FIXTURES / "listxml.xml", "mame0000.xml")
self.profile = {
"source": "https://github.com/libretro/mame2010-libretro",
"source_commit": COMMIT,
"core_version": "0.139",
}
def tearDown(self) -> None:
self.temp.cleanup()
def test_repo_profile_is_deterministic(self) -> None:
first = api.dump(api.import_profile("mame2010", self.profile, self.cache, offline=True))
second = api.dump(api.import_profile("mame2010", self.profile, self.cache, offline=True))
self.assertEqual(first, second)
doc = json.loads(first)
self.assertEqual(doc["bios"]["neogeo"], ["mslug", "mslugh"])
self.assertEqual(list(doc["source"]["files"]), ["metadata/mame2010.xml"])
def test_listxml_release_from_the_cache(self) -> None:
doc = api.import_profile("mame", {"core_version": "0.000"}, self.cache, offline=True)
self.assertEqual(doc["source"]["release"], "mamedev/mame mame0000")
self.assertEqual(doc["devices"], {"m68705p3": ["mslug"]})
@unittest.skipIf(Draft202012Validator is None, "jsonschema not installed")
def test_fixture_output_matches_the_schema(self) -> None:
doc = api.import_profile("mame2010", self.profile, self.cache, offline=True)
schema = json.loads((ROOT / "schemas" / "arcade-parents.schema.json").read_text())
self.assertEqual(list(Draft202012Validator(schema).iter_errors(doc)), [])
DRIVER_C = """/* driver.c is read twice, the list comes from the second pass */
#ifndef DRIVER_RECURSIVE
#define DRIVER_RECURSIVE
#include "driver.c"
#else
#ifdef TINY_COMPILE
DRIVER( tinyonly )
#else
DRIVER( game1 )
DRIVER( clone1 )
DRIVER( game2 )
DRIVER( hacked )
DRIVER( built1 )
DRIVER( diskgame )
#endif
#endif
"""
DRIVER_FILE = """#include "driver.h"
#include "board.h"
#define BOARD_BIOS \\
ROM_REGION( 0x100, REGION_USER1, 0 ) \\
ROM_LOAD( "b1.bin", 0, 0x100, CRC(12345678) SHA1(0000000000000000000000000000000000000000) )
ROM_START( bios1 )
BOARD_BIOS
ROM_END
ROM_START( bios2 )
ROM_REGION( 0x100, REGION_USER1, 0 )
ROM_LOAD( "b2.bin", 0, 0x100, NO_DUMP )
ROM_END
ROM_START( game1 )
ROM_LOAD( "g1.bin", 0, 0x100, CRC(11111111) SHA1(0000000000000000000000000000000000000000) )
ROM_END
ROM_START( clone1 )
ROM_LOAD( "c1.bin", 0, 0x100, CRC(22222222) SHA1(0000000000000000000000000000000000000000) )
ROM_END
ROM_START( game2 )
ROM_LOAD( "g2.bin", 0, 0x100, CRC(33333333) SHA1(0000000000000000000000000000000000000000) )
ROM_END
ROM_START( hacked )
ROM_LOAD( "h.bin", 0, 0x100, CRC(44444444) SHA1(0000000000000000000000000000000000000000) )
ROM_END
ROM_START( diskgame )
DISK_REGION( REGION_DISKS )
DISK_IMAGE( "disk", 0, MD5(00000000000000000000000000000000) )
ROM_END
#define SET( name, program ) \\
ROM_START( name ) \\
program \\
ROM_END
#define PROGRAM_A ROM_LOAD( "p.bin", 0, 0x100, CRC(55555555) SHA1(0000000000000000000000000000000000000000) )
SET( built1, PROGRAM_A )
GAMEX( 1995, bios1, 0, m, i, n, ROT0, "Maker", "Board BIOS", NOT_A_DRIVER )
GAMEX( 1995, bios2, 0, m, i, n, ROT0, "Maker", "Undumped BIOS", NOT_A_DRIVER )
GAMEX( 1995, game1, bios1, m, i, n, ROT0, "Maker", "Game (one, two)", 0 )
GAME( 1995, clone1, game1, m, i, n, ROT0, "Maker", "Clone" )
GAME( 1995, game2, bios2, m, i, n, ROT0, "Maker", "Game on an undumped BIOS" )
#if BOARD_HACK
GAME( 1995, hacked, bios1, m, i, n, ROT0, "Maker", "Hacked" )
#else
GAME( 1995, hacked, 0, m, i, n, ROT0, "Maker", "Not hacked" )
#endif
GAME( 1995, built1, bios1, m, i, n, ROT0, "Maker", "Built by a macro" )
GAME( 1995, diskgame, 0, m, i, n, ROT0, "Maker", "Disk game" )
"""
STALE_DAT = """<?xml version="1.0"?>
<mame>
<game name="game1" sourcefile="board.c" romof="oldbios">
<rom name="g1.bin" size="256" crc="11111111"/>
</game>
<game runnable="no" name="oldbios">
<rom name="o.bin" size="256" crc="77777777"/>
</game>
</mame>
"""
def _write_tarball(path: Path, files: dict[str, str]) -> None:
import tarfile
with tarfile.open(path, "w:gz") as archive:
for name, text in files.items():
data = text.encode("latin-1")
info = tarfile.TarInfo(f"repo-{COMMIT}/{name}")
info.size = len(data)
info.mode = 0o644
archive.addfile(info, io.BytesIO(data))
class DriverSource(unittest.TestCase):
"""mame2000, mame2003 and mame2003-plus are read from their driver code:
the DATs those repositories carry contradict it."""
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory(dir=TMP_ROOT)
self.cache = Path(self.temp.name)
slug = "libretro/mame2003-libretro"
tarball = self.cache / "src" / slug / f"{COMMIT}.tar.gz"
tarball.parent.mkdir(parents=True)
_write_tarball(tarball, {
"src/driver.c": DRIVER_C,
"src/driver.h": "#define NOT_A_DRIVER 0x4000\n",
"src/includes/board.h": "#define BOARD_HACK 0\n",
"src/drivers/board.c": DRIVER_FILE,
})
dat = self.cache / "repo" / slug / COMMIT / "metadata"
dat.mkdir(parents=True)
(dat / "mame2003.xml").write_text(STALE_DAT)
self.profile = {
"source": "https://github.com/libretro/mame2003-libretro",
"source_commit": COMMIT,
"core_version": "0.78",
}
self.report: dict = {}
self.doc = api.import_profile("mame2003", self.profile, self.cache, True, self.report)
def tearDown(self) -> None:
self.temp.cleanup()
def test_parents_come_from_the_game_macros(self) -> None:
self.assertEqual(self.doc["bios"], {"bios1": ["built1", "clone1", "game1"]})
def test_a_bios_without_a_dump_needs_no_archive(self) -> None:
self.assertNotIn("bios2", self.doc["bios"])
self.assertEqual(self.doc["counts"]["bios_sets"], 2)
def test_the_branch_the_build_compiles(self) -> None:
"""BOARD_HACK is 0 in the header the driver includes."""
self.assertNotIn("hacked", self.doc["bios"]["bios1"])
def test_the_tiny_build_list_is_left_out(self) -> None:
self.assertEqual(self.doc["counts"]["games"], 6)
def test_a_disk_image_is_a_chd(self) -> None:
self.assertEqual(self.doc["chd"], ["diskgame"])
def test_the_source_is_named_by_its_tree(self) -> None:
tree = self.doc["source"]["files"]["src"]
source = api.read_driver_source(self.cache / "src" / "libretro" / "mame2003-libretro" / f"{COMMIT}.tar.gz")
self.assertEqual(tree, source.tree)
self.assertEqual(list(self.doc["source"]["files"]), ["src"])
def test_the_committed_dat_is_reported_where_it_disagrees(self) -> None:
gaps = self.report["dat_gaps"]
self.assertEqual(gaps["romof"], ["game1"])
self.assertEqual(gaps["dat_only"], ["oldbios"])
self.assertIn("built1", gaps["code_only"])
def test_an_undeclared_driver_is_refused(self) -> None:
with self.assertRaises(api.DriverSourceError):
api.driver_source_sets("DRIVER( nosuch )", {"src/drivers/a.c": ""}, {})
def test_the_empty_tree(self) -> None:
self.assertEqual(api.git_tree_sha({}), "4b825dc642cb6eb9a060e54bf8d69288fbee4904")
@unittest.skipIf(Draft202012Validator is None, "jsonschema not installed")
def test_output_matches_the_schema(self) -> None:
schema = json.loads((ROOT / "schemas" / "arcade-parents.schema.json").read_text())
self.assertEqual(list(Draft202012Validator(schema).iter_errors(self.doc)), [])
FBNEO_LIST = """\t{ "1942", "pre90s/d_1942.cpp"},
\t{ "msx_1942", "msx/d_msx.cpp"},
\t{ "msx_msx", "msx/d_msx.cpp"},
\t{ "nes_zelda", "nes/d_nes.cpp"},
\t{ "fds_zelda", "nes/d_nes.cpp"},
\t{ "nes_smb", "nes/d_nes.cpp"},
"""
def _dat(*games: str) -> bytes:
return ("<datafile>" + "".join(games) + "</datafile>").encode()
def _game(name: str, source: str, romof: str | None = None) -> str:
parent = f' romof="{romof}"' if romof else ""
return (
f'<game name="{name}" sourcefile="{source}"{parent}>'
f'<rom name="{name}.bin" size="1" crc="12345678"/></game>'
)
class FbneoNamespaces(unittest.TestCase):
"""Each FBNeo DAT is its own namespace, a console set is its driver."""
def setUp(self) -> None:
names = api.fbneo_drivers(FBNEO_LIST)
arcade = _dat(_game("1942", "pre90s/d_1942.cpp"))
msx = _dat(
_game("1942", "msx/d_msx.cpp", "msx"),
'<game name="msx" sourcefile="msx/d_msx.cpp" isbios="yes">'
'<rom name="msx.rom" size="1" crc="87654321"/></game>',
)
nes = _dat(_game("zelda", "nes/d_nes.cpp"), _game("smb", "nes/d_nes.cpp"))
sets, self.archives = api.fbneo_sets(
[lambda d=d: io.BytesIO(d) for d in (arcade, msx, nes)], names
)
self.sets = sets
self.index = api.build_index(sets, "isbios", self.archives)
def test_the_msx_set_needs_the_msx_bios_and_the_arcade_one_does_not(self) -> None:
self.assertEqual(self.index["bios"], {"msx": ["msx_1942"]})
self.assertIn("1942", self.sets)
def test_the_bios_is_named_by_its_archive(self) -> None:
self.assertEqual(self.archives["msx_msx"], "msx")
def test_a_shared_source_file_takes_the_prefix_of_its_dat(self) -> None:
self.assertIn("nes_zelda", self.sets)
self.assertNotIn("fds_zelda", self.sets)
class DeclaredArchives(unittest.TestCase):
def test_names_aliases_and_archives(self) -> None:
profile = {"files": [
{"name": "neogeo.zip"},
{"name": "sp-s2.sp1", "archive": "neogeo.zip"},
{"name": "pgm.zip", "aliases": ["PGMBIOS.zip"]},
]}
self.assertEqual(api.declared_archives(profile), {"neogeo.zip", "sp-s2.sp1", "pgm.zip", "pgmbios.zip"})
class CommittedIndex(unittest.TestCase):
def test_every_profile_has_its_file_above_the_floor(self) -> None:
for profile in api.PROFILES:
with self.subTest(profile=profile):
path = COMMITTED / f"{profile}.json"
self.assertTrue(path.is_file(), path)
doc = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(doc["profile"], profile)
self.assertGreaterEqual(doc["counts"]["games_with_bios"], api.FLOORS[profile])
def test_committed_files_are_written_canonically(self) -> None:
for path in sorted(COMMITTED.glob("*.json")):
with self.subTest(path=path.name):
text = path.read_text(encoding="utf-8")
self.assertEqual(api.dump(json.loads(text)), text)
@unittest.skipIf(Draft202012Validator is None, "jsonschema not installed")
def test_committed_files_match_the_schema(self) -> None:
schema = json.loads((ROOT / "schemas" / "arcade-parents.schema.json").read_text())
validator = Draft202012Validator(schema)
for path in sorted(COMMITTED.glob("*.json")):
with self.subTest(path=path.name):
errors = list(validator.iter_errors(json.loads(path.read_text(encoding="utf-8"))))
self.assertEqual(errors, [])
if __name__ == "__main__":
unittest.main()
+7 -5
View File
@@ -24,20 +24,22 @@ from cross_reference import ( # noqa: E402
class FlatDeclaredSet(unittest.TestCase):
def test_a_name_declared_under_another_system_id_is_not_a_gap(self):
"""A platform files the Super Game Boy BIOS under nintendo-sgb, the
profile under nintendo-super-game-boy: the ids never meet."""
"""A platform files a BIOS under one system id, the profile under
another that no alias joins: per system the ids never meet.
nintendo-sgb and nintendo-super-game-boy were such a pair until
SYSTEM_ALIASES joined them."""
import tempfile # noqa: PLC0415
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
platforms = Path(tmp)
(platforms / "fx.yml").write_text(
"platform: Fx\nverification_mode: existence\nsystems:\n"
" nintendo-sgb:\n files:\n - name: sgb.boot.rom\n"
" fx-console:\n files:\n - name: sgb.boot.rom\n"
" destination: sgb.boot.rom\n"
)
profiles = {"sgbcore": {
"emulator": "SGB", "type": "libretro", "systems": ["nintendo-super-game-boy"],
"files": [{"name": "sgb.boot.rom", "system": "nintendo-super-game-boy"}],
"emulator": "SGB", "type": "libretro", "systems": ["fy-console"],
"files": [{"name": "sgb.boot.rom", "system": "fy-console"}],
}}
db = {"files": {}, "indexes": {"by_name": {}, "by_md5": {}, "by_crc32": {},
"by_path_suffix": {}}}
+53
View File
@@ -175,6 +175,59 @@ class InstallerOutputExample(unittest.TestCase):
self.assertLessEqual(abs(added - float(total) * units[unit]), 0.15 * units[unit])
class InstallerSamplesSayWhatTheCodePrints(unittest.TestCase):
"""code_audit_24 P3-8 to P3-12: samples typed by hand outside the
generated blocks, a sentence on a log read as the terminal's, pages that
described the generator as tables alone, and a sandbox that asked DNS."""
def test_the_refused_and_from_samples_are_generated(self):
import install
page = _page("installer.md")
refused = _generated(page, "check-refused")
self.assertRegex(refused, r"\n \d+ files? [a-z ]+:\n")
self.assertIn("read by beetle_psx, pcsx_rearmed at", refused)
self.assertNotRegex(page, r"opened at /\S+ by ")
self.assertIn('"read by ', inspect.getsource(install.opened_line))
ending = _generated(page, "from-run")
self.assertRegex(ending, r"Done\. \d+ files installed\.\n \d+ files copied from ")
self.assertNotIn("a line under\n`Done`", page)
self.assertNotIn("38 files copied from E:\\, 4 downloaded", page)
def test_one_line_per_file_is_said_of_a_log(self):
page = _page("installer.md")
at = page.index("<!-- generated: run-download -->")
before = page[page.rindex("\n\n", 0, at - 2):at]
self.assertIn("At a terminal one progress line", before)
self.assertIn("A pipe or a log gets one line per file", before)
def test_the_generator_pages_name_the_run_samples_and_step_7(self):
tools = _page("tools.md")
row = next(line for line in tools.splitlines()
if line.startswith("| `generate_installer_doc.py`"))
self.assertIn("run samples", row)
self.assertIn("generate_installer_doc.py", next(
line for line in tools.splitlines() if line.startswith("| 7 |")))
self.assertIn("run samples", _page("installer.md").split(
"The tables of options, events, exit codes, keys and card markers", 1)[1][:600])
self.assertIn("generate_installer_doc.py", next(
line for line in _page("adding-a-platform.md").splitlines()
if line.startswith("7. ")))
def test_the_sandbox_looks_nothing_up(self):
import socket
from unittest import mock
import generate_installer_doc
def no_lookup(*_args: object) -> str:
raise AssertionError("the sandbox asked for a name")
with mock.patch.object(socket, "getfqdn", no_lookup), \
generate_installer_doc.Sandbox() as box:
self.assertEqual(box.server.server_name, "127.0.0.1")
class OneResolutionChain(unittest.TestCase):
"""architecture.md drew path_suffix as the first step, before SHA1, while
the code and verification-modes.md try it after every hash."""
+177
View File
@@ -0,0 +1,177 @@
"""Game catalogs: DAT import, region tags, determinism, floors, schema."""
from __future__ import annotations
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TMP_ROOT = ROOT / "tmp" / "tests"
TMP_ROOT.mkdir(parents=True, exist_ok=True)
FIXTURES = ROOT / "tests" / "fixtures" / "game_dats"
sys.path.insert(0, str(ROOT / "scripts"))
from region import REGIONS # noqa: E402
from scripts.scraper import game_dat_importer as importer # noqa: E402
try:
from jsonschema import Draft202012Validator
except ImportError: # pragma: no cover - CI installs it
Draft202012Validator = None
def run_import(source: str, output: Path, *extra: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, "-m", "scripts.scraper.game_dat_importer", "--source", source,
"--pack", str(FIXTURES), "--output-dir", str(output), *extra],
cwd=ROOT, capture_output=True, text=True, check=False,
)
class RegionTags(unittest.TestCase):
def test_first_territory_group_wins(self) -> None:
self.assertEqual(importer.name_regions("Game (USA) (Rev 1)"), ["north-america"])
self.assertEqual(importer.name_regions("Game (USA, Europe)"), ["europe", "north-america"])
self.assertEqual(importer.name_regions("Game (En,Fr) (Japan)"), ["japan"])
def test_world_absorbs(self) -> None:
self.assertEqual(importer.name_regions("Game (World)"), ["world"])
def test_untagged_and_unmapped(self) -> None:
self.assertIsNone(importer.name_regions("Game (Rev 1)"))
self.assertIsNone(importer.name_regions("Game (South Africa)"))
def test_extra_words_fall_under_their_super_region(self) -> None:
self.assertEqual(importer.name_regions("Game (Scandinavia)"), ["europe"])
def test_every_mapping_is_vocabulary(self) -> None:
targets = set(importer._TERRITORY.values())
self.assertLessEqual(targets, set(REGIONS))
class FixtureImport(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory(dir=TMP_ROOT)
self.out = Path(self.temp.name)
def tearDown(self) -> None:
self.temp.cleanup()
def test_nointro_rows(self) -> None:
result = run_import("no-intro", self.out)
self.assertEqual(result.returncode, 0, result.stderr)
files = sorted(p.relative_to(self.out).as_posix() for p in self.out.rglob("*.json"))
self.assertEqual(files, ["no-intro/nintendo-snes.json"])
doc = json.loads((self.out / files[0]).read_text())
self.assertEqual(doc["fields"], ["crc32", "size", "sha1", "region", "title"])
self.assertEqual(len(doc["games"]), 5)
rows = {row[4]: row for row in doc["games"]}
self.assertEqual(rows["Zeta Quest (USA)"][:3],
["0000beef", 1048576, "1" * 40])
self.assertEqual(doc["regions"][rows["Both Worlds (USA, Europe) (Rev 1)"][3]],
["europe", "north-america"])
self.assertIsNone(rows["Untagged Demo"][3])
self.assertEqual([r[0] for r in doc["games"]], sorted(r[0] for r in doc["games"]))
self.assertIn("freely used", doc["licence"]["quote"])
self.assertEqual(doc["licence"]["mirror"]["licence"], "MIT")
def test_redump_first_data_track_and_serials(self) -> None:
result = run_import("redump", self.out)
self.assertEqual(result.returncode, 0, result.stderr)
doc = json.loads((self.out / "redump" / "sega-megacd.json").read_text())
rows = {row[4]: row for row in doc["games"]}
self.assertEqual(rows["Sonic CD (Europe)"][:2], ["0060ac74", 129590496])
self.assertEqual(rows["Sonic CD (USA)"][5], ["4407", "T-0000"])
self.assertEqual(doc["archive"]["dat_date"], "2026-10-23")
self.assertIn("public domain", doc["licence"]["quote"])
self.assertIn("serial_note", doc)
coverage = importer.coverage(doc)
self.assertEqual(coverage["serials"], 3)
self.assertEqual(coverage["multi_region_serials"], 1)
def test_two_imports_are_byte_identical(self) -> None:
first, second = self.out / "a", self.out / "b"
for out in (first, second):
for source in ("no-intro", "redump"):
self.assertEqual(run_import(source, out).returncode, 0)
for path in first.rglob("*.json"):
twin = second / path.relative_to(first)
self.assertEqual(path.read_bytes(), twin.read_bytes(), path.name)
def test_floor_refuses_a_short_dat(self) -> None:
result = run_import("no-intro", self.out, "--floor", "--dry-run")
self.assertEqual(result.returncode, 1)
self.assertIn("floor 3920", result.stderr)
self.assertEqual(list(self.out.rglob("*.json")), [])
def test_offline_without_cache_names_the_remedy(self) -> None:
result = subprocess.run(
[sys.executable, "-m", "scripts.scraper.game_dat_importer", "--source", "redump",
"--cache-dir", str(self.out / "empty"), "--output-dir", str(self.out)],
cwd=ROOT, capture_output=True, text=True, check=False,
)
self.assertEqual(result.returncode, 1)
self.assertIn("--fetch", result.stderr)
class Selection(unittest.TestCase):
def test_every_system_is_a_repository_system(self) -> None:
from romfolders import SystemResolver
ids = SystemResolver.from_repo().ids
wanted = [s for s, _ in importer.NOINTRO_DATS.values()]
wanted += [s for _, s, _ in importer.REDUMP_DATS.values()]
self.assertEqual(len(wanted), len(set(wanted)))
self.assertEqual([s for s in wanted if s not in ids], [])
self.assertEqual(len(importer.NOINTRO_DATS), 29)
self.assertEqual(len(importer.REDUMP_DATS), 9)
@unittest.skipIf(Draft202012Validator is None, "jsonschema not installed")
class CommittedCatalogs(unittest.TestCase):
def setUp(self) -> None:
schema = json.loads((ROOT / "schemas" / "game-catalog.schema.json").read_text())
self.validator = Draft202012Validator(schema)
self.paths = sorted((ROOT / "games" / "catalogs").glob("*/*.json"))
def test_every_catalog_matches_the_schema(self) -> None:
self.assertEqual(len(self.paths), 38)
for path in self.paths:
doc = json.loads(path.read_text())
errors = [e.message for e in self.validator.iter_errors(doc)]
self.assertEqual(errors, [], path.name)
def test_committed_catalogs_meet_their_floor(self) -> None:
for path in self.paths:
doc = json.loads(path.read_text())
self.assertEqual(path.stem, doc["system"])
self.assertGreaterEqual(importer.coverage(doc)["games"], importer.floor_of(doc),
path.name)
def test_region_slugs_are_vocabulary(self) -> None:
for path in self.paths:
doc = json.loads(path.read_text())
for regions in doc["regions"]:
self.assertLessEqual(set(regions), set(REGIONS), path.name)
def test_rewrite_is_a_no_op(self) -> None:
for path in self.paths[:3]:
doc = json.loads(path.read_text())
self.assertEqual(importer.dump_catalog(doc), path.read_text())
def test_fixture_output_matches_the_schema(self) -> None:
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as directory:
out = Path(directory)
for source in ("no-intro", "redump"):
self.assertEqual(run_import(source, out).returncode, 0)
for path in out.rglob("*.json"):
errors = [e.message for e in self.validator.iter_errors(json.loads(path.read_text()))]
self.assertEqual(errors, [], path.name)
if __name__ == "__main__":
unittest.main()
+211
View File
@@ -0,0 +1,211 @@
"""Header readers: schema, references between readers and cores, pinned citations."""
from __future__ import annotations
import json
import re
import sys
import unittest
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from common import load_emulator_profiles # noqa: E402
from region import REGIONS # noqa: E402
try:
from jsonschema import Draft202012Validator
except ImportError: # pragma: no cover - CI installs it
Draft202012Validator = None
HEADERS = ROOT / "games" / "headers.yml"
_CITATION = re.compile(r"^([\w.-]+/[\w.-]+)@([0-9a-f]{8,40}) ")
# Tools the containers cite that are not emulators: no profile pins them.
TOOL_REPOS = {"python/cpython", "ip7z/7zip"}
def load() -> dict:
return yaml.safe_load(HEADERS.read_text(encoding="utf-8"))
def pins(profile: dict) -> set[str]:
out: set[str] = set()
for key in ("source_commit", "upstream_commit"):
value = profile.get(key)
if isinstance(value, dict):
out.update(str(v) for v in value.values() if v)
elif value:
out.add(str(value))
return out
def outcomes(node: object) -> list[list[str]]:
"""Every region list anywhere below a node."""
found: list[list[str]] = []
if isinstance(node, dict):
for key, value in node.items():
if key == "regions" and isinstance(value, list):
found.append(value)
else:
found += outcomes(value)
elif isinstance(node, list):
if node and all(isinstance(v, str) and v in REGIONS for v in node):
found.append(node)
for value in node:
found += outcomes(value)
return found
def decode(reader: dict, raw: object) -> object:
"""A reference decoder for the kinds whose rule is a plain lookup."""
rule = reader["decode"]
kind = rule["kind"]
for entry in rule["values"]:
value = entry["value"]
hit = (
(kind == "exact-prefix" and str(raw).startswith(value))
or (kind == "prefix-nocase" and str(raw).lower().startswith(value))
or (kind == "integer" and raw == value)
or (kind == "char-at" and len(str(raw)) > rule["index"]
and str(raw)[rule["index"]] == value)
)
if hit:
return entry.get("regions") or entry.get("standard") or entry.get("label")
return rule["otherwise"]
def check(cond: dict, data: bytes) -> bool:
"""A reference evaluator for the byte, text and size tests of a predicate."""
if "all" in cond:
return all(check(c, data) for c in cond["all"])
if "any" in cond:
return any(check(c, data) for c in cond["any"])
if "file_size" in cond:
bounds = cond["file_size"]
return bounds.get("min", 0) <= len(data) <= bounds.get("max", len(data))
offset = cond["offset"]
if "text" in cond or "not_text" in cond:
want = cond.get("text", cond.get("not_text"))
hit = data[offset:offset + len(want)] == want.encode("latin-1")
return hit if "text" in cond else not hit
value = data[offset] & cond.get("mask", 0xFF)
if "equals" in cond:
return value == cond["equals"]
if "not_equals" in cond:
return value != cond["not_equals"]
return value in cond["in"]
class HeaderReaders(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.doc = load()
cls.profiles = load_emulator_profiles(str(ROOT / "emulators"))
@unittest.skipIf(Draft202012Validator is None, "jsonschema not installed")
def test_schema(self) -> None:
schema = json.loads((ROOT / "schemas" / "game-headers.schema.json").read_text())
errors = [f"{list(e.path)}: {e.message}" for e in Draft202012Validator(schema).iter_errors(self.doc)]
self.assertEqual(errors, [])
def test_core_order_names_readers_of_its_system(self) -> None:
for system, block in self.doc["systems"].items():
for core, spec in block["cores"].items():
missing = [r for r in spec["order"] if r not in block["readers"]]
self.assertEqual(missing, [], f"{system}/{core}")
def test_every_core_is_a_profile(self) -> None:
for system, block in self.doc["systems"].items():
for core in block["cores"]:
self.assertIn(core, self.profiles, system)
def test_every_system_is_a_repository_system(self) -> None:
from romfolders import SystemResolver
ids = SystemResolver.from_repo().ids
self.assertEqual([s for s in self.doc["systems"] if s not in ids], [])
def test_regions_are_vocabulary(self) -> None:
for regions in outcomes(self.doc["systems"]):
self.assertLessEqual(set(regions), set(REGIONS), regions)
def test_core_citations_name_the_profile_pin(self) -> None:
for system, block in self.doc["systems"].items():
for core, spec in block["cores"].items():
match = _CITATION.match(spec["source_ref"])
self.assertIsNotNone(match, f"{system}/{core}")
sha = match.group(2)
self.assertTrue(any(p.startswith(sha) for p in pins(self.profiles[core])),
f"{system}/{core}: {sha} is not a pin of the profile")
def test_reader_citations_name_a_profile_pin(self) -> None:
every_pin = {p for profile in self.profiles.values() for p in pins(profile)}
refs = [(f"{s}/{r}", spec["source_ref"]) for s, block in self.doc["systems"].items()
for r, spec in block["readers"].items()]
refs += [(f"containers/{c}", spec["source_ref"]) for c, spec in self.doc["containers"].items()]
for label, ref in refs:
match = _CITATION.match(ref)
self.assertIsNotNone(match, label)
if match.group(1) in TOOL_REPOS:
continue
self.assertTrue(any(p.startswith(match.group(2)) for p in every_pin),
f"{label}: {match.group(2)} pins no profile")
def test_reference_decodes(self) -> None:
psx = self.doc["systems"]["sony-playstation"]["readers"]
self.assertEqual(decode(psx["psx-serial-prefix"], "SLUS_012.34"), ["north-america"])
self.assertEqual(decode(psx["psx-serial-prefix"], "SCES_000.01"), ["europe"])
self.assertEqual(decode(psx["psx-serial-prefix"], "XXXX_000.01"), "unknown")
self.assertEqual(decode(psx["psx-serial-letter"], "SLPS_012.04"), ["japan"])
self.assertEqual(decode(psx["psx-licence-offset60"], "Euro pe "), ["europe"])
mcd = self.doc["systems"]["sega-megacd"]["readers"]["mcd-security-code"]
self.assertEqual(decode(mcd, 0x64), ["europe"])
self.assertEqual(decode(mcd, 0x7A), ["north-america"])
gc = self.doc["systems"]["nintendo-gamecube"]["readers"]["gc-region"]
self.assertEqual(decode(gc, 3), "unknown")
n64 = self.doc["systems"]["nintendo-64"]["readers"]["n64-country"]
self.assertEqual(decode(n64, 0x45), {"standard": "ntsc"})
self.assertEqual(decode(n64, 0x50), "pal")
def test_reader_ids_are_unique(self) -> None:
ids = [r for block in self.doc["systems"].values() for r in block["readers"]]
self.assertEqual(len(ids), len(set(ids)))
def test_every_reader_extras_names_exists(self) -> None:
extras = yaml.safe_load((ROOT / "games" / "extras.yml").read_text(encoding="utf-8"))
defined = {r for block in self.doc["systems"].values() for r in block["readers"]}
named = {
entry["trigger"]["header"]["reader"]
for entry in extras["extras"]
if "header" in entry.get("trigger", {})
}
self.assertTrue(named)
self.assertEqual(sorted(named - defined), [])
def test_predicate_readers_decode_reference_bytes(self) -> None:
systems = self.doc["systems"]
sgb = systems["nintendo-sgb"]["readers"]["gb-sgb-flag"]["decode"]
header = bytearray(0x150)
header[0x146], header[0x14B] = 0x03, 0x33
self.assertTrue(all(check(c, bytes(header)) for c in sgb["all"]))
header[0x143] = 0x80
self.assertFalse(all(check(c, bytes(header)) for c in sgb["all"]))
sufami = systems["nintendo-sufami-turbo"]["readers"]["sufami-magic"]["decode"]
cart = bytearray(0x80000)
cart[0:14] = b"BANDAI SFC-ADX"
self.assertTrue(all(check(c, bytes(cart)) for c in sufami["all"]))
cart[0x10:0x1E] = b"SFC-ADX BACKUP"
self.assertFalse(all(check(c, bytes(cart)) for c in sufami["all"]))
def test_cores_that_pick_no_bios_read_nothing_for_it(self) -> None:
for system, block in self.doc["systems"].items():
for core, spec in block["cores"].items():
if spec.get("selects_bios") is False:
self.assertNotIn("preference", spec, f"{system}/{core}")
if __name__ == "__main__":
unittest.main()
+202
View File
@@ -0,0 +1,202 @@
"""games/regions.yml and games/extras.yml: the rules the games-on-card mode reads.
Every rule names a profile and files that profile declares, cites the code
at the profile's own pin, and speaks the region vocabulary of region.py.
The checker is run on small fixtures, once clean and once per way a rule
can lose its backing, then on the repository's own files.
"""
from __future__ import annotations
import copy
import json
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import game_rules # noqa: E402
from common import load_emulator_profiles # noqa: E402
FIXTURES = REPO_ROOT / "tests" / "fixtures" / "game_rules"
# The cases the research names where a proven game region does not settle
# the file (games_on_card_research.md section 2).
EXPECTED_CASES = {
"user-region-option", "psx-override-bios", "duckstation-custom-region",
"cross-region-fallback", "first-disc-region", "multi-area-disc",
"saturn-kr-ipl", "saturn-disc-database", "sms-crc-list",
"megacd-region-list", "gamecube-unknown-region", "naomi-one-archive",
"nds-header-decides", "optional-pif",
}
def _schema_validator(name: str):
try:
from jsonschema import Draft202012Validator
except ImportError as exc:
raise unittest.SkipTest(f"schema check needs jsonschema: {exc}") from exc
schema = json.loads((REPO_ROOT / "schemas" / name).read_text(encoding="utf-8"))
Draft202012Validator.check_schema(schema)
return Draft202012Validator(schema)
class FixtureChecks(unittest.TestCase):
def setUp(self) -> None:
self.profiles = load_emulator_profiles(str(FIXTURES / "emulators"))
self.regions = game_rules.load_yaml(FIXTURES / "regions.yml")
self.extras = game_rules.load_yaml(FIXTURES / "extras.yml")
def errors(self, regions=None, extras=None) -> list[str]:
return game_rules.check(
regions if regions is not None else self.regions,
extras if extras is not None else self.extras,
self.profiles,
)
def psx(self, regions: dict) -> dict:
return regions["systems"]["sony-playstation"]["psxcore"]
def test_clean_fixture_passes(self) -> None:
self.assertEqual(self.errors(), [])
def test_unknown_profile(self) -> None:
extras = copy.deepcopy(self.extras)
extras["extras"][0]["profile"] = "nocore"
self.assertTrue(any("no profile nocore" in e for e in self.errors(extras=extras)))
def test_system_the_profile_does_not_run(self) -> None:
extras = copy.deepcopy(self.extras)
extras["extras"][0]["system"] = "sega-saturn"
self.assertTrue(any("does not declare system" in e for e in self.errors(extras=extras)))
def test_undeclared_file(self) -> None:
extras = copy.deepcopy(self.extras)
extras["extras"][0]["files"] = ["FdsBios.bin"]
self.assertTrue(any("declares no file FdsBios.bin" in e for e in self.errors(extras=extras)))
def test_alias_counts_as_declared(self) -> None:
regions = copy.deepcopy(self.regions)
self.psx(regions)["keep_all_regions"][0]["files"] = ["scph7003.bin"]
self.assertFalse(any("declares no file" in e for e in self.errors(regions=regions)))
def test_unknown_region(self) -> None:
regions = copy.deepcopy(self.regions)
self.psx(regions)["default_region"] = "usa-canada"
self.assertTrue(any("unknown region usa-canada" in e for e in self.errors(regions=regions)))
def test_route_to_a_region_no_file_carries(self) -> None:
regions = copy.deepcopy(self.regions)
self.psx(regions)["routes"] = {"south-korea": "oceania"}
self.assertTrue(any("no file is tagged oceania" in e for e in self.errors(regions=regions)))
def test_citation_at_another_revision(self) -> None:
regions = copy.deepcopy(self.regions)
self.psx(regions)["source_ref"] = "example/psxcore@deadbeef core.c:10"
self.assertTrue(any("not a pin of the profile" in e for e in self.errors(regions=regions)))
def test_missing_citation(self) -> None:
regions = copy.deepcopy(self.regions)
del self.psx(regions)["keep_all_regions"][0]["source_ref"]
self.assertTrue(any("no source_ref" in e for e in self.errors(regions=regions)))
def test_case_without_description(self) -> None:
regions = copy.deepcopy(self.regions)
self.psx(regions)["keep_all_regions"][0]["case"] = "made-up"
self.assertTrue(any("not described" in e for e in self.errors(regions=regions)))
def test_region_free_file_tagged_with_a_region(self) -> None:
regions = copy.deepcopy(self.regions)
self.psx(regions)["region_free"] = ["scph5500.bin"]
self.assertTrue(any("tagged ['japan']" in e for e in self.errors(regions=regions)))
def header_errors(self, header: dict) -> list[str]:
extras = copy.deepcopy(self.extras)
extras["extras"][0]["trigger"] = {"header": header}
headers = game_rules.load_yaml(FIXTURES / "headers.yml")
return game_rules.check(self.regions, extras, self.profiles, headers)
def test_header_trigger_takes_a_label_the_reader_returns(self) -> None:
self.assertEqual(self.header_errors({"reader": "fds-image", "values": ["fds"]}), [])
def test_header_trigger_with_free_text(self) -> None:
errors = self.header_errors({"reader": "fds-image", "values": ["FDS header at 0"]})
self.assertTrue(any("never returns 'FDS header at 0'" in e for e in errors))
def test_header_trigger_naming_no_reader(self) -> None:
errors = self.header_errors({"reader": "fds-magic", "values": ["fds"]})
self.assertTrue(any("no reader fds-magic" in e for e in errors))
def test_serial_reader_takes_any_value(self) -> None:
self.assertEqual(self.header_errors({"reader": "dc-product-id", "values": ["T7001D 50"]}), [])
def test_header_field_value(self) -> None:
self.assertEqual(
self.header_errors({"reader": "dc-product-id", "field": "disk_type", "values": ["MIL CD"]}), [])
errors = self.header_errors({"reader": "dc-product-id", "field": "area", "values": ["J"]})
self.assertTrue(any("has no field area" in e for e in errors))
def test_summary_is_stable(self) -> None:
first = game_rules.summary(self.regions, self.extras)
second = game_rules.summary(copy.deepcopy(self.regions), copy.deepcopy(self.extras))
self.assertEqual(json.dumps(first, sort_keys=True), json.dumps(second, sort_keys=True))
self.assertEqual(first["case_entries"][0]["case"], "psx-override-bios")
class FixtureSchemas(unittest.TestCase):
def test_fixtures_match_the_schemas(self) -> None:
for name, schema in (("regions.yml", "game-regions.schema.json"),
("extras.yml", "game-extras.schema.json")):
errors = list(_schema_validator(schema).iter_errors(
game_rules.load_yaml(FIXTURES / name)))
self.assertEqual(errors, [], name)
def test_schema_refuses_an_option_case_without_its_option(self) -> None:
regions = game_rules.load_yaml(FIXTURES / "regions.yml")
keep = regions["systems"]["sony-playstation"]["psxcore"]["keep_all_regions"][0]
del keep["option"]
self.assertTrue(list(_schema_validator("game-regions.schema.json").iter_errors(regions)))
def test_schema_refuses_two_triggers(self) -> None:
extras = game_rules.load_yaml(FIXTURES / "extras.yml")
extras["extras"][0]["trigger"]["every_game"] = True
self.assertTrue(list(_schema_validator("game-extras.schema.json").iter_errors(extras)))
class RepositoryRules(unittest.TestCase):
"""The committed files, against the committed profiles."""
@classmethod
def setUpClass(cls) -> None:
cls.regions = game_rules.load_yaml(REPO_ROOT / game_rules.REGIONS_FILE)
cls.extras = game_rules.load_yaml(REPO_ROOT / game_rules.EXTRAS_FILE)
def test_every_rule_is_backed_by_its_profile(self) -> None:
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
headers = game_rules.load_yaml(REPO_ROOT / game_rules.HEADERS_FILE)
self.assertEqual(game_rules.check(self.regions, self.extras, profiles, headers), [])
def test_files_match_the_schemas(self) -> None:
self.assertEqual(list(_schema_validator("game-regions.schema.json").iter_errors(self.regions)), [])
self.assertEqual(list(_schema_validator("game-extras.schema.json").iter_errors(self.extras)), [])
def test_the_fourteen_cases_are_encoded_and_used(self) -> None:
self.assertEqual(set(self.regions["cases"]), EXPECTED_CASES)
used = {row["case"] for row in game_rules.summary(self.regions, self.extras)["case_entries"]}
self.assertEqual(used, EXPECTED_CASES)
def test_region_free_files_never_carry_a_region(self) -> None:
# The installer keeps them for every region: a profile tag would let
# a region filter drop what the code opens for every game.
for system, cores in self.regions["systems"].items():
for core, rule in cores.items():
for keep in rule.get("keep_all_regions") or []:
if keep["keeps"] == "region-free-files":
self.assertTrue(set(keep["files"]) <= set(rule.get("region_free") or []),
f"{system}/{core}")
if __name__ == "__main__":
unittest.main()
+214
View File
@@ -0,0 +1,214 @@
"""install/games/: what the games-on-card mode reads, built from fixtures.
Profiles and platform files are the repository's own (read only). The rom
folders, the install list, the catalogs and the arcade index are fixtures,
so a test pins the join and not the state of the data.
"""
from __future__ import annotations
import json
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TMP_ROOT = ROOT / "tmp" / "tests"
TMP_ROOT.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(ROOT / "scripts"))
sys.path.insert(0, str(ROOT))
import generate_games_index as gi # noqa: E402
from romfolders import SystemResolver, load_setups # noqa: E402
SETUP = """\
setup: fixture
name: Fixture OS
platform: fixtureplat
sources:
os: {repo: https://github.com/example/os, commit: 0123456789abcdef0123456789abcdef01234567}
roms_root: {path: /storage/roms, from_bios: ../roms, source_ref: os main.c:12}
case_sensitive: true
skip:
- {name: bios, source_ref: os main.c:20}
folders:
- {folder: psx, extensions: [.cue, .chd], core: mednafen_psx_hw, source_ref: os es_systems.cfg:10,
board_defaults: {rpi1: {core: pcsx_rearmed}}}
- {folder: mame, extensions: [.zip], core: mame, arcade: true, source_ref: os es_systems.cfg:30}
- {folder: zzunknownzz, extensions: [.bin], core: nosuchcore, source_ref: os es_systems.cfg:40}
"""
MANIFEST = {
"platform": "fixtureplat",
"core_names": {"beetle_psx": ["mednafen_psx_hw"]},
"files": [
{"dest": "scph5501.bin", "size": 524288, "cores": None, "systems": ["sony-playstation"]},
{"dest": "psxonpsp660.bin", "size": 524288, "cores": ["beetle_psx"]},
{"dest": "neogeo.zip", "size": 1000, "cores": ["mame"]},
{"dest": "other.bin", "size": 7, "cores": ["snes9x"]},
],
"omitted_files": [],
}
CATALOG = {
"source": "no-intro",
"system": "sony-playstation",
"dat": "Fixture",
"version": "1",
"fetched_from": "https://example.org/x.zip",
"licence": {"quote": "free"},
"fields": ["crc32", "size", "sha1", "region", "title", "serials"],
"regions": [["japan"], ["north-america"]],
"games": [
["ffffffff", 10, "aa", 1, "B (USA)", ["SLUS-00001"]],
["00000001", 20, "bb", 0, "A (Japan)", ["SLPS-00001"]],
],
}
ARCADE = {"profile": "mame", "bios": {"neogeo": ["mslug"]}}
class GamesIndexFixture(unittest.TestCase):
def setUp(self) -> None:
self.tmp = Path(tempfile.mkdtemp(dir=TMP_ROOT))
(self.tmp / "romfolders").mkdir()
(self.tmp / "romfolders" / "fixture.yml").write_text(SETUP, encoding="utf-8")
(self.tmp / "install").mkdir()
(self.tmp / "install" / "fixtureplat.json").write_text(json.dumps(MANIFEST), encoding="utf-8")
catalogs = self.tmp / "games" / "catalogs" / "no-intro"
catalogs.mkdir(parents=True)
(catalogs / "sony-playstation.json").write_text(json.dumps(CATALOG), encoding="utf-8")
(self.tmp / "games" / "arcade").mkdir()
(self.tmp / "games" / "arcade" / "mame.json").write_text(json.dumps(ARCADE), encoding="utf-8")
self.inputs = gi.Inputs(
games=self.tmp / "games",
romfolders=self.tmp / "romfolders",
install=self.tmp / "install",
emulators=str(ROOT / "emulators"),
platforms=str(ROOT / "platforms"),
)
# The fixture platform has no YAML of its own.
original = gi._platform_required
gi._platform_required = lambda platform, platforms_dir: {}
self.addCleanup(setattr, gi, "_platform_required", original)
def tearDown(self) -> None:
shutil.rmtree(self.tmp, ignore_errors=True)
def render(self) -> tuple[dict[Path, str], dict]:
return gi.render(self.tmp / "out", self.inputs)
def doc(self, files: dict[Path, str], rel: str) -> dict:
return json.loads(files[self.tmp / "out" / rel])
def test_two_runs_write_the_same_bytes(self) -> None:
first, _ = self.render()
second, _ = self.render()
self.assertEqual(first, second)
def test_folders_resolve_to_systems_and_profiles(self) -> None:
files, coverage = self.render()
folders = {f["folder"]: f for f in self.doc(files, "setups/fixture.json")["folders"]}
self.assertEqual(folders["psx"]["systems"], ["sony-playstation"])
self.assertEqual(folders["psx"]["profile"], "beetle_psx")
self.assertEqual(folders["mame"]["arcade"], "mame")
self.assertEqual(folders["zzunknownzz"]["systems"], [])
self.assertEqual(folders["zzunknownzz"]["basis"], "unresolved")
self.assertEqual(coverage["setups"]["fixture"]["resolved"], 2)
def test_fallback_keeps_platform_and_default_core_files_of_the_system(self) -> None:
files, _ = self.render()
table = self.doc(files, "fallback/fixtureplat.json")
chosen = table["sets"]["sony-playstation|beetle_psx"]
kept = [table["files"][i][0] for i in chosen["files"]]
self.assertEqual(kept, ["psxonpsp660.bin", "scph5501.bin"])
self.assertEqual(chosen["bytes"], 2 * 524288)
def test_a_board_default_core_has_its_own_fallback_set(self) -> None:
files, _ = self.render()
folders = {f["folder"]: f for f in self.doc(files, "setups/fixture.json")["folders"]}
board = folders["psx"]["board_defaults"]["rpi1"]
self.assertEqual(board["profile"], "pcsx_rearmed")
self.assertEqual(board["fallback"], ["sony-playstation|pcsx_rearmed"])
table = self.doc(files, "fallback/fixtureplat.json")
self.assertIn("sony-playstation|pcsx_rearmed", table["sets"])
def test_index_names_the_systems_of_every_folder_name(self) -> None:
files, _ = self.render()
folders = self.doc(files, "index.json")["folders"]
self.assertEqual(folders["psx"], ["sony-playstation"])
self.assertEqual(folders["sony-playstation"], ["sony-playstation"])
self.assertNotIn("zzunknownzz", folders)
def test_catalog_rows_are_published_sorted_with_serials(self) -> None:
files, _ = self.render()
system = self.doc(files, "systems/sony-playstation.json")
games = system["identify"]["games"]
self.assertEqual([g[0] for g in games], ["00000001", "ffffffff"])
regions = system["identify"]["regions"]
self.assertEqual(regions[games[0][2]], ["japan"])
self.assertEqual(
[regions[i] for i in system["identify"]["serials"]["SLUS-00001"]], [["north-america"]]
)
self.assertEqual(system["identify"]["sources"][0]["licence"], {"quote": "free"})
def test_index_lists_every_file_with_size_and_digest(self) -> None:
files, _ = self.render()
index = self.doc(files, "index.json")
listed = {
entry["file"] for kind in ("setups", "fallback", "systems", "arcade")
for entry in index[kind].values()
}
written = {p.relative_to(self.tmp / "out").as_posix() for p in files} - {"index.json"}
self.assertEqual(listed, written)
for kind in ("setups", "systems"):
for entry in index[kind].values():
text = files[self.tmp / "out" / entry["file"]].encode()
self.assertEqual(entry["size"], len(text))
self.assertLess(entry["gzip_size"], entry["size"] + 64)
self.assertEqual(len(index["data_version"]), 16)
def test_floor_names_what_fell_short(self) -> None:
_, coverage = self.render()
failures = gi.floor_failures(coverage)
self.assertTrue(any("catalog" in f for f in failures))
class CommittedData(unittest.TestCase):
"""The data the repository publishes."""
def test_published_files_are_current(self) -> None:
if not (ROOT / "install" / "games" / "index.json").exists():
self.skipTest("install/games/ not generated")
import subprocess
result = subprocess.run(
[sys.executable, "scripts/generate_games_index.py", "--check", "--quiet"],
cwd=ROOT, capture_output=True, text=True,
)
self.assertEqual(result.returncode, 0, result.stderr[-2000:])
def test_every_folder_name_resolves_or_is_counted(self) -> None:
setups = load_setups(ROOT / "platforms" / "romfolders")
if not setups:
self.skipTest("no setup scraped")
from common import load_emulator_profiles
from romfolders import ProfileResolver
resolver = SystemResolver.from_repo(str(ROOT / "emulators"), str(ROOT / "platforms"))
cores = ProfileResolver.from_repo(str(ROOT / "emulators"))
profiles = gi.Profiles(load_emulator_profiles(str(ROOT / "emulators")), resolver)
total = unresolved = 0
for doc in setups.values():
for row in doc["folders"]:
total += 1
profile = cores.resolve(row.get("core"))
if not gi.resolve_folder(row, doc["platform"], profile, resolver, profiles).systems:
unresolved += 1
self.assertGreaterEqual((total - unresolved) / total, gi.FLOORS["folders_resolved_share"])
if __name__ == "__main__":
unittest.main()
+34 -6
View File
@@ -10,6 +10,7 @@ import io
import json
import os
import stat
import select
import shlex
import re
import shutil
@@ -31,6 +32,13 @@ _spec = importlib.util.spec_from_file_location("install", REPO_ROOT / "install.p
install = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(install)
sys.path.insert(0, str(REPO_ROOT / "tests"))
from netguard import module_guard # noqa: E402
# No connection off the loopback and no card of the host reaches a test
# (code_audit_24 P3-13): the module fails at its end otherwise.
setUpModule, tearDownModule = module_guard("test_install")
def _tempdir(test: unittest.TestCase) -> str:
"""A scratch directory under the repository's tmp/, removed after the test.
@@ -4175,17 +4183,35 @@ class AQuestionLeftWithCtrlDOrCtrlCIsAStop(unittest.TestCase):
home = Path(_tempdir(self))
master, slave = pty.openpty()
# Its output on the terminal too: piped, nothing is asked, and the
# test skipped on every machine.
proc = subprocess.Popen(
[sys.executable, "install.py"], cwd=REPO_ROOT, stdin=slave,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
env={**os.environ, "HOME": str(home), "RETROBIOS_OS": "linux"},
[sys.executable, "install.py", "--plain"], cwd=REPO_ROOT, stdin=slave,
stdout=slave, stderr=slave,
# No request leaves the machine: the standalone emulators' lists
# come from the network (code_audit_24 P3-13).
env={**os.environ, "HOME": str(home), "RETROBIOS_OS": "linux",
"RETROBIOS_INSTALL_OFFLINE": "1"},
)
os.close(slave)
self.addCleanup(os.close, master)
time.sleep(3)
out = ""
deadline = time.monotonic() + 60
while "Which setup?" not in out and proc.poll() is None \
and time.monotonic() < deadline:
if select.select([master], [], [], 0.2)[0]:
out += os.read(master, 65536).decode("utf-8", "replace")
os.write(master, b"\x04")
out, _ = proc.communicate(timeout=60)
if "Choose the platform" not in out:
proc.wait(timeout=60)
while select.select([master], [], [], 0.2)[0]:
try:
chunk = os.read(master, 65536)
except OSError:
break
if not chunk:
break
out += chunk.decode("utf-8", "replace")
if "Which setup?" not in out:
self.skipTest("a platform was detected on this machine")
self.assertEqual(proc.returncode, 130, out)
self.assertIn("Stopped", out)
@@ -5204,6 +5230,8 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
# The systems Choose systems lists, before the files are read.
{"event": "choices", "platform": "retroarch",
"systems": ["sys-a", "sys-b", "other"]},
# Nothing set aside before the choice (code_audit_24 P3-3).
{"event": "set_aside", "platform": "retroarch", "dests": []},
# a.bin and b.bin are in place: the first read and the last.
{"event": "checking", "files_done": 1, "files_total": 2},
{"event": "checking", "files_done": 2, "files_total": 2},
+976
View File
@@ -0,0 +1,976 @@
"""install.py --for-games: what the games on a card need.
The games index (install/games/) is a fixture written per test, read from
the folder beside --manifest as a run offline reads it, so a test pins the
rules and not the state of the data. Two tests read the repository's own
index, published by scripts/generate_games_index.py.
"""
from __future__ import annotations
import contextlib
import functools
import gzip
import hashlib
import http.server
import io
import json
import os
import shutil
import subprocess
import sys
import tempfile
import threading
import unittest
import zipfile
import zlib
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
import install # noqa: E402
TMP_ROOT = REPO_ROOT / "tmp" / "tests"
TMP_ROOT.mkdir(parents=True, exist_ok=True)
def _crc(data: bytes) -> str:
return f"{zlib.crc32(data) & 0xFFFFFFFF:08x}"
MD_EU = b"md europe game " * 64
MD_JP = b"md japan game " * 64
MD_ZIPPED = b"md zipped usa game " * 64
GC_HEADER = 0x458
# Every file the fixture list carries: dest -> (size, regions, cores, systems).
FILES = {
"scph5500.bin": (512, ["japan"], None, ["sony-playstation"]),
"scph5501.bin": (512, ["north-america"], None, ["sony-playstation"]),
"scph5502.bin": (512, ["europe"], None, ["sony-playstation"]),
"bios_CD_E.bin": (128, ["europe"], None, ["sega-megacd"]),
"bios_CD_U.bin": (128, ["north-america"], None, ["sega-megacd"]),
"bios_CD_J.bin": (128, ["japan"], None, ["sega-megacd"]),
"cd_extra.bin": (16, None, ["picodrive"], None),
"md_eu.bin": (64, ["europe"], None, ["sega-megadrive"]),
"md_us.bin": (64, ["north-america"], None, ["sega-megadrive"]),
"md_jp.bin": (64, ["japan"], None, ["sega-megadrive"]),
"md_tmss.bin": (32, None, None, ["sega-megadrive"]),
"dolphin/GC/USA/IPL.bin": (256, ["north-america"], ["dolphin"], None),
"dolphin/GC/EUR/IPL.bin": (256, ["europe"], ["dolphin"], None),
"dolphin/GC/JAP/IPL.bin": (256, ["japan"], ["dolphin"], None),
"neogeo.zip": (300, None, None, ["snk-neogeo"]),
"fbneo/neogeo.zip": (300, None, ["fbneo"], None),
"fbneo/pgm.zip": (200, None, ["fbneo"], None),
"fbneo/msx.zip": (150, None, ["fbneo"], None),
"fbneo/samples/big.zip": (9000, None, ["fbneo"], None),
"gba_bios.bin": (100, None, None, ["nintendo-gba"]),
}
# The fallback sets: key -> (files, optional).
SETS = {
"sony-playstation|beetle_psx": (["scph5500.bin", "scph5501.bin", "scph5502.bin"], []),
"sega-megacd|picodrive": (["bios_CD_E.bin", "bios_CD_U.bin", "bios_CD_J.bin",
"cd_extra.bin"], ["cd_extra.bin"]),
"sega-megadrive|fixcore": (["md_eu.bin", "md_us.bin", "md_jp.bin", "md_tmss.bin"],
["md_tmss.bin"]),
"nintendo-gamecube|dolphin": (["dolphin/GC/USA/IPL.bin", "dolphin/GC/EUR/IPL.bin",
"dolphin/GC/JAP/IPL.bin"], []),
"arcade|fbneo": (["fbneo/samples/big.zip"], ["fbneo/samples/big.zip"]),
"msx|fbneo": (["fbneo/msx.zip"], []),
"nintendo-gba|mgba": (["gba_bios.bin"], ["gba_bios.bin"]),
"nintendo-gba|gpsp": (["gba_bios.bin"], []),
}
REGION_READER_MCD = {
"applies_to": {"extensions": [".cue", ".bin", ".iso"], "media": "cd"},
"decode": {"kind": "integer", "otherwise": ["north-america"],
"values": [{"regions": ["europe"], "value": 100},
{"regions": ["japan"], "value": 161}]},
"location": {"encoding": "u8", "length": 1, "offset": 523, "sector": 0},
"magic": {"bytes": "SEGADISCSYSTEM", "offset": 0, "sector": 0},
}
SYSTEMS = {
"sony-playstation": {
"identify": {"sources": [{"source": "redump", "dat": "PS"}], "regions": [["europe"]],
"games": [], "serials": {"SLES-00001": [0]}},
"headers": {"readers": {"psx-serial-prefix": {
"location": {"iso_file": "SYSTEM.CNF", "key": "BOOT", "take": "basename"},
"decode": {"kind": "prefix-nocase", "values": [
{"value": "sles", "regions": ["europe"]}], "otherwise": "unknown"}}},
"cores": {"beetle_psx": {"order": ["psx-serial-prefix"],
"otherwise": ["north-america"]}}},
"region_rules": {"beetle_psx": {"keep_all_regions": [
{"case": "psx-override-bios", "condition": "option", "keeps": "region-free-files"}]}},
},
"sega-megacd": {
"identify": {"sources": [{"source": "redump", "dat": "MCD"}], "regions": [],
"games": [], "serials": {}},
"headers": {"readers": {"mcd-security-code": REGION_READER_MCD},
"cores": {"picodrive": {"order": ["mcd-security-code"],
"otherwise": ["north-america"]}}},
"region_rules": {"picodrive": {"default_region": "north-america"}},
"extras": [{"system": "sega-megacd", "profile": "picodrive",
"trigger": {"every_game": True}, "files": ["cd_extra.bin"],
"required": True}],
},
"sega-megadrive": {
"identify": {"sources": [{"source": "no-intro", "dat": "MD"}],
"regions": [["europe"], ["japan"], ["north-america"]],
"games": sorted([[_crc(MD_EU), len(MD_EU), 0, "Game (Europe)"],
[_crc(MD_JP), len(MD_JP), 1, "Game (Japan)"],
[_crc(MD_ZIPPED), len(MD_ZIPPED), 2, "Zipped (USA)"]]),
"serials": {}},
"headers": {"readers": {"md-country": {
"location": {"offset": 0x1F0, "length": 3, "encoding": "ascii"},
"decode": {"kind": "letters", "otherwise": "unknown", "values": [
{"value": "J", "regions": ["japan"]}, {"value": "E", "regions": ["europe"]},
{"value": "U", "regions": ["north-america"]}]}}},
"cores": {"fixcore": {"order": ["md-country"], "otherwise": "unknown"}}},
"region_rules": {"fixcore": {}},
},
"nintendo-gamecube": {
"identify": {"sources": [{"source": "redump", "dat": "GC"}],
"regions": [["europe", "north-america"], ["japan"]],
"games": [], "serials": {"GALE01": [0], "GJPN01": [1]}},
"headers": {"readers": {
"gc-game-id": {"location": {"container": "gamecube-disc", "disc_offset": 0,
"encoding": "ascii", "length": 6},
"decode": {"kind": "serial"}},
"gc-region": {"location": {"container": "gamecube-disc", "disc_offset": GC_HEADER,
"encoding": "u32be", "length": 4},
"decode": {"kind": "integer", "otherwise": "unknown", "values": [
{"value": 0, "regions": ["japan"]},
{"value": 1, "regions": ["north-america"]},
{"value": 2, "regions": ["europe"]}]}}},
"cores": {"dolphin": {"order": ["gc-region"], "otherwise": "unknown"}}},
"region_rules": {"dolphin": {"keep_all_regions": [
{"case": "gamecube-unknown-region", "condition": "disc"}]}},
},
"nintendo-gba": {},
}
ARCADE = {
"bios": {"neogeo": ["mslug", "mslug2"], "pgm": ["orlegend"], "nobios": ["lost"],
"msx": ["msx_1942"]},
"devices": {}, "chd": [], "missing_parents": {},
"no_bios": ["1942", "nes_1942", "pacman"], "bios_sets": ["msx", "neogeo", "nobios", "pgm"],
}
SETUP = {
"format_version": 1, "setup": "retroarch", "name": "Fixture", "platform": "retroarch",
"sources": {}, "roms_root": {"path": "/mnt/SDCARD/Roms", "from_bios": "../Roms"},
"case_sensitive": False,
"media": [{"name": "Imgs"}],
"folders": [
{"folder": "PS", "systems": ["sony-playstation"], "basis": "id",
"profile": "beetle_psx", "extensions": [".cue", ".bin", ".chd", ".m3u", ".iso"],
"fallback": ["sony-playstation|beetle_psx"], "source_ref": "x"},
{"folder": "SEGACD", "systems": ["sega-megacd"], "basis": "id",
"profile": "picodrive", "extensions": [".cue", ".iso", ".bin", ".chd", ".m3u"],
"fallback": ["sega-megacd|picodrive"], "source_ref": "x"},
{"folder": "MD", "systems": ["sega-megadrive"], "basis": "id", "profile": "fixcore",
"extensions": [".md", ".bin", ".zip", ".7z"],
"fallback": ["sega-megadrive|fixcore"], "source_ref": "x"},
{"folder": "GC", "systems": ["nintendo-gamecube"], "basis": "id",
"profile": "dolphin", "extensions": [".iso", ".rvz"],
"fallback": ["nintendo-gamecube|dolphin"], "source_ref": "x"},
{"folder": "FBNEO", "systems": ["arcade"], "basis": "id", "profile": "fbneo",
"arcade": "fbneo", "extensions": [".zip", ".7z"], "fallback": ["arcade|fbneo"],
"source_ref": "x"},
{"folder": "MSX", "systems": ["msx"], "basis": "id", "profile": "fbneo",
"arcade": "fbneo", "extensions": [".zip"], "fallback": ["msx|fbneo"],
"source_ref": "x"},
{"folder": "GBA", "systems": ["nintendo-gba"], "basis": "id", "profile": "mgba",
"extensions": [".gba"], "fallback": ["nintendo-gba|mgba"],
"board_defaults": {"rpi": {"core": "gpsp", "profile": "gpsp",
"fallback": ["nintendo-gba|gpsp"]}},
"source_ref": "x"},
{"folder": "PORTS", "systems": [], "basis": "unresolved", "source_ref": "x"},
],
}
def _dumps(doc: object) -> bytes:
return (json.dumps(doc, sort_keys=True, separators=(",", ":")) + "\n").encode()
def write_index(folder: Path, systems: dict | None = None, arcade: dict | None = None,
setup: dict | None = None, extra_sets: dict | None = None) -> None:
"""install/games/ as the generator lays it out, every digest right."""
rows = sorted({(dest, FILES[dest][0], "bios") for files, _o in SETS.values()
for dest in files})
position = {row[0]: i for i, row in enumerate(rows)}
sets = {**SETS, **(extra_sets or {})}
fallback = {"format_version": 1, "platform": "retroarch", "files": [list(r) for r in rows],
"sets": {key: {"files": [position[d] for d in files],
"optional": [position[d] for d in optional],
"bytes": 0, "required_bytes": 0}
for key, (files, optional) in sets.items()}}
docs = {"setups": {"retroarch": setup or SETUP}, "fallback": {"retroarch": fallback},
"systems": {key: {"format_version": 1, "system": key, **value}
for key, value in (systems or SYSTEMS).items()},
"arcade": {"fbneo": {"profile": "fbneo", **(arcade or ARCADE)}}}
index: dict = {"format_version": 1, "data_version": "0" * 16, "licences": [],
"folders": {"ps": ["sony-playstation"], "psx": ["sony-playstation"],
"md": ["sega-megadrive"], "megadrive": ["sega-megadrive"]}}
for kind, items in docs.items():
index[kind] = {}
for key, doc in items.items():
data = _dumps(doc)
rel = f"{kind}/{key}.json"
(folder / kind).mkdir(parents=True, exist_ok=True)
(folder / rel).write_bytes(data)
index[kind][key] = {"file": rel, "size": len(data),
"gzip_size": len(gzip.compress(data)),
"sha256": hashlib.sha256(data).hexdigest()}
(folder / "index.json").write_bytes(_dumps(index))
def manifest() -> dict:
files = []
for dest, (size, regions, cores, systems) in FILES.items():
payload = dest.encode().ljust(size, b".")[:size]
entry = {"dest": dest, "size": size, "sha1": hashlib.sha1(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(),
"repo_path": f"bios/{dest.replace('/', '_')}", "cores": cores}
if systems:
entry["systems"] = systems
if regions:
entry.update(regions=regions)
files.append(entry)
return {"manifest_version": 2, "platform": "retroarch", "files": files,
"omitted_files": []}
def mcd_iso(code: int, magic: bool = True) -> bytes:
data = bytearray(2048 * 18)
if magic:
data[0:14] = b"SEGADISCSYSTEM"
data[523] = code
return bytes(data)
def gc_iso(game_id: bytes, region: int) -> bytes:
data = bytearray(0x500)
data[0:6] = game_id
data[GC_HEADER:GC_HEADER + 4] = region.to_bytes(4, "big")
return bytes(data)
def md_rom(content: bytes, country: bytes) -> bytes:
data = bytearray(content.ljust(0x200, b"\0"))
data[0x1F0:0x1F3] = country
return bytes(data)
def ps_iso(boot: str) -> bytes:
"""A 2048-byte ISO 9660 image whose root holds SYSTEM.CNF."""
sectors = [bytearray(2048) for _ in range(20)]
volume = sectors[16]
volume[0] = 1
volume[1:6] = b"CD001"
root = bytearray(34)
root[0] = 34
root[2:6] = (17).to_bytes(4, "little")
root[10:14] = (2048).to_bytes(4, "little")
volume[156:190] = root
name = b"SYSTEM.CNF;1"
record = bytearray(33 + len(name))
record[0] = len(record)
record[2:6] = (18).to_bytes(4, "little")
text = f"BOOT = cdrom:\\{boot};1\r\n".encode()
record[10:14] = len(text).to_bytes(4, "little")
record[32] = len(name)
record[33:] = name
sectors[17][0:len(record)] = record
sectors[18][0:len(text)] = text
return b"".join(bytes(s) for s in sectors)
class Card:
"""A card in tmp/: BIOS/ and Roms/ at its root, the games index beside
a saved file list."""
def __init__(self) -> None:
self.root = Path(tempfile.mkdtemp(dir=TMP_ROOT))
self.bios = self.root / "card" / "BIOS"
self.roms = self.root / "card" / "Roms"
self.bios.mkdir(parents=True)
self.roms.mkdir()
self.lists = self.root / "lists"
(self.lists / "games").mkdir(parents=True)
self.manifest_path = self.lists / "retroarch.json"
self.manifest_path.write_text(json.dumps(manifest()))
write_index(self.lists / "games")
self.cache = self.root / "cache"
def game(self, folder: str, name: str, data: bytes = b"game") -> Path:
path = self.roms / folder / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
return path
def zipped(self, folder: str, name: str, member: str, data: bytes) -> Path:
path = self.roms / folder / name
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w") as archive:
archive.writestr(member, data)
return path
def close(self) -> None:
shutil.rmtree(self.root, ignore_errors=True)
class GamesTestCase(unittest.TestCase):
def setUp(self) -> None:
self.card = Card()
self.addCleanup(self.card.close)
patches = [mock.patch.dict(os.environ, {"RETROBIOS_CACHE": str(self.card.cache),
install.OFFLINE_VARIABLE: "1"}),
mock.patch.object(install.LOCAL, "manifest", self.card.manifest_path),
mock.patch.object(install, "GAMES", install.GamesData())]
for patcher in patches:
patcher.start()
self.addCleanup(patcher.stop)
def plan(self, folder: Path | None = None) -> install.GamesPlan:
plan = install.plan_games("retroarch", self.card.bios, manifest(), folder,
os_type="linux")
self.assertIsNotNone(plan)
return plan
def kept(self, plan: install.GamesPlan, prefix: str = "", required: bool = False) -> list:
return sorted(dest for dest in plan.wanted(required) if dest.startswith(prefix))
class Identification(GamesTestCase):
def test_a_cartridge_is_known_by_crc32_and_size(self):
self.card.game("MD", "anything.md", MD_EU)
plan = self.plan()
game = plan.games[0]
self.assertTrue(game.recognised)
self.assertEqual(game.title, "Game (Europe)")
self.assertEqual(game.dat_regions, frozenset({"europe"}))
def test_a_zip_member_is_known_from_the_central_directory(self):
self.card.zipped("MD", "z.zip", "rom.md", MD_ZIPPED)
with mock.patch.object(install, "_crc32_file") as hashed:
plan = self.plan()
hashed.assert_not_called()
self.assertTrue(plan.games[0].recognised)
self.assertEqual(plan.games[0].dat_regions, frozenset({"north-america"}))
def test_a_disc_serial_from_system_cnf(self):
self.card.game("PS", "game.iso", ps_iso("SLES_000.01"))
game = self.plan().games[0]
self.assertTrue(game.recognised)
self.assertEqual(game.title, "SLES_000.01")
def test_a_chd_is_not_read(self):
self.card.game("PS", "game.chd", b"MComprHD")
game = self.plan().games[0]
self.assertFalse(game.read)
self.assertFalse(game.recognised)
def test_files_a_cue_names_are_part_of_its_game(self):
self.card.game("SEGACD", "g.cue", b'FILE "g.bin" BINARY\n TRACK 01 MODE1/2048\n')
self.card.game("SEGACD", "g.bin", mcd_iso(100))
plan = self.plan()
self.assertEqual([game.shown for game in plan.games], ["SEGACD/g.cue"])
self.assertEqual(install.core_regions(plan.games[0], "sega-megacd",
install.SystemData(SYSTEMS["sega-megacd"]),
"picodrive"), frozenset({"europe"}))
def test_hidden_media_and_other_files_are_not_games(self):
self.card.game("MD", ".hidden.md", MD_EU)
self.card.game("MD", "Imgs/cover.md", MD_EU)
self.card.game("MD", "notes.txt", b"x")
self.card.game("MD", "game.md", MD_EU)
plan = self.plan()
self.assertEqual([game.shown for game in plan.games], ["MD/game.md"])
self.assertEqual(plan.not_games, 1)
def test_a_folder_the_setup_does_not_list_holds_no_game(self):
self.card.game("UNKNOWN", "game.md", MD_EU)
self.card.game("PORTS", "game.sh", b"x")
self.assertEqual(self.plan().games, [])
def test_a_raw_cd_image_with_its_mode(self):
user = mcd_iso(161)
raw = bytearray()
for index in range(len(user) // 2048):
sector = bytearray(2352)
sector[0:12] = b"\x00" + b"\xff" * 10 + b"\x00"
sector[15] = 1
sector[16:16 + 2048] = user[index * 2048:(index + 1) * 2048]
raw += sector
self.card.game("SEGACD", "g.bin", bytes(raw))
plan = self.plan()
self.assertEqual(self.kept(plan, "bios_CD"), ["bios_CD_J.bin"])
class RegionMatrix(GamesTestCase):
"""What each kind of evidence lets the run remove (research section 2)."""
def test_a_header_read_keeps_the_region_it_proves(self):
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
plan = self.plan()
self.assertEqual(self.kept(plan, "bios_CD"), ["bios_CD_E.bin"])
self.assertEqual(plan.systems["sega-megacd"]["regions"], ["europe"])
self.assertEqual(plan.fallback, [])
def test_a_value_the_code_reads_as_its_default_is_a_proof(self):
self.card.game("SEGACD", "us.iso", mcd_iso(7))
self.assertEqual(self.kept(self.plan(), "bios_CD"), ["bios_CD_U.bin"])
def test_two_games_keep_both_regions(self):
self.card.game("SEGACD", "a.iso", mcd_iso(100))
self.card.game("SEGACD", "b.iso", mcd_iso(161))
self.assertEqual(self.kept(self.plan(), "bios_CD"), ["bios_CD_E.bin", "bios_CD_J.bin"])
def test_a_hash_match_keeps_the_catalog_region(self):
self.card.game("MD", "eu.md", MD_EU)
self.assertEqual(self.kept(self.plan(), "md_"), ["md_eu.bin", "md_tmss.bin"])
def test_a_filename_tag_alone_keeps_every_region(self):
self.card.game("SEGACD", "Sonic CD (Europe).iso", b"no header here" * 200)
plan = self.plan()
self.assertEqual(self.kept(plan, "bios_CD"),
["bios_CD_E.bin", "bios_CD_J.bin", "bios_CD_U.bin"])
self.assertEqual([(g.shown, g.reason) for g in plan.fallback],
[("SEGACD/Sonic CD (Europe).iso", "not_recognised")])
def test_folder_only_keeps_every_region_and_names_the_game(self):
self.card.game("MD", "unknown.md", b"not in any catalog")
plan = self.plan()
self.assertEqual(self.kept(plan, "md_"),
["md_eu.bin", "md_jp.bin", "md_tmss.bin", "md_us.bin"])
self.assertEqual(plan.fallback[0].reason, "not_recognised")
lines = install.games_lines(plan, None)
self.assertIn("1 game gets every BIOS of its system: MD/unknown.md (not recognised)",
"\n".join(lines))
def test_one_unproven_game_keeps_every_region_for_all(self):
self.card.game("SEGACD", "a.iso", mcd_iso(100))
self.card.game("SEGACD", "b.chd", b"MComprHD")
plan = self.plan()
self.assertEqual(len(self.kept(plan, "bios_CD")), 3)
self.assertEqual([(g.shown, g.reason) for g in plan.fallback],
[("SEGACD/b.chd", "not_read")])
self.assertIsNone(plan.systems["sega-megacd"]["regions"])
def test_a_catalog_and_a_header_that_disagree_keep_every_region(self):
self.card.game("MD", "jp.md", md_rom(MD_JP, b"E "))
# The catalog matches the whole file: write the header into it first.
path = self.card.roms / "MD" / "jp.md"
data = path.read_bytes()
rows = [[_crc(data), len(data), 1, "Game (Japan)"]]
systems = json.loads(json.dumps(SYSTEMS))
systems["sega-megadrive"]["identify"]["games"] = rows
write_index(self.card.lists / "games", systems=systems)
plan = self.plan()
self.assertTrue(plan.games[0].recognised)
self.assertEqual(plan.games[0].readings["md-country"], frozenset({"europe"}))
self.assertEqual(len(self.kept(plan, "md_")), 4)
self.assertEqual(plan.fallback[0].reason, "region_unknown")
def test_a_multi_region_serial_keeps_its_regions(self):
self.card.game("GC", "game.iso", gc_iso(b"GALE01", 1))
plan = self.plan()
self.assertEqual(self.kept(plan, "dolphin"),
["dolphin/GC/EUR/IPL.bin", "dolphin/GC/USA/IPL.bin"])
def test_an_unknown_gamecube_region_keeps_every_ipl(self):
self.card.game("GC", "odd.iso", gc_iso(b"XXXX01", 9))
self.assertEqual(len(self.kept(self.plan(), "dolphin")), 3)
def test_a_compressed_gamecube_disc_is_not_read(self):
self.card.game("GC", "game.rvz", b"RVZ\x01")
plan = self.plan()
self.assertEqual(len(self.kept(plan, "dolphin")), 3)
self.assertEqual(plan.fallback[0].reason, "not_read")
def test_a_core_with_a_keep_all_case_never_narrows(self):
self.card.game("PS", "game.iso", ps_iso("SLES_000.01"))
plan = self.plan()
self.assertTrue(plan.games[0].recognised)
self.assertEqual(self.kept(plan, "scph"),
["scph5500.bin", "scph5501.bin", "scph5502.bin"])
# Nothing the game could have proven changes that: it is not named.
self.assertEqual(plan.fallback, [])
def test_a_game_region_no_file_matches_keeps_the_whole_set(self):
dests = ["a.bin", "b.bin"]
entries = {"a.bin": {"regions": ["japan"]}, "b.bin": {"regions": ["north-america"]}}
self.assertEqual(install.keep_regions(dests, entries, frozenset({"europe"}), set()),
dests)
def test_a_country_matches_its_region_and_region_free_files_stay(self):
dests = ["eu.bin", "us.bin", "free.bin", "world.bin"]
entries = {"eu.bin": {"regions": ["europe"]}, "us.bin": {"regions": ["north-america"]},
"free.bin": {"regions": ["japan"]}, "world.bin": {"regions": ["world"]}}
self.assertEqual(install.keep_regions(dests, entries, frozenset({"germany"}),
{"free.bin"}),
["eu.bin", "free.bin", "world.bin"])
def test_routes_send_a_region_to_another_regions_file(self):
rule = {"routes": {"south-korea": "japan"}}
self.assertEqual(install.needed_regions(rule, [frozenset({"south-korea"})]),
frozenset({"japan"}))
class KeepEveryRegionCases(unittest.TestCase):
"""Every case of games/regions.yml keeps every region where it applies."""
@classmethod
def setUpClass(cls) -> None:
# The installer's own jobs (Python 3.8 with pyte alone) import this
# module for its card builders, and install.py needs no pyyaml.
try:
import yaml # noqa: PLC0415
except ImportError as exc:
raise unittest.SkipTest("pyyaml reads games/regions.yml") from exc
with open(REPO_ROOT / "games" / "regions.yml", encoding="utf-8") as handle:
cls.rules = yaml.safe_load(handle)
def facts(self, name: str) -> install.GameFacts:
folder = install.GameFolder(Path("x"), {})
return install.GameFacts(Path(name), name, folder)
def test_there_are_fourteen_cases_and_each_is_decided(self):
cases = self.rules["cases"]
self.assertEqual(len(cases), 14)
disc_cases = {"first-disc-region", "multi-area-disc", "gamecube-unknown-region",
"saturn-kr-ipl"}
game = [self.facts("game.iso")]
for name in cases:
with self.subTest(case=name):
conditions = {case.get("condition")
for system in self.rules["systems"].values()
for rule in system.values()
for case in rule.get("keep_all_regions") or []
if case.get("case") == name}
self.assertTrue(conditions, f"{name} is named by no rule")
for condition in conditions:
rule = {"keep_all_regions": [{"case": name, "condition": condition}]}
keeps = install.region_rule_keeps_all(rule, game)
if condition in ("always", "option", "user-state"):
self.assertTrue(keeps, (name, condition))
else:
self.assertIn(name, disc_cases)
def test_first_disc_region_keeps_every_region_for_a_playlist(self):
rule = {"keep_all_regions": [{"case": "first-disc-region", "condition": "disc"}]}
self.assertTrue(install.region_rule_keeps_all(rule, [self.facts("g.m3u")]))
self.assertFalse(install.region_rule_keeps_all(rule, [self.facts("g.cue")]))
def test_an_asia_ntsc_or_korean_saturn_disc_keeps_every_region(self):
rule = {"keep_all_regions": [{"case": "saturn-kr-ipl", "condition": "disc"}]}
self.assertIsNone(install.needed_regions(rule, [frozenset({"asia-ntsc"})]))
self.assertIsNone(install.needed_regions(rule, [frozenset({"south-korea"})]))
self.assertEqual(install.needed_regions(rule, [frozenset({"japan"})]),
frozenset({"japan"}))
def test_a_multi_area_disc_keeps_every_area_it_lists(self):
rule = {"keep_all_regions": [{"case": "multi-area-disc", "condition": "disc"}]}
self.assertFalse(install.region_rule_keeps_all(rule, [self.facts("g.iso")]))
self.assertEqual(install.needed_regions(rule, [frozenset({"japan", "europe"})]),
frozenset({"japan", "europe"}))
def test_an_unknown_gamecube_region_is_no_proof(self):
system = install.SystemData({"headers": {"cores": {"dolphin": {
"order": ["gc-region"], "otherwise": "unknown"}}}})
facts = self.facts("g.iso")
facts.system = "nintendo-gamecube"
facts.readings["gc-region"] = "unknown"
self.assertIsNone(install.core_regions(facts, "nintendo-gamecube", system, "dolphin"))
def test_no_rule_read_and_a_case_not_known_keep_every_region(self):
game = [self.facts("g.iso")]
self.assertTrue(install.region_rule_keeps_all(None, game))
self.assertTrue(install.region_rule_keeps_all(
{"keep_all_regions": [{"case": "new-case", "condition": "disc"}]}, game))
def test_every_rule_of_the_repository_is_read_without_error(self):
game = [self.facts("g.iso")]
for system, cores in self.rules["systems"].items():
for core, rule in cores.items():
with self.subTest(system=system, core=core):
install.region_rule_keeps_all(rule, game)
class Arcade(GamesTestCase):
def test_a_set_gets_its_bios_parent_from_the_whole_list(self):
self.card.game("FBNEO", "mslug.zip", b"PK")
plan = self.plan()
self.assertEqual(self.kept(plan), ["fbneo/neogeo.zip", "neogeo.zip"])
self.assertEqual(self.kept(plan, required=True), ["fbneo/neogeo.zip", "neogeo.zip"])
def test_a_set_with_no_bios_needs_nothing(self):
self.card.game("FBNEO", "pacman.zip", b"PK")
plan = self.plan()
self.assertEqual(self.kept(plan), [])
self.assertTrue(plan.games[0].recognised)
def test_a_bios_set_on_the_card_is_not_a_game(self):
self.card.game("FBNEO", "neogeo.zip", b"PK")
plan = self.plan()
self.assertEqual(plan.games, [])
self.assertEqual(plan.support, 1)
def test_an_unknown_set_gets_every_bios_set_of_the_core(self):
self.card.game("FBNEO", "zzzz.zip", b"PK")
plan = self.plan()
self.assertEqual(self.kept(plan),
["fbneo/msx.zip", "fbneo/neogeo.zip", "fbneo/pgm.zip",
"fbneo/samples/big.zip"])
self.assertEqual([(g.shown, g.reason) for g in plan.fallback],
[("FBNEO/zzzz.zip", "unknown_set")])
def test_a_console_set_is_read_under_its_driver_name(self):
# msx_1942 loads the MSX BIOS, the arcade 1942 needs none.
self.card.game("MSX", "1942.zip", b"PK")
plan = self.plan()
self.assertEqual(self.kept(plan), ["fbneo/msx.zip"])
self.assertEqual(plan.games[0].title, "msx_1942")
def test_an_arcade_folder_reads_the_bare_set_name(self):
self.card.game("FBNEO", "1942.zip", b"PK")
plan = self.plan()
self.assertEqual(self.kept(plan), [])
self.assertTrue(plan.games[0].recognised)
def test_a_console_bios_on_the_card_is_not_a_game(self):
self.card.game("MSX", "msx.zip", b"PK")
self.assertEqual(self.plan().support, 1)
def test_a_parent_the_list_does_not_hold_falls_back(self):
self.card.game("FBNEO", "lost.zip", b"PK")
plan = self.plan()
self.assertEqual(plan.fallback[0].reason, "no_parent")
self.assertIn("fbneo/neogeo.zip", plan.keep)
class RequiredOnlyAndExtras(GamesTestCase):
def test_required_only_leaves_out_what_the_set_marks_optional(self):
self.card.game("MD", "eu.md", MD_EU)
plan = self.plan()
self.assertEqual(self.kept(plan, "md_"), ["md_eu.bin", "md_tmss.bin"])
self.assertEqual(self.kept(plan, "md_", required=True), ["md_eu.bin"])
def test_a_required_extra_the_game_triggers_stays(self):
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
plan = self.plan()
self.assertIn("cd_extra.bin", plan.required)
def test_apply_keeps_the_entries_and_the_omissions_of_the_games(self):
self.card.game("MD", "eu.md", MD_EU)
plan = self.plan()
omitted = [{"dest": "md_missing.bin", "systems": ["sega-megadrive"], "required": False},
{"dest": "other.bin", "systems": ["nintendo-gba"], "required": True}]
files, kept_omitted = plan.apply(manifest()["files"], omitted, False)
self.assertEqual(sorted(e["dest"] for e in files), ["md_eu.bin", "md_tmss.bin"])
self.assertEqual([o["dest"] for o in kept_omitted], ["md_missing.bin"])
_files, kept_omitted = plan.apply(manifest()["files"], omitted, True)
self.assertEqual(kept_omitted, [])
def test_a_board_default_core_adds_its_set(self):
self.card.game("GBA", "game.gba", b"gba game")
plan = self.plan()
# mgba marks the BIOS optional, gpsp (a board's default) requires it.
self.assertEqual(self.kept(plan, "gba", required=True), ["gba_bios.bin"])
class Folders(GamesTestCase):
def test_a_games_folder_named_on_a_computer_is_read_by_folder_names(self):
games = self.card.root / "pc games"
(games / "megadrive").mkdir(parents=True)
(games / "megadrive" / "eu.md").write_bytes(MD_EU)
setup = dict(SETUP, folders=[])
write_index(self.card.lists / "games", setup=setup)
plan = self.plan(games)
self.assertFalse(plan.card)
self.assertEqual(plan.where(), "in this folder")
self.assertEqual(plan.unknown_core, ["sega-megadrive"])
# The core is not known: every set of the system is read.
self.assertIn("md_eu.bin", plan.keep)
def test_a_folder_of_one_system_named_alone(self):
folder = self.card.roms / "MD"
folder.mkdir()
(folder / "eu.md").write_bytes(MD_EU)
plan = self.plan(folder)
self.assertEqual(len(plan.games), 1)
def test_no_rom_folder_beside_the_bios_folder(self):
shutil.rmtree(self.card.roms)
self.assertIsNone(install.plan_games("retroarch", self.card.bios, manifest(),
os_type="linux"))
def test_the_probe_counts_folders_holding_a_game(self):
self.card.game("MD", "eu.md", MD_EU)
self.card.game("PS", "a.chd", b"x")
(self.card.roms / "GBA").mkdir()
self.assertEqual(install.games_probe("retroarch", self.card.bios, network=False,
os_type="linux"), 2)
def test_the_probe_says_nothing_without_a_rom_folder(self):
shutil.rmtree(self.card.roms)
self.assertIsNone(install.games_probe("retroarch", self.card.bios, network=False,
os_type="linux"))
class Cache(GamesTestCase):
def test_a_second_run_hashes_nothing_that_did_not_change(self):
game = self.card.game("MD", "eu.md", MD_EU)
counted = []
real = install._crc32_file
def counting(path):
counted.append(path)
return real(path)
with mock.patch.object(install, "_crc32_file", side_effect=counting):
self.plan()
self.assertEqual(len(counted), 1)
install.GAMES.reset()
plan = self.plan()
self.assertEqual(len(counted), 1)
self.assertTrue(plan.games[0].recognised)
game.write_bytes(MD_JP)
plan = self.plan()
self.assertEqual(len(counted), 2)
self.assertEqual(plan.games[0].dat_regions, frozenset({"japan"}))
def test_the_index_is_cached_and_read_offline_without_the_list_beside_it(self):
self.card.game("MD", "eu.md", MD_EU)
index = (self.card.lists / "games" / "index.json").read_bytes()
cached = self.card.cache / "games"
cached.mkdir(parents=True)
(cached / "index.json").write_bytes(index)
for path in (self.card.lists / "games").rglob("*.json"):
if path.name != "index.json":
data = path.read_bytes()
(cached / f"{hashlib.sha256(data).hexdigest()}.json").write_bytes(data)
shutil.rmtree(self.card.lists / "games")
install.GAMES.reset()
self.assertTrue(self.plan().games[0].recognised)
class Unavailable(GamesTestCase):
def test_offline_with_no_index_is_said(self):
shutil.rmtree(self.card.lists / "games")
with self.assertRaises(install.GamesUnavailable) as caught:
install.plan_games("retroarch", self.card.bios, manifest(), os_type="linux")
self.assertEqual(caught.exception.reason, "offline")
def test_a_document_that_is_not_the_one_listed_is_refused(self):
(self.card.lists / "games" / "setups" / "retroarch.json").write_text("{}")
with self.assertRaises(install.GamesUnavailable) as caught:
install.plan_games("retroarch", self.card.bios, manifest(), os_type="linux")
self.assertEqual(caught.exception.reason, "offline")
def test_an_index_that_names_a_file_the_list_lacks_is_stale(self):
self.card.game("MD", "x.md", b"unknown")
listed = manifest()
listed["files"] = [e for e in listed["files"] if e["dest"] != "md_us.bin"]
with self.assertRaises(install.GamesUnavailable) as caught:
install.plan_games("retroarch", self.card.bios, listed, os_type="linux")
self.assertEqual(caught.exception.reason, "stale")
def run_cli(card: Card, *options: str, offline: bool = True,
base: str | None = None) -> subprocess.CompletedProcess:
env = {**os.environ, "RETROBIOS_CACHE": str(card.cache)}
env.pop(install.OFFLINE_VARIABLE, None)
if base:
env["RETROBIOS_BASE_URL"] = base
command = [sys.executable, str(REPO_ROOT / "install.py"), "--platform", "retroarch",
"--dest", str(card.bios), *options]
if offline:
command += ["--offline", "--manifest", str(card.manifest_path)]
return subprocess.run(command, env=env, capture_output=True, text=True, timeout=120,
check=False, stdin=subprocess.DEVNULL)
class CommandLine(unittest.TestCase):
def setUp(self) -> None:
self.card = Card()
self.addCleanup(self.card.close)
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
self.card.game("MD", "odd.md", b"not in the catalog")
def events(self, proc: subprocess.CompletedProcess) -> list[dict]:
return [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
def test_check_narrows_to_what_the_games_need(self):
proc = run_cli(self.card, "--for-games", "--check", "--events")
events = self.events(proc)
games = next(e for e in events if e["event"] == "games")
self.assertEqual(games["games"], 2)
self.assertEqual(games["fallback"], [{"system": "sega-megadrive", "folder": "MD",
"name": "MD/odd.md",
"reason": "not_recognised"}])
self.assertEqual(sorted(set(games)), sorted(["event", *install.EVENT_KEYS["games"]]))
dests = sorted(e["dest"] for e in events if e["event"] == "file")
self.assertEqual(dests, ["bios_CD_E.bin", "cd_extra.bin", "md_eu.bin", "md_jp.bin",
"md_tmss.bin", "md_us.bin"])
self.assertIn("1 game gets every BIOS of its system", proc.stderr)
def test_required_only_leaves_out_the_optional_files(self):
proc = run_cli(self.card, "--for-games", "--required-only", "--check", "--events")
dests = sorted(e["dest"] for e in self.events(proc) if e["event"] == "file")
self.assertEqual(dests, ["bios_CD_E.bin", "cd_extra.bin", "md_eu.bin", "md_jp.bin",
"md_us.bin"])
def test_required_only_alone_is_refused(self):
proc = run_cli(self.card, "--required-only", "--check")
self.assertEqual(proc.returncode, 2)
self.assertIn("--required-only applies to --for-games", proc.stderr)
def test_a_listing_refuses_it(self):
proc = run_cli(self.card, "--for-games", "--list-systems")
self.assertEqual(proc.returncode, 2)
def test_a_games_folder_that_does_not_exist_stops_the_run(self):
proc = run_cli(self.card, "--for-games", str(self.card.root / "nowhere"), "--check")
self.assertEqual(proc.returncode, 1)
self.assertIn("does not exist", proc.stderr)
def test_offline_without_the_index_keeps_every_file_and_says_so(self):
shutil.rmtree(self.card.lists / "games")
proc = run_cli(self.card, "--for-games", "--check", "--events")
events = self.events(proc)
unavailable = next(e for e in events if e["event"] == "games_unavailable")
self.assertEqual(unavailable["reason"], "offline")
self.assertEqual(len([e for e in events if e["event"] == "file"]), len(FILES))
self.assertIn("Every file of the list is kept", proc.stderr)
def test_a_system_choice_narrows_what_the_games_keep(self):
proc = run_cli(self.card, "--for-games", "--system", "sega-megacd", "--check",
"--events")
dests = sorted(e["dest"] for e in self.events(proc) if e["event"] == "file")
# cd_extra.bin names no system: a system choice leaves it, as without
# --for-games.
self.assertEqual(dests, ["bios_CD_E.bin"])
def test_the_install_writes_only_what_the_games_need(self):
self.card.game("MD", "eu.md", MD_EU)
root = self.card.root / "repo"
(root / "install").mkdir(parents=True)
shutil.copytree(self.card.lists / "games", root / "install" / "games")
listed = manifest()
(root / "bios").mkdir()
for entry in listed["files"]:
payload = entry["dest"].encode().ljust(entry["size"], b".")[:entry["size"]]
(root / entry["repo_path"]).write_bytes(payload)
(root / "install" / "retroarch.json").write_text(json.dumps(listed))
handler = functools.partial(_Quiet, directory=str(root))
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
self.addCleanup(server.server_close)
self.addCleanup(server.shutdown)
base = f"http://127.0.0.1:{server.server_address[1]}"
proc = run_cli(self.card, "--for-games", "--required-only", "--yes", offline=False,
base=base)
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
written = sorted(p.relative_to(self.card.bios).as_posix()
for p in self.card.bios.rglob("*") if p.is_file())
self.assertEqual(written, ["bios_CD_E.bin", "cd_extra.bin", "md_eu.bin", "md_jp.bin",
"md_us.bin"])
self.assertIn("1 game gets every BIOS of its system", proc.stdout)
# Cached for the next run, offline.
self.assertTrue((self.card.cache / "games" / "index.json").is_file())
self.assertTrue((self.card.cache / install.GAMES_SCAN_FILE).is_file())
class _Quiet(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args): # noqa: A002 - stdlib signature
return
class LineMenu(GamesTestCase):
def menu(self, answers: list[str], given: object = None) -> tuple[object, str]:
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
plan = self.plan()
listed = manifest()
replies = iter(answers)
out = io.StringIO()
with mock.patch.object(install, "_ask", side_effect=lambda _p: next(replies)), \
contextlib.redirect_stdout(out):
choice = install._prompt_custom_selection(
"retroarch", listed["files"], [], manifest=listed, played=plan, given=given,
command=lambda choice: install._INVOCATION.command("retroarch", "<dest>"))
return choice, out.getvalue()
def test_the_games_are_the_first_choice(self):
choice, text = self.menu([""])
self.assertIsInstance(choice, install.GamesChoice)
self.assertFalse(choice.required)
self.assertIn("for the 1 game on this card", text)
self.assertIn("e) everything, whatever the games", text)
self.assertIn(f"r) {install.GAMES_REQUIRED_WORDS}", text)
def test_e_installs_everything_and_h_goes_back(self):
choice, text = self.menu(["e", ""])
self.assertIsNone(choice)
self.assertIn("h) only for the games on this card", text)
choice, _text = self.menu(["e", "h", ""])
self.assertIsInstance(choice, install.GamesChoice)
def test_r_keeps_only_what_starts(self):
choice, text = self.menu(["r", ""])
self.assertTrue(choice.required)
self.assertIn("r) every file the games read", text)
self.assertEqual(choice.flags(), [("--for-games", None), ("--required-only", None)])
def test_the_choice_is_given_back_as_a_command(self):
self.card.game("SEGACD", "sonic.iso", mcd_iso(100))
plan = self.plan()
self.assertEqual(install.chosen_flags(install.GamesChoice(plan)),
[("--for-games", None)])
named = install.plan_games("retroarch", self.card.bios, manifest(), self.card.roms,
os_type="linux")
self.assertEqual(install.GamesChoice(named).flags(),
[("--for-games", str(self.card.roms))])
class Published(unittest.TestCase):
"""The repository's own index, as a run reads it."""
def test_a_card_reads_with_the_published_index(self):
if not (REPO_ROOT / "install" / "games" / "index.json").exists():
self.skipTest("install/games/ not generated")
root = Path(tempfile.mkdtemp(dir=TMP_ROOT))
self.addCleanup(shutil.rmtree, root, True)
bios = root / "userdata" / "bios"
bios.mkdir(parents=True)
(root / "userdata" / "roms" / "fbneo").mkdir(parents=True)
(root / "userdata" / "roms" / "fbneo" / "mslug.zip").write_bytes(b"PK")
(root / "userdata" / "roms" / "fbneo" / "pacman.zip").write_bytes(b"PK")
with mock.patch.object(install.LOCAL, "manifest", REPO_ROOT / "install" / "batocera.json"), \
mock.patch.object(install, "GAMES", install.GamesData()), \
mock.patch.dict(os.environ, {"RETROBIOS_CACHE": str(root / "cache"),
install.OFFLINE_VARIABLE: "1"}):
listed = json.loads((REPO_ROOT / "install" / "batocera.json").read_text())
plan = install.plan_games("batocera", bios, listed, os_type="linux")
self.assertEqual(len(plan.games), 2)
self.assertEqual(plan.fallback, [])
self.assertIn("neogeo.zip", plan.required)
self.assertTrue(all(dest.lower().endswith("neogeo.zip") for dest in plan.required))
def test_every_published_setup_resolves_a_root_layout(self):
index = json.loads((REPO_ROOT / "install" / "games" / "index.json").read_text())
for key in index["setups"]:
doc = json.loads((REPO_ROOT / "install" / "games" / "setups"
/ f"{key}.json").read_text())
with self.subTest(setup=key):
self.assertIn("from_bios", doc["roms_root"])
if __name__ == "__main__":
unittest.main()
+4 -2
View File
@@ -1632,10 +1632,12 @@ class SomethingElseAtADestination(unittest.TestCase):
proc = repo.run("--yes")
self.assertEqual(proc.returncode, 1, proc.stderr)
self.assertNotIn("Traceback", proc.stderr)
self.assertIn("failed: scph5500.bin (a folder sits where this file goes",
# Each failure named once, on its FAILED line (tui_audit_17 P3-7).
self.assertIn("FAILED scph5500.bin (a folder sits where this file goes",
proc.stdout)
self.assertIn("failed: scph5501.bin (file in place could not be kept aside",
self.assertIn("FAILED scph5501.bin (file in place could not be kept aside",
proc.stdout)
self.assertEqual(proc.stdout.count("scph5500.bin ("), 1, proc.stdout)
self.assertTrue((repo.dest / "scph5500.bin").is_dir())
self.assertIn("gba_bios.bin", repo.installed())
+905 -166
View File
File diff suppressed because it is too large. Load diff
+32
View File
@@ -32,6 +32,7 @@ from scripts.scraper.mame_devices import (
attribute_softlists,
attribute_systems,
default_floppy_samples,
device_systems,
drive_form_factor,
find_rom_block,
floppy_drives,
@@ -500,6 +501,37 @@ class DeviceSetsFollowTheMachinesThatLoadThem(unittest.TestCase):
)
class MachinesNoEntryPlacesCountUnderArcade(unittest.TestCase):
"""MAME starts any machine from its set name: a machine no seed places
is one an arcade folder can pass it, so its devices go under arcade."""
def test_a_set_only_unplaced_machines_load(self):
systems = device_systems(_release().machines, SEEDS, "arcade")
# ibm5150 is no system of the profile, and pc_kbd only it loads.
self.assertEqual(systems["pc_kbd"], ["arcade"])
def test_a_set_both_kinds_load_keeps_both(self):
# aes is placed by no seed and loads ym2610, as the MVS does.
systems = device_systems(_release().machines, SEEDS, "arcade")
self.assertEqual(systems["ym2610"], ["arcade", "snk-neogeo-mvs"])
self.assertEqual(systems["a2diskii"], ["apple2gs"])
self.assertEqual(systems["cart_dev"], ["snk-neogeo-mvs"])
def test_a_set_no_machine_instantiates_keeps_none(self):
machines = {
**_release().machines,
"orphan": MachineLinks("", True, ()),
}
systems = device_systems(machines, SEEDS, "arcade")
self.assertNotIn("orphan", systems)
def test_no_fallback_listed_is_attribute_systems(self):
machines = _release().machines
self.assertEqual(
device_systems(machines, SEEDS, ""), attribute_systems(machines, SEEDS)
)
class TheMergeWritesTheSystems(unittest.TestCase):
def setUp(self):
+3
View File
@@ -75,6 +75,9 @@ class ScriptsNeedOnlyPyyaml(unittest.TestCase):
local = {p.stem for p in scripts.rglob("*.py")} | {
p.name for p in scripts.iterdir() if p.is_dir()
} | {"scripts", "__future__"}
# install.py is the repository's own single file, stdlib only (its
# tests hold it to that): generate_games_index reads its narrowing.
local.add("install")
allowed = {"yaml"}
found = []
for path in sorted(scripts.rglob("*.py")):
+15
View File
@@ -134,6 +134,21 @@ class TestRedumpScraper(unittest.TestCase):
["/static/bios/Nintendo%20GC.dat", "/static/bios/Sony%20PSX.dat"],
)
def test_discover_bios_routes(self):
html = (
'<td><a href="/bios/XBOX">Datfile</a></td>'
'<td><a href="/bios/GC">Datfile</a></td>'
'<td><a href="/bios/PSX">Datfile</a></td>'
'<td><a href="/bios/PS2">Datfile</a></td>'
'<td><a href="/bios/PSX">Datfile</a></td>'
'<td><a href="/datfile/psx">Datfile</a></td>'
'<td><a href="/bios/">Index</a></td>'
)
self.assertEqual(
discover_bios_datfiles(html),
["/bios/GC", "/bios/PS2", "/bios/PSX", "/bios/XBOX"],
)
def test_parse_redump_dat(self):
name, version, entries = parse_redump_dat(LOGIQX_MACHINE_FIXTURE)
self.assertEqual(name, "Sony - PlayStation - BIOS Images")
+291
View File
@@ -0,0 +1,291 @@
"""Rom folder parsers of the card OSes and the menu-less frontends, offline.
The parsers run on fixture text through a fake Fetcher, so nothing here
reaches the network. The committed setup files are checked against the
schema and their own declared sources.
"""
from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from romfolders import split_refs # noqa: E402
from safeparse import yaml_load # noqa: E402
from scraper.romfolders import SourceError, load_sources, scrape_setup # noqa: E402
from scraper.romfolders.__main__ import dump_setup # noqa: E402
from scraper.romfolders.card import ( # noqa: E402
libretro_core,
parse_allium,
parse_bizhawk,
parse_crossmix,
parse_gmenu2x,
parse_koriki,
parse_minui,
parse_mister,
parse_muos,
parse_onion,
parse_plumos,
parse_romm,
parse_spruce,
parse_toml_subset,
ref,
)
FIXTURES = json.loads(
(ROOT / "tests" / "fixtures" / "romfolders_card" / "files.json").read_text(encoding="utf-8")
)
SETUPS = (
"onion", "allium", "spruceos", "miyoocfw", "koriki", "crossmix-os", "minui",
"myminui", "nextui", "plumos-pixel2", "plumos-mf", "muos", "misterfpga", "romm",
"bizhawk", "lakka", "retroarch",
)
class FakeFetcher:
"""Serves fixture files by source key, the Fetcher interface parsers use."""
def __init__(self, files: dict[str, dict[str, str]]) -> None:
self.files = files
def text(self, key: str, path: str) -> str:
try:
return self.files[key][path]
except KeyError as exc:
raise SourceError(f"{key}:{path} does not exist") from exc
def optional_text(self, key: str, path: str) -> str | None:
return self.files.get(key, {}).get(path)
def tree(self, key: str, prefix: str = "") -> list[str]:
return sorted(p for p in self.files.get(key, {}) if p.startswith(prefix))
def spec(source: str, **params: object) -> dict:
return {"params": {"source": source, **params}}
def by_folder(rows: list[dict]) -> dict[str, dict]:
return {row["folder"]: row for row in rows}
class LaunchScriptTests(unittest.TestCase):
def test_core_named_through_a_shell_variable(self) -> None:
script = "default_core=gpsp\nretroarch -L cores/${default_core}_libretro.so\n"
self.assertEqual(libretro_core(script), "gpsp")
def test_core_named_by_emu_exe(self) -> None:
self.assertEqual(libretro_core("EMU_EXE=fceumm\nminarch.elf x\n"), "fceumm")
def test_standalone_script_names_no_core(self) -> None:
self.assertIsNone(libretro_core("./drastic \"$1\"\n"))
def test_space_in_a_cited_path_is_escaped(self) -> None:
part = ref("os", "Emu/Commodore - Amiga/config.json", 3)
self.assertEqual(split_refs(part), [("os", "Emu/Commodore%20-%20Amiga/config.json", "3")])
class ParserTests(unittest.TestCase):
def setUp(self) -> None:
self.fetcher = FakeFetcher(FIXTURES)
def test_onion_emu_package_is_the_default_over_retroarch_section(self) -> None:
rows = by_folder(parse_onion({"params": {"source": "onion"}}, self.fetcher))
self.assertEqual(sorted(rows), ["GBA", "PCNINETYEIGHT"])
self.assertEqual(rows["GBA"]["core"], "gpsp")
self.assertEqual(rows["GBA"]["extensions"], [".gba", ".zip", ".7z"])
self.assertIn("Nintendo - GBA (VBA-M)", rows["GBA"]["note"])
self.assertNotIn("core", rows["PCNINETYEIGHT"])
def test_spruce_reads_default_and_alternative_folders(self) -> None:
rows = by_folder(parse_spruce(spec("spruce", skip=["-CUSTOM-SYSTEM-"]), self.fetcher))
self.assertEqual(list(rows), ["PS"])
self.assertEqual(rows["PS"]["aliases"], ["psx", "PS1"])
self.assertEqual(rows["PS"]["core"], "pcsx_rearmed")
self.assertEqual(rows["PS"]["extensions"], [".bin", ".cue", ".chd"])
self.assertIn("neogeo.zip", rows["PS"]["note"])
def test_crossmix_skips_separators_and_underscore_folders(self) -> None:
rows = by_folder(parse_crossmix(spec("crossmix"), self.fetcher))
self.assertEqual(list(rows), ["PS"])
self.assertEqual(rows["PS"]["core"], "pcsx_rearmed")
def test_koriki_first_launcher_wins(self) -> None:
params = {"source": "koriki", "sections": "base/.simplemenu/section_groups/apps and games.ini",
"launchers": "base/.simplemenu/launchers", "roms_prefix": "/mnt/SDCARD/Roms/",
"skip": ["CONSOLES"]}
rows = by_folder(parse_koriki({"params": params}, self.fetcher))
self.assertEqual(rows["PS"]["core"], "pcsx_rearmed")
self.assertEqual(rows["PS"]["extensions"], [".cue", ".chd", ".pbp"])
self.assertEqual(rows["PS"]["source_ref"],
"koriki base/.simplemenu/section_groups/apps%20and%20games.ini:4")
def test_allium_folder_is_the_first_pattern(self) -> None:
rows = by_folder(parse_allium(spec("allium", consoles="consoles.toml", cores="cores.toml"),
self.fetcher))
self.assertEqual(sorted(rows), ["ARCADE", "GBA"])
self.assertEqual(rows["ARCADE"]["core"], "mame2003_plus")
self.assertEqual(rows["ARCADE"]["aliases"], ["mame"])
self.assertEqual(rows["ARCADE"]["source_ref"], "allium consoles.toml:1")
def test_toml_subset_matches_tomllib(self) -> None:
try:
import tomllib
except ImportError:
self.skipTest("tomllib needs Python 3.11")
def clean(node: object) -> object:
if isinstance(node, dict):
return {k: clean(v) for k, v in node.items() if k != "__line__"}
if isinstance(node, list):
return [clean(v) for v in node]
return node
for name in ("consoles.toml", "cores.toml"):
text = FIXTURES["allium"][name]
self.assertEqual(clean(parse_toml_subset(text)), tomllib.loads(text))
def test_toml_outside_the_subset_is_refused(self) -> None:
with self.assertRaises(SourceError):
parse_toml_subset("when = 1979-05-27T07:32:00Z\n")
def test_minui_takes_the_core_most_devices_run(self) -> None:
rows = by_folder(parse_minui(spec("minui"), self.fetcher))
ps = rows["Sony PlayStation (PS)"]
self.assertEqual(ps["name"], "PS")
self.assertEqual(ps["core"], "pcsx_rearmed")
self.assertEqual(ps["board_defaults"]["tg5040"]["core"], "swanstation")
self.assertNotIn("rg35xx", ps["board_defaults"])
self.assertEqual(rows["A2600"]["core"], "stella2014")
self.assertNotIn("extensions", ps)
def test_plumos_shared_folder_goes_to_the_first_system(self) -> None:
rows = by_folder(parse_plumos(spec("plumos", path="systems.json"), self.fetcher))
self.assertEqual(rows["FC"]["name"], "nes")
self.assertEqual(rows["FC"]["core"], "quicknes")
self.assertEqual(rows["fds"]["emulator"], "standalone")
self.assertIn("FC", rows["fds"]["aliases"])
self.assertIn("disabled by default", rows["fds"]["note"])
def test_muos_folder_is_the_friendly_name_of_the_catalogue(self) -> None:
rows = by_folder(parse_muos(spec("muos", path="core.json"), self.fetcher))
self.assertEqual(rows["sonyplaystation"]["core"], "pcsx_rearmed")
self.assertEqual(rows["sonyplaystation"]["emulator"], "retroarch")
self.assertEqual(rows["nintendods"]["core"], "drastic")
self.assertEqual(rows["nintendods"]["emulator"], "external")
def test_gmenu2x_link_and_shipped_folders(self) -> None:
params = {"source": "gmenu2x", "shipped_roms": "board/miyoo/roms",
"link_roms_prefix": "/mnt/roms",
"links": [{"source": "gpsp", "path": "miyoo/opkg_assets/gpsp.lnk", "core": "gpsp"}]}
rows = by_folder(parse_gmenu2x({"params": params}, self.fetcher))
self.assertEqual(sorted(rows), ["GB", "GBA"])
self.assertEqual(rows["GBA"]["extensions"], [".gba", ".zip"])
self.assertEqual(rows["GBA"]["source_ref"], "gpsp miyoo/opkg_assets/gpsp.lnk:3")
self.assertNotIn("core", rows["GB"])
def test_mister_rows_are_the_shipped_core_folders(self) -> None:
rows = by_folder(parse_mister(spec("dist", arcade=["mame"], arcade_note="mra"), self.fetcher))
self.assertEqual(sorted(rows), ["SNES", "mame"])
self.assertTrue(rows["mame"]["arcade"])
self.assertIsNone(rows["SNES"]["arcade"])
def test_romm_reads_multiline_arrays_and_bios_only_slugs(self) -> None:
rows = by_folder(parse_romm(spec("romm", cores_map="index.ts",
bios_fixture="known_bios_files.json"), self.fetcher))
self.assertEqual(sorted(rows), ["3do", "3ds", "64dd", "arcade"])
self.assertEqual(rows["arcade"]["core"], "mame2003_plus")
self.assertIn("nightly", rows["3ds"]["note"])
self.assertNotIn("core", rows["64dd"])
def test_bizhawk_combined_systems_and_extension_lists(self) -> None:
rows = by_folder(parse_bizhawk(spec("bizhawk", paths="PathEntryCollection.cs",
rom_loader="RomLoader.cs", config="Config.cs"),
self.fetcher))
self.assertEqual(rows["Gameboy"]["name"], "GB")
self.assertEqual(rows["Gameboy"]["aliases"], ["GBC", "SGB"])
self.assertEqual(rows["Gameboy"]["extensions"], [".gb", ".gbc", ".sgb"])
self.assertEqual(rows["NES"]["extensions"], [".nes", ".fds", ".unf"])
self.assertIn("QuickNes", rows["NES"]["note"])
self.assertEqual(rows["32X"]["extensions"], [])
self.assertNotIn("core", rows["NES"])
class ScrapeTests(unittest.TestCase):
def test_two_scrapes_write_the_same_bytes(self) -> None:
setup = {
"name": "Fixture", "platform": "retroarch", "format": "plumos",
"params": {"source": "plumos", "path": "systems.json"},
"sources": {"plumos": {"repo": "https://github.com/a/b", "commit": "0" * 40}},
"roms_root": {"path": "/r", "from_bios": "../r", "source_ref": "plumos systems.json:1"},
"case_sensitive": None,
}
first = dump_setup(scrape_setup("fixture", setup, FakeFetcher(FIXTURES)))
second = dump_setup(scrape_setup("fixture", setup, FakeFetcher(FIXTURES)))
self.assertEqual(first, second)
self.assertEqual(yaml_load(first)["folders"][0]["folder"], "FC")
def test_an_empty_read_fails_unless_allowed(self) -> None:
setup = {"format": "mister", "params": {"source": "none", "arcade_note": "x"}}
with self.assertRaises(SourceError):
scrape_setup("empty", setup, FakeFetcher({}))
class CommittedSetupTests(unittest.TestCase):
def setUp(self) -> None:
self.sources = load_sources(ROOT / "platforms" / "romfolders" / "_sources")
def documents(self) -> dict[str, dict]:
out = {}
for setup in SETUPS:
path = ROOT / "platforms" / "romfolders" / f"{setup}.yml"
with path.open(encoding="utf-8") as handle:
out[setup] = yaml_load(handle)
return out
def test_every_owned_setup_has_sources_and_output(self) -> None:
for setup in SETUPS:
self.assertIn(setup, self.sources, setup)
for setup, doc in self.documents().items():
self.assertEqual(doc["setup"], setup)
self.assertEqual(doc["platform"], self.sources[setup]["platform"])
def test_every_ref_names_a_declared_source(self) -> None:
for setup, doc in self.documents().items():
declared = set(doc["sources"])
refs = [doc["roms_root"]["source_ref"]]
refs += [doc["case_ref"]] if doc.get("case_ref") else []
for block in ("skip", "hidden", "media"):
refs += [r["source_ref"] for r in doc.get(block, [])]
refs += [row["source_ref"] for row in doc["folders"]]
refs += [b["source_ref"] for row in doc["folders"]
for b in (row.get("board_defaults") or {}).values()]
for source_ref in refs:
for key, _, _ in split_refs(source_ref):
self.assertIn(key, declared, f"{setup}: {source_ref}")
def test_committed_setups_validate_against_the_schema(self) -> None:
try:
from jsonschema import Draft202012Validator
except ImportError:
self.skipTest("jsonschema not installed")
schema = json.loads((ROOT / "schemas" / "romfolders.schema.json").read_text(encoding="utf-8"))
validator = Draft202012Validator(schema)
for setup, doc in self.documents().items():
errors = [e.message for e in validator.iter_errors(doc)]
self.assertEqual(errors, [], setup)
def test_rows_with_folders_are_not_empty(self) -> None:
for setup, doc in self.documents().items():
if self.sources[setup].get("allow_empty"):
self.assertEqual(doc["folders"], [], setup)
else:
self.assertGreater(len(doc["folders"]), 0, setup)
if __name__ == "__main__":
unittest.main()
+330
View File
@@ -0,0 +1,330 @@
"""EmulationStation-family rom folder parsers, on fixtures and on the
committed setup files.
The parsers run offline against tests/fixtures/romfolders/es/ through a
fetcher that reads files instead of a forge. The committed files under
platforms/romfolders/ are checked for shape: schema, every source_ref
naming a declared source, and a second dump giving the same bytes.
"""
from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from romfolders import setup_paths, split_refs
from scraper.romfolders import (
Fetcher,
SourceError,
load_formats,
load_sources,
scrape_setup,
)
from scraper.romfolders.__main__ import dump_setup
from scraper.romfolders.es import _module_valid
FIXTURES = ROOT / "tests" / "fixtures" / "romfolders" / "es"
ROMFOLDERS = ROOT / "platforms" / "romfolders"
RECALBOX_PENDING = (
"Recalbox source not fetched yet: run python -m scripts.scraper.romfolders --setup recalbox"
)
ES_SETUPS = (
"batocera", "knulli", "rocknix", "jelos", "kolos", "unofficialos", "archr",
"amberelec", "chaos", "pan4elec", "emuelec", "recalbox", "retrobat", "arkos",
"darkos", "retropie", "emudeck", "emudeck-windows", "retrodeck",
)
class FixtureFetcher(Fetcher):
"""Reads `<source key>/<path>` under one fixture directory."""
def __init__(self, base: Path) -> None:
super().__init__({"os": {"repo": "https://github.com/x/y", "commit": "0" * 40}})
self.base = base
def optional_text(self, key: str, path: str) -> str | None:
target = self.base / path
return target.read_text(encoding="utf-8") if target.is_file() else None
def text(self, key: str, path: str) -> str:
found = self.optional_text(key, path)
if found is None:
raise SourceError(f"{path} does not exist")
return found
def tree(self, key: str, prefix: str = "") -> list[str]:
return sorted(
p.relative_to(self.base).as_posix() for p in self.base.rglob("*")
if p.is_file() and p.relative_to(self.base).as_posix().startswith(prefix)
)
def entries(self, key: str, directory: str) -> list[str]:
return sorted(p.relative_to(self.base).as_posix() for p in (self.base / directory).iterdir())
def scrape(fmt: str, base: str, params: dict) -> dict:
spec = {"name": "Fixture", "platform": "", "format": fmt, "params": params}
return scrape_setup("fixture", spec, FixtureFetcher(FIXTURES / base))
def by_folder(doc: dict) -> dict[str, dict]:
return {row["folder"]: row for row in doc["folders"]}
class BatoceraFormat(unittest.TestCase):
def setUp(self) -> None:
self.doc = scrape("batocera-es", "batocera", {
"es_systems": "es_systems.yml", "configgen_dir": "configs"})
self.rows = by_folder(self.doc)
def test_systems_without_emulator_or_outside_root_are_left_out(self) -> None:
self.assertEqual(sorted(self.rows), ["mame", "snes"])
def test_extensions_lowercased_with_dot(self) -> None:
self.assertEqual(self.rows["snes"]["extensions"], [".sfc", ".smc", ".zip"])
def test_default_and_board_override(self) -> None:
snes = self.rows["snes"]
self.assertEqual((snes["core"], snes["emulator"]), ("snes9x", "libretro"))
self.assertEqual(snes["board_defaults"]["bcm2835"]["core"], "pocketsnes")
# A symlinked board file is followed to its target.
self.assertEqual(snes["board_defaults"]["bcm2836"]["core"], "pocketsnes")
self.assertIn("configgen-defaults-bcm2835.yml",
snes["board_defaults"]["bcm2836"]["source_ref"])
def test_arcade_flag_from_hardware(self) -> None:
self.assertTrue(self.rows["mame"]["arcade"])
self.assertNotIn("arcade", self.rows["snes"])
def test_source_ref_names_the_lines(self) -> None:
self.assertEqual(self.rows["snes"]["source_ref"],
"os es_systems.yml:1; os configs/configgen-defaults.yml:4")
class EsXmlFormat(unittest.TestCase):
def setUp(self) -> None:
self.params = {"file": "es_systems.cfg", "path_prefixes": ["/storage/roms/"], "default": "flagged"}
self.rows = by_folder(scrape("es-xml", "xml", self.params))
def test_ignored_commented_and_outside_systems_are_left_out(self) -> None:
self.assertEqual(sorted(self.rows), ["advision", "mame", "psx"])
def test_flagged_default_wins(self) -> None:
self.assertEqual(self.rows["psx"]["core"], "swanstation")
def test_first_rule_takes_the_first_core(self) -> None:
rows = by_folder(scrape("es-xml", "xml", {**self.params, "default": "first"}))
self.assertEqual(rows["psx"]["core"], "pcsx_rearmed")
def test_core_read_from_command_when_no_emulator_is_listed(self) -> None:
self.assertEqual((self.rows["advision"]["core"], self.rows["advision"]["emulator"]),
("mess", "retroarch"))
def test_arcade_flag_from_platform(self) -> None:
self.assertTrue(self.rows["mame"]["arcade"])
def test_board_file_gives_board_default(self) -> None:
# es_systems.board.cfg flags duckstation where the main file flags swanstation.
rows = by_folder(scrape("es-xml", "xml", {**self.params, "boards": {"rk3326": "es_systems.board.cfg"}}))
self.assertEqual(rows["psx"]["board_defaults"]["rk3326"]["core"], "duckstation")
self.assertNotIn("board_defaults", rows["mame"])
def test_source_ref_points_at_the_name_line(self) -> None:
self.assertEqual(self.rows["psx"]["source_ref"], "os es_systems.cfg:4")
self.assertEqual(self.rows["advision"]["source_ref"], "os es_systems.cfg:20")
class EsJsonFormat(unittest.TestCase):
def test_flagged_default_and_ignored_system(self) -> None:
rows = by_folder(scrape("es-json", "json", {
"file": "es_systems.json", "path_prefixes": ["/storage/roms/"]}))
self.assertEqual(sorted(rows), ["pico", "pico-8", "psx"])
self.assertEqual(rows["psx"]["core"], "duckstation")
self.assertEqual(rows["psx"]["extensions"], [".cue"])
def test_a_name_given_twice_names_neither_folder(self) -> None:
rows = by_folder(scrape("es-json", "json", {
"file": "es_systems.json", "path_prefixes": ["/storage/roms/"]}))
for folder, line in (("pico-8", 19), ("pico", 25)):
with self.subTest(folder=folder):
self.assertNotIn("name", rows[folder])
self.assertIn("2 systems pico8", rows[folder]["note"])
self.assertEqual(rows[folder]["source_ref"], f"os es_systems.json:{line}")
self.assertEqual(rows["pico"]["core"], "genesis_plus_gx")
class ElecConfFormat(unittest.TestCase):
def setUp(self) -> None:
self.rows = by_folder(scrape("elec-conf", "elec", {
"package": "package.mk", "conf_dir": "config/emulators",
"path_prefixes": ["/storage/roms/"]}))
def test_folders_from_conf_paths(self) -> None:
self.assertEqual(sorted(self.rows), ["3do", "psx"])
def test_star_branch_is_the_default_and_devices_are_boards(self) -> None:
psx = self.rows["psx"]
self.assertEqual(psx["core"], "swanstation")
self.assertEqual(psx["board_defaults"], {
"DEVICE=RK3326": {"core": "pcsx_rearmed", "emulator": "retroarch",
"source_ref": "os package.mk:11"}})
def test_unconditional_default(self) -> None:
self.assertEqual(self.rows["3do"]["core"], "opera")
self.assertEqual(self.rows["3do"]["source_ref"],
"os config/emulators/3do.conf:3,4; os package.mk:7; os package.mk:6")
class EsdeFormat(unittest.TestCase):
def setUp(self) -> None:
self.rows = by_folder(scrape("esde", "esde", {
"systems": {"source": "os", "path": "es_systems.xml"},
"custom": [{"source": "os", "path": "custom_systems.xml"}],
"alternatives": {"source": "os", "path": "setup.sh", "function": "ESDE_setDefaultEmulators"},
}))
def test_alternative_emulator_label_picks_the_command(self) -> None:
psx = self.rows["psx"]
self.assertEqual((psx["core"], psx["emulator"]), ("duckstation", "duckstation"))
self.assertIn("os setup.sh:3", psx["source_ref"])
def test_custom_system_replaces_the_bundled_one(self) -> None:
self.assertNotIn("model2", self.rows)
model2 = self.rows["model2/roms"]
self.assertEqual(model2["core"], "model-2")
self.assertIn("names no command", model2["note"])
def test_first_command_without_alternative(self) -> None:
rows = by_folder(scrape("esde", "esde", {"systems": {"source": "os", "path": "es_systems.xml"}}))
self.assertEqual((rows["psx"]["core"], rows["psx"]["emulator"]), ("mednafen_psx", "retroarch"))
class RecalboxFormat(unittest.TestCase):
def setUp(self) -> None:
self.rows = by_folder(scrape("recalbox", "recalbox", {"systems_dir": "systems"}))
def test_tie_goes_to_the_last_core(self) -> None:
self.assertEqual(self.rows["psx"]["core"], "swanstation")
def test_board_priority(self) -> None:
self.assertEqual(self.rows["psx"]["board_defaults"]["rpi1"]["core"], "mednafen_psx")
def test_unopened_archive_extension(self) -> None:
mame = self.rows["mame"]
self.assertEqual(mame["extensions"], [".zip"])
self.assertTrue(mame["arcade"])
self.assertIn("!", mame["note"])
class RetroPieFormat(unittest.TestCase):
def setUp(self) -> None:
self.rows = by_folder(scrape("retropie", "retropie", {
"platforms_cfg": "platforms.cfg", "module_dirs": ["emulators", "libretrocores"],
"install_sections": ["core", "main", "opt", "exp"],
"platform_flags": ["native", "64bit", "gl", "vulkan", "x11", "x86"]}))
def test_folders_and_loop_systems(self) -> None:
self.assertEqual(sorted(self.rows), ["nes", "psx", "vectrex"])
self.assertEqual(self.rows["nes"]["core"], "lr-fceumm")
self.assertEqual(self.rows["nes"]["emulator"], "retroarch")
def test_flags_and_section_order(self) -> None:
# lr-pcsx-rearmed is excluded on x86, opt comes before exp.
self.assertEqual(self.rows["psx"]["core"], "lr-swanstation")
self.assertNotIn("core", self.rows["vectrex"])
def test_module_flags_read_in_order(self) -> None:
x86 = {"x86", "64bit"}
self.assertFalse(_module_valid(["!all"], x86))
self.assertTrue(_module_valid(["!all", "x86"], x86))
self.assertFalse(_module_valid(["!x86"], x86))
self.assertTrue(_module_valid(["!arm"], x86))
class Determinism(unittest.TestCase):
def test_two_scrapes_dump_the_same_bytes(self) -> None:
params = {"file": "es_systems.cfg", "path_prefixes": ["/storage/roms/"]}
first = dump_setup(scrape("es-xml", "xml", params))
second = dump_setup(scrape("es-xml", "xml", params))
self.assertEqual(first, second)
class CommittedSetups(unittest.TestCase):
"""The setup files this parser family writes."""
@classmethod
def setUpClass(cls) -> None:
cls.sources = load_sources(ROMFOLDERS / "_sources")
cls.paths = {p.stem: p for p in setup_paths(ROMFOLDERS)}
def written(self, setup: str) -> Path:
"""The setup file, or a skip for a source not fetched yet."""
if setup == "recalbox" and setup not in self.paths:
self.skipTest(RECALBOX_PENDING)
return self.paths[setup]
def test_every_es_setup_is_declared_and_written(self) -> None:
formats = load_formats()
for setup in ES_SETUPS:
with self.subTest(setup=setup):
self.assertIn(setup, self.sources)
self.assertIn(self.sources[setup]["format"], formats)
self.assertEqual(self.written(setup).stem, setup)
def test_schema(self) -> None:
try:
from jsonschema import Draft202012Validator
except ImportError:
self.skipTest("jsonschema not installed")
from safeparse import yaml_load
schema = json.loads((ROOT / "schemas" / "romfolders.schema.json").read_text())
validator = Draft202012Validator(schema)
for setup in ES_SETUPS:
with self.subTest(setup=setup):
doc = yaml_load(self.written(setup).read_text(encoding="utf-8"))
errors = [e.message for e in validator.iter_errors(doc)]
self.assertEqual(errors, [])
def test_every_source_ref_names_a_declared_source(self) -> None:
from safeparse import yaml_load
for setup in ES_SETUPS:
with self.subTest(setup=setup):
doc = yaml_load(self.written(setup).read_text(encoding="utf-8"))
if setup not in self.paths:
continue
refs = [doc["roms_root"]["source_ref"]]
for key in ("skip", "hidden", "media"):
refs += [rule["source_ref"] for rule in doc.get(key, [])]
for row in doc["folders"]:
refs.append(row["source_ref"])
refs += [b["source_ref"] for b in (row.get("board_defaults") or {}).values()]
for ref in refs:
for key, _path, _lines in split_refs(ref):
with self.subTest(setup=setup, ref=ref):
self.assertIn(key, doc["sources"])
def test_committed_file_is_its_own_dump(self) -> None:
from safeparse import yaml_load
for setup in ES_SETUPS:
with self.subTest(setup=setup):
text = self.written(setup).read_text(encoding="utf-8")
self.assertEqual(dump_setup(yaml_load(text)), text)
def test_elec_family_skips_its_bios_folder(self) -> None:
from safeparse import yaml_load
for setup in ("rocknix", "jelos", "kolos", "unofficialos", "archr",
"amberelec", "chaos", "pan4elec", "emuelec", "arkos", "darkos"):
doc = yaml_load(self.paths[setup].read_text(encoding="utf-8"))
with self.subTest(setup=setup):
self.assertEqual(doc["roms_root"]["from_bios"], "..")
self.assertIn("bios", {rule.get("name") for rule in doc.get("skip", [])})
self.assertNotIn("bios", {row["folder"] for row in doc["folders"]})
if __name__ == "__main__":
unittest.main()
+20
View File
@@ -374,6 +374,26 @@ class ListXmlRecipes(unittest.TestCase):
[m["name"] for m in by_name["parentset.zip"]["members"]], ["shared.rom"]
)
def test_a_machine_larger_than_the_window_is_kept(self):
"""Each machine here spans several reads: the window grows until the
element closes instead of cutting its start tag away."""
from scripts.scraper import romset_dat_importer
expected = listxml_entries(self._opener(self._document()), "MAME 0.289", None)
saved = romset_dat_importer.LISTXML_CHUNK
try:
for window in (1, 7, 16, 64):
romset_dat_importer.LISTXML_CHUNK = window
entries = listxml_entries(
self._opener(self._document()), "MAME 0.289", None
)
self.assertEqual(entries, expected, window)
finally:
romset_dat_importer.LISTXML_CHUNK = saved
self.assertEqual(
[entry["name"] for entry in entries], ["childset.zip", "parentset.zip"]
)
def test_an_undumped_only_set_is_dropped(self):
entries = listxml_entries(self._opener(self._document()), "MAME 0.289", None)
self.assertNotIn("undumped.zip", {entry["name"] for entry in entries})
+3
View File
@@ -738,6 +738,9 @@ class EveryGitHubApiCallerSendsTheToken(unittest.TestCase):
str(path.relative_to(scripts))
for path in [*scripts.rglob("*.py"), *scripts.rglob("*.sh")]
if "api.github.com" in (source := path.read_text(encoding="utf-8"))
# arcade_parent_importer names API URLs and sends them through
# romset_dat_importer._api_json, which holds the token.
and ("Request(" in source or "urlopen(" in source or path.suffix == ".sh")
and "GITHUB_TOKEN" not in source
and "github_headers(" not in source
and "github_token(" not in source
+40
View File
@@ -117,6 +117,46 @@ class ASplitEmulatorListsItsCardsAsRows(unittest.TestCase):
self.assertIn(">missing</span>", page)
class APartIsCutByTheWordsItsRowsShow(unittest.TestCase):
"""MAME's rows said "required for <system>" once 888 entries named a
system, and its first part showed 1,931 words for the 180 cards a
count allowed."""
def setUp(self):
systems = ["nintendo-nes", "sega-mega-drive", "sony-playstation"]
self.files = [_archive(i, members=1, system=systems[i % 3], region="japan",
md5=f"{i:032x}")
for i in range(gs.EMULATOR_CARDS_PER_PART * 2)]
self.profile = {"emulator": "Wide", "type": "standalone", "systems": systems,
"files": self.files}
def test_rows_that_say_more_take_more_parts(self):
parts = gs.emulator_parts("wide", self.files, self.profile, lambda _f: False)
self.assertGreater(len(parts), -(-len(self.files) // gs.EMULATOR_CARDS_PER_PART))
words = gs._entry_row_words(self.files, self.profile, lambda _f: False)
shown = [sum(words[i] for i in entries) for _stem, entries in parts]
self.assertLessEqual(max(shown), gs.EMULATOR_PART_WORDS)
# Balanced: no part a remainder of a few rows.
self.assertLess(max(shown) - min(shown), max(words) * 3)
self.assertEqual(sorted(i for _stem, entries in parts for i in entries),
list(range(len(self.files))))
def test_each_part_page_stays_under_the_built_ceiling(self):
pages = gs.emulator_pages("wide", self.profile, EMPTY_DB)
parts = gs.emulator_parts("wide", self.files, self.profile, lambda _f: False)
self.assertEqual(sorted(pages), sorted(["wide", *(stem for stem, _e in parts)]))
ceiling = VISIBLE_CEILINGS["emulator card parts"][1]
for stem, _entries in parts:
with self.subTest(stem=stem):
parser = _Visible()
parser.feed(f"<main>{pages[stem]}</main>")
self.assertLessEqual(parser.words, ceiling)
def test_the_budget_leaves_room_for_the_frame(self):
self.assertLessEqual(gs.EMULATOR_PART_WORDS + 200,
VISIBLE_CEILINGS["emulator card parts"][1])
class ALongEmulatorPageIsOneTable(unittest.TestCase):
def test_rows_replace_the_index_and_the_cards(self):
files = ([_archive(i) for i in range(gs.EMULATOR_TABLE_MIN)]
+3 -1
View File
@@ -160,7 +160,9 @@ class SetupPagesOfferPartOfThePack(unittest.TestCase):
self.assertIn(f"/retrobios/which-pack/?platform={key}#only-some-consoles", text)
# The screen's own row: the line menu's c is not what the
# one-liner opens.
self.assertIn("or choose **Choose systems** in the command.", text)
self.assertIn("or the command's **Choose systems**.", text)
# Never "choose **Choose systems**" (tui_audit_17 P3-9).
self.assertNotIn("choose **Choose systems**", text)
def test_the_anchor_exists(self):
self.assertIn("## Only some consoles?", _page("which-pack.md"))
-1
View File
@@ -32,7 +32,6 @@ UNLINKED: dict[str, str] = {
# several systems' files or not reviewed as the system's own. The
# heading names the program, not the system.
"capcom-cps3": "Arcade > FBNeo",
"cps3": "Arcade > Arcade holds one CPS-3 set among every board's",
"commanderx16": "Arcade > Arcade",
"dark-forces-ii": "Game Engines > OpenJKDF2",
"fpinball": "Other > future-pinball-fploader",
+1
View File
@@ -279,6 +279,7 @@ class EveryTestDependencyIsInstalledInCi(unittest.TestCase):
{p.stem for p in (root / "scripts").rglob("*.py")}
| {p.name for p in (root / "scripts").iterdir() if p.is_dir()}
| {p.stem for p in (root / "tests").glob("*.py")}
| {p.name for p in (root / "tests").iterdir() if p.is_dir()}
| {"scripts", "tests", "install"}
)
imported: set[str] = set()
+57 -2
View File
@@ -16,8 +16,8 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from common import SYSTEM_ALIASES, load_emulator_profiles # noqa: E402
from packextras import _slug_for # noqa: E402
from common import SYSTEM_ALIASES, _norm_system_id, load_emulator_profiles # noqa: E402
from packextras import _names_content, _slug_for # noqa: E402
class ProfilesSpellEachMachineOnce(unittest.TestCase):
@@ -35,6 +35,61 @@ class ProfilesSpellEachMachineOnce(unittest.TestCase):
self.assertEqual(sorted(set(found)), [])
class PlatformNamesReachTheProfileSystem(unittest.TestCase):
"""Folder and system names a platform gives a machine, joined to the
profile that runs it there. Each pair fell back to the whole profile in
a survey of the rom folders of 38 setups, the two spellings not meeting
once normalized."""
# (name a platform uses, profile running it there)
PAIRS = [
("apple-iigs", "mame"),
("apple2gs", "gsplus-legacy"),
("astrocde", "mame"),
("astrocade", "mame"),
("creativision", "mame"),
("gamepocketcomputer", "mame"),
("nintendo-sgb", "bsnes"),
("nintendo-sgb", "mesen-s"),
("capcom-cps1", "fbalpha2012_cps1"),
("capcom-cps2", "fbalpha2012_cps2"),
("capcom-cps3", "fbalpha2012_cps3"),
("capcom-cps1", "fbalpha2012"),
("igs-pgm", "fbalpha2012"),
]
def test_each_name_meets_a_system_of_its_profile(self):
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
unmet = [
f"{name} -> {emulator}"
for name, emulator in self.PAIRS
if _norm_system_id(name)
not in {_norm_system_id(str(s)) for s in profiles[emulator].get("systems", [])}
]
self.assertEqual(unmet, [])
def test_both_spellings_normalize_alike(self):
for alias, canonical in SYSTEM_ALIASES.items():
with self.subTest(alias=alias):
self.assertEqual(_norm_system_id(alias), _norm_system_id(canonical))
class AFoldedClaimKeepsItsProof(unittest.TestCase):
"""One spelling per machine folds bsnes-hd's SGB1.sfc onto bsnes's: the
entry still pins the bytes, so it is placed before nSide's SGB1.sfc/
tree and the file keeps its path."""
def test_a_hash_on_a_folded_claim_counts(self):
entry = {
"name": "SGB1.sfc", "md5": None, "sha1": None,
"other_claims": [{"name": "SGB1.sfc", "md5": "b15ddb15721c657d82c5bab6db982ee9"}],
}
self.assertTrue(_names_content(entry))
def test_names_alone_name_no_content(self):
self.assertFalse(_names_content({"name": "SGB1.sfc", "other_claims": [{"name": "SGB1.sfc"}]}))
class AFileGoesToItsOwnSystemFolder(unittest.TestCase):
PROFILES = {"picodrive": {"systems": ["sega-game-gear", "sega-megacd"]}}
SYSTEMS = {"sega-game-gear", "sega-mega-cd"}