using System.Drawing; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; using Cs2SuperPowers.Players; using Cs2SuperPowers.Players.PlayerValidations; using Microsoft.Extensions.Logging; namespace Cs2SuperPowers.Powers; // GlowData class public class GlowData { public CDynamicProp GlowEnt { get; set; } = null!; public CDynamicProp ModelRelay { get; set; } = null!; } public class Wallhack(IPlayerHud playerHud) : BasePower(playerHud) { public override int Id => 26; public override string Name => "Wallhack"; public override string Description => "See enemies through walls with a glow!"; public override char ChatColor => ChatColors.Purple; public override string HtmlColor => "#AB4BD1"; // Static storage public static List Wallhackers = []; public static Dictionary GlowData = []; private static BasePlugin? _plugin; // Static reference to plugin! private static readonly List _staticRegisteredDelegates = []; // Store delegates to prevent GC! // Static references to event handler delegates to prevent GC! private static readonly BasePlugin.GameEventHandler _onPlayerDeathHandler = OnPlayerDeath; private static readonly BasePlugin.GameEventHandler _onPlayerDisconnectHandler = OnPlayerDisconnect; private static readonly BasePlugin.GameEventHandler _onPlayerSpawnHandler = OnPlayerSpawn; private static readonly BasePlugin.GameEventHandler _onPlayerChangeTeamHandler = OnPlayerChangeTeam; // Also store a static reference to OnPlayerTransmit! public static readonly Action _onPlayerTransmitHandler = OnPlayerTransmit; public override void AssignToPlayer(CCSPlayerController player, bool showMessages) { base.AssignToPlayer(player, showMessages); AddWallhacker(player); _plugin?.Logger.LogInformation("Wallhack assigned to {PlayerName}", player.PlayerName); // Ensure all alive players have glows (delay slightly just in case!) NextFrame(() => { foreach (var existingPlayer in Utilities.GetPlayers()) { if (IsPlayerValid(existingPlayer) && existingPlayer.PawnIsAlive && existingPlayer.Team >= CsTeam.Terrorist) { if (!GlowData.ContainsKey(existingPlayer)) { _plugin?.Logger.LogInformation("Creating glow for {PlayerName}", existingPlayer.PlayerName); Glow(existingPlayer); } } } _plugin?.Logger.LogInformation("Wallhackers: {Count}, GlowData: {GlowCount}", Wallhackers.Count, GlowData.Count); }); } public override void UnassignFromPlayer(CCSPlayerController player) { base.UnassignFromPlayer(player); RemoveWallhacker(player); _plugin?.Logger.LogInformation("Wallhack unassigned from {PlayerName}", player.PlayerName); } protected override void OnInitialize() { _plugin = Plugin; // Set static plugin reference! RegisterEventHandler(_onPlayerDeathHandler); RegisterEventHandler(_onPlayerDisconnectHandler); RegisterEventHandler(_onPlayerSpawnHandler, HookMode.Post); RegisterEventHandler(_onPlayerChangeTeamHandler, HookMode.Post); _plugin?.Logger.LogInformation("Wallhack initialized!"); // Delay creating initial glows until global variables are initialized! NextFrame(() => { _plugin?.Logger.LogInformation("Creating initial glows for existing players!"); foreach (var existingPlayer in Utilities.GetPlayers()) { if (IsPlayerValid(existingPlayer) && existingPlayer.PawnIsAlive && existingPlayer.Team >= CsTeam.Terrorist) { if (!GlowData.ContainsKey(existingPlayer)) { _plugin?.Logger.LogInformation("Creating glow for {PlayerName} during init", existingPlayer.PlayerName); Glow(existingPlayer); } } } }); } public static HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) { var player = @event.Userid; _plugin?.Logger.LogInformation("Wallhack: OnPlayerSpawn called for {PlayerName}", player?.PlayerName ?? "NULL"); if (!IsPlayerValid(player)) { _plugin?.Logger.LogInformation("Wallhack: Player invalid, skipping"); return HookResult.Continue; } if (player!.Team < CsTeam.Terrorist) { _plugin?.Logger.LogInformation("Wallhack: Player team invalid, skipping"); return HookResult.Continue; } var gameRules = Utilities.FindAllEntitiesByDesignerName("cs_gamerules").FirstOrDefault(); if (gameRules?.GameRules?.WarmupPeriod == true) { _plugin?.Logger.LogInformation("Wallhack: Warmup period, skipping"); return HookResult.Continue; } _plugin?.Logger.LogInformation("Wallhack: Calling Glow for {PlayerName}", player.PlayerName); Glow(player!); return HookResult.Continue; } public static HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info) { var player = @event.Userid; if (!IsPlayerValid(player)) return HookResult.Continue; if (GlowData.TryGetValue(player!, out var glowData)) { if (glowData.GlowEnt.IsValid) glowData.GlowEnt.Remove(); if (glowData.ModelRelay.IsValid) glowData.ModelRelay.Remove(); GlowData.Remove(player!); } Wallhackers.Remove(player!); return HookResult.Continue; } public static HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info) { var player = @event.Userid; if (!IsPlayerValid(player)) return HookResult.Continue; if (!GlowData.TryGetValue(player!, out var glowData)) return HookResult.Continue; if (glowData.GlowEnt.IsValid) glowData.GlowEnt.Remove(); if (glowData.ModelRelay.IsValid) glowData.ModelRelay.Remove(); GlowData.Remove(player!); return HookResult.Continue; } public static HookResult OnPlayerChangeTeam(EventPlayerTeam @event, GameEventInfo info) { var player = @event.Userid; if (!IsPlayerValid(player)) return HookResult.Continue; if (!GlowData.TryGetValue(player!, out var glowData)) return HookResult.Continue; if (!glowData.GlowEnt.IsValid || !glowData.ModelRelay.IsValid) return HookResult.Continue; NextWorldUpdate(() => { glowData.GlowEnt.SetModel(GetPlayerModel(player!)); glowData.ModelRelay.SetModel(GetPlayerModel(player!)); }); return HookResult.Continue; } private static void Glow(CCSPlayerController player) { _plugin?.Logger.LogInformation("Wallhack: Glow() called for {PlayerName}", player.PlayerName); // First remove existing glow for this player if it exists if (GlowData.TryGetValue(player, out var existingGlow)) { if (existingGlow.GlowEnt.IsValid) existingGlow.GlowEnt.Remove(); if (existingGlow.ModelRelay.IsValid) existingGlow.ModelRelay.Remove(); GlowData.Remove(player); } var glowEntity = Utilities.CreateEntityByName("prop_dynamic"); var modelRelay = Utilities.CreateEntityByName("prop_dynamic"); if (glowEntity == null || modelRelay == null) { _plugin?.Logger.LogError("Wallhack: Failed to create glow entities for {PlayerName}", player.PlayerName); return; } modelRelay.Spawnflags = 256; modelRelay.Render = Color.Transparent; modelRelay.RenderMode = RenderMode_t.kRenderNone; modelRelay.CBodyComponent!.SceneNode!.Owner!.Entity!.Flags &= ~(1u << 2); modelRelay.SetModel(GetPlayerModel(player)); glowEntity.Spawnflags = 256; glowEntity.CBodyComponent!.SceneNode!.Owner!.Entity!.Flags &= ~(1u << 2); glowEntity.Render = Color.FromArgb(1, 0, 0, 0); glowEntity.SetModel(GetPlayerModel(player)); glowEntity.DispatchSpawn(); modelRelay.DispatchSpawn(); glowEntity.Glow.GlowRange = 5000; glowEntity.Glow.GlowRangeMin = 0; glowEntity.Glow.GlowColorOverride = Color.FromArgb(255, 171, 75, 209); glowEntity.Glow.GlowTeam = -1; glowEntity.Glow.GlowType = 3; modelRelay.AcceptInput("FollowEntity", player.Pawn.Value, null, "!activator"); glowEntity.AcceptInput("FollowEntity", modelRelay, null, "!activator"); GlowData.Add(player, new GlowData { GlowEnt = glowEntity, ModelRelay = modelRelay }); } private static string GetPlayerModel(CCSPlayerController player) { return player.Pawn.Value!.CBodyComponent!.SceneNode!.GetSkeletonInstance().ModelState.ModelName; } private static bool IsPlayerValid(CCSPlayerController? plr) => plr != null && plr.IsValid && plr.PlayerPawn != null && plr.PlayerPawn.IsValid && plr.Connected == PlayerConnectedState.PlayerConnected && !plr.IsHLTV; public static void OnPlayerTransmit(CCheckTransmitInfo info, CCSPlayerController player) { foreach (var entity in GlowData.ToList()) { if (Wallhackers.Contains(player!)) { if (!IsPlayerValid(entity.Key) || !IsPlayerValid(player)) continue; if (entity.Key.Team != player!.Team && player!.Team != CsTeam.Spectator && entity.Key.Team != CsTeam.Spectator) { info.TransmitEntities.Add(entity.Value.ModelRelay); info.TransmitEntities.Add(entity.Value.GlowEnt); continue; } } info.TransmitEntities.Remove(entity.Value.ModelRelay); info.TransmitEntities.Remove(entity.Value.GlowEnt); } } public static void AddWallhacker(CCSPlayerController player) { if (!Wallhackers.Contains(player)) { Wallhackers.Add(player); } } public static void RemoveWallhacker(CCSPlayerController player) { Wallhackers.Remove(player); } // Helper to wrap Server.NextWorldUpdate and store delegate to prevent GC! private static void NextWorldUpdate(Action callback) { Action? wrappedCallback = null; wrappedCallback = () => { callback(); if (wrappedCallback != null) { _staticRegisteredDelegates.Remove(wrappedCallback); } }; _staticRegisteredDelegates.Add(wrappedCallback); Server.NextWorldUpdate(wrappedCallback); } }