# 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](#plan-b--going-past-255-minutes) | ## How the value flows 1. The operator picks minutes from a dropdown in [`ConLobbyMission.script`](Gameleap/mw4/Content/ShellScripts/Multiplayer/ConLobbyMission.script) (venue console) or [`HostLobbyMission.script`](Gameleap/mw4/Content/ShellScripts/Multiplayer/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`](Gameleap/code/mw4/Code/MW4/MW4Shell.cpp#L2066)). **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`](Gameleap/code/mw4/Code/MW4/MWApplication.cpp#L790), 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`](Gameleap/code/mw4/Code/MW4/MWApplication.cpp#L3537)) — 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`](Gameleap/code/mw4/Libraries/stuff/Scalar.hpp#L225)), 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`](Gameleap/code/mw4/Code/MW4/hudscore.cpp#L443) | `%d:%02d` | `240:00` | | Radar/shell HUD, [`GUIRadarManager.cpp:1981`](Gameleap/code/mw4/Code/MW4/GUIRadarManager.cpp#L1981) | `%02d:%02d` | `240:00` (6 glyphs in a slot laid out for 5 — watch for clipping) | | Script `GetGameTime`, [`MW4Shell.cpp:3910`](Gameleap/code/mw4/Code/MW4/MW4Shell.cpp#L3910) | `%02d:%02d:%02d` | `04:00:00` | | FSConsole pane, `FSGamePane.cs` | `: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`](Gameleap/code/mw4/Code/MW4/MW4Shell.cpp#L159)) — 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 `.dep`s, 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: ```powershell 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`](Gameleap/code/mw4/Code/MW4/MWMission.hpp#L602-L608)), 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`](Gameleap/code/mw4/Code/MW4/MWGUIManager.cpp#L1633-L1649)). Nothing sets 0 today (`CTCL_DefaultHostSetup` forces 7, [`MW4Shell.cpp:13332`](Gameleap/code/mw4/Code/MW4/MW4Shell.cpp#L13332)), 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`](Gameleap/code/mw4/Code/MW4/MWMission.cpp#L1263-L1288), 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`](Gameleap/code/mw4/Code/MW4/MWApplication.cpp#L790) 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/552`](Gameleap/code/mw4/Code/mw4print/mw4dummy.cpp#L433) — `mw4print` 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 `: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.