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 returnsctx.resultinstead 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
| Field | Type |
|---|---|
entity | CBaseEntity* |
info | CTakeDamageInfo* |
damageResult | CTakeDamageResult* |
result | std::int64_t |
CanAcquireContext
CCSPlayer_ItemServices::CanAcquire(item, method, unk) -> AcquireResult
Fields
| Field | Type |
|---|---|
services | CCSPlayer_ItemServices* |
item | CEconItemView* |
method | AcquireMethod |
unk | void* |
result | AcquireResult |
CanMoveContext
CCSPlayerPawnBase::CanMove() -> bool; false while frozen, defusing, ...
Fields
| Field | Type |
|---|---|
pawn | CCSPlayerPawnBase* |
result | bool |
CanUseContext
CCSPlayer_WeaponServices::CanUse(weapon) -> bool
Fields
| Field | Type |
|---|---|
services | CCSPlayer_WeaponServices* |
weapon | CBasePlayerWeapon* |
result | bool |
PostThinkContext
CCSPlayerPawn::PostThink()
Fields
| Field | Type |
|---|---|
pawn | CCSPlayerPawnBase* |
ProcessUsercmdsContext
CCSPlayerController::ProcessUserCmd(cmds, count, paused, margin) -> void*. The commands are CUserCmd (a protobuf-backed type), left untyped here.
Fields
| Field | Type |
|---|---|
controller | CCSPlayerController* |
cmds | void* |
count | int |
paused | bool |
margin | float |
result | void* |
SimulateUserCommandsContext
CBasePlayerController::OnSimulateUserCommands()
Fields
| Field | Type |
|---|---|
controller | CBasePlayerController* |
RunCommandContext
CPlayer_MovementServices::RunCommand(cmd), on CCSPlayer_MovementServices' vtable; the command is a CUserCmd.
Fields
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
cmd | void* |
AcceptInputContext
CEntityIdentity::AcceptInput(name, activator, caller, value, ...) -> bool
Fields
| Field | Type |
|---|---|
identity | CEntityIdentity* |
inputName | CUtlSymbolLarge* |
activator | CEntityInstance* |
caller | CEntityInstance* |
value | variant_t* |
result | bool |
TouchContext
CBaseEntity::Touch(other), on CBaseEntity's own vtable -- an entity class that overrides Touch is not seen here.
Fields
| Field | Type |
|---|---|
entity | CBaseEntity* |
other | CBaseEntity* |
DropWeaponContext
CCSPlayer_WeaponServices::DropWeapon(weapon, target, velocity)
Fields
| Field | Type |
|---|---|
services | CCSPlayer_WeaponServices* |
weapon | CBasePlayerWeapon* |
target | Vector* |
velocity | Vector* |
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
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
moveData | CMoveData* |
result | bool |
AirAccelerateContext
CCSPlayer_MovementServices::AirAccelerate(move, wishDirection, wishSpeed, acceleration)
Fields
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
moveData | CMoveData* |
wishDirection | Vector* |
wishSpeed | float |
acceleration | float |
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
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
moveData | CMoveData* |
wishDirection | Vector* |
frameTime | float |
wishSpeed | float |
acceleration | float |
CategorizePositionContext
CCSPlayer_MovementServices::CategorizePosition(move, stayOnGround)
Fields
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
moveData | CMoveData* |
stayOnGround | bool |
CheckVelocityContext
CCSPlayer_MovementServices::CheckVelocity(move, unk)
Fields
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
moveData | CMoveData* |
unk | void* |
FullWalkMoveContext
CCSPlayer_MovementServices::FullWalkMove(move, onGround)
Fields
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
moveData | CMoveData* |
onGround | bool |
SetupMoveContext
CCSPlayer_MovementServices::SetupMove(cmd, move); the command is a CUserCmd.
Fields
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
cmd | CUserCmd* |
moveData | CMoveData* |
TryPlayerMoveContext
CCSPlayer_MovementServices::TryPlayerMove(move, firstDest, firstTrace, isSurfing)
Fields
| Field | Type |
|---|---|
services | CCSPlayer_MovementServices* |
moveData | CMoveData* |
firstDest | Vector* |
firstTrace | CGameTrace* |
isSurfing | bool* |
LegacyJumpContext
CCSPlayerLegacyJump::OnJump / CheckJumpButton(move)
Fields
| Field | Type |
|---|---|
jump | CCSPlayerLegacyJump* |
moveData | CMoveData* |
ModernJumpContext
CCSPlayerModernJump::OnJump / CheckJumpButton(move)
Fields
| Field | Type |
|---|---|
jump | CCSPlayerModernJump* |
moveData | CMoveData* |
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
| Field | Type | Description |
|---|---|---|
controller | CCSPlayerController* | nullptr: the server console |
args | CCommand* | the say command; Arg(1) is the line |
message | const char* | the line (Arg(1), or ArgS() for an unquoted one), never nullptr |
teamOnly | bool | say_team |
unk1 | int | |
unk2 | const char* |
EntityUseInfo
CBaseEntity::Use's argument (activator, caller, USE_TYPE, value).
Fields
| Field | Type |
|---|---|
activator | CBaseEntity* |
caller | CBaseEntity* |
useType | std::int32_t |
value | float |
SpawnContext
CBaseEntity::Spawn(keyValues) -- the entity's keyvalues are applied here.
Fields
| Field | Type |
|---|---|
entity | CBaseEntity* |
keyValues | const CEntityKeyValues* |
UseContext
CBaseEntity::Use(info) -- a player's +use, a map's Use input. Changing *info in pre changes what the entity gets.
Fields
| Field | Type |
|---|---|
entity | CBaseEntity* |
info | EntityUseInfo* |
EntityOtherContext
CBaseEntity::StartTouch / EndTouch / Blocked(other)
Fields
| Field | Type |
|---|---|
entity | CBaseEntity* |
other | CBaseEntity* |
ThinkContext
CBaseEntity::Think() -- the entity's think function, every time it runs.
Fields
| Field | Type |
|---|---|
entity | CBaseEntity* |
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
| Field | Type |
|---|---|
entity | CBaseEntity* |
damage | const CTakeDamageResult* |
result | bool |
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
| Field | Type |
|---|---|
services | CCSPlayer_WeaponServices* |
weapon | CBasePlayerWeapon* |
result | std::int32_t |
WeaponSwitchContext
CCSPlayer_WeaponServices::SwitchToWeapon(weapon, viewModelIndex) -> bool (SDKHooks' WeaponSwitch). Every player: HookTarget::All() works.
Fields
| Field | Type |
|---|---|
services | CCSPlayer_WeaponServices* |
weapon | CBasePlayerWeapon* |
viewModelIndex | std::int32_t |
result | bool |
GameFrameContext
ISource2Server::GameFrame -- every tick.
Fields
| Field | Type |
|---|---|
simulating | bool |
firstTick | bool |
lastTick | bool |
StartupServerContext
INetworkServerService::StartupServer -- a new map's server is up.
Fields
| Field | Type |
|---|---|
config | const GameSessionConfiguration_t* |
session | ISource2WorldSession* |
mapName | const 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
| Field | Type |
|---|---|
slot | CPlayerSlot |
name | const char* |
xuid | uint64 |
networkId | const char* |
unk1 | bool |
rejectReason | CBufferString* |
result | bool |
Return
using Return = bool;ClientConnectedContext
ISource2GameClients::OnClientConnected -- bots too (fakePlayer).
Fields
| Field | Type |
|---|---|
slot | CPlayerSlot |
name | const char* |
xuid | uint64 |
networkId | const char* |
address | const char* |
fakePlayer | bool |
ClientPutInServerContext
ISource2GameClients::ClientPutInServer.
Fields
| Field | Type |
|---|---|
slot | CPlayerSlot |
name | const char* |
type | int |
xuid | uint64 |
ClientDisconnectContext
ISource2GameClients::ClientDisconnect: pre while the client is still in game, post once it is gone.
Fields
| Field | Type |
|---|---|
slot | CPlayerSlot |
reason | ENetworkDisconnectionReason |
name | const char* |
xuid | uint64 |
networkId | const char* |
ClientSlotContext
ClientVoice, ClientSettingsChanged: which client.
Fields
| Field | Type |
|---|---|
slot | CPlayerSlot |
EmptyContext
GameServerSteamAPIActivated / Deactivated: nothing to pass.
LoadEventsFromFileContext
IGameEventManager2::LoadEventsFromFile -- a resource file of game events.
Fields
| Field | Type |
|---|---|
manager | IGameEventManager2* |
filename | const char* |
searchAll | bool |
Enums
AcquireResult
enum AcquireResult : std::uint32_tWhat CCSPlayer_ItemServices::CanAcquire answers.
| Name | Value |
|---|---|
Allowed | 0 |
InvalidItem | |
AlreadyOwned | |
AlreadyPurchased | |
ReachedGrenadeTypeLimit | |
ReachedGrenadeTotalLimit | |
NotAllowedByTeam | |
NotAllowedByMap | |
NotAllowedByMode | |
NotAllowedForPurchase | |
NotAllowedByProhibition |
AcquireMethod
enum AcquireMethod : std::uint32_tHow an item is being acquired in CCSPlayer_ItemServices::CanAcquire.
| Name | Value |
|---|---|
PickUp | 0 |
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.