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.
New: The UserMessage class provides automatic resource management and cleaner syntax for working with user messages. It ensures proper cleanup and provides intuitive methods!
Finding Message Definitions
You can find protobuf message definitions at:
- CS:GO Protobufs: SteamDatabase/Protobufs
- These
.protofiles 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:
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");
}
}
#include <plugify/cpp_plugin.hpp>
#include "s2sdk.hpp"
using namespace s2sdk;
class Sample : public plg::IPluginEntry {
private:
static ResultType OnTextMessage(void* userMessage) {
plg::string text = PbReadString(userMessage, "param", -1);
PrintToServer(std::format("TextMsg intercepted: {}\n", text.c_str()).c_str());
return ResultType.Continue;
}
public:
void OnPluginStart() override {
int16_t msgId = UserMessageFindMessageIdByName("TextMsg");
HookUserMessage(msgId, OnTextMessage, HookMode::Post);
}
void OnPluginEnd() override {
int16_t msgId = UserMessageFindMessageIdByName("TextMsg");
UnhookUserMessage(msgId, OnTextMessage, HookMode::Post);
}
};
from plugify.plugin import Plugin
from plugify.pps import s2sdk as s2
class Sample(Plugin):
def plugin_start(self):
msg_id = s2.UserMessageFindMessageIdByName("TextMsg")
s2.HookUserMessage(msg_id, self.on_text_message, s2.HookMode.Post)
def plugin_end(self):
msg_id = s2.UserMessageFindMessageIdByName("TextMsg")
s2.UnhookUserMessage(msg_id, self.on_text_message, s2.HookMode.Post)
def on_text_message(self, user_message):
text = s2.PbReadString(user_message, "param", -1)
s2.PrintToServer(f"TextMsg intercepted: {text}\n")
package main
import (
"fmt"
"s2sdk"
"github.com/untrustedmodders/go-plugify"
)
func onTextMessage(userMessage uintptr) {
text := s2sdk.PbReadString(userMessage, "param", -1)
s2sdk.PrintToServer(fmt.Sprintf("TextMsg intercepted: %s\n", text))
return s2sdk.ResultType.Continue;
}
func init() {
plugify.OnPluginStart(func() {
msgId := s2sdk.UserMessageFindMessageIdByName("TextMsg")
s2sdk.HookUserMessage(msgId, onTextMessage, s2sdk.HookMode.Post)
})
plugify.OnPluginEnd(func() {
msgId := s2sdk.UserMessageFindMessageIdByName("TextMsg")
s2sdk.UnhookUserMessage(msgId, onTextMessage, s2sdk.HookMode.Post)
})
}
import { Plugin } from 'plugify';
import * as s2 from ':s2sdk';
export class Sample extends Plugin {
pluginStart() {
const msgId = s2.UserMessageFindMessageIdByName("TextMsg");
s2.HookUserMessage(msgId, this.onTextMessage, s2.HookMode.Post);
}
pluginEnd() {
const msgId = s2.UserMessageFindMessageIdByName("TextMsg");
s2.UnhookUserMessage(msgId, this.onTextMessage, s2.HookMode.Post);
}
onTextMessage(userMessage) {
const text = s2.PbReadString(userMessage, "param", -1);
s2.PrintToServer(`TextMsg intercepted: ${text}\n`);
return s2.ResultType.Continue;
}
}
local plugify = require 'plugify'
local Plugin = plugify.Plugin
local s2 = require 's2sdk'
local Sample = {}
setmetatable(Sample, { __index = Plugin })
function Sample:on_text_message(user_message)
local text = s2:PbReadString(user_message, "param", -1)
s2:PrintToServer(string.format("TextMsg intercepted: %s\n", text))
return s2:ResultType.Continue
end
function Sample:plugin_start()
local msg_id = s2:UserMessageFindMessageIdByName("TextMsg")
s2:HookUserMessage(msg_id, function(msg) self:on_text_message(msg) end, s2.HookMode.Post)
end
function Sample:plugin_end()
local msg_id = s2:UserMessageFindMessageIdByName("TextMsg")
s2:UnhookUserMessage(msg_id, function(msg) self:on_text_message(msg) end, s2.HookMode.Post)
end
local M = {}
M.Sample = Sample
return M
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...
}
Create user messages using explicit functions:
// 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);
// Set recipients (choose one approach)
UserMessageAddAllPlayers(msg); // All players
// OR
UserMessageAddRecipient(msg, playerSlot); // Single player
// OR
UserMessageSetRecipientMask(msg, customMask); // Custom mask
// Set message fields
PbAddString(msg, "param", "Hello from server!");
PbSetUInt32(msg, "dest", 3); // HUD_PRINTTALK
// Send the message
UserMessageSend(msg);
// Clean up (IMPORTANT!)
UserMessageDestroy(msg);
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");
}
// 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
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");
}
// 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
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");
}
// Read
bool isActive = PbReadBool(msg, "active", 0);
// Write
PbSetBool(msg, "active", true);
Strings
using (var msg = new UserMessage("TextMsg"))
{
// Write string
msg.AddString("param", "Custom message");
// Read string (in hook callbacks)
string text = msg.GetString("param");
}
// Read
string text = PbReadString(msg, "param", -1);
// Write
PbAddString(msg, "param", "Custom message");
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");
}
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
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");
}
// Read enum value
int enumValue = PbReadEnum(msg, "type", 0);
// Write enum value
PbSetEnum(msg, "type", 2);
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;
}
// Read color (RGBA vector)
Vector4 color = PbReadColor(msg, "color", default);
// Write color
PbSetColor(msg, "color", new Vector4(1.0f, 0.0f, 0.0f, 1.0f)); // Red, full alpha
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
}
// 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:
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);
}
// 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", -1);
}
// Add nested message to repeated field
nint newMsg = IntPtr.Zero; // Create appropriately
UserMessageAddMessage(msg, "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();
}
// 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
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
}
}
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)
{
nint msg = UserMessageCreateFromName("TextMsg");
// Add all players as recipients
UserMessageAddAllPlayers(msg);
// Set message fields
PbAddString(msg, "param", text);
PbSetUInt32(msg, "dest", 3); // HUD_PRINTTALK (chat area)
// Send and cleanup
UserMessageSend(msg);
UserMessageDestroy(msg); // Don't forget this!
PrintToServer($"Sent chat message: {text}\n");
}
}
Example 2: Send Hint to Specific Player
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
}
}
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)
{
nint msg = UserMessageCreateFromName("HintText");
// Send only to this player
UserMessageAddRecipient(msg, playerSlot);
// Set hint text
PbAddString(msg, "param", text);
// Send and cleanup
UserMessageSend(msg);
UserMessageDestroy(msg);
}
}
Example 3: Screen Fade Effect
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
}
}
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)
{
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
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
}
}
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)
{
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
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
}
}
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, "param", -1);
// Filter profanity
string filtered = originalText.Replace("badword", "****");
// Modify message
if (filtered != originalText)
{
PbAddString(userMessage, "param", filtered);
PrintToServer("Filtered message content\n");
}
// Add prefix to all messages
PbAddString(userMessage, "param", "[Server] " + filtered);
}
}
Example 6: Sending Data with Repeated Fields
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
}
}
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()
{
// 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
Inspect Message Contents
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
nint msg = UserMessageCreateFromName("TextMsg");
PbAddString(msg, "param", "Debug test");
// Get debug representation
string debugStr = UserMessageGetDebugString(msg);
PrintToServer($"Message debug: {debugStr}\n");
UserMessageDestroy(msg);
Check Field Existence
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
nint msg = UserMessageCreateFromName("TextMsg");
// Check if a field exists before accessing it
if (UserMessageHasField(msg, "optional_field"))
{
int value = PbReadInt32(msg, "optional_field", 0);
PrintToServer($"Optional field value: {value}\n");
}
UserMessageDestroy(msg);
Tips and Best Practices
- Always destroy messages - Call
UserMessageDestroy()after sending to prevent memory leaks - Check message IDs - Verify
UserMessageFindMessageIdByName()returns valid ID before using - Set recipients first - Configure recipients before setting field values
- Use correct field names - Reference protobuf definitions for exact field names
- Handle hook modes properly - Use
Preto modify messages,Postto observe them - Unhook in OnPluginEnd - Always clean up hooks when plugin unloads
- Validate repeated field counts - Check count before accessing repeated field indices
- Test with clients - Verify messages display correctly on client side
- Use appropriate data types - Match
PbRead*/PbSet*functions to field types - 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 textdest(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 millisecondshold_time(int32) - Hold time in millisecondsflags(int32) - Fade flags (FFADE_IN, FFADE_OUT, etc.)clr(color) - RGBA color
ShowMenu - Display a menu:
validslots(int32) - Bitmask of valid menu slotsdisplaytime(int32) - How long to display (seconds)needmore(bool) - More pages availablestr(string) - Menu text
Damage - Show damage indicator:
amount(int32) - Damage amountvictim(int32) - Victim entity index
SayText2 - Formatted chat message:
ent_idx(int32) - Entity index of senderchat(bool) - Send to chatmsg_name(string) - Message format stringparams(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.
On This Page