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 TakeDamage

CBaseEntity::TakeDamageOld is the hook point HookPoint::TakeDamage (HookPoint_TakeDamage): the core hooks it from its own gamedata and hands every handler a TakeDamageContext — entity, info, damageResult and, for the return value, result. Hook every entity, one entity (SourceMod's SDKHook(entity, SDKHook_OnTakeDamage, …)), or a class:

plugin.cpp
#include "source2mod/ISource2ModHooks.h"
#include "source2mod/schema/takedamageinfo.h"
#include "source2mod/schema/takedamageresult.h"

bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
    SOURCE2MOD_SAVEVARS();

    // Pre, every entity. HookTarget::Instance(pPawn) would be one pawn,
    // HookTarget::Class("CCSPlayerPawn") every player pawn.
    g_pSource2ModHooks->Hook(HookPoint::TakeDamage, HookTarget::All(), SOURCE2MOD_MEMBER(this, &MyPlugin::OnTakeDamage));
    return true;
}

The hooked entity is ctx.entity — for player damage that is the victim's pawn (designer name player), not the controller. The script includes have no DMG_* constants, so define the bits you test:

plugin.cpp
HookReturn<TakeDamageContext::Return> MyPlugin::OnTakeDamage(TakeDamageContext& ctx, bool post)
{
    if (!ctx.info)
        return Action::Ignore;

    // No fall damage.
    if (ctx.info->m_bitsDamageType & DMG_FALL)
        return { Action::Supersede, 0 };

    // Halve everything else. This is a pre hook: the original runs after
    // it, with whatever the info holds by then.
    ctx.info->m_flDamage *= 0.5f;

    return Action::Ignore;
}
  • Ignore — the original runs (with your changes to the info).
  • 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 std::int64_t dealt = ctx.CallOriginal();
// ctx.damageResult is filled in now
return { Action::Supersede, dealt };

Finding the players involved — the attacker handle points at a pawn:

CCSPlayerController* pVictim = nullptr;
if (!V_strcmp(ctx.entity->GetClassname(), "player"))
    pVictim = static_cast<CCSPlayerPawn*>(ctx.entity)->GetController();

CCSPlayerController* pAttacker = nullptr;
if (CBaseEntity* pEnt = ctx.info->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