Entity handles

Entities in scripts are entity handles, not indexes — NULL_ENTITY, IsValid, why an entity is a negative number, converting to and from indexes and slots, and what happens to a handle whose entity is gone.

In C++ an entity is a CEntityInstance* for the current call and a CHandle<T> for anything that has to last. A script has only one kind of value for both: an entity handle. Every entity methodmap — CEntityInstance, CBaseEntity, CCSPlayerController, CCSPlayerPawn, … — holds one.

A handle is what the C++ CEntityHandle is: the entity's index and a serial number. When the entity is removed and another one takes its index, the old handle does not turn into the new entity; it stays bound to the one that is gone.

What the value is

ValueMeaning
NULL_ENTITY (-1)No entity: a failed lookup, an empty handle field, an argument left out
Anything an entity native or property returnsAn entity handle: the top bit set, the serial and the index below it

Because the top bit is set, every entity handle is a negative number. The SourceMod habit of testing an entity against 0 does the wrong thing:

plugin.sp
// Wrong: true for every entity, live or not.
if (view_as<int>(entity) < 0)
	return;

// Right: no entity at all.
if (entity == NULL_ENTITY)
	return;

// Right: no entity, or one that has been removed since.
if (!entity.IsValid())
	return;

!= NULL_ENTITY is enough for a value you have just been handed — a lookup, a hook argument, an event field. A value kept from earlier (a global, a timer's data, an ArrayList entry) needs IsValid(): it is false for NULL_ENTITY and for a handle whose entity has been removed.

A plain index is not an entity. view_as<CBaseEntity>(5) is not entity 5; natives refuse it with "5 is not an entity". Make a handle from an index with CBaseEntity.FromIndex.

Keeping an entity across frames

What C++ does with a CHandle, a script does with the value itself. Store it, pass it as a timer's data, put it in an ArrayList — and check IsValid() when it comes back:

plugin.cpp
CHandle<CCSPlayerController> hPlayer = pController->GetHandle();

g_pSource2ModScheduler->AddTimer(3.0f, [hPlayer]()
{
    CCSPlayerController* pPlayer = hPlayer.Get();
    if (!pPlayer)
        return; // disconnected in the meantime

    pPlayer->PrintToChat("Three seconds later");
});

There is no separate "entity reference" type to convert to and back, as SourceMod's EntIndexToEntRef has: the handle already is one.

Stale handles

A handle whose entity has been removed is stale. It keeps its index (GetEntityIndex() still returns it), but:

  • IsValid() is false;
  • CBaseEntity.FromIndex(oldIndex) returns whatever lives at the index now — NULL_ENTITY or a different entity, never the stale handle;
  • every native and every property on it fails with a run-time error ("Entity 123 (handle …) no longer exists") instead of reading or writing the entity that reused the index. The error aborts the current callback, as any native error does.

NULL_ENTITY passed where an entity is required, or used for a property, fails the same way. Where an entity argument is optional (activator, caller, a start for a search), NULL_ENTITY means "none".

Handle fields read the same way: a CHandle field of a schema class reads as the entity it points at, NULL_ENTITY when it is empty or its entity is gone.

Converting

Convert only where an index or a slot is the point — an index from a game event, a transmit bit, a slot for Source2ModClients. Everywhere else, keep the entity.

plugin.cpp
// An index: from a game event, a transmit bit.
int index = event->GetInt("entindex");
CBaseEntity* pEntity = CBaseEntity::FromIndex<CBaseEntity>(index);   // nullptr when there is none
if (pEntity)
    index = pEntity->GetEntityIndex().Get();

// A player slot (0..63) and the controller in it.
CCSPlayerController* pPlayer = CCSPlayerController::FromSlot(CPlayerSlot(0));
if (pPlayer)
{
    CPlayerSlot slot = pPlayer->GetPlayerSlot();
    CCSPlayerPawn* pPawn = pPlayer->GetPlayerPawn();
    if (pPawn)
        pPlayer = pPawn->GetController();
}

// The userid of a game event.
CCSPlayerController* pVictim = CCSPlayerController::FromUserId(event->GetInt("userid"));
FromToCall
Entity indexEntityCBaseEntity.FromIndex(index)
EntityEntity indexentity.GetEntityIndex() — the index the handle carries, even when stale; -1 for NULL_ENTITY
CPlayerSlotControllerCCSPlayerController.FromSlot(slot) — NULL_ENTITY for an empty slot
ControllerCPlayerSlotplayer.GetPlayerSlot(); GetSlot() for the int
int slotCPlayerSlotCPlayerSlot(slot); slot.Get() back
useridControllerCCSPlayerController.FromUserId(userid); player.GetUserID() back
SteamID64 (text)ControllerCCSPlayerController.FromSteamId("7656…")
PawnControllerpawn.GetController(), CCSPlayerController.FromPawn(pawn)
ControllerPawnplayer.GetPlayerPawn(), player.GetPawn()
Game event fieldController / pawn / entityevent.GetPlayerController("userid"), event.GetPlayerPawn("userid"), event.GetEntity(key)

A SourceMod "client index" is slot + 1, which is also the controller's entity index.

Casting between classes

view_as changes which methodmap a value is used through; the handle stays the same. Check what the entity is when it is not known already:

plugin.sp
void Greet(CEntityInstance entity)
{
	char classname[64];
	entity.GetClassname(classname, sizeof(classname));
	if (!Source2ModUtils.Equal(classname, "cs_player_controller"))
		return;

	CCSPlayerController player = view_as<CCSPlayerController>(entity);
	player.PrintToChat("hi");
}

Natives that return an entity are typed already: GetPlayerPawn() returns a CCSPlayerPawn, FromSlot() a CCSPlayerController.

Many entities

Entity handles are plain cells, so they go into ArrayLists and StringMaps as they are:

plugin.sp
ArrayList g_marked;

void Store(CBaseEntity entity)
{
	if (g_marked == null)
		g_marked = new ArrayList();
	g_marked.Push(entity);
}

// Drops the ones that are gone.
void Sweep()
{
	for (int i = g_marked.Length - 1; i >= 0; i--)
	{
		CBaseEntity entity = g_marked.Get(i);
		if (!entity.IsValid())
			g_marked.Erase(i);
	}
}

Searches hand them out the same way:

plugin.sp
// One at a time: NULL_ENTITY starts the search and ends it.
CBaseEntity entity = NULL_ENTITY;
while ((entity = Source2ModEntities.FindEntityByClassname(entity, "func_buyzone")) != NULL_ENTITY)
	entity.AcceptInput("Disable");

// All at once: an ArrayList of entities, closed by the caller.
ArrayList weapons = Source2ModEntities.FindAllEntitiesByDesignerName("weapon_ak47");
for (int i = 0; i < weapons.Length; i++)
{
	CBaseEntity weapon = weapons.Get(i);
	weapon.Remove();
}
delete weapons;

Objects that are not entities

Services, the scene node, the game rules and the other schema classes that are not entities are SchemaObjects. The core remembers how one was reached — the entity and the fields from it — and finds the object again on every use, so one kept after its owner is gone reads as gone instead of reading freed memory. IsNull tests for none:

plugin.sp
CPlayer_ItemServices items = pawn.m_pItemServices;
if (!items.IsNull)
	items.GiveNamedItem("weapon_ak47");

They are good as long as what they belong to exists; reach them again from the entity rather than keeping them.

On this page