Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,912 changes: 1,474 additions & 1,438 deletions managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml

Large diffs are not rendered by default.

145 changes: 145 additions & 0 deletions managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using Xunit;

public class ListenerTests
Expand Down Expand Up @@ -37,4 +42,144 @@ public async Task CanRegisterAndDeregisterListeners()

NativeAPI.IssueServerCommand("bot_quota 1");
}

[Fact]
public async Task TakeDamageListenersAreFired()
{
int preCallCount = 0;
int postCallCount = 0;

var preCallback = FunctionReference.Create((IntPtr entityPtr, IntPtr damageInfoPtr) => { preCallCount++; });

var postCallback = FunctionReference.Create((IntPtr entityPtr, IntPtr damageInfoPtr) => { postCallCount++; });

NativeAPI.AddListener("OnEntityTakeDamagePre", preCallback);
NativeAPI.AddListener("OnEntityTakeDamagePost", postCallback);

// Spawn a bot and deal damage to it
NativeAPI.IssueServerCommand("bot_kick");
NativeAPI.IssueServerCommand("bot_add");
await WaitOneFrame();

var player = Utilities.GetPlayers().FirstOrDefault(p => p.IsBot);
var playerHealth = player.PlayerPawn.Value.Health;
DealDamageFunc(player, player, 10);

await WaitOneFrame();
Assert.Equal(player.PlayerPawn.Value.Health, playerHealth - 10);

Assert.Equal(1, preCallCount);
Assert.Equal(1, postCallCount);

NativeAPI.RemoveListener("OnEntityTakeDamagePre", preCallback);
NativeAPI.RemoveListener("OnEntityTakeDamagePost", postCallback);
}

[Fact]
public async Task TakeDamageListenerCanBeCancelled()
{
int preCallCount = 0;
int postCallCount = 0;

Listeners.OnEntityTakeDamagePre preCallback = (entityPtr, damageInfoPtr) =>
{
preCallCount++;
return HookResult.Stop;
};

Listeners.OnEntityTakeDamagePre secondCallback = (entityPtr, damageInfoPtr) =>
{
preCallCount++;
return HookResult.Continue;
};

Listeners.OnEntityTakeDamagePost postCallback = (entity, damageInfo, damageResult) => { postCallCount++; };

NativeAPI.AddListener("OnEntityTakeDamagePre", preCallback);
NativeAPI.AddListener("OnEntityTakeDamagePre", secondCallback);
NativeAPI.AddListener("OnEntityTakeDamagePost", postCallback);

// Spawn a bot and deal damage to it
NativeAPI.IssueServerCommand("bot_kick");
NativeAPI.IssueServerCommand("bot_add");
await WaitOneFrame();

var player = Utilities.GetPlayers().FirstOrDefault(p => p.IsBot);
var playerHealth = player.PlayerPawn.Value.Health;
DealDamageFunc(player, player, 10);

await WaitOneFrame();
Assert.Equal(player.PlayerPawn.Value.Health, playerHealth);

Assert.Equal(1, preCallCount);
Assert.Equal(0, postCallCount);

NativeAPI.RemoveListener("OnEntityTakeDamagePre", preCallback);
NativeAPI.RemoveListener("OnEntityTakeDamagePre", secondCallback);
NativeAPI.RemoveListener("OnEntityTakeDamagePost", postCallback);
}

private static void DealDamageFunc(CCSPlayerController attacker, CCSPlayerController victim, int damage,
object data = null, DamageTypes_t type = DamageTypes_t.DMG_ENERGYBEAM)
{
var size = Schema.GetClassSize("CTakeDamageInfo");
var ptr = Marshal.AllocHGlobal(size);

for (var i = 0; i < size; i++)
Marshal.WriteByte(ptr, i, 0);

var damageInfo = new CTakeDamageInfo(ptr);
var attackerInfo = new AttackerInfo_t()
{
AttackerPawn = attacker.Pawn.Raw, AttackerPlayerSlot = attacker.Slot,
IsPawn = true, NeedInit = true,
};

Marshal.StructureToPtr(attackerInfo, new IntPtr(ptr.ToInt64() + 0x88), false);

if (attacker.Team == victim.Team)
attacker = victim;

Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo", "m_hInflictor",
attacker.PawnIsAlive ? attacker.Pawn.Raw : attacker.PlayerPawn.Raw);
Schema.SetSchemaValue(damageInfo.Handle, "CTakeDamageInfo", "m_hAttacker", attacker.Pawn.Raw);

damageInfo.Damage = damage;
damageInfo.BitsDamageType = type;
if (type == DamageTypes_t.DMG_ENERGYBEAM)
damageInfo.DamageFlags = TakeDamageFlags_t.DFLAG_IGNORE_ARMOR;

size = Schema.GetClassSize("CTakeDamageResult");
var ptr2 = Marshal.AllocHGlobal(size);

for (var i = 0; i < size; i++)
Marshal.WriteByte(ptr2, i, 0);

var damageResult = new CTakeDamageResult(ptr2);
Schema.SetSchemaValue(damageResult.Handle, "CTakeDamageResult", "m_pOriginatingInfo", damageInfo.Handle);

damageResult.HealthBefore = victim.PlayerPawn.Value.Health;
damageResult.HealthLost = damage;
damageResult.DamageDealt = damage;
damageResult.PreModifiedDamage = damage;
damageResult.TotalledHealthLost = damage;
damageResult.TotalledDamageDealt = damage;
damageResult.WasDamageSuppressed = false;

VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Invoke(victim.Pawn.Value, damageInfo, damageResult);
Marshal.FreeHGlobal(ptr);
Marshal.FreeHGlobal(ptr2);
}
}

[StructLayout(LayoutKind.Sequential)]
public struct AttackerInfo_t
{
public bool NeedInit;
public bool IsPawn;
public bool IsWorld;
public uint AttackerPawn;
public int AttackerPlayerSlot;
public int TeamChecked;
public int Team;
};
20 changes: 20 additions & 0 deletions managed/CounterStrikeSharp.Tests.Native/VirtualFunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,24 @@ public async Task ShouldHook()
mock.Verify(s => s(), Times.Never);
}
}

[Fact]
public async Task ShouldPreventPostHooks()
{
var mock = new Mock<Action>();

var preHookHandler = (DynamicHook hook) => { return HookResult.Stop; };

var postHookHandler = (DynamicHook hook) =>
{
mock.Object.Invoke();
return HookResult.Continue;
};

VirtualFunctions.CCSPlayerPawnBase_PostThinkFunc.Hook(preHookHandler, HookMode.Pre);
VirtualFunctions.CCSPlayerPawnBase_PostThinkFunc.Hook(postHookHandler, HookMode.Post);

await WaitOneFrame();
mock.Verify(s => s(), Times.Never, "Post hook should not be called if pre hook returns Stop.");
}
}
35 changes: 35 additions & 0 deletions src/core/detours.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include "core/log.h"
#include "pch.h"
#include "dynohook/core.h"
#include "dynohook/manager.h"

#include "core/globals.h"
#include "core/managers/entity_manager.h"

#include "scripting/script_engine.h"
namespace counterstrikesharp {

inline HookResult OnTakeDamageProxy(HookMode mode, dyno::Hook& hook)
{
auto* pThis = reinterpret_cast<CBaseEntity*>(hook.getArgument<void*>(0));
auto* pInfo = reinterpret_cast<CTakeDamageInfo*>(hook.getArgument<void*>(1));
auto* pResult = reinterpret_cast<CTakeDamageResult*>(hook.getArgument<void*>(2));

if (mode == Pre)
{
if (!globals::entityManager.Hook_OnTakeDamage_Alive_Pre(pThis, pInfo, pResult))
{
hook.setReturnValue(1);
return HookResult::Handled;
}
}
else
{
globals::entityManager.Hook_OnTakeDamage_Alive_Post(pThis, pInfo, pResult);
}

return HookResult::Continue;
}
} // namespace counterstrikesharp
108 changes: 103 additions & 5 deletions src/core/function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

#include "core/function.h"

#include <algorithm>

#include "core/log.h"
#include "dyncall/dyncall/dyncall.h"

Expand Down Expand Up @@ -251,9 +253,81 @@ void ValveFunction::Call(ScriptContext& script_context, int offset, bool bypass)

dyno::ReturnAction HookHandler(dyno::HookType hookType, dyno::Hook& hook)
{
auto vf = g_HookMap[&hook];
auto* vf = g_HookMap[&hook];

if (hookType == dyno::HookType::Pre)
{
auto* callback = vf->m_precallback;
auto global_callback = vf->m_callback;
HookResult maxResult = HookResult::Continue;

if (global_callback.has_value())
{
HookResult result = global_callback.value()(HookMode::Pre, hook);
maxResult = (std::max)(result, maxResult);
}

if (callback != nullptr)
{
callback->Reset();
callback->ScriptContext().Push(&hook);

auto callback = hookType == dyno::HookType::Pre ? vf->m_precallback : vf->m_postcallback;
for (auto fnMethodToCall : callback->GetFunctions())
{
if (!fnMethodToCall) continue;
fnMethodToCall(&callback->ScriptContextStruct());

auto result = callback->ScriptContext().GetResult<HookResult>();

maxResult = (std::max)(result, maxResult);

if (maxResult >= HookResult::Stop)
{
break;
}
}
}

// Store the pre-hook result for the post-hook to check
vf->m_lastPreHookResult.push_back(maxResult);

if (maxResult >= HookResult::Handled)
{
return dyno::ReturnAction::Supercede;
}

return dyno::ReturnAction::Ignored;
}

// Post hook
HookResult preResult = HookResult::Continue;
if (!vf->m_lastPreHookResult.empty())
{
preResult = vf->m_lastPreHookResult.back();
vf->m_lastPreHookResult.pop_back();
}

if (preResult >= HookResult::Handled)
{
return dyno::ReturnAction::Ignored;
}

auto* callback = vf->m_postcallback;
auto global_callback = vf->m_callback;

if (callback == nullptr && !global_callback.has_value())
{
return dyno::ReturnAction::Ignored;
}

if (global_callback.has_value())
{
HookResult result = global_callback.value()(HookMode::Post, hook);
if (result >= HookResult::Handled)
{
return dyno::ReturnAction::Supercede;
}
}

if (callback == nullptr)
{
Expand All @@ -263,20 +337,27 @@ dyno::ReturnAction HookHandler(dyno::HookType hookType, dyno::Hook& hook)
callback->Reset();
callback->ScriptContext().Push(&hook);

HookResult maxResult = HookResult::Continue;
for (auto fnMethodToCall : callback->GetFunctions())
{
if (!fnMethodToCall) continue;
fnMethodToCall(&callback->ScriptContextStruct());

auto result = callback->ScriptContext().GetResult<HookResult>();
CSSHARP_CORE_TRACE("Received hook callback result of {}, hook mode {}", result, (int)hookType);

if (result >= HookResult::Handled)
maxResult = (std::max)(result, maxResult);

if (maxResult >= HookResult::Stop)
{
return dyno::ReturnAction::Supercede;
break;
}
}

if (maxResult >= HookResult::Handled)
{
return dyno::ReturnAction::Supercede;
}

return dyno::ReturnAction::Ignored;
}

Expand All @@ -293,6 +374,23 @@ std::vector<dyno::DataObject> ConvertArgsToDynoHook(const std::vector<DataType_t
return converted;
}

void ValveFunction::AddHook(const std::function<HookResult(HookMode, dyno::Hook&)>& callback)
{
dyno::HookManager& manager = dyno::HookManager::Get();
dyno::Hook* hook = manager.hook((void*)m_ulAddr, [this] {
#ifdef _WIN32
return new dyno::x64MsFastcall(ConvertArgsToDynoHook(m_Args), static_cast<dyno::DataType>(this->m_eReturnType));
#else
return new dyno::x64SystemVcall(ConvertArgsToDynoHook(m_Args), static_cast<dyno::DataType>(this->m_eReturnType));
#endif
});
g_HookMap[hook] = this;
hook->addCallback(dyno::HookType::Post, (dyno::HookHandler*)&HookHandler);
hook->addCallback(dyno::HookType::Pre, (dyno::HookHandler*)&HookHandler);
m_trampoline = hook->getOriginal();
m_callback = callback;
}

void ValveFunction::AddHook(CallbackT callable, bool post)
{
dyno::HookManager& manager = dyno::HookManager::Get();
Expand Down
7 changes: 6 additions & 1 deletion src/core/function.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@
#include "scripting/callback_manager.h"
#include "scripting/script_engine.h"
#include <map>
#include <optional>

namespace dyno {
class Hook;
}
} // namespace dyno

namespace counterstrikesharp {

Expand Down Expand Up @@ -94,6 +95,7 @@ class ValveFunction
void SetSignature(const char* signature) { m_signature = signature; }

void Call(ScriptContext& args, int offset = 0, bool bypass = false);
void AddHook(const std::function<HookResult(HookMode, dyno::Hook&)>& callback);
void AddHook(CallbackT callable, bool post);
void RemoveHook(CallbackT callable, bool post);

Expand All @@ -112,6 +114,9 @@ class ValveFunction
const char* m_signature;
ScriptCallback* m_precallback = nullptr;
ScriptCallback* m_postcallback = nullptr;
std::optional<std::function<HookResult(HookMode, dyno::Hook&)>> m_callback;

std::vector<HookResult> m_lastPreHookResult;
};

} // namespace counterstrikesharp
Loading
Loading