diff --git a/FIRESTORM-CTCL.md b/FIRESTORM-CTCL.md new file mode 100644 index 0000000..e571659 --- /dev/null +++ b/FIRESTORM-CTCL.md @@ -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 `\\\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` | diff --git a/README.md b/README.md index 20e1229..e682dd9 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,9 @@ 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). +- [`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