Search docs
Search modules and symbols
HUD

HUD

Custom on-screen UI for CS2. Two layers:

  1. Screen effectsFade, Shake, HintText from @s2script/cs2. Usermessages; no workshop addon.
  2. Custom Panoramaui from @s2script/cs2. Prefer ui.components() (pooled modals, badges, toasts). Fall back to ui.hud() only when you ship your own workshop layout.

There is no @s2script/sdk/hud. Import ui from @s2script/cs2 and drive panels through it. @s2script/cs2/ui is types-only — require("@s2script/cs2/ui") is undefined. ui is load-window only — it throws after settle.

Operator setup

Custom Panorama needs:

  • CS2 dedicated build ≥ 24934554 (AllowCustomGameUI is on; older clients crash on addon layouts).
  • MultiAddonManager on the server.
  • mm_extra_addons "3790153369" in game/csgo/cfg/multiaddonmanager/multiaddonmanager.cfg (decimal id, quoted, no workshop/ prefix). That is the shipped s2script HUD addon.

Clients must actually mount that addon. +host_workshop_map delivers a map, not a content addon. The server cannot detect a missing client mount — drive calls succeed and the player simply sees nothing.

Screen effects (Fade / Shake / HintText) skip this checklist.

First HUD

Register each layout at load (hud() / components()). The layout entity spawns when the first client becomes SIGNON_ACTIVE — not a console-only createLayout() workaround. Paint and drive never create the entity as a side-effect; OnMapStart only resets it. Draw from player-join, a game event, a command, or any other callback after a client is active.

import type { Client } from '@s2script/sdk';
import { ui } from '@s2script/cs2';

let panels: ReturnType<typeof ui.components>;

export function OnPluginStart(): void {
  panels = ui.components();
}

export function OnClientActive(client: Client): void {
  const slot = client.slot;

  panels.toast(slot, { title: 'Hello', message: 'from s2script', variant: 'good' });

  const badge = panels.badge({ corner: 'tr', accent: 'accent' });
  badge?.show(slot, { title: 'STATUS', text: 'online' });

  const modal = panels.modal({
    title: 'Pick one',
    rows: [
      { a: 'Heal', b: '100 HP' },
      { a: 'Nope', disabled: true },
    ],
    onPick: (s, index) => {
      panels.toast(s, { message: `picked ${index}`, holdSeconds: 3 });
    },
    buttons: [{ text: 'Close', variant: 'ghost', onClick: (s) => modal?.close(s) }],
  });
  modal?.open(slot);
}

createLayout() / components().ensure() force a spawn of a specific descriptor. They are optional once hud() or components() has already run and a client is active.

Toast, badge, and modal props live on Default components. A kill feed or MOTD of your own is Mounting your own.

Screen effects

No addon. Each call returns false if the usermessage / fields do not resolve:

import { command } from '@s2script/sdk';
import { Fade, Shake, HintText } from '@s2script/cs2';

export function OnPluginStart(): void {
  command('flash', (cmd) => {
    const slot = cmd.callerSlot;
    if (slot < 0) return;
    Fade.blind(slot, 2);
    Shake.to(slot, { amplitude: 10, frequency: 40, duration: 1 });
    HintText.to(slot, 'eyes open');
  });
}

Fade.to takes packed RGBA color plus duration / holdTime / flags. Prefer Fade.blind for a full-screen flash.

Hazards

  • Wait for an active clientOnMapStart resets the layout; it does not spawn. Paint/drive never create the entity. Player-join, game events, and commands are all fine.
  • Intern budget — three × 1024 vectors shared by every plugin on the HUD entity. Prefer the component pool over private layouts.
  • No client-mount detection — missing addon → silent blank screen.
  • Do not require("@s2script/cs2/ui") — types-only subpath; runtime is ui from @s2script/cs2.
  • onCustomHudClicked is observe-only — prefer Hud.onClick.
  • disabled is cosmetic unless you also call setDisabled (primitive) or accept that onPick still fires (components).

See also

s2script — Source 2 plugin framework

GitHub