feat: add launchbox detection to installer

This commit is contained in:
Abdessamad Derraz committed 2026-08-08 04:24:17 +02:00
1 parent 393d32ed84
commit 69d53c97c9
14 files changed
+1509 -1116

No files matched your search

+174 -14
View File
@@ -1,6 +1,6 @@
# RetroBIOS installer for Windows (PowerShell 5+, no Python required)
$ErrorActionPreference = "Stop"
$baseUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main"
$baseUrl = if ($env:RETROBIOS_BASE_URL) { $env:RETROBIOS_BASE_URL } else { "https://raw.githubusercontent.com/Abdess/retrobios/main" }
$releaseUrl = "https://github.com/Abdess/retrobios/releases/download/large-files"
$platform = $null
@@ -17,22 +17,179 @@ if (Test-Path $emudeckSettings) {
}
}
# Expand the notations RetroArch writes into its config values:
# '~' is the home directory and ':' the application directory, both dropping
# two leading characters (libretro-common/file/file_path.c).
function Expand-RetroArchPath {
param([string]$Value, [string]$AppDir)
if ($Value.StartsWith('~')) { return Join-Path $env:USERPROFILE $Value.Substring(2) }
if ($Value.StartsWith(':')) { return Join-Path $AppDir $Value.Substring(2) }
return [Environment]::ExpandEnvironmentVariables($Value)
}
function Get-RetroArchSystemDir {
param([string]$CfgPath, [string]$AppDir)
if (-not (Test-Path $CfgPath)) { return $null }
foreach ($line in Get-Content $CfgPath) {
if ($line -match '^\s*system_directory\s*=\s*"?([^"]*)"?') {
$val = $Matches[1].Trim()
if (-not $val -or $val -eq "default") { return Join-Path $AppDir "system" }
return Expand-RetroArchPath -Value $val -AppDir $AppDir
}
}
return $null
}
# Detect RetroArch
if (-not $platform) {
$raCfg = Join-Path $env:APPDATA "RetroArch\retroarch.cfg"
if (Test-Path $raCfg) {
$platform = "retroarch"
$biosPath = Join-Path $env:APPDATA "RetroArch\system"
foreach ($line in Get-Content $raCfg) {
if ($line -match '^\s*system_directory\s*=\s*"?([^"]+)"?') {
$val = $Matches[1].Trim()
if ($val -and $val -ne "default") {
$biosPath = $val
$raRoot = Join-Path $env:APPDATA "RetroArch"
$found = Get-RetroArchSystemDir -CfgPath $raCfg -AppDir $raRoot
$biosPath = if ($found) { $found } else { Join-Path $raRoot "system" }
Write-Host "Found RetroArch at $biosPath"
}
}
# Locate LaunchBox. It installs wherever the user points its installer and
# writes no uninstall registry key, so the Start menu shortcut is the only
# record of that choice.
function Get-LaunchBoxRoot {
$candidates = @()
$lnk = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\LaunchBox\LaunchBox.lnk"
if (Test-Path $lnk) {
$bytes = [System.IO.File]::ReadAllBytes($lnk)
$text = [System.Text.Encoding]::ASCII.GetString($bytes)
if ($text -match '([A-Za-z]:\\[ -~]{0,260}?LaunchBox\.exe)') {
# The shortcut points at Core\LaunchBox.exe
$exeDir = Split-Path $Matches[1] -Parent
$candidates += (Split-Path $exeDir -Parent)
$candidates += $exeDir
}
}
$candidates += (Join-Path $env:USERPROFILE "LaunchBox")
foreach ($root in $candidates) {
if (Test-Path (Join-Path $root "Data\Emulators.xml")) { return $root }
}
return $null
}
# Map each emulator LaunchBox knows about to its installation directory.
# ApplicationPath is either absolute or relative to the LaunchBox root.
function Get-LaunchBoxEmulators {
param([string]$Root)
$map = @{}
if (-not $Root) { return $map }
$xmlPath = Join-Path $Root "Data\Emulators.xml"
if (-not (Test-Path $xmlPath)) { return $map }
$doc = $null
try { $doc = [xml](Get-Content $xmlPath -Raw) } catch { return $map }
foreach ($emu in $doc.LaunchBox.Emulator) {
$app = "$($emu.ApplicationPath)"
if (-not $app) { continue }
$exe = if ($app -match '^[A-Za-z]:' -or $app.StartsWith('\')) { $app } else { Join-Path $Root $app }
$dir = Split-Path $exe -Parent
$name = (Split-Path $exe -Leaf).ToLower()
if ((Test-Path $dir) -and -not $map.ContainsKey($name)) { $map[$name] = $dir }
}
return $map
}
# BIOS directories LaunchBox itself computes for the emulators it manages.
function Get-LaunchBoxBiosDirs {
$dirs = @{}
$root = Get-LaunchBoxRoot
if (-not $root) { return $dirs }
$emulators = Get-LaunchBoxEmulators -Root $root
$documents = Join-Path $env:USERPROFILE "Documents"
if ($emulators.ContainsKey("pcsx2.exe")) {
# portable.ini or portable.txt next to the executable moves the data
# root there, portable.txt naming a subfolder when it holds one
# (EmuFolders in pcsx2/Pcsx2Config.cpp). Otherwise it is Documents.
$emuDir = $emulators["pcsx2.exe"]
$portableTxt = Join-Path $emuDir "portable.txt"
$portable = (Test-Path (Join-Path $emuDir "portable.ini")) -or (Test-Path $portableTxt)
if ($portable) {
$subpath = if (Test-Path $portableTxt) { (Get-Content $portableTxt -Raw).Trim() } else { "" }
$dataRoot = if ($subpath) { Join-Path $emuDir $subpath } else { $emuDir }
} else {
$dataRoot = Join-Path $documents "PCSX2"
}
$ini = Join-Path $dataRoot "inis\PCSX2.ini"
$bios = Join-Path $dataRoot "bios"
if (Test-Path $ini) {
foreach ($line in Get-Content $ini) {
if ($line.StartsWith("Bios = ")) {
$val = $line.Substring(7).Trim()
$bios = if ($val -match '^[A-Za-z]:' -or $val.StartsWith('\')) { $val } else { Join-Path $dataRoot $val }
break
}
}
}
$dirs["pcsx2"] = $bios
}
if ($emulators.ContainsKey("xemu.exe")) {
# bootrom_path and flashrom_path name a file whose directory holds
# the images; both default to bios/ under the executable.
$emuDir = $emulators["xemu.exe"]
$toml = Join-Path $emuDir "xemu.toml"
if (-not (Test-Path $toml)) { $toml = Join-Path $env:APPDATA "xemu\xemu\xemu.toml" }
$bios = Join-Path $emuDir "bios"
if (Test-Path $toml) {
foreach ($line in Get-Content $toml) {
if ($line.StartsWith("bootrom_path") -or $line.StartsWith("flashrom_path")) {
$parts = $line.Split("'")
if ($parts.Count -gt 1 -and (Test-Path $parts[1])) {
$bios = Split-Path $parts[1] -Parent
break
}
}
}
}
$dirs["xemu"] = $bios
}
if ($emulators.ContainsKey("dolphin.exe")) {
# portable.txt beside the executable moves the user directory to User
# (SetUserDirectory in Source/Core/UICommon/UICommon.cpp)
$emuDir = $emulators["dolphin.exe"]
if (Test-Path (Join-Path $emuDir "portable.txt")) {
$dirs["dolphin"] = Join-Path $emuDir "User"
}
}
return $dirs
}
# Detect LaunchBox (portable RetroArch referenced in Data\Emulators.xml)
if (-not $platform) {
$lbRoot = Get-LaunchBoxRoot
$lbXml = if ($lbRoot) { Join-Path $lbRoot "Data\Emulators.xml" } else { $null }
if ($lbXml -and (Test-Path $lbXml)) {
$lb = $null
try { $lb = [xml](Get-Content $lbXml -Raw) } catch { Write-Host "Warning: could not parse $lbXml" }
if ($lb) {
foreach ($emu in $lb.LaunchBox.Emulator) {
$app = "$($emu.ApplicationPath)".Replace('\', '/')
if ($app -notmatch 'retroarch\.exe$') { continue }
$lbRoot = Split-Path (Split-Path $lbXml -Parent) -Parent
if ($app -match '^[A-Za-z]:' -or $app.StartsWith('/')) {
$exe = $app
} else {
$exe = Join-Path $lbRoot $app
}
$raDir = Split-Path $exe -Parent
if (Test-Path $raDir) {
$platform = "retroarch"
$found = Get-RetroArchSystemDir -CfgPath (Join-Path $raDir "retroarch.cfg") -AppDir $raDir
$biosPath = if ($found) { $found } else { Join-Path $raDir "system" }
Write-Host "Found LaunchBox with RetroArch at $biosPath"
break
}
break
}
}
Write-Host "Found RetroArch at $biosPath"
}
}
@@ -74,11 +231,6 @@ foreach ($f in $files) {
Write-Host " $upToDate/$($files.Count) up to date, $($toDownload.Count) to download"
if ($toDownload.Count -eq 0) {
Write-Host "`nDone. 0 downloaded, $upToDate already up to date."
exit 0
}
$downloaded = 0
$errors = 0
$total = $toDownload.Count
@@ -129,6 +281,7 @@ foreach ($f in $toDownload) {
# Standalone emulator copies
if ($manifest.standalone_copies) {
Write-Host "`nStandalone emulators:"
$extraDirs = Get-LaunchBoxBiosDirs
foreach ($entry in $manifest.standalone_copies) {
if ($entry.note) {
$detectPaths = @()
@@ -156,6 +309,12 @@ if ($manifest.standalone_copies) {
if ($entry.targets -and $entry.targets.windows) {
$targetDirs = $entry.targets.windows
}
if ($entry.emulator -and $extraDirs.ContainsKey($entry.emulator)) {
$extra = $extraDirs[$entry.emulator]
$subdir = if ($entry.file) { Split-Path $entry.file -Parent } else { "" }
if ($subdir) { $extra = Join-Path $extra $subdir }
$targetDirs += $extra
}
foreach ($td in $targetDirs) {
$expanded = [Environment]::ExpandEnvironmentVariables($td)
if (-not (Test-Path $expanded)) { continue }
@@ -167,6 +326,7 @@ if ($manifest.standalone_copies) {
} catch {
Write-Host " $($s.Name) -> $expanded FAILED" -ForegroundColor Red
}
}
}
}
+265 -10
View File
@@ -19,13 +19,15 @@ import hashlib
import json
import os
import platform
import re
import shutil
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
import xml.etree.ElementTree as ET
from pathlib import Path, PurePosixPath
BASE_URL = os.environ.get(
"RETROBIOS_BASE_URL",
@@ -98,10 +100,32 @@ def _parse_os_release() -> dict[str, str]:
return result
def _parse_retroarch_system_dir(cfg_path: Path) -> Path | None:
"""Parse system_directory from retroarch.cfg."""
def _expand_retroarch_path(value: str, app_dir: Path) -> Path:
"""Expand the notations RetroArch writes into its config values.
fill_pathname_expand_special (libretro-common/file/file_path.c) maps a
leading '~' to the home directory and a leading ':' to the application
directory, dropping the two leading characters in both cases.
"""
if value[:1] == "~":
return Path(str(Path.home())) / value[2:]
if value[:1] == ":":
return app_dir / value[2:]
return Path(os.path.expandvars(os.path.expanduser(value)))
def _parse_retroarch_system_dir(
cfg_path: Path, app_dir: Path | None = None
) -> Path | None:
"""Parse system_directory from retroarch.cfg.
app_dir is RetroArch's application directory, used to expand ':' values.
It defaults to the directory holding the config, which is where a portable
install keeps both.
"""
if not cfg_path.exists():
return None
app_dir = app_dir or cfg_path.parent
try:
for line in cfg_path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
@@ -109,9 +133,8 @@ def _parse_retroarch_system_dir(cfg_path: Path) -> Path | None:
_, _, value = line.partition("=")
value = value.strip().strip('"').strip("'")
if not value or value == "default":
return cfg_path.parent / "system"
value = os.path.expandvars(os.path.expanduser(value))
return Path(value)
return app_dir / "system"
return _expand_retroarch_path(value, app_dir)
except OSError:
pass
return None
@@ -251,6 +274,206 @@ def _detect_embedded() -> list[tuple[str, Path]]:
return found
def _lnk_target(lnk_path: Path, exe_name: str) -> Path | None:
"""Read the target path of a Windows shortcut.
Shortcuts embed the target as a plain local path; scanning for it avoids
parsing the whole .lnk structure for the one field that matters here.
"""
try:
if lnk_path.stat().st_size > 64 * 1024:
return None
blob = lnk_path.read_bytes()
except OSError:
return None
pattern = re.compile(
rb"[A-Za-z]:\\[ -~]{0,260}?" + re.escape(exe_name.encode()), re.IGNORECASE
)
match = pattern.search(blob)
if not match:
return None
return Path(match.group().decode("ascii", "replace").replace("\\", "/"))
def launchbox_root(os_type: str) -> Path | None:
"""Locate the LaunchBox installation directory.
LaunchBox installs wherever the user points its installer and writes no
uninstall registry key, so the Start menu shortcut is the only record of
that choice. Everything else falls back to the default location.
"""
if os_type not in ("windows", "wsl"):
return None
candidates: list[Path] = []
appdata = os.environ.get("APPDATA", "")
if appdata:
lnk = (
Path(appdata)
/ "Microsoft"
/ "Windows"
/ "Start Menu"
/ "Programs"
/ "LaunchBox"
/ "LaunchBox.lnk"
)
if lnk.exists():
target = _lnk_target(lnk, "LaunchBox.exe")
if target:
# The shortcut points at Core/LaunchBox.exe
candidates.append(target.parent.parent)
candidates.append(target.parent)
userprofile = os.environ.get("USERPROFILE", "")
if userprofile:
candidates.append(Path(userprofile) / "LaunchBox")
for root in candidates:
if (root / "Data" / "Emulators.xml").exists():
return root
return None
def launchbox_emulators(emulators_xml: Path) -> dict[str, Path]:
"""Map each emulator LaunchBox knows about to its installation directory.
LaunchBox stores its emulator list in Data/Emulators.xml; ApplicationPath
is either absolute or relative to the LaunchBox root directory. Keys are
the lowercase executable names.
"""
found: dict[str, Path] = {}
if not emulators_xml.exists():
return found
try:
if emulators_xml.stat().st_size > 10 * 1024 * 1024:
return found
raw = emulators_xml.read_text(encoding="utf-8", errors="replace")
# ElementTree expands internal entities; LaunchBox writes no doctype
if "<!DOCTYPE" in raw.upper():
return found
tree = ET.ElementTree(ET.fromstring(raw))
except (ET.ParseError, OSError):
return found
lb_root = emulators_xml.parent.parent
for emu in tree.getroot().iter("Emulator"):
app_path = (emu.findtext("ApplicationPath") or "").replace("\\", "/")
if not app_path:
continue
exe = Path(app_path)
if not exe.is_absolute():
exe = lb_root / exe
if exe.parent.is_dir():
found.setdefault(exe.name.lower(), exe.parent)
return found
def _launchbox_retroarch_system_dir(emulators_xml: Path) -> Path | None:
"""Resolve the system dir of a RetroArch referenced by LaunchBox.
Mirrors the plugin's UpdateSystemPath: read system_directory from the
retroarch.cfg sitting next to the executable, else assume system/.
"""
emu_dir = launchbox_emulators(emulators_xml).get("retroarch.exe")
if emu_dir is None:
return None
system_dir = _parse_retroarch_system_dir(emu_dir / "retroarch.cfg", emu_dir)
return system_dir or emu_dir / "system"
def _pcsx2_bios_dir(emu_dir: Path) -> Path:
"""Resolve the BIOS folder of a PCSX2 installed under LaunchBox.
EmuFolders (pcsx2/Pcsx2Config.cpp) enters portable mode when portable.ini
or portable.txt sits next to the executable, and then takes the data root
from the contents of portable.txt, relative to the executable. Otherwise
the data root is Documents/PCSX2. Bios defaults to "bios" below it and the
Bios key of PCSX2.ini overrides that.
"""
documents = Path(os.environ.get("USERPROFILE", str(Path.home()))) / "Documents"
portable_txt = emu_dir / "portable.txt"
portable = (emu_dir / "portable.ini").exists() or portable_txt.exists()
if portable:
subpath = ""
try:
subpath = portable_txt.read_text(
encoding="utf-8", errors="replace"
).strip()
except OSError:
pass
data_root = emu_dir / subpath if subpath else emu_dir
else:
data_root = documents / "PCSX2"
ini = data_root / "inis" / "PCSX2.ini"
try:
for line in ini.read_text(encoding="utf-8", errors="replace").splitlines():
if line.startswith("Bios = "):
value = line[len("Bios = "):].strip()
candidate = Path(value)
return candidate if candidate.is_absolute() else data_root / value
except OSError:
pass
return data_root / "bios"
def _xemu_bios_dir(emu_dir: Path) -> Path:
"""Resolve the BIOS folder of a xemu managed by LaunchBox.
Follows the plugin: xemu.toml next to the executable else the roaming
copy, with bootrom_path and flashrom_path naming a file whose directory
holds the images. Both default to bios/ under the executable.
"""
appdata = os.environ.get("APPDATA", "")
toml = emu_dir / "xemu.toml"
if not toml.exists() and appdata:
toml = Path(appdata) / "xemu" / "xemu" / "xemu.toml"
if toml.exists():
try:
for line in toml.read_text(encoding="utf-8", errors="replace").splitlines():
if line.startswith(("bootrom_path", "flashrom_path")):
parts = line.split("'")
if len(parts) > 1 and Path(parts[1]).exists():
return Path(parts[1]).parent
except OSError:
pass
return emu_dir / "bios"
def launchbox_bios_dirs(root: Path) -> dict[str, Path]:
"""Map emulator ids to the BIOS directory LaunchBox expects for them.
Only emulators whose destination LaunchBox itself computes are listed;
the rest keep the default locations declared in the manifest.
"""
emulators = launchbox_emulators(root / "Data" / "Emulators.xml")
dirs: dict[str, Path] = {}
emu_dir = emulators.get("pcsx2.exe")
if emu_dir:
dirs["pcsx2"] = _pcsx2_bios_dir(emu_dir)
emu_dir = emulators.get("xemu.exe")
if emu_dir:
dirs["xemu"] = _xemu_bios_dir(emu_dir)
emu_dir = emulators.get("dolphin.exe")
if emu_dir and (emu_dir / "portable.txt").exists():
# portable.txt beside the executable moves the user directory to
# User (SetUserDirectory in Source/Core/UICommon/UICommon.cpp,
# PORTABLE_USER_DIR in Source/Core/Common/CommonPaths.h)
dirs["dolphin"] = emu_dir / "User"
return dirs
def detect_frontends(os_type: str, home: Path | None = None) -> list[str]:
"""Detect frontends that reference emulators without owning a BIOS dir.
ES-DE resolves its application data directory to $ESDE_APPDATA_DIR or
<home>/ES-DE (es-core FileSystemUtil.cpp getAppDataDirectory).
"""
home = home or Path.home()
frontends: list[str] = []
esde_env = os.environ.get("ESDE_APPDATA_DIR", "")
if (esde_env and Path(esde_env).is_dir()) or (home / "ES-DE").is_dir():
frontends.append("esde")
if launchbox_root(os_type) is not None:
frontends.append("launchbox")
return frontends
def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
"""Detect installed emulator platforms and their BIOS directories."""
found: list[tuple[str, Path]] = []
@@ -321,6 +544,15 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
if ra_dir:
found.append(("retroarch", ra_dir))
# Portable RetroArch referenced by LaunchBox
lb_root = launchbox_root(os_type)
if lb_root and not any(name == "retroarch" for name, _ in found):
system_dir = _launchbox_retroarch_system_dir(
lb_root / "Data" / "Emulators.xml"
)
if system_dir:
found.append(("retroarch", system_dir))
return found
@@ -480,7 +712,8 @@ def download_files(
def do_standalone_copies(
manifest: dict, bios_path: Path, os_type: str
manifest: dict, bios_path: Path, os_type: str,
extra_dirs: dict[str, Path] | None = None,
) -> tuple[int, int]:
"""Copy BIOS files to standalone emulator directories.
@@ -490,6 +723,10 @@ def do_standalone_copies(
- note: informational message when detect path exists
- WSL fallback to linux targets
extra_dirs maps the emulator id of an entry to a root that replaces the
default per-OS locations, for setups such as LaunchBox that keep their
emulators outside them. The layout below the root is the same.
Returns (copied_count, skipped_count).
"""
from fnmatch import fnmatch
@@ -532,8 +769,17 @@ def do_standalone_copies(
if not targets and os_type == "wsl":
targets = entry.get("targets", {}).get("linux", [])
for target_dir_str in targets:
target_dir = Path(os.path.expandvars(os.path.expanduser(target_dir_str)))
target_dirs = [
Path(os.path.expandvars(os.path.expanduser(t))) for t in targets
]
extra_root = (extra_dirs or {}).get(entry.get("emulator", ""))
if extra_root is not None:
subdir = PurePosixPath(entry.get("file", "")).parent
extra = extra_root if str(subdir) == "." else extra_root / subdir
if extra not in target_dirs:
target_dirs.append(extra)
for target_dir in target_dirs:
if not target_dir.is_dir():
skipped += len(sources)
continue
@@ -675,6 +921,11 @@ def main() -> None:
platforms = [("retroarch", args.dest)]
else:
print("Detecting platform...")
frontends = detect_frontends(os_type)
if "esde" in frontends:
print(" Found ES-DE (BIOS files go to each emulator, not to ES-DE itself)")
if "launchbox" in frontends:
print(" Found LaunchBox")
platforms = detect_platforms(os_type)
if not platforms:
print(" No supported platform detected.")
@@ -755,7 +1006,11 @@ def main() -> None:
# Standalone copies
if manifest.get("standalone_copies") and not args.check:
print("\nStandalone emulators:")
copied, skipped = do_standalone_copies(manifest, bios_path, os_type)
lb_root = launchbox_root(os_type)
extra_dirs = launchbox_bios_dirs(lb_root) if lb_root else None
copied, skipped = do_standalone_copies(
manifest, bios_path, os_type, extra_dirs
)
if copied or skipped:
print(f" {copied} copied, {skipped} skipped (dir not found)")
+18 -3
View File
@@ -4,7 +4,7 @@
"platform": "batocera",
"display_name": "Batocera",
"version": "1.0",
"generated": "2026-08-07T16:30:30Z",
"generated": "2026-08-08T00:30:24Z",
"base_destination": "bios",
"detect": [
{
@@ -97,6 +97,7 @@
},
{
"pattern": "ps2-*.bin",
"emulator": "pcsx2",
"targets": {
"linux": [
"$HOME/.config/PCSX2/bios",
@@ -123,6 +124,7 @@
},
{
"file": "GC/USA/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/USA"
@@ -137,6 +139,7 @@
},
{
"file": "GC/EUR/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/EUR"
@@ -151,6 +154,7 @@
},
{
"file": "GC/JAP/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/JAP"
@@ -165,6 +169,7 @@
},
{
"file": "dsp_rom.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -179,6 +184,7 @@
},
{
"file": "dsp_coef.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -230,8 +236,8 @@
}
}
],
"total_files": 1600,
"total_size": 2868353323,
"total_files": 1601,
"total_size": 2869019853,
"files": [
{
"dest": "panafz1.bin",
@@ -9571,6 +9577,15 @@
"PokeMini"
]
},
{
"dest": "PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": [
"PPSSPP"
]
},
{
"dest": "prboom.wad",
"sha1": "5f4aed208301449c2e9514edfd325fe9dead76fa",
+12 -3
View File
@@ -4,7 +4,7 @@
"platform": "bizhawk",
"display_name": "BizHawk",
"version": "1.0",
"generated": "2026-08-07T16:15:44Z",
"generated": "2026-08-07T18:49:15Z",
"base_destination": "Firmware",
"detect": [
{
@@ -19,8 +19,8 @@
}
],
"standalone_copies": [],
"total_files": 538,
"total_size": 2548313700,
"total_files": 539,
"total_size": 2548980230,
"files": [
{
"dest": "panafz1.bin",
@@ -3242,6 +3242,15 @@
"PicoDrive"
]
},
{
"dest": "PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": [
"PPSSPP"
]
},
{
"dest": "prboom.wad",
"sha1": "5f4aed208301449c2e9514edfd325fe9dead76fa",
+18 -3
View File
@@ -4,7 +4,7 @@
"platform": "emudeck",
"display_name": "EmuDeck",
"version": "1.0",
"generated": "2026-08-06T18:09:38Z",
"generated": "2026-08-08T00:30:33Z",
"base_destination": "bios",
"detect": [
{
@@ -110,6 +110,7 @@
},
{
"pattern": "ps2-*.bin",
"emulator": "pcsx2",
"targets": {
"linux": [
"$HOME/.config/PCSX2/bios",
@@ -136,6 +137,7 @@
},
{
"file": "GC/USA/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/USA"
@@ -150,6 +152,7 @@
},
{
"file": "GC/EUR/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/EUR"
@@ -164,6 +167,7 @@
},
{
"file": "GC/JAP/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/JAP"
@@ -178,6 +182,7 @@
},
{
"file": "dsp_rom.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -192,6 +197,7 @@
},
{
"file": "dsp_coef.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -243,8 +249,8 @@
}
}
],
"total_files": 524,
"total_size": 1769405209,
"total_files": 525,
"total_size": 1770071739,
"files": [
{
"dest": "colecovision.rom",
@@ -3591,6 +3597,15 @@
"Mupen64Plus-Next"
]
},
{
"dest": "PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": [
"PPSSPP"
]
},
{
"dest": "PS3UPDAT.PUP",
"sha1": "093f8698b54b78dcb701de2043f82639de51d63b",
+10 -3
View File
@@ -4,7 +4,7 @@
"platform": "lakka",
"display_name": "Lakka",
"version": "1.0",
"generated": "2026-08-07T16:16:14Z",
"generated": "2026-08-07T18:49:47Z",
"base_destination": "system",
"detect": [
{
@@ -15,8 +15,8 @@
}
],
"standalone_copies": [],
"total_files": 1630,
"total_size": 4194971786,
"total_files": 1631,
"total_size": 4195638316,
"files": [
{
"dest": "3do_arcade_saot.bin",
@@ -3303,6 +3303,13 @@
"repo_path": "bios/Sony/PlayStation 2/.variants/GameIndex.yaml.57369df9",
"cores": null
},
{
"dest": "PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": null
},
{
"dest": "ti83se.rom",
"sha1": "8177bc6d5489d575cbfa9a004d097fc08c6f8c86",
+270 -237
View File
@@ -4,7 +4,7 @@
"platform": "recalbox",
"display_name": "Recalbox",
"version": "1.0",
"generated": "2026-08-07T16:16:21Z",
"generated": "2026-08-08T01:20:56Z",
"base_destination": "bios",
"detect": [
{
@@ -97,6 +97,7 @@
},
{
"pattern": "ps2-*.bin",
"emulator": "pcsx2",
"targets": {
"linux": [
"$HOME/.config/PCSX2/bios",
@@ -123,6 +124,7 @@
},
{
"file": "GC/USA/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/USA"
@@ -137,6 +139,7 @@
},
{
"file": "GC/EUR/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/EUR"
@@ -151,6 +154,7 @@
},
{
"file": "GC/JAP/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/JAP"
@@ -165,6 +169,7 @@
},
{
"file": "dsp_rom.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -179,6 +184,7 @@
},
{
"file": "dsp_coef.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -230,8 +236,8 @@
}
}
],
"total_files": 1181,
"total_size": 2485059730,
"total_files": 1184,
"total_size": 2494020078,
"files": [
{
"dest": "3do/panafz1.bin",
@@ -2641,6 +2647,240 @@
"repo_path": "bios/Microsoft/Xbox/mcpx_1.0.bin",
"cores": null
},
{
"dest": "hiscore.dat",
"sha1": "7381472bf046126257e51a0124e4553282f020e5",
"size": 322458,
"repo_path": "bios/Arcade/Arcade/hiscore.dat",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "cheat.dat",
"sha1": "32fc78415114a976af9c2ee53ea13de9f40b55ee",
"size": 7971360,
"repo_path": "bios/Arcade/MAME/cheat.dat",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "pgm.zip",
"sha1": "c0c001ec80fa860857000f4cfc9844a28498a355",
"size": 2094636,
"repo_path": "bios/Arcade/MAME/pgm.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "megaplay.zip",
"sha1": "c9ee16e26e03496195a7bff151efbdd89da01204",
"size": 49978,
"repo_path": "bios/Arcade/Arcade/megaplay.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "megatech.zip",
"sha1": "be1bcca32794d0eb36c86ae32a2a43c92834cdc6",
"size": 24831,
"repo_path": "bios/Arcade/Arcade/megatech.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "decocass.zip",
"sha1": "1b33cf0e730a6cde96479b0face108b6931d433e",
"size": 22379,
"repo_path": "bios/Arcade/Arcade/decocass.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "playch10.zip",
"sha1": "125e74975da3f7ac643232210ab37c33e4ece9ca",
"size": 23388,
"repo_path": "bios/Arcade/Arcade/playch10.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "nss.zip",
"sha1": "68b780f40623e51a421faf374ae5a84a53a81279",
"size": 50536,
"repo_path": "bios/Arcade/Arcade/nss.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "skns.zip",
"sha1": "4257bd14b541fafbd555cb98ba079a3416a45934",
"size": 924762,
"repo_path": "bios/Arcade/Arcade/skns.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "konamigx.zip",
"sha1": "8cf0aa7f9dca4d77485e605fb0e2173a734633bf",
"size": 8712,
"repo_path": "bios/Arcade/Arcade/konamigx.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "konamigv.zip",
"sha1": "b4ff14ed8c3e37a4b45ce2d80c692961ffc88270",
"size": 81391,
"repo_path": "bios/Arcade/Arcade/konamigv.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "cvs.zip",
"sha1": "649d8af20ba0048fa993585960bf0305d50fc560",
"size": 3113,
"repo_path": "bios/Arcade/Arcade/cvs.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "cpzn1.zip",
"sha1": "6515a4f2f3f5864c32346c2fecdc3595205f1d86",
"size": 125571,
"repo_path": "bios/Arcade/Arcade/cpzn1.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "cpzn2.zip",
"sha1": "b18e23b8908e3e920ce971b8be168fa8baf1b3f7",
"size": 137827,
"repo_path": "bios/Arcade/Arcade/cpzn2.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "tps.zip",
"sha1": "f18270ba6e747de86bc7bd4f0f2c9c6f35335871",
"size": 138474,
"repo_path": "bios/Arcade/Arcade/tps.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "taitofx1.zip",
"sha1": "2975c1ac34a18f6d262c5cc7dcc223f096742075",
"size": 126160,
"repo_path": "bios/Arcade/Arcade/taitofx1.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "psarc95.zip",
"sha1": "3c1a40887171d71915487cdb630d2b7fee2417cc",
"size": 128759,
"repo_path": "bios/Arcade/Arcade/psarc95.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "atluspsx.zip",
"sha1": "a6ff77ddecbd0888c2323a51199cadd3202e759a",
"size": 126346,
"repo_path": "bios/Arcade/Arcade/atluspsx.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "atpsx.zip",
"sha1": "86ff54e5d15d754768fe7fd1ac6871fe1b49dba5",
"size": 126298,
"repo_path": "bios/Arcade/Arcade/atpsx.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "acpsx.zip",
"sha1": "5426d52e17e0ff9195fabbb42f704342e556d08e",
"size": 126167,
"repo_path": "bios/Arcade/Arcade/acpsx.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "hng64.zip",
"sha1": "98b92ece93d3e160c446a56ac859a8311333e36c",
"size": 41210,
"repo_path": "bios/Arcade/Arcade/hng64.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "ar_bios.zip",
"sha1": "df93e5943023cc8f7ddf0ffb956fd969e04a171a",
"size": 681333,
"repo_path": "bios/Arcade/Arcade/ar_bios.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "maxaflex.zip",
"sha1": "697551fcf9557ae33e31096b118a0c6769700a2e",
"size": 13673,
"repo_path": "bios/Arcade/Arcade/maxaflex.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "crysbios.zip",
"sha1": "7f2d1c4e00ce1824ff91459d8ff6b59b2fb4b746",
"size": 43480,
"repo_path": "bios/Arcade/Arcade/crysbios.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "macsbios.zip",
"sha1": "4e0202f8430cb4842184df7b5418e32620156c7b",
"size": 305290,
"repo_path": "bios/Arcade/Arcade/macsbios.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "bctvidbs.zip",
"sha1": "5024b3bfd04ccd2061eb60d2eca254e8faf44f5c",
"size": 3753,
"repo_path": "bios/Arcade/Arcade/bctvidbs.zip",
"cores": [
"AdvanceMAME"
]
},
{
"dest": "kick13.rom",
"sha1": "90933936cce43ca9bc6bf375662c076b27e3c458",
@@ -4472,15 +4712,6 @@
"ECWolf"
]
},
{
"dest": "fbneo/hiscore.dat",
"sha1": "7381472bf046126257e51a0124e4553282f020e5",
"size": 322458,
"repo_path": "bios/Arcade/Arcade/hiscore.dat",
"cores": [
"FinalBurn Neo"
]
},
{
"dest": "fbneo/samples/donpachi.zip",
"sha1": "d380fb29287eb7fc9ff901a7653ad40785f7deb1",
@@ -5009,15 +5240,6 @@
"Hatari"
]
},
{
"dest": "pgm.zip",
"sha1": "c0c001ec80fa860857000f4cfc9844a28498a355",
"size": 2094636,
"repo_path": "bios/Arcade/MAME/pgm.zip",
"cores": [
"MAME"
]
},
{
"dest": "lindbios.zip",
"sha1": "65a2f2cee74c316d5f40b68deda66787609df353",
@@ -5081,24 +5303,6 @@
"MAME"
]
},
{
"dest": "megaplay.zip",
"sha1": "c9ee16e26e03496195a7bff151efbdd89da01204",
"size": 49978,
"repo_path": "bios/Arcade/Arcade/megaplay.zip",
"cores": [
"MAME"
]
},
{
"dest": "megatech.zip",
"sha1": "be1bcca32794d0eb36c86ae32a2a43c92834cdc6",
"size": 24831,
"repo_path": "bios/Arcade/Arcade/megatech.zip",
"cores": [
"MAME"
]
},
{
"dest": "isgsm.zip",
"sha1": "f590ccf688b4c05fa1da5c5dd92c224545170c3b",
@@ -5117,33 +5321,6 @@
"MAME"
]
},
{
"dest": "decocass.zip",
"sha1": "1b33cf0e730a6cde96479b0face108b6931d433e",
"size": 22379,
"repo_path": "bios/Arcade/Arcade/decocass.zip",
"cores": [
"MAME"
]
},
{
"dest": "playch10.zip",
"sha1": "125e74975da3f7ac643232210ab37c33e4ece9ca",
"size": 23388,
"repo_path": "bios/Arcade/Arcade/playch10.zip",
"cores": [
"MAME"
]
},
{
"dest": "nss.zip",
"sha1": "68b780f40623e51a421faf374ae5a84a53a81279",
"size": 50536,
"repo_path": "bios/Arcade/Arcade/nss.zip",
"cores": [
"MAME"
]
},
{
"dest": "sfcbox.zip",
"sha1": "14311945b06a5d013d1df5ce961ab47fbd46fb2a",
@@ -5198,24 +5375,6 @@
"MAME"
]
},
{
"dest": "konamigx.zip",
"sha1": "8cf0aa7f9dca4d77485e605fb0e2173a734633bf",
"size": 8712,
"repo_path": "bios/Arcade/Arcade/konamigx.zip",
"cores": [
"MAME"
]
},
{
"dest": "konamigv.zip",
"sha1": "b4ff14ed8c3e37a4b45ce2d80c692961ffc88270",
"size": 81391,
"repo_path": "bios/Arcade/Arcade/konamigv.zip",
"cores": [
"MAME"
]
},
{
"dest": "kviper.zip",
"sha1": "78c8e1c3c033b65758b7e53a9346b44d037fea7f",
@@ -5378,15 +5537,6 @@
"MAME"
]
},
{
"dest": "skns.zip",
"sha1": "4257bd14b541fafbd555cb98ba079a3416a45934",
"size": 924762,
"repo_path": "bios/Arcade/Arcade/skns.zip",
"cores": [
"MAME"
]
},
{
"dest": "mac2bios.zip",
"sha1": "829b2e10aff75a0857feb2b601f893db29877994",
@@ -5396,15 +5546,6 @@
"MAME"
]
},
{
"dest": "macsbios.zip",
"sha1": "4e0202f8430cb4842184df7b5418e32620156c7b",
"size": 305290,
"repo_path": "bios/Arcade/Arcade/macsbios.zip",
"cores": [
"MAME"
]
},
{
"dest": "3dobios.zip",
"sha1": "de463b0577dfd1027bf7de523ff67a0fff861cdb",
@@ -5423,24 +5564,6 @@
"MAME"
]
},
{
"dest": "hng64.zip",
"sha1": "98b92ece93d3e160c446a56ac859a8311333e36c",
"size": 41210,
"repo_path": "bios/Arcade/Arcade/hng64.zip",
"cores": [
"MAME"
]
},
{
"dest": "ar_bios.zip",
"sha1": "df93e5943023cc8f7ddf0ffb956fd969e04a171a",
"size": 681333,
"repo_path": "bios/Arcade/Arcade/ar_bios.zip",
"cores": [
"MAME"
]
},
{
"dest": "alg_bios.zip",
"sha1": "901e1927ebe87946e3e4bb6d0a825088a8a60490",
@@ -5459,24 +5582,6 @@
"MAME"
]
},
{
"dest": "maxaflex.zip",
"sha1": "697551fcf9557ae33e31096b118a0c6769700a2e",
"size": 13673,
"repo_path": "bios/Arcade/Arcade/maxaflex.zip",
"cores": [
"MAME"
]
},
{
"dest": "crysbios.zip",
"sha1": "7f2d1c4e00ce1824ff91459d8ff6b59b2fb4b746",
"size": 43480,
"repo_path": "bios/Arcade/Arcade/crysbios.zip",
"cores": [
"MAME"
]
},
{
"dest": "galgbios.zip",
"sha1": "9ec50f79c5fd6eaddd88542aa18b6c5bb81a9ed4",
@@ -6368,15 +6473,6 @@
"MAME"
]
},
{
"dest": "mame2000/cheat/cheat.dat",
"sha1": "32fc78415114a976af9c2ee53ea13de9f40b55ee",
"size": 7971360,
"repo_path": "bios/Arcade/MAME/cheat.dat",
"cores": [
"MAME 2000"
]
},
{
"dest": "history.dat",
"sha1": "42d6d9ac933a7819adfd686f629a8e43aea4e880",
@@ -6395,51 +6491,6 @@
"MAME 2000"
]
},
{
"dest": "cvs.zip",
"sha1": "649d8af20ba0048fa993585960bf0305d50fc560",
"size": 3113,
"repo_path": "bios/Arcade/Arcade/cvs.zip",
"cores": [
"MAME 2003"
]
},
{
"dest": "cpzn1.zip",
"sha1": "6515a4f2f3f5864c32346c2fecdc3595205f1d86",
"size": 125571,
"repo_path": "bios/Arcade/Arcade/cpzn1.zip",
"cores": [
"MAME 2003"
]
},
{
"dest": "cpzn2.zip",
"sha1": "b18e23b8908e3e920ce971b8be168fa8baf1b3f7",
"size": 137827,
"repo_path": "bios/Arcade/Arcade/cpzn2.zip",
"cores": [
"MAME 2003"
]
},
{
"dest": "taitofx1.zip",
"sha1": "2975c1ac34a18f6d262c5cc7dcc223f096742075",
"size": 126160,
"repo_path": "bios/Arcade/Arcade/taitofx1.zip",
"cores": [
"MAME 2003"
]
},
{
"dest": "tps.zip",
"sha1": "f18270ba6e747de86bc7bd4f0f2c9c6f35335871",
"size": 138474,
"repo_path": "bios/Arcade/Arcade/tps.zip",
"cores": [
"MAME 2003"
]
},
{
"dest": "sncwgltd.zip",
"sha1": "9d0104d44d0d1cb9bbe801d21f2fddb834efa758",
@@ -6449,15 +6500,6 @@
"MAME 2003"
]
},
{
"dest": "acpsx.zip",
"sha1": "5426d52e17e0ff9195fabbb42f704342e556d08e",
"size": 126167,
"repo_path": "bios/Arcade/Arcade/acpsx.zip",
"cores": [
"MAME 2003"
]
},
{
"dest": "mame2010/cheat.zip",
"sha1": "a737e925e3b86374da57496e3261d0303889977f",
@@ -6494,33 +6536,6 @@
"MAME 2010"
]
},
{
"dest": "atluspsx.zip",
"sha1": "a6ff77ddecbd0888c2323a51199cadd3202e759a",
"size": 126346,
"repo_path": "bios/Arcade/Arcade/atluspsx.zip",
"cores": [
"MAME 2010"
]
},
{
"dest": "atpsx.zip",
"sha1": "86ff54e5d15d754768fe7fd1ac6871fe1b49dba5",
"size": 126298,
"repo_path": "bios/Arcade/Arcade/atpsx.zip",
"cores": [
"MAME 2010"
]
},
{
"dest": "psarc95.zip",
"sha1": "3c1a40887171d71915487cdb630d2b7fee2417cc",
"size": 128759,
"repo_path": "bios/Arcade/Arcade/psarc95.zip",
"cores": [
"MAME 2010"
]
},
{
"dest": "vspsx.zip",
"sha1": "241fa39e4b321b4b7b8fc8f82b411df8a206fec5",
@@ -6530,15 +6545,6 @@
"MAME 2010"
]
},
{
"dest": "bctvidbs.zip",
"sha1": "5024b3bfd04ccd2061eb60d2eca254e8faf44f5c",
"size": 3753,
"repo_path": "bios/Arcade/Arcade/bctvidbs.zip",
"cores": [
"MAME 2010"
]
},
{
"dest": "mk6nsw11.zip",
"sha1": "b62c501e3858d6a1db6f8450ed96a6b37b1b5d58",
@@ -7340,6 +7346,15 @@
"PicoDrive"
]
},
{
"dest": "PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": [
"PPSSPP"
]
},
{
"dest": "prboom.wad",
"sha1": "5f4aed208301449c2e9514edfd325fe9dead76fa",
@@ -8411,6 +8426,15 @@
"Dolphin"
]
},
{
"dest": "fbneo/hiscore.dat",
"sha1": "7381472bf046126257e51a0124e4553282f020e5",
"size": 322458,
"repo_path": "bios/Arcade/Arcade/hiscore.dat",
"cores": [
"FinalBurn Neo"
]
},
{
"dest": "kronos/saturn_bios.bin",
"sha1": "2b8cb4f87580683eb4d760e4ed210813d667f0a2",
@@ -8420,6 +8444,15 @@
"Kronos"
]
},
{
"dest": "mame2000/cheat/cheat.dat",
"sha1": "32fc78415114a976af9c2ee53ea13de9f40b55ee",
"size": 7971360,
"repo_path": "bios/Arcade/MAME/cheat.dat",
"cores": [
"MAME 2000"
]
},
{
"dest": "mame2003/hiscore.dat",
"sha1": "7381472bf046126257e51a0124e4553282f020e5",
+16 -3
View File
@@ -4,7 +4,7 @@
"platform": "retroarch",
"display_name": "RetroArch",
"version": "1.0",
"generated": "2026-08-07T16:16:14Z",
"generated": "2026-08-08T00:30:59Z",
"base_destination": "system",
"detect": [
{
@@ -115,6 +115,7 @@
},
{
"pattern": "ps2-*.bin",
"emulator": "pcsx2",
"targets": {
"linux": [
"$HOME/.config/PCSX2/bios",
@@ -141,6 +142,7 @@
},
{
"file": "GC/USA/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/USA"
@@ -155,6 +157,7 @@
},
{
"file": "GC/EUR/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/EUR"
@@ -169,6 +172,7 @@
},
{
"file": "GC/JAP/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/JAP"
@@ -183,6 +187,7 @@
},
{
"file": "dsp_rom.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -197,6 +202,7 @@
},
{
"file": "dsp_coef.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -248,8 +254,8 @@
}
}
],
"total_files": 1630,
"total_size": 4194971786,
"total_files": 1631,
"total_size": 4195638316,
"files": [
{
"dest": "3do_arcade_saot.bin",
@@ -3536,6 +3542,13 @@
"repo_path": "bios/Sony/PlayStation 2/.variants/GameIndex.yaml.57369df9",
"cores": null
},
{
"dest": "PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": null
},
{
"dest": "ti83se.rom",
"sha1": "8177bc6d5489d575cbfa9a004d097fc08c6f8c86",
+18 -3
View File
@@ -4,7 +4,7 @@
"platform": "retrobat",
"display_name": "RetroBat",
"version": "1.0",
"generated": "2026-08-07T12:30:14Z",
"generated": "2026-08-08T00:31:14Z",
"base_destination": "bios",
"detect": [
{
@@ -96,6 +96,7 @@
},
{
"pattern": "ps2-*.bin",
"emulator": "pcsx2",
"targets": {
"linux": [
"$HOME/.config/PCSX2/bios",
@@ -122,6 +123,7 @@
},
{
"file": "GC/USA/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/USA"
@@ -136,6 +138,7 @@
},
{
"file": "GC/EUR/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/EUR"
@@ -150,6 +153,7 @@
},
{
"file": "GC/JAP/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/JAP"
@@ -164,6 +168,7 @@
},
{
"file": "dsp_rom.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -178,6 +183,7 @@
},
{
"file": "dsp_coef.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -229,8 +235,8 @@
}
}
],
"total_files": 1234,
"total_size": 3283257651,
"total_files": 1235,
"total_size": 3283924181,
"files": [
{
"dest": "panafz1.bin",
@@ -7236,6 +7242,15 @@
"PicoDrive"
]
},
{
"dest": "PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": [
"PPSSPP"
]
},
{
"dest": "prboom.wad",
"sha1": "5f4aed208301449c2e9514edfd325fe9dead76fa",
+25 -3
View File
@@ -4,7 +4,7 @@
"platform": "retrodeck",
"display_name": "RetroDECK",
"version": "1.0",
"generated": "2026-08-07T16:16:39Z",
"generated": "2026-08-08T00:31:25Z",
"base_destination": "",
"detect": [
{
@@ -97,6 +97,7 @@
},
{
"pattern": "ps2-*.bin",
"emulator": "pcsx2",
"targets": {
"linux": [
"$HOME/.config/PCSX2/bios",
@@ -123,6 +124,7 @@
},
{
"file": "GC/USA/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/USA"
@@ -137,6 +139,7 @@
},
{
"file": "GC/EUR/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/EUR"
@@ -151,6 +154,7 @@
},
{
"file": "GC/JAP/IPL.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu/GC/JAP"
@@ -165,6 +169,7 @@
},
{
"file": "dsp_rom.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -179,6 +184,7 @@
},
{
"file": "dsp_coef.bin",
"emulator": "dolphin",
"targets": {
"linux": [
"$HOME/.local/share/dolphin-emu"
@@ -230,8 +236,8 @@
}
}
],
"total_files": 3264,
"total_size": 4967595069,
"total_files": 3266,
"total_size": 4968928129,
"files": [
{
"dest": "bios/panafz1.bin",
@@ -14011,6 +14017,13 @@
"repo_path": "bios/Sony/PlayStation 2/ps2-0190r-20030623.bin",
"cores": null
},
{
"dest": "bios/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": null
},
{
"dest": "bios/supracan.zip",
"sha1": "1af645230c3f1741fb1f7efef05e9efe768e9dcc",
@@ -24173,6 +24186,15 @@
"NP2kai"
]
},
{
"dest": "bios/PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": [
"PPSSPP"
]
},
{
"dest": "bios/keropi/iplrom.dat",
"sha1": "0ed038ed2133b9f78c6e37256807424e0d927560",
+12 -3
View File
@@ -4,7 +4,7 @@
"platform": "rocknix",
"display_name": "ROCKNIX",
"version": "1.0",
"generated": "2026-08-07T16:58:58Z",
"generated": "2026-08-07T18:50:23Z",
"base_destination": "bios",
"detect": [
{
@@ -16,8 +16,8 @@
}
],
"standalone_copies": [],
"total_files": 1495,
"total_size": 4058321299,
"total_files": 1496,
"total_size": 4058987829,
"files": [
{
"dest": "xemu/bios/mcpx_1.0.bin",
@@ -8589,6 +8589,15 @@
"PokeMini"
]
},
{
"dest": "PPSSPP/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": [
"PPSSPP"
]
},
{
"dest": "prboom.wad",
"sha1": "5f4aed208301449c2e9514edfd325fe9dead76fa",
+10 -3
View File
@@ -4,7 +4,7 @@
"platform": "romm",
"display_name": "RomM",
"version": "1.0",
"generated": "2026-08-06T17:49:51Z",
"generated": "2026-08-07T18:50:25Z",
"base_destination": "bios",
"detect": [
{
@@ -14,8 +14,8 @@
}
],
"standalone_copies": [],
"total_files": 542,
"total_size": 1486691402,
"total_files": 543,
"total_size": 1487357932,
"files": [
{
"dest": "3do/3do_arcade_saot.bin",
@@ -2593,6 +2593,13 @@
"repo_path": "bios/Sony/PlayStation 2/ps2-0250j-20100415.bin",
"cores": null
},
{
"dest": "psp/ppge_atlas.zim",
"sha1": "30a33f3651f8e1b0967a5dfe6ff714baebcce41a",
"size": 666530,
"repo_path": "bios/Sony/PlayStation Portable/ppge_atlas.zim",
"cores": null
},
{
"dest": "tvc/tvcfileio.rom",
"sha1": "98889c3a56b11dedf077f866ed2e12d51b604113",
+152 -828
View File
File diff suppressed because it is too large. Load diff
+509
View File
@@ -1,12 +1,18 @@
"""Tests for install.py platform detection and config parsing."""
from __future__ import annotations
import functools
import http.server
import importlib.util
import json
import os
import re
import shutil
import subprocess
import tempfile
import threading
import unittest
import unittest.mock
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
@@ -207,6 +213,509 @@ class TestEmbeddedDetection(unittest.TestCase):
self.assertEqual(self._detect_with(set()), [])
class TestLaunchboxDetection(unittest.TestCase):
"""LaunchBox references its emulators in Data/Emulators.xml."""
def _make_launchbox(self, app_path: str, title: str = "Retroarch"):
root = Path(tempfile.mkdtemp())
data = root / "LaunchBox" / "Data"
data.mkdir(parents=True)
xml = data / "Emulators.xml"
xml.write_text(
"<?xml version=\"1.0\" standalone=\"yes\"?>\n"
"<LaunchBox>\n"
" <Emulator>\n"
" <ID>x</ID>\n"
f" <Title>{title}</Title>\n"
f" <ApplicationPath>{app_path}</ApplicationPath>\n"
" </Emulator>\n"
"</LaunchBox>\n"
)
return root, xml
def test_relative_application_path(self):
root, xml = self._make_launchbox("Emulators\\RetroArch\\retroarch.exe")
ra_dir = root / "LaunchBox" / "Emulators" / "RetroArch"
ra_dir.mkdir(parents=True)
self.assertEqual(
install._launchbox_retroarch_system_dir(xml), ra_dir / "system"
)
def test_absolute_application_path(self):
emu_dir = Path(tempfile.mkdtemp()) / "RetroArch"
emu_dir.mkdir(parents=True)
root, xml = self._make_launchbox(str(emu_dir / "retroarch.exe"))
self.assertEqual(
install._launchbox_retroarch_system_dir(xml), emu_dir / "system"
)
def test_no_retroarch_entry(self):
root, xml = self._make_launchbox("Emulators\\PCSX2\\pcsx2.exe", "PCSX2")
self.assertIsNone(install._launchbox_retroarch_system_dir(xml))
def test_missing_emulator_directory(self):
root, xml = self._make_launchbox("Emulators\\RetroArch\\retroarch.exe")
self.assertIsNone(install._launchbox_retroarch_system_dir(xml))
def test_stale_entry_before_installed_one(self):
root = Path(tempfile.mkdtemp())
data = root / "LaunchBox" / "Data"
data.mkdir(parents=True)
ra_dir = root / "LaunchBox" / "Emulators" / "RetroArch"
ra_dir.mkdir(parents=True)
xml = data / "Emulators.xml"
xml.write_text(
"<?xml version=\"1.0\" standalone=\"yes\"?>\n"
"<LaunchBox>\n"
" <Emulator>\n"
" <Title>RetroArch (old)</Title>\n"
" <ApplicationPath>Emulators\\RetroArch-old\\retroarch.exe</ApplicationPath>\n"
" </Emulator>\n"
" <Emulator>\n"
" <Title>RetroArch</Title>\n"
" <ApplicationPath>Emulators\\RetroArch\\retroarch.exe</ApplicationPath>\n"
" </Emulator>\n"
"</LaunchBox>\n"
)
self.assertEqual(
install._launchbox_retroarch_system_dir(xml), ra_dir / "system"
)
def test_missing_or_malformed_xml(self):
self.assertIsNone(
install._launchbox_retroarch_system_dir(Path("/nonexistent/Emulators.xml"))
)
root = Path(tempfile.mkdtemp())
bad = root / "Emulators.xml"
bad.write_text("not xml at all <<<")
self.assertIsNone(install._launchbox_retroarch_system_dir(bad))
class TestRetroarchPathExpansion(unittest.TestCase):
"""RetroArch abbreviates paths with '~' and ':' in its config."""
def _cfg(self, value: str) -> Path:
app_dir = Path(tempfile.mkdtemp())
(app_dir / "retroarch.cfg").write_text(f'system_directory = "{value}"\n')
return app_dir
def test_application_dir_prefix(self):
app_dir = self._cfg(":\\system")
self.assertEqual(
install._parse_retroarch_system_dir(app_dir / "retroarch.cfg"),
app_dir / "system",
)
def test_application_dir_prefix_forward_slash(self):
app_dir = self._cfg(":/system")
self.assertEqual(
install._parse_retroarch_system_dir(app_dir / "retroarch.cfg"),
app_dir / "system",
)
def test_home_prefix(self):
app_dir = self._cfg("~/bios")
self.assertEqual(
install._parse_retroarch_system_dir(app_dir / "retroarch.cfg"),
Path.home() / "bios",
)
def test_application_dir_overrides_config_dir(self):
app_dir = self._cfg(":\\system")
elsewhere = Path(tempfile.mkdtemp())
self.assertEqual(
install._parse_retroarch_system_dir(
app_dir / "retroarch.cfg", elsewhere
),
elsewhere / "system",
)
def test_absolute_path_untouched(self):
target = Path(tempfile.mkdtemp()) / "bios"
app_dir = self._cfg(str(target))
self.assertEqual(
install._parse_retroarch_system_dir(app_dir / "retroarch.cfg"), target
)
class TestLaunchboxRoot(unittest.TestCase):
"""LaunchBox installs where the user chooses and keeps no registry key."""
def _make_root(self, root: Path) -> Path:
(root / "Data").mkdir(parents=True)
(root / "Data" / "Emulators.xml").write_text(
"<?xml version=\"1.0\" standalone=\"yes\"?>\n<LaunchBox></LaunchBox>\n"
)
return root
def test_shortcut_target_is_read(self):
# Byte layout of the shortcut a real install writes
lnk = Path(tempfile.mkdtemp()) / "LaunchBox.lnk"
lnk.write_bytes(
b"L\x00\x00\x00\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00"
b"\x00\x00\x00\x46" + b"\x00" * 8
+ b"C:\\Games\\LaunchBox\\Core\\LaunchBox.exe\x00"
b"\x1c\x00\x00\x00LaunchBox\x00"
)
self.assertEqual(
install._lnk_target(lnk, "LaunchBox.exe"),
Path("C:/Games/LaunchBox/Core/LaunchBox.exe"),
)
def test_shortcut_without_target_is_ignored(self):
lnk = Path(tempfile.mkdtemp()) / "LaunchBox.lnk"
lnk.write_bytes(b"L\x00\x00\x00 no target here ")
self.assertIsNone(install._lnk_target(lnk, "LaunchBox.exe"))
def test_shortcut_pointing_elsewhere_falls_back(self):
# A stale shortcut must not win over a real installation
userprofile = Path(tempfile.mkdtemp())
root = self._make_root(userprofile / "LaunchBox")
appdata = Path(tempfile.mkdtemp())
lnk_dir = (
appdata / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "LaunchBox"
)
lnk_dir.mkdir(parents=True)
(lnk_dir / "LaunchBox.lnk").write_bytes(
b"L\x00\x00\x00D:\\Gone\\LaunchBox\\Core\\LaunchBox.exe\x00"
)
env = {"APPDATA": str(appdata), "USERPROFILE": str(userprofile)}
with unittest.mock.patch.dict(os.environ, env):
self.assertEqual(install.launchbox_root("windows"), root)
def test_falls_back_to_userprofile(self):
userprofile = Path(tempfile.mkdtemp())
root = self._make_root(userprofile / "LaunchBox")
env = {"APPDATA": str(Path(tempfile.mkdtemp())), "USERPROFILE": str(userprofile)}
with unittest.mock.patch.dict(os.environ, env):
self.assertEqual(install.launchbox_root("windows"), root)
def test_absent_on_linux(self):
self.assertIsNone(install.launchbox_root("linux"))
def test_nothing_installed(self):
env = {
"APPDATA": str(Path(tempfile.mkdtemp())),
"USERPROFILE": str(Path(tempfile.mkdtemp())),
}
with unittest.mock.patch.dict(os.environ, env):
self.assertIsNone(install.launchbox_root("windows"))
class TestLaunchboxBiosDirs(unittest.TestCase):
"""LaunchBox computes where each emulator it manages keeps its BIOS."""
def _make(self, entries: list[tuple[str, str]]) -> Path:
root = Path(tempfile.mkdtemp()) / "LaunchBox"
(root / "Data").mkdir(parents=True)
body = "".join(
" <Emulator>\n"
f" <Title>{title}</Title>\n"
f" <ApplicationPath>{app}</ApplicationPath>\n"
" </Emulator>\n"
for title, app in entries
)
(root / "Data" / "Emulators.xml").write_text(
"<?xml version=\"1.0\" standalone=\"yes\"?>\n"
f"<LaunchBox>\n{body}</LaunchBox>\n"
)
for _, app in entries:
(root / Path(app.replace("\\", "/")).parent).mkdir(parents=True, exist_ok=True)
return root
def test_pcsx2_portable_default_bios(self):
root = self._make([("PCSX2", "Emulators\\PCSX2\\pcsx2.exe")])
emu = root / "Emulators" / "PCSX2"
(emu / "portable.ini").write_text("")
(emu / "inis").mkdir()
(emu / "inis" / "PCSX2.ini").write_text("[Folders]\nBios = bios\n")
self.assertEqual(install.launchbox_bios_dirs(root)["pcsx2"], emu / "bios")
def test_pcsx2_portable_absolute_bios(self):
root = self._make([("PCSX2", "Emulators\\PCSX2\\pcsx2.exe")])
emu = root / "Emulators" / "PCSX2"
elsewhere = Path(tempfile.mkdtemp()) / "ps2bios"
(emu / "portable.ini").write_text("")
(emu / "inis").mkdir()
(emu / "inis" / "PCSX2.ini").write_text(f"Bios = {elsewhere}\n")
self.assertEqual(install.launchbox_bios_dirs(root)["pcsx2"], elsewhere)
def test_pcsx2_portable_txt_also_triggers_portable_mode(self):
root = self._make([("PCSX2", "Emulators\\PCSX2\\pcsx2.exe")])
emu = root / "Emulators" / "PCSX2"
(emu / "portable.txt").write_text("")
self.assertEqual(install.launchbox_bios_dirs(root)["pcsx2"], emu / "bios")
def test_pcsx2_portable_txt_names_a_data_root(self):
root = self._make([("PCSX2", "Emulators\\PCSX2\\pcsx2.exe")])
emu = root / "Emulators" / "PCSX2"
(emu / "portable.txt").write_text("mydata\n")
self.assertEqual(
install.launchbox_bios_dirs(root)["pcsx2"], emu / "mydata" / "bios"
)
def test_pcsx2_without_config_uses_documents(self):
root = self._make([("PCSX2", "Emulators\\PCSX2\\pcsx2.exe")])
userprofile = Path(tempfile.mkdtemp())
with unittest.mock.patch.dict(os.environ, {"USERPROFILE": str(userprofile)}):
self.assertEqual(
install.launchbox_bios_dirs(root)["pcsx2"],
userprofile / "Documents" / "PCSX2" / "bios",
)
def test_xemu_defaults_to_bios_folder(self):
root = self._make([("Xemu", "Emulators\\Xemu\\xemu.exe")])
emu = root / "Emulators" / "Xemu"
with unittest.mock.patch.dict(os.environ, {"APPDATA": str(Path(tempfile.mkdtemp()))}):
self.assertEqual(install.launchbox_bios_dirs(root)["xemu"], emu / "bios")
def test_xemu_honours_configured_bootrom(self):
root = self._make([("Xemu", "Emulators\\Xemu\\xemu.exe")])
emu = root / "Emulators" / "Xemu"
configured = Path(tempfile.mkdtemp()) / "xbox"
configured.mkdir()
(configured / "mcpx_1.0.bin").write_text("x")
(emu / "xemu.toml").write_text(
"[sys.files]\n"
f"bootrom_path = '{configured / 'mcpx_1.0.bin'}'\n"
)
self.assertEqual(install.launchbox_bios_dirs(root)["xemu"], configured)
def test_dolphin_portable_user_folder(self):
root = self._make([("Dolphin", "Emulators\\Dolphin\\Dolphin.exe")])
emu = root / "Emulators" / "Dolphin"
(emu / "portable.txt").write_text("")
self.assertEqual(install.launchbox_bios_dirs(root)["dolphin"], emu / "User")
def test_dolphin_without_portable_marker_is_absent(self):
root = self._make([("Dolphin", "Emulators\\Dolphin\\Dolphin.exe")])
(root / "Emulators" / "Dolphin" / "User").mkdir()
self.assertNotIn("dolphin", install.launchbox_bios_dirs(root))
def test_unknown_emulators_are_ignored(self):
root = self._make([("Cemu", "Emulators\\Cemu\\Cemu.exe")])
self.assertEqual(install.launchbox_bios_dirs(root), {})
def test_doctype_is_rejected(self):
root = Path(tempfile.mkdtemp()) / "LaunchBox"
(root / "Data").mkdir(parents=True)
(root / "Data" / "Emulators.xml").write_text(
"<!DOCTYPE LaunchBox [<!ENTITY a 'x'>]>\n<LaunchBox></LaunchBox>\n"
)
self.assertEqual(install.launchbox_emulators(root / "Data" / "Emulators.xml"), {})
class TestStandaloneCopiesExtraDirs(unittest.TestCase):
"""Setups that keep emulators outside the default locations."""
def _manifest(self) -> dict:
return {
"standalone_copies": [
{
"pattern": "ps2-*.bin",
"emulator": "pcsx2",
"targets": {"windows": []},
},
{
"file": "GC/USA/IPL.bin",
"emulator": "dolphin",
"targets": {"windows": []},
},
]
}
def test_pattern_entry_copies_into_extra_dir(self):
bios = Path(tempfile.mkdtemp())
(bios / "ps2-0230a.bin").write_text("rom")
extra = Path(tempfile.mkdtemp()) / "bios"
extra.mkdir()
copied, _ = install.do_standalone_copies(
self._manifest(), bios, "windows", {"pcsx2": extra}
)
self.assertEqual(copied, 1)
self.assertTrue((extra / "ps2-0230a.bin").exists())
def test_file_entry_keeps_its_subdirectory(self):
bios = Path(tempfile.mkdtemp())
(bios / "GC" / "USA").mkdir(parents=True)
(bios / "GC" / "USA" / "IPL.bin").write_text("ipl")
user = Path(tempfile.mkdtemp()) / "User"
(user / "GC" / "USA").mkdir(parents=True)
copied, _ = install.do_standalone_copies(
self._manifest(), bios, "windows", {"dolphin": user}
)
self.assertEqual(copied, 1)
self.assertTrue((user / "GC" / "USA" / "IPL.bin").exists())
def test_no_extra_dirs_is_unchanged(self):
bios = Path(tempfile.mkdtemp())
(bios / "ps2-0230a.bin").write_text("rom")
self.assertEqual(
install.do_standalone_copies(self._manifest(), bios, "windows"), (0, 0)
)
@unittest.skipUnless(shutil.which("pwsh"), "pwsh not available")
class TestLaunchboxDetectionPowershell(unittest.TestCase):
"""install.ps1 must resolve LaunchBox's portable RetroArch on its own."""
def _run(self, entries: str, seed=None, manifest: dict | None = None):
userprofile = Path(tempfile.mkdtemp())
ra_dir = userprofile / "LaunchBox" / "Emulators" / "RetroArch"
ra_dir.mkdir(parents=True)
data = userprofile / "LaunchBox" / "Data"
data.mkdir(parents=True)
(data / "Emulators.xml").write_text(
"<?xml version=\"1.0\" standalone=\"yes\"?>\n"
"<LaunchBox>\n"
f"{entries}"
"</LaunchBox>\n"
)
if seed is not None:
seed(userprofile)
serve_root = Path(tempfile.mkdtemp())
(serve_root / "install").mkdir()
(serve_root / "install" / "retroarch.json").write_text(
json.dumps(manifest if manifest is not None else {"files": []})
)
handler = functools.partial(
http.server.SimpleHTTPRequestHandler, directory=str(serve_root)
)
httpd = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
try:
env = dict(os.environ)
env.update(
USERPROFILE=str(userprofile),
APPDATA=str(tempfile.mkdtemp()),
RETROBIOS_BASE_URL=f"http://127.0.0.1:{httpd.server_address[1]}",
)
proc = subprocess.run(
["pwsh", "-NoProfile", "-File", str(REPO_ROOT / "install.ps1")],
env=env,
capture_output=True,
text=True,
timeout=60,
)
finally:
httpd.shutdown()
return proc, ra_dir
def _assert_resolved(self, proc, ra_dir):
self.assertIn("Found LaunchBox with RetroArch at", proc.stdout)
self.assertIn(str(ra_dir), proc.stdout)
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("Done.", proc.stdout)
def test_detects_launchbox_retroarch(self):
proc, ra_dir = self._run(
" <Emulator>\n"
" <Title>Retroarch</Title>\n"
" <ApplicationPath>Emulators\\RetroArch\\retroarch.exe</ApplicationPath>\n"
" </Emulator>\n"
)
self._assert_resolved(proc, ra_dir)
def test_stale_entry_before_installed_one(self):
proc, ra_dir = self._run(
" <Emulator>\n"
" <Title>RetroArch (old)</Title>\n"
" <ApplicationPath>Emulators\\RetroArch-old\\retroarch.exe</ApplicationPath>\n"
" </Emulator>\n"
" <Emulator>\n"
" <Title>RetroArch</Title>\n"
" <ApplicationPath>Emulators\\RetroArch\\retroarch.exe</ApplicationPath>\n"
" </Emulator>\n"
)
self._assert_resolved(proc, ra_dir)
def test_system_directory_from_config(self):
def seed(root: Path) -> None:
ra = root / "LaunchBox" / "Emulators" / "RetroArch"
(ra / "bios").mkdir(parents=True)
(ra / "retroarch.cfg").write_text('system_directory = ":\\bios"\n')
proc, ra_dir = self._run(
" <Emulator>\n"
" <Title>RetroArch</Title>\n"
" <ApplicationPath>Emulators\\RetroArch\\retroarch.exe</ApplicationPath>\n"
" </Emulator>\n",
seed=seed,
)
self.assertIn("Found LaunchBox with RetroArch at", proc.stdout)
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn(str(ra_dir / "bios"), proc.stdout)
def test_standalone_copy_reaches_launchbox_emulator(self):
def seed(root: Path) -> None:
system = root / "LaunchBox" / "Emulators" / "RetroArch" / "system"
system.mkdir(parents=True)
(system / "ps2-0230a.bin").write_text("rom")
pcsx2 = root / "LaunchBox" / "Emulators" / "PCSX2"
(pcsx2 / "bios").mkdir(parents=True)
(pcsx2 / "portable.ini").write_text("")
(pcsx2 / "inis").mkdir()
(pcsx2 / "inis" / "PCSX2.ini").write_text("[Folders]\nBios = bios\n")
manifest = {
"files": [],
"standalone_copies": [
{
"pattern": "ps2-*.bin",
"emulator": "pcsx2",
"targets": {"windows": []},
}
],
}
proc, ra_dir = self._run(
" <Emulator>\n"
" <Title>RetroArch</Title>\n"
" <ApplicationPath>Emulators\\RetroArch\\retroarch.exe</ApplicationPath>\n"
" </Emulator>\n"
" <Emulator>\n"
" <Title>PCSX2</Title>\n"
" <ApplicationPath>Emulators\\PCSX2\\pcsx2.exe</ApplicationPath>\n"
" </Emulator>\n",
seed=seed,
manifest=manifest,
)
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
landed = ra_dir.parent / "PCSX2" / "bios" / "ps2-0230a.bin"
self.assertTrue(landed.exists(), proc.stdout)
class TestDetectFrontends(unittest.TestCase):
"""Frontends have no BIOS directory of their own but hint at the setup."""
def test_esde_from_home_directory(self):
home = Path(tempfile.mkdtemp())
(home / "ES-DE").mkdir()
old = os.environ.pop("ESDE_APPDATA_DIR", None)
try:
self.assertIn("esde", install.detect_frontends("linux", home=home))
finally:
if old is not None:
os.environ["ESDE_APPDATA_DIR"] = old
def test_esde_from_env_dir(self):
home = Path(tempfile.mkdtemp())
appdata = Path(tempfile.mkdtemp())
os.environ["ESDE_APPDATA_DIR"] = str(appdata)
try:
self.assertIn("esde", install.detect_frontends("linux", home=home))
finally:
del os.environ["ESDE_APPDATA_DIR"]
def test_nothing_detected(self):
home = Path(tempfile.mkdtemp())
old = os.environ.pop("ESDE_APPDATA_DIR", None)
try:
self.assertEqual(install.detect_frontends("linux", home=home), [])
finally:
if old is not None:
os.environ["ESDE_APPDATA_DIR"] = old
class TestNormalizePlatform(unittest.TestCase):
"""Manifest URLs are case sensitive: user input must be normalized."""