Система хендлов

Понимание и работа с хендлами сущностей, указателями, индексами и слотами игроков в Source 2.

Понимание системы хендлов

Система хендлов является ключевой частью движка Source 2, которая обеспечивает безопасные ссылки на игровые сущности. В отличие от обычных указателей, хендлы автоматически отслеживают валидность сущностей и предотвращают крэши от обращения к удаленным или невалидным объектам.

Зачем использовать хендлы?

Большая часть S2SDK требует работы с хендлами:

  • Система схем: Чтение и запись свойств сущностей требует валидных хендлов сущностей
  • Методы сущностей: Методы GetEntity*() и SetEntity*() требуют хендлы
  • Операции с клиентами: Функциональность, связанная с игроками, использует хендлы для безопасности
  • Постоянство: Хендлы остаются валидными между игровыми тиками, даже если сущности перемещаются в памяти

Хендлы vs Указатели vs Индексы

ТипОписаниеСлучай использованияОтслеживание валидности
ХендлБезопасная ссылка на сущностьДолгосрочное хранение, операции между тиками✅ Да
УказательПрямой адрес в памятиНемедленные операции, один тик❌ Нет
ИндексИндекс сущности в списке сущностейПростая идентификация, сетевая передача⚠️ Частично
Слот игрокаИндекс для конкретного игрока (0-63)Операции с клиентом/игроком⚠️ Частично

Типы идентификации сущностей

1. Хендл сущности (рекомендуется)

int entityHandle = EntIndexToEntHandle(entityIndex);

// Хендлы остаются валидными, даже если сущность перемещается в памяти
if (IsValidEntHandle(entityHandle))
{
    nint entity = EntHandleToEntPointer(entityHandle);
    // Безопасно использовать указатель на сущность
}

Когда использовать:

  • Хранение ссылок на сущности между несколькими тиками
  • Долгосрочное отслеживание сущностей
  • Операции со схемами

2. Указатель на сущность

nint entity = EntIndexToEntPointer(entityIndex);

// Необходимо проверить перед использованием
if (IsValidEntPointer(entity))
{
    // Использовать немедленно
    string classname = GetEntityClassname(entity);
}

Когда использовать:

  • Немедленные операции в пределах одного тика
  • Критичный к производительности код
  • Когда вы уже проверили сущность

3. Индекс сущности

int entityIndex = EntPointerToEntIndex(entity);

// Индексы сущностей - это простые целые числа
PrintToServer($"Entity index: {entityIndex}\n");

Когда использовать:

  • Простая идентификация сущности
  • Сетевая передача и сериализация
  • Отладка и логирование

4. Слот игрока

int playerSlot = EntPointerToPlayerSlot(entity);

// Слоты игроков от 0 до 63 для игроков
if (playerSlot >= 0 && playerSlot < 64)
{
    PrintToServer($"Player slot: {playerSlot}\n");
}

Когда использовать:

  • Операции, специфичные для игрока
  • Команды клиента и события
  • Знакомо CS-моддинг сообществу

Функции конвертации

S2SDK предоставляет комплексные функции конвертации между всеми типами идентификаторов:

Конвертация игроков

Сущность ↔ Слот игрока
Слот игрока ↔ Хендл
Слот игрока ↔ Клиент
Сервисы игрока
// Указатель на сущность в слот игрока
int playerSlot = EntPointerToPlayerSlot(entity);

// Слот игрока в указатель на сущность
nint entity = PlayerSlotToEntPointer(playerSlot);

Конвертация сущностей

Индекс ↔ Указатель
Указатель ↔ Хендл
Индекс ↔ Хендл
// Индекс сущности в указатель
int entityIndex = 5;
nint entity = EntIndexToEntPointer(entityIndex);

// Указатель на сущность в индекс
int index = EntPointerToEntIndex(entity);

Функции валидации

Всегда проверяйте сущности перед их использованием:

// ✅ РЕКОМЕНДУЕТСЯ: Проверить хендл сущности (быстро)
if (IsValidEntHandle(entityHandle))
{
    nint entity = EntHandleToEntPointer(entityHandle);
    // Безопасно использовать
}

// ⚠️ ИЗБЕГАЙТЕ: Проверка указателя на сущность (дорого!)
if (IsValidEntPointer(entity))
{
    // Безопасно использовать немедленно, но эта проверка медленная
}

// ✅ ЛУЧШЕ: Сначала конвертировать указатель в хендл, затем проверить
int handle = EntPointerToEntHandle(entity);
if (IsValidEntHandle(handle))
{
    // Намного быстрее
}

// Проверка слота игрока
if (playerSlot >= 0 && playerSlot < 64)
{
    // ✅ ПРЕДПОЧТИТЕЛЬНО: Использовать хендл для проверки
    int entityHandle = PlayerSlotToEntHandle(playerSlot);
    if (IsValidEntHandle(entityHandle))
    {
        nint entity = EntHandleToEntPointer(entityHandle);
        // Безопасно использовать
    }
}

Функции проверки слота игрока

Для операций, специфичных для игрока, S2SDK предоставляет специализированные функции проверки, которые работают напрямую со слотами игроков. Они более удобны, чем проверка хендла, для проверки состояния подключения игрока:

// Проверить, валиден ли слот игрока и подключен ли игрок
if (IsClientConnected(playerSlot))
{
    PrintToServer($"Player {playerSlot} is connected\n");
}

// Проверить, аутентифицирован ли игрок (аутентификация Steam завершена)
if (IsClientAuthorized(playerSlot))
{
    PrintToServer($"Player {playerSlot} is authenticated\n");
}

// Проверить, полностью ли игрок вошел в игру
if (IsClientInGame(playerSlot))
{
    PrintToServer($"Player {playerSlot} is in game\n");
}

// Проверить, жив ли игрок
if (IsClientAlive(playerSlot))
{
    PrintToServer($"Player {playerSlot} is alive\n");
}

// Проверить, является ли клиент ботом
if (IsFakeClient(playerSlot))
{
    PrintToServer($"Player {playerSlot} is a bot\n");
}

// Проверить, является ли клиент SourceTV
if (IsClientSourceTV(playerSlot))
{
    PrintToServer($"Player {playerSlot} is SourceTV\n");
}

Состояния подключения игрока

Игроки проходят через несколько состояний подключения:

  1. Подключен (IsClientConnected) - Игрок подключился к серверу
  2. Авторизован (IsClientAuthorized) - Аутентификация Steam завершена
  3. В игре (IsClientInGame) - Игрок полностью загрузился и вошел в игру
Типичный паттерн проверки
Проверка перед операциями
Фильтр SourceTV
public void DoSomethingWithPlayer(int playerSlot)
{
    // Базовая проверка
    if (playerSlot < 0 || playerSlot >= 64)
        return;

    // Проверить, находится ли игрок в игре (наиболее частая проверка)
    if (!IsClientInGame(playerSlot))
    {
        PrintToServer("Player not in game yet\n");
        return;
    }

    // Теперь безопасно работать с игроком
    int handle = PlayerSlotToEntHandle(playerSlot);
    if (IsValidEntHandle(handle))
    {
        string name = GetClientName(playerSlot);
        PrintToServer($"Player {name} is ready\n");
    }
}

Когда использовать каждую функцию

ФункцияСлучай использования
IsClientConnected()Проверить, занят ли слот игрока
IsClientAuthorized()Проверить, завершена ли аутентификация Steam (для анти-чита, проверки админов)
IsClientInGame()Наиболее частая - Игрок полностью загружен и готов
IsClientAlive()Проверить, жив ли игрок (не наблюдает/мертв)
IsFakeClient()Определить ботов для специальной обработки
IsClientSourceTV()Исключить бота SourceTV из операций с игроками

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

Пример 1: Хранение ссылок на сущности

c#
using Plugify;
using static s2sdk.s2sdk;

public unsafe class EntityTracker : Plugin
{
    // Хранить хендлы сущностей, а не указатели!
    private Dictionary<int, int> trackedEntities = new Dictionary<int, int>();

    public void OnPluginStart()
    {
        HookEvent("player_spawn", Event_OnPlayerSpawn, HookMode.Post);
    }

    public void Event_OnPlayerSpawn(string name, nint @event, bool dontBroadcast)
    {
        int playerSlot = GetEventPlayerSlot(@event, "userid", 0);

        // Получить хендл сущности (безопасно для долгосрочного хранения)
        int entityHandle = PlayerSlotToEntHandle(playerSlot);

        // Хранить хендл вместо указателя
        trackedEntities[playerSlot] = entityHandle;

        PrintToServer($"Tracking player {playerSlot} with handle {entityHandle}\n");
    }

    public void CheckTrackedEntities()
    {
        foreach (var kvp in trackedEntities.ToList())
        {
            int playerSlot = kvp.Key;
            int entityHandle = kvp.Value;

            // Проверить хендл перед использованием
            if (IsValidEntHandle(entityHandle))
            {
                nint entity = EntHandleToEntPointer(entityHandle);
                PrintToServer($"Player {playerSlot} is still valid\n");
            }
            else
            {
                // Сущность больше не существует, удалить из отслеживания
                trackedEntities.Remove(playerSlot);
                PrintToServer($"Player {playerSlot} disconnected, stopped tracking\n");
            }
        }
    }
}

Пример 2: Работа со слотами игроков

c#
using Plugify;
using static s2sdk.s2sdk;

public unsafe class PlayerManager : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("player_info", "Show player information",
            ConVarFlag.LinkedConcommand | ConVarFlag.Release,
            Command_PlayerInfo, HookMode.Post);
    }

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

        // caller это playerSlot (0-63)
        PrintToServer($"Player Slot: {caller}\n");

        // ✅ ПРЕДПОЧТИТЕЛЬНО: Сначала получить хендл для проверки и использования
        int entityHandle = PlayerSlotToEntHandle(caller);
        if (!IsValidEntHandle(entityHandle))
        {
            PrintToServer("Invalid player entity\n");
            return ResultType.Handled;
        }

        // Получить другие представления
        int entityIndex = EntHandleToEntIndex(entityHandle);
        nint clientPtr = PlayerSlotToClientPtr(caller);
        nint entity = EntHandleToEntPointer(entityHandle);

        PrintToServer($"Entity Index: {entityIndex}\n");
        PrintToServer($"Entity Handle: {entityHandle}\n");
        PrintToServer($"Entity Ptr: 0x{entity:X}\n");
        PrintToServer($"Client Ptr: 0x{clientPtr:X}\n");

        // Получить свойства сущности используя хендл (SDK требует хендлы!)
        string classname = GetEntityClassname(entityHandle);
        PrintToServer($"Classname: {classname}\n");

        return ResultType.Handled;
    }
}

Пример 3: Безопасная итерация по сущностям

c#
using Plugify;
using static s2sdk.s2sdk;

public unsafe class EntityManager : Plugin
{
    public void OnPluginStart()
    {
        AddConsoleCommand("list_entities", "List all entities",
            ConVarFlag.LinkedConcommand | ConVarFlag.Release,
            Command_ListEntities, HookMode.Post);
    }

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

        string targetClass = arguments[1];
        int count = 0;

        // Итерация по всем сущностям
        for (int i = 0; i < 2048; i++)
        {
            // Конвертировать индекс в хендл
            int handle = EntIndexToEntHandle(i);

            // Проверить перед использованием
            if (!IsValidEntHandle(handle))
                continue;

            string classname = GetEntityClassname(handle);
            if (classname.Contains(targetClass))
            {
                PrintToServer($"Found {classname} at index {i}, handle {handle}\n");
                count++;
            }
        }

        PrintToServer($"Found {count} entities matching '{targetClass}'\n");
        return ResultType.Handled;
    }
}

Пример 4: Паттерн валидации хендла

c#
using Plugify;
using static s2sdk.s2sdk;

public unsafe class SafeEntityAccess : Plugin
{
    private int storedEntityHandle = -1;

    public void StoreEntity(int playerSlot)
    {
        // Конвертировать слот игрока в хендл для хранения
        storedEntityHandle = PlayerSlotToEntHandle(playerSlot);
        PrintToServer($"Stored entity handle: {storedEntityHandle}\n");
    }

    public void AccessStoredEntity()
    {
        // Проверить хендл (быстро и эффективно)
        if (!IsValidEntHandle(storedEntityHandle))
        {
            PrintToServer("Stored entity is no longer valid\n");
            storedEntityHandle = -1;
            return;
        }

        // Не нужна проверка IsValidEntPointer - проверки хендла достаточно!
        // Использование IsValidEntPointer здесь было бы дорогостоящим и избыточным

        // Использовать хендл напрямую с функциями SDK
        string classname = GetEntityClassname(storedEntityHandle);
        int health = GetEntityHealth(storedEntityHandle);

        PrintToServer($"Entity {classname} has {health} health\n");
    }

    public void OnTick()
    {
        // Проверять сохраненную сущность каждый тик
        AccessStoredEntity();
    }
}

Пример 5: Конвертация между всеми типами

c#
using Plugify;
using static s2sdk.s2sdk;

public unsafe class ConversionDemo : Plugin
{
    public void DemonstrateConversions(int playerSlot)
    {
        PrintToServer("=== Демонстрация конвертации сущностей ===\n");

        // Начальная точка: слот игрока
        PrintToServer($"Player Slot: {playerSlot}\n");

        // 1. Слот игрока в хендл сущности (предпочтительно - проверить один раз!)
        int entityHandle = PlayerSlotToEntHandle(playerSlot);
        if (!IsValidEntHandle(entityHandle))
        {
            PrintToServer("Invalid entity\n");
            return;
        }

        // 2. Получить другие представления из хендла
        int entityIndex = EntHandleToEntIndex(entityHandle);
        PrintToServer($"Entity Index: {entityIndex}\n");
        PrintToServer($"Entity Handle: {entityHandle}\n");

        // 3. Получить указатель из проверенного хендла (не нужно проверять указатель!)
        nint entity = EntHandleToEntPointer(entityHandle);
        PrintToServer($"Entity Pointer: 0x{entity:X}\n");

        // 4. Слот игрока в указатель клиента
        nint clientPtr = PlayerSlotToClientPtr(playerSlot);
        PrintToServer($"Client Pointer: 0x{clientPtr:X}\n");

        // 5. Слот игрока в индекс клиента
        int clientIndex = PlayerSlotToClientIndex(playerSlot);
        PrintToServer($"Client Index: {clientIndex}\n");

        PrintToServer("\n=== Обратные конвертации ===\n");

        // 6. Индекс сущности обратно в хендл
        int handleFromIndex = EntIndexToEntHandle(entityIndex);
        PrintToServer($"Index→Handle: {handleFromIndex}\n");

        // 7. Хендл обратно в указатель
        nint entityFromHandle = EntHandleToEntPointer(handleFromIndex);
        PrintToServer($"Handle→Pointer: 0x{entityFromHandle:X}\n");

        // 8. Указатель клиента обратно в слот игрока
        int slotFromClient = ClientPtrToPlayerSlot(clientPtr);
        PrintToServer($"ClientPtr→Slot: {slotFromClient}\n");

        // 9. Индекс клиента обратно в слот игрока
        int slotFromIndex = ClientIndexToPlayerSlot(clientIndex);
        PrintToServer($"ClientIndex→Slot: {slotFromIndex}\n");

        // Проверить, что все конвертации ведут обратно к тем же значениям
        bool allMatch = (handleFromIndex == entityHandle) &&
                       (entityFromHandle == entity) &&
                       (slotFromClient == playerSlot) &&
                       (slotFromIndex == playerSlot);

        PrintToServer($"\nAll conversions match: {allMatch}\n");
    }
}

Распространенные паттерны

Паттерн 1: Хранить хендлы, использовать указатели

// ✅ ХОРОШО: Хранить хендлы
private int playerEntityHandle;

public void SavePlayer(int playerSlot)
{
    playerEntityHandle = PlayerSlotToEntHandle(playerSlot);
}

public void UsePlayer()
{
    if (IsValidEntHandle(playerEntityHandle))
    {
        nint entity = EntHandleToEntPointer(playerEntityHandle);
        // Использовать указатель на сущность немедленно
    }
}

// ❌ ПЛОХО: Хранить указатели
private nint playerEntity; // Может стать невалидным!

public void SavePlayer(int playerSlot)
{
    playerEntity = PlayerSlotToEntPointer(playerSlot);
    // Указатель может стать невалидным, если сущность удалена
}

Паттерн 2: Проверять перед использованием

// ✅ ХОРОШО: Проверить хендл один раз
int entityHandle = PlayerSlotToEntHandle(playerSlot);
if (IsValidEntHandle(entityHandle))
{
    // Не нужно проверять указатель - проверки хендла достаточно
    nint entity = EntHandleToEntPointer(entityHandle);
    ProcessEntity(entity);
}

// ❌ ПЛОХО: Предполагать валидность
int entityHandle = PlayerSlotToEntHandle(playerSlot);
nint entity = EntHandleToEntPointer(entityHandle);
ProcessEntity(entity); // Может крашнуться!

// ⚠️ ИЗБЫТОЧНО: Двойная проверка
int entityHandle = PlayerSlotToEntHandle(playerSlot);
if (IsValidEntHandle(entityHandle))
{
    nint entity = EntHandleToEntPointer(entityHandle);
    if (IsValidEntPointer(entity))  // Дорого и не нужно!
    {
        ProcessEntity(entity);
    }
}

Паттерн 3: Использовать слоты игроков для игроков

// ✅ ХОРОШО: Использовать слоты игроков для операций с игроками
public void KickPlayer(int playerSlot)
{
    if (playerSlot < 0 || playerSlot >= 64)
        return;

    int handle = PlayerSlotToEntHandle(playerSlot);
    if (IsValidEntHandle(handle))
    {
        // Кикнуть игрока используя знакомую концепцию playerSlot
        ServerCommand($"kickid {playerSlot}");
    }
}

// ✅ ТАКЖЕ ХОРОШО: Использовать хендлы при хранении ссылок
private Dictionary<int, int> playerHandles = new Dictionary<int, int>();

public void TrackPlayer(int playerSlot)
{
    playerHandles[playerSlot] = PlayerSlotToEntHandle(playerSlot);
}

Соображения производительности

  1. Проверка указателя (дорого): IsValidEntPointer() делает полный поиск в системе сущностей. Избегайте в циклах или частых проверках.
  2. Проверка хендла (быстро): IsValidEntHandle() легковесна и эффективна. Предпочитайте это для проверки.
  3. Накладные расходы на конвертацию: Конвертация между типами имеет минимальные накладные расходы, но избегайте ненужных конвертаций в плотных циклах.
  4. Доступ через указатель: Прямой доступ через указатель самый быстрый, но безопасен только в пределах одного тика.
  5. Хранение хендлов: Хендлы - это просто целые числа (4 байта), очень эффективно для хранения.

Сравнение производительности

// ⚠️ МЕДЛЕННО: Проверка 100 сущностей с IsValidEntPointer
for (int i = 0; i < 100; i++)
{
    nint entity = EntIndexToEntPointer(i);
    if (IsValidEntPointer(entity))  // Дорого!
    {
        ProcessEntity(entity);
    }
}

// ✅ БЫСТРО: Проверка 100 сущностей с IsValidEntHandle
for (int i = 0; i < 100; i++)
{
    int handle = EntIndexToEntHandle(i);
    if (IsValidEntHandle(handle))  // Намного быстрее!
    {
        nint entity = EntHandleToEntPointer(handle);
        ProcessEntity(entity);
    }
}

Лучшие практики

  1. Хранить хендлы для долгосрочных ссылок на сущности
  2. Использовать указатели для немедленных операций в пределах одного тика
  3. Проверять с помощью IsValidEntHandle() - это быстро и эффективно
  4. Использовать слоты игроков для функциональности, специфичной для игрока (знакомо CS-сообществу)
  5. Конвертировать указатель в хендл перед проверкой, если у вас есть только указатель
  6. Не использовать IsValidEntPointer() в циклах или критичном к производительности коде - это дорого!
  7. Не хранить указатели между тиками
  8. Не предполагать, что хендлы всегда валидны - сущности могут быть удалены
  9. Не делать двойную проверку и хендла, и указателя - проверки хендла достаточно

Распространенные невалидные значения хендлов

// Эти константы указывают на невалидные/специальные хендлы:
const int INVALID_EHANDLE_INDEX = -1;
const int INVALID_ENTITY_INDEX = -1;
const int INVALID_PLAYER_SLOT = -1;

// Всегда проверяйте эти значения:
if (entityHandle == INVALID_EHANDLE_INDEX)
{
    PrintToServer("Invalid handle\n");
    return;
}

if (playerSlot < 0 || playerSlot >= 64)
{
    PrintToServer("Invalid player slot\n");
    return;
}

Устранение неполадок

Хендл становится невалидным

Проблема: Хендл был валидным, теперь возвращает false для IsValidEntHandle()Причина: Сущность была удалена из игры Решение: Всегда проверяйте перед использованием, корректно обрабатывайте невалидный случай

Указатель становится невалидным между тиками

Проблема: Указатель работал в прошлом тике, крашится в этом тике Причина: Сущность переместилась в памяти или была удалена Решение: Не хранить указатели, вместо этого использовать хендлы

Слот игрока вне диапазона

Проблема: Слот игрока отрицательный или > 63 Причина: Сущность не является игроком, или игрок отключился Решение: Проверять диапазон слота игрока перед использованием

Хендл в указатель возвращает Null

Проблема: EntHandleToEntPointer() возвращает null Причина: Хендл невалиден или сущность была удалена Решение: Сначала проверить с помощью IsValidEntHandle()

Резюме

Система хендлов фундаментальна для безопасного моддинга Source 2:

  • Хендлы обеспечивают безопасность и отслеживание валидности
  • Указатели обеспечивают производительность для немедленного использования
  • Индексы обеспечивают простую идентификацию
  • Слоты игроков обеспечивают знакомые операции, специфичные для игрока

Используйте функции конвертации для перехода между представлениями по мере необходимости, всегда проверяйте перед использованием и предпочитайте хендлы для любого долгосрочного хранения.