Deploy: pod install scaffold -- supervisor entry-point, postinstall, packaging

Network-agnostic, air-gapped pod install for the two DOSBox titles (BT/RP 4.10),
fitting the existing TeslaConsole/TeslaLauncher pod-bay architecture.

emulator/DEPLOYMENT-PLAN.md
  Full design: bridge on the one NIC (pods form a source-proven P2P TCP mesh, so
  NAT/slirp is out); launcher keeps the bay IP, the DOSBox guest bridges at
  bayIP+100 (egg/mesh/game endpoint); two-edit +100 convention (postinstall +
  console DOSBox flag); static, air-gapped, <=32 pods.

emulator/pod-launch/  (C# net8 supervising entry-point)
  Creates a Job Object (KILL_ON_JOB_CLOSE), launches DOSBox-X + the render bridge
  into it and blocks -- kill this process and both die (kernel-enforced, even on
  a hard TerminateProcess of a hung session; verified). Mode dispatch: bt/rp
  wired (also serve camera + live mission-review via egg hostType); review +
  diagnostics recognized but fail clean until their DOS launch args are known.

emulator/deploy/  (install side)
  postinstall.bat (thin elevated wrapper) + configure.ps1 (NIC detect, realnic
  bind as a contiguous letter-leading GUID fragment, game IP = bayIP+100, stable
  MAC, render net_*.conf templates, stamp WATTCP.CFG my_ip) + tokenized conf
  templates + package.ps1 (assemble the zip). Render/bind/stamp + packaging
  verified against a scratch tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-10 10:36:30 -05:00
co-authored by Claude Opus 4.8
parent d1635d4694
commit 9df87f2c01
16 changed files with 1426 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
# deploy/ -- install-side artifacts
Sources for the self-contained, air-gapped pod install. See
[`../DEPLOYMENT-PLAN.md`](../DEPLOYMENT-PLAN.md) for the full design.
## Files
| File | Role |
|---|---|
| `postinstall.bat` | elevated entry TeslaLauncher runs after extraction: finds the package folder, installs bundled Npcap, calls `configure.ps1` |
| `configure.ps1` | the per-pod step: detects the NIC + bay IP, derives the game IP = **bayIP+100**, binds DOSBox's pcap (`realnic`), renders the conf templates, stamps `WATTCP.CFG my_ip` |
| `net_loop.conf.tmpl` | BattleTech conf template (looped netnub) |
| `net_rp.conf.tmpl` | Red Planet conf template (single-shot) |
| `package.ps1` | assembles the deployable zip from repo sources + a fresh self-contained `pod-launch` publish |
Template tokens filled at install: `@@ROOT@@` (package dir), `@@REALNIC@@` (this
pod's NIC GUID fragment), `@@MACADDR@@` (stable MAC derived from the game IP).
## Packaged zip layout
The archive extracts to the games root (`C:\games`). Zip root = one package
folder + `postinstall.bat`:
```
C:\games\
postinstall.bat <- from deploy/postinstall.bat
TeslaPod410\ <- the single package folder (= ROOT)
pod-launch.exe <- built from ../pod-launch (self-contained)
dosbox-x.exe (+ DLLs)
net_loop.conf.tmpl <- from deploy/ (rendered -> net_loop.conf)
net_rp.conf.tmpl <- from deploy/ (rendered -> net_rp.conf)
renderer.exe <- frozen renderer (OPEN: packaging w/ David)
roms\awe32.raw
ALPHA_1\... <- game content (incl. the WATTCP.CFG files)
net-boot\... <- ODI/packet drivers, NET.CFG @340/INT10
deploy\
configure.ps1 <- from deploy/
npcap.exe <- bundled Npcap silent installer (NOT in repo)
vc_redist.x64.exe <- optional, if the DOSBox build needs it
```
`postinstall.bat` derives `ROOT` as the single folder beside it, so the folder
can be named per package.
## What runs at install (elevated, no network -- air-gapped)
1. `postinstall.bat` locates `ROOT`, runs `ROOT\deploy\npcap.exe /S`.
2. `configure.ps1 -Root ROOT`:
- finds the one static IPv4 (the **bay IP**) + its adapter GUID;
- `realnic` = a contiguous, letter-leading fragment of a single GUID block
(DOSBox reads a leading digit as an interface index; the fragment must be a
substring of `\Device\NPF_{GUID}`);
- **game IP = bay IP + 100** on the last octet (≤32 pods, bays `.1-.100`);
- `macaddr` = `02:00:<game IP octets in hex>` (stable, locally-administered);
- renders `*.conf.tmpl` -> `*.conf` (tokens filled);
- stamps `my_ip = gameIP` into every `ALPHA_1\...\WATTCP.CFG`.
3. `pod-launch.exe` (the supervisor) then selects + launches the rendered conf.
Verified against a scratch package tree: NIC detect, `realnic` (contiguous-match
checked), `bayIP+100`, MAC, conf render, and WATTCP stamp all correct.
## Building the zip
```
deploy\package.ps1 [-PackageName TeslaPod410] [-Npcap <installer>] `
[-Renderer <exe>] [-VcRedist <exe>] [-NoContent] [-SkipBuild]
```
Publishes `pod-launch` self-contained, stages the tree (postinstall.bat at the
zip root + the package folder), and writes `emulator\dist\<PackageName>.zip`.
`-NoContent` skips `ALPHA_1`/`net-boot` for a fast structural check. Externally-
procured pieces are bundled only when their paths are passed (else flagged).
## Not yet in the repo / OPEN
- `npcap.exe` (licensed installer -- procured out-of-band) and any `vc_redist`.
- The frozen `renderer.exe` (packaging decision with David; `pod-launch` falls
back to `pyw live_bridge.py` when absent).
- **DOSBox-X runtime DLL closure** (MinGW/SDL DLLs): none sit beside
`dosbox-x.exe` in the dev tree -- it finds them via MSYS2 on PATH. The air-
gapped package must bundle them; `package.ps1` copies any DLLs beside the exe
and warns when there are none.
- RP loop-vs-single-shot for retail parity (template currently mirrors dev).
+86
View File
@@ -0,0 +1,86 @@
# configure.ps1 -- the per-pod install step, called by postinstall.bat (elevated).
# On THIS machine it: finds the NIC + its static IP (the bay IP), derives the
# DOSBox game IP = bayIP + 100, binds DOSBox's pcap to the NIC, renders the
# net_*.conf templates, and stamps the WATTCP.CFG my_ip. Air-gapped, static,
# <=32 pods. See ../DEPLOYMENT-PLAN.md.
param([Parameter(Mandatory = $true)][string]$Root)
$ErrorActionPreference = 'Stop'
function Log($m) { Write-Host "[configure] $m" }
# --- prefix length -> dotted netmask -------------------------------------
function PrefixToMask([int]$p) {
$bits = ('1' * $p).PadRight(32, '0')
$o = 0..3 | ForEach-Object { [Convert]::ToInt32($bits.Substring($_ * 8, 8), 2) }
return ($o -join '.')
}
# --- 1. the pod's NIC + bay IP (the one real static IPv4) ----------------
$ip = Get-NetIPAddress -AddressFamily IPv4 |
Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' } |
Sort-Object -Property SkipAsSource, InterfaceMetric |
Select-Object -First 1
if (-not $ip) { throw "no usable static IPv4 found on any adapter" }
$bayIp = $ip.IPAddress
$ifIndex = $ip.InterfaceIndex
$adapter = Get-NetAdapter -InterfaceIndex $ifIndex
$guid = $adapter.InterfaceGuid.Trim('{', '}')
$mask = PrefixToMask $ip.PrefixLength
Log "bay IP $bayIp/$($ip.PrefixLength) on '$($adapter.Name)' guid $guid"
# --- 2. realnic fragment. Must be a CONTIGUOUS substring of the pcap device
# name (\Device\NPF_{GUID}, hyphens included), so take it from ONE
# hyphen-delimited GUID block -- letter-leading (DOSBox reads a leading
# DIGIT as an interface index) and length >= 6 for uniqueness. Longest. --
$realnic = $null
foreach ($blk in ($guid -split '-')) {
$mm = [regex]::Match($blk, '[A-Fa-f][0-9A-Fa-f]{5,}')
if ($mm.Success -and (-not $realnic -or $mm.Value.Length -gt $realnic.Length)) { $realnic = $mm.Value }
}
if (-not $realnic) { throw "no letter-leading >=6-char realnic fragment in any block of GUID $guid" }
Log "realnic fragment = $realnic"
# --- 3. game IP = bay IP + 100 on the last octet -------------------------
$o = $bayIp.Split('.')
$last = [int]$o[3] + 100
if ($last -gt 254) { throw "bay last octet $($o[3]) + 100 = $last overflows 254; keep bays <= .100" }
$gameIp = "$($o[0]).$($o[1]).$($o[2]).$last"
Log "game IP = $gameIp (bay + 100)"
# --- 4. stable locally-administered MAC from the game IP ------------------
$mac = "02:00:{0:X2}:{1:X2}:{2:X2}:{3:X2}" -f [int]$o[0], [int]$o[1], [int]$o[2], $last
Log "macaddr = $mac"
# --- 5. gateway/nameserver: the host's real gateway if set, else the host
# itself (always up + on-subnet -> WATTCP's boot ARP resolves; never
# actually routed, the mesh is same-subnet L2) -----------------------
$gw = $null
try { $gw = (Get-NetIPConfiguration -InterfaceIndex $ifIndex).IPv4DefaultGateway.NextHop } catch { }
if (-not $gw) { $gw = $bayIp }
Log "gateway/nameserver = $gw"
# --- 6. render the conf templates (@@ROOT@@/@@REALNIC@@/@@MACADDR@@) ------
$tokens = @{ '@@ROOT@@' = $Root; '@@REALNIC@@' = $realnic; '@@MACADDR@@' = $mac }
$tmpls = Get-ChildItem -Path $Root -Filter '*.conf.tmpl' -ErrorAction SilentlyContinue
if (-not $tmpls) { throw "no *.conf.tmpl found in $Root" }
foreach ($t in $tmpls) {
$text = Get-Content -Path $t.FullName -Raw
foreach ($k in $tokens.Keys) { $text = $text.Replace($k, $tokens[$k]) }
$out = Join-Path $Root ($t.Name -replace '\.tmpl$', '')
Set-Content -Path $out -Value $text -Encoding Ascii -NoNewline
Log "rendered $($t.Name -replace '\.tmpl$','')"
}
# --- 7. stamp WATTCP.CFG my_ip = game IP (all copies the game may read) ---
$wattcp = @(
(Join-Path $Root 'ALPHA_1\REL410\BT\WATTCP.CFG'),
(Join-Path $Root 'ALPHA_1\REL410\RP\WATTCP.CFG'),
(Join-Path $Root 'ALPHA_1\VGL_LABS\THISPOD\WATTCP.CFG')
)
$lines = @("my_ip = $gameIp", "netmask = $mask", "nameserver = $gw", "gateway = $gw")
$stamped = 0
foreach ($w in $wattcp) {
if (Test-Path $w) { Set-Content -Path $w -Value $lines -Encoding Ascii; $stamped++; Log "stamped $w" }
}
if ($stamped -eq 0) { throw "no WATTCP.CFG found under $Root\ALPHA_1" }
Log "OK: bay=$bayIp game=$gameIp mac=$mac realnic=$realnic ($stamped WATTCP.CFG)"
+58
View File
@@ -0,0 +1,58 @@
# DEPLOY TEMPLATE -- rendered to net_loop.conf by deploy/configure.ps1 at install.
# Tokens (filled by postinstall): @@ROOT@@ = package dir (C:\games\<folder>),
# @@REALNIC@@ = this pod's NIC GUID fragment, @@MACADDR@@ = stable derived MAC.
# BattleTech, GO.BAT-style netnub loop (pod stays connected + picks up missions).
[sdl]
output=opengl
priority=highest,highest
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[ne2000]
ne2000=true
nicbase=340
# IRQ 10, NOT 3: COM2 (plasma) owns IRQ 3 in-game.
nicirq=10
macaddr=@@MACADDR@@
backend=pcap
[ethernet, pcap]
realnic=@@REALNIC@@
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[serial]
serial1=directserial realport:COM1 rxpollus:100 rxburst:16
serial2=directserial realport:COM2
[autoexec]
mount c "@@ROOT@@\ALPHA_1"
mount d "@@ROOT@@\net-boot"
d:
echo === loading NE2000 packet-driver stack (pcap/bridge, port 340) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \rel410\bt
set VIDEOFORMAT=svga
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r f s p
echo === entering GO.BAT-style netnub loop (waits for console missions) ===
call d:\loop.bat
+63
View File
@@ -0,0 +1,63 @@
# DEPLOY TEMPLATE -- rendered to net_rp.conf by deploy/configure.ps1 at install.
# Tokens (filled by postinstall): @@ROOT@@ = package dir (C:\games\<folder>),
# @@REALNIC@@ = this pod's NIC GUID fragment, @@MACADDR@@ = stable derived MAC.
# Red Planet 4.10, networked pod boot (console pushes the mission egg over TCP).
# NOTE: single-shot netnub here (matches dev net_rp.conf); whether RP should loop
# like BT for retail-parity is an OPEN deploy decision (see DEPLOYMENT-PLAN.md).
[sdl]
output=opengl
priority=highest,highest
[dosbox]
memsize=32
machine=svga_s3
[cpu]
core=dynamic
cputype=pentium
cycles=max
[ne2000]
ne2000=true
nicbase=340
nicirq=10
macaddr=@@MACADDR@@
backend=pcap
[ethernet, pcap]
realnic=@@REALNIC@@
[sblaster]
sbtype=sb16
sbbase=220
irq=5
dma=1
hdma=5
[mixer]
rate=44100
blocksize=1024
prebuffer=60
[serial]
serial1=directserial realport:COM1 rxpollus:100 rxburst:16
serial2=directserial realport:COM2
[autoexec]
mount c "@@ROOT@@\ALPHA_1"
mount d "@@ROOT@@\net-boot"
d:
echo === loading NE2000 packet-driver stack (pcap/bridge, port 340) ===
d:\lsl
d:\ne2000
d:\odipkt
c:
cd \rel410\rp
set VIDEOFORMAT=svga
set BLASTER=A220 I5 D1 H5 P330 T6
c:\sb16\diagnose /s
set BLASTER=A240 I7 D3 H6 P300 T6
c:\sb16\diagnose /s
set BLASTER=A220 I5 D1 H5 P330 T6
set TEMP=c:\
set HEAPSIZE=15000000
set L4GAUGE=640x480x16
call setenv.bat r f s p
echo === launching Red Planet via NetNub (RIO + sound; waits for console) ===
32rtm.exe -x
netnub -p -f rpl4opt > nn.log
32rtm.exe -u
echo === RP-NET-RUN-DONE ===
pause
+93
View File
@@ -0,0 +1,93 @@
# package.ps1 -- assemble the deployable pod zip from repo sources + a fresh
# self-contained pod-launch publish. Output: <OutDir>\<PackageName>.zip whose
# ROOT holds postinstall.bat + the single package folder (extract to C:\games).
# See README.md for the tree. Externally-procured pieces (Npcap, frozen renderer)
# are bundled only if their paths are passed; otherwise they're flagged missing.
param(
[string]$PackageName = "TeslaPod410",
[string]$OutDir,
[string]$Npcap, # bundled Npcap silent installer -> deploy\npcap.exe
[string]$Renderer, # frozen renderer exe -> renderer.exe
[string]$VcRedist, # optional VC++ runtime -> deploy\vc_redist.x64.exe
[switch]$NoContent, # skip ALPHA_1 + net-boot (fast structural test)
[switch]$SkipBuild # reuse the last pod-launch publish
)
$ErrorActionPreference = 'Stop'
function Log($m) { Write-Host "[package] $m" }
function Warn($m) { Write-Warning "[package] $m" }
$Repo = (Resolve-Path "$PSScriptRoot\..\..").Path
$Emu = Join-Path $Repo 'emulator'
if (-not $OutDir) { $OutDir = Join-Path $Emu 'dist' }
New-Item -ItemType Directory -Force $OutDir | Out-Null
# --- 1. build pod-launch.exe (self-contained single-file, no runtime needed) --
$plProj = Join-Path $Emu 'pod-launch\pod-launch.csproj'
$plOut = Join-Path $Emu 'pod-launch\bin\pkg'
if (-not $SkipBuild) {
Log "publishing pod-launch (self-contained win-x64)..."
dotnet publish $plProj -c Release -r win-x64 --self-contained true `
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true `
-o $plOut | Out-Null
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed" }
}
$podLaunch = Join-Path $plOut 'pod-launch.exe'
if (-not (Test-Path $podLaunch)) { throw "pod-launch.exe not found at $podLaunch" }
# --- 2. stage the tree --------------------------------------------------------
$stage = Join-Path $OutDir '_stage'
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
$root = Join-Path $stage $PackageName
New-Item -ItemType Directory -Force (Join-Path $root 'deploy') | Out-Null
New-Item -ItemType Directory -Force (Join-Path $root 'roms') | Out-Null
# postinstall.bat -> zip ROOT (beside the package folder)
Copy-Item (Join-Path $Emu 'deploy\postinstall.bat') $stage
# supervisor
Copy-Item $podLaunch $root
# DOSBox-X + its DLLs
$dbDir = Join-Path $Emu 'src\src'
Copy-Item (Join-Path $dbDir 'dosbox-x.exe') $root
$dlls = Get-ChildItem (Join-Path $dbDir '*.dll') -ErrorAction SilentlyContinue
if ($dlls) { $dlls | ForEach-Object { Copy-Item $_.FullName $root } ; Log "copied $($dlls.Count) DLL(s) beside dosbox-x.exe" }
else { Warn "no DLLs beside dosbox-x.exe -- verify the MinGW/SDL runtime DLLs the build needs are present" }
# conf templates + configure.ps1
Copy-Item (Join-Path $Emu 'deploy\net_loop.conf.tmpl') $root
Copy-Item (Join-Path $Emu 'deploy\net_rp.conf.tmpl') $root
Copy-Item (Join-Path $Emu 'deploy\configure.ps1') (Join-Path $root 'deploy')
# AWE32 ROM
Copy-Item (Join-Path $Emu 'roms\awe32.raw') (Join-Path $root 'roms')
# game content + packet drivers (large)
if ($NoContent) {
Warn "-NoContent: skipping ALPHA_1 + net-boot (structural test only, NOT installable)"
} else {
Log "copying ALPHA_1 game content (large)..."
Copy-Item (Join-Path $Repo 'ALPHA_1') $root -Recurse
Copy-Item (Join-Path $Emu 'net-boot') $root -Recurse
}
# renderer (frozen exe) -- else flagged
if ($Renderer) { Copy-Item $Renderer (Join-Path $root 'renderer.exe'); Log "bundled renderer -> renderer.exe" }
else { Warn "no -Renderer: renderer NOT bundled (freeze pending). pod-launch needs --renderer or a bundled render-bridge at runtime." }
# Npcap installer -- else flagged
if ($Npcap) { Copy-Item $Npcap (Join-Path $root 'deploy\npcap.exe'); Log "bundled Npcap -> deploy\npcap.exe" }
else { Warn "no -Npcap: installer NOT bundled. postinstall will warn + the pcap bridge won't work until Npcap is present." }
if ($VcRedist) { Copy-Item $VcRedist (Join-Path $root 'deploy\vc_redist.x64.exe'); Log "bundled VC++ runtime" }
# --- 3. zip -------------------------------------------------------------------
$zip = Join-Path $OutDir "$PackageName.zip"
if (Test-Path $zip) { Remove-Item $zip -Force }
Log "compressing -> $zip"
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $zip
Remove-Item $stage -Recurse -Force
$size = "{0:N1} MB" -f ((Get-Item $zip).Length / 1MB)
Log "DONE: $zip ($size)"
Log "zip root = postinstall.bat + $PackageName\ (TeslaLauncher extracts to C:\games, runs postinstall.bat elevated)"
+46
View File
@@ -0,0 +1,46 @@
@echo off
setlocal enabledelayedexpansion
REM ==========================================================================
REM postinstall.bat -- runs ELEVATED after TeslaLauncher extracts the zip to
REM the games root (C:\games). The zip root holds this file + ONE package
REM folder; this script finds that folder, installs the bundled Npcap, and
REM hands off to deploy\configure.ps1 for the NIC bind / IP derivation / conf
REM rendering. Air-gapped: nothing is downloaded. See DEPLOYMENT-PLAN.md.
REM ==========================================================================
echo [postinstall] starting
REM --- locate ROOT: the single package folder beside this script -----------
set "HERE=%~dp0"
set "ROOT="
for /d %%D in ("%HERE%*") do set "ROOT=%%~fD"
if not defined ROOT (
echo [postinstall] ERROR: no package folder found beside postinstall.bat
exit /b 1
)
echo [postinstall] ROOT=!ROOT!
REM --- 1. Npcap silent install (bundled OEM-class installer supporting /S) --
set "NPCAP=!ROOT!\deploy\npcap.exe"
if exist "!NPCAP!" (
echo [postinstall] installing Npcap silently...
"!NPCAP!" /S
if errorlevel 1 ( echo [postinstall] ERROR: Npcap install failed & exit /b 1 )
) else (
echo [postinstall] WARNING: bundled Npcap not found at "!NPCAP!"
echo [postinstall] the pcap bridge will not work without it
)
REM --- optional: bundled VC++ runtime, if present --------------------------
set "VCREDIST=!ROOT!\deploy\vc_redist.x64.exe"
if exist "!VCREDIST!" (
echo [postinstall] installing VC++ runtime...
"!VCREDIST!" /install /quiet /norestart
)
REM --- 2-4. NIC bind + WATTCP my_ip=bayIP+100 + render confs (PowerShell) ---
echo [postinstall] configuring network + rendering confs...
powershell -NoProfile -ExecutionPolicy Bypass -File "!ROOT!\deploy\configure.ps1" -Root "!ROOT!"
if errorlevel 1 ( echo [postinstall] ERROR: configure.ps1 failed & exit /b 1 )
echo [postinstall] done
exit /b 0