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
| Value | Meaning |
|---|---|
NULL_ENTITY (-1) | No entity: a failed lookup, an empty handle field, an argument left out |
| Anything an entity native or property returns | An 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:
// 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:
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");
});void Later(CCSPlayerController player)
{
Source2ModScheduler.AddTimer(3.0, Timer_Later, player);
}
public void Timer_Later(any data)
{
CCSPlayerController player = data;
if (!player.IsValid())
return; // disconnected in the meantime
player.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()isfalse;CBaseEntity.FromIndex(oldIndex)returns whatever lives at the index now —NULL_ENTITYor 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.
// 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"));// An index: from a game event, a transmit bit.
int index = event.GetInt("entindex");
CBaseEntity entity = CBaseEntity.FromIndex(index); // NULL_ENTITY when there is none
if (entity != NULL_ENTITY)
index = entity.GetEntityIndex();
// A player slot (0..63) and the controller in it.
CCSPlayerController player = CCSPlayerController.FromSlot(CPlayerSlot(0));
if (player != NULL_ENTITY)
{
CPlayerSlot slot = player.GetPlayerSlot();
CCSPlayerPawn pawn = player.GetPlayerPawn();
if (pawn != NULL_ENTITY)
player = pawn.GetController();
}
// The userid of a game event.
CCSPlayerController victim = CCSPlayerController.FromUserId(event.GetInt("userid"));| From | To | Call |
|---|---|---|
| Entity index | Entity | CBaseEntity.FromIndex(index) |
| Entity | Entity index | entity.GetEntityIndex() — the index the handle carries, even when stale; -1 for NULL_ENTITY |
CPlayerSlot | Controller | CCSPlayerController.FromSlot(slot) — NULL_ENTITY for an empty slot |
| Controller | CPlayerSlot | player.GetPlayerSlot(); GetSlot() for the int |
| int slot | CPlayerSlot | CPlayerSlot(slot); slot.Get() back |
| userid | Controller | CCSPlayerController.FromUserId(userid); player.GetUserID() back |
| SteamID64 (text) | Controller | CCSPlayerController.FromSteamId("7656…") |
| Pawn | Controller | pawn.GetController(), CCSPlayerController.FromPawn(pawn) |
| Controller | Pawn | player.GetPlayerPawn(), player.GetPawn() |
| Game event field | Controller / pawn / entity | event.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:
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:
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:
// 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:
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.
Coming from SourceMod
For SourceMod plugin authors — what carries over unchanged, what Source2Mod does differently, and the Source2Mod calls for common SourceMod ones.
Natives and forwards
Sharing code between plugins — natives a C++ plugin or a script provides, libraries and optional natives, global and private forwards, and calling functions of other plugins.