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>
12 KiB
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 ?
stream->WriteBits(&m_maxPlayers, 5); // 16 players maximum - MWApplication.cpp ?
stream->WriteBits(&m_maxBots, 5); - matching
ReadBits(..., 5)calls inLoadParameters
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 line ~139Maximum_Connection_Numbers = 255? Network.hpp line ~23servedConnectionData[Adept::Maximum_Players]? MWApplication.hpp line ~1372 (255 entries)- Per-player scoreboards, net-stat collectors, damage-given/received tables ? all sized from
Maximum_Players(255) connectionIDis aBYTE¢¡ 0..255- DirectPlay has no relevant ceiling;
dwMaxPlayerscomes straight fromEnvironment.NetworkMaxPlayers - No 32-bit player bitmask exists. The only
0x1 << idmasks in the codebase are per-mech weapon bits (MWEntityManager.cpp,NetWeapon.cpp), not player/connection indices. (Carried over from the earlierCLAUDE.mdaudit ? 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 ? CTCL_DefaultHostSetup, non-COOP branch:
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_PARAMETERhandlers (SetNetworkMissionParamater)PLAYER_LIMIT_PARAMETER? setsEnvironment.NetworkMaxPlayers, callsgos_NetServerCommands(gos_Commend_UpdateMaxPlayers, 0)Max_Clamp(params->m_maxBots, params->m_maxPlayers)? bots clamped down to player countMW4Shell::AddBot?if ((player_count + bot_count) >= params->m_maxPlayers) return -1;(cameraships are now excluded fromplayer_count; preserve that)
Layer 3 ? CTCL roster arrays (the one that will bite)
#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 line 12:
#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)
#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 tocap + 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_PvsMAXTESLAalso 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
- Decide the target: 31 (no format change) or 32 (6-bit fields, breaking).
- Layer 3 first ? resize
g_aPlayerInfos, raiseMAX_TESLAS, sync all copies ofctcl.h, add the missing bounds check inCTCL_AddPlayer. Nothing else is safe until this is done. - Layer 2 ? raise the compiled defaults. Rebuild
MW4.exe(Release + Profile). - Layer 4 ? decide and implement the bot cap.
- Layer 5 ? lobby UI rework (roster rows, pod grid, launch guard).
- Layer 6 ? scoreboard/radar/review polish.
- Layer 8 ? one live load test.
- 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¡æ DirectPlaydwMaxPlayers) - 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_nBOTsmatches 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_aPlayerInfosoverrun ? no bounds check; corrupts memory silently.ctcl.hexists 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 = 3forever becauseCTCL_CheckServerReadynever seesnCount == g_nTeslas + 1and 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.