Handle System

Understanding and working with entity handles, pointers, indices, and player slots in Source 2.

Understanding the Handle System

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.

Why Use Handles?

Most of the S2SDK requires working with handles:

  • Schema System: Reading and writing entity properties requires valid entity handles
  • Entity Methods: GetEntity*() and SetEntity*() methods require handles
  • Client Operations: Player-related functionality uses handles for safety
  • Persistence: Handles remain valid across game ticks, even if entities move in memory

Handles vs Pointers vs Indices

TypeDescriptionUse CaseValidity Tracking
HandleSafe reference to entityLong-term storage, cross-tick operations✅ Yes
PointerDirect memory addressImmediate operations, single tick❌ No
IndexEntity index in entity listSimple identification, networking⚠️ Partial
Player SlotPlayer-specific index (0-63)Client/player operations⚠️ Partial

Entity Identification Types

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
}

When to use:

  • Storing entity references across multiple ticks
  • Long-term entity tracking
  • Schema operations

2. Entity Pointer

nint entity = EntIndexToEntPointer(entityIndex);

// Must validate before use
if (IsValidEntPointer(entity))
{
    // Use immediately
    string classname = GetEntityClassname(entity);
}

When to use:

  • Immediate, single-tick operations
  • Performance-critical code
  • When you already validated the entity

3. Entity Index

int entityIndex = EntPointerToEntIndex(entity);

// Entity indices are simple integers
PrintToServer($"Entity index: {entityIndex}\n");

When to use:

  • Simple entity identification
  • Networking and serialization
  • Debugging and logging

4. Player Slot

int playerSlot = EntPointerToPlayerSlot(entity);

// Player slots are 0-63 for players
if (playerSlot >= 0 && playerSlot < 64)
{
    PrintToServer($"Player slot: {playerSlot}\n");
}

When to use:

  • Player-specific operations
  • Client commands and events
  • Familiar to CS modding community

Conversion Functions

S2SDK provides comprehensive conversion functions between all identifier types:

Player Conversions

Entity ↔ Player Slot
Player Slot ↔ Handle
Player Slot ↔ Client
Player Services
// Entity pointer to player slot
int playerSlot = EntPointerToPlayerSlot(entity);

// Player slot to entity pointer
nint entity = PlayerSlotToEntPointer(playerSlot);

Entity Conversions

Index ↔ Pointer
Pointer ↔ Handle
Index ↔ Handle
// Entity index to pointer
int entityIndex = 5;
nint entity = EntIndexToEntPointer(entityIndex);

// Entity pointer to index
int index = EntPointerToEntIndex(entity);

Validation Functions

Always validate entities before using them:

// ✅ 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
    }
}

Player Slot Validation Functions

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:

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

Player Connection States

Players go through several connection states:

  1. Connected (IsClientConnected) - Player has connected to the server
  2. Authorized (IsClientAuthorized) - Steam authentication completed
  3. 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");
    }
}

When to Use Each Function

FunctionUse Case
IsClientConnected()Check if player slot is occupied
IsClientAuthorized()Check if Steam authentication completed (for anti-cheat, admin checks)
IsClientInGame()Most common - Player is fully loaded and ready
IsClientAlive()Check if player is alive (not spectating/dead)
IsFakeClient()Detect bots for special handling
IsClientSourceTV()Exclude SourceTV bot from player operations

Practical Examples

Example 1: Storing Entity References

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

Example 2: Working with Player Slots

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 is playerSlot (0-63)
        PrintToServer($"Player Slot: {caller}\n");

        // ✅ PREFERRED: Get handle first for validation and use
        int entityHandle = PlayerSlotToEntHandle(caller);
        if (!IsValidEntHandle(entityHandle))
        {
            PrintToServer("Invalid player entity\n");
            return ResultType.Handled;
        }

        // Get other representations
        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");

        // Get entity properties using handle (SDK requires handles!)
        string classname = GetEntityClassname(entityHandle);
        PrintToServer($"Classname: {classname}\n");

        return ResultType.Handled;
    }
}

Example 3: Safe Entity Iteration

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;

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

Example 4: Handle Validation Pattern

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

Example 5: Converting Between All Types

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

Common Patterns

Pattern 1: Store Handles, Use Pointers

// ✅ 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
}

Pattern 2: Validate Before Use

// ✅ GOOD: Validate handle once
int entityHandle = PlayerSlotToEntHandle(playerSlot);
if (IsValidEntHandle(entityHandle))
{
    // No need to validate pointer - handle validation is sufficient
    nint entity = EntHandleToEntPointer(entityHandle);
    ProcessEntity(entity);
}

// ❌ BAD: Assume validity
int entityHandle = PlayerSlotToEntHandle(playerSlot);
nint entity = EntHandleToEntPointer(entityHandle);
ProcessEntity(entity); // May crash!

// ⚠️ REDUNDANT: Double validation
int entityHandle = PlayerSlotToEntHandle(playerSlot);
if (IsValidEntHandle(entityHandle))
{
    nint entity = EntHandleToEntPointer(entityHandle);
    if (IsValidEntPointer(entity))  // Expensive and unnecessary!
    {
        ProcessEntity(entity);
    }
}

Pattern 3: Use Player Slots for Players

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

Performance Considerations

  1. Pointer Validation (Expensive): IsValidEntPointer() does a full entity system search. Avoid in loops or frequent checks.
  2. Handle Validation (Fast): IsValidEntHandle() is lightweight and efficient. Prefer this for validation.
  3. Conversion Overhead: Converting between types has minimal overhead, but avoid unnecessary conversions in tight loops.
  4. Pointer Access: Direct pointer access is fastest, but only safe within the same tick.
  5. Handle Storage: Handles are just integers (4 bytes), very efficient to store.

Performance Comparison

// ⚠️ SLOW: Validating 100 entities with IsValidEntPointer
for (int i = 0; i < 100; i++)
{
    nint entity = EntIndexToEntPointer(i);
    if (IsValidEntPointer(entity))  // Expensive!
    {
        ProcessEntity(entity);
    }
}

// ✅ FAST: Validating 100 entities with IsValidEntHandle
for (int i = 0; i < 100; i++)
{
    int handle = EntIndexToEntHandle(i);
    if (IsValidEntHandle(handle))  // Much faster!
    {
        nint entity = EntHandleToEntPointer(handle);
        ProcessEntity(entity);
    }
}

Best Practices

  1. Store handles for long-term entity references
  2. Use pointers for immediate, same-tick operations
  3. Validate with IsValidEntHandle() - it's fast and efficient
  4. Use player slots for player-specific functionality (familiar to CS community)
  5. Convert pointer to handle before validation if you only have a pointer
  6. Don't use IsValidEntPointer() in loops or performance-critical code - it's expensive!
  7. Don't store pointers across ticks
  8. Don't assume handles are always valid - entities can be deleted
  9. Don't double-validate with both handle and pointer checks - handle validation is sufficient

Common Invalid Handle Values

// These constants indicate invalid/special handles:
const int INVALID_EHANDLE_INDEX = -1;
const int INVALID_ENTITY_INDEX = -1;
const int INVALID_PLAYER_SLOT = -1;

// Always check for these values:
if (entityHandle == INVALID_EHANDLE_INDEX)
{
    PrintToServer("Invalid handle\n");
    return;
}

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

Troubleshooting

Handle Becomes Invalid

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

Pointer Becomes Invalid Across Ticks

Problem: Pointer worked last tick, crashes this tick Cause: Entity moved in memory or was deleted Solution: Don't store pointers, use handles instead

Player Slot Out of Range

Problem: Player slot is negative or > 63 Cause: Entity is not a player, or player disconnected Solution: Validate player slot range before use

Handle to Pointer Returns Null

Problem: EntHandleToEntPointer() returns null Cause: Handle is invalid or entity was deleted Solution: Check with IsValidEntHandle() first

Summary

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.