Files
firestorm/MISSION-TIME-LIMITS.md
T
136e2ff05c Extend MP mission time limit to 240 min (from LYLT 3514acf6)
Time-limit pick lists go to 23 entries: 1-15, 20, 25, 30, 45, 60, 120,
180, 240. Content + console only, no MW4.exe rebuild -- nothing between
the dropdown and the mission timer clamps the value, and m_gameLength is
8 bits on the wire (MWApplication.cpp:790), so 255 was always the ceiling.

Hand-applied rather than merged. LYLT reverted aa500be7 (9aa317ea) and
built on the original 9-entry list, while this branch carries the fixed
18-entry version from 456e1978, so the two sides have different bases.
Merging the branch would also drag in that revert of the Battlemaster and
Behemoth loadout work.

Two deliberate deviations from 3514acf6:

- Its max_displayed restructure is NOT taken. Both sides independently
  reached the same logic (i==5 -> 10, i==6 -> 4, else 16), but LYLT wrote
  the outer else without braces. That bare else-then-if is the construct
  behind the null-reference console lobby crash in the aa500be7
  regression; 456e1978 already fixed it here. Ours is kept.
- HostLobbyMission.script was untouched on this branch so LYLT's version
  is taken wholesale, but its else block (i==5 / i==11) is re-braced for
  the same reason before it can bite the PC host lobby.

Fixes a real bug this branch already had: TIME_LIST_DEFAULT was still the
C++ global g_nTimeList_Index (3), which indexed "7" in the old 9-entry
list but indexes "4" in the expanded one -- the default mission time had
silently become 4 minutes. Now the literal 6, which is "7" in both the
18- and 23-entry lists. Kept as a literal so the script stays independent
of the exe; the reset-to-defaults path matches by value (g_nTimeList_Value
= 7) and needed no change.

The rest scales on its own: drop_list_size[5], the doh loop bounds and the
nselected fallbacks in ConLobbyMission all already used TIME_LIST_COUNT /
TIME_LIST_DEFAULT symbolically. The DEMO_CODE list (11 entries, default 5)
is untouched. Verified 23 contiguous entries and balanced braces in both
files, no unbraced else-if left, CRLF and us-ascii preserved.

Not yet repacked: these live in props.mw4, so run build-env\build-resources.ps1
on the Windows box, deleting resource\props.mw4 + props.dep first to force a
full repack (incremental carries stale entries forward). Not run on a real bay.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-08-06 14:51:01 -05:00

9.3 KiB

Mission time limits — how long a match can be set for

Reference for the MP mission time limit (m_gameLength): what the operator can pick, what the engine can carry, why "No Limit" is not currently usable, and what it would take to go past 255 minutes. Written alongside the LYLT branch, which extended the pick lists to the engine's existing ceiling.

The short version

Value
Console / PC lobby list (after LYLT) 1-15, 20, 25, 30, 45, 60, 120, 180, 240 min
Wire ceiling, no code change 255 min (4 h 15 m)
Default 7 min
0 on the wire "no time limit" — do not ship; see below
Beyond 255 needs a wire-format change — see Plan B

How the value flows

  1. The operator picks minutes from a dropdown in ConLobbyMission.script (venue console) or HostLobbyMission.script (PC host lobby). FSConsole reads the same list from venue/fsconsole/content/FSConfig.xml.
  2. The script calls SetNetworkMissionParamater(GAME_LENGTH_PARAMETER, minutes)params->m_gameLength (MW4Shell.cpp:2066). No clamp anywhere on this path.
  3. The value ships to the pods inside the NMP blob as 8 bits: stream->WriteBits(&m_gameLength, 8); // 256 minutes (MWApplication.cpp:790, read back at :910). That is the entire ceiling: 0-255.
  4. At mission start the pod does mission->SetEndMissionTime(m_gameLength * 60.0f) (MWApplication.cpp:3537) — and only when m_gameLength > 0 — plus CTCL_SetGameTime(m_gameLength * 60) for the console clock (int32 seconds, no limit of its own).

Nothing between the dropdown and the mission timer cares how large the number is. The mission timer itself is double (Stuff::Time, Scalar.hpp:225), so long matches do not lose time accuracy; only the Scalar (float) countdown view does, and only by ~8 ms at the 24-hour mark.

Displays are already wide enough

Where Format At 240 min
MP scoreboard clock, hudscore.cpp:443 %d:%02d 240:00
Radar/shell HUD, GUIRadarManager.cpp:1981 %02d:%02d 240:00 (6 glyphs in a slot laid out for 5 — watch for clipping)
Script GetGameTime, MW4Shell.cpp:3910 %02d:%02d:%02d 04:00:00
FSConsole pane, FSGamePane.cs <min>:00 240:00

What LYLT changed

Content and console only — no MW4.exe rebuild:

  • ConLobbyMission.script: TIME_LIST_COUNT 9 → 23, the 23 list entries, and max_displayed = 10 for the time dropdown so it scrolls instead of overflowing the panel. TIME_LIST_DEFAULT was $$g_nTimeList_Index$$ (the C++ global g_nTimeList_Index = 3, MW4Shell.cpp:159) — index 3 is "7" in the OLD list but "4" in the new one, so it is now the literal 6, the index of "7". Changing the C++ global instead would have forced an exe rebuild for no other gain. The "reset to defaults" path is unaffected: it searches the list by value for TIME_VALUE_DEFAULT = g_nTimeList_Value = 7.
  • HostLobbyMission.script: same list, drop_list_size[5] 9 → 23, max_displayed = 10, and the two selection loops that hardcoded 9 (and the nselected = 3 fallback → 6).
  • FSConsole: the 23 entries in catalog/generate_fsconfig.py (regenerated into content/FSConfig.xml), MaxDropDownItems = 10 on the pane's combo, and a range guard in NmpBuilder — it used to write TimeLimitMinutes & 0xFF, which silently turned 256 into 0. It now throws outside 1-255. SelfTest.CheckTimeLimits() proves every catalog entry encodes to the right value at the right bit, and that 0/256/1440 are refused.

Anything already in flight is unaffected: a stored value picks the first list entry >= it, so old saved settings still resolve.

The branch carries sources only. The repacked resource\*.mw4 packages, their .deps, the deployed MW4\resource copies and the staged MW4pro.exe are build output and are not tracked on it — they get rebuilt from main once this merges. Until you repack, a checkout still shows the old 9-entry list in game:

build-env\build-resources.ps1                       # rebuilds core.mw4 + props.mw4
copy Gameleap\mw4\Resource\props.mw4 MW4\resource\  # and core.mw4

The "No Limit" trap — why 0 is not shipped

SetEndMissionTime is only called when m_gameLength > 0, so 0 looks like a free "unlimited match" option. It is not usable as the code stands:

  • With the end timer stopped, GetEndMissionTime() returns 0 (MWMission.hpp:602-608), so CheckEndMissionTime() returns 0 too.
  • The networked fade-out reads that as "under 3 seconds left" and fades the screen to full black three seconds into the mission (MWGUIManager.cpp:1633-1649).

Nothing sets 0 today (CTCL_DefaultHostSetup forces 7, MW4Shell.cpp:13332), which is consistent with the path never having been exercised. Read from code, not yet reproduced in game.

To make "No Limit" real: guard that fade block on EndMissionTimerRunning(), then audit every other consumer that treats a 0 countdown as "expired" (TestForGameShutDown, the 60 s/30 s voice cues at MWMission.cpp:1263-1288, scoreboard and mission-review). That is an MW4.exe rebuild plus a pod redeploy — cheap in code, but it needs the same lockstep rollout discipline as Plan B, and the venue gets most of the benefit from 240 minutes anyway.

Plan B — going past 255 minutes

Everything above is configuration. Past 255 it becomes a wire-format change, which is a different risk class: the NMP has no version field, so a pod on a different build mis-parses every field after m_gameLength (map name, MapClientCRC) and refuses the lobby.

B1 — widen the field. WriteBits/ReadBits for m_gameLength (MWApplication.cpp:790 and :910). Use 11 bits (2047 min): the blob is 3777 fixed bits with 7 pad bits before WriteByteAlign, so up to 15 extra bits are absorbed without changing the 473-byte fixed length, and FSConsole's length assert keeps working. The bit layout still shifts — that is what forces B3.

B2 — update every mirror, identically.

  • mw4dummy.cpp:433/552mw4print carries a full duplicate of the serialization for score printouts. Miss it and printouts garble silently.
  • venue/fsconsole/protocol/NmpBuilder.cs (+ testdata/vectors/expected.json, the golden cross-validation vectors, and the CheckTimeLimits bit offset).
  • Rebuild MW4.exe Release and Profile (Profile is the resource builder).

B3 — lockstep deployment. All pods, both consoles (legacy MW4.exe console and FSConsole) and mw4print ship together. The …A7 legacy-console rollback path is only valid against equally rolled-back pods.

B4 — display formats. %d:%02d and %02d:%02d print 1440:00; both want an h:mm:ss form above 60 minutes, with column and clip re-checks. FSConsole's <min>:00 likewise. MW4Shell::GetGameTime already emits hh:mm:ss.

B5 — the actual unknown: a day-long match. The format change is a day's work; proving the engine survives 24 hours is the project. In order: memory growth and heap fragmentation (debris, effects, chat, net-stat buffers); the .mr mission-review recording, which accumulates per frame — establish whether it has a cap or rollover before assuming a 24 h recording is even writable, and what that does to the printout pipeline; DirectPlay session stability; score and stat counters; and the 8-bit sibling timers — m_joinInProgressCutOffTime is also 8-bit minutes (the game writes 1000 and wires 232), so join-in-progress cutoff cannot express a long match either.

B6 — test ladder. vpod bench → 4 h bay run → 12 h → 24 h, sampling process memory and confirming end-of-mission, review and printout at each rung. Expect the failures in B5, not B1.

Verifying a time-limit change

  1. python venue\fsconsole\catalog\generate_fsconfig.py — regenerates FSConfig.xml and fails loudly if the authored blocks drift from the sources.
  2. dotnet publish venue\fsconsole\FSConsole.csproj -c Release -o dist\App, then FSConsole.exe -selftest -mission -site bench.ini against python C:\VWE\showrunner\tools\vpod_ctcl.py --pods 2 --cam --write-ini bench.ini. CheckTimeLimits runs in every self-test mode, network or not.
  3. build-env\build-resources.ps1 to repack props.mw4, then copy it to MW4\resource.
  4. In game: pick the longest entry, confirm the lobby dropdown scrolls, the HUD clock reads 3-digit minutes without clipping, the mission ends on time, and the debriefing prints.