mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-15 06:23:47 -06:00
chore: merge main into Sony pages
This commit is contained in:
commit
0bd206da4d
410 files changed
+146998
-134303
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
Vendored
+27
@@ -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>
|
||||
+24
@@ -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 & 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>
|
||||
@@ -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
@@ -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
|
||||
Vendored
+8
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
@@ -0,0 +1,5 @@
|
||||
ESDE_setDefaultEmulators(){
|
||||
mkdir -p "$gamelists"
|
||||
ESDE_setEmu 'DuckStation (Standalone)' psx
|
||||
ESDE_setEmu 'Missing Label' model2
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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%"
|
||||
}
|
||||
+9
@@ -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
|
||||
}
|
||||
+7
@@ -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"
|
||||
}
|
||||
+6
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
+100
-18
@@ -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
|
||||
Vendored
+48
-22
@@ -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
|
||||
Vendored
+56
-24
@@ -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
|
||||
Vendored
+68
-18
@@ -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
|
||||
Vendored
+8
-8
@@ -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
|
||||
Vendored
+10
-10
@@ -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
|
||||
Vendored
+10
-10
@@ -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
|
||||
Vendored
+8
-8
@@ -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
|
||||
Vendored
+40
@@ -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
|
||||
Vendored
+12
@@ -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
|
||||
Vendored
+15
@@ -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
|
||||
Vendored
+24
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -5,8 +5,8 @@
|
||||
|
||||
> [1] North America
|
||||
[ ] Japan
|
||||
|
||||
|
||||
None
|
||||
Done
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -8,5 +8,5 @@
|
||||
|
||||
> [1] North America
|
||||
[ ] Japan
|
||||
|
||||
1-2 of 2
|
||||
A Tick B Done <> Order ? Help
|
||||
Vendored
+2
-2
@@ -8,8 +8,8 @@
|
||||
|
||||
> [1] North America
|
||||
[ ] Japan
|
||||
|
||||
|
||||
None
|
||||
Done
|
||||
|
||||
|
||||
A Tick B Done <> Order ? Help
|
||||
Vendored
+2
-2
@@ -6,8 +6,8 @@
|
||||
|
||||
> [1] North America
|
||||
[ ] Japan
|
||||
|
||||
|
||||
None
|
||||
Done
|
||||
|
||||
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
Vendored
+2
-2
@@ -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
|
||||
Vendored
+2
-2
@@ -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
|
||||
Vendored
+1
-1
@@ -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
|
||||
Vendored
+141
-62
@@ -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
@@ -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
@@ -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
|
||||
Vendored
+172
-64
@@ -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
|
||||
Vendored
+141
-62
@@ -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
|
||||
Vendored
+170
-62
@@ -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
@@ -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
|
||||
Vendored
+7
-7
@@ -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
|
||||
Vendored
+8
-8
@@ -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
|
||||
Vendored
+6
-6
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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": {}}}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -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},
|
||||
|
||||
@@ -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()
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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):
|
||||
|
||||
@@ -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")):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in new issue
Block a user