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) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
This commit is contained in:
2026-07-24 23:40:14 -05:00
co-authored by Claude Opus 5 GitHub Copilot
parent 9ba2d594b1
commit a41dfb4aa8
+235
View File
@@ -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.