From 38cf37c5e6401165abe8bf048bd627e625df5768 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 31 Jul 2026 22:15:23 -0500 Subject: [PATCH] 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 --- deploy/build-pod.ps1 | 147 ++++++++++++++++++++++++++++++-------- docs/INPUT-INTEGRATION.md | 27 ++++--- docs/PLAN.md | 26 ++++--- 3 files changed, 153 insertions(+), 47 deletions(-) diff --git a/deploy/build-pod.ps1 b/deploy/build-pod.ps1 index cd1c9bc..3b8e789 100644 --- a/deploy/build-pod.ps1 +++ b/deploy/build-pod.ps1 @@ -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 , 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 } diff --git a/docs/INPUT-INTEGRATION.md b/docs/INPUT-INTEGRATION.md index 2c8b677..8001ce8 100644 --- a/docs/INPUT-INTEGRATION.md +++ b/docs/INPUT-INTEGRATION.md @@ -203,13 +203,22 @@ podized game's install carries its own copy, built by: deploy\build-pod.ps1 -ProfileJson ``` -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 +start "" "...\riojoy\app\RioJoy.Tray.exe" --exit-with ``` `--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 diff --git a/docs/PLAN.md b/docs/PLAN.md index e125c62..2595e37 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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--.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--.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)