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? _heCreate; private static bool _init, _ok, _nativeLogged, _manualLogged; public static void Explode(CCSPlayerController attacker, Vector pos, float baseDamage, float radius, float fuseSeconds, Action 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 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 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); } } }