using System.Collections;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Reflection;
using System.Text.Json;
using CounterStrikeSharp.API.ValveConstants.Protobuf;
using CS2_SimpleAdmin.Models;
using MenuManager;
using System.Diagnostics.CodeAnalysis;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
///
/// Handles the command that shows active penalties and warns for the caller or specified player.
/// Queries warnings and mute status, formats them locally, and sends the result to caller's chat.
///
/// The player issuing this command.
/// Command input parameters.
[CommandHelper(usage: "[#userid or name]", whoCanExecute: CommandUsage.CLIENT_ONLY)]
public void OnPenaltiesCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller == null || !caller.IsValid || !caller.UserId.HasValue || DatabaseProvider == null)
return;
var userId = caller.UserId.Value;
var steamId = caller.SteamID;
if (!string.IsNullOrEmpty(command.GetArg(1)) && AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/kick"))
{
var targets = GetTarget(command);
if (targets == null)
return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (!player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
userId = player.UserId.Value;
});
}
Task.Run(async () =>
{
try
{
var warns = await WarnManager.GetPlayerWarns(PlayersInfo[steamId], false);
// Check if the player is muted
var activeMutes = await MuteManager.IsPlayerMuted(PlayersInfo[steamId].SteamId.SteamId64.ToString());
Dictionary> mutesList = new()
{
{ PenaltyType.Gag, [] },
{ PenaltyType.Mute, [] },
{ PenaltyType.Silence, [] }
};
List warnsList = [];
bool found = false;
foreach (var warn in warns.TakeWhile(warn => (string)warn.status == "ACTIVE"))
{
DateTime ends = warn.ends;
if (_localizer == null) continue;
using (new WithTemporaryCulture(caller.GetLanguage()))
warnsList.Add(_localizer["sa_player_penalty_info_active_warn", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture), (string)warn.reason]);
found = true;
}
if (!found)
{
if (_localizer != null)
warnsList.Add(_localizer["sa_player_penalty_info_no_active_warn"]);
}
if (activeMutes.Count > 0)
{
foreach (var mute in activeMutes)
{
string muteType = mute.type;
DateTime ends = mute.ends;
using (new WithTemporaryCulture(caller.GetLanguage()))
{
switch (muteType)
{
// Apply mute penalty based on mute type
case "GAG":
if (_localizer != null)
mutesList[PenaltyType.Gag].Add(_localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
case "MUTE":
if (_localizer != null)
mutesList[PenaltyType.Mute].Add(_localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
default:
if (_localizer != null)
mutesList[PenaltyType.Silence].Add(_localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
}
}
}
}
if (_localizer != null)
{
if (mutesList[PenaltyType.Gag].Count == 0)
mutesList[PenaltyType.Gag].Add(_localizer["sa_player_penalty_info_no_active_gag"]);
if (mutesList[PenaltyType.Mute].Count == 0)
mutesList[PenaltyType.Mute].Add(_localizer["sa_player_penalty_info_no_active_mute"]);
if (mutesList[PenaltyType.Silence].Count == 0)
mutesList[PenaltyType.Silence].Add(_localizer["sa_player_penalty_info_no_active_silence"]);
}
await Server.NextWorldUpdateAsync(() =>
{
caller.SendLocalizedMessage(_localizer, "sa_player_penalty_info",
[
PlayersInfo[steamId].Name,
PlayersInfo[steamId].TotalBans,
PlayersInfo[steamId].TotalGags,
PlayersInfo[steamId].TotalMutes,
PlayersInfo[steamId].TotalSilences,
PlayersInfo[steamId].TotalWarns,
string.Join("\n", mutesList.SelectMany(kvp => kvp.Value)),
string.Join("\n", warnsList)
]);
});
}
catch (Exception ex)
{
_logger?.LogError($"Error processing player information: {ex}");
}
});
}
///
/// Toggles the admin voice listening mode or mutes/unmutes all players' voice.
/// Sends confirmation messages accordingly.
///
/// The player issuing this command.
/// Command input parameters.
[RequiresPermissions("@css/chat")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
public void OnAdminVoiceCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller == null || !caller.IsValid)
return;
if (command.ArgCount > 1)
{
if (command.GetArg(2).ToLower().Equals("muteAll"))
{
caller.SendLocalizedMessage(_localizer, "sa_admin_voice_mute_all");
foreach (var player in Helper.GetValidPlayers().Where(p => p != caller && !AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
{
player.VoiceFlags = VoiceFlags.Muted;
}
}
if (command.GetArg(2).ToLower().Equals("unmuteAll"))
{
caller.SendLocalizedMessage(_localizer, "sa_admin_voice_unmute_all");
foreach (var player in Helper.GetValidPlayers().Where(p => p != caller))
{
if (PlayerPenaltyManager.GetPlayerPenalties(player.Slot, [PenaltyType.Silence, PenaltyType.Mute]).Count == 0)
player.VoiceFlags = VoiceFlags.Normal;
}
}
return;
}
var enabled = caller.VoiceFlags.HasFlag(VoiceFlags.ListenAll);
var messageKey = enabled
? "sa_admin_voice_unlisten_all"
: "sa_admin_voice_listen_all";
caller.SendLocalizedMessage(_localizer, messageKey);
caller.VoiceFlags ^= VoiceFlags.ListenAll;
}
///
/// Opens the admin menu for the caller.
///
/// The player issuing the command.
/// Command input parameters.
[RequiresPermissions("@css/generic")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
public void OnAdminCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller == null || !caller.IsValid)
return;
AdminMenu.OpenMenu(caller);
}
///
/// Displays admin help text read from a file.
/// Outputs lines one at a time as replies to the command.
///
/// The player issuing the command.
/// Command input parameters.
[RequiresPermissions("@css/generic")]
public void OnAdminHelpCommand(CCSPlayerController? caller, CommandInfo command)
{
var lines = File.ReadAllLines(ModuleDirectory + "/admin_help.txt");
foreach (var line in lines)
{
command.ReplyToCommand(string.IsNullOrWhiteSpace(line) ? " " : line.ReplaceColorTags());
}
}
///
/// Handles adding a new admin with specified SteamID, name, flags, immunity, and duration.
///
/// The player issuing the command.
/// Command input parameters.
[CommandHelper(minArgs: 4, usage: " ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/root")]
public void OnAddAdminCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
{
command.ReplyToCommand($"Invalid SteamID64.");
return;
}
var steamid = steamId.SteamId64.ToString();
if (command.GetArg(2).Length <= 0)
{
command.ReplyToCommand($"Invalid player name.");
return;
}
if (!command.GetArg(3).Contains('@') && !command.GetArg(3).Contains('#'))
{
command.ReplyToCommand($"Invalid flag or group.");
return;
}
var name = command.GetArg(2);
var flags = command.GetArg(3);
var globalAdmin = command.GetArg(4).ToLower().Equals("-g") || command.GetArg(5).ToLower().Equals("-g") ||
command.GetArg(6).ToLower().Equals("-g");
int.TryParse(command.GetArg(4), out var immunity);
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(5)));
AddAdmin(caller, steamid, name, flags, immunity, time, globalAdmin, command);
}
///
/// Adds admin permissions and groups for a player.
///
/// The player issuing the command.
/// SteamID as string identifying the player.
/// Player's name.
/// Comma-separated admin flags/groups.
/// Admin immunity level.
/// Duration of permission (default 0 = permanent).
/// Whether admin is global.
/// Optional command info for confirmation messages.
public static void AddAdmin(CCSPlayerController? caller, string steamid, string name, string flags, int immunity, int time = 0, bool globalAdmin = false, CommandInfo? command = null)
{
if (DatabaseProvider == null) return;
var flagsList = flags.Split(',').Select(flag => flag.Trim()).ToList();
_ = Instance.PermissionManager.AddAdminBySteamId(steamid, name, flagsList, immunity, time, globalAdmin);
Helper.LogCommand(caller, $"css_addadmin {steamid} {name} {flags} {immunity} {time}");
var msg = $"Added '{flags}' flags to '{name}' ({steamid})";
if (command != null)
command.ReplyToCommand(msg);
else if (caller != null && caller.IsValid)
caller.PrintToChat(msg);
else
Server.PrintToConsole(msg);
}
///
/// Handles removing an admin's flags and groups by SteamID.
///
/// The player issuing the command.
/// Command input parameters.
[CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/root")]
public void OnDelAdminCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
{
command.ReplyToCommand($"Invalid SteamID64.");
return;
}
var globalDelete = command.GetArg(2).ToLower().Equals("-g");
RemoveAdmin(caller, steamId.SteamId64.ToString(), globalDelete, command);
}
///
/// Removes admin permissions and groups for a player.
///
/// The player issuing the command.
/// SteamID as string identifying the player.
/// Whether to delete globally.
/// Optional command info.
public void RemoveAdmin(CCSPlayerController? caller, string steamid, bool globalDelete = false, CommandInfo? command = null)
{
if (DatabaseProvider == null) return;
_ = PermissionManager.DeleteAdminBySteamId(steamid, globalDelete);
AddTimer(2, () =>
{
if (string.IsNullOrEmpty(steamid) || !SteamID.TryParse(steamid, out var steamId) ||
steamId == null) return;
if (PermissionManager.AdminCache.ContainsKey(steamId))
{
PermissionManager.AdminCache.TryRemove(steamId, out _);
}
AdminManager.ClearPlayerPermissions(steamId);
AdminManager.RemovePlayerAdminData(steamId);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
Helper.LogCommand(caller, $"css_deladmin {steamid}");
var msg = $"Removed flags from '{steamid}'";
if (command != null)
command.ReplyToCommand(msg);
else if (caller != null && caller.IsValid)
caller.PrintToChat(msg);
else
Server.PrintToConsole(msg);
}
///
/// Adds a new admin group with specified flags and immunity settings.
///
/// The player issuing the command.
/// Command input parameters.
[CommandHelper(minArgs: 3, usage: " ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/root")]
public void OnAddGroup(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
if (!command.GetArg(1).StartsWith("#"))
{
command.ReplyToCommand($"Group name must start with #.");
return;
}
if (!command.GetArg(2).StartsWith($"@") && !command.GetArg(2).StartsWith($"#"))
{
command.ReplyToCommand($"Invalid flag or group.");
return;
}
var groupName = command.GetArg(1);
var flags = command.GetArg(2);
int.TryParse(command.GetArg(3), out var immunity);
var globalGroup = command.GetArg(4).ToLower().Equals("-g");
AddGroup(caller, groupName, flags, immunity, globalGroup, command);
}
///
/// Adds a new admin group with specified flags and immunity level.
///
/// The player issuing the command.
/// Group name (prefix with #).
/// Comma-separated flags/groups string.
/// Immunity level.
/// Whether group is global.
/// Optional command info.
private static void AddGroup(CCSPlayerController? caller, string name, string flags, int immunity, bool globalGroup, CommandInfo? command = null)
{
if (DatabaseProvider == null) return;
var flagsList = flags.Split(',').Select(flag => flag.Trim()).ToList();
_ = Instance.PermissionManager.AddGroup(name, flagsList, immunity, globalGroup);
Helper.LogCommand(caller, $"css_addgroup {name} {flags} {immunity}");
var msg = $"Created group '{name}' with flags '{flags}'";
if (command != null)
command.ReplyToCommand(msg);
else if (caller != null && caller.IsValid)
caller.PrintToChat(msg);
else
Server.PrintToConsole(msg);
}
///
/// Handles removing a group by name.
///
/// The player issuing the command.
/// Command input parameters.
[CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/root")]
public void OnDelGroupCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
if (!command.GetArg(1).StartsWith($"#"))
{
command.ReplyToCommand($"Group name must start with #.");
return;
}
var groupName = command.GetArg(1);
RemoveGroup(caller, groupName, command);
}
///
/// Removes a group.
///
/// The player issuing the command.
/// The group name to remove.
/// Optional command info for confirmation.
private void RemoveGroup(CCSPlayerController? caller, string name, CommandInfo? command = null)
{
if (DatabaseProvider == null) return;
_ = PermissionManager.DeleteGroup(name);
AddTimer(2, () =>
{
ReloadAdmins(caller);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
Helper.LogCommand(caller, $"css_delgroup {name}");
var msg = $"Removed group '{name}'";
if (command != null)
command.ReplyToCommand(msg);
else if (caller != null && caller.IsValid)
caller.PrintToChat(msg);
else
Server.PrintToConsole(msg);
}
///
/// Reloads admin and group data from database and json files.
///
/// The player issuing the reload command.
/// Command input parameters.
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/root")]
public void OnRelAdminCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
ReloadAdmins(caller);
command.ReplyToCommand("Reloaded sql admins and groups");
}
///
/// Reloads bans cache.
///
/// The player issuing the reload command.
/// Command input parameters.
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/root")]
public void OnRelBans(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
_ = Instance.CacheManager?.ForceReInitializeCacheAsync();
command.ReplyToCommand("Reloaded bans");
}
///
/// Reloads admin data asynchronously and updates admin caches.
///
/// The player issuing the reload command.
public void ReloadAdmins(CCSPlayerController? caller)
{
if (DatabaseProvider == null) return;
Task.Run(async () =>
{
await PermissionManager.CrateGroupsJsonFile();
await PermissionManager.CreateAdminsJsonFile();
var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json");
var groupsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/groups.json");
await Server.NextWorldUpdateAsync(() =>
{
AddTimer(1, () =>
{
if (!string.IsNullOrEmpty(adminsFile))
AddTimer(2.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json"));
if (!string.IsNullOrEmpty(groupsFile))
AddTimer(3.0f, () => AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json"));
if (!string.IsNullOrEmpty(adminsFile))
AddTimer(4.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json"));
_logger?.LogInformation("Loaded admins!");
});
});
});
//_ = _adminManager.GiveAllGroupsFlags();
//_ = _adminManager.GiveAllFlags();
}
///
/// Toggles player visibility on the server, hiding or revealing them.
///
/// The player issuing the hide command.
/// Command input parameters.
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
[RequiresPermissions("@css/kick")]
public void OnHideCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller == null) return;
Helper.LogCommand(caller, command);
if (!SilentPlayers.Add(caller.Slot))
{
SilentPlayers.Remove(caller.Slot);
caller.PrintToChat($"You aren't hidden now!");
if (caller.TeamNum <= 1)
caller.ChangeTeam(CsTeam.Spectator);
SimpleAdminApi?.OnAdminToggleSilentEvent(caller.Slot, false);
}
else
{
Server.ExecuteCommand("sv_disable_teamselect_menu 1");
if (caller.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
caller.PlayerPawn.Value?.CommitSuicide(true, false);
caller.PrintToChat($"You are hidden now!");
if (caller.TeamNum > 1)
AddTimer(0.15f, () => { Server.NextWorldUpdate(() => caller.ChangeTeam(CsTeam.Spectator)); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
AddTimer(0.26f, () => { Server.NextWorldUpdate(() => caller.ChangeTeam(CsTeam.None)); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
AddTimer(0.50f, () => { Server.NextWorldUpdate(() => Server.ExecuteCommand("sv_disable_teamselect_menu 0")); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
SimpleAdminApi?.OnAdminToggleSilentEvent(caller.Slot, true);
}
}
///
/// Toggles penalty notification visibility to admins.
///
/// The player toggling notification visibility.
/// Command input parameters.
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
[RequiresPermissions("@css/kick")]
public void OnHideCommsCommand(CCSPlayerController? caller, CommandInfo command)
{
if (caller == null)
return;
if (!AdminDisabledJoinComms.Add(caller.SteamID))
{
AdminDisabledJoinComms.Remove(caller.SteamID);
command.ReplyToCommand("From now on, you'll see penalty notifications");
}
else
{
AdminDisabledJoinComms.Add(caller.SteamID);
command.ReplyToCommand($"You don't see penalty notifications now");
}
}
///
/// Displays detailed information about target players, including admin groups, permissions, and penalties.
///
/// The player issuing the command.
/// Command input parameters including targets.
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/generic")]
public void OnWhoCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var targets = GetTarget(command);
if (targets == null) return;
Helper.LogCommand(caller, command);
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (!player.UserId.HasValue) return;
if (!caller!.CanTarget(player)) return;
var playerInfo = PlayersInfo[player.SteamID];
Task.Run(async () =>
{
await Server.NextWorldUpdateAsync(() =>
{
Action printMethod = caller == null ? Server.PrintToConsole : caller.PrintToConsole;
var adminData = AdminManager.GetPlayerAdminData(new SteamID(player.SteamID));
printMethod($"--------- INFO ABOUT \"{playerInfo.Name}\" ---------");
printMethod($"• Clan: \"{player.Clan}\" Name: \"{playerInfo.Name}\"");
printMethod($"• UserID: \"{playerInfo.UserId}\"");
printMethod($"• SteamID64: \"{playerInfo.SteamId.SteamId64}\"");
if (adminData != null)
{
var flags = string.Join(",", adminData._flags);
var groups = string.Join(",", adminData.Groups);
printMethod($"• Groups/Flags: \"{groups}{flags}\"");
}
printMethod($"• SteamID2: \"{playerInfo.SteamId.SteamId2}\"");
printMethod($"• Community link: \"{playerInfo.SteamId.ToCommunityUrl()}\"");
if (playerInfo.IpAddress != null && AdminManager.PlayerHasPermissions(new SteamID(caller!.SteamID), "@css/showip"))
printMethod($"• IP Address: \"{playerInfo.IpAddress}\"");
printMethod($"• Ping: \"{player.Ping}\"");
printMethod($"• Total Bans: \"{playerInfo.TotalBans}\"");
printMethod($"• Total Gags: \"{playerInfo.TotalGags}\"");
printMethod($"• Total Mutes: \"{playerInfo.TotalMutes}\"");
printMethod($"• Total Silences: \"{playerInfo.TotalSilences}\"");
printMethod($"• Total Warns: \"{playerInfo.TotalWarns}\"");
var chunkedAccounts = playerInfo.AccountsAssociated.ChunkBy(3).ToList();
foreach (var chunk in chunkedAccounts)
printMethod($"• Associated Accounts: \"{string.Join(", ", chunk.Select(a => $"{a.PlayerName} ({a.SteamId})"))}\"");
printMethod($"--------- END INFO ABOUT \"{player.PlayerName}\" ---------");
});
});
});
}
///
/// Displays a menu with disconnected players, allowing the caller to apply penalties like ban, mute, gag, or silence.
///
/// The player issuing the command.
/// The command containing parameters.
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
[RequiresPermissions("@css/kick")]
public void OnDisconnectedCommand(CCSPlayerController? caller, CommandInfo command)
{
if (_localizer == null || caller == null) return;
var disconnectedMenu = Helper.CreateMenu(_localizer["sa_menu_disconnected_title"]);
DisconnectedPlayers.ForEach(player =>
{
disconnectedMenu?.AddMenuOption(player.Name, (_, _) =>
{
var disconnectedMenuAction = Helper.CreateMenu(_localizer["sa_menu_disconnected_action_title"]);
disconnectedMenuAction?.AddMenuOption(_localizer["sa_ban"], (_, _) =>
{
DurationMenu.OpenMenu(caller, _localizer["sa_ban"], player, (_, _, duration) =>
ReasonMenu.OpenMenu(caller, PenaltyType.Ban, _localizer["sa_reason"], player, (_, _, reason) =>
{
caller.ExecuteClientCommandFromServer($"css_addban {player.SteamId.SteamId64} {duration} \"{reason}\"");
// Determine message keys and arguments based on ban time
var (_, activityMessageKey, _, adminActivityArgs) = duration == 0
? ("sa_player_ban_message_perm", "sa_admin_ban_message_perm",
[reason, "CALLER"],
["CALLER", player.Name, reason])
: ("sa_player_ban_message_time", "sa_admin_ban_message_time",
new object[] { reason, duration, "CALLER" },
new object[] { "CALLER", player.Name, reason, duration });
// Display admin activity message if necessary
if (!SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, false ,adminActivityArgs);
}
MenuApi?.CloseMenu(caller);
}));
});
disconnectedMenuAction?.AddMenuOption(_localizer["sa_mute"], (_, _) =>
{
DurationMenu.OpenMenu(caller, _localizer["sa_mute"], player, (_, _, duration) =>
ReasonMenu.OpenMenu(caller, PenaltyType.Mute, _localizer["sa_reason"], player, (_, _, reason) =>
{
caller.ExecuteClientCommandFromServer($"css_addmute {player.SteamId.SteamId64} {duration} \"{reason}\"");
// Determine message keys and arguments based on mute time (permanent or timed)
var (_, activityMessageKey, _, adminActivityArgs) = duration == 0
? ("sa_player_mute_message_perm", "sa_admin_mute_message_perm",
[reason, "CALLER"],
["CALLER", player.Name, reason])
: ("sa_player_mute_message_time", "sa_admin_mute_message_time",
new object[] { reason, duration, "CALLER" },
new object[] { "CALLER", player.Name, reason, duration });
// Display admin activity message to other players
if (!SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, false, adminActivityArgs);
}
MenuApi?.CloseMenu(caller);
}));
});
disconnectedMenuAction?.AddMenuOption(_localizer["sa_gag"], (_, _) =>
{
DurationMenu.OpenMenu(caller, _localizer["sa_gag"], player, (_, _, duration) =>
ReasonMenu.OpenMenu(caller, PenaltyType.Mute, _localizer["sa_reason"], player, (_, _, reason) =>
{
caller.ExecuteClientCommandFromServer($"css_addgag {player.SteamId.SteamId64} {duration} \"{reason}\"");
// Determine message keys and arguments based on gag time (permanent or timed)
var (_, activityMessageKey, _, adminActivityArgs) = duration == 0
? ("sa_player_gag_message_perm", "sa_admin_gag_message_perm",
[reason, "CALLER"],
["CALLER", player.Name, reason])
: ("sa_player_gag_message_time", "sa_admin_gag_message_time",
new object[] { reason, duration, "CALLER" },
new object[] { "CALLER", player.Name, reason, duration});
// Display admin activity message to other players
if (!SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, false, adminActivityArgs);
}
MenuApi?.CloseMenu(caller);
}));
});
disconnectedMenuAction?.AddMenuOption(_localizer["sa_silence"], (_, _) =>
{
DurationMenu.OpenMenu(caller, _localizer["sa_silence"], player, (_, _, duration) =>
ReasonMenu.OpenMenu(caller, PenaltyType.Mute, _localizer["sa_reason"], player, (_, _, reason) =>
{
caller.ExecuteClientCommandFromServer($"css_addsilence {player.SteamId.SteamId64} {duration} \"{reason}\"");
// Determine message keys and arguments based on silence time (permanent or timed)
var (_, activityMessageKey, _, adminActivityArgs) = duration == 0
? ("sa_player_silence_message_perm", "sa_admin_silence_message_perm",
[reason, "CALLER"],
["CALLER", player.Name, reason])
: ("sa_player_silence_message_time", "sa_admin_silence_message_time",
new object[] { reason, duration, "CALLER" },
new object[] { "CALLER", player.Name, reason, duration });
// Display admin activity message to other players
if (!SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, false, adminActivityArgs);
}
MenuApi?.CloseMenu(caller);
}));
});
disconnectedMenuAction?.Open(caller);
});
});
disconnectedMenu?.Open(caller);
}
///
/// Displays the warning menu for a player specified by a command argument,
/// showing active and past warns with options to remove them.
///
/// The player issuing the command.
/// The command containing target player identifier.
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_ONLY)]
[RequiresPermissions("@css/kick")]
public void OnWarnsCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null || _localizer == null || caller == null) return;
var targets = GetTarget(command);
if (targets == null) return;
Helper.LogCommand(caller, command);
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsBot: false }).ToList();
if (playersToTarget.Count > 1)
return;
playersToTarget.ForEach(player =>
{
if (!player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
var userId = player.UserId.Value;
var steamId = player.SteamID;
IMenu? warnsMenu = Helper.CreateMenu(_localizer["sa_admin_warns_menu_title", player.PlayerName]);
Task.Run(async () =>
{
var warnsList = await WarnManager.GetPlayerWarns(PlayersInfo[steamId], false);
var sortedWarns = warnsList
.OrderBy(warn => (string)warn.status == "ACTIVE" ? 0 : 1)
.ThenByDescending(warn => (int)warn.id)
.ToList();
sortedWarns.ForEach(w =>
{
warnsMenu?.AddMenuOption($"[{((string)w.status == "ACTIVE" ? $"{ChatColors.LightRed}X" : $"{ChatColors.Lime}✔️")}{ChatColors.Default}] {(string)w.reason}",
(controller, option) =>
{
_ = WarnManager.UnwarnPlayer(PlayersInfo[steamId], (int)w.id);
player.PrintToChat(_localizer["sa_admin_warns_unwarn", player.PlayerName, (string)w.reason]);
});
});
await Server.NextWorldUpdateAsync(() =>
{
warnsMenu?.Open(caller);
});
});
});
}
///
/// Lists players currently connected to the server with options to output JSON or filter duplicate IPs.
///
/// The player issuing the command or null for console.
/// The command containing output options.
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
[RequiresPermissions("@css/generic")]
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")]
public void OnPlayersCommand(CCSPlayerController? caller, CommandInfo command)
{
var isJson = command.GetArg(1).ToLower().Equals("-json");
var isDuplicate = command.GetArg(1).ToLower().Equals("-duplicate") || command.GetArg(2).ToLower().Equals("-duplicate");
var playersToTarget = isDuplicate
? Helper.GetValidPlayers().GroupBy(player => player.IpAddress?.Split(":")[0] ?? "Unknown")
.Where(group => group.Count() > 1)
.SelectMany(group => group)
.ToList()
: Helper.GetValidPlayers();
if (!isJson)
{
if (caller != null)
{
caller.PrintToConsole("--------- PLAYER LIST ---------");
foreach (var player in playersToTarget)
{
caller.PrintToConsole(
$"• [#{player.UserId}] \"{player.PlayerName}\" (IP Address: \"{(AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/showip") ? player.IpAddress?.Split(":")[0] : "Unknown")}\" SteamID64: \"{player.SteamID}\")");
};
caller.PrintToConsole("--------- END PLAYER LIST ---------");
}
else
{
Server.PrintToConsole("--------- PLAYER LIST ---------");
foreach (var player in playersToTarget)
{
Server.PrintToConsole($"• [#{player.UserId}] \"{player.PlayerName}\" (IP Address: \"{player.IpAddress?.Split(":")[0]}\" SteamID64: \"{player.SteamID}\")");
};
Server.PrintToConsole("--------- END PLAYER LIST ---------");
}
}
else
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
var playerDtos = playersToTarget
.Where(player => player.UserId.HasValue)
.Select(player =>
{
var matchStats = player.ActionTrackingServices?.MatchStats;
return new PlayerDto(
player.UserId.GetValueOrDefault(0),
player.PlayerName,
player.SteamID.ToString(),
AdminManager.PlayerHasPermissions(new SteamID(caller!.SteamID), "@css/showip")
? player.IpAddress?.Split(":")[0] ?? "Unknown"
: "Unknown",
player.Ping,
AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/ban")
|| AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/generic"),
new PlayerStats(
player.Score,
matchStats?.Kills ?? 0,
matchStats?.Deaths ?? 0,
player.MVPs
)
);
})
.ToList();
var playersJson = JsonSerializer.Serialize(playerDtos, options);
if (caller != null)
caller.PrintToConsole(playersJson);
else
Server.PrintToConsole(playersJson);
}
}
///
/// Issues a kick to one or multiple players specified in the command arguments.
///
/// The player issuing the kick command.
/// The command with target player(s) and optional reason.
[RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnKickCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players
.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{
return;
}
var reason = command.ArgCount >= 2
? string.Join(" ", Enumerable.Range(2, command.ArgCount - 2).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
playersToTarget.ForEach(player =>
{
if (!player.IsValid)
return;
if (caller!.CanTarget(player))
{
Kick(caller, player, reason, callerName, command);
}
});
Helper.LogCommand(caller, command);
}
///
/// Kicks a specified player immediately with reason, notifying the server and logging the action.
///
/// The player issuing the kick.
/// The player to be kicked.
/// The reason for the kick.
/// Optional name of the kick issuer for notifications.
/// Optional command for logging.
public void Kick(CCSPlayerController? caller, CCSPlayerController player, string? reason = "Unknown", string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
if (!player.UserId.HasValue) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
reason ??= _localizer?["sa_unknown"] ?? "Unknown";
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Determine message keys and arguments for the kick notification
var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) =
("sa_player_kick_message", "sa_admin_kick_message",
new object[] { reason, "CALLER" },
new object[] { "CALLER", player.PlayerName, reason });
// Display center message to the kicked player
Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs);
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Schedule the kick for the player
if (player.UserId.HasValue)
{
Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED, Config.OtherSettings.KickTime);
}
// Log the command and send Discord notification
if (command == null)
Helper.LogCommand(caller, $"css_kick {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {reason}");
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Kick, reason, -1, null);
}
///
/// Changes the current map to the specified map name or workshop map ID.
///
/// The player issuing the map change.
/// The command containing the map name or ID.
[RequiresPermissions("@css/changemap")]
[CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnMapCommand(CCSPlayerController? caller, CommandInfo command)
{
var map = command.GetCommandString.Split(" ")[1];
ChangeMap(caller, map, command);
}
///
/// Changes to a specified map, validating it or handling workshop maps, and notifying the server and admins.
///
/// The player issuing the change.
/// The map name or identifier.
/// Optional command object for logging and replies.
public void ChangeMap(CCSPlayerController? caller, string map, CommandInfo? command = null)
{
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
map = map.ToLower();
if (map.StartsWith("ws:"))
{
var issuedCommand = long.TryParse(map.Replace("ws:", ""), out var mapId)
? $"host_workshop_map {mapId}"
: $"ds_workshop_changelevel {map.Replace("ws:", "")}";
AddTimer(3.0f, () =>
{
Server.ExecuteCommand(issuedCommand);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
else
{
if (!Server.IsMapValid(map))
{
var msg = $"Map {map} not found.";
if (command != null)
command.ReplyToCommand(msg);
else if (caller != null && caller.IsValid)
caller.PrintToChat(msg);
else
Server.PrintToConsole(msg);
return;
}
AddTimer(3.0f, () =>
{
Server.ExecuteCommand($"changelevel {map}");
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
var (activityMessageKey, adminActivityArgs) =
("sa_admin_changemap_message",
new object[] { "CALLER", map });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
Helper.LogCommand(caller, command?.GetCommandString ?? $"css_map {map}");
}
///
/// Changes the current map to a workshop map specified by name or ID.
///
/// The player issuing the command.
/// The command containing the workshop map identifier.
[CommandHelper(1, "")]
[RequiresPermissions("@css/changemap")]
public void OnWorkshopMapCommand(CCSPlayerController? caller, CommandInfo command)
{
var map = command.GetArg(1);
ChangeWorkshopMap(caller, map, command);
}
///
/// Changes to a specified workshop map by name or ID and notifies admins.
///
/// The player issuing the command.
/// The workshop map identifier.
/// Optional command for logging.
public void ChangeWorkshopMap(CCSPlayerController? caller, string map, CommandInfo? command = null)
{
map = map.ToLower();
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Determine the workshop command
var issuedCommand = long.TryParse(map, out var mapId)
? $"host_workshop_map {mapId}"
: $"ds_workshop_changelevel {map}";
// Define the admin activity message and arguments
var activityMessageKey = "sa_admin_changemap_message";
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, ["CALLER", map]);
}
// Add timer to execute the map change command after a delay
AddTimer(3.0f, () =>
{
Server.ExecuteCommand(issuedCommand);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
// Log the command for the change
Helper.LogCommand(caller, command?.GetCommandString ?? $"css_wsmap {map}");
}
///
/// Allows changing a console variable's value.
///
/// The player issuing the command.
/// The command with cvar name and value.
[CommandHelper(2, " ")]
[RequiresPermissions("@css/cvar")]
public void OnCvarCommand(CCSPlayerController? caller, CommandInfo command)
{
var cvar = ConVar.Find(command.GetArg(1));
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
if (cvar == null)
{
command.ReplyToCommand($"Cvar \"{command.GetArg(1)}\" not found.");
return;
}
if (cvar.Name.Equals("sv_cheats") && !AdminManager.PlayerHasPermissions(new SteamID(caller!.SteamID), "@css/cheats"))
{
command.ReplyToCommand($"You don't have permissions to change \"{command.GetArg(1)}\".");
return;
}
Helper.LogCommand(caller, command);
var value = command.GetArg(2);
Server.ExecuteCommand($"{cvar.Name} {value}");
command.ReplyToCommand($"{callerName} changed cvar {cvar.Name} to {value}.");
Logger.LogInformation($"{callerName} changed cvar {cvar.Name} to {value}.");
}
///
/// Executes an RCON command on the server.
///
/// The player issuing the command.
/// The command string to execute via RCON.
[CommandHelper(1, "")]
[RequiresPermissions("@css/rcon")]
public void OnRconCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
Helper.LogCommand(caller, command);
Server.ExecuteCommand(command.ArgString);
command.ReplyToCommand($"{callerName} executed command {command.ArgString}.");
Logger.LogInformation($"{callerName} executed command ({command.ArgString}).");
}
///
/// Restarts the game.
///
/// The player or console initiating the restart.
/// The restart command info.
[RequiresPermissions("@css/generic")]
[CommandHelper(minArgs: 0, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnRestartCommand(CCSPlayerController? caller, CommandInfo command)
{
RestartGame(caller);
}
///
/// Opens plugin manager menu for the caller with options to load or unload plugins.
///
/// The player opening the plugin manager.
/// The command parameters.
[RequiresPermissions("@css/root")]
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
public void OnPluginManagerCommand(CCSPlayerController? caller, CommandInfo commandInfo)
{
if (MenuApi == null || caller == null)
return;
var pluginManager = Helper.GetPluginManager();
if (pluginManager == null)
{
Logger.LogError("Unable to access PluginManager.");
return;
}
var getLoadedPluginsMethod = pluginManager.GetType().GetMethod("GetLoadedPlugins", BindingFlags.Public | BindingFlags.Instance);
if (getLoadedPluginsMethod?.Invoke(pluginManager, null) is not IEnumerable plugins)
{
Logger.LogError("Unable to retrieve plugins.");
return;
}
var pluginsMenu = Helper.CreateMenu(Localizer["sa_menu_pluginsmanager_title"]);
foreach (var plugin in plugins)
{
var pluginType = plugin.GetType();
// Accessing each property with the Type of the plugin
var pluginId = pluginType.GetProperty("PluginId")?.GetValue(plugin);
var state = pluginType.GetProperty("State")?.GetValue(plugin)?.ToString();
var path = pluginType.GetProperty("FilePath")?.GetValue(plugin)?.ToString();
path = Path.GetFileName(Path.GetDirectoryName(path));
// Access nested properties within "Plugin" (like ModuleName, ModuleVersion, etc.)
var nestedPlugin = pluginType.GetProperty("Plugin")?.GetValue(plugin);
if (nestedPlugin == null) continue;
var status = state?.ToUpper() != "UNLOADED" ? "ON" : "OFF";
var allowedMenuTypes = new[] { "chat", "console" };
if (!allowedMenuTypes.Contains(Config.MenuConfigs.MenuType) && MenuApi.GetMenuType(caller) >= MenuType.CenterMenu)
status = state?.ToUpper() != "UNLOADED" ? "ON" : "OFF";
var nestedType = nestedPlugin.GetType();
var moduleName = nestedType.GetProperty("ModuleName")?.GetValue(nestedPlugin)?.ToString() ?? "Unknown";
var moduleVersion = nestedType.GetProperty("ModuleVersion")?.GetValue(nestedPlugin)?.ToString();
// var moduleAuthor = nestedType.GetProperty("ModuleAuthor")?.GetValue(nestedPlugin)?.ToString();
// var moduleDescription = nestedType.GetProperty("ModuleDescription")?.GetValue(nestedPlugin)?.ToString();
pluginsMenu?.AddMenuOption($"({status}) [{moduleName} {moduleVersion}]", (_, _) =>
{
if (state?.ToUpper() != "UNLOADED")
{
caller.SendLocalizedMessage(Localizer, "sa_menu_pluginsmanager_unloaded", moduleName);
Server.ExecuteCommand($"css_plugins unload {pluginId}");
}
else
{
caller.SendLocalizedMessage(Localizer, "sa_menu_pluginsmanager_loaded", moduleName);
Server.ExecuteCommand($"css_plugins load {path}");
}
AddTimer(0.1f, () => OnPluginManagerCommand(caller, commandInfo));
});
// Console.WriteLine($"[#{pluginId}:{state?.ToUpper()}]: \"{moduleName ?? "Unknown"}\" ({moduleVersion ?? "Unknown"}) by {moduleAuthor}");
}
pluginsMenu?.Open(caller);
}
///
/// Restarts the game process by issuing the restart game command to the server and logging the action.
///
/// The admin or console requesting the restart.
public static void RestartGame(CCSPlayerController? admin)
{
Helper.LogCommand(admin, "css_restartgame");
// TODO: Localize
var name = admin == null ? _localizer?["sa_console"] ?? "Console" : admin.PlayerName;
Server.PrintToChatAll($"[SA] {name}: Restarting game...");
Server.ExecuteCommand("mp_restartgame 2");
}
}