HudResultResult of a drive call: null on success, or a human-readable reason it did not happen.
@s2script/cs2
Custom Panorama HUD: ui.components() (modals, badges, toasts) and the ui.hud() primitive.
import { HudResult } from "@s2script/cs2";Author-time types ship in the npm package; the engine injects the runtime at plugin load.
Add @s2script/sdk to your plugin's dependencies.
Generated from the shipped type definitions.
HudResultResult of a drive call: null on success, or a human-readable reason it did not happen.
LayoutSlotOne pre-declared row/slot in a pooled collection.
readonly id: stringreadonly vars: readonly string[]LayoutDescriptorWhat a layout offers — the contract between Panorama markup and server-side drive calls.
readonly addons: readonly string[]readonly resource: string.xml path under panorama/layout/custom_game/.readonly hideClass: stringreadonly text: Readonly<Record<string, string>>readonly meters: Readonly<Record<string, string>>readonly slots?: Readonly<Record<string, readonly LayoutSlot[]>>DEFAULT_HUD_DESCRIPTORDefault descriptor for the shipped s2script_hud.xml in workshop addon 3790153369.
**This targets a PROBE, not a HUD.** s2script_hud.xml carries literal text and has no dialog
variable bindings at all, so a panel driven through this descriptor permanently reads
"S2SCRIPT PROBE OK" and no call can change it. That is deliberate: literal text is what let us
prove the render pipeline end to end when nothing else could be trusted, and standalone-renders
and driveable-by-a-plugin cannot both be true of one layout.
Use it to answer "is my addon mounted and rendering at all?" — and nothing else. For real UI,
use CtxUi.components, which drives the generic pool in s2script_lib.xml and hands out
panels through a claim so two plugins cannot collide on the same one.
OnCustomHudClickedViewBlock-scoped view of one custom HUD click. Valid only during the handler.
readonly player: EntityRef | nullHudTyped custom HUD bound to one layout descriptor and its owned entity.
readonly layout: LayoutDescriptorshow(slot: number, panelId: string, opts?: { cursor?: boolean }): HudResulthide(slot: number, panelId: string): HudResultcursor(slot: number, on: boolean): HudResultset(slot: number, id: string, value: string | number): HudResultsetText(slot: number, panelId: string, value: string): HudResultsetClass(slot: number, panelId: string, className: string, on: boolean): HudResultsetMeter(slot: number, meterName: string, percent: number): HudResultcapacity(poolName: string): numbersetPool(slot: number, poolName: string, entries: readonly (readonly string[])[]): HudResultonClick(buttonId: string, handler: (slot: number) => void): voidsetDisabled(slot: number, buttonId: string, disabled: boolean): HudResultforget(slot: number): voidCtxUicomponents(descriptor?: LayoutDescriptor): ComponentscreateLayout(descriptor?: LayoutDescriptor): HudResultdescriptor. Returns null on success, or a reason.
Call from player-join (ctx.clients.onActive), a game event, a command, or any other callback
after a client is active. hud() / CtxUi.components also spawn at that point, so
this is only needed to force a spawn before the first of those calls. OnMapStart is still
too early — wait for an active client. Idempotent.hud(descriptor?: LayoutDescriptor): HudonCustomHudClicked(handler: (view: OnCustomHudClickedView) => HookResultValue | void): voiduiLoad-window custom HUD API. Same object as the former ctx.ui. Throws after settle.
VariantToast / footer-button colouring. ghost is the low-emphasis default for footers.
RowOne list row. Three columns: a is primary and flexes, b and c are right-aligned.
readonly a: stringreadonly b?: stringreadonly c?: stringreadonly disabled?: booleanonPick still fires, so you can say WHY it is unavailable.FooterButtonModalSpecreadonly title: string | ((slot: number) => string)readonly subtitle?: string | ((slot: number) => string)readonly rows: readonly Row[] | ((slot: number) => readonly Row[])readonly onPick?: (slot: number, index: number, row: Row) => voidreadonly detail?: (slot: number, row: Row | undefined, cursor: number) => readonly string[]readonly pageSize?: numberreadonly width?: "sm" | "md" | "lg" | "xl"md (560px).Modalopen(slot: number): voidclose(slot: number): voidisOpen(slot: number): booleanrefresh(slot?: number): voidslot to repaint every player who has it open.page(slot: number, delta: number): voidselect(slot: number, index: number): voidcursor(slot: number): numberforget(slot: number): voidrelease(): voidBadgeSpecreadonly corner?: "tl" | "tr" | "bl" | "br"readonly title?: stringreadonly accent?: "accent" | "good" | "warn" | "bad"BadgeA persistent corner element — the thing chat cannot be, because chat scrolls away.
show(slot: number, data?: { title?: string; text?: string }): voidhide(slot: number): voidrelease(): voidToastSpecreadonly title?: stringreadonly message?: stringreadonly variant?: Variantreadonly holdSeconds?: numberComponentsreadonly descriptor: LayoutDescriptorensure(): HudResultcomponents() also spawns once a client is active.readonly hud: Hudmodal(spec: ModalSpec): Modal | nullbadge(spec?: BadgeSpec): Badge | nulltoast(slot: number, spec: ToastSpec): HudResulthideAll(slot: number): voidforget(slot: number): voidbudget(): {
panelIds: number;
classNames: number;
variables: number;
declared: number;
warnAt: number;
cap: number;
}cap of 1024 (not one shared
3072), and all are shared by every plugin on the HUD entity — past the cap a name is refused
and its value simply never arrives. set spends from two vectors at once, charging its panel
id and its dialog variable name to different ledgers.
Interning is idempotent, so these climb only on a name's FIRST use; repainting is free.