The problem:
For this test case scenario, I have picked a situation in TF2. When you have the Sydney Sleeper equipped (or any sniper rifle with the jarate_duration attribute), you add the jarate effect to players you hit while scoped. How do you detect when this happens?
You won't be able to use TFCond_Jarate with TF2_OnConditionAdded, because that also catches regular jarate. So the only way to actually figure this out is to either recreate the logic from
CTFPlayer::OnTakeDamage_Alive or do a bunch of hook hierarchy shenanigans.
A
better way would be to deploy a midhook where the jarate effect is added under
CTFPlayer::OnTakeDamage_Alive.
The setup:
Setting up this hook will take a bit of elbow grease (as you are still dealing with assembly regardless). Fortunately, IDA has gifted us x86 decompilers so this becomes a bit easier.
Here is the decompiled code that issues the jarate TE effect and adds the jarate condition:
It would make the most sense to hook after the condition is added, but before that weird debug message further below it.
From within a midhook, we will have access to the memory around where the hook is, including the registers, the stack, frame pointer, etc. So, if we wanted to retrieve or change something in the CTakeDamageInfo parameter, we absolutely could. We just need to load it up from EBP +- its offset and then load up any member offsets from there.
Yes, this is slightly more advanced than you run-of-the-mill detour. Viewer discretion is advised.
The nitty gritty:
Let's say we want to get the
m_vecDamagePosition of the damage instance. We could of course use the victim's
m_vecOrigin but this is more fun.
We can deploy a midhook like mentioned before, but we need to get the address (and thus value of)
CTakeDamageInfo::m_vecDamagePosition.
There are a few rules with deploying a midhook.
1. Midhooks are always 5 bytes in size
2. Do not ever emplace a midhook where it will run over a label address (putting it at the start of one is fine).
3.
E8/
E9 instructions that the midhook jmp overwrites are fixed up automagically, so there is no need to worry about those.
Right here (0xF73452) is a nice place to insert the midhook. Any leftover bytes beyond the 5 used in the jmp are replaced with NOPs.
We set up the address to hook with the "Addresses" section in a gamedata file.
While we're here let's quickly grab the offset of the CTakeDamageInfo parameter from EBP. IDA pseudocode graciously gives this to us by hovering over the variable:
Pawnography:
Creating a midhook is pretty straightforward. We get an address from somewhere and call
new MidHook().
PHP Code:
MidHook g_Hook;
public void OnPluginStart()
{
GameData conf = new GameData("tf2.sleeper");
// Frankly, you don't need to cache the handle unless you plan on disabling the hook later at some point
g_Hook = new MidHook(conf.GetAddress("CTFPlayer::OnTakeDamage_Alive_jarate"), OnSydneySleeperJarate);
delete conf;
}
In the
OnSydneySleeperJarate callback, as mentioned before, you have a snapshot of all of the register values right where your midhook was deployed.
To get the
CTakeDamageInfo parameter address, we use
MidHookRegisters.Load(DHookRegister_EBP, 0xC).
To reach
m_vecDamagePosition, we add 0xC to that pointer then load x, y, and z.
And our code will look like this:
PHP Code:
public void OnSydneySleeperJarate(MidHookRegisters regs)
{
PrintToServer("Hello World!");
Address pInfo = regs.Load(DHookRegister_EBP, 0xC);
float vecPosition[3];
vecPosition[0] = LoadFromAddress(pInfo + view_as< Address >(0xC), NumberType_Int32);
vecPosition[1] = LoadFromAddress(pInfo + view_as< Address >(0x10), NumberType_Int32);
vecPosition[2] = LoadFromAddress(pInfo + view_as< Address >(0x14), NumberType_Int32);
PrintToServer("Took damage at %.2f %.2f %.2f", vecPosition[0], vecPosition[1], vecPosition[2]);
// Make an explosion or something with vecPosition
}
And it works! (Trust me).