render-bridge/freeze.ps1: PyInstaller onefile of live_bridge.py + Dave's vrboard/vrview/vrview_gl (+glcontext collected for moderngl) -> dist\ renderer.exe (~33 MB). GL backend live-verified from a pure-dist launch (no --root/--renderer overrides) on 2026-07-10. package.ps1 bundles the frozen exe automatically when present; DEPLOYMENT-PLAN's renderer OPEN item is resolved. Ignore the build-artifact dirs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
216 lines
12 KiB
Markdown
216 lines
12 KiB
Markdown
# 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_OPEN`s 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 -- **RESOLVED 2026-07-10: frozen to one exe** (PyInstaller
|
|
onefile, `render-bridge\freeze.ps1` -> `dist\renderer.exe`, ~33 MB, GL
|
|
backend live-verified from a pure-dist launch). `package.ps1` bundles it
|
|
automatically when present.
|
|
- `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, `CreateProcess`es 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 `start`ed
|
|
/ `Start-Process`ed 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`.
|