The handle system is a core part of the Source 2 engine that provides safe references to game entities. Unlike raw pointers, handles automatically track the validity of entities and prevent crashes from accessing deleted or invalid objects.
Safety First: Handles are much safer than raw pointers because they validate that an entity still exists before allowing access. This prevents crashes from use-after-free bugs common in game modding.
Player Slots: In the CS community, playerSlot is a very familiar concept (values 0-63 for players). While most client functionality uses player slots, handles provide additional safety when tracking entities across multiple game ticks.
int entityHandle = EntIndexToEntHandle(entityIndex);
// Handles remain valid even if entity moves in memory
if (IsValidEntHandle(entityHandle))
{
nint entity = EntHandleToEntPointer(entityHandle);
// Safe to use entity pointer
}
nint entity = EntIndexToEntPointer(entityIndex);
// Must validate before use
if (IsValidEntPointer(entity))
{
// Use immediately
string classname = GetEntityClassname(entity);
}
int playerSlot = EntPointerToPlayerSlot(entity);
// Player slots are 0-63 for players
if (playerSlot >= 0 && playerSlot < 64)
{
PrintToServer($"Player slot: {playerSlot}\n");
}
// Entity pointer to player slot
int playerSlot = EntPointerToPlayerSlot(entity);
// Player slot to entity pointer
nint entity = PlayerSlotToEntPointer(playerSlot);
// Player slot to entity handle
int handle = PlayerSlotToEntHandle(playerSlot);
// Handle to player slot (convert via pointer)
nint entity = EntHandleToEntPointer(handle);
int playerSlot = EntPointerToPlayerSlot(entity);
// Player slot to client pointer
nint clientPtr = PlayerSlotToClientPtr(playerSlot);
// Client pointer to player slot
int playerSlot = ClientPtrToPlayerSlot(clientPtr);
// Player slot to client index
int clientIndex = PlayerSlotToClientIndex(playerSlot);
// Client index to player slot
int playerSlot = ClientIndexToPlayerSlot(clientIndex);
// Player services to player slot
int playerSlot = PlayerServicesToPlayerSlot(playerServices);
// Entity index to pointer
int entityIndex = 5;
nint entity = EntIndexToEntPointer(entityIndex);
// Entity pointer to index
int index = EntPointerToEntIndex(entity);
// Entity pointer to handle
int handle = EntPointerToEntHandle(entity);
// Entity handle to pointer
nint entity = EntHandleToEntPointer(handle);
// Entity index to handle
int handle = EntIndexToEntHandle(entityIndex);
// Entity handle to index
int index = EntHandleToEntIndex(handle);
Performance Warning: IsValidEntPointer() is expensive - it performs a full entity system lookup. Use IsValidEntHandle() instead whenever possible. Handles are much faster to validate!
// ✅ RECOMMENDED: Validate entity handle (fast)
if (IsValidEntHandle(entityHandle))
{
nint entity = EntHandleToEntPointer(entityHandle);
// Safe to use
}
// ⚠️ AVOID: Validate entity pointer (expensive!)
if (IsValidEntPointer(entity))
{
// Safe to use immediately, but this check is slow
}
// ✅ BETTER: Convert pointer to handle first, then validate
int handle = EntPointerToEntHandle(entity);
if (IsValidEntHandle(handle))
{
// Much faster validation
}
// Player slot validation
if (playerSlot >= 0 && playerSlot < 64)
{
// ✅ PREFERRED: Use handle for validation
int entityHandle = PlayerSlotToEntHandle(playerSlot);
if (IsValidEntHandle(entityHandle))
{
nint entity = EntHandleToEntPointer(entityHandle);
// Safe to use
}
}
For player-specific operations, S2SDK provides specialized validation functions that work directly with player slots. These are more convenient than handle validation for checking player connection states:
Player-Specific: These functions are designed specifically for player slots (0-63) and check connection/authentication status, not just entity validity.
// Check if player slot is valid and player is connected
if (IsClientConnected(playerSlot))
{
PrintToServer($"Player {playerSlot} is connected\n");
}
// Check if player is authenticated (Steam auth completed)
if (IsClientAuthorized(playerSlot))
{
PrintToServer($"Player {playerSlot} is authenticated\n");
}
// Check if player has fully entered the game
if (IsClientInGame(playerSlot))
{
PrintToServer($"Player {playerSlot} is in game\n");
}
// Check if player is alive
if (IsClientAlive(playerSlot))
{
PrintToServer($"Player {playerSlot} is alive\n");
}
// Check if client is a bot
if (IsFakeClient(playerSlot))
{
PrintToServer($"Player {playerSlot} is a bot\n");
}
// Check if client is SourceTV
if (IsClientSourceTV(playerSlot))
{
PrintToServer($"Player {playerSlot} is SourceTV\n");
}
In Game (IsClientInGame) - Player has fully loaded and entered the game
Typical Validation Pattern
Check Before Operations
Filter SourceTV
public void DoSomethingWithPlayer(int playerSlot)
{
// Basic validation
if (playerSlot < 0 || playerSlot >= 64)
return;
// Check if player is in game (most common check)
if (!IsClientInGame(playerSlot))
{
PrintToServer("Player not in game yet\n");
return;
}
// Now safe to work with player
int handle = PlayerSlotToEntHandle(playerSlot);
if (IsValidEntHandle(handle))
{
string name = GetClientName(playerSlot);
PrintToServer($"Player {name} is ready\n");
}
}
public void TeleportPlayer(int playerSlot, Vector3 position)
{
// Ensure player is in game and alive
if (!IsClientInGame(playerSlot) || !IsClientAlive(playerSlot))
return;
// Ignore bots if needed
if (IsFakeClient(playerSlot))
return;
// Safe to teleport
int handle = PlayerSlotToEntHandle(playerSlot);
SetEntityOrigin(handle, position);
}
public void BroadcastToPlayers(string message)
{
for (int i = 0; i < 64; i++)
{
// Skip if not connected
if (!IsClientConnected(i))
continue;
// Skip SourceTV
if (IsClientSourceTV(i))
continue;
// Send message to real player
PrintToChat(i, message);
}
}
using Plugify;
using static s2sdk.s2sdk;
public unsafe class EntityTracker : Plugin
{
// Store entity handles, not pointers!
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);
// Get entity handle (safe for long-term storage)
int entityHandle = PlayerSlotToEntHandle(playerSlot);
// Store handle instead of pointer
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;
// Validate handle before use
if (IsValidEntHandle(entityHandle))
{
nint entity = EntHandleToEntPointer(entityHandle);
PrintToServer($"Player {playerSlot} is still valid\n");
}
else
{
// Entity no longer exists, remove from tracking
trackedEntities.Remove(playerSlot);
PrintToServer($"Player {playerSlot} disconnected, stopped tracking\n");
}
}
}
}
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;
// Iterate through all entities
for (int i = 0; i < 2048; i++)
{
// Convert index to handle
int handle = EntIndexToEntHandle(i);
// Validate before use
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)
{
// Convert player slot to handle for storage
storedEntityHandle = PlayerSlotToEntHandle(playerSlot);
PrintToServer($"Stored entity handle: {storedEntityHandle}\n");
}
public void AccessStoredEntity()
{
// Validate handle (fast and efficient)
if (!IsValidEntHandle(storedEntityHandle))
{
PrintToServer("Stored entity is no longer valid\n");
storedEntityHandle = -1;
return;
}
// No need for IsValidEntPointer check - handle validation is enough!
// Using IsValidEntPointer here would be expensive and redundant
// Use handle directly with SDK functions
string classname = GetEntityClassname(storedEntityHandle);
int health = GetEntityHealth(storedEntityHandle);
PrintToServer($"Entity {classname} has {health} health\n");
}
public void OnTick()
{
// Check stored entity every tick
AccessStoredEntity();
}
}
using Plugify;
using static s2sdk.s2sdk;
public unsafe class ConversionDemo : Plugin
{
public void DemonstrateConversions(int playerSlot)
{
PrintToServer("=== Entity Conversion Demonstration ===\n");
// Starting point: player slot
PrintToServer($"Player Slot: {playerSlot}\n");
// 1. Player slot to entity handle (preferred - validate once!)
int entityHandle = PlayerSlotToEntHandle(playerSlot);
if (!IsValidEntHandle(entityHandle))
{
PrintToServer("Invalid entity\n");
return;
}
// 2. Derive other representations from handle
int entityIndex = EntHandleToEntIndex(entityHandle);
PrintToServer($"Entity Index: {entityIndex}\n");
PrintToServer($"Entity Handle: {entityHandle}\n");
// 3. Get pointer from validated handle (no need to validate pointer!)
nint entity = EntHandleToEntPointer(entityHandle);
PrintToServer($"Entity Pointer: 0x{entity:X}\n");
// 4. Player slot to client pointer
nint clientPtr = PlayerSlotToClientPtr(playerSlot);
PrintToServer($"Client Pointer: 0x{clientPtr:X}\n");
// 5. Player slot to client index
int clientIndex = PlayerSlotToClientIndex(playerSlot);
PrintToServer($"Client Index: {clientIndex}\n");
PrintToServer("\n=== Reverse Conversions ===\n");
// 6. Entity index back to handle
int handleFromIndex = EntIndexToEntHandle(entityIndex);
PrintToServer($"Index→Handle: {handleFromIndex}\n");
// 7. Handle back to pointer
nint entityFromHandle = EntHandleToEntPointer(handleFromIndex);
PrintToServer($"Handle→Pointer: 0x{entityFromHandle:X}\n");
// 8. Client pointer back to player slot
int slotFromClient = ClientPtrToPlayerSlot(clientPtr);
PrintToServer($"ClientPtr→Slot: {slotFromClient}\n");
// 9. Client index back to player slot
int slotFromIndex = ClientIndexToPlayerSlot(clientIndex);
PrintToServer($"ClientIndex→Slot: {slotFromIndex}\n");
// Verify all conversions lead back to same values
bool allMatch = (handleFromIndex == entityHandle) &&
(entityFromHandle == entity) &&
(slotFromClient == playerSlot) &&
(slotFromIndex == playerSlot);
PrintToServer($"\nAll conversions match: {allMatch}\n");
}
}
// ✅ GOOD: Store handles
private int playerEntityHandle;
public void SavePlayer(int playerSlot)
{
playerEntityHandle = PlayerSlotToEntHandle(playerSlot);
}
public void UsePlayer()
{
if (IsValidEntHandle(playerEntityHandle))
{
nint entity = EntHandleToEntPointer(playerEntityHandle);
// Use entity pointer immediately
}
}
// ❌ BAD: Store pointers
private nint playerEntity; // Can become invalid!
public void SavePlayer(int playerSlot)
{
playerEntity = PlayerSlotToEntPointer(playerSlot);
// Pointer may become invalid if entity is deleted
}
// ✅ GOOD: Use player slots for player operations
public void KickPlayer(int playerSlot)
{
if (playerSlot < 0 || playerSlot >= 64)
return;
int handle = PlayerSlotToEntHandle(playerSlot);
if (IsValidEntHandle(handle))
{
// Kick player using familiar playerSlot concept
ServerCommand($"kickid {playerSlot}");
}
}
// ✅ ALSO GOOD: Use handles when storing references
private Dictionary<int, int> playerHandles = new Dictionary<int, int>();
public void TrackPlayer(int playerSlot)
{
playerHandles[playerSlot] = PlayerSlotToEntHandle(playerSlot);
}
Critical: IsValidEntPointer() performs a full entity system lookup and is significantly more expensive than IsValidEntHandle(). Prefer handle validation in performance-sensitive code!
Pointer Validation (Expensive): IsValidEntPointer() does a full entity system search. Avoid in loops or frequent checks.
Handle Validation (Fast): IsValidEntHandle() is lightweight and efficient. Prefer this for validation.
Conversion Overhead: Converting between types has minimal overhead, but avoid unnecessary conversions in tight loops.
Pointer Access: Direct pointer access is fastest, but only safe within the same tick.
Handle Storage: Handles are just integers (4 bytes), very efficient to store.
Problem: Handle was valid, now returns false for IsValidEntHandle()Cause: Entity was deleted from the game
Solution: Always validate before use, handle the invalid case gracefully
The handle system is fundamental to safe Source 2 modding:
Handles provide safety and validity tracking
Pointers provide performance for immediate use
Indices provide simple identification
Player Slots provide familiar player-specific operations
Use the conversion functions to move between representations as needed, always validate before use, and prefer handles for any long-term storage.
Remember: Most S2SDK functionality (schemas, entity methods, client operations) requires handles. Master the handle system to write safe, crash-free plugins!