Skip to content

Instantly share code, notes, and snippets.

@Rainyan
Last active December 19, 2024 01:19
Show Gist options
  • Select an option

  • Save Rainyan/75c5de1ca3bd13405ea54014979ae63b to your computer and use it in GitHub Desktop.

Select an option

Save Rainyan/75c5de1ca3bd13405ea54014979ae63b to your computer and use it in GitHub Desktop.
CTakeDamageInfo example with OnTakeDamage hook for Neotokyo. Requires SM 1.12 or newer.
#include <dhooks>
#include <entity>
#pragma semicolon 1
#pragma newdecls required
// Returns n bytes worth of bits.
Address Bytes(any n)
{
any sizeof_ptr = 4;
return n * sizeof_ptr;
}
methodmap DamageInfo {
// What's going on here?
// Turns out at least currently, you can't exactly store data
// in the methodmap easily. So, as a workaround, we're instantiating
// this object as the address to the real CTakeDamageInfo by just
// coercing it as if it were so. Then, inside the implementations,
// we can just cast view_as<Address>(this) to access that address as needed.
public DamageInfo(Address a) {
return view_as<DamageInfo>(a);
}
// And here is the accessor for the pointer we instantiated with,
// as described above. It's kind of hacky but I guess this works?
property Address Ptr {
public get() {
return view_as<Address>(this);
}
}
/**
Memory offsets to interesting properties
*/
// Beginning of data values from the underlying CTakeDamageInfo pointer
property Address Base {
public get() {
return this.Ptr + Bytes(4);
}
}
property Address OffsetInflictor {
public get() {
return this.Base + Bytes(9);
}
}
property Address OffsetAttacker {
public get() {
return this.Base + Bytes(10);
}
}
property Address OffsetDamage {
public get() {
return this.Base + Bytes(11);
}
}
property Address OffsetBaseDamage {
public get() {
return this.Base + Bytes(12);
}
}
/**
Property Accessors
*/
property int Inflictor {
public get() {
return LoadEntityFromHandleAddress(this.OffsetInflictor);
}
}
property int Attacker {
public get() {
return LoadEntityFromHandleAddress(this.OffsetAttacker);
}
}
property float Damage {
public get() {
return LoadFromAddress(this.OffsetDamage, NumberType_Int32);
}
// Can also declare setters (as opposed to getters) for stuff
public set(float v) {
StoreToAddress(this.OffsetDamage, v, NumberType_Int32);
}
}
property float BaseDamage {
public get() {
return LoadFromAddress(this.OffsetBaseDamage, NumberType_Int32);
}
public set(float v) {
StoreToAddress(this.OffsetBaseDamage, v, NumberType_Int32);
}
}
};
static DynamicHook _dh_OnPlayerTakeDmg;
static bool _late;
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
_late = late;
return APLRes_Success;
}
public void OnPluginStart()
{
// Grabs the offset from:
// https://github.com/alliedmodders/sourcemod/blob/master/gamedata/sdkhooks.games/game.neotokyo.txt#L17
// This is better practice than hardcoding, especially for multi-game compatible plugins.
GameData gd = new GameData("sdkhooks.games/game.neotokyo");
if (!gd)
{
SetFailState("Failed to read GameData");
}
int offset = gd.GetOffset("OnTakeDamage");
if (!offset)
{
SetFailState("Failed to find offset");
}
delete gd;
// HACK: This doesn't actually return void, but since the int retval
// is practically unused (at least for NT), we can declare it as such
// for simplicity. The real prototype would use ReturnType_Int, and we'd
// then adjust the OnTakeDamage callback prototype accordingly.
_dh_OnPlayerTakeDmg = new DynamicHook(offset, HookType_Entity,
ReturnType_Void, ThisPointer_CBaseEntity);
if (!_dh_OnPlayerTakeDmg)
{
SetFailState("Failed to create dynamic hook");
}
// This is the CTakeDamageInfo param
_dh_OnPlayerTakeDmg.AddParam(HookParamType_ObjectPtr);
// For easier reload when debugging
if (_late)
{
for (int client = 1; client <= MaxClients; ++client)
{
if (IsClientInGame(client))
{
OnClientPutInServer(client);
}
}
}
}
public void OnClientPutInServer(int client)
{
// Pre-hook, so we can block dmg for debug, but this could also be post.
if (INVALID_HOOK_ID ==
_dh_OnPlayerTakeDmg.HookEntity(Hook_Pre, client, OnTakeDamage))
{
SetFailState("Failed to hook entity");
}
}
// If this returns weird duplicates whilst shooting friendly bots for debug,
// it's probably from the mirrored ff damage feedback thing.
MRESReturn OnTakeDamage(int pThis, DHookParam hParams)
{
// The prototype is:
// virtual int OnTakeDamage( const CTakeDamageInfo &info );
// So first param holds a pointer to CTakeDamageInfo.
// Note that we're treating it as if returning void on purpose here,
// because the retval does nothing useful for us for this function, afaik.
Address ptr_damageinfo = hParams.GetAddress(1);
// Call the methodmap constructor with the pointer we got.
// This gives us a nice(?) interface for accessing the pointer data.
DamageInfo info = DamageInfo(ptr_damageinfo);
// Just as a silly example of setting values
float crit_chance = 1.0 / 3;
bool do_crit = (GetURandomFloat() <= crit_chance);
if (do_crit)
{
info.Damage *= 2; // double damage
PrintToChatAll("Crit!");
}
PrintToChatAll("OnTakeDamage %d -> (attacker %d, inflictor %d) \
inflicting (%f dmg, %f basedmg, ??? maxdmg)",
pThis,
info.Attacker, info.Inflictor,
info.Damage, info.BaseDamage
//, info.MaxDamage TODO: unimplemented
);
RequestFrame(DeferHealthCheck, GetClientUserId(pThis));
return do_crit ? MRES_Handled : MRES_Ignored;
}
// For debug, just check what happened after exiting the hook
void DeferHealthCheck(int userid)
{
int client = GetClientOfUserId(userid);
if (client)
{
PrintToChatAll("Client %d reports %d HP", client, GetClientHealth(client));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment