From b42ce33a26e4a2942f259c26e2c6f703faf4a7e3 Mon Sep 17 00:00:00 2001 From: themuffinator Date: Thu, 3 Sep 2026 09:14:07 +0100 Subject: [PATCH 1/3] Give multiplayer a compact join card over a softened map The connect-time join screen was the whole in-game menu on its Join tab. It is now its own panel: gametype and map, whether the match is in warmup, the scoring limit, how full the server is, the server name, and only the choices that mode allows - join, pick a side, or spectate. The panel is authored inside the plain 640x480 canvas and never reads gui::virtual_screen_x_expand, so it keeps a 4:3 shape and stays centred on a wide display instead of stretching across it. It deliberately does not dim the rest of the screen; the game module softens the live view behind it instead, which is what makes a partial-screen card readable. onActivate routes to the panel when the game module publishes initial_join and otherwise restores the menu chrome and its intro animation, which is what previously re-showed the chrome a frame after the join event had hidden it. Escape from the panel closes it and leaves the player spectating; the full menu is one more Escape away. A refused team switch reports in place on the card. The interactive multiplayer launch configurations now pass ui_autoJoin 0, the shipped default, so a manual run starts at the join screen. Automated multiplayer profiles still set it to 1 explicitly, and AGENTS.md now says which is which rather than implying one rule. Co-Authored-By: Claude Opus 5 --- .vscode/launch.json | 196 ++++----- AGENTS.md | 2 +- content/baseoq4/pak0/guis/mpmain.gui | 404 +++++++++++++++++- .../baseoq4/pak0/strings/english_openq4.lang | 8 + .../baseoq4/pak0/strings/french_openq4.lang | 8 + .../baseoq4/pak0/strings/italian_openq4.lang | 8 + .../baseoq4/pak0/strings/polish_openq4.lang | 8 + .../baseoq4/pak0/strings/russian_openq4.lang | 8 + .../baseoq4/pak0/strings/spanish_openq4.lang | 8 + docs/dev/release-completion.md | 2 +- docs/dev/releases/v0.12.0.md | 2 + 11 files changed, 546 insertions(+), 108 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c4be84673..b15edc2d9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -60,7 +60,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -129,7 +129,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -198,7 +198,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -267,7 +267,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -336,7 +336,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -405,7 +405,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -474,7 +474,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -543,7 +543,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -612,7 +612,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -681,7 +681,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -750,7 +750,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -819,7 +819,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -888,7 +888,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -957,7 +957,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -1026,7 +1026,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -1095,7 +1095,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -1164,7 +1164,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -1233,7 +1233,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -1302,7 +1302,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -1371,7 +1371,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -1440,7 +1440,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -1509,7 +1509,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -1578,7 +1578,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -1647,7 +1647,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -1716,7 +1716,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -1785,7 +1785,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -1854,7 +1854,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -1923,7 +1923,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -1992,7 +1992,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -2061,7 +2061,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -2130,7 +2130,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -2199,7 +2199,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -2268,7 +2268,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -2337,7 +2337,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -2406,7 +2406,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -2475,7 +2475,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -2544,7 +2544,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -2613,7 +2613,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -2682,7 +2682,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -2751,7 +2751,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -2820,7 +2820,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -2889,7 +2889,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -2958,7 +2958,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -3027,7 +3027,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -3096,7 +3096,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -3165,7 +3165,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -3234,7 +3234,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -3303,7 +3303,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -3372,7 +3372,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -3441,7 +3441,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -3510,7 +3510,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -3579,7 +3579,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -3648,7 +3648,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -3717,7 +3717,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -3786,7 +3786,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -3855,7 +3855,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -3924,7 +3924,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -3993,7 +3993,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -4062,7 +4062,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -4131,7 +4131,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -4200,7 +4200,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -4269,7 +4269,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -4338,7 +4338,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -4407,7 +4407,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -4476,7 +4476,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -4545,7 +4545,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -4614,7 +4614,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -4683,7 +4683,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -4752,7 +4752,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -4821,7 +4821,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -4890,7 +4890,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -4959,7 +4959,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -5028,7 +5028,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -5097,7 +5097,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -5166,7 +5166,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -5235,7 +5235,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -5304,7 +5304,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -5373,7 +5373,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -5442,7 +5442,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -5511,7 +5511,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -5580,7 +5580,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -5649,7 +5649,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -5718,7 +5718,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -5787,7 +5787,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -5856,7 +5856,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -5925,7 +5925,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -5994,7 +5994,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -6063,7 +6063,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -6132,7 +6132,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -6201,7 +6201,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -6270,7 +6270,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -6339,7 +6339,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -6408,7 +6408,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -6477,7 +6477,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -6546,7 +6546,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "gl", @@ -6615,7 +6615,7 @@ "0", "+set", "ui_autoJoin", - "1", + "0", "+set", "r_renderApi", "vulkan", @@ -10669,7 +10669,7 @@ "DM", "+set", "ui_autoJoin", - "1", + "0", "+set", "si_pure", "0", @@ -10736,7 +10736,7 @@ "DM", "+set", "ui_autoJoin", - "1", + "0", "+set", "si_pure", "0", diff --git a/AGENTS.md b/AGENTS.md index e2bb7e635..a92bbca65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,7 @@ This file describes project goals, rules, and upstream credits for anyone workin - Any existing custom `q4base/` content is treated as an expedient bootstrap, not a long-term solution. The goal is to remove this reliance by fixing engine compatibility issues rather than shipping replacement assets. - For investigations, reference the log file written by `logFileName` (VS Code launch uses `logs/openq4.log`), located under `fs_savepath\\` (e.g. `${workspaceFolder}\\.home\\baseoq4\\logs\\openq4.log`). - For runtime validation, use mode-specific launch tasks: use the SP launch task for single-player testing and the MP launch task for multiplayer testing. -- Keep `ui_autoJoin 1` enabled for multiplayer testing so clients enter gameplay automatically. Use an explicit `+set ui_autoJoin 0` only when the test specifically targets the join menu or initial spectator/join flow; do not rely on omission because the setting is archived. +- Automated multiplayer profiles (`renderer_gameplay_benchmark.py`, `stock_asset_baseline.py`, the dedicated smokes) keep an explicit `+set ui_autoJoin 1` so clients enter gameplay without menu input. The interactive VS Code MP launch configurations instead pass `+set ui_autoJoin 0`, which is the shipped default and starts at the join screen. Always set the value explicitly; never rely on omission, because the CVar is archived. - Do not treat main-menu startup as sufficient validation; enter in-game/map gameplay relevant to the change before concluding tests. - For macOS testing/debugging, use the compliant Apple-hardware VM/host workflow in `docs/dev/macos-vm-testing-workflow.md` and `tools/macos/Invoke-openQ4MacOSWorkflow.ps1`. Keep macOS installer/restore-image inventory under `E:\ISO\macos\`. Do not use Windows VMware macOS unlocker/Hackintosh-style setup guides for openQ4 automation. - Use `.tmp/` directory in repository for any temporary files required for tasks. diff --git a/content/baseoq4/pak0/guis/mpmain.gui b/content/baseoq4/pak0/guis/mpmain.gui index b5219878f..43d3b9f5b 100644 --- a/content/baseoq4/pak0/guis/mpmain.gui +++ b/content/baseoq4/pak0/guis/mpmain.gui @@ -22,6 +22,9 @@ windowDef Desktop float "curr" 0 float "match_tab" 0 float "match_confirm" 0 + // Set while the compact connect-time join panel owns the menu instead + // of the full in-game chrome. + float "quickjoin" 0 float "team_strogg_offset" "desktop::itemheight" * "gui::num_marine_players" + 16 float "team_marine_offset" "desktop::itemheight" * "gui::num_strogg_players" + 16 float "strogg_offset" 0 @@ -70,7 +73,11 @@ windowDef Desktop } onESC { - if ( "desktop::match_confirm" != 0 ) { + if ( "desktop::quickjoin" == 1 ) { + // Backing out of the join panel leaves the player spectating; the + // full menu is one more ESC away. + set "cmd" "play main_menu_selection close" ; + } else if ( "desktop::match_confirm" != 0 ) { set "desktop::match_confirm" "0" ; } else if ( "desktop::ingame" == 1 && ( "desktop::curr" <= 14 || "desktop::curr" == 21 ) ) { set "gui::match_referee_credential" "" ; @@ -111,11 +118,13 @@ windowDef Desktop onNamedEvent changeteam_marine_no { set "join_t_msg::text" "#str_202039" ; + set "qj_msg::text" "#str_202039" ; resettime "anim_changeteam" "0" ; } onNamedEvent changeteam_marine_same { set "join_t_msg::text" "#str_202040" ; + set "qj_msg::text" "#str_202040" ; resettime "anim_changeteam" "0" ; } @@ -125,11 +134,13 @@ windowDef Desktop onNamedEvent changeteam_strogg_no { set "join_t_msg::text" "#str_202041" ; + set "qj_msg::text" "#str_202041" ; resettime "anim_changeteam" "0" ; } onNamedEvent changeteam_strogg_same { set "join_t_msg::text" "#str_202042" ; + set "qj_msg::text" "#str_202042" ; resettime "anim_changeteam" "0" ; } @@ -198,11 +209,30 @@ windowDef Desktop set "p_join::visible" "1" ; - resettime "anim_in" "0" ; + // openQ4: a connect-time join is answered by the compact join panel, + // not by the whole in-game menu, so the chrome and its intro + // animation stay down until the player asks for the menu. + if ( "gui::initial_join" == 1 ) { + set "desktop::quickjoin" "1" ; + set "desktop::dest" "2" ; + set "desktop::curr" "2" ; + set "p_main::visible" "0" ; + set "p_btns::visible" "0" ; + set "p_quickjoin::visible" "1" ; + set "qj_msg::visible" "0" ; + } else { + set "desktop::quickjoin" "0" ; + set "p_main::visible" "1" ; + set "p_btns::visible" "1" ; + set "p_quickjoin::visible" "0" ; + + resettime "anim_in" "0" ; + } } onNamedEvent initialJoin { set "desktop::active" "0" ; + set "desktop::quickjoin" "1" ; set "desktop::dest" "2" ; set "desktop::curr" "2" ; @@ -218,9 +248,6 @@ windowDef Desktop namedevent "p_btns::hideVote" ; namedevent "p_btns::hideVcfg" ; - set "p_join::rect" "0,0,640,480" ; - set "p_join::visible" "1" ; - set "p_joinblock::visible" "1" ; set "p_players::visible" "0" ; set "p_vote::visible" "0" ; set "p_settings::visible" "0" ; @@ -230,8 +257,12 @@ windowDef Desktop set "p_voiceconfig::visible" "0" ; set "join_t_msg::visible" "0" ; - namedevent "p_btns::showMain" ; - namedevent "p_btns::showJoin" ; + // Only the compact panel is up: the menu chrome, its Join tab and the + // chat log all stay down until the player asks for the menu. + set "p_main::visible" "0" ; + set "p_btns::visible" "0" ; + set "p_quickjoin::visible" "1" ; + set "qj_msg::visible" "0" ; } onEvent { @@ -6105,13 +6136,15 @@ windowDef Desktop } onTime +50 { - set "join_t_msg::visible" "1" ; + set "join_t_msg::visible" "1" ; + set "qj_msg::visible" "1" ; } onTime +1500 { set "desktop::active" "0" ; set "join_t_msg::visible" "0" ; + set "qj_msg::visible" "0" ; namedevent "p_btns::showMain" ; namedevent "p_btns::showJoin" ; @@ -9454,4 +9487,359 @@ windowDef Desktop } } + + // ------------------------------------------------------------------- + // Compact connect-time join panel. + // + // Everything here is authored inside the plain 640x480 canvas and never + // reads "gui::virtual_screen_x_expand", so the panel keeps its 4:3 shape + // and stays centred on a wide screen instead of stretching across it. + // The live map behind it is pushed out of focus by the game module + // (idMultiplayerGame::SetJoinScreenSoftFocus), so the panel deliberately + // does not dim the rest of the screen. + // + // The button set comes from "gui::join_team_mode" / "gui::join_tourney_mode" + // rather than a copy of the gametype table. + // ------------------------------------------------------------------- + windowDef p_quickjoin + { + rect 0,0,640,480 + visible 0 + + windowDef qj_panel + { + rect 116,140,408,200 + backcolor 0.035,0.045,0.032,0.94 + bordercolor 0.45,0.5,0.27,1 + bordersize 1 + noevents 1 + } + windowDef qj_panel_grad + { + rect 117,141,406,198 + background "gfx/guis/mainmenu/bg_darkgrad" + matcolor 0.545,0.588,0.294,0.14 + noevents 1 + } + + windowDef qj_title + { + rect 132,152,376,26 + forecolor 0.96,0.97,0.9,1 + text "gui::join_match_title" + textscale 0.3 + font "fonts/marine" + noevents 1 + } + windowDef qj_rule + { + rect 132,180,376,1 + backcolor 0.45,0.5,0.27,0.8 + noevents 1 + } + windowDef qj_state + { + rect 132,188,190,20 + forecolor 0.95,0.72,0.25,1 + text "gui::join_match_state" + textscale 0.22 + font "fonts/lowpixel" + noevents 1 + } + windowDef qj_limit + { + rect 132,212,190,16 + forecolor 0.72,0.76,0.63,1 + text "gui::join_match_limit" + textscale 0.17 + font "fonts/lowpixel" + noevents 1 + } + windowDef qj_players + { + rect 132,228,190,16 + forecolor 0.72,0.76,0.63,1 + text "gui::join_match_players" + textscale 0.17 + font "fonts/lowpixel" + noevents 1 + } + windowDef qj_server + { + rect 132,244,190,16 + forecolor 0.55,0.58,0.48,1 + text "gui::join_match_server" + textscale 0.17 + font "fonts/lowpixel" + noevents 1 + } + + // -------------------------------------------------- free-for-all modes + windowDef qj_b_join + { + rect 336,196,172,34 + visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) + backcolor 0.30,0.10,0.07,0.96 + bordercolor 0.95,0.72,0.25,1 + bordersize 1 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + set "backcolor" "0.45,0.16,0.09,0.98" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + set "backcolor" "0.30,0.10,0.07,0.96" ; + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection join auto close" ; + } + } + } + windowDef qj_t_join + { + rect 336,203,172,20 + visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) + forecolor 0.98,0.98,0.94,1 + text "#str_200187" + textscale 0.28 + textalign 1 + font "fonts/lowpixel" + noevents 1 + } + + // ---------------------------------------------------------- Tourney + windowDef qj_b_tourney + { + rect 336,196,172,34 + visible ( "gui::join_tourney_mode" == 1 ) + backcolor 0.30,0.10,0.07,0.96 + bordercolor 0.95,0.72,0.25,1 + bordersize 1 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + set "backcolor" "0.45,0.16,0.09,0.98" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + set "backcolor" "0.30,0.10,0.07,0.96" ; + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection toggleTourney close" ; + } + } + } + windowDef qj_t_tourney + { + rect 336,203,172,20 + visible ( "gui::join_tourney_mode" == 1 ) + forecolor 0.98,0.98,0.94,1 + text "gui::toggleTourneyButton" + textscale 0.28 + textalign 1 + font "fonts/lowpixel" + noevents 1 + } + + // ------------------------------------------------------- team modes + windowDef qj_b_auto + { + rect 336,188,172,32 + visible ( "gui::join_team_mode" == 1 ) + backcolor 0.30,0.10,0.07,0.96 + bordercolor 0.95,0.72,0.25,1 + bordersize 1 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + set "backcolor" "0.45,0.16,0.09,0.98" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + set "backcolor" "0.30,0.10,0.07,0.96" ; + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection join auto close" ; + } + } + } + windowDef qj_t_auto + { + rect 336,195,172,20 + visible ( "gui::join_team_mode" == 1 ) + forecolor 0.98,0.98,0.94,1 + text "#str_200192" + textscale 0.28 + textalign 1 + font "fonts/lowpixel" + noevents 1 + } + windowDef qj_b_marine + { + rect 336,224,172,28 + visible ( "gui::join_team_mode" == 1 ) + backcolor 0.07,0.075,0.055,0.98 + bordercolor 0.3,0.34,0.2,1 + bordersize 1 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + set "backcolor" "0.27,0.15,0.04,0.98" ; + set "bordercolor" "0.95,0.49,0.06,1" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + set "backcolor" "0.07,0.075,0.055,0.98" ; + set "bordercolor" "0.3,0.34,0.2,1" ; + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection" ; + consoleCMD "CheckTeamBalance marine changeteam_marine_yes changeteam_marine_no changeteam_marine_same" ; + } + } + } + windowDef qj_t_marine + { + rect 336,230,172,18 + visible ( "gui::join_team_mode" == 1 ) + forecolor 0.86,0.88,0.8,1 + text "#str_200193" + textscale 0.24 + textalign 1 + font "fonts/lowpixel" + noevents 1 + } + windowDef qj_b_strogg + { + rect 336,256,172,28 + visible ( "gui::join_team_mode" == 1 ) + backcolor 0.07,0.075,0.055,0.98 + bordercolor 0.3,0.34,0.2,1 + bordersize 1 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + set "backcolor" "0.27,0.15,0.04,0.98" ; + set "bordercolor" "0.95,0.49,0.06,1" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + set "backcolor" "0.07,0.075,0.055,0.98" ; + set "bordercolor" "0.3,0.34,0.2,1" ; + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection" ; + consoleCMD "CheckTeamBalance strogg changeteam_strogg_yes changeteam_strogg_no changeteam_strogg_same" ; + } + } + } + windowDef qj_t_strogg + { + rect 336,262,172,18 + visible ( "gui::join_team_mode" == 1 ) + forecolor 0.86,0.88,0.8,1 + text "#str_200194" + textscale 0.24 + textalign 1 + font "fonts/lowpixel" + noevents 1 + } + + // ------------------------------------------------------- spectating + windowDef qj_b_spectate + { + rect 336,238,172,30 + visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) + backcolor 0.07,0.075,0.055,0.98 + bordercolor 0.3,0.34,0.2,1 + bordersize 1 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + set "backcolor" "0.27,0.15,0.04,0.98" ; + set "bordercolor" "0.95,0.49,0.06,1" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + set "backcolor" "0.07,0.075,0.055,0.98" ; + set "bordercolor" "0.3,0.34,0.2,1" ; + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection join spectator close" ; + } + } + } + windowDef qj_t_spectate + { + rect 336,244,172,18 + visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) + forecolor 0.86,0.88,0.8,1 + text "#str_200195" + textscale 0.24 + textalign 1 + font "fonts/lowpixel" + noevents 1 + } + windowDef qj_b_tspectate + { + rect 336,288,172,28 + visible ( "gui::join_team_mode" == 1 ) + backcolor 0.07,0.075,0.055,0.98 + bordercolor 0.3,0.34,0.2,1 + bordersize 1 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + set "backcolor" "0.27,0.15,0.04,0.98" ; + set "bordercolor" "0.95,0.49,0.06,1" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + set "backcolor" "0.07,0.075,0.055,0.98" ; + set "bordercolor" "0.3,0.34,0.2,1" ; + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection join spectator close" ; + } + } + } + windowDef qj_t_tspectate + { + rect 336,294,172,18 + visible ( "gui::join_team_mode" == 1 ) + forecolor 0.86,0.88,0.8,1 + text "#str_200195" + textscale 0.24 + textalign 1 + font "fonts/lowpixel" + noevents 1 + } + + // Team-balance refusals surface here instead of the Join tab's notice. + windowDef qj_msg + { + rect 132,316,376,18 + visible 0 + forecolor 0.95,0.6,0.1,1 + text "" + textscale 0.19 + font "fonts/lowpixel" + noevents 1 + } + } } diff --git a/content/baseoq4/pak0/strings/english_openq4.lang b/content/baseoq4/pak0/strings/english_openq4.lang index 7d430cba5..c97a85f9f 100644 --- a/content/baseoq4/pak0/strings/english_openq4.lang +++ b/content/baseoq4/pak0/strings/english_openq4.lang @@ -915,4 +915,12 @@ "#str_42801" "%s drowned" "#str_42802" "%s dissolved in slime" "#str_42803" "%s burned in lava" + + "#str_42810" "%d/%d PLAYERS" + "#str_42811" "MATCH WARMUP" + "#str_42812" "MATCH STARTING" + "#str_42813" "MATCH IN PROGRESS" + "#str_42814" "SUDDEN DEATH" + "#str_42815" "MATCH OVER" + "#str_42816" "WAITING FOR PLAYERS" } diff --git a/content/baseoq4/pak0/strings/french_openq4.lang b/content/baseoq4/pak0/strings/french_openq4.lang index 7ca670311..09bf82405 100644 --- a/content/baseoq4/pak0/strings/french_openq4.lang +++ b/content/baseoq4/pak0/strings/french_openq4.lang @@ -915,4 +915,12 @@ "#str_42801" "%s s'est noyé" "#str_42802" "%s s'est dissous dans la vase" "#str_42803" "%s a brûlé dans la lave" + + "#str_42810" "%d/%d JOUEURS" + "#str_42811" "ÉCHAUFFEMENT" + "#str_42812" "DÉBUT DU MATCH" + "#str_42813" "MATCH EN COURS" + "#str_42814" "MORT SUBITE" + "#str_42815" "MATCH TERMINÉ" + "#str_42816" "EN ATTENTE DE JOUEURS" } diff --git a/content/baseoq4/pak0/strings/italian_openq4.lang b/content/baseoq4/pak0/strings/italian_openq4.lang index c24907eaf..1980814e1 100644 --- a/content/baseoq4/pak0/strings/italian_openq4.lang +++ b/content/baseoq4/pak0/strings/italian_openq4.lang @@ -915,4 +915,12 @@ "#str_42801" "%s è annegato" "#str_42802" "%s si è dissolto nella melma" "#str_42803" "%s è bruciato nella lava" + + "#str_42810" "%d/%d GIOCATORI" + "#str_42811" "RISCALDAMENTO" + "#str_42812" "AVVIO PARTITA" + "#str_42813" "PARTITA IN CORSO" + "#str_42814" "MORTE IMPROVVISA" + "#str_42815" "PARTITA CONCLUSA" + "#str_42816" "IN ATTESA DI GIOCATORI" } diff --git a/content/baseoq4/pak0/strings/polish_openq4.lang b/content/baseoq4/pak0/strings/polish_openq4.lang index cb2c07d80..26c456f11 100644 --- a/content/baseoq4/pak0/strings/polish_openq4.lang +++ b/content/baseoq4/pak0/strings/polish_openq4.lang @@ -915,4 +915,12 @@ "#str_42801" "%s utonął" "#str_42802" "%s rozpuścił się w szlamie" "#str_42803" "%s spłonął w lawie" + + "#str_42810" "%d/%d GRACZY" + "#str_42811" "ROZGRZEWKA" + "#str_42812" "START MECZU" + "#str_42813" "MECZ W TOKU" + "#str_42814" "NAGŁA ŚMIERĆ" + "#str_42815" "KONIEC MECZU" + "#str_42816" "OCZEKIWANIE NA GRACZY" } diff --git a/content/baseoq4/pak0/strings/russian_openq4.lang b/content/baseoq4/pak0/strings/russian_openq4.lang index 30d4da6b3..6ef27c7aa 100644 --- a/content/baseoq4/pak0/strings/russian_openq4.lang +++ b/content/baseoq4/pak0/strings/russian_openq4.lang @@ -915,4 +915,12 @@ "#str_42801" "%s утонул" "#str_42802" "%s растворился в слизи" "#str_42803" "%s сгорел в лаве" + + "#str_42810" "%d/%d ИГРОКОВ" + "#str_42811" "РАЗМИНКА" + "#str_42812" "МАТЧ НАЧИНАЕТСЯ" + "#str_42813" "МАТЧ ИДЁТ" + "#str_42814" "ВНЕЗАПНАЯ СМЕРТЬ" + "#str_42815" "МАТЧ ОКОНЧЕН" + "#str_42816" "ОЖИДАНИЕ ИГРОКОВ" } diff --git a/content/baseoq4/pak0/strings/spanish_openq4.lang b/content/baseoq4/pak0/strings/spanish_openq4.lang index a44228cc1..859ebcf00 100644 --- a/content/baseoq4/pak0/strings/spanish_openq4.lang +++ b/content/baseoq4/pak0/strings/spanish_openq4.lang @@ -915,4 +915,12 @@ "#str_42801" "%s se ahogó" "#str_42802" "%s se disolvió en el cieno" "#str_42803" "%s ardió en la lava" + + "#str_42810" "%d/%d JUGADORES" + "#str_42811" "CALENTAMIENTO" + "#str_42812" "LA PARTIDA VA A EMPEZAR" + "#str_42813" "PARTIDA EN CURSO" + "#str_42814" "MUERTE SÚBITA" + "#str_42815" "PARTIDA TERMINADA" + "#str_42816" "ESPERANDO JUGADORES" } diff --git a/docs/dev/release-completion.md b/docs/dev/release-completion.md index ff486c3eb..8d5e778f5 100644 --- a/docs/dev/release-completion.md +++ b/docs/dev/release-completion.md @@ -223,7 +223,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Experimental macOS support now has an explicit matrix policy: `docs/dev/macos-support-matrix-policy.md` records the current Apple Silicon/arm64-only release scope, keeps Intel Mac, universal2, and Rosetta outside published support, and requires signoff evidence for both the macOS 11 floor and the latest public macOS before any first-class macOS claim can ship. macOS signoff reports and evidence records now include architecture policy, CPU architecture, OS matrix role, Xcode version, and macOS SDK version so release notes can match the actual Apple toolchain and OS coverage. - [x] Experimental macOS package UX is release-gated: the macOS Package Layout And Release Policy records the self-contained app contract and requires mounted-DMG, independently dragged-app, whole-package loose-tool, terminal, embedded-path, path-resolution, and Gatekeeper signoff evidence before macOS support claims can grow beyond the current experimental tier. The manual release workflow has an explicit first-class macOS support tier that fails instead of publishing unsigned fallback packages when signing/notarization credentials are missing. - [x] Experimental macOS runtime signoff is now evidence-gated: `docs/dev/macos-signoff-evidence.md` records the active run ID, archive checksum, hardware/OS details, OpenGL/Metal bridge coverage, OpenAL provider, package artifacts, openQ4 and `openQ4-game` commits, SP/MP evidence, and release-note limitations; `tools/macos/record_signoff_evidence.py` turns a completed-checklist archive into that record, while collected signoff archives now require a dedicated `renderer-mp-smoke` evidence folder before validation accepts the hardware signoff bundle. -- [x] Multiplayer server entry is now more deliberate and Quake Live-like: new clients start as spectators at the map intermission camera when available, see a Q4-styled join screen with server details and gametype-aware join options, and can set `ui_autoJoin 1` to keep the old immediate-join behavior. +- [x] Multiplayer server entry is now more deliberate and Quake Live-like: new clients start as spectators at the map intermission camera when available, see a Q4-styled join screen with server details and gametype-aware join options, and can set `ui_autoJoin 1` to keep the old immediate-join behavior. That screen is now a compact centred panel rather than the full in-game menu: it is authored inside the plain 640x480 GUI canvas so it keeps a 4:3 shape on a widescreen display, it carries only the match summary and the join choices the gametype allows, and the live view behind it is softened through the shared special-effect blur controller. The interactive VS Code multiplayer launch configurations pass `ui_autoJoin 0` so manual runs exercise it; automated multiplayer profiles still pass `ui_autoJoin 1`. - [x] Pull-down console mouse selection and clicks now line up with the visible cursor hotspot, so the cursor no longer appears a row below the point that receives input. - [x] Documentation now labels macOS support as experimental wherever current support or release packages are described, including the README, getting-started guide, build/platform docs, input guidance, packaged HTML docs, VM workflow, and curated release notes. - [x] Experimental macOS packaging and signoff checks now reject case-insensitive path collisions, Finder/AppleDouble metadata inside result archives, and renderer evidence that contains only placeholder directories before artifacts are accepted. diff --git a/docs/dev/releases/v0.12.0.md b/docs/dev/releases/v0.12.0.md index eae4bcd13..b6b849eb0 100644 --- a/docs/dev/releases/v0.12.0.md +++ b/docs/dev/releases/v0.12.0.md @@ -19,6 +19,7 @@ - **Long macOS sessions keep their sound instead of dropping back to the menu.** Apple Silicon packages now include OpenAL Soft, avoiding the fixed source and buffer limits in Apple's legacy OpenAL framework that could stop stock levels after sustained audio use. - **Arena Campaign scoring starts reliably.** Disabling ready-up now also clears the ready threshold, preventing deathmatch-backed Arena cards from remaining forever in a non-scoring warmup. - **Multiplayer setup is easier to control.** The server menu exposes guarded bot controls, and competitive free-for-all disclosure now accepts the active recipients intended by the match policy. +- **Joining a multiplayer match takes one click.** Connecting now opens a compact join card over the map instead of the full in-game menu. It names the gametype and map, the scoring limit, how full the server is and whether the match is in warmup, and offers just the choices that mode allows - join, pick a side, or spectate. The card keeps its shape and stays centred on a widescreen display, and the live view behind it is softened so the text stays readable. The join screen is the default again: set `ui_autoJoin 1` if you would rather drop straight into play. - **Multiplayer function keys work as shown.** F1/F2 voting, F3 ready-up, F6 team switching, and F7 spectating once again use Quake 4's real impulse actions. Ready presses travel over the reliable path, the two-line warmup instruction remains fully visible, and only the exact older openQ4 defaults are upgraded—custom bindings stay untouched. No-time-limit deathmatch also starts without a spurious competitive-rules rejection. - **Pausing single-player is immediate.** Opening the in-game menu no longer blocks on save, mod, device, display, key-binding, or multiplayer-model discovery. Those lists refresh only when their own page is opened, and the normal level-loading phase prepares the menu art, fallback image, and music before the first Escape press. - **Single-player console tuning remains under the player's control.** The weapon wheel now uses a dedicated transient slow-motion channel instead of rewriting `timescale`, so values such as `timescale 0.5` remain set after the console closes. The old forced-run shortcut is also gone: `pm_walkspeed` once again controls actual walking when Always Run is off (or the run key temporarily inverts it). The wheel still slows simulation and audio while held, then restores only its own temporary effect. @@ -76,6 +77,7 @@ - Kept view-model client entities, attached animated models, and the lightning gun beam on the exact rendered skeletal pose. - Prevented remote-client prediction replays from advancing projectile visuals twice or replacing the viewweapon's authoritative interpolation endpoint, and restored impact/fizzle visuals when a short-lived projectile is first received in a terminal state. - Fixed weapon swaps that could inherit firing state or leave stale effects behind. +- Replaced the connect-time multiplayer join screen with a compact 4:3 panel authored inside the plain GUI canvas, so it never stretches on a widescreen display. It shows gametype and map, match state, scoring limit, player count and server name, chooses its button set from the gametype descriptor instead of a copied gametype list, and reports a refused team switch in place. The live scene behind it is pushed out of focus through the existing Raven special-effect blur controller, which both the OpenGL and Vulkan backends already implement; renderers that cannot provide the pass simply draw a sharp view. The interactive VS Code multiplayer launch configurations now start at that screen rather than auto-joining. - Added complete Polish and Russian UI tables, Unicode code-point drawing, CP1250 conversion support, and stricter generated-font checksum validation. - Restored the marine hovertank engine and hover-pad loops with null-safe sound-emitter teardown. - Bundled checksum-pinned OpenAL Soft 1.25.1 in macOS release and validation packages, retained Apple's OpenAL framework only as an explicit compatibility-build option, and made backend buffer allocation or upload failures fail gracefully rather than terminating the session. From d5561a70d4c45810160f5bcfdcf456b45ed21fa3 Mon Sep 17 00:00:00 2001 From: themuffinator Date: Thu, 3 Sep 2026 10:02:49 +0100 Subject: [PATCH 2/3] Draw the join card in Quake 4's own hand, and soften the map behind it The join card was legible but generic - a flat rounded slab that could have come from any game. Quake 4's interface has a specific vocabulary and the card now speaks it. The frame is the shipped tooltip edge set, which carries an olive rail along its inside and cuts a 45-degree chamfer off every corner; the bottom cap is the top cap mirrored, the way the stock tooltip does it. The body is the menu's own screen texture under a dark gradient. Every button is a real Quake 4 plate - 45-degree cut at the bottom left, highlight down the left and along the bottom, fading away to the right - with a marine-font label and the little corner marker that is the most recognisable piece of the whole interface, and the menu's own hover: snap to orange on enter, ease back over 300ms on exit, with the label growing by a hundredth. Settings and Leave Match sit in the header as the stock gear and arrow symbols rather than as words. No new art was authored; all of it already ships in the retail PK4s. The frame comes in two heights so a gametype offering one join choice does not get a panel sized for four. The soft focus behind the panel was invisible unless you ran the classic OpenGL path. The OpenGL composite read parm 6 as a tint amount and blurred by a fixed 0.6 regardless, while the native Vulkan pass reads it as the effect strength - so a request for no tint became a request for no blur, and asking for a gentle blur produced a heavy one. The composite now reads the controller the way Vulkan does: parm 6 is strength, parm 4 converts to the same normalized falloff, and the colour tint is driven by the approach colour's own alpha. A zero-strength request draws nothing instead of a fixed blur, and the classic path, the OpenGL renderer module and Vulkan now produce the same image. This also settles the Arena Campaign's depth of field, which had the same split. docs/dev/ui-visual-design.md records the rules so the next panel does not have to be reverse-engineered from this one: the art vocabulary, palette and type scale, button and frame anatomy, the hover contract, the 4:3 authoring rule, how to soften a scene behind a partial-screen panel, and the traps - a GUI parse error is only a warning, and window properties do not resolve "$desktop::" names the way scripts do. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 1 + .../baseoq4/pak0/glprogs/rvspecial_medlabs.fs | 34 +- content/baseoq4/pak0/guis/mpmain.gui | 586 ++++++++++++++---- docs/dev/release-completion.md | 2 +- docs/dev/releases/v0.12.0.md | 7 +- docs/dev/ui-visual-design.md | 244 ++++++++ 6 files changed, 728 insertions(+), 146 deletions(-) create mode 100644 docs/dev/ui-visual-design.md diff --git a/AGENTS.md b/AGENTS.md index a92bbca65..d4493c246 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,7 @@ This file describes project goals, rules, and upstream credits for anyone workin - Treat x64 as the baseline architecture for active support while staging additional modern architectures incrementally. - Keep credits accurate and add new attributions when incorporating upstream work. - Localize all user-facing UI text: never introduce hardcoded display strings in GUIs/UI code when a `#str_*` lookup is possible; add/update language-table entries whenever new text is needed. +- Follow `docs/dev/ui-visual-design.md` for any menu, panel, or in-game GUI work: build from the shipped Quake 4 interface art, keep the 45-degree frame and button motif, and do not author new interface images. - Avoid adding engine-side content files (e.g., custom material scripts) unless absolutely required for compatibility; the goal is to run with the original game assets and only openQ4 binaries (engine + game modules, plus minimal external libs). - Any existing custom `q4base/` content is treated as an expedient bootstrap, not a long-term solution. The goal is to remove this reliance by fixing engine compatibility issues rather than shipping replacement assets. - For investigations, reference the log file written by `logFileName` (VS Code launch uses `logs/openq4.log`), located under `fs_savepath\\` (e.g. `${workspaceFolder}\\.home\\baseoq4\\logs\\openq4.log`). diff --git a/content/baseoq4/pak0/glprogs/rvspecial_medlabs.fs b/content/baseoq4/pak0/glprogs/rvspecial_medlabs.fs index c918b21ad..94452b3ff 100644 --- a/content/baseoq4/pak0/glprogs/rvspecial_medlabs.fs +++ b/content/baseoq4/pak0/glprogs/rvspecial_medlabs.fs @@ -1,3 +1,21 @@ +// Composite half of the Raven special-effect blur controller +// (SPECIAL_EFFECT_BLUR). Its parameter contract is shared with the native +// Vulkan pass in src/renderer/Vulkan/shaders/material_depth_aware_blur.frag, +// so the two backends have to read the controller the same way: +// +// parm 0-3 approachColor; rgb is the colour the blurred scene tints +// towards and a is how much of it to apply +// parm 4 effect range - the distance past the focus point over which the +// image reaches full blur, in the controller's own units +// parm 5 focus, normalized 0..1 against parm 7 +// parm 6 effect strength; 0 means the controller is off, not "blur at +// some fixed default" +// parm 7 distance scale, already consumed when Depth was written +// +// Depth here is linear view distance normalized by parm 7, so the range is +// converted the same way the Vulkan pass converts it (parm 4 * 0.125) and both +// ends measure the circle of confusion against the same normalized distance. + uniform sampler2D Depth; uniform sampler2D Blur1; uniform float effectRange; @@ -11,14 +29,14 @@ void main() { float depth = texture2D( Depth, uv ).r; vec3 blurredScene = texture2D( Blur1, uv ).rgb; - float safeRange = max( effectRange, 0.01 ); - float clearBand = max( 0.015, 1.0 / ( safeRange * 6.0 ) ); - float blurFactor = smoothstep( clearBand, clearBand * ( 1.5 + safeRange * 0.5 ), abs( depth - focus ) ); - float tintAmount = clamp( approachPercent, 0.0, 1.0 ); + float strength = clamp( approachPercent, 0.0, 1.0 ); + float blurRange = max( effectRange * 0.125, 0.002 ); + float circleOfConfusion = smoothstep( 0.0, 1.0, + clamp( abs( depth - clamp( focus, 0.0, 1.0 ) ) / blurRange, 0.0, 1.0 ) ); + float blurAmount = circleOfConfusion * strength; - float variance = sin( ( uv.x + uv.y + scroll ) * 6.28318531 ) * 0.5 + 0.5; - vec3 overlayColor = mix( blurredScene, approachColor.rgb, tintAmount * variance ); - float alpha = blurFactor * clamp( 0.6 + tintAmount * 0.4, 0.0, 1.0 ); + float tintAmount = blurAmount * clamp( approachColor.a, 0.0, 1.0 ) * 0.04; + vec3 overlayColor = mix( blurredScene, approachColor.rgb, tintAmount ); - gl_FragColor = vec4( overlayColor, alpha ); + gl_FragColor = vec4( overlayColor, blurAmount ); } diff --git a/content/baseoq4/pak0/guis/mpmain.gui b/content/baseoq4/pak0/guis/mpmain.gui index 43d3b9f5b..bccc0af49 100644 --- a/content/baseoq4/pak0/guis/mpmain.gui +++ b/content/baseoq4/pak0/guis/mpmain.gui @@ -6185,6 +6185,28 @@ windowDef Desktop } } + // The join panel's Settings symbol restores the menu chrome and then + // runs the Settings tab's own selection once anim_in has settled. + windowDef anim_qj_settings + { + rect 0,0,1,1 + visible 1 + notime 1 + + onTime 300 { + set "desktop::active" "1" ; + set "desktop::dest" "5" ; + namedevent "p_btns::hideMain" ; + namedevent "p_anims::chooseCurr" ; + namedevent "p_anims::resetMain0" ; + + set "b5::matcolor_w" "1" ; + set "t_b5::forecolor" "$desktop::orange" ; + set "b5_corner::matcolor" "$desktop::orange" ; + set "t_b5::textscale" "0.26" ; + } + + } } windowDef p_btns { @@ -9491,106 +9513,309 @@ windowDef Desktop // ------------------------------------------------------------------- // Compact connect-time join panel. // - // Everything here is authored inside the plain 640x480 canvas and never - // reads "gui::virtual_screen_x_expand", so the panel keeps its 4:3 shape - // and stays centred on a wide screen instead of stretching across it. - // The live map behind it is pushed out of focus by the game module + // Drawn in the Quake 4 menu idiom: the tooltip frame set gives the + // 45-degree corner chamfers and the olive inner rail, the header carries a + // b3_light wash and a corner marker, and every button is a Quake 4 plate + // (45-degree cut at the bottom left, highlight down the left and along the + // bottom, fading away to the right) with a marine-font label and its own + // corner marker. Read docs/dev/ui-visual-design.md before changing any of + // this - it records the rules the rest of the menus follow. + // + // Everything is authored inside the plain 640x480 canvas and never reads + // "gui::virtual_screen_x_expand", so the panel keeps its 4:3 shape and + // stays centred on a wide screen instead of stretching across it. The + // live map behind it is pushed out of focus by the game module // (idMultiplayerGame::SetJoinScreenSoftFocus), so the panel deliberately // does not dim the rest of the screen. // - // The button set comes from "gui::join_team_mode" / "gui::join_tourney_mode" - // rather than a copy of the gametype table. + // The frame comes in two heights and the button set from + // "gui::join_team_mode" / "gui::join_tourney_mode", so the panel is only + // ever as tall as the choices that gametype offers and the GUI never + // carries its own copy of the gametype table. // ------------------------------------------------------------------- windowDef p_quickjoin { rect 0,0,640,480 visible 0 - windowDef qj_panel + windowDef qj_frame_top_c + { + rect 116,140,408,16 + visible ( "gui::join_team_mode" == 0 ) + background "gfx/guis/mainmenu/tooltip_edge" + matcolor 1,1,1,0.95 + noevents 1 + } + windowDef qj_frame_mid_c { - rect 116,140,408,200 - backcolor 0.035,0.045,0.032,0.94 - bordercolor 0.45,0.5,0.27,1 - bordersize 1 + rect 116,156,408,128 + visible ( "gui::join_team_mode" == 0 ) + background "gfx/guis/mainmenu/tooltip_mid" + matcolor 1,1,1,0.95 noevents 1 } - windowDef qj_panel_grad + windowDef qj_frame_btm_c { - rect 117,141,406,198 + rect 116,284,408,16 + visible ( "gui::join_team_mode" == 0 ) + background "gfx/guis/mainmenu/tooltip_edge" + matcolor 1,1,1,0.95 + matscalex -1 + matscaley -1 + noevents 1 + } + windowDef qj_screen_c + { + rect 120,144,400,152 + visible ( "gui::join_team_mode" == 0 ) + background "gfx/guis/mainmenu/screen" + matcolor 0.09,0.11,0.07,0.88 + noevents 1 + } + windowDef qj_grad_c + { + rect 120,144,400,152 + visible ( "gui::join_team_mode" == 0 ) + background "gfx/guis/mainmenu/bg_darkgrad" + matcolor 0.545,0.588,0.294,0.14 + noevents 1 + } + windowDef qj_frame_top_t + { + rect 116,140,408,16 + visible ( "gui::join_team_mode" == 1 ) + background "gfx/guis/mainmenu/tooltip_edge" + matcolor 1,1,1,0.95 + noevents 1 + } + windowDef qj_frame_mid_t + { + rect 116,156,408,160 + visible ( "gui::join_team_mode" == 1 ) + background "gfx/guis/mainmenu/tooltip_mid" + matcolor 1,1,1,0.95 + noevents 1 + } + windowDef qj_frame_btm_t + { + rect 116,316,408,16 + visible ( "gui::join_team_mode" == 1 ) + background "gfx/guis/mainmenu/tooltip_edge" + matcolor 1,1,1,0.95 + matscalex -1 + matscaley -1 + noevents 1 + } + windowDef qj_screen_t + { + rect 120,144,400,184 + visible ( "gui::join_team_mode" == 1 ) + background "gfx/guis/mainmenu/screen" + matcolor 0.09,0.11,0.07,0.88 + noevents 1 + } + windowDef qj_grad_t + { + rect 120,144,400,184 + visible ( "gui::join_team_mode" == 1 ) background "gfx/guis/mainmenu/bg_darkgrad" matcolor 0.545,0.588,0.294,0.14 noevents 1 } + windowDef qj_head + { + rect 120,144,400,26 + visible 1 + background "gfx/guis/mainmenu/b3_light" + matcolor 1,1,1,0.16 + noevents 1 + } + windowDef qj_title_corner + { + rect 128,153,9,9 + visible 1 + background "gfx/guis/mainmenu/corner" + matcolor 0.564,0.603,0.286,0.7 + noevents 1 + } windowDef qj_title { - rect 132,152,376,26 - forecolor 0.96,0.97,0.9,1 + rect 142,148,300,22 + visible 1 + forecolor 1,1,1,0.85 text "gui::join_match_title" - textscale 0.3 + textscale 0.27 font "fonts/marine" + textspacing -1 noevents 1 } - windowDef qj_rule + windowDef qj_head_sep { - rect 132,180,376,1 - backcolor 0.45,0.5,0.27,0.8 + rect 446,147,4,21 + visible 1 + background "gfx/guis/mainmenu/vert_line2" + matcolor 1,1,1,0.28 noevents 1 } + windowDef qj_head_rule + { + rect 120,168,400,6 + visible 1 + background "gfx/guis/mainmenu/horiz_line2" + matcolor 1,1,1,0.3 + noevents 1 + } + + windowDef qj_b_settings + { + rect 454,146,30,23 + visible 1 + background "gfx/guis/mainmenu/b4_light" + matcolor 0.545,0.588,0.294,0.35 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + stoptransitions "qj_b_settings" ; + set "qj_b_settings::matcolor_w" "1" ; + transition "qj_i_settings::matcolor" "$desktop::white_8" "$desktop::orange" "0" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + if ( "desktop::active" == 0 ) { + transition "qj_b_settings::matcolor_w" "1" "0.35" "300" ; + transition "qj_i_settings::matcolor" "$desktop::orange" "$desktop::white_8" "300" ; + } + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection" ; + // Hand the menu back its full chrome, then walk to Settings the + // way the Settings tab does once the intro has settled. + set "desktop::quickjoin" "0" ; + set "p_quickjoin::visible" "0" ; + set "p_main::visible" "1" ; + set "p_btns::visible" "1" ; + set "desktop::curr" "2" ; + resettime "anim_in" "0" ; + resettime "anim_qj_settings" "0" ; + } + } + } + windowDef qj_i_settings + { + rect 461,149,16,16 + visible 1 + background "gfx/guis/common/gear1_still" + matcolor 1,1,1,0.8 + noevents 1 + } + windowDef qj_b_leave + { + rect 488,146,30,23 + visible 1 + background "gfx/guis/mainmenu/b4_light" + matcolor 0.545,0.588,0.294,0.35 + + onMouseEnter { + if ( "desktop::active" == 0 ) { + stoptransitions "qj_b_leave" ; + set "qj_b_leave::matcolor_w" "1" ; + transition "qj_i_leave::matcolor" "$desktop::white_8" "$desktop::orange" "0" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + if ( "desktop::active" == 0 ) { + transition "qj_b_leave::matcolor_w" "1" "0.35" "300" ; + transition "qj_i_leave::matcolor" "$desktop::orange" "$desktop::white_8" "300" ; + } + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "exec disconnect play main_menu_selection" ; + } + } + } + windowDef qj_i_leave + { + rect 495,149,16,16 + visible 1 + background "gfx/guis/mainmenu/icon_arrow" + matcolor 1,1,1,0.8 + noevents 1 + } + windowDef qj_state { - rect 132,188,190,20 - forecolor 0.95,0.72,0.25,1 + rect 132,180,190,20 + visible 1 + forecolor 0.890,0.537,0,1 text "gui::join_match_state" - textscale 0.22 + textscale 0.24 font "fonts/lowpixel" + textspacing -1 noevents 1 } windowDef qj_limit { - rect 132,212,190,16 + rect 132,206,190,16 + visible 1 forecolor 0.72,0.76,0.63,1 text "gui::join_match_limit" textscale 0.17 font "fonts/lowpixel" + textspacing -1 noevents 1 } windowDef qj_players { - rect 132,228,190,16 + rect 132,222,190,16 + visible 1 forecolor 0.72,0.76,0.63,1 text "gui::join_match_players" textscale 0.17 font "fonts/lowpixel" + textspacing -1 noevents 1 } windowDef qj_server { - rect 132,244,190,16 + rect 132,238,190,16 + visible 1 forecolor 0.55,0.58,0.48,1 text "gui::join_match_server" textscale 0.17 font "fonts/lowpixel" + textspacing -1 noevents 1 } - // -------------------------------------------------- free-for-all modes windowDef qj_b_join { - rect 336,196,172,34 + rect 336,190,172,32 visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) - backcolor 0.30,0.10,0.07,0.96 - bordercolor 0.95,0.72,0.25,1 - bordersize 1 + background "gfx/guis/mainmenu/b6_light" + matcolor 0.545,0.588,0.294,0.62 onMouseEnter { if ( "desktop::active" == 0 ) { - set "backcolor" "0.45,0.16,0.09,0.98" ; + stoptransitions "qj_b_join" ; + set "qj_b_join::matcolor_w" "1" ; + transition "qj_t_join::forecolor" "$desktop::white_8" "$desktop::orange" "0" ; + transition "qj_c_join::matcolor" "$desktop::orange_4" "$desktop::orange" "0" ; + set "qj_t_join::textscale" "0.27" ; set "cmd" "play main_menu_mouseover" ; } } onMouseExit { - set "backcolor" "0.30,0.10,0.07,0.96" ; + if ( "desktop::active" == 0 ) { + transition "qj_b_join::matcolor_w" "1" "0.62" "300" ; + transition "qj_t_join::forecolor" "$desktop::orange" "$desktop::white_8" "300" ; + transition "qj_c_join::matcolor" "$desktop::orange" "$desktop::orange_4" "300" ; + transition "qj_t_join::textscale" "0.27" "0.26" "300" ; + } } onAction { if ( "desktop::active" == 0 ) { @@ -9600,33 +9825,97 @@ windowDef Desktop } windowDef qj_t_join { - rect 336,203,172,20 + rect 352,197,140,18 visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) - forecolor 0.98,0.98,0.94,1 + forecolor 1,1,1,0.8 text "#str_200187" - textscale 0.28 - textalign 1 - font "fonts/lowpixel" + textscale 0.26 + font "fonts/marine" + textspacing -1 noevents 1 } + windowDef qj_c_join + { + rect 344,203,9,9 + visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) + background "gfx/guis/mainmenu/corner" + matcolor 0.890,0.537,0,0.4 + noevents 1 + } + windowDef qj_b_spectate + { + rect 336,230,172,28 + visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) + background "gfx/guis/mainmenu/b6_light" + matcolor 0.545,0.588,0.294,0.28 - // ---------------------------------------------------------- Tourney + onMouseEnter { + if ( "desktop::active" == 0 ) { + stoptransitions "qj_b_spectate" ; + set "qj_b_spectate::matcolor_w" "1" ; + transition "qj_t_spectate::forecolor" "$desktop::white_8" "$desktop::orange" "0" ; + transition "qj_c_spectate::matcolor" "$desktop::corner" "$desktop::orange" "0" ; + set "qj_t_spectate::textscale" "0.25" ; + set "cmd" "play main_menu_mouseover" ; + } + } + onMouseExit { + if ( "desktop::active" == 0 ) { + transition "qj_b_spectate::matcolor_w" "1" "0.28" "300" ; + transition "qj_t_spectate::forecolor" "$desktop::orange" "$desktop::white_8" "300" ; + transition "qj_c_spectate::matcolor" "$desktop::orange" "$desktop::corner" "300" ; + transition "qj_t_spectate::textscale" "0.25" "0.24" "300" ; + } + } + onAction { + if ( "desktop::active" == 0 ) { + set "cmd" "play main_menu_selection join spectator close" ; + } + } + } + windowDef qj_t_spectate + { + rect 352,236,140,17 + visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) + forecolor 1,1,1,0.8 + text "#str_200195" + textscale 0.24 + font "fonts/marine" + textspacing -1 + noevents 1 + } + windowDef qj_c_spectate + { + rect 344,241,9,9 + visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) + background "gfx/guis/mainmenu/corner" + matcolor 0.564,0.603,0.286,0.4 + noevents 1 + } windowDef qj_b_tourney { - rect 336,196,172,34 + rect 336,190,172,32 visible ( "gui::join_tourney_mode" == 1 ) - backcolor 0.30,0.10,0.07,0.96 - bordercolor 0.95,0.72,0.25,1 - bordersize 1 + background "gfx/guis/mainmenu/b6_light" + matcolor 0.545,0.588,0.294,0.62 onMouseEnter { if ( "desktop::active" == 0 ) { - set "backcolor" "0.45,0.16,0.09,0.98" ; + stoptransitions "qj_b_tourney" ; + set "qj_b_tourney::matcolor_w" "1" ; + transition "qj_t_tourney::forecolor" "$desktop::white_8" "$desktop::orange" "0" ; + transition "qj_c_tourney::matcolor" "$desktop::orange_4" "$desktop::orange" "0" ; + set "qj_t_tourney::textscale" "0.23" ; set "cmd" "play main_menu_mouseover" ; } } onMouseExit { - set "backcolor" "0.30,0.10,0.07,0.96" ; + if ( "desktop::active" == 0 ) { + transition "qj_b_tourney::matcolor_w" "1" "0.62" "300" ; + transition "qj_t_tourney::forecolor" "$desktop::orange" "$desktop::white_8" "300" ; + transition "qj_c_tourney::matcolor" "$desktop::orange" "$desktop::orange_4" "300" ; + transition "qj_t_tourney::textscale" "0.23" "0.22" "300" ; + } } onAction { if ( "desktop::active" == 0 ) { @@ -9636,33 +9925,47 @@ windowDef Desktop } windowDef qj_t_tourney { - rect 336,203,172,20 + rect 352,198,140,18 visible ( "gui::join_tourney_mode" == 1 ) - forecolor 0.98,0.98,0.94,1 + forecolor 1,1,1,0.8 text "gui::toggleTourneyButton" - textscale 0.28 - textalign 1 - font "fonts/lowpixel" + textscale 0.22 + font "fonts/marine" + textspacing -1 + noevents 1 + } + windowDef qj_c_tourney + { + rect 344,203,9,9 + visible ( "gui::join_tourney_mode" == 1 ) + background "gfx/guis/mainmenu/corner" + matcolor 0.890,0.537,0,0.4 noevents 1 } - - // ------------------------------------------------------- team modes windowDef qj_b_auto { - rect 336,188,172,32 + rect 336,182,172,32 visible ( "gui::join_team_mode" == 1 ) - backcolor 0.30,0.10,0.07,0.96 - bordercolor 0.95,0.72,0.25,1 - bordersize 1 + background "gfx/guis/mainmenu/b6_light" + matcolor 0.545,0.588,0.294,0.62 onMouseEnter { if ( "desktop::active" == 0 ) { - set "backcolor" "0.45,0.16,0.09,0.98" ; + stoptransitions "qj_b_auto" ; + set "qj_b_auto::matcolor_w" "1" ; + transition "qj_t_auto::forecolor" "$desktop::white_8" "$desktop::orange" "0" ; + transition "qj_c_auto::matcolor" "$desktop::orange_4" "$desktop::orange" "0" ; + set "qj_t_auto::textscale" "0.27" ; set "cmd" "play main_menu_mouseover" ; } } onMouseExit { - set "backcolor" "0.30,0.10,0.07,0.96" ; + if ( "desktop::active" == 0 ) { + transition "qj_b_auto::matcolor_w" "1" "0.62" "300" ; + transition "qj_t_auto::forecolor" "$desktop::orange" "$desktop::white_8" "300" ; + transition "qj_c_auto::matcolor" "$desktop::orange" "$desktop::orange_4" "300" ; + transition "qj_t_auto::textscale" "0.27" "0.26" "300" ; + } } onAction { if ( "desktop::active" == 0 ) { @@ -9672,33 +9975,47 @@ windowDef Desktop } windowDef qj_t_auto { - rect 336,195,172,20 + rect 352,189,140,18 visible ( "gui::join_team_mode" == 1 ) - forecolor 0.98,0.98,0.94,1 + forecolor 1,1,1,0.8 text "#str_200192" - textscale 0.28 - textalign 1 - font "fonts/lowpixel" + textscale 0.26 + font "fonts/marine" + textspacing -1 + noevents 1 + } + windowDef qj_c_auto + { + rect 344,195,9,9 + visible ( "gui::join_team_mode" == 1 ) + background "gfx/guis/mainmenu/corner" + matcolor 0.890,0.537,0,0.4 noevents 1 } windowDef qj_b_marine { - rect 336,224,172,28 + rect 336,218,172,28 visible ( "gui::join_team_mode" == 1 ) - backcolor 0.07,0.075,0.055,0.98 - bordercolor 0.3,0.34,0.2,1 - bordersize 1 + background "gfx/guis/mainmenu/b6_light" + matcolor 0.545,0.588,0.294,0.28 onMouseEnter { if ( "desktop::active" == 0 ) { - set "backcolor" "0.27,0.15,0.04,0.98" ; - set "bordercolor" "0.95,0.49,0.06,1" ; + stoptransitions "qj_b_marine" ; + set "qj_b_marine::matcolor_w" "1" ; + transition "qj_t_marine::forecolor" "$desktop::white_8" "$desktop::orange" "0" ; + transition "qj_c_marine::matcolor" "$desktop::corner" "$desktop::orange" "0" ; + set "qj_t_marine::textscale" "0.25" ; set "cmd" "play main_menu_mouseover" ; } } onMouseExit { - set "backcolor" "0.07,0.075,0.055,0.98" ; - set "bordercolor" "0.3,0.34,0.2,1" ; + if ( "desktop::active" == 0 ) { + transition "qj_b_marine::matcolor_w" "1" "0.28" "300" ; + transition "qj_t_marine::forecolor" "$desktop::orange" "$desktop::white_8" "300" ; + transition "qj_c_marine::matcolor" "$desktop::orange" "$desktop::corner" "300" ; + transition "qj_t_marine::textscale" "0.25" "0.24" "300" ; + } } onAction { if ( "desktop::active" == 0 ) { @@ -9709,33 +10026,47 @@ windowDef Desktop } windowDef qj_t_marine { - rect 336,230,172,18 + rect 352,224,140,17 visible ( "gui::join_team_mode" == 1 ) - forecolor 0.86,0.88,0.8,1 + forecolor 1,1,1,0.8 text "#str_200193" textscale 0.24 - textalign 1 - font "fonts/lowpixel" + font "fonts/marine" + textspacing -1 + noevents 1 + } + windowDef qj_c_marine + { + rect 344,229,9,9 + visible ( "gui::join_team_mode" == 1 ) + background "gfx/guis/mainmenu/corner" + matcolor 0.564,0.603,0.286,0.4 noevents 1 } windowDef qj_b_strogg { - rect 336,256,172,28 + rect 336,250,172,28 visible ( "gui::join_team_mode" == 1 ) - backcolor 0.07,0.075,0.055,0.98 - bordercolor 0.3,0.34,0.2,1 - bordersize 1 + background "gfx/guis/mainmenu/b6_light" + matcolor 0.545,0.588,0.294,0.28 onMouseEnter { if ( "desktop::active" == 0 ) { - set "backcolor" "0.27,0.15,0.04,0.98" ; - set "bordercolor" "0.95,0.49,0.06,1" ; + stoptransitions "qj_b_strogg" ; + set "qj_b_strogg::matcolor_w" "1" ; + transition "qj_t_strogg::forecolor" "$desktop::white_8" "$desktop::orange" "0" ; + transition "qj_c_strogg::matcolor" "$desktop::corner" "$desktop::orange" "0" ; + set "qj_t_strogg::textscale" "0.25" ; set "cmd" "play main_menu_mouseover" ; } } onMouseExit { - set "backcolor" "0.07,0.075,0.055,0.98" ; - set "bordercolor" "0.3,0.34,0.2,1" ; + if ( "desktop::active" == 0 ) { + transition "qj_b_strogg::matcolor_w" "1" "0.28" "300" ; + transition "qj_t_strogg::forecolor" "$desktop::orange" "$desktop::white_8" "300" ; + transition "qj_c_strogg::matcolor" "$desktop::orange" "$desktop::corner" "300" ; + transition "qj_t_strogg::textscale" "0.25" "0.24" "300" ; + } } onAction { if ( "desktop::active" == 0 ) { @@ -9746,71 +10077,47 @@ windowDef Desktop } windowDef qj_t_strogg { - rect 336,262,172,18 + rect 352,256,140,17 visible ( "gui::join_team_mode" == 1 ) - forecolor 0.86,0.88,0.8,1 + forecolor 1,1,1,0.8 text "#str_200194" textscale 0.24 - textalign 1 - font "fonts/lowpixel" + font "fonts/marine" + textspacing -1 noevents 1 } - - // ------------------------------------------------------- spectating - windowDef qj_b_spectate - { - rect 336,238,172,30 - visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) - backcolor 0.07,0.075,0.055,0.98 - bordercolor 0.3,0.34,0.2,1 - bordersize 1 - - onMouseEnter { - if ( "desktop::active" == 0 ) { - set "backcolor" "0.27,0.15,0.04,0.98" ; - set "bordercolor" "0.95,0.49,0.06,1" ; - set "cmd" "play main_menu_mouseover" ; - } - } - onMouseExit { - set "backcolor" "0.07,0.075,0.055,0.98" ; - set "bordercolor" "0.3,0.34,0.2,1" ; - } - onAction { - if ( "desktop::active" == 0 ) { - set "cmd" "play main_menu_selection join spectator close" ; - } - } - } - windowDef qj_t_spectate + windowDef qj_c_strogg { - rect 336,244,172,18 - visible ( "gui::join_team_mode" == 0 && "gui::join_tourney_mode" == 0 ) - forecolor 0.86,0.88,0.8,1 - text "#str_200195" - textscale 0.24 - textalign 1 - font "fonts/lowpixel" + rect 344,261,9,9 + visible ( "gui::join_team_mode" == 1 ) + background "gfx/guis/mainmenu/corner" + matcolor 0.564,0.603,0.286,0.4 noevents 1 } windowDef qj_b_tspectate { - rect 336,288,172,28 + rect 336,282,172,28 visible ( "gui::join_team_mode" == 1 ) - backcolor 0.07,0.075,0.055,0.98 - bordercolor 0.3,0.34,0.2,1 - bordersize 1 + background "gfx/guis/mainmenu/b6_light" + matcolor 0.545,0.588,0.294,0.28 onMouseEnter { if ( "desktop::active" == 0 ) { - set "backcolor" "0.27,0.15,0.04,0.98" ; - set "bordercolor" "0.95,0.49,0.06,1" ; + stoptransitions "qj_b_tspectate" ; + set "qj_b_tspectate::matcolor_w" "1" ; + transition "qj_t_tspectate::forecolor" "$desktop::white_8" "$desktop::orange" "0" ; + transition "qj_c_tspectate::matcolor" "$desktop::corner" "$desktop::orange" "0" ; + set "qj_t_tspectate::textscale" "0.25" ; set "cmd" "play main_menu_mouseover" ; } } onMouseExit { - set "backcolor" "0.07,0.075,0.055,0.98" ; - set "bordercolor" "0.3,0.34,0.2,1" ; + if ( "desktop::active" == 0 ) { + transition "qj_b_tspectate::matcolor_w" "1" "0.28" "300" ; + transition "qj_t_tspectate::forecolor" "$desktop::orange" "$desktop::white_8" "300" ; + transition "qj_c_tspectate::matcolor" "$desktop::orange" "$desktop::corner" "300" ; + transition "qj_t_tspectate::textscale" "0.25" "0.24" "300" ; + } } onAction { if ( "desktop::active" == 0 ) { @@ -9820,25 +10127,34 @@ windowDef Desktop } windowDef qj_t_tspectate { - rect 336,294,172,18 + rect 352,288,140,17 visible ( "gui::join_team_mode" == 1 ) - forecolor 0.86,0.88,0.8,1 + forecolor 1,1,1,0.8 text "#str_200195" textscale 0.24 - textalign 1 - font "fonts/lowpixel" + font "fonts/marine" + textspacing -1 + noevents 1 + } + windowDef qj_c_tspectate + { + rect 344,293,9,9 + visible ( "gui::join_team_mode" == 1 ) + background "gfx/guis/mainmenu/corner" + matcolor 0.564,0.603,0.286,0.4 noevents 1 } // Team-balance refusals surface here instead of the Join tab's notice. windowDef qj_msg { - rect 132,316,376,18 + rect 132,260,196,36 visible 0 forecolor 0.95,0.6,0.1,1 text "" - textscale 0.19 + textscale 0.17 font "fonts/lowpixel" + textspacing -1 noevents 1 } } diff --git a/docs/dev/release-completion.md b/docs/dev/release-completion.md index 8d5e778f5..0187c1d40 100644 --- a/docs/dev/release-completion.md +++ b/docs/dev/release-completion.md @@ -223,7 +223,7 @@ is `docs/dev/macos-moltenvk-decision.md`. - [x] Experimental macOS support now has an explicit matrix policy: `docs/dev/macos-support-matrix-policy.md` records the current Apple Silicon/arm64-only release scope, keeps Intel Mac, universal2, and Rosetta outside published support, and requires signoff evidence for both the macOS 11 floor and the latest public macOS before any first-class macOS claim can ship. macOS signoff reports and evidence records now include architecture policy, CPU architecture, OS matrix role, Xcode version, and macOS SDK version so release notes can match the actual Apple toolchain and OS coverage. - [x] Experimental macOS package UX is release-gated: the macOS Package Layout And Release Policy records the self-contained app contract and requires mounted-DMG, independently dragged-app, whole-package loose-tool, terminal, embedded-path, path-resolution, and Gatekeeper signoff evidence before macOS support claims can grow beyond the current experimental tier. The manual release workflow has an explicit first-class macOS support tier that fails instead of publishing unsigned fallback packages when signing/notarization credentials are missing. - [x] Experimental macOS runtime signoff is now evidence-gated: `docs/dev/macos-signoff-evidence.md` records the active run ID, archive checksum, hardware/OS details, OpenGL/Metal bridge coverage, OpenAL provider, package artifacts, openQ4 and `openQ4-game` commits, SP/MP evidence, and release-note limitations; `tools/macos/record_signoff_evidence.py` turns a completed-checklist archive into that record, while collected signoff archives now require a dedicated `renderer-mp-smoke` evidence folder before validation accepts the hardware signoff bundle. -- [x] Multiplayer server entry is now more deliberate and Quake Live-like: new clients start as spectators at the map intermission camera when available, see a Q4-styled join screen with server details and gametype-aware join options, and can set `ui_autoJoin 1` to keep the old immediate-join behavior. That screen is now a compact centred panel rather than the full in-game menu: it is authored inside the plain 640x480 GUI canvas so it keeps a 4:3 shape on a widescreen display, it carries only the match summary and the join choices the gametype allows, and the live view behind it is softened through the shared special-effect blur controller. The interactive VS Code multiplayer launch configurations pass `ui_autoJoin 0` so manual runs exercise it; automated multiplayer profiles still pass `ui_autoJoin 1`. +- [x] Multiplayer server entry is now more deliberate and Quake Live-like: new clients start as spectators at the map intermission camera when available, see a Q4-styled join screen with server details and gametype-aware join options, and can set `ui_autoJoin 1` to keep the old immediate-join behavior. That screen is now a compact centred panel rather than the full in-game menu: it is authored inside the plain 640x480 GUI canvas so it keeps a 4:3 shape on a widescreen display, it carries only the match summary and the join choices the gametype allows, and the live view behind it is softened through the shared special-effect blur controller. The interactive VS Code multiplayer launch configurations pass `ui_autoJoin 0` so manual runs exercise it; automated multiplayer profiles still pass `ui_autoJoin 1`. The panel is built from shipped Quake 4 interface art and carries settings and leave-match symbols, and `docs/dev/ui-visual-design.md` records the visual rules it follows. The OpenGL half of the special-effect blur was aligned with the native Vulkan pass at the same time, so the softened backdrop is identical on the classic OpenGL path, the OpenGL renderer module and Vulkan. - [x] Pull-down console mouse selection and clicks now line up with the visible cursor hotspot, so the cursor no longer appears a row below the point that receives input. - [x] Documentation now labels macOS support as experimental wherever current support or release packages are described, including the README, getting-started guide, build/platform docs, input guidance, packaged HTML docs, VM workflow, and curated release notes. - [x] Experimental macOS packaging and signoff checks now reject case-insensitive path collisions, Finder/AppleDouble metadata inside result archives, and renderer evidence that contains only placeholder directories before artifacts are accepted. diff --git a/docs/dev/releases/v0.12.0.md b/docs/dev/releases/v0.12.0.md index b6b849eb0..4c2f5860c 100644 --- a/docs/dev/releases/v0.12.0.md +++ b/docs/dev/releases/v0.12.0.md @@ -19,7 +19,8 @@ - **Long macOS sessions keep their sound instead of dropping back to the menu.** Apple Silicon packages now include OpenAL Soft, avoiding the fixed source and buffer limits in Apple's legacy OpenAL framework that could stop stock levels after sustained audio use. - **Arena Campaign scoring starts reliably.** Disabling ready-up now also clears the ready threshold, preventing deathmatch-backed Arena cards from remaining forever in a non-scoring warmup. - **Multiplayer setup is easier to control.** The server menu exposes guarded bot controls, and competitive free-for-all disclosure now accepts the active recipients intended by the match policy. -- **Joining a multiplayer match takes one click.** Connecting now opens a compact join card over the map instead of the full in-game menu. It names the gametype and map, the scoring limit, how full the server is and whether the match is in warmup, and offers just the choices that mode allows - join, pick a side, or spectate. The card keeps its shape and stays centred on a widescreen display, and the live view behind it is softened so the text stays readable. The join screen is the default again: set `ui_autoJoin 1` if you would rather drop straight into play. +- **Joining a multiplayer match takes one click.** Connecting now opens a compact join card over the map instead of the full in-game menu. It names the gametype and map, the scoring limit, how full the server is and whether the match is in warmup, and offers just the choices that mode allows - join, pick a side, or spectate - with settings and leave-match symbols in its header. The card is drawn in Quake 4's own menu style, keeps its shape and stays centred on a widescreen display, and the live view behind it is pushed out of focus so the text stays readable. The join screen is the default again: set `ui_autoJoin 1` if you would rather drop straight into play. +- **The in-game depth-of-field blur now looks the same on both renderers.** OpenGL ignored the effect strength the game asked for and always blurred by a fixed amount, while Vulkan honoured it; a request for a gentle blur came out heavy on one renderer and a request for none at all came out blurred. Both now read the same controller the same way, so the join screen and the Arena Campaign presentation match whichever renderer you run. - **Multiplayer function keys work as shown.** F1/F2 voting, F3 ready-up, F6 team switching, and F7 spectating once again use Quake 4's real impulse actions. Ready presses travel over the reliable path, the two-line warmup instruction remains fully visible, and only the exact older openQ4 defaults are upgraded—custom bindings stay untouched. No-time-limit deathmatch also starts without a spurious competitive-rules rejection. - **Pausing single-player is immediate.** Opening the in-game menu no longer blocks on save, mod, device, display, key-binding, or multiplayer-model discovery. Those lists refresh only when their own page is opened, and the normal level-loading phase prepares the menu art, fallback image, and music before the first Escape press. - **Single-player console tuning remains under the player's control.** The weapon wheel now uses a dedicated transient slow-motion channel instead of rewriting `timescale`, so values such as `timescale 0.5` remain set after the console closes. The old forced-run shortcut is also gone: `pm_walkspeed` once again controls actual walking when Always Run is off (or the run key temporarily inverts it). The wheel still slows simulation and audio while held, then restores only its own temporary effect. @@ -77,7 +78,9 @@ - Kept view-model client entities, attached animated models, and the lightning gun beam on the exact rendered skeletal pose. - Prevented remote-client prediction replays from advancing projectile visuals twice or replacing the viewweapon's authoritative interpolation endpoint, and restored impact/fizzle visuals when a short-lived projectile is first received in a terminal state. - Fixed weapon swaps that could inherit firing state or leave stale effects behind. -- Replaced the connect-time multiplayer join screen with a compact 4:3 panel authored inside the plain GUI canvas, so it never stretches on a widescreen display. It shows gametype and map, match state, scoring limit, player count and server name, chooses its button set from the gametype descriptor instead of a copied gametype list, and reports a refused team switch in place. The live scene behind it is pushed out of focus through the existing Raven special-effect blur controller, which both the OpenGL and Vulkan backends already implement; renderers that cannot provide the pass simply draw a sharp view. The interactive VS Code multiplayer launch configurations now start at that screen rather than auto-joining. +- Replaced the connect-time multiplayer join screen with a compact 4:3 panel authored inside the plain GUI canvas, so it never stretches on a widescreen display. It shows gametype and map, match state, scoring limit, player count and server name, carries settings and leave-match symbols, chooses its button set from the gametype descriptor instead of a copied gametype list, sizes its frame to the choices that gametype offers, and reports a refused team switch in place. It is built entirely from shipped Quake 4 interface art - the tooltip frame for the 45-degree corner chamfers and olive rail, the standard button plates and corner markers, and the stock gear and arrow symbols - with the menu's own instant-in, eased-out hover. The interactive VS Code multiplayer launch configurations now start at that screen rather than auto-joining. +- Aligned the OpenGL half of the Raven special-effect blur with the native Vulkan pass. Parm 6 is the effect strength on both ends rather than a tint amount on one of them, parm 4 is converted to the same normalized falloff, and the colour tint is driven by the approach colour's own alpha. A zero-strength request now draws nothing instead of a fixed blur, and the classic OpenGL path, the OpenGL renderer module and Vulkan all produce the same image. +- Added `docs/dev/ui-visual-design.md`, the visual rules new menus and in-game panels follow: the Quake 4 art vocabulary, the palette and type scale, button and frame anatomy, the hover contract, the 4:3 authoring rule, and how to soften a scene behind a partial-screen panel. - Added complete Polish and Russian UI tables, Unicode code-point drawing, CP1250 conversion support, and stricter generated-font checksum validation. - Restored the marine hovertank engine and hover-pad loops with null-safe sound-emitter teardown. - Bundled checksum-pinned OpenAL Soft 1.25.1 in macOS release and validation packages, retained Apple's OpenAL framework only as an explicit compatibility-build option, and made backend buffer allocation or upload failures fail gracefully rather than terminating the session. diff --git a/docs/dev/ui-visual-design.md b/docs/dev/ui-visual-design.md new file mode 100644 index 000000000..0a8d0671f --- /dev/null +++ b/docs/dev/ui-visual-design.md @@ -0,0 +1,244 @@ +# openQ4 UI Visual Design + +The rules every openQ4 menu, panel and in-game GUI follows, so that anything we +add looks like it shipped with Quake 4 rather than like a mod bolted on top. + +The reference is Quake 4's own main menu and settings screens. Read this before +authoring a new `.gui`, and check a change against `guis/mainmenu.gui` and the +`p_quickjoin` panel in `guis/mpmain.gui`, which are the worked examples. + +--- + +## 1. The look, in one paragraph + +Quake 4's interface is a dark olive-green military display. Panels are black +plates with a thin olive rail drawn along their inside edge and **45-degree +chamfers cut off their corners**. Buttons are the same idea in miniature: a +plate with a 45-degree cut at the bottom left, a bright edge running down the +left and along the bottom, fading away to nothing at the right. Nothing is a +plain rectangle, nothing has a rounded corner, and nothing is fully opaque. The +accent colour is a warm orange, used only for the thing the player is pointing +at or the one line of text that matters right now. + +## 2. Use the shipped art + +Do not author new interface images. Everything below already ships in the +retail PK4s and is what the stock menus are built from. Adding art also breaks +the project rule about running on stock assets, so treat "there is no image for +this" as a design constraint, not a reason to make one. + +### Frames + +| Material | Size | Use | +| --- | --- | --- | +| `gfx/guis/mainmenu/tooltip_edge` | 256x16 | Top cap of a panel frame: olive rail along the top and both sides, 45-degree chamfers at both corners. Mirror it with `matscalex -1` + `matscaley -1` for the bottom cap. | +| `gfx/guis/mainmenu/tooltip_mid` | 256x2 | The side rails, stretched to the height between the two caps. | +| `gfx/guis/mainmenu/popup_top` / `popup_mid` / `popup_btm` / `popup_bg` | — | The larger modal frame the ban-list popup uses. Same top/mid/bottom stacking. | +| `gfx/guis/mainmenu/topbar` / `btmbar` | 1024x128 | Full-width screen bordering with the 45-degree step indentations and notch detailing. Screen furniture, not panel furniture. | +| `gfx/guis/mainmenu/screen` | 512x512 | The dark screen texture that fills a panel body. | +| `gfx/guis/mainmenu/bg_darkgrad` | — | A soft vertical gradient laid over the body to lift its top edge. | +| `gfx/guis/mainmenu/horiz_line2`, `vert_line`, `vert_line2` | — | Rules and separators. Always low alpha. | +| `gfx/guis/mainmenu/corner` | 32x32 | The small 45-degree triangle marker. It is the single most recognisable piece of Quake 4 UI vocabulary - put one to the left of a button label and one before a panel title. | + +### Button plates + +All of them carry the 45-degree cut at the bottom left, a highlight down the +left and along the bottom, and a fade to nothing at the right. + +| Material | Size | Use | +| --- | --- | --- | +| `b6_light` | 256x32 | Standard button. The panel workhorse. | +| `b4_light` | 128x32 | Short button - icon buttons, small controls. | +| `b3_light` | 512x32 | Wide light plate. Good as a header wash behind a title. | +| `b5_dark` / `b1_dark` / `b2_dark` | — | Dark plates. Nearly invisible on a dark body except for their bottom rail, which is exactly the main menu's look; do not mix them with light plates in the same column. | + +### Symbols + +Use a real Quake 4 image before reaching for a word. + +| Material | Meaning | +| --- | --- | +| `gfx/guis/common/gear1_still` | Settings. (`gear1` is the same art with a slow rotation - use the still one unless the motion earns its place.) | +| `gfx/guis/mainmenu/icon_arrow` | Leave / exit / go. | +| `gfx/guis/mainmenu/icon_locked`, `icon_favorite`, `icon_dedserver`, `icon_pb`, `icon_repeater` | Server-list state. | +| `gfx/guis/mainmenu/box`, `box_check` | Checkboxes. | + +## 3. Colour + +Declared once per GUI as `definevec4` on the desktop and referenced as +`$desktop::name` **inside scripts**. Window properties do not resolve those +names, so a `matcolor` or `forecolor` written as a property must be literal +numbers. This is a silent failure: the parser reports +`expected ',' but found 'desktop'` and drops the rest of that windowDef. + +| Name | Value | Use | +| --- | --- | --- | +| olive | `0.545,0.588,0.294` | Panel and button plate tint. Plates are tinted olive, never white. | +| `corner` | `0.564,0.603,0.286,0.4` | Corner markers and rails at rest. | +| `orange` | `0.890,0.537,0,1` | Hover, focus, and the one line of status text that matters. | +| `orange_4` | `0.890,0.537,0,0.4` | The corner marker of a primary button at rest. | +| `white_8` | `1,1,1,0.8` | Button and body text. | +| `white_4` | `1,1,1,0.4` | Disabled text. | +| body fill | `0.09,0.11,0.07,0.88` | `screen` under a panel. | +| secondary text | `0.72,0.76,0.63,1` | Supporting lines. | +| tertiary text | `0.55,0.58,0.48,1` | Least important line, e.g. a server name. | + +Rest alpha for a plate is `0.28`-`0.62` depending on whether it is a secondary +or primary action; hover takes it to `1`. Frame caps and rails sit at `0.95`. + +## 4. Typography + +| Font | Use | +| --- | --- | +| `fonts/marine` | Titles and button labels. Renders lower case as small caps, which is why mixed-case source text still reads as a heading. | +| `fonts/lowpixel` | Body text, status lines, lists. | + +- Button labels `0.24`-`0.26`; panel titles `0.27`-`0.30`; body `0.17`; status + `0.24`. +- Always set `textspacing -1`. The stock menus do, and text set without it sits + noticeably looser than everything around it. +- Text is drawn with its first baseline at `rect.y + lineHeight`, **not** + centred in the rect. A label inside a 32-high plate therefore needs its own + windowDef at about `plate.y + 7`, which is why the stock menus keep the plate, + the label and the corner marker as three separate windows. +- Localise everything. A visible string is a `#str_` lookup, never a literal. + +## 5. Anatomy + +### A button + +Three windows. The plate is the interactive one - it owns the rect the player +clicks, and it drives the other two. + +``` +windowDef qj_b_join rect 336,190,172,32 background b6_light matcolor ,0.62 +windowDef qj_t_join rect 352,197,140,18 text, fonts/marine, 0.26, forecolor 1,1,1,0.8 +windowDef qj_c_join rect 344,203,9,9 background corner matcolor +``` + +Label inset is 16 from the plate's left edge; the corner marker sits at plate +`+8` horizontally and roughly centres on the label. + +### Hover + +Instant in, eased out. That asymmetry is the Quake 4 feel and is worth copying +exactly: + +``` +onMouseEnter { + if ( "desktop::active" == 0 ) { + stoptransitions "qj_b_join" ; + set "qj_b_join::matcolor_w" "1" ; + transition "qj_t_join::forecolor" "$desktop::white_8" "$desktop::orange" "0" ; + transition "qj_c_join::matcolor" "$desktop::orange_4" "$desktop::orange" "0" ; + set "qj_t_join::textscale" "0.27" ; + set "cmd" "play main_menu_mouseover" ; + } +} +onMouseExit { + if ( "desktop::active" == 0 ) { + transition "qj_b_join::matcolor_w" "1" "0.62" "300" ; + transition "qj_t_join::forecolor" "$desktop::orange" "$desktop::white_8" "300" ; + transition "qj_c_join::matcolor" "$desktop::orange" "$desktop::orange_4" "300" ; + transition "qj_t_join::textscale" "0.27" "0.26" "300" ; + } +} +``` + +- The label grows by exactly `0.01` on hover. It is a very small move and it is + what makes the buttons feel physical. +- `play main_menu_mouseover` on enter, `play main_menu_selection` on action. + A button that does not make a sound reads as broken. +- Guard both handlers with `"desktop::active" == 0` so a running animation or a + modal cannot be clicked through. +- `stoptransitions` names the window that *started* the transitions, which is + the interactive one, not the window being animated. + +### A panel + +``` +qj_frame_top tooltip_edge x,y,w,16 matcolor 1,1,1,0.95 +qj_frame_mid tooltip_mid x,y+16,w,h-32 matcolor 1,1,1,0.95 +qj_frame_btm tooltip_edge x,y+h-16,w,16 matscale -1,-1 matcolor 1,1,1,0.95 +qj_screen screen x+4,y+4,w-8,h-8 matcolor 0.09,0.11,0.07,0.88 +qj_grad bg_darkgrad x+4,y+4,w-8,h-8 matcolor ,0.14 +qj_head b3_light x+4,y+4,w-8,26 matcolor 1,1,1,0.16 +qj_head_rule horiz_line2 x+4,y+28,w-8,6 matcolor 1,1,1,0.3 +``` + +Body inset from the frame is 4. Content inset from the frame is 16. A header +band is 26 high with the title baseline inside it and a rule under it. + +## 6. Layout + +- **Author inside the plain 640x480 canvas.** Aspect correction is on by + default (`ui_aspectCorrection`), so a panel authored there keeps its 4:3 shape + and stays centred on a wide display. Only full-bleed backgrounds should read + `gui::virtual_screen_x_expand` / `_y_expand` to cover the extra width. +- **Size the panel to its content.** If a panel's content varies - a gametype + with four join options versus one - give the frame two heights and switch + them with `visible ( expr )` rather than leaving a third of the panel empty. +- **A partial-screen panel does not dim the screen.** Quake 4 does not, and + neither should we; if the panel needs the background to recede, soften it + (see below) rather than laying a black sheet over the whole display. +- Two columns work well: information on the left, actions on the right. +- Never let a control cross the frame rail. + +## 7. Softening the scene behind an in-game panel + +A panel that covers part of the screen over live gameplay uses the Raven +special-effect blur, not a dimming quad: + +```cpp +renderSystem->SetSpecialEffectParm( SPECIAL_EFFECT_BLUR, 0..3, 0.0f ); // no tint +renderSystem->SetSpecialEffectParm( SPECIAL_EFFECT_BLUR, 4, 0.2f ); // range +renderSystem->SetSpecialEffectParm( SPECIAL_EFFECT_BLUR, 5, 0.004f ); // focus +renderSystem->SetSpecialEffectParm( SPECIAL_EFFECT_BLUR, 6, 0.85f ); // strength +renderSystem->SetSpecialEffectParm( SPECIAL_EFFECT_BLUR, 7, 512.0f ); // distance scale +renderSystem->SetSpecialEffect( SPECIAL_EFFECT_BLUR, true ); +``` + +**Parm 6 is the effect strength and must not be zero.** Leaving it at zero asks +for no effect and Vulkan correctly draws nothing. Parm 4 is the distance over +which the image reaches full blur and parm 5 the focus point, both measured +against parm 7; focusing just past the near plane softens the whole scene rather +than picking out a subject. `r_forceSpecialEffects 1` turns the pass on without +any game state, which is the way to tell "the controller is off" apart from "the +pass is broken". + +Turn the effect off on every path that can leave the panel, including map +shutdown, and never cancel it while another owner (the arena presentation) has +it enabled. + +## 8. Checklist + +- [ ] Every visible string is a `#str_` lookup, present in all six `.lang` files. +- [ ] Every `matcolor` / `forecolor` written as a property is literal numbers; + `$desktop::` names appear only inside scripts. +- [ ] Buttons are plate + label + corner marker, with the instant-in / + 300ms-out hover and both menu sounds. +- [ ] The panel is framed with real Quake 4 art and has 45-degree corners. +- [ ] Nothing reads `virtual_screen_x_expand` unless it is a full-bleed + background. +- [ ] Launched and looked at, on both `r_renderApi gl` and `vulkan`, at a + widescreen resolution. +- [ ] `python tools/tests/match_control_ui_contract.py` and + `python tools/tests/lang_table_encoding.py` pass. + +## 9. Traps + +- A GUI parse error is a **warning**, not a failure. The engine logs + `mpmain.gui, line N: expected ...` and silently drops the rest of that + windowDef, so the menu comes up looking almost right. Grep the log for + `.gui, line` after any GUI edit. +- `testGUI guis/.gui` renders a GUI standalone with no game state. It is + the fastest way to separate "this does not draw" from "this is never made + visible". +- A loose `guis/*.gui` under `.install/baseoq4/` overrides the packed copy - + that directory is first in the search path. It is the fast iteration loop; + delete the file when you are done or it will shadow your next build. +- Window names are resolved after the whole GUI is parsed, so a script may + reference a window declared later in the file. + +Related: [`stock-asset-baseline.md`](stock-asset-baseline.md), +[`renderer-validation-matrix.md`](renderer-validation-matrix.md). From d5c7ffb6198032f69098e1be0456cb4feaf7aece Mon Sep 17 00:00:00 2001 From: themuffinator Date: Thu, 3 Sep 2026 10:35:34 +0100 Subject: [PATCH 3/3] Point CI at the game module this branch needs, and at the join screen default Validation Script Smoke failed on weapon_zoom_view_alignment. CI pins openQ4-game by SHA, and that pin was fourteen commits stale: it still carried the UpdateZoomGuiViewState call from the 2D overlay pass that the test forbids, so the failure was already there on main. It also predates the join card's own model, so this branch could not have gone green against it either way. Both workflow pins move to bc60187, the current openQ4-game main, which carries the zoom fix and the join screen's UpdateJoinScreenGui / SetJoinScreenSoftFocus. vscode_fast_build then failed on the second thing this branch changed: the contract still required every MP launch configuration to set ui_autoJoin to 1. The interactive configurations now pass 0, which is the shipped default and starts at the join screen; the automated MP profiles it checks next - the listen-server helper, the RenderDoc helper, the renderer benchmark and the stock-asset baseline - still pin 1, and the contract still insists the value is set explicitly exactly once. Its two AGENTS.md assertions follow the wording that rule now uses. All 156 scripts in the smoke list pass locally against the new pin, the full build links against it, and a Team Deathmatch join on Vulkan comes up clean with no GUI parse warnings. Co-Authored-By: Claude Opus 5 --- .github/workflows/commit-validation.yml | 2 +- .github/workflows/push-verification.yml | 2 +- tools/tests/vscode_fast_build.py | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/commit-validation.yml b/.github/workflows/commit-validation.yml index 782f9604a..ab45b4ef0 100644 --- a/.github/workflows/commit-validation.yml +++ b/.github/workflows/commit-validation.yml @@ -15,7 +15,7 @@ env: # Keep the engine's shared File/FileSystem ABI locked to the companion # revision that implements GAME_API 46, generated-animation cache v3, and # the game-owned transient time-scale interface. - OPENQ4_GAMELIBS_SHA: 67fcd3e2142bc07c4871873ea1ec6fe592ac8a5a + OPENQ4_GAMELIBS_SHA: bc60187fb3e8552d33cb902360fa66bad1bfa101 jobs: script-smoke: diff --git a/.github/workflows/push-verification.yml b/.github/workflows/push-verification.yml index bc40344bc..0091d09b7 100644 --- a/.github/workflows/push-verification.yml +++ b/.github/workflows/push-verification.yml @@ -11,7 +11,7 @@ env: # Keep the engine's shared File/FileSystem ABI locked to the companion # revision that implements GAME_API 46, generated-animation cache v3, and # the game-owned transient time-scale interface. - OPENQ4_GAMELIBS_SHA: 67fcd3e2142bc07c4871873ea1ec6fe592ac8a5a + OPENQ4_GAMELIBS_SHA: bc60187fb3e8552d33cb902360fa66bad1bfa101 concurrency: group: openq4-push-verification-${{ github.ref }} diff --git a/tools/tests/vscode_fast_build.py b/tools/tests/vscode_fast_build.py index 90c611fb1..e9b0686ac 100644 --- a/tools/tests/vscode_fast_build.py +++ b/tools/tests/vscode_fast_build.py @@ -125,9 +125,11 @@ def validate_launch_configs() -> None: for index, token in enumerate(args[:-2]) if token in ("+set", "+seta") and args[index + 1] == "ui_autoJoin" ] - if values != ["1"]: + # The interactive configurations start at the join screen, which is the + # shipped default; the automated MP profiles below still pin it to 1. + if values != ["0"]: raise AssertionError( - f"MP launch config {config.get('name')!r} must set ui_autoJoin exactly once to 1" + f"MP launch config {config.get('name')!r} must set ui_autoJoin exactly once to 0" ) if not mp_configs: raise AssertionError("Expected at least one VS Code MP launch configuration") @@ -163,8 +165,16 @@ def validate_mp_autojoin_policy() -> None: ) guide = read("AGENTS.md") - require(guide, "Keep `ui_autoJoin 1` enabled for multiplayer testing", "agent MP test policy") - require(guide, "explicit `+set ui_autoJoin 0`", "join-menu test exception") + require( + guide, + "keep an explicit `+set ui_autoJoin 1`", + "agent MP test policy", + ) + require( + guide, + "pass `+set ui_autoJoin 0`", + "interactive join-screen launch policy", + ) def validate_validation_coverage() -> None: