User Messages

How to send and receive protobuf user messages between server and clients.

Understanding User Messages

User messages are the primary method for sending custom data from the server to clients in Source 2. They use Protocol Buffers (protobuf) for serialization, providing a structured and efficient way to transmit game information.

Common use cases include:

  • Displaying HUD elements and notifications
  • Playing sounds or visual effects on clients
  • Sending custom game state information
  • Creating chat messages and hints
  • Triggering client-side events

User messages work similarly to SourceMod's implementation, with protobuf replacing the old bitbuffer system for better type safety and flexibility.

Finding Message Definitions

You can find protobuf message definitions at:

  • CS:GO Protobufs: SteamDatabase/Protobufs
  • These .proto files define available messages and their fields
  • Common messages include TextMsg, HintText, ShowMenu, Fade, and many more

Each message definition shows the message name, ID, and available fields with their types.

Hooking User Messages

You can intercept user messages before they're sent to clients:

c#
c++
python
go
js
lua
using Plugify;
using static s2sdk.s2sdk;

public unsafe class Sample : Plugin
{
    public void OnPluginStart()
    {
        // Find message ID by name
        short msgId = UserMessageFindMessageIdByName("TextMsg");

        // Hook the message in Post mode
        HookUserMessage(msgId, OnTextMessage, HookMode.Post);
    }
    
    public void OnPluginEnd()
    {
        short msgId = UserMessageFindMessageIdByName("TextMsg");
        UnhookUserMessage(msgId, OnTextMessage, HookMode.Post);
    }
    
    private static void OnTextMessage(nint userMessage)
    {
        // Read message data
        string text = PbReadString(userMessage, "param", -1);
        PrintToServer($"TextMsg intercepted: {text}\n");
    }
}

Creating and Sending User Messages

Create user messages using the UserMessage class with automatic cleanup:

// Create from message name (most common)
using (var msg = new UserMessage("TextMsg"))
{
    // Set recipients
    msg.AddAllPlayers();

    // Set message fields
    msg.AddString("param", "Hello from server!");
    msg.SetUInt32("dest", 3);  // HUD_PRINTTALK

    // Send the message
    msg.Send();
} // Automatically destroyed

// Or create from message ID
short msgId = UserMessage.FindMessageIdByName("TextMsg");
using (var msg = new UserMessage(msgId))
{
    // Configure and send...
}

Reading and Writing Data Types

User messages support various data types. You can use either the UserMessage class methods (recommended) or the PbRead* and PbSet* functions:

Integers

using (var msg = new UserMessage("TextMsg"))
{
    // Write integers
    msg.SetInt32("fieldName", 42);
    msg.SetInt64("bigField", 1000000L);
    msg.SetUInt32("unsignedField", 99u);

    // Read integers (in hook callbacks)
    int value = msg.GetInt32("fieldName");
    long bigValue = msg.GetInt64("bigField");
    uint unsigned = msg.GetUInt32("unsignedField");
}

Floats and Doubles

using (var msg = new UserMessage("CustomMsg"))
{
    // Write floating-point values
    msg.SetFloat("speed", 1.5f);
    msg.SetDouble("precision", 3.14159);

    // Read floating-point values (in hook callbacks)
    float speed = msg.GetFloat("speed");
    double precision = msg.GetDouble("precision");
}

Booleans

using (var msg = new UserMessage("ShowMenu"))
{
    // Write boolean
    msg.SetBool("needmore", true);
    msg.SetBool("active", false);

    // Read boolean (in hook callbacks)
    bool isActive = msg.GetBool("active");
}

Strings

using (var msg = new UserMessage("TextMsg"))
{
    // Write string
    msg.AddString("param", "Custom message");

    // Read string (in hook callbacks)
    string text = msg.GetString("param");
}

Vectors and Angles

using System.Numerics;

using (var msg = new UserMessage("CustomPositionMsg"))
{
    // Write 3D vectors
    msg.SetVector3("origin", new Vector3(100, 200, 300));
    msg.SetQAngle("angles", new Vector3(0, 90, 0));

    // Write 2D vector
    msg.SetVector2("pos", new Vector2(50, 100));

    // Read vectors (in hook callbacks)
    Vector3 position = msg.GetVector3("origin");
    Vector3 angles = msg.GetQAngle("angles");
    Vector2 pos2d = msg.GetVector2("pos");
}

Enums

using (var msg = new UserMessage("CustomMsg"))
{
    // Write enum value
    msg.SetEnum("type", 2);
    msg.SetEnum("state", (int)GameState.Running);

    // Read enum value (in hook callbacks)
    int enumValue = msg.GetEnum("type");
    GameState state = (GameState)msg.GetEnum("state");
}

Colors

using (var msg = new UserMessage("Fade"))
{
    // Write color (RGBA vector)
    msg.SetColor("clr", new Vector4(1.0f, 0.0f, 0.0f, 1.0f)); // Red, full alpha

    // Create color from components
    float r = 1.0f;
    float g = 0.5f;
    float b = 0.0f;
    float a = 1.0f;
    var color = new Vector4(r, g, b, a);
    msg.SetColor("clr", color);

    // Read color (in hook callbacks)
    Vector4 clr = msg.GetColor("clr");

    // Extract components
    float red   = clr.X;
    float green = clr.Y;
    float blue  = clr.Z;
    float alpha = clr.W;
}

Working with Repeated Fields

Some protobuf fields are arrays (repeated fields):

using (var msg = new UserMessage("CustomMsg"))
{
    // Add values to repeated field
    msg.AddInt32("items", 10);
    msg.AddInt32("items", 20);
    msg.AddInt32("items", 30);
    msg.AddString("names", "Player1");
    msg.AddString("names", "Player2");

    // Get count of repeated field
    int count = msg.GetRepeatedFieldCount("items");
    PrintToServer($"Total items: {count}\n");

    // Read repeated values
    for (int i = 0; i < count; i++)
    {
        int value = msg.GetInt32("items", i);
        PrintToServer($"Item[{i}] = {value}\n");
    }

    // Set specific index in repeated field
    msg.SetRepeatedInt32("items", 0, 999);  // Set index 0 to 999

    // Remove value from repeated field
    msg.RemoveRepeatedFieldValue("items", 0);  // Remove index 0
}

Working with Nested Messages

Some messages contain nested message fields:

using (var msg = new UserMessage("ComplexMsg"))
{
    // Get nested message
    var nestedMsg = msg.GetMessage("nested_field");
    if (nestedMsg != null)
    {
        // Read from nested message
        int value = nestedMsg.GetInt32("value", 0);
        string name = nestedMsg.GetString("name", 0);
        PrintToServer($"Nested: {name} = {value}\n");
    }

    // Get repeated nested message
    int count = msg.GetRepeatedFieldCount("repeated_nested");
    for (int i = 0; i < count; i++)
    {
        var repeatedMsg = msg.GetRepeatedMessage("repeated_nested", i);
        if (repeatedMsg != null)
        {
            string name = repeatedMsg.GetString("name", 0);
            int score = repeatedMsg.GetInt32("score", 0);
            PrintToServer($"Player {i}: {name} - {score}\n");
        }
    }

    // Add nested message to repeated field
    var newMsg = new UserMessage("PlayerInfo");
    newMsg.SetString("name", "NewPlayer");
    newMsg.SetInt32("score", 100);
    msg.AddMessage("repeated_nested", newMsg);
}

Managing Recipients

Control who receives the message:

using (var msg = new UserMessage("TextMsg"))
{
    // Send to all players
    msg.AddAllPlayers();

    // OR send to specific player
    msg.AddRecipient(playerSlot);

    // OR send to multiple specific players
    msg.AddRecipient(0);  // Player slot 0
    msg.AddRecipient(3);  // Player slot 3
    msg.AddRecipient(7);  // Player slot 7

    // OR use recipient mask (advanced)
    ulong mask = 0x1F;  // Binary mask for first 5 players
    msg.SetRecipientMask(mask);

    // Get current recipient mask
    ulong currentMask = msg.GetRecipientMask();
    PrintToServer($"Recipient mask: 0x{currentMask:X}\n");

    // Configure message fields...
    msg.AddString("param", "Hello!");

    // Send
    msg.Send();
}

Practical Examples

Example 1: Send Chat Message to All Players

With Classes
Without Classes
using Plugify;
using static s2sdk.s2sdk;

public unsafe class ChatMessage : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("say_all", "Send message to all players",
        ConVarFlag.LinkedConcommand | ConVarFlag.Release | ConVarFlag.ClientCanExecute,
        Command_SayAll, HookMode.Post);
    }

    public ResultType Command_SayAll(int caller, int context, string[] arguments)
    {
        if (arguments.Length < 2)
        {
            PrintToServer("Usage: say_all <message>\n");
            return ResultType.Handled;
        }

        string message = string.Join(" ", arguments, 1, arguments.Length - 1);
        SendChatMessage(message);

        return ResultType.Handled;
    }

    private void SendChatMessage(string text)
    {
        // Using statement ensures automatic cleanup
        using (var msg = new UserMessage("TextMsg"))
        {
            // Add all players as recipients
            msg.AddAllPlayers();

            // Set message fields
            msg.AddString("param", text);
            msg.SetUInt32("dest", 3);  // HUD_PRINTTALK (chat area)

            // Send
            msg.Send();

            PrintToServer($"Sent chat message: {text}\n");
        } // Automatically destroyed here
    }
}

Example 2: Send Hint to Specific Player

With Classes
Without Classes
using Plugify;
using static s2sdk.s2sdk;

public unsafe class HintSystem : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("hint", "Show hint text",
        ConVarFlag.LinkedConcommand | ConVarFlag.Release | ConVarFlag.ClientCanExecute,
        Command_Hint, HookMode.Post);
    }

    public ResultType Command_Hint(int caller, int context, string[] arguments)
    {
        if (caller == -1) return ResultType.Handled;

        if (arguments.Length < 2)
        {
            PrintToServer("Usage: hint <message>\n");
            return ResultType.Handled;
        }

        string hintText = string.Join(" ", arguments, 1, arguments.Length - 1);
        SendHintToPlayer(caller, hintText);

        return ResultType.Handled;
    }

    private void SendHintToPlayer(int playerSlot, string text)
    {
        using (var msg = new UserMessage("HintText"))
        {
            // Send only to this player
            msg.AddRecipient(playerSlot);

            // Set hint text
            msg.AddString("param", text);

            // Send
            msg.Send();
        } // Automatically destroyed
    }
}

Example 3: Screen Fade Effect

With Classes
Without Classes
using System.Numerics;
using Plugify;
using static s2sdk.s2sdk;

public unsafe class FadeEffect : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("fade", "Apply screen fade effect",
        ConVarFlag.LinkedConcommand | ConVarFlag.Release | ConVarFlag.ClientCanExecute,
        Command_Fade, HookMode.Post);
    }

    public ResultType Command_Fade(int caller, int context, string[] arguments)
    {
        if (caller == -1) return ResultType.Handled;

        // Fade to red
        FadeScreen(caller, 2000, 1000, 255, 0, 0, 128);

        return ResultType.Handled;
    }

    private void FadeScreen(int playerSlot, int duration, int holdTime,
                           int r, int g, int b, int alpha)
    {
        using (var msg = new UserMessage("Fade"))
        {
            // Send to specific player
            msg.AddRecipient(playerSlot);

            // Set fade parameters
            msg.SetInt32("duration", duration);  // Fade duration in ms
            msg.SetInt32("hold_time", holdTime); // Hold time in ms
            msg.SetInt32("flags", 0x0001);       // FFADE_IN

            // Set RGBA color
            int colorPacked = (r << 24) | (g << 16) | (b << 8) | alpha;
            msg.SetColor("clr", colorPacked);

            // Send
            msg.Send();

            PrintToServer("Screen fade applied\n");
        } // Automatically destroyed
    }
}

Example 4: Show Custom Menu

With Classes
Without Classes
using Plugify;
using static s2sdk.s2sdk;

public unsafe class MenuSystem : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("showmenu", "Display a menu",
        ConVarFlag.LinkedConcommand | ConVarFlag.Release | ConVarFlag.ClientCanExecute,
        Command_ShowMenu, HookMode.Post);
    }

    public ResultType Command_ShowMenu(int caller, int context, string[] arguments)
    {
        if (caller == -1) return ResultType.Handled;

        ShowGameMenu(caller);

        return ResultType.Handled;
    }

    private void ShowGameMenu(int playerSlot)
    {
        using (var msg = new UserMessage("ShowMenu"))
        {
            // Send to specific player
            msg.AddRecipient(playerSlot);

            // Set menu parameters
            msg.SetInt32("validslots", 0x1FF);  // Slots 1-9 valid
            msg.SetInt32("displaytime", 10);    // Display for 10 seconds
            msg.SetBool("needmore", false);

            // Menu text with options
            string menuText = "Choose an option:\n1. Option 1\n2. Option 2\n3. Option 3";
            msg.SetString("str", menuText);

            // Send
            msg.Send();

            PrintToServer("Menu displayed\n");
        } // Automatically destroyed
    }
}

Example 5: Hook and Modify Messages

With Classes
Without Classes
using Plugify;
using static s2sdk.s2sdk;

public unsafe class MessageFilter : Plugin
{
    public void OnPluginStart()
    {
        short msgId = UserMessage.FindMessageIdByName("TextMsg");
        HookUserMessage(msgId, OnTextMessagePre, HookMode.Pre);
    }

    public void OnPluginEnd()
    {
        short msgId = UserMessage.FindMessageIdByName("TextMsg");
        UnhookUserMessage(msgId, OnTextMessagePre, HookMode.Pre);
    }

    private static void OnTextMessagePre(nint userMessagePtr)
    {
        // Wrap the raw pointer in UserMessage class
        var userMessage = new UserMessage(userMessagePtr, ownsHandle: false);

        // Read original message
        string originalText = userMessage.GetString("param", -1);

        // Filter profanity
        string filtered = originalText.Replace("badword", "****");

        // Modify message
        if (filtered != originalText)
        {
            userMessage.AddString("param", filtered);
            PrintToServer("Filtered message content\n");
        }

        // Add prefix to all messages
        userMessage.AddString("param", "[Server] " + filtered);

        // Note: No need to destroy - we don't own the handle
    }
}

Example 6: Sending Data with Repeated Fields

With Classes
Without Classes
using Plugify;
using static s2sdk.s2sdk;

public unsafe class RepeatedFieldDemo : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("send_scores", "Send player scores",
        ConVarFlag.LinkedConcommand | ConVarFlag.Release | ConVarFlag.ClientCanExecute,
        Command_SendScores, HookMode.Post);
    }

    public ResultType Command_SendScores(int caller, int context, string[] arguments)
    {
        SendScoresList();
        return ResultType.Handled;
    }

    private void SendScoresList()
    {
        using (var msg = new UserMessage("CustomScoreMsg"))
        {
            // Add all players as recipients
            msg.AddAllPlayers();

            // Add multiple player names (repeated field)
            msg.AddString("player_names", "Player1");
            msg.AddString("player_names", "Player2");
            msg.AddString("player_names", "Player3");

            // Add corresponding scores (repeated field)
            msg.AddInt32("scores", 100);
            msg.AddInt32("scores", 85);
            msg.AddInt32("scores", 72);

            // Verify count
            int nameCount = msg.GetRepeatedFieldCount("player_names");
            int scoreCount = msg.GetRepeatedFieldCount("scores");

            PrintToServer($"Sending {nameCount} players with {scoreCount} scores\n");

            // Send
            msg.Send();
        } // Automatically destroyed
    }
}

Debugging User Messages

Inspect Message Contents

With Classes
Without Classes
using (var msg = new UserMessage("TextMsg"))
{
    msg.AddString("param", "Debug test");

    // Get debug representation
    string debugStr = msg.GetDebugString();
    PrintToServer($"Message debug: {debugStr}\n");
} // Automatically destroyed

Check Field Existence

With Classes
Without Classes
using (var msg = new UserMessage("TextMsg"))
{
    // Check if a field exists before accessing it
    if (msg.HasField("optional_field"))
    {
        int value = msg.GetInt32("optional_field", 0);
        PrintToServer($"Optional field value: {value}\n");
    }
} // Automatically destroyed

Tips and Best Practices

  1. Always destroy messages - Call UserMessageDestroy() after sending to prevent memory leaks
  2. Check message IDs - Verify UserMessageFindMessageIdByName() returns valid ID before using
  3. Set recipients first - Configure recipients before setting field values
  4. Use correct field names - Reference protobuf definitions for exact field names
  5. Handle hook modes properly - Use Pre to modify messages, Post to observe them
  6. Unhook in OnPluginEnd - Always clean up hooks when plugin unloads
  7. Validate repeated field counts - Check count before accessing repeated field indices
  8. Test with clients - Verify messages display correctly on client side
  9. Use appropriate data types - Match PbRead*/PbSet* functions to field types
  10. Consider performance - Avoid sending large messages or too many messages per tick

Common User Messages

Here are frequently used user messages in CS:GO/CS2:

TextMsg - Send text to chat or console:

  • text (string) - Message text
  • dest (uint32) - Destination: 2=console, 3=char, 4=center

HintText - Show hint message on screen:

  • text (string) - Hint text to display

Fade - Screen fade effect:

  • duration (int32) - Fade duration in milliseconds
  • hold_time (int32) - Hold time in milliseconds
  • flags (int32) - Fade flags (FFADE_IN, FFADE_OUT, etc.)
  • clr (color) - RGBA color

ShowMenu - Display a menu:

  • validslots (int32) - Bitmask of valid menu slots
  • displaytime (int32) - How long to display (seconds)
  • needmore (bool) - More pages available
  • str (string) - Menu text

Damage - Show damage indicator:

  • amount (int32) - Damage amount
  • victim (int32) - Victim entity index

SayText2 - Formatted chat message:

  • ent_idx (int32) - Entity index of sender
  • chat (bool) - Send to chat
  • msg_name (string) - Message format string
  • params (repeated string) - Message parameters

Refer to the CS:GO protobuf definitions for the complete list of messages and their fields.

Hook Modes

  • Pre (HookMode.Pre) - Hook before message is processed, allows modification
  • Post (HookMode.Post) - Hook after message is processed, read-only observation

Use Pre mode when you want to modify or block messages, Post mode for logging or triggering side effects.