Skip to content

feat(hud): clickable custom HUD (custom_hud_layout + Workshop addon), v2 screens - #293

Draft
sivert-io wants to merge 77 commits into
masterfrom
feat/custom-hud
Draft

sivert-io wants to merge 77 commits into
masterfrom
feat/custom-hud

Conversation

@sivert-io

@sivert-io sivert-io commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Custom in-game HUD for Ready Up: the server drives Panorama layouts from the Auto Tournament HUD Workshop addon (3827905393) through CS2's custom_hud_layout entity. Players can click buttons on it. Vikunja #428.

Design: HUD kit canvas. The "Buttons (v2)" page is what this PR builds; "Commands (v1)" is the earlier typed-command design. Plan and test list: hud/BUTTONS-PLAN.md.

Addon (hud/)

  • at_ui (v2, clickable), generated by hud/tools/gen_ui.py. One card (bottom left) and one centre panel (lower half) with fixed slots: text, team dots, confirm chips, command chips, 8 pill buttons and a close button. Colours and motion follow the canvas.
  • at_kit (v1) is kept for the end-screen leaderboard. at_hello is the P0 proof and at_click the first click test.
  • hud/build.ps1 compiles everything with the Workshop Tools resource compiler.

Server

  • core: ru_api 1.18 run_chat_command(slot, text) runs a line as if the player typed it in chat (same routing and permission checks). Buttons use it.

  • hud.so: readyup.hud.v1 gains screen(slot, spec, on_click, user). A whole screen is given as key=value lines (plugins/hud/hud_screen.h, new ctest hud_screen). Behaviour:

    • A new screen id plays the leave and enter animation; the same id updates in place.
    • hud.so owns the cursor: mouse=on captures it, mouse=b captures it while the buy menu is open (B), and chips=1 never does.
    • The cursor is released on a click, when the screen changes, or on disconnect.
    • Clicks come from user message 390 through a verified ClientSvcUserMessage vtable hook (gamedata/engine-surface.hud.json).
    • Debug tools: ru hudkit demo <screen> [slot], ru hudkit screen <slot> k=v;k=v, and ru hudkit buttons (held-keys probe).
  • match.so (hud_screens.{h,cpp}, built from the same data as the center-HTML panels):

    • warmup card with Ready / Unready (B)
    • "everyone ready" countdown
    • knife side pick: CT / T for the winners (cursor on, no close); "picking a side" for everyone else
    • pauses: an Unpause button, confirm chips, then "Waiting for …"
    • give up vote (.gg) and replay request (.stop)
    • .menu: the admin menu (pause, replay a round, change map, swap, force ready, restart, end match with a confirm) or help for players

    Mid-round no screen takes the cursor; buttons are drawn as typed commands instead. readyup.cfg hud_screens=0 (or no hud.so) keeps the center HTML.

Tested

  • All 57 ctests pass (Debian build container).
  • Deployed to the test instance (0.1.0-hud.10): selftest PASS, the click hook is installed, and ru hudkit demo creates the at_ui entity and writes slot state.
  • In game earlier (v1 layouts, at_click): the layouts render, blur works, and clicks arrive with forced capture.

Not done

  • Publish the addon update that contains at_ui, then check every screen in game (BUTTONS-PLAN.md, "Tomorrow").
  • Section 1 server tests: Y chat without capture, buying while captured, Tab / E / R probe.
  • The map vote, plus the welcome, go-live and result cards, still use center HTML.

🤖 Generated with Claude Code

sivert-io and others added 22 commits October 26, 2026 20:04
One hand-written layout (panorama/layout/custom_game/at_hello.xml): a centered
card with a Label bound to {s:text}, plus its stylesheet, a build script that
compiles both with the Workshop Tools resource compiler, and the build/publish
steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One card on the left above the chat, shown only when nobody is fighting:
a header (state in a few words), one optional body (players, ready dots,
round damage or download bar) and up to two command chips. Every change is
a class transition with the highlights overlay's easing; text rolls between
two layers. Hides itself while the buy menu is open. The README lists the
ids and classes the server drives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ram logo and AUTO TOURNAMENT, centered above the state header with a
hairline under it. The logo ships as hud/images/at_logo.png with a .vtex
recipe that compiles it to BGRA8888 (DXT5 renders pink in Panorama);
build.ps1 now compiles images too and only warns when Workshop Tools hold
their caches open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…i_vtable

plugins/hud drives CS2's custom_hud_layout entity for the Auto Tournament
HUD addon (workshop 3827905393): string tables, per-player and global
layout states, text and classes, written with tier0's allocator and the
dialog-variable class's own vtable. It keeps the desired state and pushes it
again after a map change and a few seconds after a player takes a slot.
Other plugins use readyup.hud.v1; `ru hud kit <state>` shows the card.

Core: API 1.17 adds surface_rtti_vtable(key), the primary vtable of an
engine-surface rtti entry (gamedata/engine-surface.hud.json lists
HUDPanelDialogVariableString_t). Packaged as the hud component, in the
essentials and full bundles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nds in the HUD hints

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…screen

One element at a time: the card above the chat for routine information,
the centre panel in the lower half for moments everyone must see, and the
end-of-map leaderboard (world blurred, K D A DMG ADR KAST HS, MVP per team,
the viewer's row by name). Sentences with inline command chips, short
commands only, countdowns as seconds. The layout is generated by
hud/tools/gen_kit.py (fixed slots). `ru hudkit kit <state>` shows every
state in game. README lists the ids and classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the same as .p)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…edge divider

CS2 loads fonts with fontconfig from fixed folders on disk (csgo/panorama/fonts,
the OS font folder), never from addons, so an addon can't ship Geist or Sora:
the HUD uses Noto Sans and Noto Mono, which every player has (P2 font question
answered). The card background is darker, the divider under the brand runs
edge to edge, and punctuation after a bold word is no longer bold (part class
`tight`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne on the edge

Each element leaves and enters through its "on" class, and the demo waits
0.35 s between the two (both in one server frame showed nothing). Rows,
sentence parts, commands and the end screen come in staggered; the card
header rolls between states. The centre panel is 640 px wide instead of
growing with its full-width layers, and the end screen's accent line sits
on the card's edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Message hook

`ru hudkit click [slot]` opens at_click.xml for a player: "Which side do you
want?" with CT and T pill buttons, and captures their cursor. A click comes
back as user message 390 (CS_UM_CustomHudClicked: layout handle + Button id),
caught by patching ISource2GameClients::ClientSvcUserMessage (vtable slot 38,
verified by two string anchors in engine-surface.hud.json; that slot calls the
handler that compares the type with 390). The plugin then shows the pick,
gives the mouse back and closes the card. A capture never outlives 30 s, a
round start or a disconnect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k hook

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e_buymenu releases)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds); 30 s safety stays

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… closed without a pick

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… with buttons

- hud/: at_ui.xml (generated by tools/gen_ui.py) + at_ui.css: one card and one centre panel
  with fixed slots (text, team dots, confirm chips, command chips, 8 pill buttons, close)
- core: ru_api 1.18 run_chat_command (a button runs its chat command as if typed)
- hud.so: readyup.hud.v1 screen(slot, spec, on_click): key=value specs (hud_screen.h, ctest
  hud_screen), leave/enter animation per id, cursor rules (mouse off|b|on, chips mid-round),
  ru hudkit demo / screen for testing without a match
- match.so: hud_screens: warmup card (Ready/Unready, B), knife side pick (CT/T, cursor on),
  pauses (Unpause), give up / replay votes, .help and .menu (admin menu: pause, replay round,
  change map, end match with confirm); readyup.cfg hud_screens=0 keeps the center HTML

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sivert-io sivert-io changed the title feat(hud): custom_hud_layout P0 spike addon (at_hud) feat(hud): clickable custom HUD (custom_hud_layout + Workshop addon), v2 screens Oct 26, 2026
sivert-io and others added 7 commits October 27, 2026 00:25
…zip)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A round restart deletes map entities. Marking the dying custom_hud_layout
changed crashed the server (WriteEnterPVS: GetEntServerClass failed) when
the first player joined and CS2 began the match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tred

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Ready was clicked the card kept its id, so Unready never got the
cursor back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- at_ui: info rows (line=label|value, a typed command boxed) and size=mini,
  the card as a one-line pill; the ready card shrinks to it
- .menu help home: ready / tech pause, votes, call an admin, match info,
  commands, admin; votes, match info and commands pages
- admin: change map shows the pool plus typed .map; end match asks who won
  and confirms
- .endmatch / .ru match end take team1 | team2 | none; a platform match
  ends through the platform's end_match path so the winner reaches it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sivert-io and others added 30 commits October 27, 2026 14:34
…he right

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… unpause card

The full card took the mouse while the round was still being played.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… side card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tead of chat lines

The card was only in the HUD tour; matches printed the chat report. Chat stays without the HUD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Halftime, overtime, round restored, sides chosen / time is up, match ended by an admin and
unpausing in the centre; no timeouts / tech pauses left, not now, admin on the way / already
called, .asay and the platform's on-screen say on the side card. HudScreensNoticeAll shows a
card to everyone or one side. The admin-call alert stays on the side (it can come mid-round).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round history strip, wins per side, four awards, +/- / UD / EK columns and MVP counts, from the
map's own stats; each player sees it from their side. Column heads explain themselves on hover:
the end screen holds the cursor (ru_hud_v1 set_capture, v1.3). The result card stays as the
fallback without the HUD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l names; the HUD at 85 %

.dev shows 20 screens a page in five columns, each named for what it is. The end-screen demo
holds the cursor so its tooltips work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… word to "Souvenir"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…card, only your row tinted

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; damage at the next freeze time

at_kit's end screen is now generic slots (12 columns x 6 rows per team, round strip, awards) plus
utility classes (text tones, fills, tints, tags, column widths, line, blur, dim, scale): columns,
headers, tooltip text, widths and colours come from end_screen.cpp, so changing them needs no
addon publish. .dev's leaderboard runs the real end screen with made-up numbers (.enddemo).
HUD scale: scale-70..100 on at_ui's wraps,  and cfg scale= (default 85).
The damage report waits for the next round's freeze time (players may still go for exits).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m A take the whole card)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a in the buy menu); HUD at 75 % by default

The centre panel gets the pill style too (addon).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he buy grid; pill width and sturdier tooltips (addon)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ok, chat while it is up)

Background classes bg-100 .. bg-70 on the card (addon); hover tooltips dropped from use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, bga), end card bg 0-100, margins, radius, row height

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…like the HUD cards

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds solid

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…use) as ico-* classes; the round strip uses them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d-drawn set is gone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…user, timer, rescue)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er reloaded a workshop home every 3 minutes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…til an event names it)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… messages without delay

The round damage card uses the at_kit table (given / taken / HP) the .dev menu showed as the
second variant; the line card is gone. The go-live card shows when the live round starts, and
every platform say (the auto-start countdown) is also a HUD side card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The write pause after round_prestart now ends 0.1 s after round_poststart (the map cleanup is
done by then) instead of a full second; the damage card came ~1 s late. The .dev menu has one
damage card, the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Leaderboard (server side): the viewer's orange row shows every number in ink (grey and red/green
were 2.9-4.2:1 there), only their MVP tag is orange, award values and 100+ ADR are ink, and the
win-reason pill shows only for overtime, damage or forfeit wins.
Addon: every card gets a second background layer (one renders see-through in CS2, which let the
map wash out the text).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server: no title that repeats its label (timeout/tech pause fallbacks), "Starts at freeze time"
for a pending pause, a Forfeit label on the missing-team card, "Going live" next to its timer,
no ready count when the team dots show it, leaderboard at 90 % (its 10 px labels were ~8 px).
Addon (next publish): 11 px table and award labels, button subtext without 70 % opacity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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