Finding Entities

Getting a player controller from a slot, userid, SteamID or pawn, and finding other entities by index, classname, name or crosshair.

Most plugin code starts from something that identifies an entity — a command caller's slot, a game event's userid, a SteamID from your database, an entity index — and needs the typed object. This page lists the ways in.

Players

CCSPlayerController has static lookups for every way a player is identified:

#include "source2mod/schema/entity/classes/CCSPlayerController.h"

static CCSPlayerController* FromSlot(int iSlot);
static CCSPlayerController* FromSlot(CPlayerSlot slot);
static CCSPlayerController* FromUserId(int iUserId);
static CCSPlayerController* FromUserId(CPlayerUserId userId);
static CCSPlayerController* FromSteamId(uint64 uSteamId);
static CCSPlayerController* FromSteamId(CSteamID steamId);
static CCSPlayerController* FromPawn(CCSPlayerPawn* pPawn);

All of them return nullptr when nothing matches. Scripts have the same statics on the CCSPlayerController methodmap — FromSlot(CPlayerSlot), FromUserId(int), FromSteamId (the SteamID64 as decimal text) and FromPawn — returning NULL_ENTITY instead.

  • FromSlot reads the entity at index slot + 1 — controllers always occupy indices 1 to 64. The CPlayerSlot overload also returns null for an invalid slot (-1, which is what the server console has).
  • FromUserId and FromSteamId walk the slots up to maxClients. Bots have a SteamID of 0.
  • FromPawn follows the pawn's m_hController handle; the pawn must not be null.

The command context gives you the caller's slot. The sample plugin's helper:

plugin.cpp
// A controller for a player, nullptr for the server console.
static CCSPlayerController* CallerOf(const Source2ModCommandContext& context)
{
    const CPlayerSlot slot = context.GetPlayerSlot();
    return slot.IsValid() ? CCSPlayerController::FromSlot(slot) : nullptr;
}

For getting the player out of a game event, see Events.

Iterating players

There is no player list to ask for — loop over the slots and skip the empty ones:

for (int i = 0; i < GetGlobalVars()->maxClients; ++i)
{
    CCSPlayerController* pPlayer = CCSPlayerController::FromSlot(i);
    if (!pPlayer || pPlayer->m_iConnected() != PlayerConnectedState::Connected)
        continue;

    if (pPlayer->IsBot())
        continue;

    // ...
}

A controller exists from the moment a client starts connecting until it has fully left, so m_iConnected is worth checking when you are about to message or act on the player (a script can ask Source2ModClients.IsInGame(slot)). For "alive players", also check m_bPawnIsAlive() and that GetPlayerPawn() is not null.

From controller to everything else

You wantCall
The bodyGetPlayerPawn() → CCSPlayerPawn*
The observer pawnGetObserverPawn() → CCSObserverPawn*
SlotGetSlot() (int) / GetPlayerSlot() (CPlayerSlot)
UseridGetUserID() (int) / GetPlayerUserID() (CPlayerUserId)
SteamIDGetSteamID() (uint64) / GetPlayerSteamID() (CSteamID)
NameGetPlayerName()
IP addressGetIpAddress() → CUtlString, without the port
Engine clientGetServerSideClient() → CServerSideClient*

In a script, the text getters fill a buffer (GetPlayerName(name, sizeof(name)), GetSteamID, GetIpAddress); there is no GetServerSideClient().

Any entity by index

CBaseEntity::FromIndex<T> looks an entity up by index and casts it to the type you name; every generated class also has a non-template FromIndex returning its own type. A script has CBaseEntity.FromIndex(int) and view_as for the type:

CBaseEntity* pEntity = CBaseEntity::FromIndex<CBaseEntity>(nIndex);
CCSPlayerPawn* pPawn = CCSPlayerPawn::FromIndex(nIndex);   // same, typed

Both take an int or a CEntityIndex. No type check is done — the cast is only right if the entity at that index really is that class. Check GetClassname() when you are not sure.

By classname, name or crosshair

These live on ISource2ModEntities, with macros that fill in the g_pSource2ModEntities call; a script calls the Source2ModEntities statics:

#include "source2mod/ISource2ModEntities.h"

FIND_ENTITY_BY_CLASSNAME(start, "func_breakable")  // FindEntityByClassname
FIND_ENTITY_BY_NAME(start, "door_main")            // FindEntityByName (targetname)
FIND_PICKER_ENTITY(pController)                    // FindPickerEntity: under the crosshair
GET_GAME_RULES()                                   // the live CCSGameRules, or null

The Find* functions return one match at a time. Pass nullptr (NULL_ENTITY in a script) to start and the previous match to continue:

plugin.cpp
int nBroken = 0;

CBaseEntity* pEntity = nullptr;
while ((pEntity = FIND_ENTITY_BY_CLASSNAME(pEntity, "func_breakable")) != nullptr)
{
    pEntity->AcceptInput("Break");
    nBroken++;
}

FindEntityByName has optional searching-entity, activator, caller and filter arguments, the same as the engine function it wraps (in a script, all but the filter).

All at once

UTIL_FindAllEntitiesByDesignerName<T> (in ISource2ModEntities.h) returns a std::vector<T*> of every live entity whose designer name matches exactly: g_pSource2ModEntities->FindAllEntitiesByDesignerName (the core walks the entity list and returns std::vector<CBaseEntity*>), cast to T. A script gets the same as an ArrayList of entities from Source2ModEntities.FindAllEntitiesByDesignerName, and closes it:

for (auto* pTeam : UTIL_FindAllEntitiesByDesignerName<CTeam>("cs_team_manager"))
{
    // ...
}

for (auto* pProp : UTIL_FindAllEntitiesByDesignerName<CDynamicProp>("prop_dynamic"))
    pProp->AcceptInput("Kill");

Like FromIndex, the cast is unchecked — keep the name and the type consistent.

Watching entities come and go

Instead of searching, you can be told. Implement IEntityListener (from the engine's entity2/entitysystem.h) — override what you need — and register it with g_pSource2ModEntities->AddEntityListener. A script passes one function per callback to Source2ModEntities.AddEntityListener (INVALID_FUNCTION, or _, for the ones it does not need).

CallbackCalled
OnEntityCreatedthe entity exists; keyvalues not applied, not spawned yet
OnEntitySpawnedspawned — usually the one you want
OnEntityDeletedthe last call for it
OnEntityParentChangedits parent changed

A registration can be for every entity, for one designer name ("player", "smokegrenade_projectile"), or for one entity:

plugin.cpp
class SamplePlugin : public ISource2ModPlugin, public IEntityListener
{
    void OnEntitySpawned(CEntityInstance* pEntity) override;
};

// In Load(): only smoke grenades.
g_pSource2ModEntities->AddEntityListener(this, "smokegrenade_projectile");

// Every entity:            AddEntityListener(this);
// One entity:              AddEntityListener(this, nullptr, pEntity);

Some entities are filled in by the game right after they spawn, so a change made in OnEntitySpawned can be overwritten; the sample plugin waits one frame, through a handle:

plugin.cpp
void SamplePlugin::OnEntitySpawned(CEntityInstance* pEntity)
{
    // The designer name above lets only smoke grenades through.
    CHandle<CSmokeGrenadeProjectile> hSmoke = static_cast<CSmokeGrenadeProjectile*>(pEntity)->GetHandle();

    g_pSource2ModScheduler->NextFrame([hSmoke]()
    {
        if (CSmokeGrenadeProjectile* pSmoke = hSmoke.Get())
            pSmoke->m_vSmokeColor = Vector(170.0f, 0.0f, 255.0f);
    });
}

AddEntityListener takes no plugin id: the listener belongs to the plugin whose library its class lives in, and it is dropped for you when that plugin unloads. The same listener may be added again with other filters; it is then called once per matching registration. RemoveEntityListener(this) (REMOVE_ENTITY_LISTENER(this)) drops every registration of it. A registration for one entity goes with the entity, after its OnEntityDeleted. In a script, the id AddEntityListener returns is what Source2ModEntities.RemoveEntityListener takes.

Entity outputs have a listener of the same shape — see Entity I/O.

On this page