Files
riojoy/deploy/build-pod.ps1
T
CydandClaude Opus 5 63bdb2c1da pod bundle: explicit app-level config, and the registry profile catches up
The portable config a bundle shipped carried only Profiles, so every app-level
setting - ports, baud, poll rate - rode whatever that build of RIOJoy compiled
in. That matched today and would drift invisibly the day a default changes or
the FastRIO path moves AnalogPollMs. The generated config now writes them out,
so a bundle says what it runs with. Verified by building the Descent 3 bundle
and parsing the result.

profiles/descent3.json was a pre-cockpit draft: old button layout, no explicit
ports, no overlay labels. Replaced verbatim with the flown-and-confirmed Tesla
profile from the game's own repo (Descent3 venue/riojoy/tesla.riojoy.json,
which pack-dist feeds to build-pod and is the canonical copy) - explicit
COM1/COM2, the 30-mapping layout, afterburner on the thumb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:59:17 -05:00

258 lines
13 KiB
PowerShell

<#
.SYNOPSIS
Build a SELF-CONTAINED pod bundle of RIOJoy for one podized game
(PLAN.md Phase 10): a drop-in the game's install carries inside its own
directory - app + portable config (the game's profile) + start script +
ALL prerequisites (ViGEmBus / XP driver + .NET payload) with an
idempotent install entry point. The game's postinstall.bat calls
install-riojoy.bat (safe to re-run); the game's launch script calls
start-riojoy.bat; RIOJoy exits by itself when the game exits
(--exit-with). No operator ever touches the pod to install anything.
There is deliberately NO uninstall step: drivers are abandoned in place
on game removal, because nothing can know whether another podized game
still uses them - and idle drivers are harmless.
.PARAMETER ProfileJson
Path to the game's single-profile JSON document (must carry "Name";
same format as profiles\descent-d1x.json).
.PARAMETER GameExe
Game executable name for --exit-with. Default: the profile's first
MatchExecutables entry.
.PARAMETER Flavor
net48 (Windows 10/11 pods, default) or net40 (XP pods, x86). Selects
both the app build and which prerequisites are bundled.
.PARAMETER VigemInstaller
net48 only: path to the signed ViGEmBus installer. If omitted, looks in
deploy\vendor\ViGEmBus*.exe.
.PARAMETER OutDir
Where to write the zip (default: dist, relative to the repo root).
.PARAMETER Configuration
Build configuration (default: Release).
#>
param(
[Parameter(Mandatory = $true)]
[string]$ProfileJson,
[string]$GameExe,
[ValidateSet('net48', 'net40')]
[string]$Flavor = 'net48',
[string]$VigemInstaller,
[string]$OutDir = 'dist',
[string]$Configuration = 'Release'
)
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent # deploy\ -> repo root
$staging = Join-Path ([IO.Path]::GetTempPath()) "riojoy-pod-$([Guid]::NewGuid().ToString('N'))"
Write-Host '== RIOJoy pod bundle (self-contained) ==' -ForegroundColor Cyan
# --- profile document -----------------------------------------------------
if (-not (Test-Path $ProfileJson)) { throw "Profile document not found: $ProfileJson" }
$profileText = Get-Content $ProfileJson -Raw
$profileDoc = $profileText | ConvertFrom-Json
if (-not $profileDoc.Name) { throw "Profile file '$ProfileJson' has no Name." }
if (-not $GameExe) {
$GameExe = @($profileDoc.MatchExecutables) | Select-Object -First 1
if (-not $GameExe) { throw "Profile has no MatchExecutables; pass -GameExe <name>." }
}
# --- version stamp --------------------------------------------------------
$sha = (& git -C $repo rev-parse --short HEAD 2>$null)
if (-not $sha) { $sha = 'nogit' }
$version = "{0}-{1}" -f (Get-Date -Format 'yyyyMMdd'), $sha
$safeName = $profileDoc.Name
foreach ($c in [IO.Path]::GetInvalidFileNameChars()) { $safeName = $safeName.Replace($c, '_') }
$safeName = $safeName -replace ' ', '-'
try {
# The payload uses the SAME inner layout as the universal package
# (app / app-xp, vendor, install-core.bat, install-rio.ps1), so the
# idempotent install machinery is reused verbatim - no forked scripts.
$pkgDir = Join-Path $staging 'riojoy'
New-Item -ItemType Directory -Force -Path $pkgDir | Out-Null
$appDirName = if ($Flavor -eq 'net48') { 'app' } else { 'app-xp' }
$appOut = Join-Path $pkgDir $appDirName
# 1. Publish the tray app (framework-dependent, like the universal package).
Write-Host "Publishing RioJoy.Tray ($Configuration, $Flavor)..."
& dotnet publish (Join-Path $repo 'src\RioJoy.Tray\RioJoy.Tray.csproj') `
-c $Configuration -f $Flavor -p:DebugType=none `
-o $appOut | Out-Null
if ($LASTEXITCODE -ne 0) { throw "dotnet publish ($Flavor) failed." }
if ($Flavor -eq 'net48') {
# Same SkiaSharp pruning as build-package.ps1.
foreach ($d in 'x64', 'x86', 'arm64') {
$nd = Join-Path $appOut $d
if (Test-Path $nd) { Remove-Item $nd -Recurse -Force }
}
Get-ChildItem $appOut -Filter '*.dylib' -ErrorAction SilentlyContinue | Remove-Item -Force
Get-ChildItem $appOut -Filter '*.so' -ErrorAction SilentlyContinue | Remove-Item -Force
}
# 2. Portable config beside the exe (ConfigLocator picks it over %APPDATA%):
# an AppConfig wrapping this game's profile, verbatim, plus the app-level
# settings written out explicitly. A config that carries only Profiles
# leaves every app-level value to whatever this build of RIOJoy compiles
# in - which happens to match today, and would drift invisibly the day a
# default changes or a pod needs tuning (the FastRIO path will move
# AnalogPollMs). A pod bundle should say what it runs with.
$configJson = @"
{
"DefaultRioComPort": "COM1",
"DefaultPlasmaComPort": "COM2",
"RioBaudRate": 9600,
"AnalogPollMs": 55,
"Profiles": [
$profileText
]
}
"@
Set-Content -Path (Join-Path $appOut 'config.json') -Value $configJson -Encoding utf8
Set-Content -Path (Join-Path $pkgDir 'VERSION.txt') -Value "RIOJoy pod bundle $version ($Flavor) - $($profileDoc.Name)" -Encoding utf8
# 3. Prerequisites + the shared idempotent install core, per flavor.
Copy-Item (Join-Path $PSScriptRoot 'install-core.bat') $pkgDir
$vendorOut = Join-Path $pkgDir 'vendor'
New-Item -ItemType Directory -Force -Path $vendorOut | Out-Null
if ($Flavor -eq 'net48') {
Copy-Item (Join-Path $PSScriptRoot 'install-rio.ps1') $pkgDir
if (-not $VigemInstaller) {
$VigemInstaller = Get-ChildItem -Path (Join-Path $PSScriptRoot 'vendor') -Filter 'ViGEmBus*.exe' `
-ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
}
if (-not $VigemInstaller -or -not (Test-Path $VigemInstaller)) {
throw "ViGEmBus installer not found. Pass -VigemInstaller <path>, or drop ViGEmBus_*.exe into deploy\vendor\."
}
$sig = Get-AuthenticodeSignature $VigemInstaller
if ($sig.Status -ne 'Valid') { throw "ViGEmBus installer is not validly signed ($($sig.Status)): $VigemInstaller" }
Copy-Item $VigemInstaller $vendorOut
Write-Host "Bundled prerequisite: $(Split-Path $VigemInstaller -Leaf) (signature $($sig.Status))"
} else {
$vendorXpSrc = Join-Path $PSScriptRoot 'vendor\xp'
$vendorXpOut = Join-Path $vendorOut 'xp'
New-Item -ItemType Directory -Force -Path $vendorXpOut | Out-Null
$xpWanted = @(
'dotNetFx40_Full_x86_x64.exe', 'NDP40-KB2468871-v2-x86.exe',
'RioGamepadXP.inf', 'RioGamepadXP.sys', 'devcon.exe'
)
foreach ($name in $xpWanted) {
$src = Join-Path $vendorXpSrc $name
if (Test-Path $src) {
Copy-Item $src $vendorXpOut
Write-Host "Bundled XP prerequisite: $name"
} else {
Write-Warning "XP prerequisite missing from deploy\vendor\xp: $name - install will warn/skip."
}
}
}
# 4. install-riojoy.bat - the game's postinstall.bat calls this. Elevates
# (modern) like the universal postinstall.bat, runs the shared core,
# deletes nothing. Idempotent; there is no uninstall counterpart.
$installBat = @(
'@echo off'
'rem ==========================================================================='
"rem RIOJoy pod install for $($profileDoc.Name) - call from the game's"
'rem postinstall.bat. Idempotent: safe to run on every (re)install/update.'
'rem Installs only what is absent (drivers, runtime); never removes anything.'
'rem There is deliberately NO uninstall: other podized games may share the'
'rem drivers, and abandoned-in-place drivers are harmless.'
'rem ==========================================================================='
'setlocal'
'net session >nul 2>&1'
'if %errorlevel% neq 0 ('
' ver | findstr /C:"Version 5.1" >nul'
' if not errorlevel 1 ('
' echo install-riojoy.bat must run as an Administrator user.'
' exit /b 1'
' )'
' echo Requesting administrator privileges...'
" powershell -NoProfile -Command `"Start-Process -FilePath '%~f0' -Verb RunAs -Wait`""
' exit /b 0'
')'
'call "%~dp0riojoy\install-core.bat"'
'exit /b %errorlevel%'
) -join "`r`n"
Set-Content -Path (Join-Path $staging 'install-riojoy.bat') -Value $installBat -Encoding Ascii
# 5. Start script: the game's launch script calls this before the game.
# Everything explicit - no foreground detection: --profile activates
# immediately so the virtual pad exists before the game enumerates
# controllers, and --exit-with ends RIOJoy when the game exits.
$startBat = @(
'@echo off'
"rem RIOJoy pod companion for $($profileDoc.Name)."
'rem Call from the game''s launch script BEFORE starting the game:'
'rem the profile activates immediately (no foreground detection), so'
'rem the virtual controller exists before the game enumerates devices.'
'rem RIOJoy exits by itself when the game exits (--exit-with).'
"start `"`" `"%~dp0riojoy\$appDirName\RioJoy.Tray.exe`" --profile `"$($profileDoc.Name)`" --exit-with $GameExe"
) -join "`r`n"
Set-Content -Path (Join-Path $staging 'start-riojoy.bat') -Value $startBat -Encoding Ascii
# 6. Integrator note.
$readme = @(
"RIOJoy pod bundle - $($profileDoc.Name) ($version, $Flavor)"
''
'Self-contained: app, this game''s profile, and all RIOJoy prerequisites'
'(drivers/runtime). Nothing is installed on the pod by hand.'
''
'Integrate into the game''s package:'
' 1. Ship the riojoy\ folder, install-riojoy.bat and start-riojoy.bat'
' inside the game''s install directory.'
' 2. Call install-riojoy.bat from the game''s postinstall.bat.'
' Idempotent - safe on every install, reinstall and update; it only'
' adds what is missing and never removes anything.'
' 3. Call start-riojoy.bat from the game''s launch script before the'
' game. The profile activates immediately and explicitly - no'
' foreground detection - so the virtual controller exists before'
' the game enumerates devices; RIOJoy exits by itself when the'
' game exits. A launcher should wait (with a timeout) on the named'
' event "RIOJoy.Tray.Ready" before starting the game: signaled ='
' pad + ports ready; RIOJoy exit code 4/5 = config/activation'
' failure (reason on stderr); neither = stuck, report.'
' 4. Put NOTHING in the game''s pre-uninstall: drivers stay in place by'
' design (another podized game may use them; idle drivers are'
' harmless).'
''
"The game's profile lives in riojoy\$appDirName\config.json (portable"
'mode - the per-user %APPDATA% config is ignored while it exists). Edit'
"it there, or run riojoy\$appDirName\RioJoy.Tray.exe with no arguments"
'for the profile editor.'
) -join "`r`n"
Set-Content -Path (Join-Path $staging 'README-POD.txt') -Value $readme -Encoding Ascii
# 7. Zip with forward-slash entry names (same rationale as build-package.ps1).
$outDirFull = if ([IO.Path]::IsPathRooted($OutDir)) { $OutDir } else { Join-Path $repo $OutDir }
New-Item -ItemType Directory -Force -Path $outDirFull | Out-Null
$zip = Join-Path $outDirFull "RIOJoy-pod-$safeName-$version.zip"
if (Test-Path $zip) { Remove-Item $zip -Force }
Write-Host "Zipping -> $zip"
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$base = (Resolve-Path $staging).Path.TrimEnd('\') + '\'
$fs = [System.IO.File]::Open($zip, [System.IO.FileMode]::CreateNew)
try {
$archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
try {
foreach ($file in Get-ChildItem -Path $staging -Recurse -File) {
$entryName = $file.FullName.Substring($base.Length) -replace '\\', '/'
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive, $file.FullName, $entryName,
[System.IO.Compression.CompressionLevel]::Optimal) | Out-Null
}
} finally { $archive.Dispose() }
} finally { $fs.Dispose() }
$size = '{0:N1} MB' -f ((Get-Item $zip).Length / 1MB)
Write-Host ''
Write-Host "Pod bundle built: $zip ($size)" -ForegroundColor Green
Write-Host "Integrate: extract into the game's folder; game postinstall.bat calls install-riojoy.bat; launch script calls start-riojoy.bat (game exe: $GameExe)."
}
finally {
if (Test-Path $staging) { Remove-Item $staging -Recurse -Force }
}