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.
Almost everything on a CS2 server is an entity: players, weapons, grenades,
doors, triggers, the game rules, even a Panorama HUD layout. The SDK gives you
every one of them as a C++ class generated from the game's own schema, so you
work with CCSPlayerPawn* and pPawn->m_iHealth instead of raw offsets.
This section covers finding entities, reading and writing their fields, creating new ones, the common player helpers, entity I/O and damage.
Finding entities
Schema fields
Creating entities
Players
Entity I/O
Damage
Controller and pawn
A CS2 player is two entities:
| Entity | Class | Classname | Lives |
|---|---|---|---|
| Controller | CCSPlayerController | cs_player_controller | From connect to disconnect. Holds the name, SteamID, team, score, money. |
| Pawn | CCSPlayerPawn | player | The body in the world. Holds health, armor, position, weapons. Replaced or reused across deaths and respawns. |
The controller is the one to keep track of a player; the pawn is the one to act on when something happens to their body. They point at each other:
CCSPlayerPawn* pPawn = pController->GetPlayerPawn(); // m_hPlayerPawn
CCSPlayerController* pOwner = pPawn->GetController(); // m_hController
CCSPlayerController* pSame = CCSPlayerController::FromPawn(pPawn);CCSPlayerPawn pawn = controller.GetPlayerPawn(); // m_hPlayerPawn
CCSPlayerController owner = pawn.GetController(); // m_hController
CCSPlayerController same = CCSPlayerController.FromPawn(pawn);A dead or spectating player also has an observer pawn (CCSObserverPawn,
GetObserverPawn()). CBasePlayerController::GetPawn() returns m_hPawn,
whichever pawn the controller is currently driving, cast to CCSPlayerPawn* —
when you mean "the player's body", use GetPlayerPawn().
Health lives on the pawn. The controller has m_iPawnHealth and
m_bPawnIsAlive, which the game mirrors for the scoreboard; read them, but
change health on the pawn.
Services
A lot of behaviour is not on the pawn or controller itself but on service objects hanging off them as pointers:
| Pointer | Class | For |
|---|---|---|
pPawn->m_pItemServices() | CPlayer_ItemServices | GiveNamedItem, RemoveWeapons |
pPawn->m_pWeaponServices() | CPlayer_WeaponServices | m_hMyWeapons, m_hActiveWeapon, DropWeapon, SelectItem |
pController->m_pInGameMoneyServices() | CCSPlayerController_InGameMoneyServices | m_iAccount (money) |
Any of these can be null on a pawn that does not have them, so check before use.
In a script they are properties (pawn.m_pItemServices), and a missing one
reads IsNull.
Service fields are networked through the entity that owns them; writing one
works exactly like writing a field on the entity (see
Schema fields).
Indices, slots and handles
| Type | What it is | Get it |
|---|---|---|
CEntityIndex | Position in the entity list (.Get() for the int). Reused after the entity is gone. | pEntity->GetEntityIndex(), GetIndex() for the int |
CPlayerSlot | Player slot, 0-based. A controller's entity index is always slot + 1. | pController->GetPlayerSlot(), GetSlot() for the int |
CPlayerUserId | The engine's userid, as game events carry it. | pController->GetPlayerUserID(), GetUserID() for the int |
CHandle<T> | Index plus a serial number. Resolves to null once the entity is removed, even if the index is reused. | pEntity->GetHandle() |
The rule: a raw pointer is only good for the current call. Anything that
crosses a frame — a timer, a NEXT_FRAME lambda, a map in your plugin — holds a
CHandle and checks it when it comes back. A script has no raw pointers: every
entity value already is a handle, kept as it is and checked with IsValid():
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 RemindLater(CCSPlayerController controller)
{
// The entity value is a handle already: pass it on as it is.
Source2ModScheduler.AddTimer(3.0, Timer_Later, controller);
}
public void Timer_Later(any data)
{
CCSPlayerController player = view_as<CCSPlayerController>(data);
if (!player.IsValid())
return; // disconnected in the meantime
player.PrintToChat("Three seconds later");
}In C++, every generated entity class shadows GetHandle() with one returning a handle
of its own type, so pController->GetHandle() is a
CHandle<CCSPlayerController> without a template argument.
How the SDK exposes schema classes
The classes under source2mod/schema/entity/classes/ are generated from a
dump of the game's schema by tools/schemagen/generator.py, one header per
class, following the game's own inheritance
(CCSPlayerPawn → CCSPlayerPawnBase → CBasePlayerPawn → … → CBaseEntity
→ CEntityInstance). Each class carries:
- Fields — one
SCHEMA_FIELDper schema member. The offset is looked up by name in the running game's schema the first time you touch it, so a CS2 update that moves a field does not break your binary. - Helpers — hand-written methods (
GiveNamedItem,Teleport,PrintToChat, …) that call into the game. They are declared by the generator so they survive regeneration and implemented in the SDK'ssrc/schema/entity/classes/*.cpp. - Static constructors —
New(classname),FromIndex(index)and a typedGetHandle()on every entity class.
Scripts get the same classes as methodmaps of the same names and bases
(source2mod/schema/classes.inc, generated too): fields are properties
(pawn.m_iHealth), helpers are methods, and FromIndex is on CBaseEntity.
Include the header of the class you use; a script includes them all at once:
#include "source2mod/schema/entity/classes/CCSPlayerController.h"
#include "source2mod/schema/entity/classes/CCSPlayerPawn.h"#include <source2mod/api> // every schema class, as a methodmap of the same namedynamic_cast works on game types: the SDK checks the cast against the
engine's own RTTI and caches the answer, so a cast costs a few nanoseconds
(see Casting). When you
already know the type, static_cast is enough. In a script, check the
classname (GetClassname(), e.g. "cs_player_controller") and view_as the
methodmap.
The full list of classes, fields and helpers is in the schema reference; lookup and entity I/O functions are on ISource2ModEntities.