Files
firestorm/RAISING-PLAYER-CAP.md
T
0009f868eb RAISING-PLAYER-CAP.md: sharpen the drop-zone wording
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>
2026-07-24 23:50:12 -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 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 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 (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

  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 ? '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.