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>
289 lines
14 KiB
Markdown
289 lines
14 KiB
Markdown
# 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` |
|