using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Core.Capabilities; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.ValveConstants.Protobuf; using CS2_SimpleAdminApi; using Microsoft.Extensions.Logging; namespace AdminStealth; // Companion to CS2-SimpleAdmin's css_hide. When an admin hides, everyone else sees them leave the // server, and nothing the hide does afterwards (the suicide, the team changes, their real // disconnect later) is announced. Also blocks the status command for players, which would // otherwise still list a hidden admin. public class AdminStealth : BasePlugin { public override string ModuleName => "AdminStealth"; public override string ModuleVersion => "0.1.0"; public override string ModuleAuthor => "astra"; public override string ModuleDescription => "Makes CS2-SimpleAdmin's css_hide look like leaving the server"; private static readonly PluginCapability SimpleAdminCapability = new("simpleadmin:api"); private ICS2_SimpleAdminApi? _simpleAdmin; public override void Load(bool hotReload) { AddCommandListener("status", OnStatus); } public override void OnAllPluginsLoaded(bool hotReload) { try { _simpleAdmin = SimpleAdminCapability.Get(); } catch (KeyNotFoundException) { } if (_simpleAdmin == null) { Logger.LogError("CS2-SimpleAdmin's API isn't available, so hiding won't look like leaving"); return; } _simpleAdmin.OnAdminToggleSilent += OnAdminToggleSilent; } public override void Unload(bool hotReload) { if (_simpleAdmin != null) _simpleAdmin.OnAdminToggleSilent -= OnAdminToggleSilent; } // Players get nothing back. The server console and RCON still work. private HookResult OnStatus(CCSPlayerController? player, CommandInfo command) { return player == null ? HookResult.Continue : HookResult.Stop; } private void OnAdminToggleSilent(int slot, bool hidden) { if (!hidden) return; var admin = Utilities.GetPlayerFromSlot(slot); if (admin is not { IsValid: true, IsBot: false }) return; // A fake player_disconnect, sent to each client rather than fired on the server, so no // plugin (SimpleAdmin included) treats the admin as really gone. var leave = new EventPlayerDisconnect(true) { Userid = admin, Playerid = admin.Slot, Name = admin.PlayerName, Networkid = new SteamID(admin.SteamID).SteamId3, Xuid = admin.SteamID, Reason = (int)NetworkDisconnectionReason.NETWORK_DISCONNECT_DISCONNECT_BY_USER, EverFullyConnected = true, }; foreach (var player in Utilities.GetPlayers()) { if (player is { IsValid: true, IsBot: false, IsHLTV: false } && player.Slot != slot) leave.FireEventToClient(player); } leave.Free(); } [GameEventHandler(HookMode.Pre)] public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info) { if (IsHidden(@event.Userid)) info.DontBroadcast = true; return HookResult.Continue; } [GameEventHandler(HookMode.Pre)] public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info) { if (IsHidden(@event.Userid)) info.DontBroadcast = true; return HookResult.Continue; } // Pre runs before SimpleAdmin's own handler forgets the player was hidden. [GameEventHandler(HookMode.Pre)] public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info) { if (IsHidden(@event.Userid)) info.DontBroadcast = true; return HookResult.Continue; } private bool IsHidden(CCSPlayerController? player) { return player is { IsValid: true, IsBot: false } && _simpleAdmin?.IsAdminSilent(player) == true; } }