Search docs
Search modules and symbols
Mounting your own

Mounting your own

Use ui.hud(descriptor) when you publish your own workshop layout. The descriptor is the contract between the XML and the server: which addon to mount, which panel ids exist, and how text / buttons / meters / row pools are named.

components(descriptor) only works if that XML declares the same pooled ids as s2script_lib.xml (s2_m0_*, s2_t0_*, s2_b0_*, …). A kill feed, scoreboard, or MOTD of your own is a new LayoutDescriptor plus hud().

Authoring the XML and CSS is Creating a layout. This page is the server side: bind the descriptor, spawn, drive.

Mount

  1. Publish a workshop addon. Put the layout at panorama/layout/custom_game/<name>.xml (source .xml, never .vxml / .vxml_c).
  2. List the decimal addon id in mm_extra_addons (and in descriptor.addons).
  3. Bind a descriptor and take the HUD at load. The layout entity spawns when the first client becomes active.
import { ui } from '@s2script/cs2';
import type { LayoutDescriptor } from '@s2script/cs2';

const KILLFEED: LayoutDescriptor = {
  addons: ['3790153369'], // your workshop id
  resource: 'panorama/layout/custom_game/killfeed.xml',
  hideClass: 's2-hide',
  text: {
    feed_0_a: 'feed_0_a',
    feed_0_w: 'feed_0_w',
    feed_0_v: 'feed_0_v',
    feed_1_a: 'feed_1_a',
    feed_1_w: 'feed_1_w',
    feed_1_v: 'feed_1_v',
  },
  buttons: [],
  meters: {},
  slots: {
    rows: [
      { id: 'feed_0', vars: ['feed_0_a', 'feed_0_w', 'feed_0_v'] },
      { id: 'feed_1', vars: ['feed_1_a', 'feed_1_w', 'feed_1_v'] },
    ],
  },
};

export function OnPluginStart(): void {
  const hud = ui.hud(KILLFEED);
  // ...
}

LayoutDescriptor fields:

  • addons — decimal workshop ids clients must mount.
  • resource.xml path under panorama/layout/custom_game/.
  • hideClass — class that hides a panel when applied.
  • text — panel id → dialog variable for setText. When id equals the variable name, set is enough.
  • buttons — ids delivered by onClick.
  • meters — name → fill panel id (setMeter applies s2-w0s2-w10).
  • slots — named pools of { id, vars[] } for setPool.

DEFAULT_HUD_DESCRIPTOR targets the shipped probe (s2script_hud.xml). It paints literal “S2SCRIPT PROBE OK” with no dialog-variable bindings — use it only to answer “is my addon mounted?”

Drive

Every call takes a player slot and returns HudResult (null or a reason). slot < 0 is "needs a player slot". Drive never creates the layout entity as a side-effect — register at load and let spawn wait for an active client (or call createLayout() explicitly).

CallRole
show(slot, panelId, { cursor? })Clear hideClass. { cursor: true } grabs input.
hide(slot, panelId)Apply hideClass; release that panel’s cursor lease.
set(slot, id, value)Dialog variable where id equals the variable name.
setText(slot, panelId, value)Dialog variable via the descriptor text map.
setClass(slot, panelId, className, on)Toggle a class.
setMeter(slot, name, percent)0..100, quantized to s2-w0s2-w10.
setPool(slot, pool, entries)Fill a slot pool. Overflow refuses (does not truncate).
capacity(pool)How many rows that pool holds.
onClick(buttonId, (slot) => …)Typed click. Conflicting handlers throw.
setDisabled(slot, buttonId, on)Server-side gate. A cosmetic class alone does not.
cursor(slot, on)Input capture for that player.
forget(slot)Drop per-player state.
import { hook, Clients } from '@s2script/sdk';
import { Player, ui } from '@s2script/cs2';

export function OnPluginStart(): void {
  const hud = ui.hud(KILLFEED);

  hook.on('player_death', (ev) => {
    const attacker = ev.getPlayerSlot('attacker');
    const victim = ev.getPlayerSlot('userid');
    const a = attacker >= 0 ? (Player.fromSlot(attacker)?.playerName ?? '?') : 'world';
    const v = victim >= 0 ? (Player.fromSlot(victim)?.playerName ?? '?') : '?';
    const w = ev.getString('weapon').replace(/^weapon_/, '');

    for (const client of Clients.all()) {
      if (client.slot < 0) continue;
      hud.setPool(client.slot, 'rows', [[a, w, v]]);
    }
  });
}

Prefer Hud.onClick for typed button routing. ui.onCustomHudClicked is a raw observer (player + button id); it always observes and cannot suppress the engine handler.

See also

s2script — Source 2 plugin framework

GitHub