Пользовательские сообщения
Как отправлять и получать пользовательские сообщения Protobuf между сервером и клиентами.
Понимание User Messages
User messages - это основной метод отправки пользовательских данных с сервера клиентам в Source 2. Они используют Protocol Buffers (protobuf) для сериализации, обеспечивая структурированный и эффективный способ передачи игровой информации.
Типичные случаи использования включают:
- Отображение HUD-элементов и уведомлений
- Воспроизведение звуков или визуальных эффектов на клиентах
- Отправка пользовательской информации о состоянии игры
- Создание чат-сообщений и подсказок
- Запуск событий на стороне клиента
User messages работают аналогично реализации SourceMod, где protobuf заменяет старую систему bitbuffer для лучшей типобезопасности и гибкости.
Новое: Класс UserMessage обеспечивает автоматическое управление ресурсами и более чистый синтаксис для работы с user messages. Он гарантирует правильную очистку и предоставляет интуитивные методы!
Поиск определений сообщений
Вы можете найти определения protobuf-сообщений здесь:
- CS:GO Protobufs: SteamDatabase/Protobufs
- Эти
.protoфайлы определяют доступные сообщения и их поля - Часто используемые сообщения включают
TextMsg,HintText,ShowMenu,Fadeи многие другие
Каждое определение сообщения показывает имя сообщения, ID и доступные поля с их типами.
Перехват User Messages
Вы можете перехватывать user messages до того, как они будут отправлены клиентам:
using Plugify;
using static s2sdk.s2sdk;
public unsafe class Sample : Plugin
{
public void OnPluginStart()
{
// Найти ID сообщения по имени
short msgId = UserMessageFindMessageIdByName("TextMsg");
// Перехватить сообщение в режиме Post
HookUserMessage(msgId, OnTextMessage, HookMode.Post);
}
public void OnPluginEnd()
{
short msgId = UserMessageFindMessageIdByName("TextMsg");
UnhookUserMessage(msgId, OnTextMessage, HookMode.Post);
}
private static void OnTextMessage(nint userMessage)
{
// Прочитать данные сообщения
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
Создание и отправка User Messages
Создавайте user messages используя класс UserMessage с автоматической очисткой:
// Создать из имени сообщения (наиболее распространенный способ)
using (var msg = new UserMessage("TextMsg"))
{
// Установить получателей
msg.AddAllPlayers();
// Установить поля сообщения
msg.AddString("param", "Hello from server!");
msg.SetUInt32("dest", 3); // HUD_PRINTTALK
// Отправить сообщение
msg.Send();
} // Автоматически уничтожается
// Или создать из ID сообщения
short msgId = UserMessage.FindMessageIdByName("TextMsg");
using (var msg = new UserMessage(msgId))
{
// Настроить и отправить...
}
Создавайте user messages используя явные функции:
// Метод 1: Из имени сообщения (наиболее распространенный)
nint msg = UserMessageCreateFromName("TextMsg");
// Метод 2: Из ID сообщения
short msgId = UserMessageFindMessageIdByName("TextMsg");
nint msg = UserMessageCreateFromId(msgId);
// Метод 3: Из serializable (продвинутый)
nint msg = UserMessageCreateFromSerializable(msgSerializable, message, recipientMask);
// Установить получателей (выберите один подход)
UserMessageAddAllPlayers(msg); // Все игроки
// ИЛИ
UserMessageAddRecipient(msg, playerSlot); // Один игрок
// ИЛИ
UserMessageSetRecipientMask(msg, customMask); // Пользовательская маска
// Установить поля сообщения
PbAddString(msg, "param", "Hello from server!");
PbSetUInt32(msg, "dest", 3); // HUD_PRINTTALK
// Отправить сообщение
UserMessageSend(msg);
// Очистить (ВАЖНО!)
UserMessageDestroy(msg);
Чтение и запись типов данных
User messages поддерживают различные типы данных. Вы можете использовать либо методы класса UserMessage (рекомендуется), либо функции PbRead* и PbSet*:
Целые числа
using (var msg = new UserMessage("TextMsg"))
{
// Записать целые числа
msg.SetInt32("fieldName", 42);
msg.SetInt64("bigField", 1000000L);
msg.SetUInt32("unsignedField", 99u);
// Прочитать целые числа (в callback-перехватчиках)
int value = msg.GetInt32("fieldName");
long bigValue = msg.GetInt64("bigField");
uint unsigned = msg.GetUInt32("unsignedField");
}
// Чтение
int value = PbReadInt32(msg, "fieldName", 0);
long bigValue = PbReadInt64(msg, "fieldName", 0);
uint unsigned = PbReadUInt32(msg, "fieldName", 0);
// Запись
PbSetInt32(msg, "fieldName", 42);
PbSetInt64(msg, "fieldName", 1000000L);
PbSetUInt32(msg, "fieldName", 99u);
Числа с плавающей точкой
using (var msg = new UserMessage("CustomMsg"))
{
// Записать значения с плавающей точкой
msg.SetFloat("speed", 1.5f);
msg.SetDouble("precision", 3.14159);
// Прочитать значения с плавающей точкой (в callback-перехватчиках)
float speed = msg.GetFloat("speed");
double precision = msg.GetDouble("precision");
}
// Чтение
float speed = PbReadFloat(msg, "speed", 0);
double precision = PbReadDouble(msg, "value", 0);
// Запись
PbSetFloat(msg, "speed", 1.5f);
PbSetDouble(msg, "value", 3.14159);
Булевы значения
using (var msg = new UserMessage("ShowMenu"))
{
// Записать булево значение
msg.SetBool("needmore", true);
msg.SetBool("active", false);
// Прочитать булево значение (в callback-перехватчиках)
bool isActive = msg.GetBool("active");
}
// Чтение
bool isActive = PbReadBool(msg, "active", 0);
// Запись
PbSetBool(msg, "active", true);
Строки
using (var msg = new UserMessage("TextMsg"))
{
// Записать строку
msg.AddString("param", "Custom message");
// Прочитать строку (в callback-перехватчиках)
string text = msg.GetString("param");
}
// Чтение
string text = PbReadString(msg, "param", -1);
// Запись
PbAddString(msg, "param", "Custom message");
Векторы и углы
using System.Numerics;
using (var msg = new UserMessage("CustomPositionMsg"))
{
// Записать 3D векторы
msg.SetVector3("origin", new Vector3(100, 200, 300));
msg.SetQAngle("angles", new Vector3(0, 90, 0));
// Записать 2D вектор
msg.SetVector2("pos", new Vector2(50, 100));
// Прочитать векторы (в callback-перехватчиках)
Vector3 position = msg.GetVector3("origin");
Vector3 angles = msg.GetQAngle("angles");
Vector2 pos2d = msg.GetVector2("pos");
}
using System.Numerics;
// Прочитать 3D вектор
Vector3 position = PbReadVector3(msg, "origin", 0);
Vector3 angles = PbReadQAngle(msg, "angles", 0);
// Записать 3D вектор
PbSetVector3(msg, "origin", new Vector3(100, 200, 300));
PbSetQAngle(msg, "angles", new Vector3(0, 90, 0));
// Прочитать 2D вектор
Vector2 pos2d = PbReadVector2(msg, "pos", 0);
// Записать 2D вектор
PbSetVector2(msg, "pos", new Vector2(50, 100));
Перечисления
using (var msg = new UserMessage("CustomMsg"))
{
// Записать значение enum
msg.SetEnum("type", 2);
msg.SetEnum("state", (int)GameState.Running);
// Прочитать значение enum (в callback-перехватчиках)
int enumValue = msg.GetEnum("type");
GameState state = (GameState)msg.GetEnum("state");
}
// Прочитать значение enum
int enumValue = PbReadEnum(msg, "type", 0);
// Записать значение enum
PbSetEnum(msg, "type", 2);
Цвета
using (var msg = new UserMessage("Fade"))
{
// Записать цвет (упакованный RGBA)
msg.SetColor("clr", new Vector4(1.0f, 0.0f, 0.0f, 1.0f)); // Красный с полной альфой
// Создать цвет из компонентов
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);
// Прочитать цвет (в callback-перехватчиках)
Vector4 clr = msg.GetColor("clr");
// Извлечь компоненты
float red = clr.X;
float green = clr.Y;
float blue = clr.Z;
float alpha = clr.W;
}
// Прочитать цвет (упакованный RGBA)
Vector4 color = PbReadColor(msg, "color", default);
// Записать цвет
PbSetColor(msg, "color", new Vector4(1.0f, 0.0f, 0.0f, 1.0f)); // Красный с полной альфой
Работа с повторяющимися полями
Некоторые protobuf-поля являются массивами (repeated fields):
using (var msg = new UserMessage("CustomMsg"))
{
// Добавить значения в повторяющееся поле
msg.AddInt32("items", 10);
msg.AddInt32("items", 20);
msg.AddInt32("items", 30);
msg.AddString("names", "Player1");
msg.AddString("names", "Player2");
// Получить количество элементов в повторяющемся поле
int count = msg.GetRepeatedFieldCount("items");
PrintToServer($"Total items: {count}\n");
// Прочитать повторяющиеся значения
for (int i = 0; i < count; i++)
{
int value = msg.GetInt32("items", i);
PrintToServer($"Item[{i}] = {value}\n");
}
// Установить значение по конкретному индексу в повторяющемся поле
msg.SetRepeatedInt32("items", 0, 999); // Установить индекс 0 в 999
// Удалить значение из повторяющегося поля
msg.RemoveRepeatedFieldValue("items", 0); // Удалить индекс 0
}
// Получить количество элементов в повторяющемся поле
int count = UserMessageGetRepeatedFieldCount(msg, "items");
// Прочитать повторяющиеся значения
for (int i = 0; i < count; i++)
{
int value = PbReadInt32(msg, "items", i); // Примечание: индекс - третий параметр
PrintToServer($"Item[{i}] = {value}\n");
}
// Добавить значения в повторяющееся поле
PbAddInt32(msg, "items", 10);
PbAddInt32(msg, "items", 20);
PbAddString(msg, "names", "Player1");
// Установить значение по конкретному индексу в повторяющемся поле
PbSetRepeatedInt32(msg, "items", 0, 999); // Установить индекс 0 в 999
// Удалить значение из повторяющегося поля
UserMessageRemoveRepeatedFieldValue(msg, "items", 0); // Удалить индекс 0
Работа с вложенными сообщениями
Некоторые сообщения содержат вложенные поля сообщений:
using (var msg = new UserMessage("ComplexMsg"))
{
// Получить вложенное сообщение
var nestedMsg = msg.GetMessage("nested_field");
if (nestedMsg != null)
{
// Прочитать из вложенного сообщения
int value = nestedMsg.GetInt32("value", 0);
string name = nestedMsg.GetString("name", 0);
PrintToServer($"Nested: {name} = {value}\n");
}
// Получить повторяющееся вложенное сообщение
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");
}
}
// Добавить вложенное сообщение в повторяющееся поле
var newMsg = new UserMessage("PlayerInfo");
newMsg.SetString("name", "NewPlayer");
newMsg.SetInt32("score", 100);
msg.AddMessage("repeated_nested", newMsg);
}
// Получить вложенное сообщение
nint nestedMsg = IntPtr.Zero;
if (UserMessageGetMessage(msg, "nested_field", nestedMsg))
{
// Прочитать из вложенного сообщения
int value = PbReadInt32(nestedMsg, "value", 0);
}
// Получить повторяющееся вложенное сообщение
nint repeatedMsg = IntPtr.Zero;
if (UserMessageGetRepeatedMessage(msg, "repeated_nested", 0, repeatedMsg))
{
// Прочитать из повторяющегося вложенного сообщения
string name = PbReadString(repeatedMsg, "name", -1);
}
// Добавить вложенное сообщение в повторяющееся поле
nint newMsg = IntPtr.Zero; // Создать соответствующим образом
UserMessageAddMessage(msg, "repeated_nested", newMsg);
Управление получателями
Контролируйте, кто получает сообщение:
using (var msg = new UserMessage("TextMsg"))
{
// Отправить всем игрокам
msg.AddAllPlayers();
// ИЛИ отправить конкретному игроку
msg.AddRecipient(playerSlot);
// ИЛИ отправить нескольким конкретным игрокам
msg.AddRecipient(0); // Слот игрока 0
msg.AddRecipient(3); // Слот игрока 3
msg.AddRecipient(7); // Слот игрока 7
// ИЛИ использовать маску получателей (продвинутый)
ulong mask = 0x1F; // Бинарная маска для первых 5 игроков
msg.SetRecipientMask(mask);
// Получить текущую маску получателей
ulong currentMask = msg.GetRecipientMask();
PrintToServer($"Recipient mask: 0x{currentMask:X}\n");
// Настроить поля сообщения...
msg.AddString("param", "Hello!");
// Отправить
msg.Send();
}
// Отправить всем игрокам
UserMessageAddAllPlayers(msg);
// Отправить конкретному игроку
UserMessageAddRecipient(msg, playerSlot);
// Отправить нескольким конкретным игрокам
UserMessageAddRecipient(msg, 0); // Слот игрока 0
UserMessageAddRecipient(msg, 3); // Слот игрока 3
UserMessageAddRecipient(msg, 7); // Слот игрока 7
// Использовать маску получателей (продвинутый)
ulong mask = 0x1F; // Бинарная маска для первых 5 игроков
UserMessageSetRecipientMask(msg, mask);
// Получить текущую маску получателей
ulong currentMask = UserMessageGetRecipientMask(msg);
Практические примеры
Пример 1: Отправка чат-сообщения всем игрокам
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 обеспечивает автоматическую очистку
using (var msg = new UserMessage("TextMsg"))
{
// Добавить всех игроков в качестве получателей
msg.AddAllPlayers();
// Установить поля сообщения
msg.AddString("param", text);
msg.SetUInt32("dest", 3); // HUD_PRINTTALK (область чата)
// Отправить
msg.Send();
PrintToServer($"Sent chat message: {text}\n");
} // Автоматически уничтожается здесь
}
}
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");
// Добавить всех игроков в качестве получателей
UserMessageAddAllPlayers(msg);
// Установить поля сообщения
PbAddString(msg, "param", text);
PbSetUInt32(msg, "dest", 3); // HUD_PRINTTALK (область чата)
// Отправить и очистить
UserMessageSend(msg);
UserMessageDestroy(msg); // Не забудьте это!
PrintToServer($"Sent chat message: {text}\n");
}
}
Пример 2: Отправка подсказки конкретному игроку
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"))
{
// Отправить только этому игроку
msg.AddRecipient(playerSlot);
// Установить текст подсказки
msg.AddString("param", text);
// Отправить
msg.Send();
} // Автоматически уничтожается
}
}
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");
// Отправить только этому игроку
UserMessageAddRecipient(msg, playerSlot);
// Установить текст подсказки
PbAddString(msg, "param", text);
// Отправить и очистить
UserMessageSend(msg);
UserMessageDestroy(msg);
}
}
Пример 3: Эффект затемнения экрана
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;
// Затемнение в красный
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"))
{
// Отправить конкретному игроку
msg.AddRecipient(playerSlot);
// Установить параметры затемнения
msg.SetInt32("duration", duration); // Длительность затемнения в мс
msg.SetInt32("hold_time", holdTime); // Время удержания в мс
msg.SetInt32("flags", 0x0001); // FFADE_IN
// Установить цвет RGBA
int colorPacked = (r << 24) | (g << 16) | (b << 8) | alpha;
msg.SetColor("clr", colorPacked);
// Отправить
msg.Send();
PrintToServer("Screen fade applied\n");
} // Автоматически уничтожается
}
}
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;
// Затемнение в красный
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");
// Отправить конкретному игроку
UserMessageAddRecipient(msg, playerSlot);
// Установить параметры затемнения
PbSetInt32(msg, "duration", duration); // Длительность затемнения в мс
PbSetInt32(msg, "hold_time", holdTime); // Время удержания в мс
PbSetInt32(msg, "flags", 0x0001); // FFADE_IN
// Установить цвет RGBA
int colorPacked = (r << 24) | (g << 16) | (b << 8) | alpha;
PbSetColor(msg, "clr", colorPacked);
// Отправить и очистить
UserMessageSend(msg);
UserMessageDestroy(msg);
PrintToServer("Screen fade applied\n");
}
}
Пример 4: Показать пользовательское меню
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"))
{
// Отправить конкретному игроку
msg.AddRecipient(playerSlot);
// Установить параметры меню
msg.SetInt32("validslots", 0x1FF); // Слоты 1-9 действительны
msg.SetInt32("displaytime", 10); // Отображать 10 секунд
msg.SetBool("needmore", false);
// Текст меню с опциями
string menuText = "Choose an option:\n1. Option 1\n2. Option 2\n3. Option 3";
msg.SetString("str", menuText);
// Отправить
msg.Send();
PrintToServer("Menu displayed\n");
} // Автоматически уничтожается
}
}
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");
// Отправить конкретному игроку
UserMessageAddRecipient(msg, playerSlot);
// Установить параметры меню
PbSetInt32(msg, "validslots", 0x1FF); // Слоты 1-9 действительны
PbSetInt32(msg, "displaytime", 10); // Отображать 10 секунд
PbSetBool(msg, "needmore", false);
// Текст меню с опциями
string menuText = "Choose an option:\n1. Option 1\n2. Option 2\n3. Option 3";
PbSetString(msg, "str", menuText);
// Отправить и очистить
UserMessageSend(msg);
UserMessageDestroy(msg);
PrintToServer("Menu displayed\n");
}
}
Пример 5: Перехват и изменение сообщений
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)
{
// Обернуть сырой указатель в класс UserMessage
var userMessage = new UserMessage(userMessagePtr, ownsHandle: false);
// Прочитать оригинальное сообщение
string originalText = userMessage.GetString("param", -1);
// Фильтровать ненормативную лексику
string filtered = originalText.Replace("badword", "****");
// Изменить сообщение
if (filtered != originalText)
{
userMessage.AddString("param", filtered);
PrintToServer("Filtered message content\n");
}
// Добавить префикс ко всем сообщениям
userMessage.AddString("param", "[Server] " + filtered);
// Примечание: Не нужно уничтожать - мы не владеем 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)
{
// Прочитать оригинальное сообщение
string originalText = PbReadString(userMessage, "param", -1);
// Фильтровать ненормативную лексику
string filtered = originalText.Replace("badword", "****");
// Изменить сообщение
if (filtered != originalText)
{
PbAddString(userMessage, "param", filtered);
PrintToServer("Filtered message content\n");
}
// Добавить префикс ко всем сообщениям
PbAddString(userMessage, "param", "[Server] " + filtered);
}
}
Пример 6: Отправка данных с повторяющимися полями
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"))
{
// Добавить всех игроков в качестве получателей
msg.AddAllPlayers();
// Добавить несколько имен игроков (повторяющееся поле)
msg.AddString("player_names", "Player1");
msg.AddString("player_names", "Player2");
msg.AddString("player_names", "Player3");
// Добавить соответствующие счета (повторяющееся поле)
msg.AddInt32("scores", 100);
msg.AddInt32("scores", 85);
msg.AddInt32("scores", 72);
// Проверить количество
int nameCount = msg.GetRepeatedFieldCount("player_names");
int scoreCount = msg.GetRepeatedFieldCount("scores");
PrintToServer($"Sending {nameCount} players with {scoreCount} scores\n");
// Отправить
msg.Send();
} // Автоматически уничтожается
}
}
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()
{
// Пример: Создать пользовательское сообщение с повторяющимися полями
nint msg = UserMessageCreateFromName("CustomScoreMsg");
// Добавить всех игроков в качестве получателей
UserMessageAddAllPlayers(msg);
// Добавить несколько имен игроков (повторяющееся поле)
PbAddString(msg, "player_names", "Player1");
PbAddString(msg, "player_names", "Player2");
PbAddString(msg, "player_names", "Player3");
// Добавить соответствующие счета (повторяющееся поле)
PbAddInt32(msg, "scores", 100);
PbAddInt32(msg, "scores", 85);
PbAddInt32(msg, "scores", 72);
// Проверить количество
int nameCount = UserMessageGetRepeatedFieldCount(msg, "player_names");
int scoreCount = UserMessageGetRepeatedFieldCount(msg, "scores");
PrintToServer($"Sending {nameCount} players with {scoreCount} scores\n");
// Отправить и очистить
UserMessageSend(msg);
UserMessageDestroy(msg);
}
}
Отладка User Messages
Инспектировать содержимое сообщения
using (var msg = new UserMessage("TextMsg"))
{
msg.AddString("param", "Debug test");
// Получить отладочное представление
string debugStr = msg.GetDebugString();
PrintToServer($"Message debug: {debugStr}\n");
} // Автоматически уничтожается
nint msg = UserMessageCreateFromName("TextMsg");
PbAddString(msg, "param", "Debug test");
// Получить отладочное представление
string debugStr = UserMessageGetDebugString(msg);
PrintToServer($"Message debug: {debugStr}\n");
UserMessageDestroy(msg);
Проверка существования поля
using (var msg = new UserMessage("TextMsg"))
{
// Проверить существование поля перед доступом к нему
if (msg.HasField("optional_field"))
{
int value = msg.GetInt32("optional_field", 0);
PrintToServer($"Optional field value: {value}\n");
}
} // Автоматически уничтожается
nint msg = UserMessageCreateFromName("TextMsg");
// Проверить существование поля перед доступом к нему
if (UserMessageHasField(msg, "optional_field"))
{
int value = PbReadInt32(msg, "optional_field", 0);
PrintToServer($"Optional field value: {value}\n");
}
UserMessageDestroy(msg);
Советы и лучшие практики
- Всегда уничтожайте сообщения - Вызывайте
UserMessageDestroy()после отправки, чтобы предотвратить утечки памяти - Проверяйте ID сообщений - Убедитесь, что
UserMessageFindMessageIdByName()возвращает действительный ID перед использованием - Сначала устанавливайте получателей - Настраивайте получателей перед установкой значений полей
- Используйте правильные имена полей - Обращайтесь к определениям protobuf для точных имен полей
- Правильно обрабатывайте режимы перехвата - Используйте
Preдля изменения сообщений,Postдля наблюдения за ними - Отменяйте перехваты в OnPluginEnd - Всегда очищайте перехваты при выгрузке плагина
- Проверяйте количество повторяющихся полей - Проверяйте количество перед доступом к индексам повторяющихся полей
- Тестируйте с клиентами - Убедитесь, что сообщения правильно отображаются на стороне клиента
- Используйте соответствующие типы данных - Сопоставляйте функции
PbRead*/PbSet*с типами полей - Учитывайте производительность - Избегайте отправки больших сообщений или слишком большого количества сообщений за тик
Часто используемые User Messages
Вот часто используемые user messages в CS:GO/CS2:
TextMsg - Отправить текст в чат или консоль:
param(string) - Текст сообщенияdest(uint32) - Назначение: 2=консоль, 3=чат, 4=центр
HintText - Показать подсказку на экране:
text(string) - Текст подсказки для отображения
Fade - Эффект затемнения экрана:
duration(int32) - Длительность затемнения в миллисекундахhold_time(int32) - Время удержания в миллисекундахflags(int32) - Флаги затемнения (FFADE_IN, FFADE_OUT и т.д.)clr(color) - Цвет RGBA
ShowMenu - Отобразить меню:
validslots(int32) - Битовая маска допустимых слотов менюdisplaytime(int32) - Как долго отображать (секунды)needmore(bool) - Доступны дополнительные страницыstr(string) - Текст меню
Damage - Показать индикатор урона:
amount(int32) - Количество уронаvictim(int32) - Индекс сущности жертвы
SayText2 - Форматированное чат-сообщение:
ent_idx(int32) - Индекс сущности отправителяchat(bool) - Отправить в чатmsg_name(string) - Строка формата сообщенияparams(repeated string) - Параметры сообщения
Обратитесь к определениям protobuf CS:GO для полного списка сообщений и их полей.
Режимы перехвата
- Pre (
HookMode.Pre) - Перехват до обработки сообщения, позволяет модификацию - Post (
HookMode.Post) - Перехват после обработки сообщения, только чтение для наблюдения
Используйте режим Pre, когда хотите изменить или заблокировать сообщения, режим Post для логирования или запуска побочных эффектов.
На этой странице