swiftlys2/docs/api/ai/

Custom HUD

A custom_hud_layout entity builds a custom HUD for players. The interface is Panorama XML/CSS; the server updates its state through CCSCustomHudLayout methods.

Custom HUD is still a new and unstable feature - Valve may make breaking changes that cause incompatible API in the future. If actual behavior differs from what's described here, contact the development team so the docs can be corrected.

Example XML

<!-- example.vxml -->
<root>
    <styles>
        <include src="s2r://panorama/styles/custom_game/example.vcss_c" />
    </styles>
    <Panel class="example_class">
        <Panel id="main_panel">
            <Label class="example_label" id="text1" text="{s:dynamic}" />
            <Button class="example_button" id="action_button">
                <Label text="Click me" />
            </Button>
        </Panel>
    </Panel>
</root>

id identifies the HUD element to update - text1 is the panelId in the methods below. {s:dynamic} is a dynamic string the server can update, where dynamic is the variableName.

Creating a custom_hud_layout Entity

Create with the Entity API, set the layout path, notify the engine, then spawn:

var hud = Core.EntitySystem.CreateEntity<CCSCustomHudLayout>();
hud.StrLayout = "panorama/layout/custom_game/example.xml";
hud.StrLayoutUpdated();
hud.DispatchSpawn();

Layout Members

CCSCustomHudLayout extends CBaseEntity and holds the layout state directly. After assigning a field, call the matching ...Updated() method (StrLayout/StrLayoutUpdated above) so the change is picked up:

MemberDescription
StrLayout (string)Path of the layout file
Observable (bool)Whether the layout is observable
PanelIds (CUtlVector<CUtlString>)Panel id values the layout state refers to
ClassNames (CUtlVector<CUtlString>)CSS class names the layout state refers to
DialogVariableNames (CUtlVector<CUtlString>)Dialog variable names the layout state refers to
GlobalLayoutState (CCSCustomHudLayoutState)The layout state applied to every player
PlayerLayoutStates (CUtlVector<CCSCustomHudLayoutState>)The per-player layout states, indexed by player id

Their update methods are StrLayoutUpdated, ObservableUpdated, PanelIdsUpdated, ClassNamesUpdated, DialogVariableNamesUpdated, GlobalLayoutStateUpdated and PlayerLayoutStatesUpdated.

Layout State

CCSCustomHudLayoutState is the state the global and per-player methods below read and write. GlobalLayoutState applies to everyone, PlayerLayoutStates holds one entry per player:

MemberDescription
PlayerSlot (uint)The player the state belongs to
InputCaptureEnabled (bool)Whether input capture is enabled for that player
DialogVariableStrings (CUtlVector<HUDPanelDialogVariableString_t>)The dialog variable string overrides
HasClasses (CUtlVector<HUDPanelHasClass_t>)The CSS class overrides

Its update methods are InputCaptureEnabledUpdated, DialogVariableStringsUpdated and HasClassesUpdated.

Both override lists store indices into the layout's id/name lists instead of the strings themselves:

HUDPanelDialogVariableString_tDescription
PanelIdIndex (ushort)Index into CCSCustomHudLayout.PanelIds
DialogVariableIndex (ushort)Index into CCSCustomHudLayout.DialogVariableNames
Value (string)The value to show
IsSet (bool)Whether the override is set
HUDPanelHasClass_tDescription
PanelIdIndex (ushort)Index into CCSCustomHudLayout.PanelIds
ClassNameIndex (ushort)Index into CCSCustomHudLayout.ClassNames
ClassStatus (EHudPanelClassStatus_t)Whether the panel has the class

The methods in the sections below take the panelId, variableName and className strings directly, so you rarely need these indices yourself.

Dynamic Strings

text1 is the XML element's id, dynamic is the variable name in {s:dynamic}. Getters return null if unset; a per-player override never falls back to the global value, and removing it reverts the player to the global value.

hud.SetDialogVariableString("text1", "dynamic", "Global text");
string? globalValue = hud.GetDialogVariableString("text1", "dynamic");
 
const int playerId = 0;
hud.SetDialogVariableStringForPlayer(playerId, "text1", "dynamic", "Text shown only to this player");
string? playerValue = hud.GetDialogVariableStringForPlayer(playerId, "text1", "dynamic");
hud.RemoveDialogVariableStringForPlayer(playerId, "text1", "dynamic");

Dynamic CSS Classes

Control whether a HUD element has a CSS class. panelId is the element's id; className is a class defined in the Panorama CSS.

EHudPanelClassStatus_t states:

StateMeaning
k_eHudPanelClassStatus_HasClassThe element has the class
k_eHudPanelClassStatus_DoesNotHaveClassThe element does not have the class
k_eHudPanelClassStatus_UndefinedDoesn't override the existing status; also returned when reading a state that doesn't exist

Both getters return k_eHudPanelClassStatus_Undefined if the panelId/className/state doesn't exist.

hud.SetHasClass("main_panel", "highlight", EHudPanelClassStatus_t.k_eHudPanelClassStatus_HasClass);
EHudPanelClassStatus_t globalClassStatus = hud.GetHasClass("main_panel", "highlight");
 
const int playerId = 0;
hud.SetHasClassForPlayer(playerId, "main_panel", "highlight", EHudPanelClassStatus_t.k_eHudPanelClassStatus_DoesNotHaveClass);
EHudPanelClassStatus_t playerClassStatus = hud.GetHasClassForPlayer(playerId, "main_panel", "highlight");

Input Capture

Controls whether the Custom HUD receives player input (mouse cursor to click buttons), globally or per-player:

hud.SetInputCaptureEnabled(true);
bool globallyEnabled = hud.IsInputCaptureEnabled();
 
const int playerId = 0;
hud.SetInputCaptureEnabledForPlayer(playerId, true);
bool enabledForPlayer = hud.IsInputCaptureEnabledForPlayer(playerId);
hud.SetInputCaptureEnabledForPlayer(playerId, false); // disable once the interaction is complete

Handling Button Clicks

With input capture enabled, clicking a button fires APIIEventSubscriber.OnCustomHudClicked (Core.Event.OnCustomHudClicked). Subscribe on load, unsubscribe on unload:

public override void Load(bool hotReload)
{
    Core.Event.OnCustomHudClicked += OnCustomHudClicked;
}
 
public override void Unload()
{
    Core.Event.OnCustomHudClicked -= OnCustomHudClicked;
}
 
private void OnCustomHudClicked(<ApiRef name="IOnCustomHudClickedEvent" /> @event)
{
    if (@event.ButtonId != "action_button")
    {
        return;
    }
 
    Console.WriteLine($"Player {@event.PlayerId} clicked {@event.ButtonId}");
}
PropertyDescription
PlayerIdThe ID of the player who clicked the button
ButtonIdThe clicked button's id from the Panorama XML layout
CustomHudLayoutThe CCSCustomHudLayout entity that contains the clicked button

OnCustomHudClicked fires for clicks from every Custom HUD layout - if a plugin creates multiple layouts, compare against the layout entity you stored.

See Core Events for attribute-based listeners and general event subscription behavior.

Async Variants

Each thread-unsafe CCSCustomHudLayout extension method has an Async counterpart returning a Task, for use from background tasks or async code - it runs immediately on the game thread, or schedules onto it otherwise.

await hud.SetDialogVariableStringAsync("text1", "dynamic", "Global text");
 
await hud.SetHasClassForPlayerAsync(playerId, "main_panel", "highlight", EHudPanelClassStatus_t.k_eHudPanelClassStatus_HasClass);
 
await hud.SetInputCaptureEnabledForPlayerAsync(playerId, true);

Same parameters/behavior as the sync counterparts: SetDialogVariableStringAsync, SetDialogVariableStringForPlayerAsync, RemoveDialogVariableStringForPlayerAsync, SetHasClassAsync, SetHasClassForPlayerAsync, SetInputCaptureEnabledAsync, SetInputCaptureEnabledForPlayerAsync. Getters stay synchronous - no async variant for reads.

Synchronous methods that change HUD state are thread-unsafe. From a background task, await the async variant instead. See Thread Safety.