3 Commits
Author SHA1 Message Date
CydandClaude Fable 5 0deb8303ee Add GAME-INTEGRATION.md: the pod-bay integration guide for new games
Specs everything a game team needs to deploy and command a title in a
Tesla pod bay: package zip + Apps.xml catalog contract, launch/watchdog
semantics, the full Munga wire spec (framing, message set, state machine,
egg envelope), the dedicated game-console escape hatch, the modern bay
address plan, LC/MR presentation roles, score-sheet printing, and RIO
board integration (native preferred over the RIOJoy shim). XP support is
documented as a nice-to-have for new games; the floor remains suite-side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 12:18:46 -05:00
CydandClaude Fable 5 8d6c1e7881 Register Descent 3 in the product catalog
Single GameClient entry (no {res}: D3 takes -width/-height, resolution
pinned to the pod main screen; no supervisor: single process). Args boot
straight into the campaign first level for phase-0 free flight; they
change to -venue once the D3_VENUE Munga build ships. LC/MR keys
...971/...972 reserved. Catalog diff tests updated (21 passing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 08:11:46 -05:00
CydandClaude Opus 5 ad6f566834 Document CTCL: what commanding FireStorm from the console would take
BT4/RP4 speak Munga (TCP 1501); BattleTech FireStorm is a MechWarrior 4
conversion and speaks CTCL instead - pod launcher on TCP 1000, game on
1001. Researched from the FireStorm sources at C:\VWE\firestorm and
written up in FIRESTORM-CTCL.md: roles, framing, message/order tables,
the mission handshake (including how the server pod is designated - the
console picks it positionally, there is no election), and the session
rendezvous by game-name GUID over LAN enumeration.

The console already launches FireStorm via Apps.xml; what it cannot do
is build or run a mission. Three blockers: the bit-packed NMP blob is
the MW4 engine's own MWNetMissionParameters serialization, the mech and
map identifiers (including a 64-bit map CRC) come from installed game
content, and CTCL carries no in-mission telemetry at all.

Decision recorded: a pure C# CTCL client is rejected (the NMP layout is
engine-internal and would need reworking on every FireStorm change);
pod-level control is unnecessary since Site Management already covers
it; teaching MW4 to accept a plain mission spec and build the NMP
pod-side is the option if this is ever picked up. Parked for now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:46:29 -05:00
5 changed files with 843 additions and 2 deletions
+21
View File
@@ -121,6 +121,27 @@
hostType="None" />
</Product>
<!-- Descent 3 (open-source engine + site-owned GOG assets; C:\VWE\Descent3
repo, venue\pack-dist.ps1 builds the package). Pod input arrives through
the RIOJoy "Descent 3" profile (RioGamepad HID), merged by the package's
postinstall. No {res} token: D3 takes -width/-height, not " -res W H" -
resolution is pinned to the pod main screen. Single process, no
supervisor; kill = terminate Descent3.exe. The args boot straight into
the campaign's first level (phase-0 free flight); when the D3_VENUE
Munga build lands, args change to "-venue -nointro{res} -framecap 60"
and LC/MR role entries get added (keys ...971/...972 reserved). -->
<Product id="B7E6D3A0-52C4-4F19-9A8E-6D40C1F2A970"
name="Descent 3"
menuText="Descent 3..."
hostTypeDialog="false">
<Launch key="B7E6D3A0-52C4-4F19-9A8E-6D40C1F2A970"
displayName="Descent 3"
exe="C:\Games\Descent3\Descent3.exe"
args="-pilot pod -nointro -nooutragelogo -fullscreen -width 800 -height 600 -framecap 60 -mission d3 -loadlevel 1"
autoRestart="true"
hostType="None" />
</Product>
<!-- TeslaRel410 - the DOSBox-X preservation pods (C:\VWE\TeslaRel410 repo).
The package (emulator\dist\TeslaPod410.zip, built by deploy\package.ps1)
extracts to C:\Games (postinstall.bat + TeslaPod410\); pod-launch.exe is
@@ -25,8 +25,8 @@ namespace TeslaConsole.DiffTests
=> _fx.Recovered.Run("CatalogEntry", new[] { _catalog, launchKey, w, h });
[Fact]
public void Catalog_Has_Five_Products_And_Fourteen_Entries()
=> Assert.Equal("products=5;entries=14",
public void Catalog_Has_Six_Products_And_Fifteen_Entries()
=> Assert.Equal("products=6;entries=15",
_fx.Recovered.Run("CatalogSummary", new[] { _catalog }));
[Fact]
@@ -101,6 +101,22 @@ namespace TeslaConsole.DiffTests
@"BattleTech 4.11 MR|f4c957fd-72f7-4c5f-8971-28095007e8d1|C:\Games\BT411\btl4.exe|-net 1501 -mr|C:\Games\BT411|True",
Entry("F4C957FD-72F7-4C5F-8971-28095007E8D1"));
// Descent 3 — open-source engine + site GOG assets (C:\VWE\Descent3 repo,
// venue\pack-dist.ps1 package). Single GameClient entry, resolution pinned
// in args ({res} absent — D3 takes -width/-height, not " -res W H").
[Fact]
public void Descent3_Matches_Expected()
=> Assert.Equal(
@"Descent 3|b7e6d3a0-52c4-4f19-9a8e-6d40c1f2a970|C:\Games\Descent3\Descent3.exe|-pilot pod -nointro -nooutragelogo -fullscreen -width 800 -height 600 -framecap 60 -mission d3 -loadlevel 1|C:\Games\Descent3|True",
Entry("B7E6D3A0-52C4-4F19-9A8E-6D40C1F2A970"));
[Fact]
public void Descent3_Resolution_Choice_Has_No_Effect()
=> Assert.Equal(
@"Descent 3|b7e6d3a0-52c4-4f19-9a8e-6d40c1f2a970|C:\Games\Descent3\Descent3.exe|-pilot pod -nointro -nooutragelogo -fullscreen -width 800 -height 600 -framecap 60 -mission d3 -loadlevel 1|C:\Games\Descent3|True",
Entry("B7E6D3A0-52C4-4F19-9A8E-6D40C1F2A970", "1024", "768"));
// TeslaRel410 — the DOSBox-X preservation pods. All six entries launch
// pod-launch.exe; the mode arg ("bt"/"rp") selects the game, LC/MR boot
// identically (the console assigns the role via the egg hostType), and
+288
View File
@@ -0,0 +1,288 @@
# Commanding FireStorm from TeslaConsole — CTCL research
Researched 2026-07-28. **Status: parked.** No code was written; this is the record of
what CTCL is, what integrating it would cost, and which option we'd take if we ever
pick it up.
BT4 and RP4 speak **Munga** (TCP 1501) — the protocol the console already drives via
the vendored `Munga Net.dll`. BattleTech **FireStorm** is a MechWarrior 4 total
conversion and speaks something else entirely: **CTCL**, the coin-op / LAN-centre
control layer the Korean team added to the MW4 engine (every call site is bracketed
`// jcem - begin` / `// jcem - end`; the acronym is never expanded in the source).
Source for everything below is the FireStorm repo at `C:\VWE\firestorm`
(`gitea.mysticmachines.com/VWE/firestorm.git`). Paths in this document are relative to
`firestorm/`. That repo's `LAUNCHER-AND-MW4.md` already documents the **launcher**
link; the **game** link (the mission handshake on port 1001) is documented here for the
first time.
---
## Where we are today
The console can already **launch** FireStorm on a pod — it is a product in
[`Console/RedPlanet/Apps.xml`](Console/RedPlanet/Apps.xml), pointing at
`C:\Games\MW4\launcher.exe`. So today a FireStorm pod runs:
```
TeslaLauncher (ours, TCP 53290)
└─ launcher.exe (MW4's CTCL agent, listens TCP 1000)
└─ MW4.exe -ctcltype 2 (the game, listens TCP 1001) ← started from c:\ctcl.ini [config] run=
```
What the console **cannot** do is build or run a mission. That is the gap.
---
## CTCL in one page
### Roles (`Gameleap/code/ctcls/ctcl_params.h:28-33`)
| Value | Role | Who | Listens |
|-------|------|-----|---------|
| `_ECTCL_Launcher` = 0 | pod agent | `Launcher.exe` | 1000 |
| `_ECTCL_Console` = 1 | operator console | `MW4.exe` with **no** `-ctcltype` | — (dials out) |
| `_ECTCL_Game` = 2 | pod running the game | `MW4.exe -ctcltype 2` | 1001 |
| `_ECTCL_CameraShip` = 3 | spectator / camera pod | `MW4.exe -ctcltype 3` | 1001 |
| `_ECTCL_None` = 4 | CTCL disabled | `MW4.exe -dragon` | — |
The console is *the same binary as the game*. It reads `c:\ctcl.ini` `[teslas]` for the
pod list and opens two sockets per pod (1000 + 1001). Pods listen; the console never does.
### Framing (`Gameleap/code/Launcher/mugSocs.cpp`)
A tiny hand-rolled "MUG" socket library. Frame is:
```
uint16 length (big-endian, = 1 + payload) uint8 cmd payload…
```
Payload fields are described by printf-style format strings
(`CPacket::vAssemble`, `mugSocs.cpp:1078`):
| code | meaning |
|------|---------|
| `b` `B` | uint8 |
| `w` `W` | uint16, network byte order |
| `n` `d` `D` | uint32, network byte order |
| `f` / `F` / `6` | float / double / int64 (host order) |
| `s` | NUL-terminated string, inline |
| `S` | uint16 length + NUL-terminated string |
| `x` | uint16 length + raw bytes |
| `X` | raw bytes, no length prefix |
### Messages (`Gameleap/code/Launcher/ctcl.h:73-91`)
| id | name | direction | notes |
|----|------|-----------|-------|
| 1 | `C_GameInfo` / `S_GameInfo` | console ↔ launcher (:1000) | poll ~1 Hz; reply is `applType, applState, gameState, gameTime, isServer` |
| 10 | `C_OrderAppl` | console → launcher **or** game | routed by value: **≥100 → :1000, <100 → :1001** |
| 19 | `C_ErrorStartGame` | console → all | abort, back to main menu |
| 20 | `C_ReadyStartGame` | console → each pod (:1001) | the big one — NMP blob + that pod's player record |
| 21 | `C_BOTS` | console → **server pod only** | full roster incl. bots; doubles as "you are the host, create it" |
| 22 | `C_SetMechs` | console → **clients only** | "join now" |
| 23 | `C_GetReady` | — | **declared but dead** — no handler in any packet map |
| 24 | `C_DoLaunch` | console → server | drop |
| 30 | `S_GameReturn` | game → console | progress / error, `_EGR_*` codes |
| 40 | `C_SendFile` / `S_SendFile` | both ways | print + mission-review file *notifications* only |
| 50 | `C_InviteCOOP` / `S_InviteCOOP` | both ways | coin-op invite; irrelevant to console-run games |
### Orders (`ctcl_params.h:40-46`)
| order | value | handled by | effect |
|-------|-------|-----------|--------|
| `_CTCL_Order_Terminate` | 0 | game | `gos_TerminateApplication()` |
| `_CTCL_Order_EndMission` | 1 | game | drop out of the mission |
| `_CTCL_Order_Launch` | 100 | launcher | `RunExec()` the `c:\ctcl.ini` `[games]` command line |
| `_CTCL_Order_Shutdown` | 101 | launcher | `ExitWindowsEx(EWX_POWEROFF)` |
| `_CTCL_Order_Reboot` | 102 | launcher | `ExitWindowsEx(EWX_REBOOT)` |
| `_CTCL_Order_Unload` | 103 | launcher | `PostQuitMessage(0)` — quit the launcher |
### Pod state (`ctcl_params.h:1-26`)
The launcher has no visibility into the game process; it reads five ints out of
`ctcls.dll`'s `.SHARED_DATA` section (a process-spanning shared segment the game writes):
`applType` (`_EAT_MW4`), `applState` (PreLaunch/Launched/PostLaunch),
`gameState` (Idle/Preparing/Running/Closing), `gameTime` (seconds), `isServer`.
---
## The mission handshake
### Who hosts — the console decides, there is no election
While validating the player list (`Gameleap/code/mw4/Code/MW4/MW4Shell.cpp:13492-13504`)
the console records the first cameraship pod (`:13421`) and the first ordinary pilot pod
(`:13432`), then:
```cpp
if (nCameraship != -1) g_nServer = nCameraship; // cameraship hosts if one is playing
else g_nServer = n1stTesla; // otherwise the topmost pilot pod
```
**The server designation is positional** — whatever pilot ordering the operator UI shows
*is* the host-selection UI. The original console offered no explicit control over it.
Each pod learns its role from one byte in `C_ReadyStartGame`
(`mw4/Code/MW4Application/ctcl.cpp:1544`); the receiver sets `g_bIsServer = (bType == 1)`
(`:822`). The server's IP travels in the same packet but is discarded on arrival
(`MW4Shell/MWApplication.cpp:18534``PI.m_dwAddr = 0; // dwAddr;`).
### Rendezvous is by name, not address
The console generates `g_guidGameDatas = gos_GenerateUniqueGUID()` per mission and ships
it in `C_ReadyStartGame`; every pod formats it identically into `g_szGameName`
(`ctcl.cpp:824-827`).
- **Server** (`CTCL_DoCreateGame`, `MW4Application.cpp:1708`): `gos_NetStartGame`
`PreConnect``CTCL_DefaultHostSetup(0)``Mech4CreateGame(g_szGameName, pilot, NULL)`.
`AdvertiseThisGame = 0` — LAN enumeration only, never published to the Zone.
- **Clients** (`CTCL_DoJoinGame` `:1745``CTCL_CheckJoinGame` `:1768`): open the LAN
browser, poll `gos_GameIsExist(g_szGameName)` until it appears, then
`gos_JoinGame(...)`. 25-second timeout.
### The sequence (`mw4/Code/MW4Application/ctcl.cpp:1486-1670`, state var `g_nMech4Comm`)
Its whole purpose is to **serialize create-before-join** — a client that browses before
the host exists just burns its timeout.
| console sends | waits for | pod does |
|---|---|---|
| `C_ReadyStartGame` → all pods | every pod → `_EGR_PreparingStarted` | stores roster + NMP |
| `C_BOTS`**server only** | server → `_EGR_OkCreateSession` | `CTCL_DoCreateGame` |
| `C_SetMechs`**clients only** | server → `_EGR_OkLaunchReady` | `CTCL_DoJoinGame`, browse + join |
| *(operator presses Launch)* | | |
| `C_DoLaunch` → server | | everyone drops |
Failures come back as `_EGR_ErrCreateSession` / `_EGR_ErrJoinSession` on `S_GameReturn`;
the console responds by broadcasting `C_ErrorStartGame` (`ctcl.cpp:1520-1533`).
---
## The three blockers
### 1. The NMP blob
`C_ReadyStartGame` carries an opaque byte array the console produces by calling the MW4
engine's own `MWNetMissionParameters::SaveParameters()`
(`mw4/Code/MW4/MWApplication.cpp:749-864`) — a **bit-packed** stream: ~60 fields at
widths of 1, 2, 3, 5, 7, 8, 16, 32 and 64 bits, then 8 × `TeamParameters` (`:513-535`,
another 10 × 32-bit allow-masks each), a byte-align, then eight length-prefixed strings.
Reproducing it in C# means porting `Stuff::DynamicMemoryStream::WriteBits` bit-for-bit
and getting every field width right. **A one-bit drift silently corrupts every field
after it**, and the layout changes whenever the FireStorm codebase adds a parameter — the
existing `// MSL 5.05 Advance Mode` / `// MSL 5.06 Armor Mode` markers in that function
are exactly that happening twice already.
### 2. Content-derived identifiers
The CTCL console is a full MW4 install because it needs game data:
- `m_nMechIndex` / `m_fileID` / `m_recordID` come from the console's own sorted mech
resource table (`shl->m_mechIDs`, built from the installed `.erf` content).
- `m_mapID` indexes the console's `scenarios[]` table.
- `m_mapClientCRC` is a **64-bit CRC of the map's `.mw4` file**
(`MWApplication.cpp:7048`) which every pod recomputes and compares (`:7538-7553`);
a mismatch makes the pod decide it doesn't have the map.
So console and pods must run byte-identical content, and any external console needs that
table regenerated per FireStorm build.
### 3. No telemetry
CTCL gives the console `gameState` and `gameTime` (seconds) and **nothing else** — no
kills, no damage, no scores. The whole `BTGame`/`RPGame` model (live scoreboard,
`BTMissionRecorder`, `BTPrintDocument`) has no CTCL equivalent.
FireStorm results instead land as files on the pod: the game writes
`{guid}.pr` (print) and `{guid}.mr` (mission review) into `\mw4files`
(`mw4/Code/MW4/recscore.cpp:246-249`). `S_SendFile` is only a *notification*; the console
reads the file over SMB as `\\<pod>\mw4files\{guid}.pr` and hands it to the `mw4print`
helper via `WM_COPYDATA`. Mission review is replayed by sending the path to the
cameraship pod flagged `m_bMissionReview`.
Also free, for the same reason: **plasma displays and the RIO board are the game's own
business** on FireStorm pods (`PLASMA_Do`, `mw4/Code/MW4/CRIOMAIN.CPP:468`, gated by
`-noplasma`). The console does not drive them, unlike BT4/RP4.
---
## Options considered, and the decision
**A — Pure C# CTCL client in TeslaConsole.** Port the protocol *and* the bit-packed NMP
serializer *and* a per-build content-table extractor. No game-side change.
**Rejected:** the NMP layout is engine-internal and unversioned, so this would need
reworking every time the FireStorm codebase changes. Maintenance nightmare for the
value returned.
**B — Teach MW4 a console-friendly protocol.** We have a working VC6 build environment for
FireStorm (`build-env/`, with a verified Release build in `rel.bin/`). Add a message on
:1001 that accepts a plain mission spec (map name, rules, per-player mech names) and let
the *server pod* build the NMP locally with the engine's own serializer. The console then
never touches bit-packing, map CRCs or resource IDs.
**Viable — this is the option if we ever do it.** Cost is a FireStorm rebuild and a
redeploy to every pod.
**C — Pod-level control only** (`C_GameInfo` + `C_OrderAppl`: state, launch, terminate,
end mission, shutdown, reboot).
**Not needed.** Site Management already gives us reboot/shutdown/launch/kill through our
own TeslaLauncher; those same functions being built into MW4's `launcher.exe` is
redundant with what we already use. And if mission parameters still have to be set on the
MW4 console, there is no reason to come back to the TeslaConsole just to start and stop
the mission — the operator is already sitting at the machine that can do it.
---
## If B is ever picked up
Rough shape of the work:
1. **Game side (C++/VC6).** New packet id on :1001 carrying a text/JSON mission spec.
Handler calls the existing `CTCL_DefaultHostSetup` + the `MW4Shell` parameter setters
(`MAP_ID_PARAMETER` etc. — they already recompute `m_mapClientCRC` and the mech
ResourceIDs locally), then enters the normal `g_nMech4Comm` path. Reuse
`C_ReadyStartGame`/`C_BOTS`/`C_SetMechs`/`C_DoLaunch` unchanged between pods.
Remember: `ctcl.cpp` exists as **four hand-copied siblings** (Launcher,
MW4Application, MW4GameEd2, Tools\AnimScript) — change every copy.
2. **Console side (C#).** A `CtclGame` sibling to
[`Console/TeslaConsole/MungaGame.cs`](Console/TeslaConsole/MungaGame.cs) — same
connect/poll/own shape, ~400 lines — plus a `FSGame` pane modelled on `BTGame`, minus
everything telemetry-driven. `Pod`/`Site`/`AppRegistry` and the TeslaLauncher RPC are
already protocol-agnostic and need no change.
3. **vPOD.** A CTCL mode: listen on 1000/1001, answer `C_GameInfo`, walk the `_EGR_*`
ladder. Without it none of this is testable off-cockpit.
4. **Results.** Decide whether to parse `.pr` files ourselves or keep shelling out to
`mw4print`.
### Open questions
- **Which FireStorm build is canonical?** `FS Build V4H`, `FS507C_20160909` and
`FS507D_20161015` are all on this machine; content tables and map CRCs differ per build.
- **`ctcl.ini` ownership.** CTCL's pod list is hardcoded to `c:\ctcl.ini` and uses the
2005-era `200.0.0.x` scheme; TeslaConsole owns pod addressing via `Site`. Either the
console writes `ctcl.ini` at Install Product time, or the pod list becomes console-side
only (pods only ever read `[games]` + `[config]`).
- **Server designation UI.** Positional today; worth making explicit if pods differ in spec.
- **Known shipped bug** (`Gameleap/code/Launcher/ctcl.cpp:1310`): the remote-launch table
looks up the cameraship command under key `#1`, but every shipped ini writes `*1`. A
console `Launch` aimed at a cameraship pod silently does nothing. Cameraships were
evidently autostarted, not console-launched.
- `_EAT_RP` = 2 exists in `ctcl_params.h` alongside `_EAT_MW4` = 1 — CTCL had a Red Planet
application type. Vestigial as far as this repo is concerned; the second `[games]` slot.
## Source map
| Thing | Where (relative to `C:\VWE\firestorm`) |
|-------|------|
| Protocol + manager, game copy | `Gameleap/code/mw4/Code/MW4Application/ctcl.cpp` |
| Protocol + manager, launcher copy | `Gameleap/code/Launcher/ctcl.cpp` (built `/D CTCL_LAUNCHER`) |
| Constants (roles, orders, states) | `ctcl_params.h` (four copies) |
| Message ids + structs | `ctcl.h` |
| Socket library / framing | `Gameleap/code/Launcher/mugSocs.cpp`, `mugsocs.h` |
| Shared-state DLL | `Gameleap/code/ctcls/ctcls.cpp` |
| NMP serialization | `Gameleap/code/mw4/Code/MW4/MWApplication.cpp:749-864` |
| Session create/join | `Gameleap/code/mw4/Code/MW4Application/MW4Application.cpp:1708-1800` |
| Console mission build | `Gameleap/code/mw4/Code/MW4/MW4Shell.cpp:13369-13548` |
| Console UI (shell script) | `Gameleap/mw4/Content/ShellScripts/ConLobby.script` |
| Launcher link (already documented) | `LAUNCHER-AND-MW4.md` |
+510
View File
@@ -0,0 +1,510 @@
# Deploying and commanding a game in a Tesla pod bay
**Audience:** developers of games being brought to the Tesla cockpit pods —
current and future titles alike. This is the integration
contract from the game's point of view: what your package must look like so the
operator console can **deploy** it to pods, and what your executable must speak
so the console can **command** it through a mission.
Everything here is implemented and validated in this repo (TeslaSuite v4.11.4.x)
— file references point at the authoritative source.
---
## 1. The big picture: two independent channels
A pod (cockpit PC) runs two things that matter to you:
| Channel | Port | Who listens | Purpose |
|---|---|---|---|
| **Launcher RPC** | TCP **53290** | `TeslaLauncher.exe` (pod tray app) | Deploy: install/uninstall packages, register launch entries, launch/kill your exe, volume, reboot |
| **Munga game control** | TCP **1501** | **your game exe** | Command: mission load (the "egg"), run/stop/abort/suspend/resume, state polling, in-mission events |
These are separate. The launcher channel is fully game-agnostic — any exe can be
deployed and launched with **zero code changes** to your game. The Munga channel
is what your game implements if the console is to drive missions in it.
That split gives two integration tiers:
- **Tier 0 — deploy + launch only.** The console installs your package, starts
and stops your exe, and keeps it alive (watchdog). Your game runs its own show.
Examples in the shipped catalog: BattleTech Firestorm, RIOJoy.
Requires only §2 (a package + a catalog entry).
- **Tier 1 — full mission command.** Your game is a Munga TCP server; the console
streams it a mission egg, drives the state machine, and receives scoring
events. Examples: Red Planet 4.11 (`rpl4opt.exe`), BattleTech 4.11
(`btl4.exe`), TeslaRel410 (supervisor wrapping the DOS games). Requires §2 + §3,
plus a console-side game module (§3.7).
There is one escape hatch: a game that Munga control would not serve well may
ship its own dedicated **game console** instead (§3.9) — deployment still goes
through Tier 0 unchanged.
---
## 2. Deployment spec (Tier 0 — every game needs this)
### 2.1 The package zip
The console's **Manage Site → Install Product** streams a zip to the pod; the
launcher extracts it into the games root **`C:\Games`** (the zip is opened at
that root, not inside a product folder). Lay the zip out as:
```
YourGame.zip
├── YourGame\ ← your product folder → becomes C:\Games\YourGame\
│ ├── yourgame.exe
│ ├── (data files...)
│ └── pre-uninstall.bat ← optional; run on uninstall (driver/config removal)
└── postinstall.bat ← optional; at ZIP ROOT; run once after extract, then deleted
```
Rules and lifecycle (implementation: [Launcher/TeslaLauncher.cs](Launcher/TeslaLauncher.cs),
[Launcher/MiniZip.cs](Launcher/MiniZip.cs)):
- **Everything must extract under one `C:\Games\<Product>\` folder** (plus the
optional root `postinstall.bat`). Uninstall deletes `C:\Games\<Product>`
recursively — don't scatter files elsewhere unless `postinstall.bat` puts them
there and `pre-uninstall.bat` removes them.
- **`postinstall.bat`** (zip root) runs after extraction with the launcher's
token — the kiosk account is an Administrator, so driver installs work (RIOJoy
installs ViGEmBus this way). It is waited on, then deleted.
- **`pre-uninstall.bat`** (inside your product folder) runs before the folder is
deleted on uninstall.
- Zip format: stored + deflate (+ ZIP64) — the launcher uses its own extractor
(`MiniZip.cs`), no exotic compression methods.
- Install progress reported to the operator: 050% receive, 5095% extract,
~96% postinstall, 99100% complete.
- **OS range:** pods run **Windows XP SP3 through Windows 11** on one image.
The *suite itself* is deliberately held to the XP floor (one net40 binary
set) to retain compatibility with the original cockpit hardware. For new
games, XP support is a **nice-to-have, not a requirement**: meeting it
(native exes: x86 + XP-safe API surface; .NET exes: net40 — runs in-place on
Win10/11's 4.8 runtime) lets your game reach the original-hardware pods too.
If you skip it, note modern-pods-only in your catalog entry's comment so
operators don't push it to XP-era machines.
Existing package builders to crib from: [vPOD/pack.ps1](vPOD/pack.ps1)
(minimal) and TeslaRel410's `deploy\package.ps1` (in its own repo — produces
an Install-Product-ready zip with postinstall).
### 2.2 The catalog entry (`Apps.xml`)
The console's product menu is data-driven from
[Console/RedPlanet/Apps.xml](Console/RedPlanet/Apps.xml) (parser:
[Console/TeslaConsole/AppRegistry.cs](Console/TeslaConsole/AppRegistry.cs)).
Your game ships as one `<Product>` element:
```xml
<Product id="NEW-GUID-HERE" fresh Guid; also the install key
name="Your Game" friendly name (menus, status text)
menuText="Your Game..." exact Install Product submenu text
hostTypeDialog="false"> ← "true" only if you have LC/MR roles
<Launch key="NEW-GUID-HERE" first entry reuses the product id
displayName="Your Game" name in the pod's app list
exe="C:\Games\YourGame\yourgame.exe"
args="-net 1501{res}" whatever your exe takes; see below
workingDirectory="" optional; defaults to exe's folder
autoRestart="true"
hostType="None" /> ← GameClient|LiveCamera|MissionReview|None
</Product>
```
- **Key convention** (documented in the Apps.xml header — follow it exactly):
generate ONE fresh Guid for the product id; the first `<Launch>` reuses it;
each additional `<Launch>` increments the **last hex digit** (+1, +2…,
wrapping F→0). Never append `-1`/`-2` to the string — keys parse as
`System.Guid` and a suffixed string silently collapses to `Guid.Empty`.
- **`{res}`** in `args` expands to ` -res W H` when the operator picks a custom
resolution, else to nothing. Only use it if your exe accepts `-res W H`
(the RP411 engine convention); a game with different resolution flags just
pins them in `args`.
- **`autoRestart="true"`** enables the pod watchdog (§2.3). Almost always what
you want for a game client.
- **`hostTypeDialog="true"` + per-entry `hostType`** is for games with separate
live-camera / mission-review roles (RP/BT use `-lc` / `-mr` flags) — see §4
for what those stations do. A plain game ships one entry with
`hostType="None"` and `hostTypeDialog="false"`.
- XML gotcha: comments in this file must not contain `--``XmlDocument.Load`
throws and the whole catalog comes up empty.
Registering entries on pods does **not** require reinstalling files: the
console's **Register Product on Pods** context action pushes the catalog's
launch entries to connected pods over the `InstallApp` RPC. The wire shape is
`LaunchData { LaunchPair{LaunchKey, DisplayName}, WorkingDirectory, ExeFile,
Arguments, AutoRestart }` ([Contract/WireContract.cs](Contract/WireContract.cs)).
### 2.3 Launch / kill / watchdog semantics
What the launcher does with your entry
([Launcher/TeslaLauncher.cs](Launcher/TeslaLauncher.cs)):
- **LaunchApp** starts `exe` with `args`, working directory = `workingDirectory`
or the exe's folder. Missing exe → clean "registered but not yet installed"
error at the console (register-first / install-later is supported).
- **Kill** terminates the process. Console-ordered kills stay down.
- **Watchdog:** an `autoRestart` entry whose process **exits on its own** is
relaunched ~2 s later. The original games lean on this for their per-mission
cycle: `rpl4opt`/`btl4` **terminate after each mission** and the watchdog
brings a fresh process up waiting for the next egg. That exit-and-relaunch
cycle is **not strictly required** (FireStorm doesn't do it): a game may
instead stay resident and return itself to a dark waiting state, ready for
the next group (§3.6). Either way, design your exe so a cold start goes
straight to that ready state with no menus in the way — the watchdog is
still your crash recovery.
- Your process runs in the auto-logged-in kiosk session (account `Firestorm`,
Administrator, UAC disabled) — desktop, audio, and DirectX/OpenAL are all
available. Firewall is disabled on pods; don't ship your own rules.
---
## 3. Command spec (Tier 1 — the Munga protocol)
Reference implementations, in order of usefulness:
- **[vPOD/MungaPodServer.cs](vPOD/MungaPodServer.cs)** — the framing, complete
and commented (vPOD is a working software pod; the console can't tell it from
a real one).
- **[vPOD/PodSimulator.cs](vPOD/PodSimulator.cs)** — the pod-side state machine
and egg handling.
- **[Console/TeslaConsole/MungaGame.cs](Console/TeslaConsole/MungaGame.cs)** —
the console side you're talking to.
- The typed message classes live in the vendored `Console/lib/Munga Net.dll`;
the C++ originals are in the RP411 game repo.
### 3.1 Transport
**Your game is the TCP server.** Listen on **TCP 1501**; the console connects to
`<podIP>:1501` and keeps one connection open. One console at a time (a new
connection replaces the old — see `MungaPodServer.AcceptLoop`). Convention: the
port is passed on your command line (`-net 1501`) rather than hardcoded.
### 3.2 Framing (little-endian throughout)
Every message, both directions:
```
[16-byte NetworkPacketHeader][MungaMessage]
header: int32 ClientID | int32 GameID | int32 FromHost | int32 Timestamp(ms tick)
message: int32 MessageLength | int32 MessageID | int32 Flags | body...
```
`MessageLength` counts the 12-byte message base **but not** the 16-byte header.
Messages are dispatched by **(ClientID, MessageID)** pairs.
### 3.3 Message set
Console → pod (what you must accept):
| ClientID | MessageID | Message | Your reaction |
|---|---|---|---|
| Application | 3 | `StateQuery` | reply `StateResponse(host, state, appId)` |
| Application | 4 | `CheckLoad` | (load probe) |
| Application | 5 | `RunMission` | `WaitingForLaunch → LaunchingMission → RunningMission` |
| Application | 6 | `StopMission` | `RunningMission → EndingMission → exit` (§3.6) |
| Application | 8 | `SuspendMission` | `RunningMission → SuspendingMission` (pause) |
| Application | 9 | `ResumeMission` | `SuspendingMission → ResumingMission → RunningMission` |
| Application | 10 | `LoadMission` | `WaitingForEgg → LoadingMission` |
| Application | 11 | `AbortMission` | `AbortingMission → WaitingForEgg` (no results) |
| Application | 12 | `LightsOutMission` | cockpit lights-out |
| NetworkManager | 3 | `EggFile` | egg chunk — buffer it (§3.5) |
Pod → console (what you send): `StateResponse` (answer to every `StateQuery`),
`AcknowledgeEggFile` (NetworkManager 4, once the egg is complete), and the
in-mission event messages (§3.6).
### 3.4 Identity and state
- **`ApplicationID`** — which game this pod is running, reported in every
`StateResponse`. The enum lives in `Munga Net.dll`: `RPL4 = 0` (Red Planet),
`BTL4 = 1` (BattleTech), plus `NDL4`. **A brand-new title needs a new value**
agreed with the TeslaSuite side (the console maps `ApplicationID` → game
module), or it reuses an existing one if it's a port of that game.
- **`ApplicationState`** — the cockpit state machine. The values the console
drives/observes: `InitializingState, WaitingForEgg, LoadingMission,
WaitingForLaunch, LaunchingMission, RunningMission, SuspendingMission,
ResumingMission, EndingMission, AbortingMission, CreatingMission`.
- The console polls `StateQuery` about **once per second** and gates every
operator action on your reported state. Report honestly — the console's UI
("busy, must stop first", ready-to-run, etc.) is driven entirely by it.
The normal lifecycle:
```
boot → InitializingState → WaitingForEgg
← egg streamed (console sends it when it sees WaitingForEgg)
→ LoadingMission → WaitingForLaunch (send AcknowledgeEggFile)
← RunMission
→ LaunchingMission → RunningMission
← StopMission (once, at mission end)
→ EndingMission → back to WaitingForEgg, either by:
exiting (watchdog relaunches a fresh process — the original games), or
resetting in-process to a dark waiting state
```
### 3.5 The egg (mission definition)
The console streams the mission as **`EggFileMessage` chunks of ≤1000 bytes**
(`index, totalLength, thisLength, buffer`). Reassemble by index until
`totalLength` bytes have arrived, then send **`AcknowledgeEggFileMessage`** and
move to `LoadingMission`.
Content: ASCII, INI-style sections, with every `key=value` separated by **NUL**
(`\0`) on the wire (the console builds it with `\n` separators and replaces them
before encoding). Parse by section name — section **order is not guaranteed**.
General shape (full field-by-field spec for an existing game:
[410console/battletech-port/BATTLETECH-PORT-SPEC.md](410console/battletech-port/BATTLETECH-PORT-SPEC.md) §2):
```
[mission] adventure= map= scenario= time= weather= temperature= length=...
[pilots] pilot=<podIP> ← one line per participant
[<podIP>] hostType= name= vehicle= dropzone= color= ... ← per-participant
[ordinals] 1st4th place plasma bitmaps (128×32)
[BitMap::Large::<pilot>] 128×32 pilot-name plasma bitmap
[BitMap::Small::<pilot>] 64×16 variant
```
- Participants are keyed by **pod IP**. `hostType` assigns the pod's role:
`0` = game machine, `2` = mission review / camera, `3` = console
([Console/TeslaConsole/HostType.cs](Console/TeslaConsole/HostType.cs)).
- The `[BitMap::*]`/`[ordinals]` sections are pre-rendered graphics for the
cockpit's 128×32 plasma scoreboard — the console authors them; your game just
forwards them to the plasma display if the cockpit has one.
- Egg *content* is game-specific; the envelope above (chunking, ack, NUL
delimiting, sections) is fixed.
### 3.6 Mission end, events, results
- **In-mission events** are pod → console `MungaMessage`s. Red Planet sends
`Scored / Killed / Damaged / Boost / ScoreUpdate`; BattleTech's set maps its
DamageMatrix/KillMarker model. Your game defines its own set, but the console
module (§3.7) must know how to decode it — coordinate the two.
- **Mission end / egress:** the console is the **sole mission timekeeper**. It
sends **one** `StopMission` when mission time expires. Any end-of-mission
ritual (RP/BT hold pilots in the cockpit ~30 s of egress) is the *game's* own
behavior after receiving it — the console does not send a second stop. After
egress the game must end up back in a **dark waiting state** reporting
`WaitingForEgg`, ready for the next group. The original games get there by
**exiting** — the launcher watchdog relaunches a fresh process (§2.3) and the
console reconnects — but staying resident and resetting in-process is equally
valid (FireStorm-style); the console only acts on the state you report and
tolerates either a dropped-and-reconnected or a continuously open socket.
- `AbortMission` is the operator bailing out: return to `WaitingForEgg`
(via `AbortingMission`), no results expected.
### 3.7 The console side of Tier 1
Commanding a game isn't only pod-side work — the console needs a per-game module
that builds the egg and provides the mission UI:
`Console/TeslaConsole.<YourGame>/` mirroring
[Console/TeslaConsole.RedPlanet/](Console/TeslaConsole.RedPlanet/)
(mission classes + `ToEggString()`, a config XML catalog of maps/vehicles/
scenarios, the game pane driving `MungaGame`). The BattleTech port spec
([BATTLETECH-PORT-SPEC.md](410console/battletech-port/BATTLETECH-PORT-SPEC.md))
is the worked example of adding one — budget for it in your plan, and open the
conversation with the TeslaSuite maintainers early (ApplicationID assignment,
event vocabulary, egg fields).
### 3.8 If your game already has its own control protocol
Precedent: BattleTech FireStorm (MechWarrior 4) speaks **CTCL** on ports
1000/1001, not Munga — researched and parked in
[FIRESTORM-CTCL.md](FIRESTORM-CTCL.md). The standing guidance: **do not teach
the console a second protocol**. Put an adapter on the pod that speaks Munga to
the console and your native protocol to the game (TeslaRel410 does exactly this:
`pod-launch.exe` supervises the DOS game and fronts for it). The console then
sees a normal Munga pod.
### 3.9 Escape hatch: a dedicated game console
If Munga control — direct or through a §3.8 adapter — would be **limiting or
detrimental to the game experience** (the mission model doesn't map to the
egg/state machine, the game needs richer or real-time operator control than
load/run/stop, the adapter would cost fidelity), you may instead build a
separate **game console**: a purpose-built operator application for your game,
run on the console computer alongside TeslaConsole. FireStorm is the precedent —
MechWarrior 4 venues ran their own dedicated console rather than bending the
game to the Tesla mission model ([FIRESTORM-CTCL.md](FIRESTORM-CTCL.md)).
Rules if you take this path:
- **It ships inside the same deployment zip as the pod-side game.** One Install
Product archive is the whole product — no separate installer, no side-channel
distribution. Put it in a subfolder of your product
(e.g. `YourGame\GameConsole\`); the pod-side extraction just carries the
folder along, and the operator runs it from that same archive on the console
machine.
- It runs on the **console computer**, never on pods.
- **TeslaConsole still owns deployment and process lifecycle** (§2):
install/uninstall, launch/kill, and the watchdog all stay on the launcher
channel. Your game console owns only in-game command — it is a replacement
for §3.13.7, not for §2.
- Don't collide with the suite's ports on either end: 1501, 53290, 53291/53292
are spoken for.
---
## 4. Presentation: live camera, mission review, score sheets
A pod bay is more than cockpits: spectators watch the current game on a bay
display, players coming out of the pods watch a replay at the mission-review
station, and everyone walks away with a printed score sheet. Plan for all
three.
### 4.1 The bay address plan
Modern bay deployments follow the FireStorm/CTCL site layout: squads of eight
pods per address decade, with the `9`/`10` slots of each decade reserved for
stations.
| Address | Station |
|---|---|
| `x.x.x.18` | Pods, squad 1 |
| `x.x.x.9` | **Live camera** |
| `x.x.x.10` | **Operator console** |
| `x.x.x.1118` | Pods, squad 2 |
| `x.x.x.19` | **Mission review** |
| `x.x.x.20` | **Score-sheet printer** |
| `x.x.x.2128` | Pods, squad 3 |
| `x.x.x.3138` | Pods, squad 4 — and so on: pods at `18` of every further decade, `9`/`10` slots reserved for future stations |
(Legacy RP/BT-era installs used the `200.0.0.x` scheme with the console at
`.1` and pods from `.11`. The suite doesn't hardcode either — addressing is
per-site via Manage Site — but new bays should follow the plan above.)
### 4.2 Live camera — presenting the current game
The live-cam station runs your game exe in a **spectator role**, rendering the
running mission on the bay display. The RP/BT model, which the console
generalizes:
- The station is a pod like any other — installed, launched, watchdogged —
whose catalog launch entry has `hostType="LiveCamera"`; RP/BT pass a `-lc`
flag so the exe boots into the camera role (§2.2).
- The console enrolls every enabled camera station in the mission as a
**camera participant**: it receives the same egg and walks the same Munga
state machine as a game pod, but its participant block says `hostType=2`,
`vehicle=camera`, `name=Camera`, `loadzones=0`
([RPCamera.cs](Console/TeslaConsole.RedPlanet/RPCamera.cs)).
- Your game's job in the role: observe the running mission and render a
spectator view — no cockpit input, no scoring participation. The camera's
view of the action travels over the game's own network traffic between pods;
the console only issues the egg and state commands.
### 4.3 Mission review — replaying the finished game
The mission-review station **replays the completed mission** for the players
who just climbed out. To the console it looks exactly like the live cam — a
`hostType="MissionReview"` catalog entry (`-mr` flag in RP/BT), enrolled as an
egg camera participant — the difference is entirely inside your game: the MR
role captures the mission as it runs and replays it on demand afterwards.
**Replay capture and playback are the game's responsibility.** The console does
not record or transport replay data: RP/BT capture from the game's own network
traffic; FireStorm writes `{guid}.mr` files pod-side and its console points the
review station at them ([FIRESTORM-CTCL.md](FIRESTORM-CTCL.md)).
### 4.4 Score sheets
Players get a printed score sheet. How it works for a Tier 1 game:
- During the mission the console builds results from your **in-mission event
messages** (§3.6) via the game module's mission recorder
([RPMissionRecorder.cs](Console/TeslaConsole.RedPlanet/RPMissionRecorder.cs)).
The event vocabulary is what makes score sheets possible — a game that
reports no events has nothing to print.
- The game module renders the results as a print document
([RPPrintDocument.cs](Console/TeslaConsole.RedPlanet/RPPrintDocument.cs)).
The operator's **Auto Print** checkbox prints each mission as it ends;
**Print Last Mission** reprints on demand. Output goes to the bay's
score-sheet printer (`x.x.x.20`, a network printer configured on the console
machine).
- A §3.9 dedicated game console owns its own scoring and printing (FireStorm:
the game writes `{guid}.pr` files pod-side, the console hands them to the
`mw4print` helper) — it should print to the same bay printer.
---
## 5. Pod environment reference
| Fact | Value |
|---|---|
| OS range | Windows XP SP3 → Windows 11, one binary set (.NET products: net40) |
| Session | auto-login kiosk account `Firestorm` (Administrator, UAC off) |
| Games root | `C:\Games\<Product>\` |
| Launcher state | `<CommonAppData>\TeslaLauncher\` (`LaunchApps.xml`, key store, log) |
| Network plan | squads of 8 pods per address decade; live cam `.9`, console `.10`, mission review `.19`, printer `.20` — see §4.1 |
| Ports | 1501 TCP game control (your game listens) · 53290 TCP launcher RPC · 53291/53292 UDP first-boot provisioning |
| Cockpit extras | 128×32 plasma scoreboard on COM2; RIO board cockpit controls — native integration preferred, RIOJoy shim optional (§5.1) |
| Firewall | disabled by the pod installer |
### 5.1 Cockpit controls: the RIO board
The cockpit's controls come in through the **RIO board**. Two integration
paths:
- **Native RIO integration — preferred.** Talk to the board directly, as the
original games do (FireStorm's `CRIOMAIN.CPP` is the documented precedent).
This is the only path that reaches the board's **output side** — the
**cockpit lighting** — which native games get as a feedback channel to the
player. A game that only reads a gamepad can't touch it.
- **RIOJoy — optional shim.** A deployable catalog product that feeds RIO
board input into a virtual gamepad (RioGamepad HID via the ViGEmBus driver;
Win10+ only) with per-game mapping profiles. Zero game-side changes — if
your engine already reads a standard gamepad, a RIOJoy profile gets a
cockpit playable. **Input only:** no lighting, no feedback.
A RIOJoy profile is a fine way to get playable quickly during development;
plan on native RIO integration for the real deployment so the cockpit lighting
works for your game. (The board interface and the feeder implementation live
in the RIOJoy repo.)
---
## 6. Testing your integration without a pod bay
- **[vPOD/](vPOD/)** is a full software pod: it emulates the launcher side
(install your zip into a real `C:\Games`, launch/kill with the real watchdog
semantics) *and* the Munga side. Two ways to use it:
1. **Test your package/catalog entry:** run vPOD on any machine, provision it
from the console (README walk-through), Install Product your zip, launch —
with "Actually launch apps" checked your real exe runs.
2. **Test your Munga implementation:** point the console at your game instead
of vPOD (the shipped site has a `local` pod at `127.0.0.1` — run your exe
with `-net 1501` on the console machine). vPOD's egg viewer is also handy:
drive a mission at vPOD, copy the egg it captures, and use it as a fixture
for your parser.
- Console-side changes are pinned by the differential suite
([Console/tests/TeslaConsole.DiffTests](Console/tests/TeslaConsole.DiffTests)).
---
## 7. Checklists
**To make your game deployable (Tier 0):**
- [ ] Package zip: `<Product>\` folder + optional root `postinstall.bat`,
optional `<Product>\pre-uninstall.bat` (§2.1)
- [ ] Runs on your target pod OS range — XP SP3 support is a nice-to-have that
reaches the original hardware (x86 + net40 if .NET); if skipped, catalog
comment says modern-pods-only
- [ ] `Apps.xml` `<Product>` entry, key convention respected (§2.2)
- [ ] Cold start reaches gameplay/ready state unattended (kiosk + watchdog)
- [ ] Cockpit controls wired: native RIO integration preferred (enables
lighting feedback), RIOJoy profile acceptable (§5.1)
- [ ] Install → launch → kill → uninstall verified against vPOD
**To make your game commandable (Tier 1), additionally** *(or, if Munga control
would hurt the game: a dedicated game console per §3.9, shipped in the same
deployment zip)*:
- [ ] TCP server on 1501 (`-net` arg), Munga framing per §3.2
- [ ] `StateQuery``StateResponse` with an agreed `ApplicationID`
- [ ] Egg reassembly + `AcknowledgeEggFile` + state walk to `RunningMission`
- [ ] `Stop/Abort/Suspend/Resume` honored; after mission end, back to a dark
`WaitingForEgg` — by exiting (watchdog relaunch) or by in-process reset
- [ ] Event vocabulary agreed with the console module — rich enough for score
sheets (§4.4)
- [ ] Live-camera and mission-review roles: spectator rendering + replay
capture/playback in the game (§4.24.3), LC/MR catalog entries (§2.2)
- [ ] Console game module exists or is planned (§3.7)
+6
View File
@@ -60,6 +60,12 @@ Release packages for all three are attached to the
baselines** — both are now built from source (`Contract/`, `SecureConfig/`).
- `Console/RedPlanet/Apps.xml` — the data-driven product catalog (see the console's
Site Management → Add Product / Register Product on Pods).
- [`GAME-INTEGRATION.md`](GAME-INTEGRATION.md) — the integration spec for games being
brought to the pods: package/catalog requirements to **deploy** a game, and the Munga
protocol contract to **command** one. Start here when adding a new title.
- [`FIRESTORM-CTCL.md`](FIRESTORM-CTCL.md) — research notes on driving **BattleTech
FireStorm** (MechWarrior 4) from the console. FireStorm speaks CTCL, not Munga; the
document specs that protocol, the three blockers, and why the work is parked.
## History