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 revertedaa500be7(9aa317ea) and built on the original 9-entry list, while this branch carries the fixed 18-entry version from456e1978, 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 from3514acf6: - 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 theaa500be7regression;456e1978already 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>
This commit is contained in:
co-authored by
Claude Opus 5
GitHub Copilot
parent
020dc04612
commit
136e2ff05c
@@ -39,8 +39,8 @@
|
||||
#define TIME_LIST_DEFAULT $$g_nTimeList_Index$$ // 5 // 7 minutes
|
||||
#define TIME_VALUE_DEFAULT $$g_nTimeList_Value$$ // 7 // 7 minutes
|
||||
#else // !DEMO_CODE
|
||||
#define TIME_LIST_COUNT 18 // MSL // expanded 1-15, 20, 25, 30 min
|
||||
#define TIME_LIST_DEFAULT $$g_nTimeList_Index$$ // 3 // 7 minutes
|
||||
#define TIME_LIST_COUNT 23 // MSL // expanded 1-15, 20, 25, 30, 45, 60, 120, 180, 240 min
|
||||
#define TIME_LIST_DEFAULT 6 // index of "7"; the C++ g_nTimeList_Index (3) indexed the old 9-entry list
|
||||
#define TIME_VALUE_DEFAULT $$g_nTimeList_Value$$ // 7 // 7 minutes
|
||||
#endif // DEMO_CODE
|
||||
|
||||
@@ -485,6 +485,11 @@ main
|
||||
o_game_options[i].list_item[15] = "20"
|
||||
o_game_options[i].list_item[16] = "25"
|
||||
o_game_options[i].list_item[17] = "30"
|
||||
o_game_options[i].list_item[18] = "45"
|
||||
o_game_options[i].list_item[19] = "60"
|
||||
o_game_options[i].list_item[20] = "120"
|
||||
o_game_options[i].list_item[21] = "180"
|
||||
o_game_options[i].list_item[22] = "240"
|
||||
|
||||
if callback($$GetLocalNetworkMissionParamater$$, time_limit) == 0
|
||||
o_game_options[i].nselected = TIME_LIST_DEFAULT
|
||||
|
||||
@@ -110,7 +110,7 @@ main
|
||||
drop_list_size[2] = 2
|
||||
drop_list_size[3] = 5
|
||||
drop_list_size[4] = 2
|
||||
drop_list_size[5] = 9
|
||||
drop_list_size[5] = 23 // [LYLT] extended time list
|
||||
drop_list_size[6] = 10
|
||||
drop_list_size[7] = 10
|
||||
drop_list_size[8] = 2
|
||||
@@ -202,13 +202,20 @@ main
|
||||
o_game_options[i].offsetLabel = drop_name_loc[i]
|
||||
o_game_options[i].arrowHeight = 15
|
||||
|
||||
if i == 11
|
||||
if i == 5 // extended time list -> scroll instead of overflowing the panel
|
||||
{
|
||||
o_game_options[i].max_displayed = 4
|
||||
o_game_options[i].max_displayed = 10
|
||||
}
|
||||
else
|
||||
{
|
||||
o_game_options[i].max_displayed = 16
|
||||
if i == 11
|
||||
{
|
||||
o_game_options[i].max_displayed = 4
|
||||
}
|
||||
else
|
||||
{
|
||||
o_game_options[i].max_displayed = 16
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -545,24 +552,38 @@ main
|
||||
{
|
||||
o_game_options[i].list_item[0] = "1"
|
||||
o_game_options[i].list_item[1] = "2"
|
||||
o_game_options[i].list_item[2] = "5"
|
||||
o_game_options[i].list_item[3] = "10"
|
||||
o_game_options[i].list_item[4] = "15"
|
||||
o_game_options[i].list_item[5] = "30"
|
||||
o_game_options[i].list_item[6] = "60"
|
||||
o_game_options[i].list_item[7] = "90"
|
||||
o_game_options[i].list_item[8] = "120"
|
||||
o_game_options[i].list_item[2] = "3"
|
||||
o_game_options[i].list_item[3] = "4"
|
||||
o_game_options[i].list_item[4] = "5"
|
||||
o_game_options[i].list_item[5] = "6"
|
||||
o_game_options[i].list_item[6] = "7"
|
||||
o_game_options[i].list_item[7] = "8"
|
||||
o_game_options[i].list_item[8] = "9"
|
||||
o_game_options[i].list_item[9] = "10"
|
||||
o_game_options[i].list_item[10] = "11"
|
||||
o_game_options[i].list_item[11] = "12"
|
||||
o_game_options[i].list_item[12] = "13"
|
||||
o_game_options[i].list_item[13] = "14"
|
||||
o_game_options[i].list_item[14] = "15"
|
||||
o_game_options[i].list_item[15] = "20"
|
||||
o_game_options[i].list_item[16] = "25"
|
||||
o_game_options[i].list_item[17] = "30"
|
||||
o_game_options[i].list_item[18] = "45"
|
||||
o_game_options[i].list_item[19] = "60"
|
||||
o_game_options[i].list_item[20] = "120"
|
||||
o_game_options[i].list_item[21] = "180"
|
||||
o_game_options[i].list_item[22] = "240"
|
||||
|
||||
if callback($$GetLocalNetworkMissionParamater$$, time_limit) == 0
|
||||
o_game_options[i].nselected = 3
|
||||
o_game_options[i].nselected = 6 // [LYLT] index of "7"
|
||||
else
|
||||
{
|
||||
for int doh = 0; doh< 9; doh++
|
||||
for int doh = 0; doh< 23; doh++
|
||||
{
|
||||
if makeint(o_game_options[i].list_item[doh]) >= callback($$GetLocalNetworkMissionParamater$$, time_limit)
|
||||
{
|
||||
o_game_options[i].nselected = doh
|
||||
doh = 9
|
||||
doh = 23
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,15 +805,15 @@ main
|
||||
//------------------------------------------------------------------------
|
||||
i = 5
|
||||
if callback($$GetLocalNetworkMissionParamater$$, time_limit) == 0
|
||||
o_game_options[i].nselected = 3
|
||||
o_game_options[i].nselected = 6 // [LYLT] index of "7"
|
||||
else
|
||||
{
|
||||
for int doh = 0; doh< 9; doh++
|
||||
for int doh = 0; doh< 23; doh++
|
||||
{
|
||||
if makeint(o_game_options[i].list_item[doh]) >= callback($$GetLocalNetworkMissionParamater$$, time_limit)
|
||||
{
|
||||
o_game_options[i].nselected = doh
|
||||
doh = 9
|
||||
doh = 23
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# 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` | `<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`](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 `<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.
|
||||
Reference in New Issue
Block a user