Skip to content

Give multiplayer a compact join card in Quake 4's own style, over a softened map - #149

Merged
themuffinator merged 4 commits into
mainfrom
claude/mp-join-menu-redesign-2db92e
Sep 3, 2026
Merged

Give multiplayer a compact join card in Quake 4's own style, over a softened map#149
themuffinator merged 4 commits into
mainfrom
claude/mp-join-menu-redesign-2db92e

Conversation

@themuffinator

Copy link
Copy Markdown
Owner

Connecting to a multiplayer server opened the entire in-game menu on its Join tab — ten navigation buttons, a full server dump, a chat log and a text field, all to answer one question. This replaces that with a compact join card drawn in Quake 4's own menu style, over a map that is pushed out of focus behind it.

What changed

A dedicated join card. It carries only what a player needs before picking a side: gametype and map, whether the match is in warmup, the scoring limit that gametype actually enforces, how full the server is, the server name, and just the choices that mode allows — join, pick a side, or spectate. Settings and Leave Match sit in the header as symbols. A refused team switch reports in place on the card instead of on the Join tab nobody is looking at.

The button set comes from the gametype descriptor via two GUI state flags, so the GUI no longer carries its own copy of the gametype table, and the scoring-limit lookup moved into one helper so the card and the Join tab cannot drift apart. The frame comes in two heights, so a Duel offering one choice does not get a panel sized for four.

Quake 4's visual idiom, from shipped art. No new interface images were authored; everything is already in the retail PK4s.

  • tooltip_edge / tooltip_mid for the frame, which carries the olive inner rail and cuts a 45° chamfer off every corner. The bottom cap is the top cap mirrored with matscalex -1 matscaley -1, which is how the stock tooltip does it.
  • Real Q4 button plates (b6_light, b4_light): 45° cut at the bottom left, highlight down the left and along the bottom, fading to nothing at the right. Marine-font label, corner marker, plates tinted olive rather than white.
  • The menu's own hover contract: snap to orange on enter, ease back over 300 ms on exit, label growing by exactly 0.01, main_menu_mouseover / main_menu_selection on both ends.
  • gfx/guis/common/gear1_still and gfx/guis/mainmenu/icon_arrow for the header symbols.

The panel is authored inside the plain 640x480 canvas and never reads gui::virtual_screen_x_expand, so it keeps its 4:3 shape and stays centred on a widescreen display instead of stretching across it. It deliberately does not dim the rest of the screen — Quake 4 doesn't, and the softened backdrop does that job.

A renderer parity fix for the soft focus. The OpenGL half of the Raven special-effect blur 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; parm 4 was inverted between them too. So a request for no tint became a request for no blur on Vulkan, a gentle blur came out heavy on OpenGL, and the OpenGL renderer module produced a half-blurred quadrant with a hard seam. rvspecial_medlabs.fs now mirrors the Vulkan contract — parm 6 is strength, parm 4 converts to the same normalized falloff, the tint is driven by the approach colour's own alpha — and a zero-strength request correctly draws nothing. The Arena Campaign's depth of field had the same split and is fixed by the same change.

ui_autoJoin was never the gate. It has always defaulted to 0; what suppressed the join screen was all 98 VS Code MP launch configurations passing +set ui_autoJoin 1, and because the CVar is archived one such launch made it stick permanently. Those now pass 0. Automated profiles (renderer_gameplay_benchmark.py, stock_asset_baseline.py, the dedicated smokes) still set 1 in their own code, and AGENTS.md now says which is which instead of implying one blanket rule.

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 with a material table, palette and type scale, button and frame anatomy with the exact hover script, the 4:3 authoring rule, how to soften a scene behind a partial-screen panel, a pre-flight checklist, and the traps. AGENTS.md cites it as a project rule.

Companion game-module change

This needs openQ4-game at bc60187 or later, on main:

  • cdcde5e — the join screen's model (UpdateJoinScreenGui, SetJoinScreenSoftFocus) and a sticky joinScreenPending flag.
  • bc60187 — the blur controller parameters that go with the shader change here.

Notes for a reviewer

  • The MP menu is Activate()d twice per openidMultiplayerGame::StartMenu activates it, then idAsyncNetwork::ExecuteSessionCommand hands the result to session->SetGUI, which activates it again; a listen-server host is also offered the screen again after its own player entity respawns. The old one-shot initial_join GUI state bit was consumed by the first of those, so every later activation fell back to the full menu. That is why the offer is now tracked in code and cleared only when the player answers.
  • onActivate restores the chrome and its intro animation on the non-join path. anim_in sets p_main::visible 1 a frame after the join event hid it, which is the trap that made the first version of this look like it had not been applied at all.
  • A GUI parse error is only a warning. The engine logs mpmain.gui, line N: expected ... and silently drops the rest of that windowDef. Related: windowDef properties do not resolve $desktop:: names — only scripts do — so a matcolor property has to be literal numbers. Both are written up in the new design doc.

Validation

  • Built clean (engine, both renderer modules, both game modules) and staged to .install/.
  • Entered gameplay on mp/q4dm1, mp/q4dm2 and mp/q4dm3 across Deathmatch, Team Deathmatch and Tourney, on r_renderApi gl, gl-module and vulkan, at 1600x900. Screenshots captured for each; all three backends now produce the same softened backdrop.
  • Zero GUI parse warnings in openq4.log on every run.
  • 13 validation scripts pass: match_control_ui_contract, competitive_match_layer, lang_table_encoding, competitive_match_localization, match_control_localization_bridge, arena_campaign, mp_bot_navigation, renderer_budget_contract, stock_asset_baseline, settings_menu_coverage, mp_bot_server_menu, docs_link_integrity, release_tooling_safety.
  • Seven new #str_ entries added to all six language tables.
  • Release-note material updated in docs/dev/release-completion.md and docs/dev/releases/v0.12.0.md.

Not covered: clicking the buttons was verified by reading the emitted commands rather than by driving the mouse, so the join/spectate/team actions themselves are unchanged stock commands but have not been clicked end to end in an automated run.

🤖 Generated with Claude Code

themuffinator and others added 2 commits September 3, 2026 09:14
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

themuffinator and others added 2 commits September 3, 2026 10:35
… 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 <noreply@anthropic.com>
Both sides moved the CI GameLibs pin, which is the whole conflict. main's
pin (f5596b6) is a descendant of the two openQ4-game commits this branch
needs - cdcde5e for the join screen's model and bc60187 for the blur
controller parameters - so taking main's value keeps both intents and
carries its own newer multiplayer effects and clock work as well.

Nothing else on main touched this branch's files. Verified against the
merged tree: all 156 scripts in the validation smoke list pass with
OPENQ4_GAMELIBS_REPO pinned to f5596b6, the full build links, and a Team
Deathmatch join on Vulkan plus a Deathmatch join on OpenGL both come up
clean with no GUI parse warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@themuffinator
themuffinator merged commit 9083c2f into main Sep 3, 2026
21 of 25 checks passed
@themuffinator
themuffinator deleted the claude/mp-join-menu-redesign-2db92e branch September 3, 2026 11:06
@themuffinator
themuffinator restored the claude/mp-join-menu-redesign-2db92e branch September 3, 2026 11:06
@themuffinator
themuffinator deleted the claude/mp-join-menu-redesign-2db92e branch September 7, 2026 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant