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:

FieldTypeMeaning
m_flDamagefloatDamage to apply. Change it in a Pre hook to scale the hit.
m_bitsDamageTypeint32_tDMG_* bits: DMG_BULLET, DMG_FALL, DMG_BLAST, DMG_BURN, …
m_hAttackerCHandle<CBaseEntity>Who is credited — for players, their pawn.
m_hInflictorCHandle<CBaseEntity>What did it — the pawn for a bullet, the projectile for a grenade.
m_hAbilityCHandle<CBaseEntity>The weapon, when there is one.
m_vecDamagePosition, m_vecDamageForce, m_vecDamageDirection, m_vecReportedPositionVectorWhere and which way.
m_nDamageFlagsTakeDamageFlags_tDFLAG_* flags, e.g. DFLAG_SUPPRESS_DAMAGE_MODIFICATION, DFLAG_PREVENT_DEATH.
m_iHitGroupIdHitGroup_tHit group field.
m_flOriginalDamagefloatDamage before modification.
m_pTraceCGameTrace*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)
    // ...

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:

plugin.h
#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);
};
plugin.cpp
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;
}

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:

plugin.cpp
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 };
}
  • Ignore — the original runs (with your changes to pInfo).
  • 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 };

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();

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);

It 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:

explosions.cpp
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.

On this page