Damage
CTakeDamageInfo and CTakeDamageResult, hooking CBaseEntity::TakeDamageOld to change or block damage, and dealing damage yourself.
All damage in CS2 — bullets, knives, grenades, falling, the world — goes
through one function, CBaseEntity::TakeDamageOld (the game's own asserts call
it that). It takes a CTakeDamageInfo describing the hit and a
CTakeDamageResult it fills in. Hook it to change or block damage; call it to
deal damage yourself.
CTakeDamageInfo
#include "source2mod/schema/takedamageinfo.h"
CTakeDamageInfo();
CTakeDamageInfo(CBaseEntity* pInflictor, CBaseEntity* pAttacker, CBaseEntity* pAbility,
float flDamage, DamageTypes_t bitsDamageType);
HitGroup_t GetHitGroup() const;Both constructors call the game's own constructor, so every field you do not set has the game's default. The fields you will use:
| Field | Type | Meaning |
|---|---|---|
m_flDamage | float | Damage to apply. Change it in a Pre hook to scale the hit. |
m_bitsDamageType | int32_t | DMG_* bits: DMG_BULLET, DMG_FALL, DMG_BLAST, DMG_BURN, … |
m_hAttacker | CHandle<CBaseEntity> | Who is credited — for players, their pawn. |
m_hInflictor | CHandle<CBaseEntity> | What did it — the pawn for a bullet, the projectile for a grenade. |
m_hAbility | CHandle<CBaseEntity> | The weapon, when there is one. |
m_vecDamagePosition, m_vecDamageForce, m_vecDamageDirection, m_vecReportedPosition | Vector | Where and which way. |
m_nDamageFlags | TakeDamageFlags_t | DFLAG_* flags, e.g. DFLAG_SUPPRESS_DAMAGE_MODIFICATION, DFLAG_PREVENT_DEATH. |
m_iHitGroupId | HitGroup_t | Hit group field. |
m_flOriginalDamage | float | Damage before modification. |
m_pTrace | CGameTrace* | The trace that caused the hit, when there is one. |
m_iDamageCustom, m_iAmmoType, m_bShouldBleed, m_bShouldSpark, m_AttackerInfo, … | See the reference. |
Scripts cannot construct one: they get it from the TakeDamage hook (ctx.info),
with these fields as properties — the vectors and m_nDamageFlags as methods
filling a buffer, the handles as entities.
GetHitGroup() reads the hit group from the attached hit data through a
gamedata offset; it returns HITGROUP_INVALID when that is unavailable and
HITGROUP_GENERIC when the hit carries no group. Use it rather than
m_iHitGroupId for "was this a headshot" (scripts compare to the number,
HITGROUP_HEAD is 1):
if (pInfo->GetHitGroup() == HitGroup_t::HITGROUP_HEAD)
// ...if (info.GetHitGroup() == 1) // HITGROUP_HEAD
{
// ...
}CTakeDamageResult
#include "source2mod/schema/takedamageresult.h"
CTakeDamageResult(float damage);
void CopyFrom(CTakeDamageInfo* pInfo);What the game fills in while applying damage: m_nHealthLost,
m_nHealthBefore, m_flDamageDealt, m_flPreModifiedDamage,
m_nTotalledHealthLost, m_flTotalledDamageDealt, m_bWasDamageSuppressed
and more. There is no default constructor — build it from a damage value, and
call CopyFrom(&info) to point it at your info and copy the damage over when
you call TakeDamageOld yourself.
See the reference for every field.
Hooking TakeDamageOld
ADDR_TAKE_DAMAGE_OLD() returns the resolved function; hook it with
KHOOK_MEMBER, which evaluates the address when KHOOK_INIT() runs. Scripts do
not hook it themselves: Source2ModGameHooks.HookTakeDamage adds a handler to
the hook the core already has on it, so the class declaration is C++ only:
#include "source2mod/ISource2ModAddresses.h"
#include "source2mod/ISource2ModHooks.h"
#include "source2mod/schema/takedamageinfo.h"
#include "source2mod/schema/takedamageresult.h"
class MyPlugin : public ISource2ModPlugin
{
public:
KHook::Return<int64_t> Hook_TakeDamageOld(CBaseEntity* pThis, CTakeDamageInfo* pInfo, CTakeDamageResult* pResult);
KHOOK_MEMBER(m_hTakeDamageOld, ADDR_TAKE_DAMAGE_OLD(), &MyPlugin::Hook_TakeDamageOld, nullptr);
};bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
SOURCE2MOD_SAVEVARS();
KHOOK_INIT();
return true;
}
bool MyPlugin::Unload(char* error, size_t maxlen)
{
KHOOK_DESTRUCT();
return true;
}public void OnPluginStart()
{
// The core hooks CBaseEntity::TakeDamageOld from its own gamedata;
// false = Pre. The hook goes when the plugin unloads.
Source2ModGameHooks.HookTakeDamage(OnTakeDamage, false);
}The hooked entity arrives first — for player damage that is the victim's
pawn (classname player), not the controller. The handler returns a
KHook::Return<int64_t>; a script handler gets a TakeDamageContext
(ctx.entity, ctx.info, ctx.damageResult) and returns a Source2ModAction,
with the function's return value in result. The script includes have no
DMG_* constants, so define the bits you test:
KHook::Return<int64_t> MyPlugin::Hook_TakeDamageOld(CBaseEntity* pThis, CTakeDamageInfo* pInfo, CTakeDamageResult* pResult)
{
if (!pInfo)
return { KHook::Action::Ignore, 0 };
// No fall damage.
if (pInfo->m_bitsDamageType & DMG_FALL)
return { KHook::Action::Supersede, 0 };
// Halve everything else. This is a Pre hook: the original runs after
// it, with whatever pInfo holds by then.
pInfo->m_flDamage *= 0.5f;
return { KHook::Action::Ignore, 0 };
}#define DMG_FALL (1 << 5) // DamageTypes_t; the includes have no DMG_* constants
public Source2ModAction OnTakeDamage(TakeDamageContext ctx, bool post, int &result)
{
CTakeDamageInfo info = ctx.info;
// No fall damage.
if (info.m_bitsDamageType & DMG_FALL)
{
result = 0;
return Action_Supersede;
}
// Halve everything else. This is a Pre hook: the original runs after
// it, with whatever the info holds by then.
info.m_flDamage *= 0.5;
return Action_Ignore;
}Ignore— the original runs (with your changes topInfo).Supersede— the original does not run: no damage at all.- To act on the result, run the original yourself and supersede so it does not run twice:
const int64_t ret = m_hTakeDamageOld.CallOriginal(pThis, pInfo, pResult);
// pResult is filled in now
return { KHook::Action::Supersede, ret };result = ctx.CallOriginal();
// ctx.damageResult is filled in now
return Action_Supersede;Finding the players involved — the attacker handle points at a pawn:
CCSPlayerController* pVictim = nullptr;
if (!V_strcmp(pThis->GetClassname(), "player"))
pVictim = static_cast<CCSPlayerPawn*>(pThis)->GetController();
CCSPlayerController* pAttacker = nullptr;
if (CBaseEntity* pEnt = pInfo->m_hAttacker.Get(); pEnt && !V_strcmp(pEnt->GetClassname(), "player"))
pAttacker = static_cast<CCSPlayerPawn*>(pEnt)->GetController();CTakeDamageInfo info = ctx.info;
char classname[64];
CCSPlayerController victim = NULL_ENTITY;
ctx.entity.GetClassname(classname, sizeof(classname));
if (Source2ModUtils.Equal(classname, "player"))
victim = view_as<CCSPlayerPawn>(ctx.entity).GetController();
CCSPlayerController attacker = NULL_ENTITY;
CBaseEntity attackerEnt = info.m_hAttacker;
if (attackerEnt != NULL_ENTITY)
{
attackerEnt.GetClassname(classname, sizeof(classname));
if (Source2ModUtils.Equal(classname, "player"))
attacker = view_as<CCSPlayerPawn>(attackerEnt).GetController();
}The hooks section covers handlers, CallOriginal and Post hooks in depth:
Hooking.
Dealing damage
Player to player
The simplest route is the controller helper:
void CCSPlayerController::TakeDamage(CCSPlayerController* pAttacker, int iDamage,
DamageTypes_t bitsDamageType,
HookChain eChain = HookChain::Bypass);pVictim->TakeDamage(pAttacker, 25, DMG_GENERIC);victim.TakeDamage(attacker, 25, 0); // 0 = DMG_GENERICIt builds the info with the attacker's pawn as inflictor and attacker, sets
DFLAG_SUPPRESS_DAMAGE_MODIFICATION so armor and scaling leave the number
alone, and calls TakeDamageOld on the victim's pawn. It needs both players
connected with a pawn and the victim alive; otherwise it does nothing.
By default it bypasses TakeDamageOld hooks — yours and other plugins' —
because it is the server dealing damage, not the game. Pass HookChain::Run
(HookChain_Run in a script)
when it stands in for a weapon and other plugins' damage rules should apply.
Anything else
For damage with no attacker player, a different inflictor, or fields the helper does not set, build the info yourself and call the function. The TroubleTerroristTown plugin's explosions do it like this:
CTakeDamageInfo info(nullptr, pAttackerPawn, nullptr, flDamage, DMG_BLAST);
info.m_vecReportedPosition = vecTargetOrigin;
CTakeDamageResult result(flDamage);
result.CopyFrom(&info);
SOURCE2MOD_ORIGINAL(ADDR_TAKE_DAMAGE_OLD())(pVictimPawn, &info, &result);SOURCE2MOD_ORIGINAL calls the function past every hook on it — the same as
HookChain::Bypass. Call ADDR_TAKE_DAMAGE_OLD()(…) without it to run the
hook chain. Resolve it at the call, not once into a global: the original
behind a hook only exists once the hook does.
This route is C++ only: scripts cannot build a CTakeDamageInfo, so they deal
damage through CCSPlayerController.TakeDamage.
Damage a player through their pawn. Calling TakeDamageOld on the
controller reaches an entity whose m_iHealth is 0.