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, "text", 0);
        PrintToServer($"TextMsg intercepted: {text}\n");
    }
}

Creating and Sending User Messages

There are three ways to create a user message:

// Method 1: From message name (most common)
nint msg = UserMessageCreateFromName("TextMsg");

// Method 2: From message ID
short msgId = UserMessageFindMessageIdByName("TextMsg");
nint msg = UserMessageCreateFromId(msgId);

// Method 3: From serializable (advanced)
nint msg = UserMessageCreateFromSerializable(msgSerializable, message, recipientMask);

Complete Send Workflow

// 1. Create the message
nint msg = UserMessageCreateFromName("TextMsg");

// 2. Set recipients (choose one approach)
UserMessageAddAllPlayers(msg);  // All players
// OR
UserMessageAddRecipient(msg, playerSlot);  // Single player
// OR
UserMessageSetRecipientMask(msg, customMask);  // Custom mask

// 3. Set message fields
PbSetString(msg, "text", "Hello from server!"); // if non repeated
PbSetInt32(msg, "msg_dst", 4);  // HUD_PRINTTALK

// 4. Send the message
UserMessageSend(msg);

// 5. Clean up
UserMessageDestroy(msg);

Reading and Writing Data Types

User messages support various data types through PbRead* and PbSet* functions:

Integers

// Read
int value = PbReadInt32(msg, "fieldName", 0);
long bigValue = PbReadInt64(msg, "fieldName", 0);
uint unsigned = PbReadUInt32(msg, "fieldName", 0);

// Write
PbSetInt32(msg, "fieldName", 42);
PbSetInt64(msg, "fieldName", 1000000L);
PbSetUInt32(msg, "fieldName", 99u);

Floats and Doubles

// Read
float speed = PbReadFloat(msg, "speed", 0);
double precision = PbReadDouble(msg, "value", 0);

// Write
PbSetFloat(msg, "speed", 1.5f);
PbSetDouble(msg, "value", 3.14159);

Booleans

// Read
bool isActive = PbReadBool(msg, "active", 0);

// Write
PbSetBool(msg, "active", true);

Strings

// Read
string text = PbReadString(msg, "text", 0);

// Write
PbSetString(msg, "text", "Custom message");

Vectors and Angles

using System.Numerics;

// Read 3D vector
Vector3 position = PbReadVector3(msg, "origin", 0);
Vector3 angles = PbReadQAngle(msg, "angles", 0);

// Write 3D vector
PbSetVector3(msg, "origin", new Vector3(100, 200, 300));
PbSetQAngle(msg, "angles", new Vector3(0, 90, 0));

// Read 2D vector
Vector2 pos2d = PbReadVector2(msg, "pos", 0);

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

Enums

// Read enum value
int enumValue = PbReadEnum(msg, "type", 0);

// Write enum value
PbSetEnum(msg, "type", 2);

Colors

// Read color (packed RGBA)
int color = PbReadColor(msg, "color", 0);

// Write color
PbSetColor(msg, "color", 0xFF0000FF);  // Red with full alpha

Working with Repeated Fields

Some protobuf fields are arrays (repeated fields):

// Get count of repeated field
int count = UserMessageGetRepeatedFieldCount(msg, "items");

// Read repeated values
for (int i = 0; i < count; i++)
{
    int value = PbReadInt32(msg, "items", i);  // Note: index is 3rd parameter
    PrintToServer($"Item[{i}] = {value}\n");
}

// Add values to repeated field
PbAddInt32(msg, "items", 10);
PbAddInt32(msg, "items", 20);
PbAddString(msg, "names", "Player1");

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

// Remove value from repeated field
UserMessageRemoveRepeatedFieldValue(msg, "items", 0);  // Remove index 0

Working with Nested Messages

Some messages contain nested message fields:

// Get nested message
nint nestedMsg = IntPtr.Zero;
if (UserMessageGetMessage(msg, "nested_field", nestedMsg))
{
    // Read from nested message
    int value = PbReadInt32(nestedMsg, "value", 0);
}

// Get repeated nested message
nint repeatedMsg = IntPtr.Zero;
if (UserMessageGetRepeatedMessage(msg, "repeated_nested", 0, repeatedMsg))
{
    // Read from repeated nested message
    string name = PbReadString(repeatedMsg, "name", 0);
}

// Add nested message to repeated field
nint newMsg = IntPtr.Zero;  // Create appropriately
UserMessageAddMessage(msg, "repeated_nested", newMsg);

Managing Recipients

Control who receives the message:

// Send to all players
UserMessageAddAllPlayers(msg);

// Send to specific player
UserMessageAddRecipient(msg, playerSlot);

// Send to multiple specific players
UserMessageAddRecipient(msg, 0);  // Player slot 0
UserMessageAddRecipient(msg, 3);  // Player slot 3
UserMessageAddRecipient(msg, 7);  // Player slot 7

// Use recipient mask (advanced)
ulong mask = 0x1F;  // Binary mask for first 5 players
UserMessageSetRecipientMask(msg, mask);

// Get current recipient mask
ulong currentMask = UserMessageGetRecipientMask(msg);

Practical Examples

Example 1: Send Chat Message to All Players

c#
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,
        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)
    {
        nint msg = UserMessageCreateFromName("TextMsg");
        
        // Add all players as recipients
        UserMessageAddAllPlayers(msg);
        
        // Set message fields
        PbSetString(msg, "text", text);
        PbSetInt32(msg, "msg_dst", 4);  // HUD_PRINTTALK (chat area)
        
        // Send and cleanup
        UserMessageSend(msg);
        UserMessageDestroy(msg);
        
        PrintToServer($"Sent chat message: {text}\n");
    }
}

Example 2: Send Hint to Specific Player

c#
using Plugify;
using static s2sdk.s2sdk;

public unsafe class HintSystem : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("hint", "Show hint text",
        ConVarFlag.LinkedConcommand | ConVarFlag.Release,
        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)
    {
        nint msg = UserMessageCreateFromName("HintText");
        
        // Send only to this player
        UserMessageAddRecipient(msg, playerSlot);
        
        // Set hint text
        PbSetString(msg, "text", text);
        
        // Send and cleanup
        UserMessageSend(msg);
        UserMessageDestroy(msg);
    }
}

Example 3: Screen Fade Effect

c#
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,
        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)
    {
        nint msg = UserMessageCreateFromName("Fade");
        
        // Send to specific player
        UserMessageAddRecipient(msg, playerSlot);
        
        // Set fade parameters
        PbSetInt32(msg, "duration", duration);  // Fade duration in ms
        PbSetInt32(msg, "hold_time", holdTime); // Hold time in ms
        PbSetInt32(msg, "flags", 0x0001);       // FFADE_IN
        
        // Set RGBA color
        int colorPacked = (r << 24) | (g << 16) | (b << 8) | alpha;
        PbSetColor(msg, "clr", colorPacked);
        
        // Send and cleanup
        UserMessageSend(msg);
        UserMessageDestroy(msg);
        
        PrintToServer("Screen fade applied\n");
    }
}

Example 4: Show Custom Menu

c#
using Plugify;
using static s2sdk.s2sdk;

public unsafe class MenuSystem : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("showmenu", "Display a menu",
        ConVarFlag.LinkedConcommand | ConVarFlag.Release,
        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)
    {
        nint msg = UserMessageCreateFromName("ShowMenu");
        
        // Send to specific player
        UserMessageAddRecipient(msg, playerSlot);
        
        // Set menu parameters
        PbSetInt32(msg, "validslots", 0x1FF);  // Slots 1-9 valid
        PbSetInt32(msg, "displaytime", 10);    // Display for 10 seconds
        PbSetBool(msg, "needmore", false);
        
        // Menu text with options
        string menuText = "Choose an option:\n1. Option 1\n2. Option 2\n3. Option 3";
        PbSetString(msg, "str", menuText);
        
        // Send and cleanup
        UserMessageSend(msg);
        UserMessageDestroy(msg);
        
        PrintToServer("Menu displayed\n");
    }
}

Example 5: Hook and Modify Messages

c#
using Plugify;
using static s2sdk.s2sdk;

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

    public void OnPluginEnd()
    {
        short msgId = UserMessageFindMessageIdByName("TextMsg");
        UnhookUserMessage(msgId, OnTextMessagePre, HookMode.Pre);
    }
    
    private static void OnTextMessagePre(nint userMessage)
    {
        // Read original message
        string originalText = PbReadString(userMessage, "text", 0);
        
        // Filter profanity
        string filtered = originalText.Replace("badword", "****");
        
        // Modify message
        if (filtered != originalText)
        {
            PbSetString(userMessage, "text", filtered);
            PrintToServer("Filtered message content\n");
        }
        
        // Add prefix to all messages
        PbSetString(userMessage, "text", "[Server] " + filtered);
    }
}

Example 6: Sending Data with Repeated Fields

c#
using Plugify;
using static s2sdk.s2sdk;

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

    public ResultType Command_SendScores(int caller, int context, string[] arguments)
    {
        SendScoresList();
        return ResultType.Handled;
    }
    
    private void SendScoresList()
    {
        // Example: Create a custom message with repeated fields
        nint msg = UserMessageCreateFromName("CustomScoreMsg");
        
        // Add all players as recipients
        UserMessageAddAllPlayers(msg);
        
        // Add multiple player names (repeated field)
        PbAddString(msg, "player_names", "Player1");
        PbAddString(msg, "player_names", "Player2");
        PbAddString(msg, "player_names", "Player3");
        
        // Add corresponding scores (repeated field)
        PbAddInt32(msg, "scores", 100);
        PbAddInt32(msg, "scores", 85);
        PbAddInt32(msg, "scores", 72);
        
        // Verify count
        int nameCount = UserMessageGetRepeatedFieldCount(msg, "player_names");
        int scoreCount = UserMessageGetRepeatedFieldCount(msg, "scores");
        
        PrintToServer($"Sending {nameCount} players with {scoreCount} scores\n");
        
        // Send and cleanup
        UserMessageSend(msg);
        UserMessageDestroy(msg);
    }
}

Debugging User Messages

Use the debug string function to inspect message contents:

nint msg = UserMessageCreateFromName("TextMsg");
PbSetString(msg, "text", "Debug test");

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

UserMessageDestroy(msg);

Check if a field exists before accessing it:

if (UserMessageHasField(msg, "optional_field"))
{
    int value = PbReadInt32(msg, "optional_field", 0);
}

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
  • msg_dst (int32) - Destination: 2=console, 3=center, 4=chat

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.