initial commit
All checks were successful
CI / build (push) Successful in 32s
CI / release (push) Successful in 32s
CI / lint (push) Successful in 30s

This commit is contained in:
Kamal Tufekcic 2026-07-05 13:28:35 +03:00
commit d701598350
67 changed files with 9351 additions and 0 deletions

View file

@ -0,0 +1,93 @@
using System.Runtime.InteropServices;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
namespace Outnumbered.Engine;
// A real HE blast at a point, attributed to a player. Spawns via the game's NATIVE projectile-create (the same call used
// when a player throws) because Valve removed the think-arming from the InitializeSpawnFromWorld input, so a
// CreateEntityByName grenade is inert on current CS2. Signature from MatchZy / cs2-executes. Lazy-resolved; if it doesn't
// match this build, falls back to a manual radius blast (no engine VFX). Config-agnostic: damage/radius/fuse are passed in;
// `log` receives one-time path notes. Args: (position, angle, velocity, velocity, IntPtr.Zero, itemDefIndex).
internal static class GrenadeSpawner
{
private static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
private static MemoryFunctionWithReturn<nint, nint, nint, nint, nint, int, CHEGrenadeProjectile>? _heCreate;
private static bool _init, _ok, _nativeLogged, _manualLogged;
public static void Explode(CCSPlayerController attacker, Vector pos, float baseDamage, float radius, float fuseSeconds, Action<string, Exception?> log)
{
if (attacker is not { IsValid: true } || attacker.PlayerPawn.Value is null) return;
if (!TrySpawnHe(attacker, pos, baseDamage, radius, fuseSeconds, log))
ManualBlast(attacker, pos, baseDamage, radius, log);
}
private static bool TrySpawnHe(CCSPlayerController attacker, Vector pos, float baseDamage, float radius, float fuseSeconds, Action<string, Exception?> log)
{
if (!_init)
{
_init = true;
try
{
_heCreate = new(IsLinux
? "55 4C 89 C1 48 89 E5 41 57 49 89 D7"
: "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 50 48 8B AC 24 80 00 00 00 49 8B F8");
_ok = true;
}
catch (Exception ex)
{
log("explode-on-kill: HE native-create signature not found — manual blast fallback", ex);
_ok = false;
}
}
if (!_ok || _heCreate is null) return false;
try
{
var apawn = attacker.PlayerPawn.Value!;
var spawn = new Vector(pos.X, pos.Y, pos.Z + 12f);
var ang = new QAngle();
var vel = new Vector(0, 0, -10f);
var nade = _heCreate.Invoke(spawn.Handle, ang.Handle, vel.Handle, vel.Handle, IntPtr.Zero, EngineNames.HeGrenadeItemDef);
if (nade is null || !nade.IsValid) return false;
nade.Teleport(spawn, ang, vel);
nade.Globalname = "custom";
nade.TeamNum = apawn.TeamNum; // CT -> only damages bots; survivors safe (ff off)
nade.Thrower.Raw = attacker.PlayerPawn.Raw; // kill attribution + killfeed
nade.OriginalThrower.Raw = attacker.PlayerPawn.Raw;
nade.OwnerEntity.Raw = attacker.PlayerPawn.Raw;
nade.Damage = baseDamage; // scaled UP by the offense hook on detonation
nade.DmgRadius = radius;
nade.DetonateTime = Server.CurrentTime + Math.Max(0f, fuseSeconds); // think is scheduled now -> respected
if (!_nativeLogged) { _nativeLogged = true; log("explode-on-kill: native HE grenade spawned + armed OK", null); }
return true;
}
catch (Exception ex)
{
log("explode-on-kill: native HE invoke failed — manual blast fallback", ex);
_ok = false; // don't keep retrying a bad signature this session
return false;
}
}
// Snapshot the list — a lethal Deal fires player_death synchronously, which mutates bot state mid-loop.
private static void ManualBlast(CCSPlayerController attacker, Vector center, float baseDamage, float radius, Action<string, Exception?> log)
{
double r = Math.Max(1.0, radius);
foreach (var bot in Utilities.GetPlayers().ToList())
{
if (bot is not { IsValid: true, IsBot: true, IsHLTV: false } || !bot.PawnIsAlive) continue;
var bpos = bot.PlayerPawn.Value?.AbsOrigin;
if (bpos is null) continue;
double dx = bpos.X - center.X, dy = bpos.Y - center.Y, dz = bpos.Z - center.Z;
double dist = Math.Sqrt(dx * dx + dy * dy + dz * dz);
if (dist > r) continue;
float dmg = (float)(baseDamage * (1.0 - dist / r)); // linear falloff
if (dmg >= 1f) DamageDealer.Deal(bot, attacker, dmg); // non-raw -> scales + attributed + chains
}
if (!_manualLogged) { _manualLogged = true; log("explode-on-kill: native HE unavailable -> MANUAL radius blast (no engine VFX)", null); }
}
}