<# .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 ." } } # --- 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 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 $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 , 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\$appDirName\RioJoy.Tray.exe`" --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. 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\$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 } }