Пользовательские сообщения

Как отправлять и получать пользовательские сообщения Protobuf между сервером и клиентами.

Понимание User Messages

User messages - это основной метод отправки пользовательских данных с сервера клиентам в Source 2. Они используют Protocol Buffers (protobuf) для сериализации, обеспечивая структурированный и эффективный способ передачи игровой информации.

Типичные случаи использования включают:

  • Отображение HUD-элементов и уведомлений
  • Воспроизведение звуков или визуальных эффектов на клиентах
  • Отправка пользовательской информации о состоянии игры
  • Создание чат-сообщений и подсказок
  • Запуск событий на стороне клиента

User messages работают аналогично реализации SourceMod, где protobuf заменяет старую систему bitbuffer для лучшей типобезопасности и гибкости.

Поиск определений сообщений

Вы можете найти определения protobuf-сообщений здесь:

  • CS:GO Protobufs: SteamDatabase/Protobufs
  • Эти .proto файлы определяют доступные сообщения и их поля
  • Часто используемые сообщения включают TextMsg, HintText, ShowMenu, Fade и многие другие

Каждое определение сообщения показывает имя сообщения, ID и доступные поля с их типами.

Перехват User Messages

Вы можете перехватывать user messages до того, как они будут отправлены клиентам:

c#
c++
python
go
js
lua
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");
    }
}

Создание и отправка 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 поддерживают различные типы данных. Вы можете использовать либо методы класса 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");
}

Числа с плавающей точкой

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");
}

Булевы значения

using (var msg = new UserMessage("ShowMenu"))
{
    // Записать булево значение
    msg.SetBool("needmore", true);
    msg.SetBool("active", false);

    // Прочитать булево значение (в callback-перехватчиках)
    bool isActive = msg.GetBool("active");
}

Строки

using (var msg = new UserMessage("TextMsg"))
{
    // Записать строку
    msg.AddString("param", "Custom message");

    // Прочитать строку (в callback-перехватчиках)
    string text = msg.GetString("param");
}

Векторы и углы

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 (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");
}

Цвета

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;
}

Работа с повторяющимися полями

Некоторые 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
}

Работа с вложенными сообщениями

Некоторые сообщения содержат вложенные поля сообщений:

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);
}

Управление получателями

Контролируйте, кто получает сообщение:

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();
}

Практические примеры

Пример 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");
        } // Автоматически уничтожается здесь
    }
}

Пример 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();
        } // Автоматически уничтожается
    }
}

Пример 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");
        } // Автоматически уничтожается
    }
}

Пример 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");
        } // Автоматически уничтожается
    }
}

Пример 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
    }
}

Пример 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();
        } // Автоматически уничтожается
    }
}

Отладка User Messages

Инспектировать содержимое сообщения

С классами
Без классов
using (var msg = new UserMessage("TextMsg"))
{
    msg.AddString("param", "Debug test");

    // Получить отладочное представление
    string debugStr = msg.GetDebugString();
    PrintToServer($"Message debug: {debugStr}\n");
} // Автоматически уничтожается

Проверка существования поля

С классами
Без классов
using (var msg = new UserMessage("TextMsg"))
{
    // Проверить существование поля перед доступом к нему
    if (msg.HasField("optional_field"))
    {
        int value = msg.GetInt32("optional_field", 0);
        PrintToServer($"Optional field value: {value}\n");
    }
} // Автоматически уничтожается

Советы и лучшие практики

  1. Всегда уничтожайте сообщения - Вызывайте UserMessageDestroy() после отправки, чтобы предотвратить утечки памяти
  2. Проверяйте ID сообщений - Убедитесь, что UserMessageFindMessageIdByName() возвращает действительный ID перед использованием
  3. Сначала устанавливайте получателей - Настраивайте получателей перед установкой значений полей
  4. Используйте правильные имена полей - Обращайтесь к определениям protobuf для точных имен полей
  5. Правильно обрабатывайте режимы перехвата - Используйте Pre для изменения сообщений, Post для наблюдения за ними
  6. Отменяйте перехваты в OnPluginEnd - Всегда очищайте перехваты при выгрузке плагина
  7. Проверяйте количество повторяющихся полей - Проверяйте количество перед доступом к индексам повторяющихся полей
  8. Тестируйте с клиентами - Убедитесь, что сообщения правильно отображаются на стороне клиента
  9. Используйте соответствующие типы данных - Сопоставляйте функции PbRead*/PbSet* с типами полей
  10. Учитывайте производительность - Избегайте отправки больших сообщений или слишком большого количества сообщений за тик

Часто используемые 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 для логирования или запуска побочных эффектов.