Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/dev/release-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ is `docs/dev/macos-moltenvk-decision.md`.
- [x] Windows x64 engine and standalone game-module builds are warning-clean again. Legacy native-width conversions now use checked boundaries or native-width accounting, intentional allocator alignment is explicit and layout-guarded, dead SDK remnants are removed, and the cleaned warning classes fail the build if they return. SDL configuration no longer reports a false missing-libdecor-version warning on platforms that do not use libdecor, and the Windows validation wrapper now forwards an explicitly selected companion repository correctly instead of treating its parameter name as a path.
- [x] Bound controls are now easier to recognize at a glance: Settings key-bind rows show a bounded summary across keyboard, mouse, and controller inputs, while supported in-game prompts follow the device family the player used most recently. Keyboard bindings use clean labeled keycaps with square arrow symbols, upright mouse silhouettes fill the bound physical button without ambiguous numbers, and controller bindings use neutral positional graphics plus a localized Back/View label rather than assuming an Xbox, PlayStation, or Nintendo legend scheme. Inline icons now sit at nearly the full text-line height, important centered multiplayer prompts use a larger 150% treatment, and higher-sample rounded edges keep the procedural button art smooth at modern resolutions; the spectator HUD uses the same presentation for its follow-cycle and exit controls. Bind capture is safer too: Escape, Start, or Back cancels without erasing the action, while Backspace or Delete explicitly clears it.
- [x] Mods and server packages can now change a stock map's runtime entities without editing or redistributing the original map: a matching `.ent` supplies a complete entity-string replacement, while `.entx` safely appends point entities after that replacement or the stock list. Both formats are bounded, validate atomically, reject geometry and unsafe or ambiguous keys/names, preserve original collision loading, participate in map reload detection, and remain excluded from editor/export writes.
- [x] Single-player Arena matches now advance from their entrance ceremony into the countdown and live scoring phase. Disabling ready-up also transactionally zeroes the ready threshold required by the match-rule validator, preventing the clean-profile default from leaving combat in an infinite, non-scoring warmup with both scores stuck at zero. Each card now freshly imports its authored limits through the casual match profile while preserving the player's archived profile, so consecutive deathmatch-backed cards cannot reuse the previous card's time and frag limits. This fixes GitHub issue #110.
- [x] Single-player now includes an Arena Campaign beside the original Mission path: five escalating tiers combine Duel, Deathmatch, Team Deathmatch, Red Rover, and Clan Arena across twenty stock-map matches, with hand-picked character bots, boss fights, adjustable difficulty, persistent wins, and clear unlock progress. Tier briefings, effective-skill previews, a visual progress bar, distinct retry/replay/next actions, recommended keyboard and controller focus, hidden boss identities, explicit draw results, and reset confirmation keep the ladder readable and protect earned progress. The first three wins in each tier open its boss match, and defeating that boss opens the next tier; the final Makron duel now uses the distinct Stroggenomenon tournament arena. Each match now has an Arena-only ceremony: its challenge card expands into a cinematic no-ready entrance, the terminal score freezes the field for a collision-aware depth-of-field orbit around the victor, and the return passes through a staged report before visibly recording the win and pulsing new boss or tier gates on the ladder. Exact authored rosters, pre-countdown Duel contender assignment, authoritative score reporting, a campaign-owned result screen, and transactional restoration of temporary local-server settings make launches, completion, replay, disconnect, and quit paths deterministic. The ladder uses only required retail Quake 4 maps and openQ4's game modules, so no community map pack or replacement assets are needed.
- [x] The Demo Library, Single Player selector, and Arena browser now share Quake 4's stock rails, grid, panels, controls, and popup silhouette. Tiled chrome, full-viewport shades, and edge-anchored playback treatment fill additional horizontal and vertical display space without stretching the readable cards. Deliberate, input-safe transitions carry players from the main menu into Mission or Arena, between Arena views, and back again; campaign reset, progress, and result dialogs now expand on both axes while keeping their modern staged feedback within the original menu family. A final readability pass adds persistent Demo filter feedback, bounded browser columns with a selected-demo identity line, compact localized Arena card labels, roomier translated briefings, and modal exits that cover their nested content before closing.
- [x] Multiplayer bots now complete deliberate obstacle traversals instead of skipping their landing corners and jumping in place below elevated goals. Jump, drop, jump-pad, and teleporter routes retain their entry and exit; jumps are confirmed by leaving the ground and landing on the destination side, combat steering cannot disrupt them mid-action, and stuck detection measures real horizontal progress. Goal selection also respects one-way navigation, preventing bots from gathering below ledges they cannot climb. This fixes the two-jump route to the yellow armour crates on q4dm1 without map-specific content.
Expand Down
17 changes: 17 additions & 0 deletions src/framework/ArenaCampaign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,10 @@ static const char *ARENA_SAVED_CVARS[] = {
"si_roundWarmupDelay",
"si_roundEndDelay",
"g_gameReviewPause",
"g_matchProfile",
"si_tourneyLimit",
"si_useReady",
"si_warmupReadyPercentage",
"si_allowVoting",
"si_isBuyingEnabled",
"si_dropWeaponsInBuyingModes",
Expand Down Expand Up @@ -2011,6 +2013,17 @@ bool idArenaCampaign::PrepareServer() {
// so this incremental save also captures settings unavailable in game_sp.
ArenaSaveCurrentCVars( state );

// Arena authors its limits through the casual legacy-CVar adapter. Force a
// fresh import for every match: the multiplayer object survives MapShutdown,
// and otherwise two consecutive DM-backed cards can reuse the first card's
// committed rules. The player's archived profile is restored with the other
// transactional overrides after the match.
cvarSystem->SetCVarString( "g_matchProfile", "casual" );
idCVar *matchProfile = cvarSystem->Find( "g_matchProfile" );
if ( matchProfile != NULL ) {
matchProfile->SetModified();
}

cvarSystem->SetCVarInteger( "net_serverDedicated", 0 );
cvarSystem->SetCVarBool( "net_LANServer", true );
// A full engine reload during ExecuteMapChange replays a bare "spawnServer"
Expand Down Expand Up @@ -2041,6 +2054,10 @@ bool idArenaCampaign::PrepareServer() {
ArenaSetMatchCVar( "g_gameReviewPause", 60 );
ArenaSetMatchCVar( "si_tourneyLimit", 1 );
cvarSystem->SetCVarBool( "si_useReady", false );
// The typed match-rule adapter requires a zero threshold when readiness is
// disabled. Leaving the archived 0.51 default in place rejects the complete
// rule draft and strands Arena play in non-scoring WARMUP forever.
ArenaSetMatchCVar( "si_warmupReadyPercentage", 0 );
cvarSystem->SetCVarBool( "si_allowVoting", false );
cvarSystem->SetCVarBool( "si_isBuyingEnabled", false );
cvarSystem->SetCVarBool( "si_dropWeaponsInBuyingModes", false );
Expand Down
39 changes: 39 additions & 0 deletions tools/tests/arena_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,45 @@ def validate_engine_hooks() -> None:
raise AssertionError("Could not inspect Arena overridden-CVar snapshot list")
saved_cvars = set(re.findall(r'"([A-Za-z0-9_]+)"', saved_cvars_match.group("body")))
prepare_body = function_body(arena, "bool idArenaCampaign::PrepareServer()")
for saved_rule_cvar in ("g_matchProfile", "si_warmupReadyPercentage"):
if saved_rule_cvar not in saved_cvars:
raise AssertionError(
f"Arena must restore the rule-boundary CVar {saved_rule_cvar!r}"
)

save_current_cvars = prepare_body.find("ArenaSaveCurrentCVars( state );")
casual_profile = prepare_body.find(
'cvarSystem->SetCVarString( "g_matchProfile", "casual" );'
)
find_profile = prepare_body.find(
'idCVar *matchProfile = cvarSystem->Find( "g_matchProfile" );'
)
refresh_profile = prepare_body.find("matchProfile->SetModified();")
first_match_override = prepare_body.find(
'cvarSystem->SetCVarInteger( "net_serverDedicated", 0 );'
)
if not (
0
<= save_current_cvars
< casual_profile
< find_profile
< refresh_profile
< first_match_override
):
raise AssertionError(
"Arena must save, force, and explicitly refresh its casual match profile "
"before importing per-card rules"
)
readiness_disabled = prepare_body.find(
'cvarSystem->SetCVarBool( "si_useReady", false );'
)
readiness_threshold_zeroed = prepare_body.find(
'ArenaSetMatchCVar( "si_warmupReadyPercentage", 0 );'
)
if not 0 <= readiness_disabled < readiness_threshold_zeroed:
raise AssertionError(
"Arena must pair disabled readiness with a zero ready threshold before rule import"
)
overridden_cvars = set(
re.findall(
r'(?:ArenaSetMatchCVar|cvarSystem->SetCVar(?:Bool|Float|Integer|String))'
Expand Down
Loading