# 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 | Lobby pod-grid / scoreboard layout. Drop zones are **not** a blocker ? see Layer 7 | | 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 (drop zones: a polish item, NOT a blocker) Most stock maps define ~16 drop zones / start points. When there are more 'Mechs than drop zones, the surplus 'Mechs **are placed on already-occupied spawn points** ? two 'Mechs dropped on the same spot. That part happens exactly as reading the code suggests. What does *not* follow ? and what an earlier draft of this document got wrong ? is the consequence. The surplus 'Mechs do **not** fail to spawn. They spawn stacked, the **collision system separates them within a second or two**, they take some minor contact damage in the process, and play continues normally. Verified on real pods. So a drop-zone shortfall is a **quality-of-experience issue, not a functional failure**. It does not block raising the cap, and it should not gate the schedule. Worth doing eventually for the maps that see the most high-count play ? an overlapping spawn is untidy and hands out free chip damage ? but it can happen any time, before or after the code work, and it never needs to be complete. Audit approach if/when you want it: enumerate drop zones per map, produce a map ¡æ start-point count table, and add start points to the busiest maps first in the mission editor (`Gameleap\mw4\run-editor.bat`). ### 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 5** ? lobby UI rework (roster rows, pod grid, launch guard). 6. **Layer 6** ? scoreboard/radar/review polish. 7. **Layer 8** ? one live load test. 8. **Layer 7** ? optional: add drop zones to the busiest maps. Non-blocking; surplus 'Mechs already spawn stacked and separate on their own. ## 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** (surplus 'Mechs stacking on shared drop zones is expected and self-correcting ? not a failure) - [ ] 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 is benign** ? 'Mechs really are dropped onto shared spawn points, but the collision system separates them within seconds for minor contact damage. Do not mistake this for a bug, and do not let it gate the work. - **Writing 32 into a 5-bit field truncates to 0**, which will look like "max players became unlimited/zero" rather than an overflow.