Last active
December 19, 2024 01:19
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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