ISource2ModHookContexts

What the handlers of the typed hook points get (ISource2ModHooks.h).

Every point hands its handler a context: the hooked object, the arguments, and result where the function returns something. What the handler returns is the usual Action:

  • Ignore -- nothing changed.
  • Override -- pre: the original still runs but the function returns ctx.result; post: the function returns ctx.result instead of what the original produced.
  • Supersede -- pre only: the original does not run, the function returns ctx.result. Stops the remaining pre handlers.

Arguments passed by pointer (the damage info, the move data, the weapon) can be changed in place and the original sees the change; arguments passed by value (a speed, a flag) cannot -- HookPoint::Function (FunctionContext) can. Handlers run in registration order, pre before the original and post after it; in post, result holds what the original returned.

The game-function points are one detour per function, placed by the core from its gamedata the first time a plugin asks for it and kept until shutdown (a detour taken out and put back would miss calls while KHook inserts it again). When Valve changes one -- an argument added, the order swapped between platforms, as GroundAccelerate's is -- the core's gamedata and its one call site change, and the plugins keep running.

Header source2mod/ISource2ModHookContexts.h

TakeDamageContext

CBaseEntity::TakeDamageOld(info, result) -> int64

Fields

FieldType
entityCBaseEntity*
infoCTakeDamageInfo*
damageResultCTakeDamageResult*
resultstd::int64_t

CanAcquireContext

CCSPlayer_ItemServices::CanAcquire(item, method, unk) -> AcquireResult

Fields

CanMoveContext

CCSPlayerPawnBase::CanMove() -> bool; false while frozen, defusing, ...

Fields

FieldType
pawnCCSPlayerPawnBase*
resultbool

CanUseContext

CCSPlayer_WeaponServices::CanUse(weapon) -> bool

Fields

FieldType
servicesCCSPlayer_WeaponServices*
weaponCBasePlayerWeapon*
resultbool

PostThinkContext

CCSPlayerPawn::PostThink()

Fields

FieldType
pawnCCSPlayerPawnBase*

ProcessUsercmdsContext

CCSPlayerController::ProcessUserCmd(cmds, count, paused, margin) -> void*. The commands are CUserCmd (a protobuf-backed type), left untyped here.

Fields

FieldType
controllerCCSPlayerController*
cmdsvoid*
countint
pausedbool
marginfloat
resultvoid*

SimulateUserCommandsContext

CBasePlayerController::OnSimulateUserCommands()

Fields

FieldType
controllerCBasePlayerController*

RunCommandContext

CPlayer_MovementServices::RunCommand(cmd), on CCSPlayer_MovementServices' vtable; the command is a CUserCmd.

Fields

FieldType
servicesCCSPlayer_MovementServices*
cmdvoid*

AcceptInputContext

CEntityIdentity::AcceptInput(name, activator, caller, value, ...) -> bool

Fields

FieldType
identityCEntityIdentity*
inputNameCUtlSymbolLarge*
activatorCEntityInstance*
callerCEntityInstance*
valuevariant_t*
resultbool

TouchContext

CBaseEntity::Touch(other), on CBaseEntity's own vtable -- an entity class that overrides Touch is not seen here.

Fields

FieldType
entityCBaseEntity*
otherCBaseEntity*

DropWeaponContext

CCSPlayer_WeaponServices::DropWeapon(weapon, target, velocity)

Fields

FieldType
servicesCCSPlayer_WeaponServices*
weaponCBasePlayerWeapon*
targetVector*
velocityVector*

MovementContext

The CCSPlayer_MovementServices functions that take the move data alone: AirMove, CheckFalling, CheckParameters, Duck, Friction, PlayerMove, ProcessMovement, WalkMove, WaterMove (void), and CanUnduck, CheckWater, LadderMove, MoveInit (bool, in result).

Fields

FieldType
servicesCCSPlayer_MovementServices*
moveDataCMoveData*
resultbool

AirAccelerateContext

CCSPlayer_MovementServices::AirAccelerate(move, wishDirection, wishSpeed, acceleration)

Fields

FieldType
servicesCCSPlayer_MovementServices*
moveDataCMoveData*
wishDirectionVector*
wishSpeedfloat
accelerationfloat

GroundAccelerateContext

CCSPlayer_MovementServices::GroundAccelerate(move, wishDirection, frameTime, wishSpeed, acceleration). The engine's argument order differs between Linux and Windows; here it is always this one.

Fields

FieldType
servicesCCSPlayer_MovementServices*
moveDataCMoveData*
wishDirectionVector*
frameTimefloat
wishSpeedfloat
accelerationfloat

CategorizePositionContext

CCSPlayer_MovementServices::CategorizePosition(move, stayOnGround)

Fields

FieldType
servicesCCSPlayer_MovementServices*
moveDataCMoveData*
stayOnGroundbool

CheckVelocityContext

CCSPlayer_MovementServices::CheckVelocity(move, unk)

Fields

FieldType
servicesCCSPlayer_MovementServices*
moveDataCMoveData*
unkvoid*

FullWalkMoveContext

CCSPlayer_MovementServices::FullWalkMove(move, onGround)

Fields

FieldType
servicesCCSPlayer_MovementServices*
moveDataCMoveData*
onGroundbool

SetupMoveContext

CCSPlayer_MovementServices::SetupMove(cmd, move); the command is a CUserCmd.

Fields

FieldType
servicesCCSPlayer_MovementServices*
cmdCUserCmd*
moveDataCMoveData*

TryPlayerMoveContext

CCSPlayer_MovementServices::TryPlayerMove(move, firstDest, firstTrace, isSurfing)

Fields

FieldType
servicesCCSPlayer_MovementServices*
moveDataCMoveData*
firstDestVector*
firstTraceCGameTrace*
isSurfingbool*

LegacyJumpContext

CCSPlayerLegacyJump::OnJump / CheckJumpButton(move)

Fields

FieldType
jumpCCSPlayerLegacyJump*
moveDataCMoveData*

ModernJumpContext

CCSPlayerModernJump::OnJump / CheckJumpButton(move)

Fields

FieldType
jumpCCSPlayerModernJump*
moveDataCMoveData*

PlayerChatContext

Host_Say(controller, args, teamOnly, ...): a say / say_team line the game is about to print and send. Lines a silent chat trigger hid for a Source2Mod command never get here; "!command" lines do (they stay in chat). Pre: Supersede drops the line (no chat, no player_say event). Source2Mod does not fire a player_chat game event (CounterStrikeSharp does, from its own Host_Say hook): this point is the way to see chat.

Fields

FieldTypeDescription
controllerCCSPlayerController*nullptr: the server console
argsCCommand*the say command; Arg(1) is the line
messageconst char*the line (Arg(1), or ArgS() for an unquoted one), never nullptr
teamOnlyboolsay_team
unk1int
unk2const char*

EntityUseInfo

CBaseEntity::Use's argument (activator, caller, USE_TYPE, value).

Fields

FieldType
activatorCBaseEntity*
callerCBaseEntity*
useTypestd::int32_t
valuefloat

SpawnContext

CBaseEntity::Spawn(keyValues) -- the entity's keyvalues are applied here.

Fields

FieldType
entityCBaseEntity*
keyValuesconst CEntityKeyValues*

UseContext

CBaseEntity::Use(info) -- a player's +use, a map's Use input. Changing *info in pre changes what the entity gets.

Fields

FieldType
entityCBaseEntity*
infoEntityUseInfo*

EntityOtherContext

CBaseEntity::StartTouch / EndTouch / Blocked(other)

Fields

FieldType
entityCBaseEntity*
otherCBaseEntity*

ThinkContext

CBaseEntity::Think() -- the entity's think function, every time it runs.

Fields

FieldType
entityCBaseEntity*

KilledContext

CBaseEntity::Event_Killed(damage) -> bool -- health reached 0 and m_lifeState is already "dying". Supersede skips the class's death handling (a player's ragdoll and death, a breakable's gibs).

Fields

FieldType
entityCBaseEntity*
damageconst CTakeDamageResult*
resultbool

WeaponPickupContext

CCSPlayer_WeaponServices::PickupWeapon(weapon) -> 0 refused, 1 taken, 2 taken as ammo -- touching or +using a weapon (SDKHooks' WeaponEquip / WeaponCanUse). Every player: HookTarget::All() works.

Fields

FieldType
servicesCCSPlayer_WeaponServices*
weaponCBasePlayerWeapon*
resultstd::int32_t

WeaponSwitchContext

CCSPlayer_WeaponServices::SwitchToWeapon(weapon, viewModelIndex) -> bool (SDKHooks' WeaponSwitch). Every player: HookTarget::All() works.

Fields

FieldType
servicesCCSPlayer_WeaponServices*
weaponCBasePlayerWeapon*
viewModelIndexstd::int32_t
resultbool

GameFrameContext

ISource2Server::GameFrame -- every tick.

Fields

FieldType
simulatingbool
firstTickbool
lastTickbool

StartupServerContext

INetworkServerService::StartupServer -- a new map's server is up.

Fields

FieldType
configconst GameSessionConfiguration_t*
sessionISource2WorldSession*
mapNameconst char*

ClientConnectContext

ISource2GameClients::ClientConnect. Pre only means anything: answer { Action::Supersede, false } (or set ctx.result = false and Supersede) to turn the client away, with the reason in ctx.rejectReason. The first handler that refuses ends it. Not called for bots.

Fields

FieldType
slotCPlayerSlot
nameconst char*
xuiduint64
networkIdconst char*
unk1bool
rejectReasonCBufferString*
resultbool

Return

using Return = bool;

ClientConnectedContext

ISource2GameClients::OnClientConnected -- bots too (fakePlayer).

Fields

FieldType
slotCPlayerSlot
nameconst char*
xuiduint64
networkIdconst char*
addressconst char*
fakePlayerbool

ClientPutInServerContext

ISource2GameClients::ClientPutInServer.

Fields

FieldType
slotCPlayerSlot
nameconst char*
typeint
xuiduint64

ClientDisconnectContext

ISource2GameClients::ClientDisconnect: pre while the client is still in game, post once it is gone.

Fields

FieldType
slotCPlayerSlot
reasonENetworkDisconnectionReason
nameconst char*
xuiduint64
networkIdconst char*

ClientSlotContext

ClientVoice, ClientSettingsChanged: which client.

Fields

FieldType
slotCPlayerSlot

EmptyContext

GameServerSteamAPIActivated / Deactivated: nothing to pass.

LoadEventsFromFileContext

IGameEventManager2::LoadEventsFromFile -- a resource file of game events.

Fields

FieldType
managerIGameEventManager2*
filenameconst char*
searchAllbool

Enums

AcquireResult

enum AcquireResult : std::uint32_t

What CCSPlayer_ItemServices::CanAcquire answers.

NameValue
Allowed0
InvalidItem
AlreadyOwned
AlreadyPurchased
ReachedGrenadeTypeLimit
ReachedGrenadeTotalLimit
NotAllowedByTeam
NotAllowedByMap
NotAllowedByMode
NotAllowedForPurchase
NotAllowedByProhibition

AcquireMethod

enum AcquireMethod : std::uint32_t

How an item is being acquired in CCSPlayer_ItemServices::CanAcquire.

NameValue
PickUp0
Buy
BuyWithCtrl

Macros

SOURCE2MOD_HOOK_CALL_ORIGINAL

#define SOURCE2MOD_HOOK_CALL_ORIGINAL(RET) \
    using Return = RET;                                                   
    RET CallOriginal() const { return callOriginal_(callOriginalData_); } 
    RET (*callOriginal_)(const void*) = nullptr;                          
    const void* callOriginalData_ = nullptr;

Every context ends with CallOriginal(): the game's function, run now with the arguments the game passed, past every hook on it -- SourceHook's SH_CALL. What it returns is what the game would have answered.

In a pre handler the function really runs (with all its side effects), so a handler that calls it should answer Supersede with ctx.result set to the value, or the game runs it a second time after the handlers:

const AcquireResult game = ctx.CallOriginal(); ctx.result = game == AcquireResult::Allowed && Blocked(ctx) ? AcquireResult::NotAllowedByMode : game; return Action::Supersede;

A changed pointed-to object (the damage info, the move data) counts; a scalar field changed in the context does not, the call takes what the game passed. In a post handler ctx.result already holds the game's answer.

The two members after it belong to the core; the movement contexts shared by void and bool functions answer false for the void ones.

On this page