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>
250 lines
12 KiB
Markdown
250 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. 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.
|