Files
TeslaSuite/FIRESTORM-CTCL.md
T
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

289 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` |