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:
co-authored by
Claude Opus 5
GitHub Copilot
parent
9ba2d594b1
commit
a41dfb4aa8
@@ -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.
|
||||
Reference in New Issue
Block a user