mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 13:33:24 -05:00
336 lines
13 KiB
PowerShell
336 lines
13 KiB
PowerShell
# RetroBIOS installer for Windows (PowerShell 5+, no Python required)
|
|
$ErrorActionPreference = "Stop"
|
|
$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
|
|
$biosPath = $null
|
|
|
|
# Detect EmuDeck
|
|
$emudeckSettings = Join-Path $env:APPDATA "EmuDeck\settings.ps1"
|
|
if (Test-Path $emudeckSettings) {
|
|
$content = Get-Content $emudeckSettings -Raw
|
|
if ($content -match '\$emulationPath\s*=\s*"([^"]+)"') {
|
|
$platform = "emudeck"
|
|
$biosPath = Join-Path $Matches[1] "bios"
|
|
Write-Host "Found EmuDeck at $biosPath"
|
|
}
|
|
}
|
|
|
|
# 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"
|
|
$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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Fallback
|
|
if (-not $platform) {
|
|
$available = @("retroarch", "batocera", "recalbox", "retrobat", "emudeck", "lakka", "retrodeck", "rocknix", "romm", "bizhawk", "misterfpga")
|
|
$platform = (Read-Host "Platform ($($available -join ', '))").Trim().ToLower()
|
|
$biosPath = (Read-Host "BIOS directory path").Trim()
|
|
if (-not $platform -or -not $biosPath) {
|
|
Write-Host "Aborted." -ForegroundColor Red; exit 1
|
|
}
|
|
if ($available -notcontains $platform) {
|
|
Write-Host "Unknown platform '$platform'. Available: $($available -join ', ')" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
Write-Host "`nFetching file index for $platform..."
|
|
$manifest = Invoke-RestMethod "$baseUrl/install/$platform.json"
|
|
$files = $manifest.files
|
|
Write-Host " $($files.Count) files"
|
|
|
|
Write-Host "`nChecking existing files..."
|
|
$toDownload = @()
|
|
$upToDate = 0
|
|
|
|
foreach ($f in $files) {
|
|
$dest = Join-Path $biosPath $f.dest
|
|
if (Test-Path $dest) {
|
|
if ($f.sha1) {
|
|
$actual = (Get-FileHash $dest -Algorithm SHA1).Hash.ToLower()
|
|
if ($actual -eq $f.sha1) { $upToDate++; continue }
|
|
} else {
|
|
$upToDate++; continue
|
|
}
|
|
}
|
|
$toDownload += $f
|
|
}
|
|
|
|
Write-Host " $upToDate/$($files.Count) up to date, $($toDownload.Count) to download"
|
|
|
|
$downloaded = 0
|
|
$errors = 0
|
|
$total = $toDownload.Count
|
|
|
|
foreach ($f in $toDownload) {
|
|
$dest = Join-Path $biosPath $f.dest
|
|
$dir = Split-Path $dest -Parent
|
|
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
|
|
|
if ($f.release_asset) {
|
|
$url = "$releaseUrl/$($f.release_asset)"
|
|
} else {
|
|
$url = "$baseUrl/$($f.repo_path)"
|
|
}
|
|
|
|
$tmp = "$dest.tmp"
|
|
$ok = $false
|
|
foreach ($attempt in 1..3) {
|
|
try {
|
|
Invoke-WebRequest -Uri $url -OutFile $tmp -UseBasicParsing
|
|
} catch {
|
|
continue
|
|
}
|
|
if ($f.sha1) {
|
|
$actual = (Get-FileHash $tmp -Algorithm SHA1).Hash.ToLower()
|
|
if ($actual -ne $f.sha1) {
|
|
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
continue
|
|
}
|
|
}
|
|
Move-Item $tmp $dest -Force
|
|
$ok = $true
|
|
break
|
|
}
|
|
|
|
if ($ok) {
|
|
$downloaded++
|
|
$i = $downloaded + $errors
|
|
Write-Host " [$i/$total] $($f.dest) ok"
|
|
} else {
|
|
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
$errors++
|
|
$i = $downloaded + $errors
|
|
Write-Host " [$i/$total] $($f.dest) FAILED" -ForegroundColor Red
|
|
}
|
|
}
|
|
|
|
# Standalone emulator copies
|
|
if ($manifest.standalone_copies) {
|
|
Write-Host "`nStandalone emulators:"
|
|
$extraDirs = Get-LaunchBoxBiosDirs
|
|
foreach ($entry in $manifest.standalone_copies) {
|
|
if ($entry.note) {
|
|
$detectPaths = @()
|
|
if ($entry.detect -and $entry.detect.windows) {
|
|
$detectPaths = $entry.detect.windows
|
|
}
|
|
foreach ($dp in $detectPaths) {
|
|
$expanded = [Environment]::ExpandEnvironmentVariables($dp)
|
|
if (Test-Path $expanded) {
|
|
Write-Host " $($entry.note)"
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
$sources = @()
|
|
if ($entry.pattern) {
|
|
$sources = Get-ChildItem -Path $biosPath -Filter $entry.pattern -File -ErrorAction SilentlyContinue
|
|
} elseif ($entry.file) {
|
|
$src = Join-Path $biosPath $entry.file
|
|
if (Test-Path $src) { $sources = @(Get-Item $src) }
|
|
}
|
|
if ($sources.Count -eq 0) { continue }
|
|
$targetDirs = @()
|
|
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 }
|
|
foreach ($s in $sources) {
|
|
$dest = Join-Path $expanded $s.Name
|
|
try {
|
|
Copy-Item $s.FullName $dest -Force
|
|
Write-Host " $($s.Name) -> $expanded"
|
|
} catch {
|
|
Write-Host " $($s.Name) -> $expanded FAILED" -ForegroundColor Red
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Host "`nDone. $downloaded downloaded, $upToDate already up to date."
|