using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Utils; using CS2_SimpleAdminApi; using Microsoft.Extensions.Localization; namespace CS2_SimpleAdmin.Menus; public static class BasicMenu { private const int MaxPlayerNameLength = 26; private const int MaxHistoryLabelLength = 40; /// /// Initializes all menus in the system by registering them with the MenuManager. /// Register with translation keys instead of static names - translation happens per-player. /// public static void Initialize() { MenuManager manager = MenuManager.Instance; // Players category menus manager.RegisterMenu("players", "slap", "sa_slap", CreateSlapMenu, "@css/slay"); manager.RegisterMenu("players", "slay", "sa_slay", CreateSlayMenu, "@css/slay"); manager.RegisterMenu("players", "kick", "sa_kick", CreateKickMenu, "@css/kick"); manager.RegisterMenu("players", "warn", "sa_warn", admin => CreatePenaltyPlayersMenu(admin, PenaltyType.Warn), "@css/kick"); manager.RegisterMenu("players", "ban", "sa_ban", admin => CreatePenaltyPlayersMenu(admin, PenaltyType.Ban), "@css/ban"); manager.RegisterMenu("players", "gag", "sa_gag", admin => CreatePenaltyPlayersMenu(admin, PenaltyType.Gag), "@css/chat"); manager.RegisterMenu("players", "mute", "sa_mute", admin => CreatePenaltyPlayersMenu(admin, PenaltyType.Mute), "@css/chat"); manager.RegisterMenu("players", "silence", "sa_silence", admin => CreatePenaltyPlayersMenu(admin, PenaltyType.Silence), "@css/chat"); manager.RegisterMenu("players", "team", "sa_team_force", CreateForceTeamMenu, "@css/kick"); manager.RegisterMenu("players", "history", "sa_history", CreateHistoryMenu, "@css/kick"); // Server category menus manager.RegisterMenu("server", "plugins", "sa_menu_pluginsmanager_title", CreatePluginsMenu, "@css/root"); manager.RegisterMenu("server", "changemap", "sa_changemap", CreateChangeMapMenu, "@css/changemap"); manager.RegisterMenu("server", "restart", "sa_restart_game", CreateRestartGameMenu, "@css/generic"); manager.RegisterMenu("server", "custom", "sa_menu_custom_commands", CreateCustomCommandsMenu, "@css/generic"); // Admin category menus manager.RegisterMenu("admin", "add", "sa_admin_add", CreateAddAdminMenu, "@css/root"); manager.RegisterMenu("admin", "remove", "sa_admin_remove", CreateRemoveAdminMenu, "@css/root"); manager.RegisterMenu("admin", "reload", "sa_admin_reload", CreateReloadAdminsMenu, "@css/root"); } /// /// Duration then reason selection for a penalty on an online player, applied on the last click. /// /// The admin applying the penalty. /// The target player. /// Ban, Warn, Gag, Mute or Silence. /// A MenuBuilder instance for the duration menu. internal static MenuBuilder CreatePenaltyMenu(CCSPlayerController admin, CCSPlayerController target, PenaltyType penaltyType) { return CreateDurationMenu(admin, target.PlayerName, penaltyType, duration => CreateReasonMenu(admin, target.PlayerName, penaltyType, reason => ApplyPenalty(admin, target, penaltyType, duration, reason))); } /// /// Creates duration selection menu for time-based penalties (ban, mute, gag, etc.). /// /// The admin player selecting duration. /// The name of the target shown in the title. /// The penalty being applied. /// Returns the next menu when a duration is selected. /// A MenuBuilder instance for the duration menu. internal static MenuBuilder CreateDurationMenu(CCSPlayerController admin, string targetName, PenaltyType penaltyType, Func onSelect) { MenuBuilder durationMenu = new MenuBuilder(GetPenaltyTitle(admin, penaltyType, "sa_duration", "Duration", targetName)); foreach (DurationItem durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) { int duration = durationItem.Duration; durationMenu.AddSubMenu(durationItem.Name, () => onSelect(duration)); } return durationMenu.WithBackButton(); } /// /// Creates reason selection menu for penalties with predefined reasons from configuration. /// /// The admin player selecting reason. /// The name of the target shown in the title. /// The type of penalty to determine which reason list to use. /// Executed when a reason is selected. /// A MenuBuilder instance for the reason menu. internal static MenuBuilder CreateReasonMenu(CCSPlayerController admin, string targetName, PenaltyType penaltyType, Action onSelect) { MenuBuilder reasonMenu = new MenuBuilder(GetPenaltyTitle(admin, penaltyType, "sa_reason", "Reason", targetName)); foreach (string reason in GetReasons(penaltyType)) { reasonMenu.AddOption(reason, _ => onSelect(reason)); } return reasonMenu.WithBackButton(); } private static void ApplyPenalty(CCSPlayerController admin, CCSPlayerController target, PenaltyType penaltyType, int duration, string reason) { if (!target.IsValid) { return; } switch (penaltyType) { case PenaltyType.Ban: CS2_SimpleAdmin.Instance.Ban(admin, target, duration, reason, admin.PlayerName); break; case PenaltyType.Warn: CS2_SimpleAdmin.Instance.Warn(admin, target, duration, reason, admin.PlayerName); break; case PenaltyType.Gag: CS2_SimpleAdmin.Instance.Gag(admin, target, duration, reason); break; case PenaltyType.Mute: CS2_SimpleAdmin.Instance.Mute(admin, target, duration, reason); break; case PenaltyType.Silence: CS2_SimpleAdmin.Instance.Silence(admin, target, duration, reason); break; } } private static List GetReasons(PenaltyType penaltyType) { MenuConfig menuConfig = CS2_SimpleAdmin.Instance.Config.MenuConfigs; return penaltyType switch { PenaltyType.Kick => menuConfig.KickReasons, PenaltyType.Warn => menuConfig.WarnReasons, PenaltyType.Mute or PenaltyType.Gag or PenaltyType.Silence => menuConfig.MuteReasons, _ => menuConfig.BanReasons }; } private static string GetPenaltyKey(PenaltyType penaltyType) { return penaltyType switch { PenaltyType.Kick => "sa_kick", PenaltyType.Warn => "sa_warn", PenaltyType.Gag => "sa_gag", PenaltyType.Mute => "sa_mute", PenaltyType.Silence => "sa_silence", _ => "sa_ban" }; } /// /// "Ban Duration: name", in the admin's language. /// private static string GetPenaltyTitle(CCSPlayerController admin, PenaltyType penaltyType, string stepKey, string stepFallback, string targetName) { IStringLocalizer? localizer = CS2_SimpleAdmin._localizer; string penaltyKey = GetPenaltyKey(penaltyType); using (new WithTemporaryCulture(admin.GetLanguage())) { string action = localizer?[penaltyKey] ?? penaltyType.ToString(); string step = localizer?[stepKey] ?? stepFallback; return $"{action} {step}: {targetName}"; } } private static string GetLocalized(CCSPlayerController admin, string key, string fallback) { using (new WithTemporaryCulture(admin.GetLanguage())) { return CS2_SimpleAdmin._localizer?[key] ?? fallback; } } /// /// One entry per targetable player, each opening the penalty flow for that player. /// private static MenuBuilder CreatePenaltyPlayersMenu(CCSPlayerController admin, PenaltyType penaltyType) { MenuBuilder playersMenu = new MenuBuilder(GetPenaltyKey(penaltyType), admin); foreach (CCSPlayerController player in Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p))) { CCSPlayerController target = player; playersMenu.AddSubMenu(Helper.Truncate(target.PlayerName, MaxPlayerNameLength), () => CreatePenaltyMenu(admin, target, penaltyType)); } return playersMenu.WithBackButton(); } private static MenuBuilder CreateSlapMenu(CCSPlayerController admin) { MenuBuilder slapMenu = new MenuBuilder("sa_slap", admin); foreach (CCSPlayerController player in Helper.GetValidPlayers().Where(admin.CanTarget)) { CCSPlayerController target = player; slapMenu.AddSubMenu(Helper.Truncate(target.PlayerName, MaxPlayerNameLength), () => CreateSlapDamageMenu(admin, target)); } return slapMenu.WithBackButton(); } private static MenuBuilder CreateSlapDamageMenu(CCSPlayerController admin, CCSPlayerController target) { MenuBuilder slapDamageMenu = new MenuBuilder($"{GetLocalized(admin, "sa_slap", "Slap")}: {target.PlayerName}"); int[] damages = [0, 1, 5, 10, 50, 100]; foreach (int damage in damages) { slapDamageMenu.AddOption($"{damage} HP", currentAdmin => { if (target.IsValid) { CS2_SimpleAdmin.Slap(currentAdmin, target, damage); } }, keepOpen: true); } return slapDamageMenu.WithBackButton(); } private static MenuBuilder CreateSlayMenu(CCSPlayerController admin) { MenuBuilder slayMenu = new MenuBuilder("sa_slay", admin); foreach (CCSPlayerController player in Helper.GetValidPlayers().Where(admin.CanTarget)) { CCSPlayerController target = player; slayMenu.AddOption(Helper.Truncate(target.PlayerName, MaxPlayerNameLength), _ => { if (target.IsValid) { CS2_SimpleAdmin.Slay(admin, target); } }); } return slayMenu.WithBackButton(); } /// /// Pick an online player, then one entry per past penalty. /// Selecting an entry prints its full details to the admin's console. /// private static MenuBuilder CreateHistoryMenu(CCSPlayerController admin) { MenuBuilder historyMenu = new MenuBuilder("sa_history", admin); foreach (CCSPlayerController player in Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p))) { CCSPlayerController target = player; historyMenu.AddOption(Helper.Truncate(target.PlayerName, MaxPlayerNameLength), _ => OpenPlayerHistory(admin, target), keepOpen: true); } return historyMenu.WithBackButton(); } private static void OpenPlayerHistory(CCSPlayerController admin, CCSPlayerController target) { ulong steamId = target.SteamID; string name = target.PlayerName; ulong adminSteamId = admin.SteamID; // The list is built after the DB round trip, so it is opened from the world update Task.Run(async () => { List rows = await CS2_SimpleAdmin.PlayerManager.GetPenaltyHistory(steamId); await Server.NextWorldUpdateAsync(() => { // The slot may have been reused by another player while the query ran if (!admin.IsValid || admin.SteamID != adminSteamId) { return; } if (rows.Count == 0) { CS2_SimpleAdmin.Instance.PrintHistory(admin.PrintToConsole, name, steamId, rows); return; } MenuBuilder historyList = new MenuBuilder("sa_history", admin); foreach (dynamic row in rows) { dynamic capturedRow = row; string label = Helper.Truncate($"{GetStatusMark((string)row.status)} {(string)row.type} {(string)row.reason}", MaxHistoryLabelLength); historyList.AddOption(label, _ => CS2_SimpleAdmin.Instance.PrintHistory(admin.PrintToConsole, name, steamId, [capturedRow]), keepOpen: true); } historyList.WithBackButton().OpenMenu(admin); }); }); } /// /// ✖ for an active penalty, ✔ for one that is over. /// internal static string GetStatusMark(string status) { if (status == "ACTIVE") { return "✖"; } return "✔"; } private static MenuBuilder CreateKickMenu(CCSPlayerController admin) { MenuBuilder kickMenu = new MenuBuilder("sa_kick", admin); foreach (CCSPlayerController player in Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p))) { CCSPlayerController target = player; kickMenu.AddSubMenu(Helper.Truncate(target.PlayerName, MaxPlayerNameLength), () => CreateReasonMenu(admin, target.PlayerName, PenaltyType.Kick, reason => { if (target.IsValid) { CS2_SimpleAdmin.Instance.Kick(admin, target, reason, admin.PlayerName); } })); } return kickMenu.WithBackButton(); } private static MenuBuilder CreateForceTeamMenu(CCSPlayerController admin) { MenuBuilder teamMenu = new MenuBuilder("sa_team_force", admin); foreach (CCSPlayerController player in Helper.GetValidPlayers().Where(admin.CanTarget)) { CCSPlayerController target = player; teamMenu.AddSubMenu(Helper.Truncate(target.PlayerName, MaxPlayerNameLength), () => CreateTeamSelectionMenu(admin, target)); } return teamMenu.WithBackButton(); } private static MenuBuilder CreateTeamSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { MenuBuilder teamSelectionMenu = new MenuBuilder($"{GetLocalized(admin, "sa_team_force", "Force Team")}: {target.PlayerName}"); (string Name, string TeamName, CsTeam TeamNum)[] teams = [ (GetLocalized(admin, "sa_team_ct", "CT"), "ct", CsTeam.CounterTerrorist), (GetLocalized(admin, "sa_team_t", "T"), "t", CsTeam.Terrorist), (GetLocalized(admin, "sa_team_swap", "Swap"), "swap", CsTeam.Spectator), (GetLocalized(admin, "sa_team_spec", "Spec"), "spec", CsTeam.Spectator) ]; foreach ((string name, string teamName, CsTeam teamNum) in teams) { teamSelectionMenu.AddOption(name, _ => { if (target.IsValid) { CS2_SimpleAdmin.ChangeTeam(admin, target, teamName, teamNum, true); } }); } return teamSelectionMenu.WithBackButton(); } private static MenuBuilder CreatePluginsMenu(CCSPlayerController admin) { MenuBuilder pluginsMenu = new MenuBuilder("sa_menu_pluginsmanager_title", admin); pluginsMenu.AddOption("Open Plugins Manager", _ => { admin.ExecuteClientCommandFromServer("css_pluginsmanager"); }); return pluginsMenu.WithBackButton(); } private static MenuBuilder CreateChangeMapMenu(CCSPlayerController admin) { MenuBuilder mapMenu = new MenuBuilder("sa_changemap", admin); foreach (string map in CS2_SimpleAdmin.Instance.Config.DefaultMaps) { mapMenu.AddOption(map, _ => { CS2_SimpleAdmin.Instance.ChangeMap(admin, map); }); } foreach (KeyValuePair wsMap in CS2_SimpleAdmin.Instance.Config.WorkshopMaps) { string mapId = wsMap.Value?.ToString() ?? wsMap.Key; mapMenu.AddOption($"{wsMap.Key} (WS)", _ => { CS2_SimpleAdmin.Instance.ChangeWorkshopMap(admin, mapId); }); } return mapMenu.WithBackButton(); } private static MenuBuilder CreateRestartGameMenu(CCSPlayerController admin) { MenuBuilder restartMenu = new MenuBuilder("sa_restart_game", admin); restartMenu.AddOption("Restart Round", _ => { CS2_SimpleAdmin.RestartGame(admin); }); return restartMenu.WithBackButton(); } private static MenuBuilder CreateCustomCommandsMenu(CCSPlayerController admin) { MenuBuilder customMenu = new MenuBuilder("sa_menu_custom_commands", admin); SteamID steamId = new SteamID(admin.SteamID); foreach (CustomServerCommandData customCommand in CS2_SimpleAdmin.Instance.Config.CustomServerCommands) { if (string.IsNullOrEmpty(customCommand.DisplayName) || string.IsNullOrEmpty(customCommand.Command)) { continue; } if (!AdminManager.PlayerHasPermissions(steamId, customCommand.Flag)) { continue; } customMenu.AddOption(customCommand.DisplayName, _ => { Helper.TryLogCommandOnDiscord(admin, customCommand.Command); if (customCommand.ExecuteOnClient) { admin.ExecuteClientCommandFromServer(customCommand.Command); } else { Server.ExecuteCommand(customCommand.Command); } }); } return customMenu.WithBackButton(); } private static MenuBuilder CreateAddAdminMenu(CCSPlayerController admin) { MenuBuilder addAdminMenu = new MenuBuilder("sa_admin_add", admin); foreach (CCSPlayerController player in Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p))) { CCSPlayerController target = player; addAdminMenu.AddSubMenu(Helper.Truncate(target.PlayerName, MaxPlayerNameLength), () => CreateAdminFlagsMenu(admin, target)); } return addAdminMenu.WithBackButton(); } private static MenuBuilder CreateAdminFlagsMenu(CCSPlayerController admin, CCSPlayerController target) { MenuBuilder flagsMenu = new MenuBuilder($"{GetLocalized(admin, "sa_admin_add", "Add Admin")}: {target.PlayerName}"); foreach (AdminFlag adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags) { // Disabled if player already has this flag bool hasFlag = AdminManager.PlayerHasPermissions(target, adminFlag.Flag); flagsMenu.AddOption(adminFlag.Name, _ => { if (target.IsValid) { CS2_SimpleAdmin.AddAdmin(admin, target.SteamID.ToString(), target.PlayerName, adminFlag.Flag, 10); } }, hasFlag); } return flagsMenu.WithBackButton(); } private static MenuBuilder CreateRemoveAdminMenu(CCSPlayerController admin) { MenuBuilder removeAdminMenu = new MenuBuilder("sa_admin_remove", admin); IEnumerable adminPlayers = Helper.GetValidPlayers().Where(p => AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0 && p != admin && admin.CanTarget(p)); foreach (CCSPlayerController player in adminPlayers) { CCSPlayerController target = player; removeAdminMenu.AddOption(Helper.Truncate(target.PlayerName, MaxPlayerNameLength), _ => { if (target.IsValid) { CS2_SimpleAdmin.Instance.RemoveAdmin(admin, target.SteamID.ToString()); } }); } return removeAdminMenu.WithBackButton(); } private static MenuBuilder CreateReloadAdminsMenu(CCSPlayerController admin) { MenuBuilder reloadMenu = new MenuBuilder("sa_admin_reload", admin); reloadMenu.AddOption("Reload Admins", _ => { CS2_SimpleAdmin.Instance.ReloadAdmins(admin); }); return reloadMenu.WithBackButton(); } }