From a41dfb4aa8331035909f6dd660039056afff7d5d Mon Sep 17 00:00:00 2001 From: RT Date: Fri, 24 Jul 2026 23:40:14 -0500 Subject: [PATCH] Add RAISING-PLAYER-CAP.md reference Captures the full audit of every player-count limit in the codebase, done while tracking down the "16 pilots + 1 cameraship" launch failure. Research only -- nothing in it is implemented. Key finding: the practical ceiling without a wire-format change is 31, not 32. m_maxPlayers and m_maxBots are serialized as 5-bit fields in NetMissionParameters, so 32 truncates to 0. This supersedes the "32" figure in the existing CLAUDE.md plan sections. Documents, with file references: - what is NOT a limit (Adept::Maximum_Players is 255, connectionID is a BYTE, DirectPlay imposes nothing, and there is no 32-bit player bitmask) - the 5-bit serialization ceiling and what widening it would cost - the compiled defaults in CTCL_DefaultHostSetup that actually gate connections - the CTCL roster arrays, including that g_aPlayerInfos[20] has NO bounds check in CTCL_AddPlayer and that ctcl.h is duplicated across ~6 directories - MAX_LANCEMATES 16 for bots - the lobby script constants and the pod-grid UI work - scoreboard/radar/review layout work - per-map drop zones, which is the real gating task and produces silent spawn failures when short - the O(n^2) replication cost, reframed as verify-don't-assume on modern hardware Also records the failure signatures to expect, so a future attempt recognises them quickly: silent launch hang from a count mismatch, silent non-spawn from missing drop zones, and 5-bit truncation looking like "max players became zero". Co-authored-by: Claude Opus 5 (Anthropic) Co-authored-by: GitHub Copilot --- RAISING-PLAYER-CAP.md | 235 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 RAISING-PLAYER-CAP.md diff --git a/RAISING-PLAYER-CAP.md b/RAISING-PLAYER-CAP.md new file mode 100644 index 00000000..60b79cce --- /dev/null +++ b/RAISING-PLAYER-CAP.md @@ -0,0 +1,235 @@ +# Raising the multiplayer player cap + +Reference notes for increasing FireStorm's 16-player multiplayer cap toward the maximum +achievable **without a protocol/format rewrite: 31 players.** + +Status: **research only ? nothing here is implemented.** Written 2026-07-24 while fixing the +"16 pilots + 1 cameraship" launch bug, which forced a full audit of every player-count limit +in the codebase. + +Related reading: `CLAUDE.md` (sections "Reference: raising the MP player cap 16 ¡æ 24" and +"PLAN: raise MP cap 16 ¡æ 32"). **This document supersedes the "32" figure in those sections** ? +see "Why 31, not 32" below. + +--- + +## TL;DR + +| Question | Answer | +|---|---| +| Hard ceiling without changing the wire format | **31** | +| Hard ceiling if you also widen one serialized field | 255 (engine arrays are sized 255) | +| Is 16 an engine limit? | **No.** It is a set of independent constants and one 5-bit field | +| Biggest *code* task | CTCL roster arrays + lobby UI layout | +| Biggest *non-code* task | **Per-map drop zones** ? most maps only define ~16 | +| Does netcode bandwidth block it? | Almost certainly not on modern hardware; verify, don't assume | + +--- + +## Why 31, not 32 + +`NetMissionParameters` is bit-packed when the host serializes mission parameters and ships them +to every client. Two fields are **5 bits wide**: + +- [MWApplication.cpp](Gameleap/code/mw4/Code/MW4/MWApplication.cpp) ? `stream->WriteBits(&m_maxPlayers, 5); // 16 players maximum` +- [MWApplication.cpp](Gameleap/code/mw4/Code/MW4/MWApplication.cpp) ? `stream->WriteBits(&m_maxBots, 5);` +- matching `ReadBits(..., 5)` calls in `LoadParameters` + +5 bits ¢¡ values `0..31`. Writing 32 silently truncates to 0. + +So **31 is free** (no format change), and **32 requires widening those fields to 6 bits** in both +`SaveParameters` and `LoadParameters`. That is a one-line-each change, but it is a **breaking wire +format change**: every pod must run the identical build. Per the project's stated constraint +(all pods at a site update together, no cross-version compatibility required) that is acceptable ? +but it must be a deliberate decision, and the resource/parameter blob is also persisted by +`CTCL_SaveNMP`, so any stored parameter blobs from an older build become unreadable. + +**Recommendation: target 31 first.** It costs nothing extra and proves out every other layer. +Only widen to 6 bits if 32 specifically matters. + +--- + +## What is NOT a limit (verified ? don't waste time here) + +These were checked directly and are already sized far beyond 31: + +- **`Adept::Maximum_Players = 255`** ? [Application.hpp](Gameleap/code/mw4/Libraries/Adept/Application.hpp) line ~139 +- **`Maximum_Connection_Numbers = 255`** ? [Network.hpp](Gameleap/code/mw4/Libraries/Adept/Network.hpp) line ~23 +- **`servedConnectionData[Adept::Maximum_Players]`** ? [MWApplication.hpp](Gameleap/code/mw4/Code/MW4/MWApplication.hpp) line ~1372 (255 entries) +- Per-player scoreboards, net-stat collectors, damage-given/received tables ? all sized from + `Maximum_Players` (255) +- **`connectionID` is a `BYTE`** ¢¡ 0..255 +- **DirectPlay** has no relevant ceiling; `dwMaxPlayers` comes straight from + `Environment.NetworkMaxPlayers` +- **No 32-bit player bitmask exists.** The only `0x1 << id` masks in the codebase are per-mech + *weapon* bits (`MWEntityManager.cpp`, `NetWeapon.cpp`), not player/connection indices. + (Carried over from the earlier `CLAUDE.md` audit ? worth a re-grep before implementing.) + +--- + +## The actual limits, layer by layer + +### Layer 1 ? Serialized mission parameters (the 31 ceiling) + +| Field | Width | Max | File | +|---|---|---|---| +| `m_maxPlayers` | 5 bits | 31 | `MWApplication.cpp` Save/LoadParameters | +| `m_maxBots` | 5 bits | 31 | `MWApplication.cpp` Save/LoadParameters | +| `m_teamCount` | 4 bits | 15 teams | `MWApplication.cpp` | +| `m_maxPlayersOnTeam` | 7 bits | 127 | `TeamParameters::SaveParameters` | +| `m_playerLimit` | **not serialized** | ? | host-side only | + +Only the first two matter for 31. `m_maxPlayersOnTeam` at 7 bits is already generous. + +### Layer 2 ? Compiled defaults (the real connect gate) + +[MW4Shell.cpp](Gameleap/code/mw4/Code/MW4/MW4Shell.cpp) ? `CTCL_DefaultHostSetup`, non-COOP branch: + +```cpp +params->m_maxPlayers = 16; +Environment.NetworkMaxPlayers = params->m_maxPlayers + MW4_CAMERASHIP_RESERVE; +params->m_maxBots = 16; +``` + +Editing only the lobby script shows more slots but the session still rejects the extra players ? +**this is the branch that actually decides.** Note `MW4_CAMERASHIP_RESERVE` (added 2026-07-24) +already reserves camera seats on top of the pilot cap; keep that behaviour when raising the number. + +COOP branch is separate (`m_maxPlayers = 9`, `m_maxBots = 8`) ? decide explicitly whether COOP +should move at all. + +Also in `MW4Shell.cpp`: +- `MAX_PLAYERS_PARAMETER` / `MAX_BOTS_PARAMETER` handlers (`SetNetworkMissionParamater`) +- `PLAYER_LIMIT_PARAMETER` ? sets `Environment.NetworkMaxPlayers`, calls + `gos_NetServerCommands(gos_Commend_UpdateMaxPlayers, 0)` +- `Max_Clamp(params->m_maxBots, params->m_maxPlayers)` ? bots clamped down to player count +- `MW4Shell::AddBot` ? `if ((player_count + bot_count) >= params->m_maxPlayers) return -1;` + (cameraships are now excluded from `player_count`; preserve that) + +### Layer 3 ? CTCL roster arrays (**the one that will bite**) + +[ctcl.cpp](Gameleap/code/mw4/Code/MW4Application/ctcl.cpp) / [ctcl.h](Gameleap/code/mw4/Code/MW4/ctcl.h): + +```cpp +#define MAX_TESLAS 16 // pilot pods admitted from ctcl.ini +#define MAX_CAMERAS 4 // cameraship seats +extern SPlayerInfo g_aPlayerInfos[20]; // 16 + 4 ? NO BOUNDS CHECK +``` + +**`CTCL_AddPlayer` does `g_aPlayerInfos[g_nPlayerInfos++]` with no bounds check.** Raising the +roster past 20 without resizing this array is a **silent buffer overrun**. This is the single most +dangerous spot in the whole change. + +For 31 players + 4 cameras you need `g_aPlayerInfos[35]` (or `MAX_TESLAS + MAX_CAMERAS`), and +`MAX_TESLAS` raised to 31. **Note `ctcl.h` is duplicated in ~6 directories** (`MW4/`, +`MW4Application/`, `MW4GameEd2/`, `Adept/`, `Launcher/`, `Tools/AnimScript/`) ? they must be kept +in sync or you get ODR mismatches that manifest as memory corruption, not compile errors. + +Also add a defensive bounds check to `CTCL_AddPlayer` while you're there. + +### Layer 4 ? Bots / lancemates + +[MWApplication.hpp](Gameleap/code/mw4/Code/MW4/MWApplication.hpp) line 12: + +```cpp +#define MAX_LANCEMATES 16 +``` + +¢¡ `Maximum_Lancemates`, and `lancemateConnectionData[Maximum_Lancemates]`. If you want 31 *bots* +(not just 31 humans) this must rise too. If bots stay capped at 16, that's a legitimate design +choice ? just make it deliberate, because `m_maxBots` would then be misleading. + +### Layer 5 ? Lobby / console UI (script) + +[ConLobby.script](Gameleap/mw4/Content/ShellScripts/ConLobby.script): + +``` +#define MAX_ROSTER_COUNT 16 // under USE_O_MORE_PODS (else 8) +#define MAX_TEAMMATE_COUNT 8 // (else 4) +``` + +- `MAXTESLA_P` = `CTCL_GetTeslaCount()` (live pod count), clamped to 8 in one branch ? check that + clamp before raising anything +- Launch guard: `if nLaunchState == 0 && (nTempPlayerCount > 17)` ¡æ raise to `cap + cameras` +- **The mech-selection "pods" grid is the biggest UI lift.** It is a fixed layout; 31 needs a + reworked grid, scrolling, or smaller pods +- Team-assignment panel (`team_max_plyrs`), decal/skin columns, and the per-slot row layout all + assume ¡Â16 rows +- `MAXTESLA_P` vs `MAXTESLA` also drives the cameraship dropdown (`o_cam_list`) + +Team camo/decal arrays are already sized 16 in `CTCL_DoMission`'s prepare branch +(`for(i = 0; i < 16; i++) g_naTeamSkins[i] = ...`) ? these are per-*team*, not per-player, and 16 +teams is beyond the 4-bit `m_teamCount` limit of 15, so they are fine. + +### Layer 6 ? In-game HUD + +- **Scoreboard** (`hudscore.cpp`) ? data is fine (255 arrays), the on-screen table is laid out for + ¡Â16. Needs scrolling or two columns. +- **Radar** (`GUIRadarManager.cpp`) ? same story. +- **Mission review** (`MP_Review.script`) ? top-down review screen. + +These are cosmetic and non-blocking: 31 players can connect, spawn and play with an ugly +scoreboard. Defer. + +### Layer 7 ? Map content (**the true gating task**) + +Every MP map used at high player counts must define **¡Ã N drop zones / start points**. Most stock +maps define ~16. Players beyond the available start points **silently fail to spawn** ? no error. + +This is per-map authoring work in the mission editor (`Gameleap\mw4\run-editor.bat`), completely +independent of the code changes, and it is the item most likely to dominate the schedule. + +Audit approach: enumerate drop zones per map, produce a table of map ¡æ start-point count, and +decide which maps are certified for 31-player play. + +### Layer 8 ? Network performance (verify, don't assume) + +Replication cost is roughly O(n©÷): 31 vs 16 ? (31/16)©÷ ? **3.75¡¿ host outbound and CPU**. This was +the binding constraint in 2002 on dial-up. On modern broadband and CPUs it is very unlikely to +block, but it should be measured rather than assumed. + +Residual item to check during implementation: any **fixed-size netcode buffers or per-frame +packet/replication caps** hardcoded for ¡Â16 (distinct from raw bandwidth). The 255-sized array +design suggests none exist, but grep for it. + +--- + +## Suggested implementation order + +1. **Decide the target: 31 (no format change) or 32 (6-bit fields, breaking).** +2. **Layer 3 first** ? resize `g_aPlayerInfos`, raise `MAX_TESLAS`, sync all copies of `ctcl.h`, + add the missing bounds check in `CTCL_AddPlayer`. Nothing else is safe until this is done. +3. **Layer 2** ? raise the compiled defaults. Rebuild `MW4.exe` (Release + Profile). +4. **Layer 4** ? decide and implement the bot cap. +5. **Layer 7** ? audit maps for drop zones; author more on the maps you care about. Runs in + parallel with everything else and will take the longest. +6. **Layer 5** ? lobby UI rework (roster rows, pod grid, launch guard). +7. **Layer 6** ? scoreboard/radar/review polish. +8. **Layer 8** ? one live load test. + +## Verification checklist + +- [ ] Rebuild clean (0 errors) in Release and Profile +- [ ] Host a game; lobby shows the new slot count and the launch guard accepts it +- [ ] Player N connects (watch `Environment.NetworkMaxPlayers` ¡æ DirectPlay `dwMaxPlayers`) +- [ ] All N players actually **spawn** on a map with ¡Ã N drop zones (this is where it will fail) +- [ ] Cameraship still connects on top of the pilot cap (regression from the 2026-07-24 fix) +- [ ] Bots fill correctly to the intended bot cap, and `g_nBOTs` matches connected lancemates + (mismatch = silent infinite "loading", the classic failure mode) +- [ ] COOP mode unaffected +- [ ] Scoreboard / radar / mission review legible +- [ ] Load/bandwidth measurement at full count + +## Known traps + +- **`g_aPlayerInfos` overrun** ? no bounds check; corrupts memory silently. +- **`ctcl.h` exists in ~6 copies** ? desync between them is invisible at compile time. +- **Editing only the lobby script** looks like it works until the (N+1)th player is rejected by the + compiled default. +- **Silent launch hang** is the standard symptom of any count mismatch: the console sits in + `nLaunchState = 3` forever because `CTCL_CheckServerReady` never sees + `nCount == g_nTeslas + 1` and matching lancemates. If you see that, suspect a count mismatch, + not a crash. +- **Drop-zone shortfall produces no error** ? just missing players. +- **Writing 32 into a 5-bit field truncates to 0**, which will look like "max players became + unlimited/zero" rather than an overflow.