Files
firestorm/RAISING-PLAYER-CAP.md
T
33c28cf816 RAISING-PLAYER-CAP.md: correct the drop-zone analysis
The original draft claimed that players beyond a map's available drop zones
"silently fail to spawn", and called per-map drop-zone authoring the true gating
task for raising the player cap. That was inferred from reading the code and is
wrong.

Corrected from real pod testing: when there are more 'Mechs than drop zones, the
surplus 'Mechs spawn on top of each other. They clip and collide briefly, take
some minor damage, then separate and play normally. It resolves itself within
seconds.

So a drop-zone shortfall is a quality-of-experience issue, not a functional
failure. It does not block raising the cap and should not gate the schedule.
Adding start points to busy maps is still worth doing eventually -- overlapping
spawns are untidy and hand out free chip damage -- but it can happen at any point
and never needs to be complete.

Updated accordingly: the TL;DR table, Layer 7, the implementation order (drop
zones moved from step 5 to last and marked optional), the verification checklist,
and the known-traps list.

The biggest remaining non-code task is now the lobby pod-grid and scoreboard
layout rework.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-07-24 23:49:01 -05:00

247 lines
12 KiB
Markdown

# 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.
**This was previously assumed to be the gating task. It is not.** Verified empirically on real
pods: when there are more 'Mechs than drop zones, the surplus 'Mechs **spawn on top of each
other**. They clip and collide for a moment, take some minor damage, then separate and play
normally. It resolves itself within seconds.
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** ? surplus 'Mechs spawn stacked, bump, take minor damage and
separate. 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.