The previous correction overstated the case by implying the code reading was wrong outright. It was not. The engine really does place surplus 'Mechs on already-occupied spawn points -- two 'Mechs dropped on the same spot -- exactly as reading the code suggests. The only wrong part was the predicted consequence. The original draft said those players "silently fail to spawn". They do spawn; the collision system then pushes the stacked 'Mechs apart within a second or two, costing some minor contact damage, and play continues normally. Reworded Layer 7 and the known-traps entry to separate the two claims: the spawn-point reuse is real and code-predictable, the failure-to-spawn conclusion was not. 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. 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
- 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 ? '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.