pod: self-contained bundles - drivers install via the game postinstall
Per Cyd: no operator ever installs anything on a pod by hand. build-pod.ps1 now mirrors the universal inner layout (app/app-xp, vendor, install-core.bat + install-rio.ps1 reused verbatim) and bundles the flavor prerequisites (ViGEmBus / XP .NET+KB+RioGamepadXP). New install-riojoy.bat entry point is called from the game postinstall.bat: self-elevating, idempotent, add-only. Deliberately no uninstall step - other podized games may share the drivers, so they are abandoned in place. Also: deploy scripts must stay pure ASCII (PS5.1 reads BOM-less UTF-8 as ANSI; an em dash decodes to a smart quote that terminates strings). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+117
-30
@@ -1,12 +1,17 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build a POD-BUNDLED RIOJoy for one podized game (PLAN.md §Phase 10): a
|
||||
drop-in folder the game's install carries inside its own directory —
|
||||
app + portable config (the game's profile) + start script. RIOJoy starts
|
||||
with the game (--exit-with) and exits by itself when the game exits, so
|
||||
no resident RIOJoy runs on the pod and the console operator never touches
|
||||
it. Carries NO drivers/vendor payload: the pod is provisioned once by the
|
||||
universal package (build-package.ps1 → install-rio.ps1).
|
||||
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";
|
||||
@@ -15,7 +20,11 @@
|
||||
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).
|
||||
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
|
||||
@@ -27,6 +36,7 @@ param(
|
||||
[string]$GameExe,
|
||||
[ValidateSet('net48', 'net40')]
|
||||
[string]$Flavor = 'net48',
|
||||
[string]$VigemInstaller,
|
||||
[string]$OutDir = 'dist',
|
||||
[string]$Configuration = 'Release'
|
||||
)
|
||||
@@ -35,7 +45,7 @@ $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 ==' -ForegroundColor Cyan
|
||||
Write-Host '== RIOJoy pod bundle (self-contained) ==' -ForegroundColor Cyan
|
||||
|
||||
# --- profile document -----------------------------------------------------
|
||||
if (-not (Test-Path $ProfileJson)) { throw "Profile document not found: $ProfileJson" }
|
||||
@@ -56,8 +66,13 @@ foreach ($c in [IO.Path]::GetInvalidFileNameChars()) { $safeName = $safeName.Rep
|
||||
$safeName = $safeName -replace ' ', '-'
|
||||
|
||||
try {
|
||||
$appOut = Join-Path $staging 'riojoy'
|
||||
New-Item -ItemType Directory -Force -Path $appOut | Out-Null
|
||||
# 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)..."
|
||||
@@ -67,8 +82,7 @@ try {
|
||||
if ($LASTEXITCODE -ne 0) { throw "dotnet publish ($Flavor) failed." }
|
||||
|
||||
if ($Flavor -eq 'net48') {
|
||||
# Same SkiaSharp pruning as build-package.ps1: keep the win-x64 native
|
||||
# at the app root, drop arch subdirs and non-Windows natives.
|
||||
# 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 }
|
||||
@@ -81,39 +95,112 @@ try {
|
||||
# an AppConfig wrapping just this game's profile, verbatim.
|
||||
$configJson = "{`n `"Profiles`": [`n$profileText`n ]`n}"
|
||||
Set-Content -Path (Join-Path $appOut 'config.json') -Value $configJson -Encoding utf8
|
||||
Set-Content -Path (Join-Path $appOut 'VERSION.txt') -Value "RIOJoy pod bundle $version ($Flavor) - $($profileDoc.Name)" -Encoding utf8
|
||||
Set-Content -Path (Join-Path $pkgDir 'VERSION.txt') -Value "RIOJoy pod bundle $version ($Flavor) - $($profileDoc.Name)" -Encoding utf8
|
||||
|
||||
# 3. Start script: the game's launch script calls this before the game.
|
||||
# 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.
|
||||
$startBat = @(
|
||||
'@echo off'
|
||||
"rem RIOJoy pod companion for $($profileDoc.Name)."
|
||||
'rem Call from the game''s launch script BEFORE starting the game;'
|
||||
'rem RIOJoy exits by itself when the game exits (--exit-with).'
|
||||
"start `"`" `"%~dp0riojoy\RioJoy.Tray.exe`" --exit-with $GameExe"
|
||||
"start `"`" `"%~dp0riojoy\$appDirName\RioJoy.Tray.exe`" --exit-with $GameExe"
|
||||
) -join "`r`n"
|
||||
Set-Content -Path (Join-Path $staging 'start-riojoy.bat') -Value $startBat -Encoding Ascii
|
||||
|
||||
# 4. Integrator note.
|
||||
# 6. Integrator note.
|
||||
$readme = @(
|
||||
"RIOJoy pod bundle - $($profileDoc.Name) ($version, $Flavor)"
|
||||
''
|
||||
'Drop the riojoy\ folder and start-riojoy.bat into the game''s install'
|
||||
'directory on the pod, and call start-riojoy.bat from the game''s launch'
|
||||
'script before starting the game. RIOJoy activates when the game window'
|
||||
'comes foreground and exits by itself when the game exits - no resident'
|
||||
'RIOJoy, nothing for the console operator to manage.'
|
||||
'Self-contained: app, this game''s profile, and all RIOJoy prerequisites'
|
||||
'(drivers/runtime). Nothing is installed on the pod by hand.'
|
||||
''
|
||||
'The pod must be provisioned once with the universal RIOJoy package'
|
||||
'(drivers: ViGEmBus / RioGamepad - see install-rio.ps1); pod bundles'
|
||||
'deliberately carry no drivers.'
|
||||
'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. RIOJoy activates when the game window comes foreground and'
|
||||
' exits by itself when the game exits.'
|
||||
' 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\config.json (portable mode - the'
|
||||
'per-user %APPDATA% config is ignored while it exists). Edit it there,'
|
||||
'or run riojoy\RioJoy.Tray.exe with no arguments for the profile editor.'
|
||||
"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
|
||||
|
||||
# 5. Zip with forward-slash entry names (same rationale as build-package.ps1).
|
||||
# 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"
|
||||
@@ -138,7 +225,7 @@ try {
|
||||
$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, call start-riojoy.bat from its launch script (game exe: $GameExe)."
|
||||
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 }
|
||||
|
||||
@@ -203,13 +203,22 @@ podized game's install carries its own copy, built by:
|
||||
deploy\build-pod.ps1 -ProfileJson <your-game-profile.json>
|
||||
```
|
||||
|
||||
That emits a ~5 MB drop-in — `riojoy\` (app + a **portable** `config.json`
|
||||
holding just this game's profile; a config beside the exe wins over the
|
||||
per-user `%APPDATA%` store) plus `start-riojoy.bat` — which the game's
|
||||
launch script calls before starting the game:
|
||||
That emits a **self-contained** drop-in (~8 MB on net48): `riojoy\` — the app,
|
||||
a **portable** `config.json` beside the exe holding just this game's profile
|
||||
(it wins over the per-user `%APPDATA%` store), and all RIOJoy prerequisites
|
||||
(ViGEmBus; on the XP flavor .NET 4.0 + the RioGamepadXP driver) — plus two
|
||||
entry points the game's package wires up:
|
||||
|
||||
- **`install-riojoy.bat`** — call from the game's `postinstall.bat`.
|
||||
Self-elevating and **idempotent**: safe on every install, reinstall, and
|
||||
update; it installs only what's absent and never removes anything. Put
|
||||
**nothing** in the game's pre-uninstall — drivers stay in place by design,
|
||||
since another podized game may share them and idle drivers are harmless.
|
||||
- **`start-riojoy.bat`** — call from the game's launch script before the
|
||||
game:
|
||||
|
||||
```
|
||||
start "" "...\riojoy\RioJoy.Tray.exe" --exit-with <game exe>
|
||||
start "" "...\riojoy\app\RioJoy.Tray.exe" --exit-with <game exe>
|
||||
```
|
||||
|
||||
`--exit-with` makes RIOJoy self-managing: it activates when the game's window
|
||||
@@ -223,10 +232,10 @@ release the single-instance lock.
|
||||
Properties that matter on a cabinet: each game pins the RIOJoy build it was
|
||||
verified with (updating RIOJoy for a new game can't regress an old one);
|
||||
native games simply don't bundle RIOJoy, so the COM ports are free for them
|
||||
by construction; and drivers stay a one-time pod provisioning step (the
|
||||
universal package's `install-rio.ps1`) — pod bundles deliberately carry none.
|
||||
The bundled exe is still the full tray app: run it with no arguments on the
|
||||
pod and you have the profile editor.
|
||||
by construction; and every bundle is fully self-sufficient — drivers install
|
||||
through the game's own postinstall, so a fresh pod needs no separate RIOJoy
|
||||
provisioning pass. The bundled exe is still the full tray app: run it with no
|
||||
arguments on the pod and you have the profile editor.
|
||||
|
||||
## Testing without hardware or game
|
||||
|
||||
|
||||
+18
-8
@@ -516,14 +516,24 @@ total across the suite.
|
||||
down while game B's starts) instead of the historical silent exit-0; plain
|
||||
launches keep the instant-exit behavior. Abandoned mutex (predecessor
|
||||
crash) counts as acquired.
|
||||
- **`deploy/build-pod.ps1`**: emits the per-game drop-in — `riojoy\` app
|
||||
(flavor-selectable net48/net40, Skia-pruned) + portable `config.json`
|
||||
wrapping the game's profile document verbatim + `start-riojoy.bat`
|
||||
(`--exit-with` prefilled from the profile's first trigger) + README-POD —
|
||||
zipped as `RIOJoy-pod-<name>-<stamp>.zip` (~4.5 MB). Deliberately **no
|
||||
drivers**: pods are provisioned once by the universal package. Verified by
|
||||
building the Descent bundle and round-tripping its emitted config through
|
||||
`ConfigStore.Load`.
|
||||
- **`deploy/build-pod.ps1`**: emits the **self-contained** per-game drop-in —
|
||||
nothing is ever installed on a pod by hand. Inner layout mirrors the
|
||||
universal package (`app`/`app-xp`, `vendor`, `install-core.bat`,
|
||||
`install-rio.ps1` reused **verbatim** — no forked install logic), plus the
|
||||
portable `config.json` beside the exe (the game's profile document
|
||||
verbatim), `start-riojoy.bat` (`--exit-with` prefilled from the profile's
|
||||
first trigger), and `install-riojoy.bat`: called from the **game's
|
||||
`postinstall.bat`**, self-elevating, **idempotent** (installs only what's
|
||||
absent — ViGEmBus on net48; .NET 4.0 + KB2468871 + RioGamepadXP via devcon
|
||||
on net40 — and never removes anything). There is deliberately **no
|
||||
uninstall step**: drivers are abandoned in place on game removal, since
|
||||
nothing can know whether another podized game still uses them and idle
|
||||
drivers are harmless. Zipped as `RIOJoy-pod-<name>-<stamp>.zip` (~8.4 MB
|
||||
net48 incl. ViGEmBus). Verified by building the Descent bundle and
|
||||
round-tripping its emitted config through `ConfigStore.Load`.
|
||||
Gotcha for future edits: PS 5.1 reads BOM-less scripts as ANSI, where a
|
||||
UTF-8 em dash decodes into a smart quote that *terminates strings* — keep
|
||||
deploy scripts pure ASCII.
|
||||
- ⏳ **Remaining:** on-pod verification of the full launch/handoff cycle
|
||||
(launcher → game A → quit → game B); podize a first real game with the
|
||||
bundle; revisit the universal zip's `install.bat` framing (dev-setup only)
|
||||
|
||||
Reference in New Issue
Block a user