pod: bundled per-game deployment (portable config, --exit-with, build-pod)
Phase 10: RIO hardware exists only on pods + dev boxes, so production is one RIOJoy copy inside each podized game folder, no resident tray. ConfigLocator makes a config.json beside the exe win over %APPDATA%; --exit-with <exe|pid> (CompanionTarget/CompanionExit, 60s startup grace) tears down and quits when the game exits; a starting --exit-with instance waits up to 15s for the predecessor mutex instead of silently exiting. deploy/build-pod.ps1 emits the ~4.5MB drop-in (app + portable config wrapping the profile + start script, no drivers) - verified against the shipped Descent profile. 455 tests; PLAN.md Phase 10 + INPUT-INTEGRATION.md pod section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<#
|
||||
.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).
|
||||
|
||||
.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).
|
||||
.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]$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 ==' -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 {
|
||||
$appOut = Join-Path $staging 'riojoy'
|
||||
New-Item -ItemType Directory -Force -Path $appOut | Out-Null
|
||||
|
||||
# 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: keep the win-x64 native
|
||||
# at the app root, drop arch subdirs and non-Windows natives.
|
||||
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 $appOut '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.
|
||||
$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"
|
||||
) -join "`r`n"
|
||||
Set-Content -Path (Join-Path $staging 'start-riojoy.bat') -Value $startBat -Encoding Ascii
|
||||
|
||||
# 4. 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.'
|
||||
''
|
||||
'The pod must be provisioned once with the universal RIOJoy package'
|
||||
'(drivers: ViGEmBus / RioGamepad - see install-rio.ps1); pod bundles'
|
||||
'deliberately carry no drivers.'
|
||||
''
|
||||
'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.'
|
||||
) -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).
|
||||
$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, call start-riojoy.bat from its launch script (game exe: $GameExe)."
|
||||
}
|
||||
finally {
|
||||
if (Test-Path $staging) { Remove-Item $staging -Recurse -Force }
|
||||
}
|
||||
Reference in New Issue
Block a user