SwiftlyS2
Development

Custom HUD

You can use a custom_hud_layout entity to create a custom HUD for players. The interface is built with Panorama XML and CSS, while the server updates its state through the methods provided by CCSCustomHudLayout.

Custom HUD is still a new and unstable feature, valve may makes breaking change that cause incompatible API in the future. If the actual behavior differs from what is described here, contact the development team so the documentation can be corrected.

Example XML

The examples below use the following XML layout:

<!-- example.xml -->
<root>
    <styles>
        <include src="s2r://panorama/styles/custom_game/example.vcss_c" />
    </styles>
    <Panel class="example_class" 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>
</root>

The id attribute identifies the HUD element to update. For example, text1 is passed as the panelId in the methods below. {s:dynamic} is a dynamic string that can be updated by the server, where dynamic is the variableName.

Creating a custom_hud_layout Entity

Create the entity with the Entity API, set the path to the Panorama XML layout, notify the engine that the field has changed, and then spawn the entity:

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

Dynamic Strings

Setting and Reading a Global Value

SetDialogVariableString sets a global value used by all players:

hud.SetDialogVariableString("text1", "dynamic", "Global text");

string? globalValue = hud.GetDialogVariableString("text1", "dynamic");

The text1 argument corresponds to the XML element's id, while dynamic is the variable name in {s:dynamic}. GetDialogVariableString returns null when no global value has been set.

Setting, Reading, and Removing a Per-Player Override

Use methods with the ForPlayer suffix to set an override for a specific player:

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");

GetDialogVariableStringForPlayer reads only the player's override and does not fall back to the global value. It therefore returns null when no override has been set, even if a global value exists. After RemoveDialogVariableStringForPlayer is called, the value displayed to that player follows the global setting again.

Dynamic CSS Classes

You can dynamically control whether a HUD element has a particular CSS class. panelId corresponds to the element's id in the XML layout, and className should be the name of a class defined in the Panorama CSS.

EHudPanelClassStatus_t supports the following states:

StateMeaning
k_eHudPanelClassStatus_HasClassThe element has the class
k_eHudPanelClassStatus_DoesNotHaveClassThe element does not have the class
k_eHudPanelClassStatus_UndefinedNo class state is defined; this is also returned when reading a state that does not exist

Setting and Reading the Global Class State

hud.SetHasClass(
    "main_panel",
    "highlight",
    EHudPanelClassStatus_t.k_eHudPanelClassStatus_HasClass
);

EHudPanelClassStatus_t globalClassStatus = hud.GetHasClass(
    "main_panel",
    "highlight"
);

Setting and Reading a Per-Player Class State

const int playerId = 0;

hud.SetHasClassForPlayer(
    playerId,
    "main_panel",
    "highlight",
    EHudPanelClassStatus_t.k_eHudPanelClassStatus_DoesNotHaveClass
);

EHudPanelClassStatus_t playerClassStatus = hud.GetHasClassForPlayer(
    playerId,
    "main_panel",
    "highlight"
);

Both GetHasClass and GetHasClassForPlayer return k_eHudPanelClassStatus_Undefined if the specified panelId, className, or corresponding state does not exist.

Input Capture

The input capture state controls whether the Custom HUD receives player input. When enabled, the player can move the mouse cursor freely to click buttons. You can set the state globally for all players or separately for a specific player.

Global Input Capture

hud.SetInputCaptureEnabled(true);

bool globallyEnabled = hud.IsInputCaptureEnabled();

Per-Player Input Capture

const int playerId = 0;

hud.SetInputCaptureEnabledForPlayer(playerId, true);

bool enabledForPlayer = hud.IsInputCaptureEnabledForPlayer(playerId);

// Disable input capture for this player when the interaction is complete.
hud.SetInputCaptureEnabledForPlayer(playerId, false);

Handling Button Clicks

When input capture is enabled, clicking a button in the layout triggers Core.Event.OnCustomHudClicked. Subscribe to the event when the plugin loads and unsubscribe when it unloads:

public override void Load(bool hotReload)
{
    Core.Event.OnCustomHudClicked += OnCustomHudClicked;
}

public override void Unload(bool hotReload)
{
    Core.Event.OnCustomHudClicked -= OnCustomHudClicked;
}

private void OnCustomHudClicked(IOnCustomHudClickedEvent @event)
{
    if (@event.ButtonId != "action_button")
    {
        return;
    }

    Console.WriteLine($"Player {@event.PlayerId} clicked {@event.ButtonId}");
}

The event provides the following properties:

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 receives clicks from every Custom HUD layout. If a plugin creates multiple layouts, compare the layout entity you stored before handling the button ID.

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

Async Variants

Each thread-unsafe CCSCustomHudLayout extension method described above has a corresponding Async variant that returns a Task. Use these variants from background tasks or other asynchronous code. They run immediately when called on the game thread and otherwise schedule the operation on the game thread.

await hud.SetDialogVariableStringAsync("text1", "dynamic", "Global text");

await hud.SetHasClassForPlayerAsync(
    playerId,
    "main_panel",
    "highlight",
    EHudPanelClassStatus_t.k_eHudPanelClassStatus_HasClass
);

await hud.SetInputCaptureEnabledForPlayerAsync(playerId, true);

The async methods use the same parameters and behavior as their synchronous counterparts. This includes SetDialogVariableStringAsync, SetDialogVariableStringForPlayerAsync, RemoveDialogVariableStringForPlayerAsync, SetHasClassAsync, SetHasClassForPlayerAsync, SetInputCaptureEnabledAsync, and SetInputCaptureEnabledForPlayerAsync. Getter methods remain synchronous.

Method Summary

MethodDescription
SetDialogVariableStringSets a global dynamic string
GetDialogVariableStringReads a global dynamic string, returning null if it has not been set
SetDialogVariableStringForPlayerSets a player's dynamic string override
GetDialogVariableStringForPlayerReads a player's override without falling back to the global value
RemoveDialogVariableStringForPlayerRemoves a player's override so the displayed value follows the global setting again
SetHasClassSets the global CSS class state
GetHasClassReads the global CSS class state
SetHasClassForPlayerSets a player's CSS class state
GetHasClassForPlayerReads a player's CSS class state
SetInputCaptureEnabledSets the global input capture state
IsInputCaptureEnabledReads the global input capture state
SetInputCaptureEnabledForPlayerSets a player's input capture state
IsInputCaptureEnabledForPlayerReads a player's input capture state

Synchronous methods that change HUD state are thread-unsafe. From a background task, prefer and await their async variants instead of calling the synchronous methods directly. See Thread Safety for more information.

On this page