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.
FromSlotreads the entity at indexslot + 1— controllers always occupy indices 1 to 64. TheCPlayerSlotoverload also returns null for an invalid slot (-1, which is what the server console has).FromUserIdandFromSteamIdwalk the slots up tomaxClients. Bots have a SteamID of 0.FromPawnfollows the pawn'sm_hControllerhandle; the pawn must not be null.
The command context gives you the caller's slot. The sample plugin's helper:
// 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;
}// A controller for a player, NULL_ENTITY for the server console.
CCSPlayerController CallerOf(Source2ModCommandContext context)
{
CPlayerSlot slot = context.GetPlayerSlot();
return slot.IsValid() ? CCSPlayerController.FromSlot(slot) : NULL_ENTITY;
}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;
// ...
}for (int i = 0; i < Source2ModApi.GetGlobalVars().maxClients; ++i)
{
CCSPlayerController player = CCSPlayerController.FromSlot(CPlayerSlot(i));
if (player == NULL_ENTITY || !Source2ModClients.IsInGame(CPlayerSlot(i)))
continue;
if (player.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 want | Call |
|---|---|
| The body | GetPlayerPawn() → CCSPlayerPawn* |
| The observer pawn | GetObserverPawn() → CCSObserverPawn* |
| Slot | GetSlot() (int) / GetPlayerSlot() (CPlayerSlot) |
| Userid | GetUserID() (int) / GetPlayerUserID() (CPlayerUserId) |
| SteamID | GetSteamID() (uint64) / GetPlayerSteamID() (CSteamID) |
| Name | GetPlayerName() |
| IP address | GetIpAddress() → CUtlString, without the port |
| Engine client | GetServerSideClient() → 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, typedCBaseEntity entity = CBaseEntity.FromIndex(index);
CCSPlayerPawn pawn = view_as<CCSPlayerPawn>(CBaseEntity.FromIndex(index)); // typedBoth 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 nullSource2ModEntities.FindEntityByClassname(start, "func_breakable") // NULL_ENTITY when none
Source2ModEntities.FindEntityByName(start, "door_main") // by targetname
Source2ModEntities.FindPickerEntity(controller) // under the crosshair
Source2ModEntities.GetGameRules() // the live CCSGameRulesThe Find* functions return one match at a time. Pass nullptr (NULL_ENTITY
in a script) to start and the previous match to continue:
int nBroken = 0;
CBaseEntity* pEntity = nullptr;
while ((pEntity = FIND_ENTITY_BY_CLASSNAME(pEntity, "func_breakable")) != nullptr)
{
pEntity->AcceptInput("Break");
nBroken++;
}int broken = 0;
CBaseEntity entity = NULL_ENTITY;
while ((entity = Source2ModEntities.FindEntityByClassname(entity, "func_breakable")) != NULL_ENTITY)
{
entity.AcceptInput("Break");
broken++;
}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. It
walks the entity system's active list directly. 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");ArrayList teams = Source2ModEntities.FindAllEntitiesByDesignerName("cs_team_manager");
for (int i = 0; i < teams.Length; i++)
{
CTeam team = teams.Get(i);
// ...
}
delete teams;
ArrayList props = Source2ModEntities.FindAllEntitiesByDesignerName("prop_dynamic");
for (int i = 0; i < props.Length; i++)
view_as<CDynamicProp>(props.Get(i)).AcceptInput("Kill");
delete props;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) and register it with ADD_ENTITY_LISTENER.
A script passes one function per callback to Source2ModEntities.AddEntityListener
(INVALID_FUNCTION for the ones it does not need):
class MyPlugin : public ISource2ModPlugin, public IEntityListener
{
void OnEntityCreated(CEntityInstance* pEntity) override;
void OnEntitySpawned(CEntityInstance* pEntity) override;
void OnEntityDeleted(CEntityInstance* pEntity) override;
void OnEntityParentChanged(CEntityInstance* pEntity, CEntityInstance* pNewParent) override;
};public void OnEntityCreated(CEntityInstance entity) {}
public void OnEntitySpawned(CEntityInstance entity) {}
public void OnEntityDeleted(CEntityInstance entity) {}
public void OnEntityParentChanged(CEntityInstance entity, CEntityInstance newParent) {}ADD_ENTITY_LISTENER(this); // in Load()int listener = Source2ModEntities.AddEntityListener(OnEntityCreated, OnEntitySpawned,
OnEntityDeleted, OnEntityParentChanged); // in OnPluginStart()OnEntityCreated is early — the keyvalues have not been applied and the entity
has not spawned. OnEntitySpawned is usually the one you want. Some entities
are filled in by the game right after they spawn, so a change made there can be
overwritten; the sample plugin waits one frame, through a handle:
void SamplePlugin::OnEntitySpawned(CEntityInstance* pEntity)
{
if (V_strcmp(pEntity->GetClassname(), "smokegrenade_projectile") != 0)
return;
CHandle<CSmokeGrenadeProjectile> hSmoke = static_cast<CSmokeGrenadeProjectile*>(pEntity)->GetHandle();
NEXT_FRAME([hSmoke]()
{
if (CSmokeGrenadeProjectile* pSmoke = hSmoke.Get())
pSmoke->m_vSmokeColor = Vector(170.0f, 0.0f, 255.0f);
});
}public void OnEntitySpawned(CEntityInstance entity)
{
char classname[64];
entity.GetClassname(classname, sizeof(classname));
if (!Source2ModUtils.Equal(classname, "smokegrenade_projectile"))
return;
// The entity is a handle: if the smoke is gone by the next frame,
// IsValid() says so.
Source2ModScheduler.NextFrame(PaintSmoke, entity);
}
public void PaintSmoke(any data)
{
CSmokeGrenadeProjectile smoke = view_as<CSmokeGrenadeProjectile>(data);
if (!smoke.IsValid())
return;
float purple[3] = { 170.0, 0.0, 255.0 };
smoke.Set_m_vSmokeColor(purple);
}AddEntityListener(IEntityListener* pListener) takes no plugin id; the core
works out which plugin the listener belongs to from the object's vtable, i.e.
the library its class lives in. The listener is removed for you when that
plugin unloads; REMOVE_ENTITY_LISTENER(this) removes it earlier. In a script,
the id AddEntityListener returns is what Source2ModEntities.RemoveEntityListener
takes, and the listener also goes when the plugin unloads.
Entities
How CS2 models players and other entities, and how the SDK exposes them as typed schema classes you read, write and call helpers on.
Schema Fields
Reading and writing entity fields, when a change reaches clients on its own and when you call NetworkStateChanged, embedded objects, pointers, arrays and vectors, and extending the classes.