using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.ValveConstants.Protobuf; using CS2_SimpleAdmin.Managers; using CS2_SimpleAdmin.Menus; using CS2_SimpleAdminApi; namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { /// /// Handles the 'ban' command, allowing admins to ban one or more valid connected players. /// /// The player issuing the ban command, or null for console. /// The command information including arguments. [RequiresPermissions("@css/ban")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnBanCommand(CCSPlayerController? caller, CommandInfo command) { string callerName = Helper.GetCallerName(caller); if (command.ArgCount < 2) { return; } TargetResult? targets = GetTarget(command); if (targets == null) { return; } List playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToList(); if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0) { return; } string reason = Helper.GetArgsReason(command, 3); int time = Helper.ParsePenaltyTime(command.GetArg(2)); playersToTarget.ForEach(player => { if (!caller.CanTarget(player)) { return; } if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime) { BasicMenu.CreatePenaltyMenu(caller, player, PenaltyType.Ban).OpenMenu(caller); return; } Ban(caller, player, time, reason, callerName, BanManager, command); }); } /// /// Core logic to ban a specific player, scheduling database updates, notifications, and kicks. /// /// The player issuing the ban, or null for console. /// The player to be banned. /// Ban duration in minutes; 0 means permanent. /// Reason for the ban. /// Optional caller name string. If null, defaults to player name or console. /// Optional BanManager to handle ban persistence. /// Optional command info object for logging. /// If true, suppresses command logging. internal void Ban(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, BanManager? banManager = null, CommandInfo? command = null, bool silent = false) { if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) { return; } if (!caller.CanTarget(player)) { return; } if (!CheckValidBan(caller, time)) { return; } // Set default caller name if not provided callerName = Helper.GetCallerName(caller); // Get player and admin information PlayerInfo playerInfo = PlayersInfo[player.SteamID]; PlayerInfo? adminInfo = Helper.GetAdminInfo(caller); // Asynchronously handle banning logic Task.Run(async () => { int? penaltyId = await BanManager.BanPlayer(playerInfo, adminInfo, reason, time); await Server.NextWorldUpdateAsync(() => { SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Ban, reason, time, penaltyId); }); }); // Determine message keys and arguments based on ban time string? messageKey; string? activityMessageKey; object[]? centerArgs; object[]? adminActivityArgs; if (time == 0) { messageKey = "sa_player_ban_message_perm"; activityMessageKey = "sa_admin_ban_message_perm"; centerArgs = [reason, "CALLER"]; adminActivityArgs = ["CALLER", player.PlayerName, reason]; } else { messageKey = "sa_player_ban_message_time"; activityMessageKey = "sa_admin_ban_message_time"; centerArgs = new object[] { reason, time, "CALLER" }; adminActivityArgs = new object[] { "CALLER", player.PlayerName, reason, time }; } // Display center message to the player Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs); // Display admin activity message if necessary if (caller == null || !SilentPlayers.Contains(caller.Slot)) { Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); } // Schedule a kick timer if (player.UserId.HasValue) { Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED, Config.OtherSettings.KickTime); } // Execute ban command if necessary if (UnlockedCommands) { Server.ExecuteCommand($"banid 1 {new SteamID(player.SteamID).SteamId3}"); } if (!silent) { if (command == null) { Helper.LogCommand(caller, $"css_ban {Helper.GetLogName(player)} {time} {reason}"); } else { Helper.LogCommand(caller, command); } } Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Ban, _localizer); } /// /// Adds a ban for a player by their SteamID, including offline bans. /// /// The player issuing the ban command. /// SteamID of the player to ban. /// Ban duration in minutes (0 means permanent). /// Reason for banning. /// Optional ban manager for database operations. internal void AddBan(CCSPlayerController? caller, SteamID steamid, int time, string reason, BanManager? banManager = null) { // Set default caller name if not provided string callerName = Helper.GetCallerName(caller); PlayerInfo? adminInfo = Helper.GetAdminInfo(caller); CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid.SteamId64); if (player != null && player.IsValid) { if (!caller.CanTarget(player)) { return; } Ban(caller, player, time, reason, callerName, silent: true); //command.ReplyToCommand($"Banned player {player.PlayerName}."); } else { if (!caller.CanTarget(steamid)) { return; } // Asynchronous ban operation if player is not online or not found Task.Run(async () => { int? penaltyId = await BanManager.AddBanBySteamid(steamid.SteamId64, adminInfo, reason, time); await Server.NextWorldUpdateAsync(() => { SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Ban, reason, time, penaltyId); }); }); Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Ban, _localizer); } } /// /// Handles banning a player by specifying their SteamID via command. /// /// The player issuing the command, or null if console. /// Command information including arguments (SteamID, time, reason). [RequiresPermissions("@css/ban")] [CommandHelper(minArgs: 1, usage: " [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo command) { if (DatabaseProvider == null) { return; } string callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console"; if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) { return; } if (!Helper.ValidateSteamId(command.GetArg(1), out SteamID? steamId) || steamId == null) { command.ReplyToCommand("Invalid SteamID64."); return; } ulong steamid = steamId.SteamId64; string reason = Helper.GetArgsReason(command, 3); int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2))); if (!CheckValidBan(caller, time)) { return; } PlayerInfo? adminInfo = Helper.GetAdminInfo(caller); CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid); if (player != null && player.IsValid) { if (!caller.CanTarget(player)) { return; } Ban(caller, player, time, reason, callerName, silent: true); //command.ReplyToCommand($"Banned player {player.PlayerName}."); } else { if (!caller.CanTarget(new SteamID(steamId.SteamId64))) { return; } // Asynchronous ban operation if player is not online or not found Task.Run(async () => { int? penaltyId = await BanManager.AddBanBySteamid(steamid, adminInfo, reason, time); await Server.NextWorldUpdateAsync(() => { SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Ban, reason, time, penaltyId); }); }); Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Ban, _localizer); command.ReplyToCommand($"Player with steamid {steamid} is not online. Ban has been added offline."); } Helper.LogCommand(caller, command); if (UnlockedCommands) { Server.ExecuteCommand($"banid 1 {steamId.SteamId3}"); } } /// /// Handles banning a player by their IP address, supporting offline banning if player is not online. /// /// The player issuing the ban command. /// The command containing the IP, time, and reason arguments. [RequiresPermissions("@css/ban")] [CommandHelper(minArgs: 1, usage: " [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnBanIpCommand(CCSPlayerController? caller, CommandInfo command) { if (DatabaseProvider == null) { return; } string callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console"; if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) { return; } string ipAddress = command.GetArg(1); if (!Helper.IsValidIp(ipAddress)) { command.ReplyToCommand($"Invalid IP address."); return; } string reason = Helper.GetArgsReason(command, 3); int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2))); if (!CheckValidBan(caller, time)) { return; } PlayerInfo? adminInfo = Helper.GetAdminInfo(caller); List players = Helper.GetPlayerFromIp(ipAddress); if (players.Count >= 1) { foreach (CCSPlayerController player in players) { if (player == null || !player.IsValid) { continue; } if (!caller.CanTarget(player)) { return; } Ban(caller, player, time, reason, callerName, silent: true); } } else { // Asynchronous ban operation if player is not online or not found Task.Run(async () => { await BanManager.AddBanByIp(ipAddress, adminInfo, reason, time); }); command.ReplyToCommand($"Player with ip {ipAddress} is not online. Ban has been added offline."); } Helper.LogCommand(caller, command); } /// /// Checks whether the ban duration is valid based on the caller's permissions and configured limits. /// /// The player issuing the ban command. /// Requested ban duration in minutes. /// True if ban duration is valid; otherwise, false. private bool CheckValidBan(CCSPlayerController? caller, int duration) { if (caller == null) { return true; } bool canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban"); if (duration <= 0 && !canPermBan) { caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}"); return false; } if (duration <= Config.OtherSettings.MaxBanDuration || canPermBan) { return true; } caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_max_duration_exceeded", Config.OtherSettings.MaxBanDuration]}"); return false; } /// /// Handles unbanning players by pattern (steamid, name, or IP). /// /// The player issuing the unban command. /// Command containing target pattern and optional reason. [RequiresPermissions("@css/unban")] [CommandHelper(minArgs: 1, usage: " [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnUnbanCommand(CCSPlayerController? caller, CommandInfo command) { if (DatabaseProvider == null) { return; } string callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console"; if (command.GetArg(1).Length <= 1) { command.ReplyToCommand($"Too short pattern to search."); return; } string pattern = command.GetArg(1); string reason = Helper.GetArgsReason(command, 2); Task.Run(async () => await BanManager.UnbanPlayer(pattern, callerSteamId, reason)); Helper.LogCommand(caller, command); command.ReplyToCommand($"Unbanned player with pattern {pattern}."); } /// /// Handles warning players, supporting multiple targets and warning durations. /// /// The player issuing the warn command. /// The command containing target, time, and reason parameters. [RequiresPermissions("@css/kick")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnWarnCommand(CCSPlayerController? caller, CommandInfo command) { if (DatabaseProvider == null) { return; } string callerName = Helper.GetCallerName(caller); if (command.ArgCount < 2) { return; } TargetResult? targets = GetTarget(command); if (targets == null) { return; } List playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.Connected && !player.IsHLTV).ToList(); if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0) { return; } int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2))); string reason = Helper.GetArgsReason(command, 3); playersToTarget.ForEach(player => { if (caller!.CanTarget(player)) { Warn(caller, player, time, reason, callerName, command); } }); } /// /// Issues a warning penalty to a specific player with optional duration and reason. /// /// The player issuing the warning. /// The player to warn. /// Duration of the warning in minutes. /// Reason for the warning. /// Optional display name of the caller. /// Optional command info for logging. internal void Warn(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null) { if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) { return; } if (!caller.CanTarget(player)) { return; } if (!CheckValidBan(caller, time)) { return; } // Set default caller name if not provided callerName = Helper.GetCallerName(caller); // Freeze player pawn if alive if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE) { player.PlayerPawn?.Value?.Freeze(); AddTimer(5.0f, () => player.PlayerPawn?.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); } // Get player and admin information PlayerInfo playerInfo = PlayersInfo[player.SteamID]; PlayerInfo? adminInfo = Helper.GetAdminInfo(caller); // Asynchronously handle warning logic Task.Run(async () => { int? penaltyId = await WarnManager.WarnPlayer(playerInfo, adminInfo, reason, time); await Server.NextWorldUpdateAsync(() => { SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Warn, reason, time, penaltyId); }); // Check for warn thresholds and execute punish command if applicable int totalWarns = await WarnManager.GetPlayerWarnsCount(player.SteamID); if (Config.WarnThreshold.Count > 0) { string? punishCommand = null; int lastKey = Config.WarnThreshold.Keys.Max(); if (totalWarns >= lastKey) { punishCommand = Config.WarnThreshold[lastKey]; } else if (Config.WarnThreshold.TryGetValue(totalWarns, out string? value)) { punishCommand = value; } if (!string.IsNullOrEmpty(punishCommand)) { await Server.NextWorldUpdateAsync(() => { Server.ExecuteCommand(punishCommand.Replace("USERID", playerInfo.UserId.ToString()).Replace("STEAMID64", playerInfo.SteamId?.ToString())); }); } } }); // Determine message keys and arguments based on warning time string? messageKey; string? activityMessageKey; object[]? centerArgs; object[]? adminActivityArgs; if (time == 0) { messageKey = "sa_player_warn_message_perm"; activityMessageKey = "sa_admin_warn_message_perm"; centerArgs = new object[] { reason, "CALLER" }; adminActivityArgs = new object[] { "CALLER", player.PlayerName, reason }; } else { messageKey = "sa_player_warn_message_time"; activityMessageKey = "sa_admin_warn_message_time"; centerArgs = [reason, time, "CALLER"]; adminActivityArgs = ["CALLER", player.PlayerName, reason, time]; } // Display center message to the playser Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs); // Display admin activity message if necessary if (caller == null || !SilentPlayers.Contains(caller.Slot)) { Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); } // Log the warning command if (command == null) { Helper.LogCommand(caller, $"css_warn {Helper.GetLogName(player)} {time} {reason}"); } else { Helper.LogCommand(caller, command); } // Send Discord notification for the warning Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Warn, _localizer); } /// /// Adds a warning to a player by their SteamID, including support for offline players. /// /// The player issuing the warn command. /// SteamID of the player to warn. /// Warning duration in minutes. /// Reason for the warning. /// Optional warn manager instance. internal void AddWarn(CCSPlayerController? caller, SteamID steamid, int time, string reason, WarnManager? warnManager = null) { // Set default caller name if not provided string callerName = Helper.GetCallerName(caller); PlayerInfo? adminInfo = Helper.GetAdminInfo(caller); CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid.SteamId64); if (player != null && player.IsValid) { if (!caller.CanTarget(player)) { return; } Warn(caller, player, time, reason, callerName); //command.ReplyToCommand($"Banned player {player.PlayerName}."); } else { if (!caller.CanTarget(steamid)) { return; } // Asynchronous ban operation if player is not online or not found Task.Run(async () => { int? penaltyId = await WarnManager.AddWarnBySteamid(steamid.SteamId64, adminInfo, reason, time); await Server.NextWorldUpdateAsync(() => { SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Warn, reason, time, penaltyId); }); // Check for warn thresholds and execute punish command if applicable int totalWarns = await WarnManager.GetPlayerWarnsCount(steamid.SteamId64); if (Config.WarnThreshold.Count > 0) { string? punishCommand = null; int lastKey = Config.WarnThreshold.Keys.Max(); if (totalWarns >= lastKey) { punishCommand = Config.WarnThreshold[lastKey]; } else if (Config.WarnThreshold.TryGetValue(totalWarns, out string? value)) { punishCommand = value; } if (!string.IsNullOrEmpty(punishCommand)) { await Server.NextWorldUpdateAsync(() => { Server.ExecuteCommand(punishCommand.Replace("STEAMID64", steamid.SteamId64.ToString())); }); } } }); Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Warn, _localizer); } } /// /// Handles removing a warning (unwarn) by a pattern string. /// /// The player issuing the unwarn command. /// The command containing target pattern. [RequiresPermissions("@css/kick")] [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnUnwarnCommand(CCSPlayerController? caller, CommandInfo command) { if (DatabaseProvider == null) { return; } if (command.GetArg(1).Length <= 1) { command.ReplyToCommand($"Too short pattern to search."); return; } string pattern = command.GetArg(1); Task.Run(async () => await WarnManager.UnwarnPlayer(pattern)); Helper.LogCommand(caller, command); command.ReplyToCommand($"Unwarned player with pattern {pattern}."); } }