mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-12 06:23:23 -05:00
feat: add launchbox detection to installer
This commit is contained in:
1 parent
393d32ed84
commit
69d53c97c9
14 files changed
+1509
-1116
No files matched your search
+174
-14
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user