Система хендлов является ключевой частью движка Source 2, которая обеспечивает безопасные ссылки на игровые сущности. В отличие от обычных указателей, хендлы автоматически отслеживают валидность сущностей и предотвращают крэши от обращения к удаленным или невалидным объектам.
Безопасность прежде всего: Хендлы намного безопаснее обычных указателей, потому что они проверяют существование сущности перед разрешением доступа. Это предотвращает крэши от ошибок use-after-free, часто встречающихся в моддинге игр.
Слоты игроков: В CS-сообществе playerSlot - очень знакомое понятие (значения 0-63 для игроков). Хотя большинство функциональности клиента использует слоты игроков, хендлы обеспечивают дополнительную безопасность при отслеживании сущностей между несколькими игровыми тиками.
int entityHandle = EntIndexToEntHandle(entityIndex);
// Хендлы остаются валидными, даже если сущность перемещается в памяти
if (IsValidEntHandle(entityHandle))
{
nint entity = EntHandleToEntPointer(entityHandle);
// Безопасно использовать указатель на сущность
}
Когда использовать:
Хранение ссылок на сущности между несколькими тиками
nint entity = EntIndexToEntPointer(entityIndex);
// Необходимо проверить перед использованием
if (IsValidEntPointer(entity))
{
// Использовать немедленно
string classname = GetEntityClassname(entity);
}
int playerSlot = EntPointerToPlayerSlot(entity);
// Слоты игроков от 0 до 63 для игроков
if (playerSlot >= 0 && playerSlot < 64)
{
PrintToServer($"Player slot: {playerSlot}\n");
}
// Указатель на сущность в слот игрока
int playerSlot = EntPointerToPlayerSlot(entity);
// Слот игрока в указатель на сущность
nint entity = PlayerSlotToEntPointer(playerSlot);
// Слот игрока в хендл сущности
int handle = PlayerSlotToEntHandle(playerSlot);
// Хендл в слот игрока (конвертация через указатель)
nint entity = EntHandleToEntPointer(handle);
int playerSlot = EntPointerToPlayerSlot(entity);
// Слот игрока в указатель клиента
nint clientPtr = PlayerSlotToClientPtr(playerSlot);
// Указатель клиента в слот игрока
int playerSlot = ClientPtrToPlayerSlot(clientPtr);
// Слот игрока в индекс клиента
int clientIndex = PlayerSlotToClientIndex(playerSlot);
// Индекс клиента в слот игрока
int playerSlot = ClientIndexToPlayerSlot(clientIndex);
// Сервисы игрока в слот игрока
int playerSlot = PlayerServicesToPlayerSlot(playerServices);
// Индекс сущности в указатель
int entityIndex = 5;
nint entity = EntIndexToEntPointer(entityIndex);
// Указатель на сущность в индекс
int index = EntPointerToEntIndex(entity);
// Указатель на сущность в хендл
int handle = EntPointerToEntHandle(entity);
// Хендл сущности в указатель
nint entity = EntHandleToEntPointer(handle);
// Индекс сущности в хендл
int handle = EntIndexToEntHandle(entityIndex);
// Хендл сущности в индекс
int index = EntHandleToEntIndex(handle);
Всегда проверяйте сущности перед их использованием:
Предупреждение о производительности: IsValidEntPointer()дорогостоящая операция - она выполняет полный поиск в системе сущностей. По возможности используйте IsValidEntHandle() вместо неё. Хендлы проверяются намного быстрее!
// ✅ РЕКОМЕНДУЕТСЯ: Проверить хендл сущности (быстро)
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 предоставляет специализированные функции проверки, которые работают напрямую со слотами игроков. Они более удобны, чем проверка хендла, для проверки состояния подключения игрока:
Специфично для игроков: Эти функции разработаны специально для слотов игроков (0-63) и проверяют состояние подключения/аутентификации, а не просто валидность сущности.
// Проверить, валиден ли слот игрока и подключен ли игрок
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");
}
В игре (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");
}
}
public void TeleportPlayer(int playerSlot, Vector3 position)
{
// Убедиться, что игрок в игре и жив
if (!IsClientInGame(playerSlot) || !IsClientAlive(playerSlot))
return;
// Игнорировать ботов, если нужно
if (IsFakeClient(playerSlot))
return;
// Безопасно телепортировать
int handle = PlayerSlotToEntHandle(playerSlot);
SetEntityOrigin(handle, position);
}
public void BroadcastToPlayers(string message)
{
for (int i = 0; i < 64; i++)
{
// Пропустить, если не подключен
if (!IsClientConnected(i))
continue;
// Пропустить SourceTV
if (IsClientSourceTV(i))
continue;
// Отправить сообщение реальному игроку
PrintToChat(i, message);
}
}
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");
}
}
}
}
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;
}
}
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;
}
}
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();
}
}
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");
}
}
// ✅ ХОРОШО: Хранить хендлы
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);
// Указатель может стать невалидным, если сущность удалена
}
// ✅ ХОРОШО: Использовать слоты игроков для операций с игроками
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);
}
Критично: IsValidEntPointer() выполняет полный поиск в системе сущностей и значительно дороже, чем IsValidEntHandle(). Предпочитайте проверку хендла в критичном к производительности коде!
Проверка указателя (дорого): IsValidEntPointer() делает полный поиск в системе сущностей. Избегайте в циклах или частых проверках.
Проверка хендла (быстро): IsValidEntHandle() легковесна и эффективна. Предпочитайте это для проверки.
Накладные расходы на конвертацию: Конвертация между типами имеет минимальные накладные расходы, но избегайте ненужных конвертаций в плотных циклах.
Доступ через указатель: Прямой доступ через указатель самый быстрый, но безопасен только в пределах одного тика.
Хранение хендлов: Хендлы - это просто целые числа (4 байта), очень эффективно для хранения.
// Эти константы указывают на невалидные/специальные хендлы:
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
Причина: Сущность не является игроком, или игрок отключился
Решение: Проверять диапазон слота игрока перед использованием
Проблема: EntHandleToEntPointer() возвращает null
Причина: Хендл невалиден или сущность была удалена
Решение: Сначала проверить с помощью IsValidEntHandle()
Система хендлов фундаментальна для безопасного моддинга Source 2:
Хендлы обеспечивают безопасность и отслеживание валидности
Указатели обеспечивают производительность для немедленного использования
Индексы обеспечивают простую идентификацию
Слоты игроков обеспечивают знакомые операции, специфичные для игрока
Используйте функции конвертации для перехода между представлениями по мере необходимости, всегда проверяйте перед использованием и предпочитайте хендлы для любого долгосрочного хранения.
Запомните: Большая часть функциональности S2SDK (схемы, методы сущностей, операции с клиентами) требует хендлы. Освойте систему хендлов, чтобы писать безопасные плагины без крашей!