terminalCode Example

Chat UI Pattern with RPC Broadcasting

calendar_today May 3, 2026 schedule ~1 min read person patrickjr verified 50

Chat UI with RPC Broadcasting

A Razor UI component for game chat that handles local input, RPC broadcasting, Steam filtering, and automatic message expiration.

Implementation

CSHARP
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Utility
@namespace Sandbox
@inherits PanelComponent

<root>
    <div class="output">
        @foreach (var entry in Entries)
        {
            <div class="chat_entry">
                @if (entry.steamid > 0)
                {
                    <div class="avatar" style="background-image: url( avatar:@entry.steamid )"></div>
                }
                <div class="author">@entry.author</div>
                <div class="message">@entry.message</div>
            </div>
        }
    </div>

    <div class="input">
        <TextEntry @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>
    </div>
</root>

@code
{
    TextEntry InputBox = default;

    public record Entry(ulong steamid, string author, string message, RealTimeSince timeSinceAdded);
    List<Entry> Entries = new();

    protected override void OnUpdate()
    {
        if (InputBox is null)
            return;

        Panel.AcceptsFocus = false;

        // Toggle chat focus with input action
        if (Input.Pressed("chat"))
        {
            InputBox.Focus();
        }

        // Auto-remove entries after 20 seconds
        if (Entries.RemoveAll(x => x.timeSinceAdded > 20.0f) > 0)
        {
            StateHasChanged();
        }

        // CSS state management
        SetClass("open", InputBox.HasFocus);
        SetClass("hide", Player.FindLocalPlayer()?.WantsHideHud ?? false);
    }

    void ChatFinished()
    {
        var text = InputBox.Text;
        InputBox.Text = "";

        if (string.IsNullOrWhiteSpace(text))
            return;

        AddText(text);
    }

    [ConCmd("say")]
    private static void Say(string msg)
    {
        var chat = Game.ActiveScene.GetAll<Chat>().FirstOrDefault();

        if (Application.IsDedicatedServer)
        {
            chat.AddSystemText(msg);
        }
        else
        {
            chat.AddText(msg);
        }
    }

    [Rpc.Broadcast]
    public void AddText(string message)
    {
        message = message.Truncate(300);

        if (string.IsNullOrWhiteSpace(message))
            return;

        var author = Rpc.Caller.DisplayName;
        var steamid = Rpc.Caller.SteamId;

        Log.Info($"{author}: {message}");

        // Steam content filtering
        var filteredName = Steam.FilterName(author, steamid);
        message = Steam.FilterChat(message, steamid);

        Entries.Add(new Entry(steamid, filteredName, message, 0.0f));
        StateHasChanged();
    }

    [Rpc.Broadcast(NetFlags.HostOnly)]
    public void AddSystemText(string message, string icon = "ℹ️")
    {
        message = message.Truncate(300);

        if (string.IsNullOrWhiteSpace(message))
            return;

        Entries.Add(new Entry(0, icon, message, 0.0f));
        StateHasChanged();
    }
}

Key Patterns

TextEntry Component

CSHARP
<TextEntry @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>

// In code:
void ChatFinished()
{
    var text = InputBox.Text;
    InputBox.Text = "";
    // Process the submitted text...
}

RPC Broadcasting

CSHARP
// Called by client, executes on host, broadcasts to all clients
[Rpc.Broadcast]
public void AddText(string message)
{
    // Rpc.Caller contains the connection that initiated the RPC
    var author = Rpc.Caller.DisplayName;
    var steamid = Rpc.Caller.SteamId;
    
    // This runs on all clients
    Entries.Add(new Entry(steamid, author, message, 0.0f));
    StateHasChanged(); // Trigger UI re-render
}

// Host-only broadcast
[Rpc.Broadcast(NetFlags.HostOnly)]
public void AddSystemText(string message, string icon)
{
    // Only the host can call this
}

Steam Integration

CSHARP
// Filter names and chat through Steam's content filters
var filteredName = Steam.FilterName(author, steamid);
var filteredMessage = Steam.FilterChat(message, steamid);

Avatar Display

CSS
/* CSS for Steam avatar display */
.avatar {
    background-image: url(avatar:76561197991348132);
    /* The avatar: protocol automatically fetches Steam avatars */
}

RealTimeSince for Expiration

CSHARP
// RealTimeSince automatically tracks time since creation
public record Entry(..., RealTimeSince timeSinceAdded);

// Check if 20 seconds have passed
if (entry.timeSinceAdded > 20.0f)
    // Remove entry

Input Focus Management

CSHARP
// Prevent panel from stealing focus
Panel.AcceptsFocus = false;

// Check if TextEntry has focus
SetClass("open", InputBox.HasFocus);

// Blur all inputs
Sandbox.UI.InputFocus.Clear();

CSS Classes for State

  • open - When chat input is focused
  • hide - When player wants HUD hidden
These are toggled via SetClass() in OnUpdate() for reactive styling.
Was this helpful?