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
- Publish a workshop addon. Put the layout at
panorama/layout/custom_game/<name>.xml(source.xml, never.vxml/.vxml_c). - List the decimal addon id in
mm_extra_addons(and indescriptor.addons). - 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—.xmlpath underpanorama/layout/custom_game/.hideClass— class that hides a panel when applied.text— panel id → dialog variable forsetText. When id equals the variable name,setis enough.buttons— ids delivered byonClick.meters— name → fill panel id (setMeterappliess2-w0…s2-w10).slots— named pools of{ id, vars[] }forsetPool.
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).
| Call | Role |
|---|---|
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-w0…s2-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
- Creating a layout — XML / CSS the descriptor binds to
- Default components — skip this page if the pool is enough
- HUD
- HUD module