99 lines
5.8 KiB
C#
99 lines
5.8 KiB
C#
using System.Runtime.InteropServices;
|
|
using CounterStrikeSharp.API.Core;
|
|
using CounterStrikeSharp.API.Modules.Memory;
|
|
|
|
namespace Outnumbered.Engine;
|
|
|
|
// Deals real, attributed damage as if `source` dealt it to `target`, returning the actual damage dealt. By default the hit
|
|
// re-enters the offense hook so it scales with the source's build + handicap (Explode-on-Kill relies on this) and fires
|
|
// EventPlayerDeath on a kill (proper credit / GG-rung / killstreak). A TakeDamageOld invoke does NOT raise player_hurt, so
|
|
// EventPlayerHurt effects (lifesteal, per-hit XP) don't trigger — callers apply those on the returned amount. Mirrors the
|
|
// CSSharp reference: a zeroed CTakeDamageInfo with AttackerInfo_t at +0x88, plus a CTakeDamageResult, fed to TakeDamageOld.
|
|
// raw = a FLAT, armor-skipping BURN hit (DoT): DFLAG_IGNORE_ARMOR + DMG_BURN.
|
|
// flat = a normal bullet that respects armor but is NOT scaled (thorns: the bot eats exactly what was computed).
|
|
// Both set Unscaled so the offense hook applies `damage` verbatim; attribution is still kept so a kill credits the source.
|
|
internal static class DamageDealer
|
|
{
|
|
// TRUE around an unscaled invoke (raw burn DoT or flat thorns reflect): the offense hook (OnEntityTakeDamagePre) returns
|
|
// immediately when set, so `damage` lands verbatim — no build/handicap scaling, no crit roll. Toggled inside Deal only.
|
|
internal static bool Unscaled;
|
|
|
|
// Byte offset of AttackerInfo_t inside CTakeDamageInfo (the CSSharp Tests.Native layout). A build-volatile fixup point
|
|
// — if the struct layout shifts on a CS2 update, this is the one-line change (kept here with the marshalling it serves).
|
|
private const int AttackerInfoOffset = 0x88;
|
|
|
|
// Cached all-zero source for bulk-clearing the two unmanaged buffers (one memcpy each vs N per-byte WriteByte interop
|
|
// crossings). Grows once to the largest class size; never written so it stays zero. Game-thread only, Deal not re-entrant.
|
|
private static byte[] _zero = [];
|
|
|
|
// The two unmanaged buffers, REUSED across calls instead of AllocHGlobal/FreeHGlobal per Deal (hot in burn waves: per
|
|
// burn-tick-per-bot, per thorns hit, per explode). Safe because Deal is game-thread + NOT re-entrant — the synchronous
|
|
// player_death its Invoke can fire only REMOVES burns (ClearBurnsForBot), and explode-on-kill is deferred a frame; no
|
|
// path re-enters Deal before it returns. Grow-only to the class size; never freed (process-lifetime); re-zeroed each call.
|
|
private static IntPtr _infoBuf, _resBuf;
|
|
private static int _infoCap, _resCap;
|
|
|
|
internal static float Deal(CCSPlayerController target, CCSPlayerController source, float damage, bool raw = false, bool flat = false)
|
|
{
|
|
if (damage <= 0 || !target.IsValid || !source.IsValid) return 0f;
|
|
var targetPawn = target.PlayerPawn.Value;
|
|
if (targetPawn is null || targetPawn.Health <= 0) return 0f;
|
|
|
|
int infoSize = Schema.GetClassSize(EngineNames.CTakeDamageInfo);
|
|
int resSize = Schema.GetClassSize(EngineNames.CTakeDamageResult);
|
|
if (_zero.Length < infoSize || _zero.Length < resSize) _zero = new byte[Math.Max(infoSize, resSize)];
|
|
// Grow the reused buffers if needed (class sizes are fixed per build, so this fires only on the first call), then
|
|
// re-zero them — no per-call AllocHGlobal/FreeHGlobal.
|
|
if (_infoCap < infoSize) { _infoBuf = _infoBuf == IntPtr.Zero ? Marshal.AllocHGlobal(infoSize) : Marshal.ReAllocHGlobal(_infoBuf, (IntPtr)infoSize); _infoCap = infoSize; }
|
|
if (_resCap < resSize) { _resBuf = _resBuf == IntPtr.Zero ? Marshal.AllocHGlobal(resSize) : Marshal.ReAllocHGlobal(_resBuf, (IntPtr)resSize); _resCap = resSize; }
|
|
IntPtr infoPtr = _infoBuf, resPtr = _resBuf;
|
|
Marshal.Copy(_zero, 0, infoPtr, infoSize); // bulk-zero the reused buffers (memcpy) — restore the clean-slate contract
|
|
Marshal.Copy(_zero, 0, resPtr, resSize);
|
|
|
|
var info = new CTakeDamageInfo(infoPtr);
|
|
var ai = new AttackerInfo_t
|
|
{
|
|
NeedInit = true,
|
|
IsPawn = true,
|
|
AttackerPawn = source.Pawn.Raw,
|
|
AttackerPlayerSlot = source.Slot,
|
|
};
|
|
Marshal.StructureToPtr(ai, new IntPtr(infoPtr.ToInt64() + AttackerInfoOffset), false);
|
|
|
|
uint inflictor = source.PawnIsAlive ? source.Pawn.Raw : source.PlayerPawn.Raw;
|
|
Schema.SetSchemaValue(info.Handle, EngineNames.CTakeDamageInfo, EngineNames.Inflictor, inflictor);
|
|
Schema.SetSchemaValue(info.Handle, EngineNames.CTakeDamageInfo, EngineNames.Attacker, source.Pawn.Raw);
|
|
info.Damage = damage;
|
|
info.BitsDamageType = raw ? DamageTypes_t.DMG_BURN : DamageTypes_t.DMG_BULLET;
|
|
if (raw) info.DamageFlags |= TakeDamageFlags_t.DFLAG_IGNORE_ARMOR;
|
|
|
|
var result = new CTakeDamageResult(resPtr);
|
|
Schema.SetSchemaValue(result.Handle, EngineNames.CTakeDamageResult, EngineNames.OriginatingInfo, info.Handle);
|
|
result.HealthBefore = targetPawn.Health;
|
|
result.HealthLost = (int)damage;
|
|
result.DamageDealt = damage;
|
|
result.PreModifiedDamage = damage;
|
|
|
|
bool unscaled = raw || flat;
|
|
#pragma warning disable CS0618
|
|
if (unscaled) Unscaled = true;
|
|
try { VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Invoke(targetPawn, info, result); }
|
|
finally { if (unscaled) Unscaled = false; }
|
|
#pragma warning restore CS0618
|
|
return info.Damage;
|
|
}
|
|
}
|
|
|
|
// Attribution payload the engine reads out of CTakeDamageInfo at +0x88 (kill credit / assists). Byte-laid-out to match
|
|
// the CSSharp reference (Tests.Native); written via Marshal in Deal.
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
internal struct AttackerInfo_t
|
|
{
|
|
public bool NeedInit;
|
|
public bool IsPawn;
|
|
public bool IsWorld;
|
|
public uint AttackerPawn;
|
|
public int AttackerPlayerSlot;
|
|
public int TeamChecked;
|
|
public int Team;
|
|
}
|