Files
firestorm/RAISING-PLAYER-CAP.md
T
a41dfb4aa8 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>
2026-07-24 23:40:14 -05:00

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 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 ? stream->WriteBits(&m_maxPlayers, 5); // 16 players maximum
  • 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 line ~139
  • Maximum_Connection_Numbers = 255 ? Network.hpp line ~23
  • servedConnectionData[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)
  • 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 ? 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_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 / ctcl.h:

#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)

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.