Files
TeslaRel410/emulator/DEPLOYMENT-PLAN.md
T
CydandClaude Fable 5 33a192a1fc Deploy: Npcap is a one-time MANUAL install per cockpit (operator consensus)
Retire the bundled-silent-installer requirement: postinstall now detects an
existing Npcap service and moves on, warns clearly when absent, and still
honors a bundled deploy\npcap.exe if one is shipped. package.ps1 no longer
flags the missing bundle as a warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:49:36 -05:00

12 KiB

Pod-bay deployment plan (network-agnostic archive)

How the two emulated titles (BattleTech + Red Planet, the only DOSBox games) ship into the existing TeslaConsole / TeslaLauncher pod-bay architecture as a self-contained, air-gapped, statically-addressed install. Everything here is settled unless marked OPEN.

Built so far: the supervising launch entry-point (pod-launch/, C# Job-Object supervisor -- kill-cascade verified) and the install-side artifacts (deploy/: postinstall.bat, configure.ps1, net_*.conf.tmpl -- rendering + NIC-bind + bayIP+100 + WATTCP-stamp verified against a scratch tree). Remaining build work is in the OPEN list.

The existing contract (do NOT change it)

  • A pod bay is a Windows PC on a self-contained, air-gapped network (no venue LAN, no internet) shared by the console + up to 32 pods (~140 units left worldwide -- a bounded preservation fleet, not a scaling problem).
  • All addresses are static, assigned by TeslaConsole. No DHCP.
  • TeslaConsole -> TeslaLauncher: transmits a zip; the zip has ONE folder + a postinstall.bat at its root; TeslaLauncher extracts to the games root (C:\games almost always) and runs postinstall.bat elevated.
  • TeslaConsole registers/invokes the launch -- it tells TeslaLauncher to execute a command in the game dir with args (args select game/mode). postinstall.bat does NOT register anything.
  • TeslaLauncher also STOPS the app -- e.g. on a console "kill" call when a DOSBox hangs. Its control path must stay alive independent of DOSBox.
  • Only BT/RP 4.10 are emulated (DOSBox); the rest of the catalog runs native (preservation project). For every native title, TeslaLauncher and the game share the bay's single IP as one process space; these two are the only exception, because the game's TCP stack lives inside DOSBox, separate from the host.

Network model -- THE core of this plan

Why a bridge, not slirp/NAT (settled, source-proven)

The pods form a direct peer-to-peer TCP mesh, not star-through-console: each pod TCP_OPENs to the other pods' real IPs from the egg roster (MasterMode/SlaveMode decides who dials), tracked separately from the console stream. Evidence: NETNUB_TCP_OPEN ("Opens a TCP stream to another computer", NETNUB.HPP), OpenConnection(...,int internet_address), NetworkStartupMode{SlaveMode,MasterMode}, numberOfMungaHostsConnected vs numberOfConsoleHostsConnected (L4NET.HPP). See memory pod-multiplayer-mesh. => NAT can't carry a pod dialing a same-subnet peer, so the emulated NIC must be a real bridged host on the segment.

Two IPs per bay -- launcher keeps the bay IP; the DOS game gets bayIP+100

The launcher's IP never moves. The DOSBox guest gets a second address so it can be a first-class host without stealing the bay IP:

Role Address Owner Used for
Bay / control IP e.g. 10.0.0.5 (static, console-assigned) host Windows / TeslaLauncher zip, launch, kill -- unchanged from native titles; alive even if DOSBox hangs
Game IP bayIP + 100 = 10.0.0.105 DOSBox guest (bridged, own emulated-NIC MAC) egg identity, mesh peer target, :1501

Both live at once on the one physical NIC (host MAC = bay IP, emulated NE2000 MAC = game IP). The +100 offset on the last octet is safe forever at this scale: 32 bays fit in .1-.32, games in .101-.132, no wrap, no collision.

The +100 convention -- two edits, both derive the same value

  1. postinstall.bat reads the host's static IP and stamps the guest WATTCP.CFG my_ip = bayIP+100 (netmask 255.255.255.0, gateway harmless -- air-gapped, no routing; the mesh is all same-subnet L2).
  2. TeslaConsole -- a per-title "runs in DOSBox" flag. When set, apply +100 at the single point where the console resolves a bay to its game endpoint, so BOTH inherit it:
    • the roster entries written into the egg (peers dial +100), AND
    • the address the console itself dials to deliver the egg / poll StateQuery / send StopMission (bayIP+100:1501, not the bay IP -- else the SYN hits the launcher, not the DOS game).

Blanket +100 is valid because every host in a BT/RP egg is itself a DOSBox pod. Only the BT/RP 4.10 engine is emulated, and ALL of its roles -- cockpit, camera ship, mission review -- are that same one DOSBoxed binary; every other catalog title runs native. So a BT/RP mission roster is entirely emulated and takes +100 uniformly. The console's own address is never a roster entry (pods reply on the console's inbound socket), so it's never offset.

What this retires (dev-only artifacts, gone in deployment)

SendToRxAdapters, the Windows Network Bridge, the two-TAP setup, and tap2_mirror.py all existed ONLY because dev co-located the console on the pod PC and had to loop pcap frames back to the host stack. On the real air-gapped segment the console is a separate machine and hears every pod over the wire. Also gone: the machine-specific realnic=DB5521D GUID (postinstall binds the one NIC) and the 200.0.0.x hardcoding (postinstall stamps the assigned IPs).

Co-located smoke-test caveats (all hit 2026-07-10, first dist smoke test; end-to-end egg->mission achieved same night): running TeslaConsole on the pod PC itself brings dev-era requirements back. On the real segment, with the console on its own machine, none of these apply:

  1. Npcap SendToRxAdapters must list the pod NIC's \Device\{GUID} (restart the npcap service after setting it -- the stop fails silently while DOSBox holds a capture).
  2. The host must NOT hold the game IP. A leftover static at bayIP+100 (the old bridge address) makes the console dial itself -- source = destination, the SYN never reaches the wire, SynSent forever.
  3. Disable NIC checksum offload (+ LSO) on the pod NIC (Disable-NetAdapterChecksumOffload/-NetAdapterLso). The host's OWN outbound frames are captured BEFORE the hardware fills in IP/TCP checksums; WATTCP silently discards every bad-checksum IP packet while still answering checksum-less ARP -- so ARP resolves, the NIC accepts the frames, and the pod stays dead silent at the TCP layer (netnub prints "discarding..."). This was the final console-connect blocker. Frames arriving over the real wire always carry completed checksums, so separate-machine deployments are immune.

Also learned same night, not co-location-specific: bt/rp REQUIRE sound (--no-sound strips the AWE32; the FAST SOS clock never ticks and BTL4OPT freezes in the RIO-reset busy-wait), and the pod requires a packaged renderer (pod-launch refuses to start without one unless --no-bridge; the smoke test ran with the dev tree's render bridge via --root) -- closes the "freeze renderer vs bundle Python" OPEN item in favor of: MUST ship one. Related guard: configure.ps1 refuses to guess when several candidate bay IPs survive its physical-adapter filter (-BayIp x.x.x.x disambiguates, and also allows deliberately binding a virtual adapter on a dev rig); -ConsoleIp sets the WATTCP gateway/nameserver (default: the bay IP).

The archive (self-contained -- air-gap forbids any download)

Single root folder + postinstall.bat. Everything the install needs is inside:

  • dosbox-x.exe -- a static build (182 MB; imports only Windows system DLLs, empty delay-import table), so no MinGW/SDL/VC++ DLLs to bundle; the only runtime DLL, wpcap.dll, comes from the Npcap install
  • ALPHA_1\ game content (REL410\BT, REL410\RP, SB16, GAUGE, ...) + net-boot\ drivers (LSL/NE2000/ODIPKT/NET.CFG @ PORT 340/INT 10, loop.bat)
  • Npcap silent installer (bundled) retired 2026-07-10 (operator consensus): Npcap is installed MANUALLY, once per cockpit PC -- a one-time affair that survives game updates. postinstall detects an existing install and only warns when absent (a bundled deploy\npcap.exe is still honored if shipped)
  • The renderer (OPEN: freeze to one exe vs bundle embeddable Python -- decide with David; "decide later")
  • net_*.conf templates with @@ROOT@@ / @@REALNIC@@ tokens, launch dispatcher, postinstall.bat

postinstall.bat responsibilities (elevated, one-shot, no network fetch)

Implemented in deploy/ (postinstall.bat = thin wrapper; configure.ps1 does 2-4):

  1. Check Npcap (manual-install policy, 2026-07-10): detect the existing service and move on; warn clearly if absent. Silent /S install only if a bundled deploy\npcap.exe was shipped.
  2. Bind the NIC: pick the one active adapter, write a safe letter-leading realnic= fragment (a contiguous substring of ONE GUID block, so it matches \Device\NPF_{GUID}; avoids the leading-digit-as-index trap).
  3. Stamp game identity: WATTCP.CFG my_ip = <host static IP> + 100 in every ALPHA_1\...\WATTCP.CFG, plus a stable macaddr = 02:00:<gameIP hex>.
  4. Relocate paths: render net_*.conf.tmpl -> net_*.conf, filling @@ROOT@@ (the package dir), @@REALNIC@@, @@MACADDR@@.

NOT done here: registering the launch entry-point (console does that), firewall rules, SendToRx, TAP, or mirror.

Launch dispatch

TeslaConsole invokes a single entry-point in the game dir with args selecting game/mode. The dispatcher maps args -> conf + renderer: bt/looped -> net_loop.conf; rp -> net_rp.conf; the "alternate games/ modes" map onto the decoded ExitCodeID set (RunBattleTech/RunRedPlanet + SinglePlayer variants, RunCamera, RunMissionReview, test patterns, TestPlasmaDisplay, ResetRIO).

The entry-point MUST be a supervising parent, not fire-and-forget -- it stays authoritative for the whole session, lifetime == the game's:

  • It creates a Windows Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, launches DOSBox-X and the render bridge into the job, then blocks on DOSBox (also doing the window focus/layout, folding in focus_dosbox.ps1).
  • Kill the entry-point -> DOSBox + renderer die with it. The kernel enforces the teardown when the job's last handle closes (the instant the entry-point process dies), so it holds even on a hard TerminateProcess -- which is exactly the hung-DOSBox case, where no cooperative shutdown can be relied on. No orphaned emulator/renderer, ever.
  • When DOSBox exits on its own (mission end), the supervisor tears down the job (renderer too) and returns DOSBox's exit code.

Ship the entry-point as a small compiled supervisor exe (C#/C++) that creates the job, CreateProcesses DOSBox + the bridge into it, and blocks on WaitForSingleObject(dosbox). It must NOT be a batch file / cmd.exe, nor use start or Start-Process -- all of those DECOUPLE from the child: a started / Start-Processed DOSBox is an independent process, and even a foreground dosbox.exe under cmd.exe is NOT killed when cmd is (Windows doesn't reap children with the parent gone, absent a job object). The current launch_pod.ps1/pod_deploy.ps1 do exactly this wrong (Start-Process + pid-files, then exit, leaving DOSBox running) and must be replaced by the supervisor.

Kill flow: console -> TeslaLauncher terminates the entry-point process -> job closes -> DOSBox + renderer die. Graceful mission-end still flows to the game as StopMission on :1501, after which the supervisor exits with DOSBox.

Provisioning rules

  • Bays static in .1-.32, game endpoints auto-fall in .101-.132, console + its special hosts elsewhere in the /24. All on one flat, air-gapped L2 segment.
  • Dumb unmanaged switch; no venue cooperation needed.

OPEN items

  1. Renderer packaging -- freeze (PyInstaller, one exe) vs embeddable Python (with David).
  2. Per-rig display RECTS (already isolated in pod_deploy.ps1; deployment variant of the window layout).

Related: NET-NOTES.md, LAUNCH.md, ENV-VARS.md, memory pod-multiplayer-mesh, pod-deployment.