diff --git a/.gitignore b/.gitignore index 8eebf6c..28e335d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ CS2-SimpleAdmin.sln.DotSettings.user Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.sln.DotSettings.user CS2-SimpleAdmin_BanSoundModule — kopia *.user +CLAUDE.md diff --git a/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs b/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs index 56c1b7f..95c27db 100644 --- a/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs +++ b/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs @@ -1,47 +1,57 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Modules.Commands; +using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Entities; using CS2_SimpleAdmin.Managers; +using CS2_SimpleAdmin.Menus; using CS2_SimpleAdminApi; +using Microsoft.Extensions.Localization; namespace CS2_SimpleAdmin.Api; public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi { + public event Action? OnSimpleAdminReady; + public void OnSimpleAdminReadyEvent() => OnSimpleAdminReady?.Invoke(); + public PlayerInfo GetPlayerInfo(CCSPlayerController player) { - if (!player.UserId.HasValue) - throw new KeyNotFoundException("Player with specific UserId not found"); - - return CS2_SimpleAdmin.PlayersInfo[player.SteamID]; + return !player.UserId.HasValue + ? throw new KeyNotFoundException("Player with specific UserId not found") + : CS2_SimpleAdmin.PlayersInfo[player.SteamID]; } - - public string GetConnectionString() => CS2_SimpleAdmin.Instance.DbConnectionString; - public string GetServerAddress() => CS2_SimpleAdmin.IpAddress; - public int? GetServerId() => CS2_SimpleAdmin.ServerId; - public Dictionary> GetPlayerMuteStatus(CCSPlayerController player) + public string GetConnectionString() => CS2_SimpleAdmin.Instance.DbConnectionString; + public string GetServerAddress() => CS2_SimpleAdmin.IpAddress; + public int? GetServerId() => CS2_SimpleAdmin.ServerId; + + public Dictionary> GetPlayerMuteStatus( + CCSPlayerController player) { return PlayerPenaltyManager.GetAllPlayerPenalties(player.Slot); } - + public event Action? OnPlayerPenaltied; public event Action? OnPlayerPenaltiedAdded; public event Action? OnAdminShowActivity; public event Action? OnAdminToggleSilent; public void OnPlayerPenaltiedEvent(PlayerInfo player, PlayerInfo? admin, PenaltyType penaltyType, string reason, - int duration, int? penaltyId) => OnPlayerPenaltied?.Invoke(player, admin, penaltyType, reason, duration, penaltyId, CS2_SimpleAdmin.ServerId); - + int duration, int? penaltyId) => OnPlayerPenaltied?.Invoke(player, admin, penaltyType, reason, duration, + penaltyId, CS2_SimpleAdmin.ServerId); + public void OnPlayerPenaltiedAddedEvent(SteamID player, PlayerInfo? admin, PenaltyType penaltyType, string reason, - int duration, int? penaltyId) => OnPlayerPenaltiedAdded?.Invoke(player, admin, penaltyType, reason, duration, penaltyId, CS2_SimpleAdmin.ServerId); - - public void OnAdminShowActivityEvent(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs) => OnAdminShowActivity?.Invoke(messageKey, callerName, dontPublish, messageArgs); + int duration, int? penaltyId) => OnPlayerPenaltiedAdded?.Invoke(player, admin, penaltyType, reason, duration, + penaltyId, CS2_SimpleAdmin.ServerId); - public void OnAdminToggleSilentEvent(int slot, bool status) => OnAdminToggleSilent?.Invoke(slot, status); + public void OnAdminShowActivityEvent(string messageKey, string? callerName = null, bool dontPublish = false, + params object[] messageArgs) => OnAdminShowActivity?.Invoke(messageKey, callerName, dontPublish, messageArgs); - public void IssuePenalty(CCSPlayerController player, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1) + public void OnAdminToggleSilentEvent(int slot, bool status) => OnAdminToggleSilent?.Invoke(slot, status); + + public void IssuePenalty(CCSPlayerController player, CCSPlayerController? admin, PenaltyType penaltyType, + string reason, int duration = -1) { switch (penaltyType) { @@ -79,8 +89,9 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null); } } - - public void IssuePenalty(SteamID steamid, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1) + + public void IssuePenalty(SteamID steamid, CCSPlayerController? admin, PenaltyType penaltyType, string reason, + int duration = -1) { switch (penaltyType) { @@ -123,12 +134,12 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi { Helper.LogCommand(caller, command); } - + public bool IsAdminSilent(CCSPlayerController player) { return CS2_SimpleAdmin.SilentPlayers.Contains(player.Slot); - } - + } + public HashSet ListSilentAdminsSlots() { return CS2_SimpleAdmin.SilentPlayers; @@ -147,7 +158,7 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi list = new List(); RegisterCommands._commandDefinitions[name] = list; } - + list.Add(definition); } @@ -162,9 +173,145 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi CS2_SimpleAdmin.Instance.RemoveCommand(commandName, definition.Callback); } } - - public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs) + + public TargetResult? GetTarget(CommandInfo command) + { + return CS2_SimpleAdmin.GetTarget(command); + } + + public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, + params object[] messageArgs) { Helper.ShowAdminActivity(messageKey, callerName, dontPublish, messageArgs); } + + public void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null, + bool dontPublish = false) + { + Helper.ShowAdminActivityTranslated(translatedMessage, callerName, dontPublish); + } + + public void ShowAdminActivityLocalized(object moduleLocalizer, string messageKey, string? callerName = null, + bool dontPublish = false, params object[] messageArgs) + { + if (moduleLocalizer is not IStringLocalizer localizer) + throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance"); + + Helper.ShowAdminActivityLocalized(localizer, messageKey, callerName, dontPublish, messageArgs); + } + + public void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic") + { + Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryName, permission); + } + + public void RegisterMenu(string categoryId, string menuId, string menuName, + Func menuFactory, string? permission = null) + { + Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuName, BuilderFactory, permission); + return; + + MenuBuilder BuilderFactory(CCSPlayerController player) + { + if (menuFactory(player) is not MenuBuilder menuBuilder) + throw new InvalidOperationException("Menu factory must return MenuBuilder"); + + // Dodaj automatyczną obsługę przycisku 'Wróć' + menuBuilder.WithBackAction(p => + { + if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category)) + { + Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p); + } + else + { + Menus.MenuManager.Instance.OpenMainMenu(p); + } + }); + + return menuBuilder; + } + } + + + public void UnregisterMenu(string categoryId, string menuId) + { + Menus.MenuManager.Instance.UnregisterMenu(categoryId, menuId); + } + + public object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player) + { + var builder = new MenuBuilder(title); + builder.WithBackAction(p => + { + if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category)) + { + Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p); + } + else + { + Menus.MenuManager.Instance.OpenMainMenu(p); + } + }); + + return builder; + } + + public List GetValidPlayers() + { + return Helper.GetValidPlayers(); + } + + public object CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin, + Func filter, Action onSelect) + { + var menu = (MenuBuilder)CreateMenuWithBack(title, categoryId, admin); + var players = Helper.GetValidPlayers().Where(filter); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + menu.AddOption(playerName, _ => + { + if (player.IsValid) + { + onSelect(admin, player); + } + }); + } + + return menu; + } + + public void AddMenuOption(object menu, string name, Action action, bool disabled = false, + string? permission = null) + { + if (menu is not MenuBuilder menuBuilder) + throw new InvalidOperationException("Menu must be a MenuBuilder instance"); + + menuBuilder.AddOption(name, action, disabled, permission); + } + + public void AddSubMenu(object menu, string name, Func subMenuFactory, + bool disabled = false, string? permission = null) + { + if (menu is not MenuBuilder menuBuilder) + throw new InvalidOperationException("Menu must be a MenuBuilder instance"); + + menuBuilder.AddSubMenu(name, player => + { + var subMenu = subMenuFactory(player); + if (subMenu is not MenuBuilder builder) + throw new InvalidOperationException("SubMenu factory must return MenuBuilder"); + return builder; + }, disabled, permission); + } + + public void OpenMenu(object menu, CCSPlayerController player) + { + if (menu is not MenuBuilder menuBuilder) + throw new InvalidOperationException("Menu must be a MenuBuilder instance"); + + menuBuilder.OpenMenu(player); + } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index b1d53f7..825eb09 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -1,5 +1,4 @@ -using System.Reflection; -using CounterStrikeSharp.API; +using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Capabilities; @@ -8,6 +7,7 @@ using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; using CS2_SimpleAdmin.Database; using CS2_SimpleAdmin.Managers; +using CS2_SimpleAdmin.Menus; using CS2_SimpleAdminApi; using Microsoft.Extensions.Logging; using MySqlConnector; @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; public override string ModuleAuthor => "daffyy & Dliix66"; - public override string ModuleVersion => "1.7.7-alpha-10"; + public override string ModuleVersion => "1.7.8-beta-1"; public override void Load(bool hotReload) { @@ -67,6 +67,9 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig ReloadAdmins(null)); RegisterEvents(); - RegisterCommands.InitializeCommands(); + AddTimer(0.5f, RegisterCommands.InitializeCommands); if (!CoreConfig.UnlockConCommands) { @@ -232,7 +235,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig PreserveNewest + + PreserveNewest + PreserveNewest @@ -126,6 +129,9 @@ PreserveNewest + + PreserveNewest + diff --git a/CS2-SimpleAdmin/Commands/RegisterCommands.cs b/CS2-SimpleAdmin/Commands/RegisterCommands.cs index d323e29..584f3ec 100644 --- a/CS2-SimpleAdmin/Commands/RegisterCommands.cs +++ b/CS2-SimpleAdmin/Commands/RegisterCommands.cs @@ -65,24 +65,11 @@ public static class RegisterCommands new("css_vote", CS2_SimpleAdmin.Instance.OnVoteCommand), - new("css_noclip", CS2_SimpleAdmin.Instance.OnNoclipCommand), - new("css_freeze", CS2_SimpleAdmin.Instance.OnFreezeCommand), - new("css_unfreeze", CS2_SimpleAdmin.Instance.OnUnfreezeCommand), - new("css_godmode", CS2_SimpleAdmin.Instance.OnGodCommand), - new("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand), new("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand), - new("css_give", CS2_SimpleAdmin.Instance.OnGiveCommand), - new("css_strip", CS2_SimpleAdmin.Instance.OnStripCommand), - new("css_hp", CS2_SimpleAdmin.Instance.OnHpCommand), - new("css_speed", CS2_SimpleAdmin.Instance.OnSpeedCommand), - new("css_gravity", CS2_SimpleAdmin.Instance.OnGravityCommand), - new("css_resize", CS2_SimpleAdmin.Instance.OnResizeCommand), - new("css_money", CS2_SimpleAdmin.Instance.OnMoneyCommand), new("css_team", CS2_SimpleAdmin.Instance.OnTeamCommand), new("css_rename", CS2_SimpleAdmin.Instance.OnRenameCommand), new("css_prename", CS2_SimpleAdmin.Instance.OnPrenameCommand), - new("css_respawn", CS2_SimpleAdmin.Instance.OnRespawnCommand), new("css_tp", CS2_SimpleAdmin.Instance.OnGotoCommand), new("css_bring", CS2_SimpleAdmin.Instance.OnBringCommand), new("css_pluginsmanager", CS2_SimpleAdmin.Instance.OnPluginManagerCommand), @@ -160,23 +147,12 @@ public static class RegisterCommands { "css_addsilence", new Command { Aliases = ["css_addsilence"] } }, { "css_unsilence", new Command { Aliases = ["css_unsilence"] } }, { "css_vote", new Command { Aliases = ["css_vote"] } }, - { "css_noclip", new Command { Aliases = ["css_noclip"] } }, - { "css_freeze", new Command { Aliases = ["css_freeze"] } }, - { "css_unfreeze", new Command { Aliases = ["css_unfreeze"] } }, - { "css_godmode", new Command { Aliases = ["css_godmode"] } }, { "css_slay", new Command { Aliases = ["css_slay"] } }, { "css_slap", new Command { Aliases = ["css_slap"] } }, - { "css_give", new Command { Aliases = ["css_give"] } }, - { "css_strip", new Command { Aliases = ["css_strip"] } }, - { "css_hp", new Command { Aliases = ["css_hp"] } }, - { "css_speed", new Command { Aliases = ["css_speed"] } }, - { "css_gravity", new Command { Aliases = ["css_gravity"] } }, - { "css_resize", new Command { Aliases = ["css_resize", "css_size"] } }, - { "css_money", new Command { Aliases = ["css_money"] } }, { "css_team", new Command { Aliases = ["css_team"] } }, { "css_rename", new Command { Aliases = ["css_rename"] } }, { "css_prename", new Command { Aliases = ["css_prename"] } }, - { "css_respawn", new Command { Aliases = ["css_respawn"] } }, + { "css_resize", new Command { Aliases = ["css_resize", "css_size"] } }, { "css_tp", new Command { Aliases = ["css_tp", "css_tpto", "css_goto"] } }, { "css_bring", new Command { Aliases = ["css_bring", "css_tphere"] } }, { "css_pluginsmanager", new Command { Aliases = ["css_pluginsmanager", "css_pluginmanager"] } }, @@ -205,25 +181,26 @@ public static class RegisterCommands var commandsConfig = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (commandsConfig?.Commands == null) return; - - foreach (var command in commandsConfig.Commands) + if (commandsConfig?.Commands != null) { - if (command.Value.Aliases == null) continue; - - CS2_SimpleAdmin._logger?.LogInformation( - $"Registering command: `{command.Key}` with aliases: `{string.Join(", ", command.Value.Aliases)}`"); - - var mapping = CommandMappings.FirstOrDefault(m => m.CommandKey == command.Key); - if (mapping == null || command.Value.Aliases.Length == 0) continue; - - foreach (var alias in command.Value.Aliases) + foreach (var command in commandsConfig.Commands) { - CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback); + if (command.Value.Aliases == null) continue; + + CS2_SimpleAdmin._logger?.LogInformation( + $"Registering command: `{command.Key}` with aliases: `{string.Join(", ", command.Value.Aliases)}`"); + + var mapping = CommandMappings.FirstOrDefault(m => m.CommandKey == command.Key); + if (mapping == null || command.Value.Aliases.Length == 0) continue; + + foreach (var alias in command.Value.Aliases) + { + CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback); + } } } - foreach (var (name, definitions) in RegisterCommands._commandDefinitions) + foreach (var (name, definitions) in _commandDefinitions) { foreach (var definition in definitions) { diff --git a/CS2-SimpleAdmin/Commands/basebans.cs b/CS2-SimpleAdmin/Commands/basebans.cs index 7b15b55..761ecbd 100644 --- a/CS2-SimpleAdmin/Commands/basebans.cs +++ b/CS2-SimpleAdmin/Commands/basebans.cs @@ -316,7 +316,7 @@ public partial class CS2_SimpleAdmin var canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban"); - if (duration <= 0 && canPermBan == false) + if (duration <= 0 && !canPermBan) { caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}"); return false; diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index 290bbd0..8d95c33 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -33,7 +33,7 @@ public partial class CS2_SimpleAdmin [CommandHelper(usage: "[#userid or name]", whoCanExecute: CommandUsage.CLIENT_ONLY)] public void OnPenaltiesCommand(CCSPlayerController? caller, CommandInfo command) { - if (caller == null || caller.IsValid == false || !caller.UserId.HasValue || DatabaseProvider == null) + if (caller == null || !caller.IsValid || !caller.UserId.HasValue || DatabaseProvider == null) return; var userId = caller.UserId.Value; @@ -160,7 +160,7 @@ public partial class CS2_SimpleAdmin [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void OnAdminVoiceCommand(CCSPlayerController? caller, CommandInfo command) { - if (caller == null || caller.IsValid == false) + if (caller == null || !caller.IsValid) return; if (command.ArgCount > 1) @@ -205,7 +205,7 @@ public partial class CS2_SimpleAdmin [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void OnAdminCommand(CCSPlayerController? caller, CommandInfo command) { - if (caller == null || caller.IsValid == false) + if (caller == null || !caller.IsValid) return; AdminMenu.OpenMenu(caller); diff --git a/CS2-SimpleAdmin/Commands/basecomms.cs b/CS2-SimpleAdmin/Commands/basecomms.cs index 0cc9a8c..e202c1e 100644 --- a/CS2-SimpleAdmin/Commands/basecomms.cs +++ b/CS2-SimpleAdmin/Commands/basecomms.cs @@ -959,7 +959,7 @@ public partial class CS2_SimpleAdmin var canPermMute = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permmute"); - if (duration <= 0 && canPermMute == false) + if (duration <= 0 && !canPermMute) { caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}"); return false; diff --git a/CS2-SimpleAdmin/Commands/funcommands.cs b/CS2-SimpleAdmin/Commands/funcommands.cs index 5f4b64f..0eb040c 100644 --- a/CS2-SimpleAdmin/Commands/funcommands.cs +++ b/CS2-SimpleAdmin/Commands/funcommands.cs @@ -1,307 +1,307 @@ -using System.Globalization; -using CounterStrikeSharp.API; -using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Admin; -using CounterStrikeSharp.API.Modules.Commands; - -namespace CS2_SimpleAdmin; - -public partial class CS2_SimpleAdmin -{ - /// - /// Enables or disables no-clip mode for specified player(s). - /// - /// The player issuing the command. - /// The command input containing targets. - [CommandHelper(1, "<#userid or name>")] - [RequiresPermissions("@css/cheats")] - public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command) - { - var callerName = caller == null ? _localizer?["sa_console"] ?? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; - - var targets = GetTarget(command); - if (targets == null) return; - var playersToTarget = targets.Players.Where(player => - player.IsValid && - player is { IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (caller!.CanTarget(player)) - { - NoClip(caller, player, callerName); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Toggles no-clip mode for a player and shows admin activity messages. - /// - /// The player/admin toggling no-clip. - /// The target player whose no-clip state changes. - /// Optional caller name for messages. - /// Optional command info for logging. - internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) - { - if (!player.IsValid) return; - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Toggle no-clip mode for the player - player.Pawn.Value?.ToggleNoclip(); - - // Determine message keys and arguments for the no-clip notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_noclip_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); - } - - /// - /// Enables or disables god mode for specified player(s). - /// - /// The player issuing the command. - /// The command input containing targets. - - [RequiresPermissions("@css/cheats")] - [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] - public void OnGodCommand(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.IsValid && player is {IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (player.Connected != PlayerConnectedState.PlayerConnected) - return; - - if (caller!.CanTarget(player)) - { - God(caller, player, command); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Toggles god mode for a player and notifies admins. - /// - /// The player/admin toggling god mode. - /// The target player whose god mode changes. - /// Optional command info for logging. - internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null) - { - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Toggle god mode for the player - if (!GodPlayers.Add(player.Slot)) - { - GodPlayers.Remove(player.Slot); - } - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); - - // Determine message key and arguments for the god mode notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_god_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - } - - /// - /// Freezes target player(s) for an optional specified duration. - /// - /// The player issuing the freeze command. - /// The command input containing targets and duration. - [CommandHelper(1, "<#userid or name> [duration]")] - [RequiresPermissions("@css/slay")] - public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command) - { - var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; - int.TryParse(command.GetArg(2), out var time); - - var targets = GetTarget(command); - if (targets == null) return; - var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (caller!.CanTarget(player)) - { - Freeze(caller, player, time, callerName, command); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Resizes the target player(s) models to a specified scale. - /// - /// The player issuing the resize command. - /// The command input containing targets and scale factor. - [CommandHelper(1, "<#userid or name> [size]")] - [RequiresPermissions("@css/slay")] - public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command) - { - var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; - float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size); - - var targets = GetTarget(command); - if (targets == null) return; - var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (!caller!.CanTarget(player)) return; - - var sceneNode = player.PlayerPawn.Value!.CBodyComponent?.SceneNode; - if (sceneNode == null) return; - - sceneNode.GetSkeletonInstance().Scale = size; - player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture)); - - Server.NextWorldUpdate(() => - { - Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent"); - }); - - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_resize_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Freezes a single player and optionally schedules automatic unfreeze after a duration. - /// - /// The player/admin freezing the player. - /// The player to freeze. - /// Duration of freeze in seconds. - /// Optional name for notifications. - /// Optional command info for logging. - internal static void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time, string? callerName = null, CommandInfo? command = null) - { - if (!player.IsValid) return; - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Freeze player pawn - player.Pawn.Value?.Freeze(); - - // Determine message keys and arguments for the freeze notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_freeze_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - - // Schedule unfreeze for the player if time is specified - if (time > 0) - { - Instance.AddTimer(time, () => player.Pawn.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); - } - - // Log the command and send Discord notification - if (command == null) - Helper.LogCommand(caller, $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}"); - } - - /// - /// Unfreezes target player(s). - /// - /// The player issuing the unfreeze command. - /// The command input with targets. - [CommandHelper(1, "<#userid or name>")] - [RequiresPermissions("@css/slay")] - public void OnUnfreezeCommand(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, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - Unfreeze(caller, player, callerName, command); - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Unfreezes a single player and notifies admins. - /// - /// The player/admin unfreezing the player. - /// The player to unfreeze. - /// Optional name for notifications. - /// Optional command info for logging. - internal static void Unfreeze(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) - { - if (!player.IsValid) return; - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Unfreeze player pawn - player.Pawn.Value?.Unfreeze(); - - // Determine message keys and arguments for the unfreeze notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_unfreeze_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - - // Log the command and send Discord notification - if (command == null) - Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); - } -} \ No newline at end of file +// using System.Globalization; +// using CounterStrikeSharp.API; +// using CounterStrikeSharp.API.Core; +// using CounterStrikeSharp.API.Modules.Admin; +// using CounterStrikeSharp.API.Modules.Commands; +// +// namespace CS2_SimpleAdmin; +// +// public partial class CS2_SimpleAdmin +// { +// /// +// /// Enables or disables no-clip mode for specified player(s). +// /// +// /// The player issuing the command. +// /// The command input containing targets. +// [CommandHelper(1, "<#userid or name>")] +// [RequiresPermissions("@css/cheats")] +// public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command) +// { +// var callerName = caller == null ? _localizer?["sa_console"] ?? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; +// +// var targets = GetTarget(command); +// if (targets == null) return; +// var playersToTarget = targets.Players.Where(player => +// player.IsValid && +// player is { IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); +// +// playersToTarget.ForEach(player => +// { +// if (caller!.CanTarget(player)) +// { +// NoClip(caller, player, callerName); +// } +// }); +// +// Helper.LogCommand(caller, command); +// } +// +// /// +// /// Toggles no-clip mode for a player and shows admin activity messages. +// /// +// /// The player/admin toggling no-clip. +// /// The target player whose no-clip state changes. +// /// Optional caller name for messages. +// /// Optional command info for logging. +// internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) +// { +// if (!player.IsValid) return; +// if (!caller.CanTarget(player)) return; +// +// // Set default caller name if not provided +// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; +// +// // Toggle no-clip mode for the player +// player.Pawn.Value?.ToggleNoclip(); +// +// // Determine message keys and arguments for the no-clip notification +// var (activityMessageKey, adminActivityArgs) = +// ("sa_admin_noclip_message", +// new object[] { "CALLER", player.PlayerName }); +// +// // Display admin activity message to other players +// if (caller == null || !SilentPlayers.Contains(caller.Slot)) +// { +// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); +// } +// +// // Log the command +// if (command == null) +// Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); +// } +// +// /// +// /// Enables or disables god mode for specified player(s). +// /// +// /// The player issuing the command. +// /// The command input containing targets. +// +// [RequiresPermissions("@css/cheats")] +// [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] +// public void OnGodCommand(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.IsValid && player is {IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); +// +// playersToTarget.ForEach(player => +// { +// if (player.Connected != PlayerConnectedState.PlayerConnected) +// return; +// +// if (caller!.CanTarget(player)) +// { +// God(caller, player, command); +// } +// }); +// +// Helper.LogCommand(caller, command); +// } +// +// /// +// /// Toggles god mode for a player and notifies admins. +// /// +// /// The player/admin toggling god mode. +// /// The target player whose god mode changes. +// /// Optional command info for logging. +// internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null) +// { +// if (!caller.CanTarget(player)) return; +// +// // Set default caller name if not provided +// var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; +// +// // Toggle god mode for the player +// if (!GodPlayers.Add(player.Slot)) +// { +// GodPlayers.Remove(player.Slot); +// } +// +// // Log the command +// if (command == null) +// Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); +// +// // Determine message key and arguments for the god mode notification +// var (activityMessageKey, adminActivityArgs) = +// ("sa_admin_god_message", +// new object[] { "CALLER", player.PlayerName }); +// +// // Display admin activity message to other players +// if (caller == null || !SilentPlayers.Contains(caller.Slot)) +// { +// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); +// } +// } +// +// /// +// /// Freezes target player(s) for an optional specified duration. +// /// +// /// The player issuing the freeze command. +// /// The command input containing targets and duration. +// [CommandHelper(1, "<#userid or name> [duration]")] +// [RequiresPermissions("@css/slay")] +// public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command) +// { +// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; +// int.TryParse(command.GetArg(2), out var time); +// +// var targets = GetTarget(command); +// if (targets == null) return; +// var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); +// +// playersToTarget.ForEach(player => +// { +// if (caller!.CanTarget(player)) +// { +// Freeze(caller, player, time, callerName, command); +// } +// }); +// +// Helper.LogCommand(caller, command); +// } +// +// /// +// /// Resizes the target player(s) models to a specified scale. +// /// +// /// The player issuing the resize command. +// /// The command input containing targets and scale factor. +// [CommandHelper(1, "<#userid or name> [size]")] +// [RequiresPermissions("@css/slay")] +// public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command) +// { +// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; +// float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size); +// +// var targets = GetTarget(command); +// if (targets == null) return; +// var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); +// +// playersToTarget.ForEach(player => +// { +// if (!caller!.CanTarget(player)) return; +// +// var sceneNode = player.PlayerPawn.Value!.CBodyComponent?.SceneNode; +// if (sceneNode == null) return; +// +// sceneNode.GetSkeletonInstance().Scale = size; +// player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture)); +// +// Server.NextWorldUpdate(() => +// { +// Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent"); +// }); +// +// var (activityMessageKey, adminActivityArgs) = +// ("sa_admin_resize_message", +// new object[] { "CALLER", player.PlayerName }); +// +// // Display admin activity message to other players +// if (caller == null || !SilentPlayers.Contains(caller.Slot)) +// { +// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); +// } +// }); +// +// Helper.LogCommand(caller, command); +// } +// +// /// +// /// Freezes a single player and optionally schedules automatic unfreeze after a duration. +// /// +// /// The player/admin freezing the player. +// /// The player to freeze. +// /// Duration of freeze in seconds. +// /// Optional name for notifications. +// /// Optional command info for logging. +// internal static void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time, string? callerName = null, CommandInfo? command = null) +// { +// if (!player.IsValid) return; +// if (!caller.CanTarget(player)) return; +// +// // Set default caller name if not provided +// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; +// +// // Freeze player pawn +// player.Pawn.Value?.Freeze(); +// +// // Determine message keys and arguments for the freeze notification +// var (activityMessageKey, adminActivityArgs) = +// ("sa_admin_freeze_message", +// new object[] { "CALLER", player.PlayerName }); +// +// // Display admin activity message to other players +// if (caller == null || !SilentPlayers.Contains(caller.Slot)) +// { +// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); +// } +// +// // Schedule unfreeze for the player if time is specified +// if (time > 0) +// { +// Instance.AddTimer(time, () => player.Pawn.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); +// } +// +// // Log the command and send Discord notification +// if (command == null) +// Helper.LogCommand(caller, $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}"); +// } +// +// /// +// /// Unfreezes target player(s). +// /// +// /// The player issuing the unfreeze command. +// /// The command input with targets. +// [CommandHelper(1, "<#userid or name>")] +// [RequiresPermissions("@css/slay")] +// public void OnUnfreezeCommand(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, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); +// +// playersToTarget.ForEach(player => +// { +// Unfreeze(caller, player, callerName, command); +// }); +// +// Helper.LogCommand(caller, command); +// } +// +// /// +// /// Unfreezes a single player and notifies admins. +// /// +// /// The player/admin unfreezing the player. +// /// The player to unfreeze. +// /// Optional name for notifications. +// /// Optional command info for logging. +// internal static void Unfreeze(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) +// { +// if (!player.IsValid) return; +// if (!caller.CanTarget(player)) return; +// +// // Set default caller name if not provided +// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; +// +// // Unfreeze player pawn +// player.Pawn.Value?.Unfreeze(); +// +// // Determine message keys and arguments for the unfreeze notification +// var (activityMessageKey, adminActivityArgs) = +// ("sa_admin_unfreeze_message", +// new object[] { "CALLER", player.PlayerName }); +// +// // Display admin activity message to other players +// if (caller == null || !SilentPlayers.Contains(caller.Slot)) +// { +// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); +// } +// +// // Log the command and send Discord notification +// if (command == null) +// Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); +// } +// } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Commands/playercommands.cs b/CS2-SimpleAdmin/Commands/playercommands.cs index b66adf2..25385b5 100644 --- a/CS2-SimpleAdmin/Commands/playercommands.cs +++ b/CS2-SimpleAdmin/Commands/playercommands.cs @@ -11,9 +11,6 @@ namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { - internal static readonly Dictionary SpeedPlayers = []; - internal static readonly Dictionary GravityPlayers = []; - /// /// Executes the 'slay' command, forcing the targeted players to commit suicide. /// Checks player validity and permissions. @@ -72,451 +69,6 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, $"css_slay {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); } - /// - /// Executes the 'give' command to provide a specified weapon to targeted players. - /// Enforces server rules for prohibited weapons. - /// - /// Player or console issuing the command. - /// Command details, including targets and weapon name. - [RequiresPermissions("@css/cheats")] - [CommandHelper(minArgs: 2, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] - public void OnGiveCommand(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.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - var weaponName = command.GetArg(2); - - // check if weapon is knife - if (weaponName.Contains("_knife") || weaponName.Contains("bayonet")) - { - if (CoreConfig.FollowCS2ServerGuidelines) - { - command.ReplyToCommand($"Cannot Give {weaponName} because it's illegal to be given."); - return; - } - } - - playersToTarget.ForEach(player => - { - if (player.Connected != PlayerConnectedState.PlayerConnected) - return; - - GiveWeapon(caller, player, weaponName, callerName, command); - }); - - Helper.LogCommand(caller, command); - } - - - /// - /// Gives a weapon identified by name to a player, handling ambiguous matches and logging. - /// - /// Admin issuing the command. - /// Target player to receive the weapon. - /// Weapon name or partial name. - /// Optional name to display in notifications. - /// Optional command info for logging. - private static void GiveWeapon(CCSPlayerController? caller, CCSPlayerController player, string weaponName, string? callerName = null, CommandInfo? command = null) - { - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - var weapons = WeaponHelper.GetWeaponsByPartialName(weaponName); - - switch (weapons.Count) - { - case 0: - return; - case > 1: - { - var weaponList = string.Join(", ", weapons.Select(w => w.EnumMemberValue)); - command?.ReplyToCommand($"Found weapons with a similar name: {weaponList}"); - return; - } - } - - // Give weapon to the player - player.GiveNamedItem(weapons.First().EnumValue); - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_giveweapon {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {weaponName}"); - - // Determine message keys and arguments for the weapon give notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_give_message", - new object[] { "CALLER", player.PlayerName, weaponName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - } - - /// - /// Gives a specific weapon to a player, with notifications and logging. - /// - /// Admin issuing the command. - /// Target player. - /// Weapon item object. - /// Optional caller name for notifications. - /// Optional command info. - internal static void GiveWeapon(CCSPlayerController? caller, CCSPlayerController player, CsItem weapon, string? callerName = null, CommandInfo? command = null) - { - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Give weapon to the player - player.GiveNamedItem(weapon); - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_giveweapon {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {weapon.ToString()}"); - - // Determine message keys and arguments for the weapon give notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_give_message", - new object[] { "CALLER", player.PlayerName, weapon.ToString() }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - } - - /// - /// Executes the 'strip' command, removing all weapons from targeted players. - /// Checks player validity and permissions. - /// - /// Player or console issuing the command. - /// Command details including targets. - [RequiresPermissions("@css/slay")] - [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] - public void OnStripCommand(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.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (caller!.CanTarget(player)) - { - StripWeapons(caller, player, callerName, command); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Removes all weapons from a player, with notifications and logging. - /// - /// Admin or console issuing the strip command. - /// Target player. - /// Optional caller name. - /// Optional command info for logging. - internal static void StripWeapons(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) - { - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Check if player is valid, alive, and connected - if (!player.IsValid || player.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE || player.Connected != PlayerConnectedState.PlayerConnected) - return; - - // Strip weapons from the player - player.RemoveWeapons(); - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_strip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); - - // Determine message keys and arguments for the weapon strip notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_strip_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - } - - /// - /// Sets health value on targeted players. - /// - /// Admin or console issuing the command. - /// Command details including targets and health value. - [RequiresPermissions("@css/slay")] - [CommandHelper(minArgs: 1, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] - public void OnHpCommand(CCSPlayerController? caller, CommandInfo command) - { - int.TryParse(command.GetArg(2), out var health); - - var targets = GetTarget(command); - if (targets == null) return; - - var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (caller!.CanTarget(player)) - { - SetHp(caller, player, health, command); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Changes health of a player and logs the action. - /// - /// Admin or console calling the method. - /// Target player. - /// Health value to set. - /// Optional command info. - internal static void SetHp(CCSPlayerController? caller, CCSPlayerController player, int health, CommandInfo? command = null) - { - if (!player.IsValid || player.IsHLTV) return; - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Set player's health - player.SetHp(health); - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_hp {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {health}"); - - // Determine message keys and arguments for the HP set notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_hp_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - } - - /// - /// Sets movement speed on targeted players. - /// - /// Admin or console issuing the command. - /// Command details including targets and speed. - [RequiresPermissions("@css/slay")] - [CommandHelper(minArgs: 1, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] - public void OnSpeedCommand(CCSPlayerController? caller, CommandInfo command) - { - float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var speed); - - var targets = GetTarget(command); - if (targets == null) return; - - var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (player.Connected != PlayerConnectedState.PlayerConnected) - return; - - if (caller!.CanTarget(player)) - { - SetSpeed(caller, player, speed, command); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Changes speed of a player and logs the action. - /// - /// Admin or console calling the method. - /// Target player. - /// Speed value to set. - /// Optional command info. - internal static void SetSpeed(CCSPlayerController? caller, CCSPlayerController player, float speed, CommandInfo? command = null) - { - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Set player's speed - player.SetSpeed(speed); - - if (speed == 1f) - SpeedPlayers.Remove(player); - else - SpeedPlayers[player] = speed; - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_speed {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {speed}"); - - // Determine message keys and arguments for the speed set notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_speed_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - } - - /// - /// Sets gravity on targeted players. - /// - /// Admin or console issuing the command. - /// Command details including targets and gravity value. - [RequiresPermissions("@css/slay")] - [CommandHelper(minArgs: 1, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] - public void OnGravityCommand(CCSPlayerController? caller, CommandInfo command) - { - float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var gravity); - - var targets = GetTarget(command); - if (targets == null) return; - - var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (player.Connected != PlayerConnectedState.PlayerConnected) - return; - - if (caller!.CanTarget(player)) - { - SetGravity(caller, player, gravity, command); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Changes gravity of a player and logs the action. - /// - /// Admin or console calling the method. - /// Target player. - /// Gravity value to set. - /// Optional command info. - internal static void SetGravity(CCSPlayerController? caller, CCSPlayerController player, float gravity, CommandInfo? command = null) - { - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Set player's gravity - player.SetGravity(gravity); - - if (gravity == 1f) - GravityPlayers.Remove(player); - else - GravityPlayers[player] = gravity; - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_gravity {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {gravity}"); - - // Determine message keys and arguments for the gravity set notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_gravity_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - } - - /// - /// Sets the money amount for the targeted players. - /// - /// The player/admin executing the command. - /// The command containing target player and money value. - [RequiresPermissions("@css/slay")] - [CommandHelper(minArgs: 1, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] - public void OnMoneyCommand(CCSPlayerController? caller, CommandInfo command) - { - var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; - int.TryParse(command.GetArg(2), out var money); - - var targets = GetTarget(command); - if (targets == null) return; - - var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); - - playersToTarget.ForEach(player => - { - if (player.Connected != PlayerConnectedState.PlayerConnected) - return; - - if (caller!.CanTarget(player)) - { - SetMoney(caller, player, money, command); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Applies money value to a single targeted player and logs the operation. - /// - /// The player/admin setting the money. - /// The player whose money will be set. - /// The value of money to set. - /// Optional command info for logging. - internal static void SetMoney(CCSPlayerController? caller, CCSPlayerController player, int money, CommandInfo? command = null) - { - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; - - // Set player's money - player.SetMoney(money); - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_money {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {money}"); - - // Determine message keys and arguments for the money set notification - var (activityMessageKey, adminActivityArgs) = - ("sa_admin_money_message", - new object[] { "CALLER", player.PlayerName }); - - // Display admin activity message to other players - if (caller == null || !SilentPlayers.Contains(caller.Slot)) - { - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - } - /// /// Applies damage as a slap effect to the targeted players. /// @@ -802,75 +354,6 @@ public partial class CS2_SimpleAdmin }); } - /// - /// Respawns targeted players, restoring their state. - /// - /// The admin or player issuing respawn. - /// The command including target players. - [CommandHelper(1, "<#userid or name>")] - [RequiresPermissions("@css/cheats")] - public void OnRespawnCommand(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(); - - playersToTarget.ForEach(player => - { - if (player.Connected != PlayerConnectedState.PlayerConnected) - return; - - if (caller!.CanTarget(player)) - { - Respawn(caller, player, callerName, command); - } - }); - - Helper.LogCommand(caller, command); - } - - /// - /// Respawns a specified player and updates admin notifications. - /// - /// Admin or player executing respawn. - /// Player to respawn. - /// Optional admin name. - /// Optional command info. - internal static void Respawn(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) - { - // Check if the caller can target the player - if (!caller.CanTarget(player)) return; - - // Set default caller name if not provided - callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; - - // Ensure the player's pawn is valid before attempting to respawn - if (_cBasePlayerControllerSetPawnFunc == null || player.PlayerPawn.Value == null || !player.PlayerPawn.IsValid) return; - - // Perform the respawn operation - var playerPawn = player.PlayerPawn.Value; - _cBasePlayerControllerSetPawnFunc.Invoke(player, playerPawn, true, false); - VirtualFunction.CreateVoid(player.Handle, GameData.GetOffset("CCSPlayerController_Respawn"))(player); - - if (player.UserId.HasValue && PlayersInfo.TryGetValue(player.SteamID, out var value) && value.DiePosition != null) - playerPawn.Teleport(value.DiePosition?.Position, value.DiePosition?.Angle); - - // Log the command - if (command == null) - Helper.LogCommand(caller, $"css_respawn {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); - - // Determine message key and arguments for the respawn notification - var activityMessageKey = "sa_admin_respawn_message"; - var adminActivityArgs = new object[] { "CALLER", player.PlayerName }; - - // Display admin activity message to other players - if (caller != null && SilentPlayers.Contains(caller.Slot)) return; - - Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); - } - /// /// Teleports targeted player(s) to another player's location. /// diff --git a/CS2-SimpleAdmin/Database/Migration.cs b/CS2-SimpleAdmin/Database/Migration.cs index 7a25747..5b97b9d 100644 --- a/CS2-SimpleAdmin/Database/Migration.cs +++ b/CS2-SimpleAdmin/Database/Migration.cs @@ -17,16 +17,27 @@ public class Migration(string migrationsPath) if (files.Count == 0) return; await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); - await using (var cmd = connection.CreateCommand()) { - cmd.CommandText = """ - CREATE TABLE IF NOT EXISTS sa_migrations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - version TEXT NOT NULL - ); - - """; + if (migrationsPath.Contains("sqlite", StringComparison.CurrentCultureIgnoreCase)) + { + cmd.CommandText = """ + CREATE TABLE IF NOT EXISTS sa_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version TEXT NOT NULL + ); + + """; + } + else + { + cmd.CommandText = """ + CREATE TABLE IF NOT EXISTS sa_migrations ( + id INT PRIMARY KEY AUTO_INCREMENT, + version VARCHAR(128) NOT NULL + ); + """; + } await cmd.ExecuteNonQueryAsync(); } diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/006_ServerGroupsFeature.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/006_ServerGroupsFeature.sql index 4424189..4256cec 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Mysql/006_ServerGroupsFeature.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/006_ServerGroupsFeature.sql @@ -20,7 +20,6 @@ CREATE TABLE IF NOT EXISTS `sa_groups_servers` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ALTER TABLE `sa_admins` ADD `group_id` INT NULL AFTER `created`; - ALTER TABLE `sa_groups_flags` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE CASCADE; ALTER TABLE `sa_groups_servers` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE CASCADE; ALTER TABLE `sa_admins` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE SET NULL; \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/009_BanAllUsedIpAddress.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/009_BanAllUsedIpAddress.sql index 2fa6688..d05bec7 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Mysql/009_BanAllUsedIpAddress.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/009_BanAllUsedIpAddress.sql @@ -1,8 +1,6 @@ CREATE TABLE IF NOT EXISTS `sa_players_ips` ( - `id` int(11) NOT NULL AUTO_INCREMENT, `steamid` bigint(20) NOT NULL, `address` varchar(64) NOT NULL, `used_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `steamid` (`steamid`,`address`) + PRIMARY KEY (`steamid`, `address`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/013_AddNameColumnToSaPlayerIpsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/013_AddNameColumnToSaPlayerIpsTable.sql index 81a26a6..aa455a1 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Mysql/013_AddNameColumnToSaPlayerIpsTable.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/013_AddNameColumnToSaPlayerIpsTable.sql @@ -1,11 +1,3 @@ -DELETE FROM `sa_players_ips` -WHERE `id` NOT IN ( - SELECT * FROM ( - SELECT MIN(`id`) - FROM `sa_players_ips` - GROUP BY `steamid` - ) AS `keep_ids` -); DELETE FROM sa_players_ips WHERE INET_ATON(address) IS NULL AND address IS NOT NULL; UPDATE `sa_players_ips` SET `address` = INET_ATON(address); ALTER TABLE `sa_players_ips` CHANGE `address` `address` INT UNSIGNED NOT NULL; diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/016_OptimizeTablesAndIndexes.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/016_OptimizeTablesAndIndexes.sql new file mode 100644 index 0000000..c674d89 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/016_OptimizeTablesAndIndexes.sql @@ -0,0 +1,33 @@ +-- Migration 016: Optimize tables and indexes +-- Add proper indexes for all tables to improve query performance + +-- Optimize sa_players_ips table indexes +-- Add index on used_at for efficient date-based queries +ALTER TABLE `sa_players_ips` ADD INDEX IF NOT EXISTS `idx_used_at` (`used_at` DESC); + +-- Optimize sa_bans table indexes +-- Add composite indexes for common query patterns +CREATE INDEX IF NOT EXISTS `idx_bans_steamid_status` ON `sa_bans` (`player_steamid`, `status`); +CREATE INDEX IF NOT EXISTS `idx_bans_ip_status` ON `sa_bans` (`player_ip`, `status`); +CREATE INDEX IF NOT EXISTS `idx_bans_status_ends` ON `sa_bans` (`status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_bans_server_status` ON `sa_bans` (`server_id`, `status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_bans_created` ON `sa_bans` (`created` DESC); + +-- Optimize sa_admins table indexes +CREATE INDEX IF NOT EXISTS `idx_admins_steamid` ON `sa_admins` (`player_steamid`); +CREATE INDEX IF NOT EXISTS `idx_admins_server_ends` ON `sa_admins` (`server_id`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_admins_ends` ON `sa_admins` (`ends`); + +-- Optimize sa_mutes table indexes (in addition to migration 014) +-- Add index for expire queries +CREATE INDEX IF NOT EXISTS `idx_mutes_status_ends` ON `sa_mutes` (`status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_mutes_server_status` ON `sa_mutes` (`server_id`, `status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_mutes_created` ON `sa_mutes` (`created` DESC); + +-- Optimize sa_warns table indexes (if exists) +CREATE INDEX IF NOT EXISTS `idx_warns_steamid_status` ON `sa_warns` (`player_steamid`, `status`); +CREATE INDEX IF NOT EXISTS `idx_warns_status_ends` ON `sa_warns` (`status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_warns_server_status` ON `sa_warns` (`server_id`, `status`, `ends`); + +-- Add index on sa_servers for faster lookups +CREATE INDEX IF NOT EXISTS `idx_servers_hostname` ON `sa_servers` (`hostname`); diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql index e28a4b3..7a141c5 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql @@ -1,7 +1,6 @@ CREATE TABLE IF NOT EXISTS `sa_players_ips` ( - `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `steamid` INTEGER NOT NULL, `address` VARCHAR(64) NOT NULL, - `used_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE (`steamid`, `address`) - ); \ No newline at end of file + `used_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`steamid`, `address`) +); \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/016_OptimizeTablesAndIndexes.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/016_OptimizeTablesAndIndexes.sql new file mode 100644 index 0000000..749229a --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/016_OptimizeTablesAndIndexes.sql @@ -0,0 +1,33 @@ +-- Migration 016: Optimize tables and indexes +-- Add proper indexes for all tables to improve query performance + +-- Optimize sa_players_ips table indexes +-- Add index on used_at for efficient date-based queries +CREATE INDEX IF NOT EXISTS `idx_used_at` ON `sa_players_ips` (`used_at` DESC); + +-- Optimize sa_bans table indexes +-- Add composite indexes for common query patterns +CREATE INDEX IF NOT EXISTS `idx_bans_steamid_status` ON `sa_bans` (`player_steamid`, `status`); +CREATE INDEX IF NOT EXISTS `idx_bans_ip_status` ON `sa_bans` (`player_ip`, `status`); +CREATE INDEX IF NOT EXISTS `idx_bans_status_ends` ON `sa_bans` (`status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_bans_server_status` ON `sa_bans` (`server_id`, `status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_bans_created` ON `sa_bans` (`created` DESC); + +-- Optimize sa_admins table indexes +CREATE INDEX IF NOT EXISTS `idx_admins_steamid` ON `sa_admins` (`player_steamid`); +CREATE INDEX IF NOT EXISTS `idx_admins_server_ends` ON `sa_admins` (`server_id`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_admins_ends` ON `sa_admins` (`ends`); + +-- Optimize sa_mutes table indexes (in addition to migration 014) +-- Add index for expire queries +CREATE INDEX IF NOT EXISTS `idx_mutes_status_ends` ON `sa_mutes` (`status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_mutes_server_status` ON `sa_mutes` (`server_id`, `status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_mutes_created` ON `sa_mutes` (`created` DESC); + +-- Optimize sa_warns table indexes (if exists) +CREATE INDEX IF NOT EXISTS `idx_warns_steamid_status` ON `sa_warns` (`player_steamid`, `status`); +CREATE INDEX IF NOT EXISTS `idx_warns_status_ends` ON `sa_warns` (`status`, `ends`); +CREATE INDEX IF NOT EXISTS `idx_warns_server_status` ON `sa_warns` (`server_id`, `status`, `ends`); + +-- Add index on sa_servers for faster lookups +CREATE INDEX IF NOT EXISTS `idx_servers_hostname` ON `sa_servers` (`hostname`); diff --git a/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs index 91de7bb..9e79d93 100644 --- a/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs @@ -9,6 +9,15 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider { var connection = new MySqlConnection(connectionString); await connection.OpenAsync(); + + await using var cmd = connection.CreateCommand(); + + cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';"; + await cmd.ExecuteNonQueryAsync(); + + cmd.CommandText = "SET time_zone = '+00:00';"; + await cmd.ExecuteNonQueryAsync(); + return connection; } diff --git a/CS2-SimpleAdmin/Events.cs b/CS2-SimpleAdmin/Events.cs index 68847f4..4a3755a 100644 --- a/CS2-SimpleAdmin/Events.cs +++ b/CS2-SimpleAdmin/Events.cs @@ -96,9 +96,6 @@ public partial class CS2_SimpleAdmin CachedPlayers.Remove(player); SilentPlayers.Remove(player.Slot); - GodPlayers.Remove(player.Slot); - SpeedPlayers.Remove(player); - GravityPlayers.Remove(player); if (player.IsBot) return HookResult.Continue; @@ -247,10 +244,6 @@ public partial class CS2_SimpleAdmin Logger.LogCritical("[OnRoundStart]"); #endif - GodPlayers.Clear(); - SpeedPlayers.Clear(); - GravityPlayers.Clear(); - foreach (var player in PlayersInfo.Values) { player.DiePosition = null; @@ -467,8 +460,6 @@ public partial class CS2_SimpleAdmin GodPlayers.Clear(); SilentPlayers.Clear(); - SpeedPlayers.Clear(); - GravityPlayers.Clear(); PlayerPenaltyManager.RemoveAllPenalties(); } @@ -494,13 +485,9 @@ public partial class CS2_SimpleAdmin { var player = @event.Userid; - if (player?.UserId == null || !player.IsValid || player.IsHLTV || player.Connected != PlayerConnectedState.PlayerConnected) - return HookResult.Continue; - - SpeedPlayers.Remove(player); - GravityPlayers.Remove(player); - - if (!PlayersInfo.ContainsKey(player.SteamID) || @event.Attacker == null) + if (player?.UserId == null || !player.IsValid || player.IsHLTV || + player.Connected != PlayerConnectedState.PlayerConnected || !PlayersInfo.ContainsKey(player.SteamID) || + @event.Attacker == null) return HookResult.Continue; var playerPosition = player.PlayerPawn.Value?.AbsOrigin; diff --git a/CS2-SimpleAdmin/Extensions/PlayerExtensions.cs b/CS2-SimpleAdmin/Extensions/PlayerExtensions.cs index 0477263..c2d8e07 100644 --- a/CS2-SimpleAdmin/Extensions/PlayerExtensions.cs +++ b/CS2-SimpleAdmin/Extensions/PlayerExtensions.cs @@ -335,10 +335,10 @@ public static class PlayerExtensions { StringBuilder sb = new(); sb.Append(localizer[messageKey, messageArgs]); - + foreach (var part in Helper.SeparateLines(sb.ToString())) { - var lineWithPrefix = localizer["sa_prefix"] + part.Trim(); + var lineWithPrefix = (CS2_SimpleAdmin._localizer?["sa_prefix"] ?? "") + part.Trim(); controller.PrintToChat(lineWithPrefix); } } diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 84b7041..089e083 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -441,20 +441,20 @@ internal static class Helper public static void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs) { string[] publishActions = ["ban", "gag", "silence", "mute"]; - + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return; if (CS2_SimpleAdmin._localizer == null) return; - + if (string.IsNullOrWhiteSpace(callerName)) callerName = CS2_SimpleAdmin._localizer["sa_console"]; var formattedMessageArgs = messageArgs.Select(arg => arg.ToString() ?? string.Empty).ToArray(); - - if (dontPublish == false && publishActions.Any(messageKey.Contains)) + + if (!dontPublish && publishActions.Any(messageKey.Contains)) { CS2_SimpleAdmin.SimpleAdminApi?.OnAdminShowActivityEvent(messageKey, callerName, dontPublish, messageArgs); } - + // // Replace placeholder based on showActivityType // for (var i = 0; i < formattedMessageArgs.Length; i++) // { @@ -478,7 +478,7 @@ internal static class Helper AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban")); } - + foreach (var controller in validPlayers.ToList()) { var currentMessageArgs = (string[])formattedMessageArgs.Clone(); @@ -499,6 +499,89 @@ internal static class Helper } } + /// + /// Shows admin activity with a custom translated message (for modules with their own localizer). + /// + public static void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null, bool dontPublish = false) + { + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return; + if (CS2_SimpleAdmin._localizer == null) return; + + if (string.IsNullOrWhiteSpace(callerName)) + callerName = CS2_SimpleAdmin._localizer["sa_console"]; + + var validPlayers = GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false }); + + if (!validPlayers.Any()) + return; + + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 3) + { + validPlayers = validPlayers.Where(c => + AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") || + AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban")); + } + + foreach (var controller in validPlayers.ToList()) + { + // Replace "CALLER" placeholder based on showActivityType + var message = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch + { + 1 => translatedMessage.Replace("CALLER", AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]), + _ => translatedMessage.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]), + }; + + // Send the pre-translated message to the player + controller.PrintToChat(message); + } + } + + /// + /// Shows admin activity using module's localizer for per-player language support. + /// Each player receives the message in their configured language using SendLocalizedMessage. + /// + public static void ShowAdminActivityLocalized(IStringLocalizer moduleLocalizer, string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs) + { + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return; + if (CS2_SimpleAdmin._localizer == null) return; + + if (string.IsNullOrWhiteSpace(callerName)) + callerName = CS2_SimpleAdmin._localizer["sa_console"]; + + var formattedMessageArgs = messageArgs.Select(arg => arg.ToString() ?? string.Empty).ToArray(); + + var validPlayers = GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false }); + + if (!validPlayers.Any()) + return; + + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 3) + { + validPlayers = validPlayers.Where(c => + AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") || + AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban")); + } + + foreach (var controller in validPlayers.ToList()) + { + var currentMessageArgs = (string[])formattedMessageArgs.Clone(); + + // Replace "CALLER" placeholder based on showActivityType + for (var i = 0; i < currentMessageArgs.Length; i++) + { + var arg = currentMessageArgs[i]; + currentMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch + { + 1 => arg.Replace("CALLER", AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]), + _ => arg.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]), + }; + } + + // Send the localized message to each player using their language + controller.SendLocalizedMessage(moduleLocalizer, messageKey, currentMessageArgs.Cast().ToArray()); + } + } + public static void DisplayCenterMessage( CCSPlayerController player, string messageKey, @@ -794,7 +877,7 @@ internal static class Helper if (CS2_SimpleAdmin.DiscordWebhookClientLog == null || CS2_SimpleAdmin._localizer == null) return; - if (caller != null && caller.IsValid == false) + if (caller != null && !caller.IsValid) caller = null; var callerName = caller == null ? CS2_SimpleAdmin._localizer["sa_console"] : caller.PlayerName; @@ -806,32 +889,38 @@ internal static class Helper commandString])); } - public static IMenu? CreateMenu(string title, Action? backAction = null) + #pragma warning disable CS8604 + public static IMenu? CreateMenu(string title, Action? backAction = null, Action? resetAction = null) { + if (CS2_SimpleAdmin.MenuApi == null) + { + return null; + } + var menuType = CS2_SimpleAdmin.Instance.Config.MenuConfigs.MenuType.ToLower(); - var menu = menuType switch { _ when menuType.Equals("selectable", StringComparison.CurrentCultureIgnoreCase) => - CS2_SimpleAdmin.MenuApi?.GetMenu(title), + CS2_SimpleAdmin.MenuApi.GetMenu(title, backAction, resetAction), _ when menuType.Equals("dynamic", StringComparison.CurrentCultureIgnoreCase) => - CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ButtonMenu), + CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ButtonMenu, backAction, resetAction), _ when menuType.Equals("center", StringComparison.CurrentCultureIgnoreCase) => - CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.CenterMenu), + CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.CenterMenu, backAction, resetAction), _ when menuType.Equals("chat", StringComparison.CurrentCultureIgnoreCase) => - CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ChatMenu), + CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ChatMenu, backAction, resetAction), _ when menuType.Equals("console", StringComparison.CurrentCultureIgnoreCase) => - CS2_SimpleAdmin.MenuApi?.GetMenuForcetype(title, MenuType.ConsoleMenu), + CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ConsoleMenu, backAction, resetAction), - _ => CS2_SimpleAdmin.MenuApi?.GetMenu(title) + _ => CS2_SimpleAdmin.MenuApi.GetMenu(title, backAction, resetAction) }; return menu; } + #pragma warning restore CS8604 internal static IPluginManager? GetPluginManager() { diff --git a/CS2-SimpleAdmin/Managers/BanManager.cs b/CS2-SimpleAdmin/Managers/BanManager.cs index 038a6d0..2aac526 100644 --- a/CS2-SimpleAdmin/Managers/BanManager.cs +++ b/CS2-SimpleAdmin/Managers/BanManager.cs @@ -29,7 +29,6 @@ internal class BanManager(IDatabaseProvider? databaseProvider) try { var sql = databaseProvider.GetAddBanQuery(); - var banId = await connection.ExecuteScalarAsync(sql, new { playerSteamid = player.SteamId.SteamId64, diff --git a/CS2-SimpleAdmin/Managers/CacheManager.cs b/CS2-SimpleAdmin/Managers/CacheManager.cs index 7398eb1..4e580c6 100644 --- a/CS2-SimpleAdmin/Managers/CacheManager.cs +++ b/CS2-SimpleAdmin/Managers/CacheManager.cs @@ -70,39 +70,44 @@ internal class CacheManager: IDisposable if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) { - var ipHistory = - (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>( - "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC")).ToList(); - - foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid)) - { - var ipSet = group - .GroupBy(x => x.address) - .Select(g => - { - var latest = g.MaxBy(x => x.used_at); - return new IpRecord( - g.Key, - latest.used_at, - string.IsNullOrEmpty(latest.name) - ? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown" - : latest.name - ); - }) - .ToHashSet(new IpRecordComparer()); + // Optimization: Load IP history and build cache in single pass + var ipHistory = await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>( + "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY steamid, address, used_at DESC"); - _playerIpsCache.AddOrUpdate( - group.Key, - _ => ipSet, - (_, existingSet) => - { - foreach (var ip in ipSet) - { - existingSet.Remove(ip); - existingSet.Add(ip); - } - return existingSet; - }); + var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; + var currentSteamId = 0UL; + var currentIpSet = new HashSet(new IpRecordComparer()); + var latestIpTimestamps = new Dictionary(); + + foreach (var record in ipHistory) + { + // When we encounter a new steamid, save the previous one + if (record.steamid != currentSteamId && currentSteamId != 0) + { + _playerIpsCache[currentSteamId] = currentIpSet; + currentIpSet = new HashSet(new IpRecordComparer()); + latestIpTimestamps.Clear(); + } + + currentSteamId = record.steamid; + + // Only keep the latest timestamp for each IP + if (!latestIpTimestamps.TryGetValue(record.address, out var existingTimestamp) || + record.used_at > existingTimestamp) + { + latestIpTimestamps[record.address] = record.used_at; + currentIpSet.Add(new IpRecord( + record.address, + record.used_at, + string.IsNullOrEmpty(record.name) ? unknownName : record.name + )); + } + } + + // Don't forget the last steamid + if (currentSteamId != 0) + { + _playerIpsCache[currentSteamId] = currentIpSet; } } @@ -150,8 +155,10 @@ internal class CacheManager: IDisposable { await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); IEnumerable updatedBans; - - var allIds = (await connection.QueryAsync("SELECT id FROM sa_bans")).ToHashSet(); + + // Optimization: Only get IDs for comparison if we need to check for deletions + // Most of the time bans are just added/updated, not deleted + HashSet? allIds = null; if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) { @@ -161,11 +168,19 @@ internal class CacheManager: IDisposable player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, - status AS Status + status AS Status FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC """, new { lastUpdate = _lastUpdateTime } )); + + // Optimization: Only fetch all IDs if there were updates + var updatedList = updatedBans.ToList(); + if (updatedList.Count > 0) + { + allIds = (await connection.QueryAsync("SELECT id FROM sa_bans")).ToHashSet(); + } + updatedBans = updatedList; } else { @@ -175,32 +190,47 @@ internal class CacheManager: IDisposable player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, - status AS Status + status AS Status FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC """, new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId } )); - } - - foreach (var id in _banCache.Keys) - { - if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue; - - if (ban.PlayerSteamId != null && - _steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans)) - { - steamBans.RemoveAll(b => b.Id == id); - if (steamBans.Count == 0) - _steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _); - } - if (string.IsNullOrWhiteSpace(ban.PlayerIp) || - !IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) || - !_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue; + // Optimization: Only fetch all IDs if there were updates + var updatedList = updatedBans.ToList(); + if (updatedList.Count > 0) { - ipBans.RemoveAll(b => b.Id == id); - if (ipBans.Count == 0) - _ipIndex.TryRemove(ipUInt, out _); + allIds = (await connection.QueryAsync( + "SELECT id FROM sa_bans WHERE server_id = @serverId", + new { serverId = CS2_SimpleAdmin.ServerId } + )).ToHashSet(); + } + updatedBans = updatedList; + } + + // Optimization: Only process deletions if we have the full ID list + if (allIds != null) + { + foreach (var id in _banCache.Keys) + { + if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue; + + if (ban.PlayerSteamId != null && + _steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans)) + { + steamBans.RemoveAll(b => b.Id == id); + if (steamBans.Count == 0) + _steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _); + } + + if (string.IsNullOrWhiteSpace(ban.PlayerIp) || + !IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) || + !_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue; + { + ipBans.RemoveAll(b => b.Id == id); + if (ipBans.Count == 0) + _ipIndex.TryRemove(ipUInt, out _); + } } } @@ -245,12 +275,21 @@ internal class CacheManager: IDisposable } } + // Update cache with new/modified bans + var hasUpdates = false; foreach (var ban in updatedBans) { _banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban); + hasUpdates = true; } - - RebuildIndexes(); + + // Always rebuild indexes if there were any updates + // This ensures status changes (ACTIVE -> UNBANNED) are reflected + if (hasUpdates) + { + RebuildIndexes(); + } + _lastUpdateTime = Time.ActualDateTime().AddSeconds(-1); } catch (Exception) @@ -266,38 +305,38 @@ internal class CacheManager: IDisposable { _steamIdIndex.Clear(); _ipIndex.Clear(); - - foreach (var ban in _banCache.Values) - { - if (ban.StatusEnum != BanStatus.ACTIVE) - continue; - if (ban.PlayerSteamId != null) + // Optimization: Cache config value to avoid repeated property access + var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType; + var checkIpBans = banType != 0; + + // Optimization: Pre-filter only ACTIVE bans to avoid checking status in loop + var activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE); + + foreach (var ban in activeBans) + { + // Index by Steam ID + if (ban.PlayerSteamId.HasValue) { - var steamId = ban.PlayerSteamId; - _steamIdIndex.AddOrUpdate( - steamId.Value, - key => [ban], - (key, list) => - { - list.Add(ban); - return list; - }); + var steamId = ban.PlayerSteamId.Value; + if (!_steamIdIndex.TryGetValue(steamId, out var steamList)) + { + steamList = new List(); + _steamIdIndex[steamId] = steamList; + } + steamList.Add(ban); } - - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) continue; - - if (ban.PlayerIp != null && + + // Index by IP (only if IP bans are enabled) + if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) && IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt)) { - _ipIndex.AddOrUpdate( - ipUInt, - key => [ban], - (key, list) => - { - list.Add(ban); - return list; - }); + if (!_ipIndex.TryGetValue(ipUInt, out var ipList)) + { + ipList = new List(); + _ipIndex[ipUInt] = ipList; + } + ipList.Add(ban); } } } @@ -332,16 +371,16 @@ internal class CacheManager: IDisposable { var ipAsUint = IpHelper.IpToUint(ipAddress); var results = new List<(ulong, DateTime, string)>(); - var comparer = _playerIpsCache.Comparer; + + // Optimization: Direct lookup using HashSet.Contains instead of TryGetValue + var searchRecord = new IpRecord(ipAsUint, default, null!); foreach (var (steamId, ipSet) in _playerIpsCache) { - if (!ipSet.TryGetValue(new IpRecord(ipAsUint, Time.ActualDateTime(), "Unknown"), out var actualEntry)) continue; - results.Add((steamId, actualEntry.UsedAt, actualEntry.PlayerName)); - + // Optimization: Single pass through the set foreach (var entry in ipSet) { - if (entry.Ip == ipAsUint && !Equals(entry, actualEntry)) + if (entry.Ip == ipAsUint) { results.Add((steamId, entry.UsedAt, entry.PlayerName)); } diff --git a/CS2-SimpleAdmin/Managers/PlayerManager.cs b/CS2-SimpleAdmin/Managers/PlayerManager.cs index 6787471..aff7930 100644 --- a/CS2-SimpleAdmin/Managers/PlayerManager.cs +++ b/CS2-SimpleAdmin/Managers/PlayerManager.cs @@ -95,66 +95,36 @@ internal class PlayerManager { await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); - if (CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer( - steamId, ipAddress)) - { - const string updateQuery = """ - UPDATE `sa_players_ips` - SET used_at = CURRENT_TIMESTAMP, - name = @playerName - WHERE steamid = @SteamID AND address = @IPAddress; - """; - await connection.ExecuteAsync(updateQuery, new - { - playerName, - SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64, - IPAddress = IpHelper.IpToUint(ipAddress) - }); - } - else - { - const string selectQuery = - "SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;"; - var recordExists = await connection.ExecuteScalarAsync(selectQuery, new - { - SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64, - IPAddress = IpHelper.IpToUint(ipAddress) - }); + // Eliminates the need for SELECT COUNT and duplicate UPDATE queries + var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64; + var ipUint = IpHelper.IpToUint(ipAddress); - if (recordExists > 0) - { - const string updateQuery = """ - UPDATE `sa_players_ips` - SET used_at = CURRENT_TIMESTAMP, - name = @playerName - WHERE steamid = @SteamID AND address = @IPAddress; - """; - await connection.ExecuteAsync(updateQuery, new - { - playerName, - SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64, - IPAddress = IpHelper.IpToUint(ipAddress) - }); - } - else - { - const string insertQuery = """ - INSERT INTO `sa_players_ips` (steamid, name, address, used_at) - VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP); - """; - await connection.ExecuteAsync(insertQuery, new - { - SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64, - playerName, - IPAddress = IpHelper.IpToUint(ipAddress) - }); - } - } + // MySQL: INSERT ... ON DUPLICATE KEY UPDATE pattern + const string upsertQuery = """ + INSERT INTO `sa_players_ips` (steamid, name, address, used_at) + VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE + used_at = CURRENT_TIMESTAMP, + name = @playerName; + """; + + await connection.ExecuteAsync(upsertQuery, new + { + SteamID = steamId64, + playerName, + IPAddress = ipUint + }); + + // // Cache will be updated on next refresh cycle + // if (!CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(steamId, ipAddress)) + // { + // // IP association will be reflected after cache refresh + // } } catch (Exception ex) { CS2_SimpleAdmin._logger?.LogError( - $"Unable to save ip address for {playerInfo.Name} ({ipAddress}) {ex.Message}"); + $"Unable to save ip address for {playerInfo.Name} ({ipAddress}): {ex.Message}"); } playerInfo.AccountsAssociated = @@ -313,31 +283,6 @@ internal class PlayerManager /// public void CheckPlayersTimer() { - CS2_SimpleAdmin.Instance.AddTimer(0.12f, () => - { - if (CS2_SimpleAdmin.SpeedPlayers.Count > 0) - { - foreach (var (player, speed) in CS2_SimpleAdmin.SpeedPlayers) - { - if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }) - { - player.SetSpeed(speed); - } - } - } - - if (CS2_SimpleAdmin.GravityPlayers.Count > 0) - { - foreach (var (player, gravity) in CS2_SimpleAdmin.GravityPlayers) - { - if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }) - { - player.SetGravity(gravity); - } - } - } - }, TimerFlags.REPEAT); - CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () => { #if DEBUG @@ -346,18 +291,26 @@ internal class PlayerManager if (CS2_SimpleAdmin.DatabaseProvider == null) return; - var tempPlayers = Helper.GetValidPlayers() - .Select(p => new - { - p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot, - }) - .ToList(); - + // Optimization: Get players once and avoid allocating anonymous types + var validPlayers = Helper.GetValidPlayers(); + if (validPlayers.Count == 0) + return; + + // Use ValueTuple instead of anonymous type - better performance and less allocations + var tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count); + foreach (var p in validPlayers) + { + tempPlayers.Add((p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot)); + } + var pluginInstance = CS2_SimpleAdmin.Instance; + var config = _config.OtherSettings; // Cache config access + _ = Task.Run(async () => { try { + // Run all expire tasks in parallel var expireTasks = new[] { pluginInstance.BanManager.ExpireOldBans(), @@ -384,22 +337,33 @@ internal class PlayerManager if (pluginInstance.CacheManager == null) return; - - var bannedPlayers = tempPlayers.AsValueEnumerable() - .Where(player => - { - var playerName = player.PlayerName; - var steamId = player.SteamID; - var ip = player.IpAddress?.Split(':')[0]; - return CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch - { - 0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null), - _ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp - ? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip) - : pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip) - }; - }).ToList(); + // Optimization: Cache ban type and multi-account check to avoid repeated config access + var banType = config.BanType; + var checkMultiAccounts = config.CheckMultiAccountsByIp; + + var bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(); + + // Manual loop instead of LINQ - better performance + foreach (var player in tempPlayers) + { + var playerName = player.PlayerName; + var steamId = player.SteamID; + var ip = player.IpAddress?.Split(':')[0]; + + bool isBanned = banType switch + { + 0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null), + _ => checkMultiAccounts + ? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip) + : pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip) + }; + + if (isBanned) + { + bannedPlayers.Add(player); + } + } if (bannedPlayers.Count > 0) { @@ -410,32 +374,38 @@ internal class PlayerManager } } - if (_config.OtherSettings.TimeMode == 0) + if (config.TimeMode == 0) { - var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList(); - if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return; + // Optimization: Manual projection instead of LINQ + var onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count); + foreach (var player in tempPlayers) + { + onlinePlayers.Add((player.SteamID, player.UserId, player.Slot)); + } + + if (onlinePlayers.Count > 0) + { await pluginInstance.MuteManager.CheckOnlineModeMutes(onlinePlayers); + } } }); - + try { + // Optimization: Process penalties without LINQ allocations var players = Helper.GetValidPlayers(); - var penalizedSlots = players - .Where(player => PlayerPenaltyManager.IsSlotInPenalties(player.Slot)) - .Select(player => new - { - Player = player, - IsMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _), - IsSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _), - IsGagged = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _) - }); - - foreach (var entry in penalizedSlots) + foreach (var player in players) { - if (!entry.IsMuted && !entry.IsSilenced) + if (!PlayerPenaltyManager.IsSlotInPenalties(player.Slot)) + continue; + + var isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _); + var isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _); + + // Only reset voice flags if not muted or silenced + if (!isMuted && !isSilenced) { - entry.Player.VoiceFlags = VoiceFlags.Normal; + player.VoiceFlags = VoiceFlags.Normal; } } diff --git a/CS2-SimpleAdmin/Managers/ServerManager.cs b/CS2-SimpleAdmin/Managers/ServerManager.cs index ccde9cf..e2401bc 100644 --- a/CS2-SimpleAdmin/Managers/ServerManager.cs +++ b/CS2-SimpleAdmin/Managers/ServerManager.cs @@ -35,29 +35,43 @@ public class ServerManager { if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return; - if (_getIpTryCount > 32 && Helper.GetServerIp().StartsWith("0.0.0.0") || string.IsNullOrEmpty(Helper.GetServerIp())) + // Optimization: Get server IP once and reuse + var serverIp = Helper.GetServerIp(); + var isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0"); + + // Check if we've exceeded retry limit with invalid IP + if (_getIpTryCount > 32 && isInvalidIp) { CS2_SimpleAdmin._logger?.LogError("Unable to load server data - can't fetch ip address!"); return; } - var ipAddress = ConVar.Find("ip")?.StringValue; + // Optimization: Cache ConVar lookups + var ipConVar = ConVar.Find("ip"); + var ipAddress = ipConVar?.StringValue; + + // Use Helper IP if ConVar IP is invalid if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0")) { - ipAddress = Helper.GetServerIp(); + ipAddress = serverIp; - if (_getIpTryCount <= 32 && (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))) + // Retry if still invalid and under retry limit + if (_getIpTryCount <= 32 && isInvalidIp) { _getIpTryCount++; - LoadServerData(); return; } } - var address = $"{ipAddress}:{ConVar.Find("hostport")?.GetPrimitiveValue()}"; - var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; - var rconPassword = ConVar.Find("rcon_password")?.StringValue ?? ""; + // Optimization: Cache remaining ConVar lookups + var hostportConVar = ConVar.Find("hostport"); + var hostnameConVar = ConVar.Find("hostname"); + var rconPasswordConVar = ConVar.Find("rcon_password"); + + var address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue()}"; + var hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; + var rconPassword = rconPasswordConVar?.StringValue ?? ""; CS2_SimpleAdmin.IpAddress = address; Task.Run(async () => @@ -118,6 +132,8 @@ public class ServerManager } } }); + + CS2_SimpleAdmin.SimpleAdminApi?.OnSimpleAdminReadyEvent(); }); } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Menus/AdminMenu.cs b/CS2-SimpleAdmin/Menus/AdminMenu.cs index 77c7ba3..4770d62 100644 --- a/CS2-SimpleAdmin/Menus/AdminMenu.cs +++ b/CS2-SimpleAdmin/Menus/AdminMenu.cs @@ -7,6 +7,11 @@ namespace CS2_SimpleAdmin.Menus; public static class AdminMenu { + public static void OpenMenu(CCSPlayerController admin) + { + MenuManager.Instance.OpenMainMenu(admin); + } + public static IMenu? CreateMenu(string title, Action? backAction = null) { return Helper.CreateMenu(title, backAction); @@ -27,44 +32,44 @@ public static class AdminMenu // } } - public static void OpenMenu(CCSPlayerController admin) - { - if (admin.IsValid == false) - return; - - var localizer = CS2_SimpleAdmin._localizer; - if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false) - { - admin.PrintToChat(localizer?["sa_prefix"] ?? - "[SimpleAdmin] " + - (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command") - ); - return; - } - - var menu = CreateMenu(localizer?["sa_title"] ?? "SimpleAdmin"); - List options = - [ - new(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)), - new(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)), - new(localizer?["sa_menu_fun_commands"] ?? "Fun Commands", () => FunActionsMenu.OpenMenu(admin)), - ]; - - var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands; - if (customCommands.Count > 0) - { - options.Add(new ChatMenuOptionData(localizer?["sa_menu_custom_commands"] ?? "Custom Commands", () => CustomCommandsMenu.OpenMenu(admin))); - } - - if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root")) - options.Add(new ChatMenuOptionData(localizer?["sa_menu_admins_manage"] ?? "Admins Manage", () => ManageAdminsMenu.OpenMenu(admin))); - - foreach (var menuOptionData in options) - { - var menuName = menuOptionData.Name; - menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled); - } - - if (menu != null) OpenMenu(admin, menu); - } + // public static void OpenMenu(CCSPlayerController admin) + // { + // if (admin.IsValid == false) + // return; + // + // var localizer = CS2_SimpleAdmin._localizer; + // if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false) + // { + // admin.PrintToChat(localizer?["sa_prefix"] ?? + // "[SimpleAdmin] " + + // (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command") + // ); + // return; + // } + // + // var menu = CreateMenu(localizer?["sa_title"] ?? "SimpleAdmin"); + // List options = + // [ + // new(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)), + // new(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)), + // new(localizer?["sa_menu_fun_commands"] ?? "Fun Commands", () => FunActionsMenu.OpenMenu(admin)), + // ]; + // + // var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands; + // if (customCommands.Count > 0) + // { + // options.Add(new ChatMenuOptionData(localizer?["sa_menu_custom_commands"] ?? "Custom Commands", () => CustomCommandsMenu.OpenMenu(admin))); + // } + // + // if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root")) + // options.Add(new ChatMenuOptionData(localizer?["sa_menu_admins_manage"] ?? "Admins Manage", () => ManageAdminsMenu.OpenMenu(admin))); + // + // foreach (var menuOptionData in options) + // { + // var menuName = menuOptionData.Name; + // menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled); + // } + // + // if (menu != null) OpenMenu(admin, menu); + // } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Menus/BasicMenu.cs b/CS2-SimpleAdmin/Menus/BasicMenu.cs new file mode 100644 index 0000000..b0fba78 --- /dev/null +++ b/CS2-SimpleAdmin/Menus/BasicMenu.cs @@ -0,0 +1,602 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Admin; +using CounterStrikeSharp.API.Modules.Entities; +using CounterStrikeSharp.API.Modules.Entities.Constants; +using CounterStrikeSharp.API.Modules.Utils; +using CS2_SimpleAdminApi; + +namespace CS2_SimpleAdmin.Menus; + +public abstract class BasicMenu +{ + /// + /// Initializes all menus in the system by registering them with the MenuManager. + /// + public static void Initialize() + { + var manager = MenuManager.Instance; + + // Players category menus + manager.RegisterMenu("players", "slap", "Slap Player", CreateSlapMenu, "@css/slay"); + manager.RegisterMenu("players", "slay", "Slay Player", CreateSlayMenu, "@css/slay"); + manager.RegisterMenu("players", "kick", "Kick Player", CreateKickMenu, "@css/kick"); + manager.RegisterMenu("players", "warn", "Warn Player", CreateWarnMenu, "@css/kick"); + manager.RegisterMenu("players", "ban", "Ban Player", CreateBanMenu, "@css/ban"); + manager.RegisterMenu("players", "gag", "Gag Player", CreateGagMenu, "@css/chat"); + manager.RegisterMenu("players", "mute", "Mute Player", CreateMuteMenu, "@css/chat"); + manager.RegisterMenu("players", "silence", "Silence Player", CreateSilenceMenu, "@css/chat"); + manager.RegisterMenu("players", "team", "Force Team", CreateForceTeamMenu, "@css/kick"); + + // Server category menus + manager.RegisterMenu("server", "plugins", "Manage Plugins", CreatePluginsMenu, "@css/root"); + manager.RegisterMenu("server", "changemap", "Change Map", CreateChangeMapMenu, "@css/changemap"); + manager.RegisterMenu("server", "restart", "Restart Game", CreateRestartGameMenu, "@css/generic"); + manager.RegisterMenu("server", "custom", "Custom Commands", CreateCustomCommandsMenu, "@css/generic"); + + // Admin category menus + manager.RegisterMenu("admin", "add", "Add Admin", CreateAddAdminMenu, "@css/root"); + manager.RegisterMenu("admin", "remove", "Remove Admin", CreateRemoveAdminMenu, "@css/root"); + manager.RegisterMenu("admin", "reload", "Reload Admins", CreateReloadAdminsMenu, "@css/root"); + } + + /// + /// Creates menu for slapping players with selectable damage amounts. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the slap menu. + + private static MenuBuilder CreateSlapMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var slapMenu = new MenuBuilder(localizer?["sa_slap"] ?? "Slap Player"); + + var players = Helper.GetValidPlayers().Where(admin.CanTarget); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + slapMenu.AddSubMenu(playerName, () => CreateSlapDamageMenu(admin, player)); + } + + return slapMenu.WithBackButton(); + } + + /// + /// Creates damage selection submenu for slapping a specific player. + /// + /// The admin player executing the slap. + /// The target player to be slapped. + /// A MenuBuilder instance for the slap damage menu. + private static MenuBuilder CreateSlapDamageMenu(CCSPlayerController admin, CCSPlayerController target) + { + var slapDamageMenu = new MenuBuilder($"Slap: {target.PlayerName}"); + var damages = new[] { 0, 1, 5, 10, 50, 100 }; + + foreach (var damage in damages) + { + slapDamageMenu.AddOption($"{damage} HP", _ => + { + if (target.IsValid) + { + CS2_SimpleAdmin.Slap(admin, target, damage); + // Keep menu open for consecutive slaps + CreateSlapDamageMenu(admin, target).OpenMenu(admin); + } + }); + } + + return slapDamageMenu.WithBackButton(); + } + + /// + /// Creates menu for slaying (killing) players. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the slay menu. + private static MenuBuilder CreateSlayMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var slayMenu = new MenuBuilder(localizer?["sa_slay"] ?? "Slay Player"); + + var players = Helper.GetValidPlayers().Where(admin.CanTarget); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + slayMenu.AddOption(playerName, _ => + { + if (player.IsValid) + { + CS2_SimpleAdmin.Slay(admin, player); + } + }); + } + + return slayMenu.WithBackButton(); + } + + /// + /// Creates menu for kicking players with reason selection. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the kick menu. + private static MenuBuilder CreateKickMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var kickMenu = new MenuBuilder(localizer?["sa_kick"] ?? "Kick Player"); + + var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, player, "Kick", PenaltyType.Kick, + (_, _, reason) => + { + if (player.IsValid) + { + CS2_SimpleAdmin.Instance.Kick(admin, player, reason, admin.PlayerName); + } + })); + } + + return kickMenu.WithBackButton(); + } + + /// + /// Creates menu for warning players with duration and reason selection. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the warn menu. + private static MenuBuilder CreateWarnMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var warnMenu = new MenuBuilder(localizer?["sa_warn"] ?? "Warn Player"); + + var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + warnMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Warn", + (_, _, duration) => CreateReasonMenu(admin, player, "Warn", PenaltyType.Warn, + (_, _, reason) => + { + if (player.IsValid) + { + CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason, admin.PlayerName); + } + }))); + } + + return warnMenu.WithBackButton(); + } + + /// + /// Creates menu for banning players with duration and reason selection. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the ban menu. + private static MenuBuilder CreateBanMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var banMenu = new MenuBuilder(localizer?["sa_ban"] ?? "Ban Player"); + + var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + banMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Ban", + (_, _, duration) => CreateReasonMenu(admin, player, "Ban", PenaltyType.Ban, + (_, _, reason) => + { + if (player.IsValid) + { + CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason, admin.PlayerName); + } + }))); + } + + return banMenu.WithBackButton(); + } + + /// + /// Creates menu for gagging (text chat muting) players with duration and reason selection. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the gag menu. + private static MenuBuilder CreateGagMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var gagMenu = new MenuBuilder(localizer?["sa_gag"] ?? "Gag Player"); + + var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + gagMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Gag", + (_, _, duration) => CreateReasonMenu(admin, player, "Gag", PenaltyType.Gag, + (_, _, reason) => + { + if (player.IsValid) + { + CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason); + } + }))); + } + + return gagMenu.WithBackButton(); + } + + /// + /// Creates menu for muting (voice chat muting) players with duration and reason selection. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the mute menu. + private static MenuBuilder CreateMuteMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var muteMenu = new MenuBuilder(localizer?["sa_mute"] ?? "Mute Player"); + + var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + muteMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Mute", + (_, _, duration) => CreateReasonMenu(admin, player, "Mute", PenaltyType.Mute, + (_, _, reason) => + { + if (player.IsValid) + { + CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason); + } + }))); + } + + return muteMenu.WithBackButton(); + } + + /// + /// Creates menu for silencing (both text and voice chat muting) players with duration and reason selection. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the silence menu. + private static MenuBuilder CreateSilenceMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var silenceMenu = new MenuBuilder(localizer?["sa_silence"] ?? "Silence Player"); + + var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + silenceMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Silence", + (_, _, duration) => CreateReasonMenu(admin, player, "Silence", PenaltyType.Silence, + (_, _, reason) => + { + if (player.IsValid) + { + CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason); + } + }))); + } + + return silenceMenu.WithBackButton(); + } + + /// + /// Creates menu for forcing players to switch teams. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the force team menu. + private static MenuBuilder CreateForceTeamMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var teamMenu = new MenuBuilder(localizer?["sa_team_force"] ?? "Force Team"); + + var players = Helper.GetValidPlayers().Where(p => admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + teamMenu.AddSubMenu(playerName, () => CreateTeamSelectionMenu(admin, player)); + } + + return teamMenu.WithBackButton(); + } + + /// + /// Creates team selection submenu for forcing a specific player to a team. + /// + /// The admin player executing the team change. + /// The target player to be moved. + /// A MenuBuilder instance for the team selection menu. + private static MenuBuilder CreateTeamSelectionMenu(CCSPlayerController admin, CCSPlayerController target) + { + var localizer = CS2_SimpleAdmin._localizer; + var teamSelectionMenu = new MenuBuilder($"Force Team: {target.PlayerName}"); + + var teams = new[] + { + (localizer?["sa_team_ct"] ?? "CT", "ct", CsTeam.CounterTerrorist), + (localizer?["sa_team_t"] ?? "T", "t", CsTeam.Terrorist), + (localizer?["sa_team_swap"] ?? "Swap", "swap", CsTeam.Spectator), + (localizer?["sa_team_spec"] ?? "Spec", "spec", CsTeam.Spectator) + }; + + foreach (var (name, teamName, teamNum) in teams) + { + teamSelectionMenu.AddOption(name, _ => + { + if (target.IsValid) + { + CS2_SimpleAdmin.ChangeTeam(admin, target, teamName, teamNum, true); + } + }); + } + + return teamSelectionMenu.WithBackButton(); + } + + /// + /// Creates menu for managing server plugins. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the plugins menu. + private static MenuBuilder CreatePluginsMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var pluginsMenu = new MenuBuilder(localizer?["sa_menu_pluginsmanager_title"] ?? "Manage Plugins"); + + pluginsMenu.AddOption("Open Plugins Manager", _ => + { + admin.ExecuteClientCommandFromServer("css_pluginsmanager"); + }); + + return pluginsMenu.WithBackButton(); + } + + /// + /// Creates menu for changing the current map (includes default and workshop maps). + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the change map menu. + private static MenuBuilder CreateChangeMapMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var mapMenu = new MenuBuilder(localizer?["sa_changemap"] ?? "Change Map"); + + // Add default maps + var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps; + foreach (var map in maps) + { + mapMenu.AddOption(map, _ => + { + CS2_SimpleAdmin.Instance.ChangeMap(admin, map); + }); + } + + // Add workshop maps + var wsMaps = CS2_SimpleAdmin.Instance.Config.WorkshopMaps; + foreach (var wsMap in wsMaps) + { + mapMenu.AddOption($"{wsMap.Key} (WS)", _ => + { + CS2_SimpleAdmin.Instance.ChangeWorkshopMap(admin, wsMap.Value?.ToString() ?? wsMap.Key); + }); + } + + return mapMenu.WithBackButton(); + } + + /// + /// Creates menu for restarting the current game/round. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the restart game menu. + private static MenuBuilder CreateRestartGameMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var restartMenu = new MenuBuilder(localizer?["sa_restart_game"] ?? "Restart Game"); + + restartMenu.AddOption("Restart Round", _ => + { + CS2_SimpleAdmin.RestartGame(admin); + }); + + return restartMenu.WithBackButton(); + } + + /// + /// Creates menu for executing custom server commands defined in configuration. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the custom commands menu. + private static MenuBuilder CreateCustomCommandsMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var customMenu = new MenuBuilder(localizer?["sa_menu_custom_commands"] ?? "Custom Commands"); + + var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands; + + foreach (var customCommand in customCommands) + { + if (string.IsNullOrEmpty(customCommand.DisplayName) || string.IsNullOrEmpty(customCommand.Command)) + continue; + + var steamId = new SteamID(admin.SteamID); + 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(); + } + + /// + /// Creates menu for adding admin privileges to players. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the add admin menu. + private static MenuBuilder CreateAddAdminMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var addAdminMenu = new MenuBuilder(localizer?["sa_admin_add"] ?? "Add Admin"); + + var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + addAdminMenu.AddSubMenu(playerName, () => CreateAdminFlagsMenu(admin, player)); + } + + return addAdminMenu.WithBackButton(); + } + + /// + /// Creates admin flags selection submenu for granting specific permissions to a player. + /// + /// The admin player granting permissions. + /// The target player to receive admin privileges. + /// A MenuBuilder instance for the admin flags menu. + private static MenuBuilder CreateAdminFlagsMenu(CCSPlayerController admin, CCSPlayerController target) + { + var flagsMenu = new MenuBuilder($"Add Admin: {target.PlayerName}"); + + foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags) + { + var 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); // Disabled if player already has this flag + } + + return flagsMenu.WithBackButton(); + } + + /// + /// Creates menu for removing admin privileges from players. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the remove admin menu. + private static MenuBuilder CreateRemoveAdminMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var removeAdminMenu = new MenuBuilder(localizer?["sa_admin_remove"] ?? "Remove Admin"); + + var adminPlayers = Helper.GetValidPlayers().Where(p => + AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0 && + p != admin && + admin.CanTarget(p)); + + foreach (var player in adminPlayers) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + removeAdminMenu.AddOption(playerName, _ => + { + if (player.IsValid) + { + CS2_SimpleAdmin.Instance.RemoveAdmin(admin, player.SteamID.ToString()); + } + }); + } + + return removeAdminMenu.WithBackButton(); + } + + /// + /// Creates menu for reloading admin list from database. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the reload admins menu. + private static MenuBuilder CreateReloadAdminsMenu(CCSPlayerController admin) + { + var localizer = CS2_SimpleAdmin._localizer; + var reloadMenu = new MenuBuilder(localizer?["sa_admin_reload"] ?? "Reload Admins"); + + reloadMenu.AddOption("Reload Admins", _ => + { + CS2_SimpleAdmin.Instance.ReloadAdmins(admin); + }); + + return reloadMenu.WithBackButton(); + } + + /// + /// Creates duration selection submenu for time-based penalties (ban, mute, gag, etc.). + /// + /// The admin player selecting duration. + /// The target player for the penalty. + /// The name of the penalty action. + /// Callback action executed when duration is selected. + /// A MenuBuilder instance for the duration menu. + private static MenuBuilder CreateDurationMenu(CCSPlayerController admin, CCSPlayerController player, string actionName, + Action onSelectAction) + { + var durationMenu = new MenuBuilder($"{actionName} Duration: {player.PlayerName}"); + + foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) + { + durationMenu.AddOption(durationItem.Name, _ => + { + onSelectAction(admin, player, durationItem.Duration); + }); + } + + return durationMenu.WithBackButton(); + } + + /// + /// Creates reason selection submenu for penalties with predefined reasons from configuration. + /// + /// The admin player selecting reason. + /// The target player for the penalty. + /// The name of the penalty action. + /// The type of penalty to determine which reason list to use. + /// Callback action executed when reason is selected. + /// A MenuBuilder instance for the reason menu. + private static MenuBuilder CreateReasonMenu(CCSPlayerController admin, CCSPlayerController player, string actionName, + PenaltyType penaltyType, Action onSelectAction) + { + var reasonMenu = new MenuBuilder($"{actionName} Reason: {player.PlayerName}"); + + var reasons = penaltyType switch + { + PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons, + PenaltyType.Kick => CS2_SimpleAdmin.Instance.Config.MenuConfigs.KickReasons, + PenaltyType.Mute => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons, + PenaltyType.Warn => CS2_SimpleAdmin.Instance.Config.MenuConfigs.WarnReasons, + PenaltyType.Gag or PenaltyType.Silence => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons, + _ => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons + }; + + foreach (var reason in reasons) + { + reasonMenu.AddOption(reason, _ => + { + onSelectAction(admin, player, reason); + }); + } + + return reasonMenu.WithBackButton(); + } + } diff --git a/CS2-SimpleAdmin/Menus/CustomCommandsMenu.cs b/CS2-SimpleAdmin/Menus/CustomCommandsMenu.cs index 47eb577..0c883b2 100644 --- a/CS2-SimpleAdmin/Menus/CustomCommandsMenu.cs +++ b/CS2-SimpleAdmin/Menus/CustomCommandsMenu.cs @@ -9,11 +9,11 @@ public static class CustomCommandsMenu { public static void OpenMenu(CCSPlayerController admin) { - if (admin.IsValid == false) + if (!admin.IsValid) return; var localizer = CS2_SimpleAdmin._localizer; - if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false) + if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic")) { admin.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " + diff --git a/CS2-SimpleAdmin/Menus/DurationMenu.cs b/CS2-SimpleAdmin/Menus/DurationMenu.cs index a0cc724..20a0acc 100644 --- a/CS2-SimpleAdmin/Menus/DurationMenu.cs +++ b/CS2-SimpleAdmin/Menus/DurationMenu.cs @@ -8,7 +8,6 @@ public static class DurationMenu public static void OpenMenu(CCSPlayerController admin, string menuName, CCSPlayerController player, Action onSelectAction) { var menu = AdminMenu.CreateMenu(menuName); - foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) { menu?.AddMenuOption(durationItem.Name, (_, _) => { onSelectAction(admin, player, durationItem.Duration); }); @@ -20,7 +19,6 @@ public static class DurationMenu public static void OpenMenu(CCSPlayerController admin, string menuName, DisconnectedPlayer player, Action onSelectAction) { var menu = AdminMenu.CreateMenu(menuName); - foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) { menu?.AddMenuOption(durationItem.Name, (_, _) => { onSelectAction(admin, player, durationItem.Duration); }); diff --git a/CS2-SimpleAdmin/Menus/FunActionsMenu.cs b/CS2-SimpleAdmin/Menus/FunActionsMenu.cs index 394f27b..f691c91 100644 --- a/CS2-SimpleAdmin/Menus/FunActionsMenu.cs +++ b/CS2-SimpleAdmin/Menus/FunActionsMenu.cs @@ -1,267 +1,267 @@ -using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Admin; -using CounterStrikeSharp.API.Modules.Entities; -using CounterStrikeSharp.API.Modules.Entities.Constants; - -namespace CS2_SimpleAdmin.Menus; - -public static class FunActionsMenu -{ - private static Dictionary? _weaponsCache; - - private static Dictionary GetWeaponsCache - { - get - { - if (_weaponsCache != null) return _weaponsCache; - - var weaponsArray = Enum.GetValues(typeof(CsItem)); - - // avoid duplicates in the menu - _weaponsCache = new Dictionary(); - foreach (CsItem item in weaponsArray) - { - if (item == CsItem.Tablet) - continue; - - _weaponsCache[(int)item] = item; - } - - return _weaponsCache; - } - } - - public static void OpenMenu(CCSPlayerController admin) - { - if (admin.IsValid == false) - return; - - var localizer = CS2_SimpleAdmin._localizer; - if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false) - { - admin.PrintToChat(localizer?["sa_prefix"] ?? - "[SimpleAdmin] " + - (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command") - ); - return; - } - - var menu = AdminMenu.CreateMenu(localizer?["sa_menu_fun_commands"] ?? "Fun Commands"); - List options = []; - - //var hasCheats = AdminManager.PlayerHasPermissions(admin, "@css/cheats"); - //var hasSlay = AdminManager.PlayerHasPermissions(admin, "@css/slay"); - - // options added in order - - if (AdminManager.CommandIsOverriden("css_god") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_god")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats")) - options.Add(new ChatMenuOptionData(localizer?["sa_godmode"] ?? "God Mode", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_godmode"] ?? "God Mode", GodMode))); - if (AdminManager.CommandIsOverriden("css_noclip") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_noclip")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats")) - options.Add(new ChatMenuOptionData(localizer?["sa_noclip"] ?? "No Clip", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_noclip"] ?? "No Clip", NoClip))); - if (AdminManager.CommandIsOverriden("css_respawn") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_respawn")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats")) - options.Add(new ChatMenuOptionData(localizer?["sa_respawn"] ?? "Respawn", () => PlayersMenu.OpenDeadMenu(admin, localizer?["sa_respawn"] ?? "Respawn", Respawn))); - if (AdminManager.CommandIsOverriden("css_give") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_give")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats")) - options.Add(new ChatMenuOptionData(localizer?["sa_give_weapon"] ?? "Give Weapon", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_give_weapon"] ?? "Give Weapon", GiveWeaponMenu))); - - if (AdminManager.CommandIsOverriden("css_strip") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_strip")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) - options.Add(new ChatMenuOptionData(localizer?["sa_strip_weapons"] ?? "Strip Weapons", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_strip_weapons"] ?? "Strip Weapons", StripWeapons))); - if (AdminManager.CommandIsOverriden("css_freeze") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_freeze")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) - options.Add(new ChatMenuOptionData(localizer?["sa_freeze"] ?? "Freeze", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_freeze"] ?? "Freeze", Freeze))); - if (AdminManager.CommandIsOverriden("css_hp") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_hp")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) - options.Add(new ChatMenuOptionData(localizer?["sa_set_hp"] ?? "Set Hp", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_hp"] ?? "Set Hp", SetHpMenu))); - if (AdminManager.CommandIsOverriden("css_speed") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_speed")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) - options.Add(new ChatMenuOptionData(localizer?["sa_set_speed"] ?? "Set Speed", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_speed"] ?? "Set Speed", SetSpeedMenu))); - if (AdminManager.CommandIsOverriden("css_gravity") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gravity")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) - options.Add(new ChatMenuOptionData(localizer?["sa_set_gravity"] ?? "Set Gravity", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_gravity"] ?? "Set Gravity", SetGravityMenu))); - if (AdminManager.CommandIsOverriden("css_money") - ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_money")) - : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) - options.Add(new ChatMenuOptionData(localizer?["sa_set_money"] ?? "Set Money", () => PlayersMenu.OpenMenu(admin, localizer?["sa_set_money"] ?? "Set Money", SetMoneyMenu))); - - foreach (var menuOptionData in options) - { - var menuName = menuOptionData.Name; - menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action(); }, menuOptionData.Disabled); - } - - if (menu != null) AdminMenu.OpenMenu(admin, menu); - } - - private static void GodMode(CCSPlayerController admin, CCSPlayerController player) - { - CS2_SimpleAdmin.God(admin, player); - } - - private static void NoClip(CCSPlayerController admin, CCSPlayerController player) - { - CS2_SimpleAdmin.NoClip(admin, player); - } - - private static void Respawn(CCSPlayerController? admin, CCSPlayerController player) - { - CS2_SimpleAdmin.Respawn(admin, player); - } - - private static void GiveWeaponMenu(CCSPlayerController admin, CCSPlayerController player) - { - var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_give_weapon"] ?? "Give Weapon"}: {player.PlayerName}"); - - foreach (var weapon in GetWeaponsCache) - { - menu?.AddMenuOption(weapon.Value.ToString(), (_, _) => { GiveWeapon(admin, player, weapon.Value); }); - } - - if (menu != null) AdminMenu.OpenMenu(admin, menu); - } - - private static void GiveWeapon(CCSPlayerController admin, CCSPlayerController player, CsItem weaponValue) - { - CS2_SimpleAdmin.GiveWeapon(admin, player, weaponValue); - } - - private static void StripWeapons(CCSPlayerController admin, CCSPlayerController player) - { - CS2_SimpleAdmin.StripWeapons(admin, player); - } - - private static void Freeze(CCSPlayerController admin, CCSPlayerController player) - { - if (!(player.PlayerPawn.Value?.IsValid ?? false)) - return; - - if (player.PlayerPawn.Value.MoveType != MoveType_t.MOVETYPE_INVALID) - CS2_SimpleAdmin.Freeze(admin, player, -1); - else - CS2_SimpleAdmin.Unfreeze(admin, player); - } - - private static void SetHpMenu(CCSPlayerController admin, CCSPlayerController player) - { - var hpArray = new[] - { - new Tuple("1", 1), - new Tuple("10", 10), - new Tuple("25", 25), - new Tuple("50", 50), - new Tuple("100", 100), - new Tuple("200", 200), - new Tuple("500", 500), - new Tuple("999", 999) - }; - - var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_hp"] ?? "Set Hp"}: {player.PlayerName}"); - - foreach (var (optionName, value) in hpArray) - { - menu?.AddMenuOption(optionName, (_, _) => { SetHp(admin, player, value); }); - } - - if (menu != null) AdminMenu.OpenMenu(admin, menu); - } - - private static void SetHp(CCSPlayerController admin, CCSPlayerController player, int hp) - { - CS2_SimpleAdmin.SetHp(admin, player, hp); - } - - private static void SetSpeedMenu(CCSPlayerController admin, CCSPlayerController player) - { - var speedArray = new[] - { - new Tuple("0.1", .1f), - new Tuple("0.25", .25f), - new Tuple("0.5", .5f), - new Tuple("0.75", .75f), - new Tuple("1", 1), - new Tuple("2", 2), - new Tuple("3", 3), - new Tuple("4", 4) - }; - - var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_speed"] ?? "Set Speed"}: {player.PlayerName}"); - - foreach (var (optionName, value) in speedArray) - { - menu?.AddMenuOption(optionName, (_, _) => { SetSpeed(admin, player, value); }); - } - - if (menu != null) AdminMenu.OpenMenu(admin, menu); - } - - private static void SetSpeed(CCSPlayerController admin, CCSPlayerController player, float speed) - { - CS2_SimpleAdmin.SetSpeed(admin, player, speed); - } - - private static void SetGravityMenu(CCSPlayerController admin, CCSPlayerController player) - { - var gravityArray = new[] - { - new Tuple("0.1", .1f), - new Tuple("0.25", .25f), - new Tuple("0.5", .5f), - new Tuple("0.75", .75f), - new Tuple("1", 1), - new Tuple("2", 2) - }; - - var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_gravity"] ?? "Set Gravity"}: {player.PlayerName}"); - - foreach (var (optionName, value) in gravityArray) - { - menu?.AddMenuOption(optionName, (_, _) => { SetGravity(admin, player, value); }); - } - - if (menu != null) AdminMenu.OpenMenu(admin, menu); - } - - private static void SetGravity(CCSPlayerController admin, CCSPlayerController player, float gravity) - { - CS2_SimpleAdmin.SetGravity(admin, player, gravity); - } - - private static void SetMoneyMenu(CCSPlayerController admin, CCSPlayerController player) - { - var moneyArray = new[] - { - new Tuple("$0", 0), - new Tuple("$1000", 1000), - new Tuple("$2500", 2500), - new Tuple("$5000", 5000), - new Tuple("$10000", 10000), - new Tuple("$16000", 16000) - }; - - var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_money"] ?? "Set Money"}: {player.PlayerName}"); - - foreach (var (optionName, value) in moneyArray) - { - menu?.AddMenuOption(optionName, (_, _) => { SetMoney(admin, player, value); }); - } - - if (menu != null) AdminMenu.OpenMenu(admin, menu); - } - - private static void SetMoney(CCSPlayerController admin, CCSPlayerController player, int money) - { - CS2_SimpleAdmin.SetMoney(admin, player, money); - } -} \ No newline at end of file +// using CounterStrikeSharp.API.Core; +// using CounterStrikeSharp.API.Modules.Admin; +// using CounterStrikeSharp.API.Modules.Entities; +// using CounterStrikeSharp.API.Modules.Entities.Constants; +// +// namespace CS2_SimpleAdmin.Menus; +// +// public static class FunActionsMenu +// { +// private static Dictionary? _weaponsCache; +// +// private static Dictionary GetWeaponsCache +// { +// get +// { +// if (_weaponsCache != null) return _weaponsCache; +// +// var weaponsArray = Enum.GetValues(typeof(CsItem)); +// +// // avoid duplicates in the menu +// _weaponsCache = new Dictionary(); +// foreach (CsItem item in weaponsArray) +// { +// if (item == CsItem.Tablet) +// continue; +// +// _weaponsCache[(int)item] = item; +// } +// +// return _weaponsCache; +// } +// } +// +// public static void OpenMenu(CCSPlayerController admin) +// { +// if (!admin.IsValid) +// return; +// +// var localizer = CS2_SimpleAdmin._localizer; +// if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic")) +// { +// admin.PrintToChat(localizer?["sa_prefix"] ?? +// "[SimpleAdmin] " + +// (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command") +// ); +// return; +// } +// +// var menu = AdminMenu.CreateMenu(localizer?["sa_menu_fun_commands"] ?? "Fun Commands"); +// List options = []; +// +// //var hasCheats = AdminManager.PlayerHasPermissions(admin, "@css/cheats"); +// //var hasSlay = AdminManager.PlayerHasPermissions(admin, "@css/slay"); +// +// // options added in order +// +// if (AdminManager.CommandIsOverriden("css_god") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_god")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats")) +// options.Add(new ChatMenuOptionData(localizer?["sa_godmode"] ?? "God Mode", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_godmode"] ?? "God Mode", GodMode))); +// if (AdminManager.CommandIsOverriden("css_noclip") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_noclip")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats")) +// options.Add(new ChatMenuOptionData(localizer?["sa_noclip"] ?? "No Clip", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_noclip"] ?? "No Clip", NoClip))); +// if (AdminManager.CommandIsOverriden("css_respawn") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_respawn")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats")) +// options.Add(new ChatMenuOptionData(localizer?["sa_respawn"] ?? "Respawn", () => PlayersMenu.OpenDeadMenu(admin, localizer?["sa_respawn"] ?? "Respawn", Respawn))); +// if (AdminManager.CommandIsOverriden("css_give") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_give")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats")) +// options.Add(new ChatMenuOptionData(localizer?["sa_give_weapon"] ?? "Give Weapon", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_give_weapon"] ?? "Give Weapon", GiveWeaponMenu))); +// +// if (AdminManager.CommandIsOverriden("css_strip") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_strip")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) +// options.Add(new ChatMenuOptionData(localizer?["sa_strip_weapons"] ?? "Strip Weapons", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_strip_weapons"] ?? "Strip Weapons", StripWeapons))); +// if (AdminManager.CommandIsOverriden("css_freeze") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_freeze")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) +// options.Add(new ChatMenuOptionData(localizer?["sa_freeze"] ?? "Freeze", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_freeze"] ?? "Freeze", Freeze))); +// if (AdminManager.CommandIsOverriden("css_hp") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_hp")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) +// options.Add(new ChatMenuOptionData(localizer?["sa_set_hp"] ?? "Set Hp", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_hp"] ?? "Set Hp", SetHpMenu))); +// if (AdminManager.CommandIsOverriden("css_speed") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_speed")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) +// options.Add(new ChatMenuOptionData(localizer?["sa_set_speed"] ?? "Set Speed", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_speed"] ?? "Set Speed", SetSpeedMenu))); +// if (AdminManager.CommandIsOverriden("css_gravity") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gravity")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) +// options.Add(new ChatMenuOptionData(localizer?["sa_set_gravity"] ?? "Set Gravity", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_gravity"] ?? "Set Gravity", SetGravityMenu))); +// if (AdminManager.CommandIsOverriden("css_money") +// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_money")) +// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay")) +// options.Add(new ChatMenuOptionData(localizer?["sa_set_money"] ?? "Set Money", () => PlayersMenu.OpenMenu(admin, localizer?["sa_set_money"] ?? "Set Money", SetMoneyMenu))); +// +// foreach (var menuOptionData in options) +// { +// var menuName = menuOptionData.Name; +// menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action(); }, menuOptionData.Disabled); +// } +// +// if (menu != null) AdminMenu.OpenMenu(admin, menu); +// } +// +// private static void GodMode(CCSPlayerController admin, CCSPlayerController player) +// { +// CS2_SimpleAdmin.God(admin, player); +// } +// +// private static void NoClip(CCSPlayerController admin, CCSPlayerController player) +// { +// CS2_SimpleAdmin.NoClip(admin, player); +// } +// +// private static void Respawn(CCSPlayerController? admin, CCSPlayerController player) +// { +// CS2_SimpleAdmin.Respawn(admin, player); +// } +// +// private static void GiveWeaponMenu(CCSPlayerController admin, CCSPlayerController player) +// { +// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_give_weapon"] ?? "Give Weapon"}: {player.PlayerName}"); +// +// foreach (var weapon in GetWeaponsCache) +// { +// menu?.AddMenuOption(weapon.Value.ToString(), (_, _) => { GiveWeapon(admin, player, weapon.Value); }); +// } +// +// if (menu != null) AdminMenu.OpenMenu(admin, menu); +// } +// +// private static void GiveWeapon(CCSPlayerController admin, CCSPlayerController player, CsItem weaponValue) +// { +// CS2_SimpleAdmin.GiveWeapon(admin, player, weaponValue); +// } +// +// private static void StripWeapons(CCSPlayerController admin, CCSPlayerController player) +// { +// CS2_SimpleAdmin.StripWeapons(admin, player); +// } +// +// private static void Freeze(CCSPlayerController admin, CCSPlayerController player) +// { +// if (!(player.PlayerPawn.Value?.IsValid ?? false)) +// return; +// +// if (player.PlayerPawn.Value.MoveType != MoveType_t.MOVETYPE_INVALID) +// CS2_SimpleAdmin.Freeze(admin, player, -1); +// else +// CS2_SimpleAdmin.Unfreeze(admin, player); +// } +// +// private static void SetHpMenu(CCSPlayerController admin, CCSPlayerController player) +// { +// var hpArray = new[] +// { +// new Tuple("1", 1), +// new Tuple("10", 10), +// new Tuple("25", 25), +// new Tuple("50", 50), +// new Tuple("100", 100), +// new Tuple("200", 200), +// new Tuple("500", 500), +// new Tuple("999", 999) +// }; +// +// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_hp"] ?? "Set Hp"}: {player.PlayerName}"); +// +// foreach (var (optionName, value) in hpArray) +// { +// menu?.AddMenuOption(optionName, (_, _) => { SetHp(admin, player, value); }); +// } +// +// if (menu != null) AdminMenu.OpenMenu(admin, menu); +// } +// +// private static void SetHp(CCSPlayerController admin, CCSPlayerController player, int hp) +// { +// CS2_SimpleAdmin.SetHp(admin, player, hp); +// } +// +// private static void SetSpeedMenu(CCSPlayerController admin, CCSPlayerController player) +// { +// var speedArray = new[] +// { +// new Tuple("0.1", .1f), +// new Tuple("0.25", .25f), +// new Tuple("0.5", .5f), +// new Tuple("0.75", .75f), +// new Tuple("1", 1), +// new Tuple("2", 2), +// new Tuple("3", 3), +// new Tuple("4", 4) +// }; +// +// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_speed"] ?? "Set Speed"}: {player.PlayerName}"); +// +// foreach (var (optionName, value) in speedArray) +// { +// menu?.AddMenuOption(optionName, (_, _) => { SetSpeed(admin, player, value); }); +// } +// +// if (menu != null) AdminMenu.OpenMenu(admin, menu); +// } +// +// private static void SetSpeed(CCSPlayerController admin, CCSPlayerController player, float speed) +// { +// CS2_SimpleAdmin.SetSpeed(admin, player, speed); +// } +// +// private static void SetGravityMenu(CCSPlayerController admin, CCSPlayerController player) +// { +// var gravityArray = new[] +// { +// new Tuple("0.1", .1f), +// new Tuple("0.25", .25f), +// new Tuple("0.5", .5f), +// new Tuple("0.75", .75f), +// new Tuple("1", 1), +// new Tuple("2", 2) +// }; +// +// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_gravity"] ?? "Set Gravity"}: {player.PlayerName}"); +// +// foreach (var (optionName, value) in gravityArray) +// { +// menu?.AddMenuOption(optionName, (_, _) => { SetGravity(admin, player, value); }); +// } +// +// if (menu != null) AdminMenu.OpenMenu(admin, menu); +// } +// +// private static void SetGravity(CCSPlayerController admin, CCSPlayerController player, float gravity) +// { +// CS2_SimpleAdmin.SetGravity(admin, player, gravity); +// } +// +// private static void SetMoneyMenu(CCSPlayerController admin, CCSPlayerController player) +// { +// var moneyArray = new[] +// { +// new Tuple("$0", 0), +// new Tuple("$1000", 1000), +// new Tuple("$2500", 2500), +// new Tuple("$5000", 5000), +// new Tuple("$10000", 10000), +// new Tuple("$16000", 16000) +// }; +// +// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_money"] ?? "Set Money"}: {player.PlayerName}"); +// +// foreach (var (optionName, value) in moneyArray) +// { +// menu?.AddMenuOption(optionName, (_, _) => { SetMoney(admin, player, value); }); +// } +// +// if (menu != null) AdminMenu.OpenMenu(admin, menu); +// } +// +// private static void SetMoney(CCSPlayerController admin, CCSPlayerController player, int money) +// { +// CS2_SimpleAdmin.SetMoney(admin, player, money); +// } +// } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Menus/ManageAdminsMenu.cs b/CS2-SimpleAdmin/Menus/ManageAdminsMenu.cs index 30bbf95..c820db6 100644 --- a/CS2-SimpleAdmin/Menus/ManageAdminsMenu.cs +++ b/CS2-SimpleAdmin/Menus/ManageAdminsMenu.cs @@ -8,11 +8,11 @@ public static class ManageAdminsMenu { public static void OpenMenu(CCSPlayerController admin) { - if (admin.IsValid == false) + if (!admin.IsValid) return; var localizer = CS2_SimpleAdmin._localizer; - if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root") == false) + if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root")) { admin.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " + diff --git a/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs b/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs index 4c60555..e31186a 100644 --- a/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs +++ b/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs @@ -10,11 +10,11 @@ public static class ManagePlayersMenu { public static void OpenMenu(CCSPlayerController admin) { - if (admin.IsValid == false) + if (!admin.IsValid) return; var localizer = CS2_SimpleAdmin._localizer; - if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false) + if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic")) { admin.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " + diff --git a/CS2-SimpleAdmin/Menus/ManageServerMenu.cs b/CS2-SimpleAdmin/Menus/ManageServerMenu.cs index b8cf52a..94dd1e6 100644 --- a/CS2-SimpleAdmin/Menus/ManageServerMenu.cs +++ b/CS2-SimpleAdmin/Menus/ManageServerMenu.cs @@ -8,11 +8,11 @@ public static class ManageServerMenu { public static void OpenMenu(CCSPlayerController admin) { - if (admin.IsValid == false) + if (!admin.IsValid) return; var localizer = CS2_SimpleAdmin._localizer; - if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false) + if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic")) { admin.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " + diff --git a/CS2-SimpleAdmin/Menus/MenuBuilder.cs b/CS2-SimpleAdmin/Menus/MenuBuilder.cs new file mode 100644 index 0000000..70655c3 --- /dev/null +++ b/CS2-SimpleAdmin/Menus/MenuBuilder.cs @@ -0,0 +1,170 @@ +using CounterStrikeSharp.API.Core; + +namespace CS2_SimpleAdmin.Menus; +public class MenuBuilder(string title) +{ + private readonly List _options = []; + private MenuBuilder? _parentMenu; + private Action? _backAction; + private Action? _resetAction; + + /// + /// Adds a menu option with an action. + /// + public MenuBuilder AddOption(string name, Action action, bool disabled = false, string? permission = null) + { + _options.Add(new MenuOption + { + Name = name, + Action = action, + Disabled = disabled, + Permission = permission + }); + return this; + } + + /// + /// Adds a menu option that opens a submenu. + /// + public MenuBuilder AddSubMenu(string name, Func subMenuFactory, bool disabled = false, string? permission = null) + { + _options.Add(new MenuOption + { + Name = name, + Action = player => + { + var subMenu = subMenuFactory(); + subMenu.SetParent(this); + // Automatically add back button to submenu + subMenu.WithBackButton(); + subMenu.OpenMenu(player); + }, + Disabled = disabled, + Permission = permission + }); + return this; + } + + /// + /// Adds a menu option that opens a submenu (with player parameter in factory). + /// + public MenuBuilder AddSubMenu(string name, Func subMenuFactory, bool disabled = false, string? permission = null) + { + _options.Add(new MenuOption + { + Name = name, + Action = player => + { + var subMenu = subMenuFactory(player); + subMenu.SetParent(this); + // Automatically add back button to submenu + subMenu.WithBackButton(); + subMenu.OpenMenu(player); + }, + Disabled = disabled, + Permission = permission + }); + return this; + } + + /// + /// Adds a back button to return to the previous menu. + /// + public MenuBuilder WithBackButton() + { + if (_parentMenu != null) + { + _backAction = player => _parentMenu.OpenMenu(player); + + // Add back option at the end of menu + // AddOption(backButtonText, _backAction); + } + return this; + } + + /// + /// Sets the parent menu (for navigation). + /// + private void SetParent(MenuBuilder parent) + { + _parentMenu = parent; + _backAction = player => _parentMenu.OpenMenu(player); + } + + /// + /// Opens the menu for a player. + /// + /// The player to open the menu for. + public void OpenMenu(CCSPlayerController player) + { + if (!player.IsValid) return; + + // Use MenuManager dependency + var menu = Helper.CreateMenu(title, _backAction); + if (menu == null) return; + + foreach (var option in _options) + { + // Check permissions if required + if (!string.IsNullOrEmpty(option.Permission)) + { + var steamId = new CounterStrikeSharp.API.Modules.Entities.SteamID(player.SteamID); + if (!CounterStrikeSharp.API.Modules.Admin.AdminManager.PlayerHasPermissions(steamId, option.Permission)) + { + continue; // Skip option if player doesn't have permission + } + } + + menu.AddMenuOption(option.Name, (menuPlayer, menuOption) => + { + option.Action?.Invoke(menuPlayer); + }, option.Disabled); + } + + menu.Open(player); + } + + /// + /// Clears all menu options. + /// + /// This MenuBuilder instance for chaining. + public MenuBuilder Clear() + { + _options.Clear(); + return this; + } + + /// + /// Sets a reset action for the menu. + /// + /// The action to execute on reset. + /// This MenuBuilder instance for chaining. + public MenuBuilder WithResetAction(Action resetAction) + { + _resetAction = resetAction; + return this; + } + + /// + /// Sets a custom back action for the menu. + /// + /// The action to execute when going back (nullable). + /// This MenuBuilder instance for chaining. + public MenuBuilder WithBackAction(Action? backAction) + { + _backAction = backAction; + return this; + } +} + +/// +/// Represents an option within a menu. +/// +public class MenuOption +{ + public string Name { get; set; } = string.Empty; + public Action? Action { get; set; } + public bool Disabled { get; set; } + public string? Permission { get; set; } +} + \ No newline at end of file diff --git a/CS2-SimpleAdmin/Menus/MenuManager.cs b/CS2-SimpleAdmin/Menus/MenuManager.cs new file mode 100644 index 0000000..a9c8a36 --- /dev/null +++ b/CS2-SimpleAdmin/Menus/MenuManager.cs @@ -0,0 +1,190 @@ +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Admin; +using CounterStrikeSharp.API.Modules.Entities; + +namespace CS2_SimpleAdmin.Menus; + +public class MenuManager +{ + private static MenuManager? _instance; + public static MenuManager Instance => _instance ??= new MenuManager(); + + private readonly Dictionary> _menuFactories = []; + private readonly Dictionary _menuCategories = []; + + /// + /// Provides public access to menu categories (for API usage). + /// + /// Dictionary of menu categories keyed by category ID. + public Dictionary GetMenuCategories() + { + return _menuCategories; + } + + /// + /// Registers a new menu category with specified permissions. + /// + /// Unique identifier for the category. + /// Display name of the category. + /// Required permission to access this category (default: @css/generic). + public void RegisterCategory(string categoryId, string categoryName, string permission = "@css/generic") + { + _menuCategories[categoryId] = new MenuCategory + { + Id = categoryId, + Name = categoryName, + Permission = permission, + MenuFactories = new Dictionary>() + }; + } + + /// + /// Registers a menu within a category (API for other plugins). + /// + /// The category to add this menu to. + /// Unique identifier for the menu. + /// Display name of the menu. + /// Factory function that creates the menu for a player. + /// Required permission to access this menu (optional). + public void RegisterMenu(string categoryId, string menuId, string menuName, Func menuFactory, string? permission = null) + { + if (!_menuCategories.ContainsKey(categoryId)) + { + RegisterCategory(categoryId, categoryId); // Auto-create category if it doesn't exist + } + + _menuCategories[categoryId].MenuFactories[menuId] = menuFactory; + _menuCategories[categoryId].MenuNames[menuId] = menuName; + if (permission != null) + { + _menuCategories[categoryId].MenuPermissions[menuId] = permission; + } + } + + /// + /// Unregisters a menu from a category. + /// + /// The category containing the menu. + /// The menu to unregister. + public void UnregisterMenu(string categoryId, string menuId) + { + if (!_menuCategories.TryGetValue(categoryId, out var category)) return; + category.MenuFactories.Remove(menuId); + _menuCategories[categoryId].MenuNames.Remove(menuId); + _menuCategories[categoryId].MenuPermissions.Remove(menuId); + } + + /// + /// Creates the main admin menu for a player with accessible categories. + /// + /// The player to create the menu for. + /// A MenuBuilder instance for the main menu. + public MenuBuilder CreateMainMenu(CCSPlayerController player) + { + var localizer = CS2_SimpleAdmin._localizer; + var mainMenu = new MenuBuilder(localizer?["sa_title"] ?? "SimpleAdmin"); + + foreach (var category in _menuCategories.Values) + { + if (category.MenuFactories.Count <= 0) continue; + // Check category permissions + var steamId = new SteamID(player.SteamID); + if (!AdminManager.PlayerHasPermissions(steamId, category.Permission)) + continue; + + // Pass player to CreateCategoryMenu + mainMenu.AddSubMenu(category.Name, () => CreateCategoryMenu(category, player), + permission: category.Permission); + } + + return mainMenu; + } + + /// + /// Creates a category submenu containing all registered menus in that category. + /// + /// The menu category to create. + /// The player to create the menu for. + /// A MenuBuilder instance for the category menu. + private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player) + { + var categoryMenu = new MenuBuilder(category.Name); + + foreach (var kvp in category.MenuFactories) + { + var menuId = kvp.Key; + var menuFactory = kvp.Value; + var menuName = category.MenuNames.TryGetValue(menuId, out var name) ? name : menuId; + var permission = category.MenuPermissions.TryGetValue(menuId, out var perm) ? perm : null; + + // Check permissions + if (!string.IsNullOrEmpty(permission)) + { + var steamId = new SteamID(player.SteamID); + if (!AdminManager.PlayerHasPermissions(steamId, permission)) + continue; + } + + // Call the actual factory with player parameter + categoryMenu.AddSubMenu(menuName, () => menuFactory(player), permission: permission); + } + + return categoryMenu.WithBackButton(); + } + + /// + /// Opens the main admin menu for a player. + /// + /// The player to open the menu for. + public void OpenMainMenu(CCSPlayerController player) + { + var localizer = CS2_SimpleAdmin._localizer; + + var steamId = new SteamID(player.SteamID); + if (!AdminManager.PlayerHasPermissions(steamId, "@css/generic")) + { + player.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " + + (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")); + return; + } + + CreateMainMenu(player).OpenMenu(player); + } + + /// + /// Initializes default menu categories (Players, Server, Admin). + /// + public void InitializeDefaultCategories() + { + var localizer = CS2_SimpleAdmin._localizer; + + RegisterCategory("players", localizer?["sa_menu_players_manage"] ?? "Manage Players", "@css/generic"); + RegisterCategory("server", localizer?["sa_menu_server_manage"] ?? "Server Management", "@css/generic"); + // RegisterCategory("fun", localizer?["sa_menu_fun_commands"] ?? "Fun Commands", "@css/generic"); + RegisterCategory("admin", localizer?["sa_menu_admins_manage"] ?? "Admin Management", "@css/root"); + } + + /// + /// Public method for creating category menus (for API usage). + /// + /// The menu category to create. + /// The player to create the menu for. + /// A MenuBuilder instance for the category menu. + public MenuBuilder CreateCategoryMenuPublic(MenuCategory category, CCSPlayerController player) + { + return CreateCategoryMenu(category, player); + } +} + +/// +/// Represents a menu category containing multiple menus. +/// +public class MenuCategory +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Permission { get; set; } = "@css/generic"; + public Dictionary> MenuFactories { get; set; } = []; + public Dictionary MenuNames { get; set; } = []; + public Dictionary MenuPermissions { get; set; } = []; +} diff --git a/CS2-SimpleAdmin/Menus/PlayersMenu.cs b/CS2-SimpleAdmin/Menus/PlayersMenu.cs index 19b76c2..e14270a 100644 --- a/CS2-SimpleAdmin/Menus/PlayersMenu.cs +++ b/CS2-SimpleAdmin/Menus/PlayersMenu.cs @@ -8,7 +8,7 @@ public static class PlayersMenu { public static void OpenRealPlayersMenu(CCSPlayerController admin, string menuName, Action onSelectAction, Func? enableFilter = null) { - OpenMenu(admin, menuName, onSelectAction, p => p.IsBot == false); + OpenMenu(admin, menuName, onSelectAction, p => !p.IsBot); } public static void OpenAdminPlayersMenu(CCSPlayerController admin, string menuName, Action onSelectAction, Func? enableFilter = null) @@ -37,7 +37,7 @@ public static class PlayersMenu var playerName = player != null && player.PlayerName.Length > 26 ? player.PlayerName[..26] : player?.PlayerName; var optionName = HttpUtility.HtmlEncode(playerName); - if (player != null && enableFilter != null && enableFilter(player) == false) + if (player != null && enableFilter != null && !enableFilter(player)) continue; var enabled = admin.CanTarget(player); @@ -47,7 +47,7 @@ public static class PlayersMenu { if (player != null) onSelectAction.Invoke(admin, player); }, - enabled == false); + !enabled); } if (menu != null) AdminMenu.OpenMenu(admin, menu); diff --git a/CS2-SimpleAdmin/VERSION b/CS2-SimpleAdmin/VERSION index 4973945..1109b0c 100644 --- a/CS2-SimpleAdmin/VERSION +++ b/CS2-SimpleAdmin/VERSION @@ -1 +1 @@ -1.7.7-alpha-10 \ No newline at end of file +1.7.8-beta-1 \ No newline at end of file diff --git a/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs b/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs index 3d4c9f6..9048c92 100644 --- a/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs +++ b/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs @@ -1,6 +1,7 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Capabilities; using CounterStrikeSharp.API.Modules.Commands; +using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Entities; namespace CS2_SimpleAdminApi; @@ -8,6 +9,8 @@ namespace CS2_SimpleAdminApi; public interface ICS2_SimpleAdminApi { public static readonly PluginCapability PluginCapability = new("simpleadmin:api"); + + public event Action? OnSimpleAdminReady; /// /// Gets player information associated with the specified player controller. @@ -83,6 +86,25 @@ public interface ICS2_SimpleAdminApi /// public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs); + /// + /// Shows an admin activity message with a custom translated message (for modules with their own localizer). + /// + /// Already translated message to display to players. + /// Name of the admin executing the action (optional). + /// If true, won't trigger publish events. + public void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null, bool dontPublish = false); + + /// + /// Shows an admin activity message using module's localizer for per-player language support. + /// This method sends messages in each player's configured language. + /// + /// The module's IStringLocalizer instance. + /// The translation key from the module's lang files. + /// Name of the admin executing the action (optional). + /// If true, won't trigger publish events. + /// Arguments to format the localized message. + public void ShowAdminActivityLocalized(object moduleLocalizer, string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs); + /// /// Returns true if the specified admin player is in silent mode (not broadcasting activity). /// @@ -102,4 +124,54 @@ public interface ICS2_SimpleAdminApi /// Unregisters an existing command by its name. /// public void UnRegisterCommand(string name); + + /// + /// Gets target players from command + /// + TargetResult? GetTarget(CommandInfo command); + + /// + /// Returns the list of current valid players, available to call from other plugins. + /// + List GetValidPlayers(); + + /// + /// Registers a menu category. + /// + void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic"); + + /// + /// Registers a menu in a category. + /// + void RegisterMenu(string categoryId, string menuId, string menuName, Func menuFactory, string? permission = null); + + /// + /// Unregisters a menu from a category. + /// + void UnregisterMenu(string categoryId, string menuId); + + /// + /// Creates a menu with an automatic back button. + /// + object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player); + + /// + /// Creates a menu with a list of players with filter and action. + /// + object CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin, Func filter, Action onSelect); + + /// + /// Adds an option to the menu (extension method helper). + /// + void AddMenuOption(object menu, string name, Action action, bool disabled = false, string? permission = null); + + /// + /// Adds a submenu to the menu (extension method helper). + /// + void AddSubMenu(object menu, string name, Func subMenuFactory, bool disabled = false, string? permission = null); + + /// + /// Opens a menu for a player. + /// + void OpenMenu(object menu, CCSPlayerController player); } \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll new file mode 100644 index 0000000..45fa756 Binary files /dev/null and b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.sln b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.sln new file mode 100644 index 0000000..c2fa528 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CS2-SimpleAdmin_FunCommands", "CS2-SimpleAdmin_FunCommands\CS2-SimpleAdmin_FunCommands.csproj", "{72713A40-688F-401F-8211-3D28B068C791}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {72713A40-688F-401F-8211-3D28B068C791}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72713A40-688F-401F-8211-3D28B068C791}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72713A40-688F-401F-8211-3D28B068C791}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72713A40-688F-401F-8211-3D28B068C791}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Actions.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Actions.cs new file mode 100644 index 0000000..e6521e9 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Actions.cs @@ -0,0 +1,237 @@ +using System.Globalization; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using Microsoft.Extensions.Logging; + +namespace CS2_SimpleAdmin_FunCommands; + +public partial class CS2_SimpleAdmin_FunCommands +{ + private void God(CCSPlayerController? caller, CCSPlayerController player) + { + if (!caller.CanTarget(player)) return; + + var callerName = caller?.PlayerName ?? "Console"; + + // Toggle god mode for the player (like in main plugin) + if (!GodPlayers.Add(player.Slot)) + { + GodPlayers.Remove(player.Slot); + } + + // Show admin activity using module's own localizer with per-player language support + var activityArgs = new object[] { "CALLER", player.PlayerName }; + if (caller == null || !_sharedApi!.IsAdminSilent(caller)) + { + if (Localizer != null) + { + _sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_god_message", callerName, false, + activityArgs); + } + else + { + _sharedApi!.ShowAdminActivity("fun_admin_god_message", callerName, false, activityArgs); + } + } + + // Log command using API + _sharedApi!.LogCommand(caller, + $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); + } + + private void NoClip(CCSPlayerController? caller, CCSPlayerController player) + { + if (!player.IsValid) return; + if (!caller.CanTarget(player)) return; + + var callerName = caller?.PlayerName ?? "Console"; + + // Toggle no-clip mode using PlayerExtensions + player.Pawn.Value?.ToggleNoclip(); + + // Show admin activity using module's own localizer with per-player language support + var activityArgs = new object[] { "CALLER", player.PlayerName }; + if (caller == null || !_sharedApi!.IsAdminSilent(caller)) + { + if (Localizer != null) + { + _sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_noclip_message", callerName, false, + activityArgs); + } + else + { + _sharedApi!.ShowAdminActivity("fun_admin_noclip_message", callerName, false, activityArgs); + } + } + + // Log command using API + _sharedApi!.LogCommand(caller, + $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); + } + + private void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time) + { + if (!player.IsValid) return; + if (!caller.CanTarget(player)) return; + + var callerName = caller?.PlayerName ?? "Console"; + + // Freeze player using PlayerExtensions + player.Pawn.Value?.Freeze(); + + // Show admin activity using module's own localizer with per-player language support + var activityArgs = new object[] { "CALLER", player.PlayerName }; + if (caller == null || !_sharedApi!.IsAdminSilent(caller)) + { + if (Localizer != null) + { + _sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_freeze_message", callerName, false, + activityArgs); + } + else + { + _sharedApi!.ShowAdminActivity("fun_admin_freeze_message", callerName, false, activityArgs); + } + } + + // Auto-unfreeze after duration + if (time > 0) + { + AddTimer(time, () => player.Pawn.Value?.Unfreeze(), + CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + } + + // Log command using API + _sharedApi!.LogCommand(caller, + $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}"); + } + + private void Unfreeze(CCSPlayerController? caller, CCSPlayerController player) + { + if (!player.IsValid) return; + if (!caller.CanTarget(player)) return; + + var callerName = caller?.PlayerName ?? "Console"; + + // Unfreeze player using PlayerExtensions + player.Pawn.Value?.Unfreeze(); + + // Show admin activity using module's own localizer with per-player language support + var activityArgs = new object[] { "CALLER", player.PlayerName }; + if (caller == null || !_sharedApi!.IsAdminSilent(caller)) + { + if (Localizer != null) + { + _sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_unfreeze_message", callerName, false, + activityArgs); + } + else + { + _sharedApi!.ShowAdminActivity("fun_admin_unfreeze_message", callerName, false, activityArgs); + } + } + + // Log command using API + _sharedApi!.LogCommand(caller, + $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); + } + + /// + /// Respawns a player and teleports them to their last death position if available. + /// This demonstrates using the GetPlayerInfo API to access player data. + /// + private void Respawn(CCSPlayerController? caller, CCSPlayerController player) + { + if (!player.IsValid) return; + if (!caller.CanTarget(player)) return; + + var callerName = caller?.PlayerName ?? "Console"; + + // Respawn the player + player.Respawn(); + + // Get death position from API and teleport player to it + // BEST PRACTICE: Use API to access player data like death position + if (_sharedApi != null && player.UserId.HasValue) + { + try + { + var playerInfo = _sharedApi.GetPlayerInfo(player); + + // Teleport to death position if available + if (playerInfo?.DiePosition != null && player.PlayerPawn?.Value != null) + { + player.PlayerPawn.Value.Teleport( + playerInfo.DiePosition.Position, + playerInfo.DiePosition.Angle); + } + } + catch (Exception ex) + { + Logger.LogWarning($"Failed to get player info for respawn: {ex.Message}"); + } + } + + // Show admin activity using module's own localizer with per-player language support + var activityArgs = new object[] { "CALLER", player.PlayerName }; + if (caller == null || !_sharedApi!.IsAdminSilent(caller)) + { + if (Localizer != null) + { + _sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_respawn_message", callerName, false, + activityArgs); + } + else + { + _sharedApi!.ShowAdminActivity("fun_admin_respawn_message", callerName, false, activityArgs); + } + } + + // Log command using API + _sharedApi!.LogCommand(caller, + $"css_respawn {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); + } + + /// + /// Resizes a player's model to the specified scale. + /// + private void Resize(CCSPlayerController? caller, CCSPlayerController player, float size) + { + if (!player.IsValid) return; + if (!caller.CanTarget(player)) return; + + var callerName = caller?.PlayerName ?? "Console"; + + // Resize the player + var sceneNode = player.PlayerPawn.Value!.CBodyComponent?.SceneNode; + if (sceneNode != null) + { + sceneNode.GetSkeletonInstance().Scale = size; + player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture)); + + Server.NextWorldUpdate(() => + { + Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent"); + }); + } + + // Show admin activity using module's own localizer with per-player language support + var activityArgs = new object[] { "CALLER", player.PlayerName, size.ToString(CultureInfo.InvariantCulture) }; + if (caller == null || !_sharedApi!.IsAdminSilent(caller)) + { + if (Localizer != null) + { + _sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_resize_message", callerName, false, + activityArgs); + } + else + { + _sharedApi!.ShowAdminActivity("fun_admin_resize_message", callerName, false, activityArgs); + } + } + + // Log command using API + _sharedApi!.LogCommand(caller, + $"css_resize {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {size}"); + } +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs new file mode 100644 index 0000000..d7d6238 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs @@ -0,0 +1,492 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Capabilities; +using CounterStrikeSharp.API.Modules.Admin; +using CounterStrikeSharp.API.Modules.Commands; +using CounterStrikeSharp.API.Modules.Entities; +using CounterStrikeSharp.API.Modules.Entities.Constants; +using CounterStrikeSharp.API.Modules.Utils; +using CS2_SimpleAdminApi; +using Microsoft.Extensions.Logging; +using System.Globalization; + +namespace CS2_SimpleAdmin_FunCommands; + +/// +/// CS2-SimpleAdmin Fun Commands Module +/// +/// This module serves as a REFERENCE IMPLEMENTATION for creating CS2-SimpleAdmin modules. +/// Study this code to learn best practices for: +/// - Command registration from configuration +/// - Menu creation with SimpleAdmin API +/// - Per-player translation support +/// - Proper cleanup on module unload +/// - Code organization using partial classes +/// +/// File Structure: +/// - CS2-SimpleAdmin_FunCommands.cs (this file) - Plugin initialization and registration +/// - Commands.cs - Command handlers +/// - Actions.cs - Action methods (God, NoClip, Freeze, etc.) +/// - Menus.cs - Menu creation +/// - Config.cs - Configuration with command lists +/// - lang/ - Translation files (13 languages) +/// +/// See README.md for detailed explanation of all patterns demonstrated here. +/// +public partial class CS2_SimpleAdmin_FunCommands : BasePlugin, IPluginConfig +{ + public Config Config { get; set; } + + /// + /// BEST PRACTICE: Cache expensive operations + /// Weapons enum values don't change, so we cache them on first access + /// + private static Dictionary? _weaponsCache; + private static Dictionary GetWeaponsCache() + { + if (_weaponsCache != null) return _weaponsCache; + + var weaponsArray = Enum.GetValues(typeof(CsItem)); + _weaponsCache = new Dictionary(); + + foreach (CsItem item in weaponsArray) + { + if (item == CsItem.Tablet) continue; // Skip tablet (invalid weapon) + _weaponsCache[(int)item] = item; + } + + return _weaponsCache; + } + + /// + /// Track players with god mode enabled + /// HashSet for O(1) lookup performance + /// + private static readonly HashSet GodPlayers = []; + + /// + /// Track players with modified speed + /// Dictionary for storing speed values per player + /// + private static readonly Dictionary SpeedPlayers = []; + + /// + /// Track players with modified gravity + /// Dictionary for storing gravity values per player + /// + private static readonly Dictionary GravityPlayers = []; + + /// + /// BEST PRACTICE: Use capability system to get SimpleAdmin API + /// This ensures your module works even if SimpleAdmin loads after your module + /// + private ICS2_SimpleAdminApi? _sharedApi; + private readonly PluginCapability _pluginCapability = new("simpleadmin:api"); + + /// + /// BEST PRACTICE: Track menu registration state to prevent duplicate registrations + /// + private bool _menusRegistered = false; + + // Plugin metadata + public override string ModuleName => "CS2-SimpleAdmin Fun Commands"; + public override string ModuleVersion => "1.0.0"; + public override string ModuleAuthor => "Your Name"; + public override string ModuleDescription => "Fun commands extension for CS2-SimpleAdmin"; + + /// + /// BEST PRACTICE: Initialize plugin after all plugins are loaded + /// This ensures SimpleAdmin API is available + /// + public override void OnAllPluginsLoaded(bool hotReload) + { + // STEP 1: Get SimpleAdmin API using capability system + _sharedApi = _pluginCapability.Get(); + if (_sharedApi == null) + { + Logger.LogError("CS2-SimpleAdmin API not found - make sure CS2-SimpleAdmin is loaded!"); + Unload(false); + return; + } + + // STEP 2: Register commands (can be done immediately) + RegisterFunCommands(); + + // STEP 3: Register menus (wait for SimpleAdmin to be ready) + // BEST PRACTICE: Use event + fallback to handle both normal load and hot reload + _sharedApi.OnSimpleAdminReady += RegisterFunMenus; + RegisterFunMenus(); // Fallback for hot reload case + + // STEP 4: Start timer to maintain speed and gravity modifications + StartSpeedGravityTimer(); + } + + public override void Unload(bool hotReload) + { + if (_sharedApi == null) return; + + // Unregister commands + if (Config.NoclipCommands.Count > 0) + { + foreach (var command in Config.NoclipCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.GodCommands.Count > 0) + { + foreach (var command in Config.GodCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.FreezeCommands.Count > 0) + { + foreach (var command in Config.FreezeCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.UnfreezeCommands.Count > 0) + { + foreach (var command in Config.UnfreezeCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.RespawnCommands.Count > 0) + { + foreach (var command in Config.RespawnCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.GiveCommands.Count > 0) + { + foreach (var command in Config.GiveCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.StripCommands.Count > 0) + { + foreach (var command in Config.StripCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.HpCommands.Count > 0) + { + foreach (var command in Config.HpCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.SpeedCommands.Count > 0) + { + foreach (var command in Config.SpeedCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.GravityCommands.Count > 0) + { + foreach (var command in Config.GravityCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.MoneyCommands.Count > 0) + { + foreach (var command in Config.MoneyCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + if (Config.ResizeCommands.Count > 0) + { + foreach (var command in Config.ResizeCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + // Unregister menus + if (Config.NoclipCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "noclip"); + + if (Config.GodCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "god"); + + if (Config.RespawnCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "respawn"); + + if (Config.GiveCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "give"); + + if (Config.StripCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "strip"); + + if (Config.FreezeCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "freeze"); + + if (Config.HpCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "hp"); + + if (Config.SpeedCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "speed"); + + if (Config.GravityCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "gravity"); + + if (Config.MoneyCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "money"); + + if (Config.ResizeCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "resize"); + + _sharedApi.OnSimpleAdminReady -= RegisterFunMenus; + } + + private void RegisterFunCommands() + { + if (_sharedApi == null) return; + + if (Config.NoclipCommands.Count > 0) + { + foreach (var command in Config.NoclipCommands) + { + _sharedApi.RegisterCommand(command, "Enable noclip", OnNoclipCommand); + } + } + + if (Config.GodCommands.Count > 0) + { + foreach (var command in Config.GodCommands) + { + _sharedApi.RegisterCommand(command, "Enable god mode", OnGodCommand); + } + } + + if (Config.FreezeCommands.Count > 0) + { + foreach (var command in Config.FreezeCommands) + { + _sharedApi.RegisterCommand(command, "Freeze player", OnFreezeCommand); + } + } + + if (Config.UnfreezeCommands.Count > 0) + { + foreach (var command in Config.UnfreezeCommands) + { + _sharedApi.RegisterCommand(command, "Unfreeze player", OnUnfreezeCommand); + } + } + + if (Config.RespawnCommands.Count > 0) + { + foreach (var command in Config.RespawnCommands) + { + _sharedApi.RegisterCommand(command, "Respawn player", OnRespawnCommand); + } + } + + if (Config.GiveCommands.Count > 0) + { + foreach (var command in Config.GiveCommands) + { + _sharedApi.RegisterCommand(command, "Give weapon", OnGiveWeaponCommand); + } + } + + if (Config.StripCommands.Count > 0) + { + foreach (var command in Config.StripCommands) + { + _sharedApi.RegisterCommand(command, "Strip weapons", OnStripWeaponsCommand); + } + } + + if (Config.HpCommands.Count > 0) + { + foreach (var command in Config.HpCommands) + { + _sharedApi.RegisterCommand(command, "Set HP", OnSetHpCommand); + } + } + + if (Config.SpeedCommands.Count > 0) + { + foreach (var command in Config.SpeedCommands) + { + _sharedApi.RegisterCommand(command, "Set speed", OnSetSpeedCommand); + } + } + + if (Config.GravityCommands.Count > 0) + { + foreach (var command in Config.GravityCommands) + { + _sharedApi.RegisterCommand(command, "Set gravity", OnSetGravityCommand); + } + } + + if (Config.MoneyCommands.Count > 0) + { + foreach (var command in Config.MoneyCommands) + { + _sharedApi.RegisterCommand(command, "Set money", OnSetMoneyCommand); + } + } + + if (Config.ResizeCommands.Count > 0) + { + foreach (var command in Config.ResizeCommands) + { + _sharedApi.RegisterCommand(command, "Resize player", OnSetResizeCommand); + } + } + } + + private void RegisterFunMenus() + { + if (_sharedApi == null || _menusRegistered) return; + + try + { + _sharedApi.RegisterMenuCategory("fun", Localizer?["fun_category_name"] ?? "Fun Commands", "@css/generic"); + + if (Config.GodCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "god", + Localizer?["fun_menu_god"] ?? "God Mode", + CreateGodModeMenu, "@css/cheats"); + + if (Config.NoclipCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "noclip", + Localizer?["fun_menu_noclip"] ?? "No Clip", + CreateNoClipMenu, "@css/cheats"); + + if (Config.RespawnCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "respawn", + Localizer?["fun_menu_respawn"] ?? "Respawn", + CreateRespawnMenu, "@css/cheats"); + + if (Config.GiveCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "give", + Localizer?["fun_menu_give"] ?? "Give Weapon", + CreateGiveWeaponMenu, "@css/cheats"); + + if (Config.StripCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "strip", + Localizer?["fun_menu_strip"] ?? "Strip Weapons", + CreateStripWeaponsMenu, "@css/slay"); + + if (Config.FreezeCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "freeze", + Localizer?["fun_menu_freeze"] ?? "Freeze", + CreateFreezeMenu, "@css/slay"); + + if (Config.HpCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "hp", + Localizer?["fun_menu_hp"] ?? "Set HP", + CreateSetHpMenu, "@css/slay"); + + if (Config.SpeedCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "speed", + Localizer?["fun_menu_speed"] ?? "Set Speed", + CreateSetSpeedMenu, "@css/slay"); + + if (Config.GravityCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "gravity", + Localizer?["fun_menu_gravity"] ?? "Set Gravity", + CreateSetGravityMenu, "@css/slay"); + + if (Config.MoneyCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "money", + Localizer?["fun_menu_money"] ?? "Set Money", + CreateSetMoneyMenu, "@css/slay"); + + if (Config.ResizeCommands.Count > 0) + _sharedApi.RegisterMenu("fun", "resize", + Localizer?["fun_menu_resize"] ?? "Resize Player", + CreateSetResizeMenu, "@css/slay"); + + _menusRegistered = true; + Logger.LogInformation("Fun menus registered successfully!"); + } + catch (Exception ex) + { + Logger.LogError($"Failed to register Fun menus: {ex.Message}"); + } + } + + public void OnConfigParsed(Config config) + { + Config = config; + } + + /// + /// Starts a repeating timer to maintain speed and gravity modifications for players. + /// This ensures that speed/gravity changes persist even after respawns or round changes. + /// + private void StartSpeedGravityTimer() + { + AddTimer(0.12f, () => + { + // Early exit if no players have modified speed or gravity + var hasSpeedPlayers = SpeedPlayers.Count > 0; + var hasGravityPlayers = GravityPlayers.Count > 0; + + if (!hasSpeedPlayers && !hasGravityPlayers) + return; + + if (hasSpeedPlayers) + { + // Iterate through players with modified speed + foreach (var kvp in SpeedPlayers) + { + var player = kvp.Key; + // Early validation check - avoid accessing PlayerPawn if player is invalid + if (player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected) + { + var pawn = player.PlayerPawn?.Value; + if (pawn != null && pawn.LifeState == (int)LifeState_t.LIFE_ALIVE) + { + player.SetSpeed(kvp.Value); + } + } + } + } + + if (hasGravityPlayers) + { + // Iterate through players with modified gravity + foreach (var kvp in GravityPlayers) + { + var player = kvp.Key; + if (player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected) + { + var pawn = player.PlayerPawn?.Value; + if (pawn != null && pawn.LifeState == (int)LifeState_t.LIFE_ALIVE) + { + player.SetGravity(kvp.Value); + } + } + } + } + }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.REPEAT); + } + +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.csproj b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.csproj new file mode 100644 index 0000000..3990512 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + CS2_SimpleAdmin_FunCommands + enable + enable + + + + + ..\CS2-SimpleAdminApi.dll + + + + + + + + + + + + diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Commands.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Commands.cs new file mode 100644 index 0000000..47778e5 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Commands.cs @@ -0,0 +1,320 @@ +using System.Globalization; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Admin; +using CounterStrikeSharp.API.Modules.Commands; +using CounterStrikeSharp.API.Modules.Entities.Constants; + +namespace CS2_SimpleAdmin_FunCommands; + +public partial class CS2_SimpleAdmin_FunCommands +{ + // ================================= + // COMMAND HANDLERS + // ================================= + + [CommandHelper(1, "<#userid or name>")] + [RequiresPermissions("@css/cheats")] + private void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + NoClip(caller, player); + } + }); + } + + [RequiresPermissions("@css/cheats")] + [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] + private void OnGodCommand(CCSPlayerController? caller, CommandInfo command) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + God(caller, player); + } + }); + } + + [CommandHelper(1, "<#userid or name> [duration]")] + [RequiresPermissions("@css/slay")] + private void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command) + { + int.TryParse(command.GetArg(2), out var time); + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + Freeze(caller, player, time); + } + }); + } + + [CommandHelper(1, "<#userid or name>")] + [RequiresPermissions("@css/slay")] + private void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + Unfreeze(caller, player); + } + }); + } + + [CommandHelper(1, "<#userid or name>")] + [RequiresPermissions("@css/cheats")] + private void OnRespawnCommand(CCSPlayerController? caller, CommandInfo command) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + Respawn(caller, player); + } + }); + } + + [CommandHelper(2, "<#userid or name> ")] + [RequiresPermissions("@css/cheats")] + private void OnGiveWeaponCommand(CCSPlayerController? caller, CommandInfo command) + { + var weaponName = command.GetArg(2); + if (Enum.TryParse(weaponName, true, out CsItem weapon)) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + player.GiveNamedItem(weapon); + LogAndShowActivity(caller, player, "fun_admin_give_message", "css_give", weapon.ToString()); + } + }); + } + } + + [CommandHelper(1, "<#userid or name>")] + [RequiresPermissions("@css/slay")] + private void OnStripWeaponsCommand(CCSPlayerController? caller, CommandInfo command) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + player.RemoveWeapons(); + LogAndShowActivity(caller, player, "fun_admin_strip_message", "css_strip"); + } + }); + } + + [CommandHelper(2, "<#userid or name> ")] + [RequiresPermissions("@css/slay")] + private void OnSetHpCommand(CCSPlayerController? caller, CommandInfo command) + { + if (int.TryParse(command.GetArg(2), out var hp)) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + player.SetHp(hp); + LogAndShowActivity(caller, player, "fun_admin_hp_message", "css_hp", hp.ToString()); + } + }); + } + } + + [CommandHelper(2, "<#userid or name> ")] + [RequiresPermissions("@css/slay")] + private void OnSetSpeedCommand(CCSPlayerController? caller, CommandInfo command) + { + if (float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var speed)) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + player.SetSpeed(speed); + + // Track speed modification for timer + if (speed == 1f) + SpeedPlayers.Remove(player); + else + SpeedPlayers[player] = speed; + + LogAndShowActivity(caller, player, "fun_admin_speed_message", "css_speed", speed.ToString(CultureInfo.InvariantCulture)); + } + }); + } + } + + [CommandHelper(2, "<#userid or name> ")] + [RequiresPermissions("@css/slay")] + private void OnSetGravityCommand(CCSPlayerController? caller, CommandInfo command) + { + if (float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var gravity)) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + player.SetGravity(gravity); + + // Track gravity modification for timer + if (gravity == 1f) + GravityPlayers.Remove(player); + else + GravityPlayers[player] = gravity; + + LogAndShowActivity(caller, player, "fun_admin_gravity_message", "css_gravity", gravity.ToString(CultureInfo.InvariantCulture)); + } + }); + } + } + + [CommandHelper(2, "<#userid or name> ")] + [RequiresPermissions("@css/slay")] + private void OnSetMoneyCommand(CCSPlayerController? caller, CommandInfo command) + { + if (int.TryParse(command.GetArg(2), out var money)) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + player.SetMoney(money); + LogAndShowActivity(caller, player, "fun_admin_money_message", "css_money", money.ToString()); + } + }); + } + } + + [CommandHelper(2, "<#userid or name> ")] + [RequiresPermissions("@css/slay")] + private void OnSetResizeCommand(CCSPlayerController? caller, CommandInfo command) + { + if (float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size)) + { + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + Resize(caller, player, size); + } + }); + } + } + + // ================================= + // HELPER METHOD FOR ACTIVITIES WITH INDIVIDUAL COMMAND LOGGING + // ================================= + + private void LogAndShowActivity(CCSPlayerController? caller, CCSPlayerController target, string messageKey, string baseCommand, params string[] extraArgs) + { + var callerName = caller?.PlayerName ?? "Console"; + + // Build activity args + var args = new List { "CALLER", target.PlayerName }; + args.AddRange(extraArgs); + + // Show admin activity using module's own localizer with per-player language support + if (caller == null || !_sharedApi!.IsAdminSilent(caller)) + { + // Use module's own translations with automatic per-player language support + if (Localizer != null) + { + // This will send the message in each player's configured language + _sharedApi!.ShowAdminActivityLocalized(Localizer, messageKey, callerName, false, args.ToArray()); + } + else + { + // Fallback to old method if localizer is not available + _sharedApi!.ShowAdminActivity(messageKey, callerName, false, args.ToArray()); + } + } + + // Build and log command using API string method + var logCommand = $"{baseCommand} {(string.IsNullOrEmpty(target.PlayerName) ? target.SteamID.ToString() : target.PlayerName)}"; + if (extraArgs.Length > 0) + { + logCommand += $" {string.Join(" ", extraArgs)}"; + } + + _sharedApi!.LogCommand(caller, logCommand); + } +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Config.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Config.cs new file mode 100644 index 0000000..effcd88 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Config.cs @@ -0,0 +1,21 @@ +using CounterStrikeSharp.API.Core; + +namespace CS2_SimpleAdmin_FunCommands; + +public class Config : IBasePluginConfig +{ + public int Version { get; set; } = 1; + + public List NoclipCommands { get; set; } = ["css_noclip"]; + public List GodCommands { get; set; } = ["css_god"]; + public List FreezeCommands { get; set; } = ["css_freeze"]; + public List UnfreezeCommands { get; set; } = ["css_unfreeze"]; + public List RespawnCommands { get; set; } = ["css_respawn"]; + public List GiveCommands { get; set; } = ["css_give"]; + public List StripCommands { get; set; } = ["css_strip"]; + public List HpCommands { get; set; } = ["css_hp"]; + public List SpeedCommands { get; set; } = ["css_speed"]; + public List GravityCommands { get; set; } = ["css_gravity"]; + public List MoneyCommands { get; set; } = ["css_money"]; + public List ResizeCommands { get; set; } = ["css_resize"]; +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Events.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Events.cs new file mode 100644 index 0000000..8e2586e --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Events.cs @@ -0,0 +1,69 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Attributes.Registration; + +namespace CS2_SimpleAdmin_FunCommands; + +public partial class CS2_SimpleAdmin_FunCommands +{ + [GameEventHandler] + public HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info) + { + var player = @event.Userid; + if (player == null || !player.IsValid) return HookResult.Continue; + + // Check if player has god mode (similar to main plugin) + if (!GodPlayers.Contains(player.Slot)) return HookResult.Continue; + + // Cancel damage + @event.DmgHealth = 0; + @event.DmgArmor = 0; + + // Reset health to full + if (player.PlayerPawn?.Value == null) return HookResult.Continue; + + player.PlayerPawn.Value.Health = player.PlayerPawn.Value.MaxHealth; + Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_iHealth"); + + return HookResult.Continue; + } + + [GameEventHandler] + public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info) + { + var player = @event.Userid; + if (player == null || !player.IsValid) return HookResult.Continue; + + // Remove player from god mode, speed, and gravity tracking on death + GodPlayers.Remove(player.Slot); + SpeedPlayers.Remove(player); + GravityPlayers.Remove(player); + + return HookResult.Continue; + } + + [GameEventHandler] + public HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) + { + // Clear all fun command modifications at round start + GodPlayers.Clear(); + SpeedPlayers.Clear(); + GravityPlayers.Clear(); + + return HookResult.Continue; + } + + [GameEventHandler] + public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info) + { + var player = @event.Userid; + if (player == null || !player.IsValid) return HookResult.Continue; + + // Clean up player from all tracking when they disconnect + GodPlayers.Remove(player.Slot); + SpeedPlayers.Remove(player); + GravityPlayers.Remove(player); + + return HookResult.Continue; + } +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs new file mode 100644 index 0000000..b479fe8 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs @@ -0,0 +1,391 @@ +using System.Globalization; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; + +namespace CS2_SimpleAdmin_FunCommands; + +/// +/// Menu creation methods for Fun Commands module. +/// This file demonstrates different menu patterns using SimpleAdmin API. +/// +public partial class CS2_SimpleAdmin_FunCommands +{ + // ================================= + // SIMPLE PLAYER SELECTION MENUS + // ================================= + // Pattern: Direct player selection with immediate action + // Use CreateMenuWithPlayers when you just need to select a player and execute an action + + /// + /// Creates a simple player selection menu for god mode. + /// PATTERN: CreateMenuWithPlayers with method reference + /// + private object CreateGodModeMenu(CCSPlayerController admin) + { + return _sharedApi!.CreateMenuWithPlayers( + Localizer?["fun_menu_god"] ?? "God Mode", // Menu title from translation + "fun", // Category ID (for back button navigation) + admin, // Admin opening the menu + player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player), // Filter: only alive, targetable players + God); // Action to execute (method reference) + } + + private object CreateNoClipMenu(CCSPlayerController admin) + { + return _sharedApi!.CreateMenuWithPlayers( + Localizer?["fun_menu_noclip"] ?? "No Clip", + "fun", + admin, + player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player), + NoClip); + } + + /// + /// Creates a player selection menu for respawn command. + /// PATTERN: CreateMenuWithPlayers with method reference + /// + private object CreateRespawnMenu(CCSPlayerController admin) + { + return _sharedApi!.CreateMenuWithPlayers( + Localizer?["fun_menu_respawn"] ?? "Respawn", // Menu title from translation + "fun", // Category ID + admin, // Admin + admin.CanTarget, // Filter: only targetable players (no LifeState check - can respawn dead players) + Respawn); // Use the Respawn method which includes death position teleport + } + + // ================================= + // NESTED MENUS - PLAYER → VALUE SELECTION + // ================================= + // Pattern: First select player, then select a value/option for that player + // Use CreateMenuWithBack + AddSubMenu for multi-level menus + + /// + /// Creates a nested menu: Player selection → Weapon selection. + /// PATTERN: CreateMenuWithBack + foreach + AddSubMenu + /// + private object CreateGiveWeaponMenu(CCSPlayerController admin) + { + var menu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_give"] ?? "Give Weapon", + "fun", + admin); + var players = _sharedApi.GetValidPlayers().Where(p => + p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + + // AddSubMenu automatically adds a "Back" button to the submenu + // The lambda receives 'p' but we use captured 'player' variable (closure) + _sharedApi.AddSubMenu(menu, playerName, p => CreateWeaponSelectionMenu(admin, player)); + } + + return menu; + } + + /// + /// Creates weapon selection submenu for a specific player. + /// PATTERN: CreateMenuWithBack + foreach + AddMenuOption + /// + private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target) + { + var weaponMenu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_give_player", target.PlayerName] ?? $"Give Weapon: {target.PlayerName}", + "fun", + admin); + + // Loop through cached weapons (performance optimization) + foreach (var weapon in GetWeaponsCache()) + { + // AddMenuOption for each selectable option + // IMPORTANT: Always validate target.IsValid before executing action + _sharedApi.AddMenuOption(weaponMenu, weapon.Value.ToString(), _ => + { + if (target.IsValid) // Player might disconnect before selection + { + target.GiveNamedItem(weapon.Value); + LogAndShowActivity(admin, target, "fun_admin_give_message", $"css_give", weapon.Value.ToString()); + } + }); + } + + return weaponMenu; + } + + private object CreateStripWeaponsMenu(CCSPlayerController admin) + { + return _sharedApi!.CreateMenuWithPlayers( + Localizer?["fun_menu_strip"] ?? "Strip Weapons", + "fun", + admin, + player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player), + (adminPlayer, targetPlayer) => + { + targetPlayer.RemoveWeapons(); + LogAndShowActivity(adminPlayer, targetPlayer, "fun_admin_strip_message", "css_strip"); + }); + } + + private object CreateFreezeMenu(CCSPlayerController admin) + { + return _sharedApi!.CreateMenuWithPlayers( + Localizer?["fun_menu_freeze"] ?? "Freeze", + "fun", + admin, + player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player), + (adminPlayer, targetPlayer) => { Freeze(adminPlayer, targetPlayer, -1); }); + } + + /// + /// Creates a nested menu for setting player HP with predefined values. + /// PATTERN: Same as Give Weapon (player selection → value selection) + /// This is a common pattern you'll use frequently! + /// + private object CreateSetHpMenu(CCSPlayerController admin) + { + var menu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_hp"] ?? "Set HP", + "fun", + admin); + var players = _sharedApi.GetValidPlayers().Where(p => + p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + _sharedApi.AddSubMenu(menu, playerName, p => CreateHpSelectionMenu(admin, player)); + } + + return menu; + } + + /// + /// Creates HP value selection submenu. + /// TIP: Use arrays for predefined values - easy to modify and maintain + /// + private object CreateHpSelectionMenu(CCSPlayerController admin, CCSPlayerController target) + { + var hpSelectionMenu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_hp_player", target.PlayerName] ?? $"Set HP: {target.PlayerName}", + "fun", + admin); + + // Predefined HP values - easy to customize + var hpValues = new[] { 1, 10, 25, 50, 100, 200, 500, 999 }; + + foreach (var hp in hpValues) + { + _sharedApi.AddMenuOption(hpSelectionMenu, + Localizer?["fun_menu_hp_value", hp] ?? $"{hp} HP", + _ => + { + if (target.IsValid) + { + target.SetHp(hp); + LogAndShowActivity(admin, target, "fun_admin_hp_message", "css_hp", hp.ToString()); + } + }); + } + + return hpSelectionMenu; + } + + private object CreateSetSpeedMenu(CCSPlayerController admin) + { + var menu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_speed"] ?? "Set Speed", + "fun", + admin); + var players = _sharedApi.GetValidPlayers().Where(p => + p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + _sharedApi.AddSubMenu(menu, playerName, p => CreateSpeedSelectionMenu(admin, player)); + } + + return menu; + } + + /// + /// Creates speed value selection submenu. + /// TIP: Use tuples (value, display) when you need different internal value vs display text + /// Example: (0.5f, "0.5") - float value for code, string for display + /// + private object CreateSpeedSelectionMenu(CCSPlayerController admin, CCSPlayerController target) + { + var speedSelectionMenu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_speed_player", target.PlayerName] ?? $"Set Speed: {target.PlayerName}", + "fun", + admin); + + // Tuple pattern: (actualValue, displayText) + // Useful when display text differs from actual value + var speedValues = new[] + { + (0.1f, "0.1"), (0.25f, "0.25"), (0.5f, "0.5"), (0.75f, "0.75"), (1f, "1"), (2f, "2"), (3f, "3"), (4f, "4") + }; + + foreach (var (speed, display) in speedValues) + { + _sharedApi.AddMenuOption(speedSelectionMenu, + Localizer?["fun_menu_speed_value", display] ?? $"Speed {display}", + _ => + { + if (target.IsValid) + { + target.SetSpeed(speed); + + // Track speed modification for timer + if (speed == 1f) + SpeedPlayers.Remove(target); + else + SpeedPlayers[target] = speed; + + LogAndShowActivity(admin, target, "fun_admin_speed_message", "css_speed", speed.ToString(CultureInfo.InvariantCulture)); + } + }); + } + + return speedSelectionMenu; + } + + private object CreateSetGravityMenu(CCSPlayerController admin) + { + var menu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_gravity"] ?? "Set Gravity", + "fun", + admin); + var players = _sharedApi.GetValidPlayers().Where(p => + p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + _sharedApi.AddSubMenu(menu, playerName, p => CreateGravitySelectionMenu(admin, player)); + } + + return menu; + } + + private object CreateGravitySelectionMenu(CCSPlayerController admin, CCSPlayerController target) + { + var gravitySelectionMenu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_gravity_player", target.PlayerName] ?? $"Set Gravity: {target.PlayerName}", + "fun", + admin); + var gravityValues = new[] + { (0.1f, "0.1"), (0.25f, "0.25"), (0.5f, "0.5"), (0.75f, "0.75"), (1f, "1"), (2f, "2") }; + + foreach (var (gravity, display) in gravityValues) + { + _sharedApi.AddMenuOption(gravitySelectionMenu, + Localizer?["fun_menu_gravity_value", display] ?? $"Gravity {display}", + _ => + { + if (target.IsValid) + { + target.SetGravity(Convert.ToSingle(gravity, CultureInfo.InvariantCulture)); + + // Track gravity modification for timer + if (gravity == 1f) + GravityPlayers.Remove(target); + else + GravityPlayers[target] = gravity; + + LogAndShowActivity(admin, target, "fun_admin_gravity_message", "css_gravity", gravity.ToString(CultureInfo.InvariantCulture)); + } + }); + } + + return gravitySelectionMenu; + } + + private object CreateSetMoneyMenu(CCSPlayerController admin) + { + var menu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_money"] ?? "Set Money", + "fun", + admin); + var players = _sharedApi.GetValidPlayers().Where(p => admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + _sharedApi.AddSubMenu(menu, playerName, p => CreateMoneySelectionMenu(admin, player)); + } + + return menu; + } + + private object CreateMoneySelectionMenu(CCSPlayerController admin, CCSPlayerController target) + { + var moneySelectionMenu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_money_player", target.PlayerName] ?? $"Set Money: {target.PlayerName}", + "fun", + admin); + var moneyValues = new[] { 0, 1000, 2500, 5000, 10000, 16000 }; + + foreach (var money in moneyValues) + { + _sharedApi.AddMenuOption(moneySelectionMenu, + Localizer?["fun_menu_money_value", money] ?? $"${money}", + _ => + { + if (target.IsValid) + { + target.SetMoney(money); + LogAndShowActivity(admin, target, "fun_admin_money_message", "css_money", money.ToString()); + } + }); + } + + return moneySelectionMenu; + } + + private object CreateSetResizeMenu(CCSPlayerController admin) + { + var menu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_resize"] ?? "Resize Player", + "fun", + admin); + var players = _sharedApi.GetValidPlayers().Where(p => + p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p)); + + foreach (var player in players) + { + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + _sharedApi.AddSubMenu(menu, playerName, p => CreateResizeSelectionMenu(admin, player)); + } + + return menu; + } + + private object CreateResizeSelectionMenu(CCSPlayerController admin, CCSPlayerController target) + { + var resizeSelectionMenu = _sharedApi!.CreateMenuWithBack( + Localizer?["fun_menu_resize_player", target.PlayerName] ?? $"Resize: {target.PlayerName}", + "fun", + admin); + var resizeValues = new[] + { (0.5f, "0.5"), (0.75f, "0.75"), (1f, "1"), (1.25f, "1.25"), (1.5f, "1.5"), (2f, "2"), (3f, "3") }; + + foreach (var (resize, display) in resizeValues) + { + _sharedApi.AddMenuOption(resizeSelectionMenu, + Localizer?["fun_menu_resize_value", display] ?? $"Size {display}", + _ => + { + if (target.IsValid) + { + Resize(admin, target, resize); + } + }); + } + + return resizeSelectionMenu; + } +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/PlayerExtensions.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/PlayerExtensions.cs new file mode 100644 index 0000000..76fefbc --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/PlayerExtensions.cs @@ -0,0 +1,271 @@ +using System.Drawing; +using System.Globalization; +using System.Numerics; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Admin; +using CounterStrikeSharp.API.Modules.Entities; +using CounterStrikeSharp.API.Modules.Memory; +using CounterStrikeSharp.API.Modules.UserMessages; + +namespace CS2_SimpleAdmin_FunCommands; + +public static class PlayerExtensions +{ + /// + /// Slaps the player pawn by applying optional damage and adding a random velocity knockback. + /// + /// The player pawn to slap. + /// The amount of damage to apply (default is 0). + public static void Slap(this CBasePlayerPawn pawn, int damage = 0) + { + PerformSlap(pawn, damage); + } + + /// + /// Determines if the player controller can target another player controller, respecting admin permissions and immunity. + /// + /// The player controller who wants to target. + /// The player controller being targeted. + /// True if targeting is allowed, false otherwise. + public static bool CanTarget(this CCSPlayerController? controller, CCSPlayerController? target) + { + if (controller is null || target is null) return true; + if (target.IsBot) return true; + + return AdminManager.CanPlayerTarget(controller, target) || + AdminManager.CanPlayerTarget(new SteamID(controller.SteamID), + new SteamID(target.SteamID)) || + AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(target); + } + + /// + /// Checks if the controller can target a player by SteamID, considering targeting permissions and immunities. + /// + /// The attacker player controller. + /// The SteamID of the target player. + /// True if targeting is permitted, false otherwise. + public static bool CanTarget(this CCSPlayerController? controller, SteamID steamId) + { + if (controller is null) return true; + + return AdminManager.CanPlayerTarget(new SteamID(controller.SteamID), steamId) || + AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(steamId); + } + + /// + /// Sets the movement speed modifier of the player controller. + /// + /// The player controller. + /// The speed modifier value. + public static void SetSpeed(this CCSPlayerController? controller, float speed) + { + var playerPawnValue = controller?.PlayerPawn.Value; + if (playerPawnValue == null) return; + + playerPawnValue.VelocityModifier = speed; + } + + /// + /// Sets the gravity scale for the player controller. + /// + /// The player controller. + /// The gravity scale. + public static void SetGravity(this CCSPlayerController? controller, float gravity) + { + var playerPawnValue = controller?.PlayerPawn.Value; + if (playerPawnValue == null) return; + + playerPawnValue.ActualGravityScale = gravity; + } + + /// + /// Sets the player's in-game money amount. + /// + /// The player controller. + /// The amount of money to set. + public static void SetMoney(this CCSPlayerController? controller, int money) + { + var moneyServices = controller?.InGameMoneyServices; + if (moneyServices == null) return; + + moneyServices.Account = money; + + if (controller != null) Utilities.SetStateChanged(controller, "CCSPlayerController", "m_pInGameMoneyServices"); + } + + /// + /// Sets the player's health points. + /// + /// The player controller. + /// The health value, default is 100. + public static void SetHp(this CCSPlayerController? controller, int health = 100) + { + if (controller == null) return; + if (health <= 0 || controller.PlayerPawn.Value == null || controller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) return; + + controller.PlayerPawn.Value.Health = health; + + if (health > 100) + { + controller.PlayerPawn.Value.MaxHealth = health; + } + + Utilities.SetStateChanged(controller.PlayerPawn.Value, "CBaseEntity", "m_iHealth"); + } + + /// + /// Buries the player pawn by moving it down by a depth offset. + /// + /// The player pawn to bury. + /// The depth offset (default 10 units). + public static void Bury(this CBasePlayerPawn pawn, float depth = 10f) + { + var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y, + pawn.AbsOrigin!.Z - depth); + var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z); + var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z); + + pawn.Teleport(newPos, newRotation, newVelocity); + } + + /// + /// Unburies the player pawn by moving it up by a depth offset. + /// + /// The player pawn to unbury. + /// The depth offset (default 15 units). + public static void Unbury(this CBasePlayerPawn pawn, float depth = 15f) + { + var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y, + pawn.AbsOrigin!.Z + depth); + var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z); + var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z); + + pawn.Teleport(newPos, newRotation, newVelocity); + } + + /// + /// Freezes the player pawn, disabling movement. + /// + /// The player pawn to freeze. + public static void Freeze(this CBasePlayerPawn pawn) + { + pawn.MoveType = MoveType_t.MOVETYPE_INVALID; + Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 11); // invalid + Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType"); + } + + /// + /// Unfreezes the player pawn, enabling movement. + /// + /// The player pawn to unfreeze. + public static void Unfreeze(this CBasePlayerPawn pawn) + { + pawn.MoveType = MoveType_t.MOVETYPE_WALK; + Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 2); // walk + Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType"); + } + + /// + /// Changes the player's color tint to specified RGBA values. + /// + /// The pawn to colorize. + /// Red component (0-255). + /// Green component (0-255). + /// Blue component (0-255). + /// Alpha (transparency) component (0-255). + public static void Colorize(this CBasePlayerPawn pawn, int r = 255, int g = 255, int b = 255, int a = 255) + { + pawn.Render = Color.FromArgb(a, r, g, b); + Utilities.SetStateChanged(pawn, "CBaseModelEntity", "m_clrRender"); + } + + /// + /// Toggles noclip mode for the player pawn. + /// + /// The player pawn. + public static void ToggleNoclip(this CBasePlayerPawn pawn) + { + if (pawn.MoveType == MoveType_t.MOVETYPE_NOCLIP) + { + pawn.MoveType = MoveType_t.MOVETYPE_WALK; + Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 2); // walk + Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType"); + } + else + { + pawn.MoveType = MoveType_t.MOVETYPE_NOCLIP; + Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 8); // noclip + Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType"); + } + } + + /// + /// Teleports a player controller to the position, rotation, and velocity of another player controller. + /// + /// The controller to teleport. + /// The target controller whose position to copy. + public static void TeleportPlayer(this CCSPlayerController? controller, CCSPlayerController? target) + { + if (controller?.PlayerPawn.Value == null && target?.PlayerPawn.Value == null) + return; + + if ( + controller?.PlayerPawn.Value is { AbsOrigin: not null, AbsRotation: not null } && + target?.PlayerPawn.Value is { AbsOrigin: not null, AbsRotation: not null } + ) + { + controller.PlayerPawn.Value.Teleport( + target.PlayerPawn.Value.AbsOrigin, + target.PlayerPawn.Value.AbsRotation, + target.PlayerPawn.Value.AbsVelocity + ); + } + } + + /// + /// Applies a slap effect to the given player pawn, optionally inflicting damage and adding velocity knockback. + /// + /// The player pawn to slap. + /// The amount of damage to deal (default is 0). + private static void PerformSlap(CBasePlayerPawn pawn, int damage = 0) + { + if (pawn.LifeState != (int)LifeState_t.LIFE_ALIVE) + return; + + var controller = pawn.Controller.Value?.As(); + + /* Teleport in a random direction - thank you, Mani!*/ + /* Thank you AM & al!*/ + var random = new Random(); + var vel = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z); + + vel.X += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1); + vel.Y += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1); + vel.Z += random.Next(200) + 100; + + pawn.AbsVelocity.X = vel.X; + pawn.AbsVelocity.Y = vel.Y; + pawn.AbsVelocity.Z = vel.Z; + + if (controller != null && controller.IsValid) + { + var shakeMessage = UserMessage.FromPartialName("Shake"); + shakeMessage.SetFloat("duration", 1); + shakeMessage.SetFloat("amplitude", 10); + shakeMessage.SetFloat("frequency", 1f); + shakeMessage.SetInt("command", 0); + shakeMessage.Recipients.Add(controller); + shakeMessage.Send(); + } + + if (damage <= 0) + return; + + pawn.Health -= damage; + Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth"); + + if (pawn.Health <= 0) + pawn.CommitSuicide(true, true); + } +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/ar.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/ar.json new file mode 100644 index 0000000..6439233 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/ar.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "أوامر المرح", + + "fun_admin_give_message": "{lightred}{0}{default} أعطى {lightred}{1}{default} {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} أخذ جميع أسلحة اللاعب {lightred}{1}{default}!", + "fun_admin_hp_message": "{lightred}{0}{default} غيّر عدد نقاط الحياة لـ {lightred}{1}{default}!", + "fun_admin_speed_message": "{lightred}{0}{default} غيّر السرعة لـ {lightred}{1}{default}!", + "fun_admin_gravity_message": "{lightred}{0}{default} غيّر الجاذبية لـ {lightred}{1}{default}!", + "fun_admin_money_message": "{lightred}{0}{default} غيّر المال لـ {lightred}{1}{default}!", + "fun_admin_god_message": "{lightred}{0}{default} غيّر وضع الله لـ {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} فعّل/ألغى نمط اللا تصادم لـ {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} جمد {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} أذاب {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} أحيى {lightred}{1}{default}!", + + "fun_menu_god": "وضع الله", + "fun_menu_noclip": "اللا تصادم", + "fun_menu_respawn": "إحياء", + "fun_menu_give": "إعطاء سلاح", + "fun_menu_give_player": "إعطاء سلاح: {0}", + "fun_menu_strip": "نزع الأسلحة", + "fun_menu_freeze": "تجميد", + "fun_menu_hp": "ضبط الحياة", + "fun_menu_hp_player": "ضبط الحياة: {0}", + "fun_menu_speed": "ضبط السرعة", + "fun_menu_speed_player": "ضبط السرعة: {0}", + "fun_menu_gravity": "ضبط الجاذبية", + "fun_menu_gravity_player": "ضبط الجاذبية: {0}", + "fun_menu_money": "ضبط المال", + "fun_menu_money_player": "ضبط المال: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "السرعة {0}", + "fun_menu_gravity_value": "الجاذبية {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} غيّر حجم {lightred}{1}{default} إلى {lightred}{2}{default}!", + "fun_menu_resize": "تغيير الحجم", + "fun_menu_resize_player": "تغيير الحجم: {0}", + "fun_menu_resize_value": "الحجم {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/de.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/de.json new file mode 100644 index 0000000..4ef4912 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/de.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Spaß-Befehle", + + "fun_admin_give_message": "{lightred}{0}{default} hat {lightred}{1}{default} ein {lightred}{2}{default} gegeben!", + "fun_admin_strip_message": "{lightred}{0}{default} hat alle Waffen von Spieler {lightred}{1}{default} entfernt!", + "fun_admin_hp_message": "{lightred}{0}{default} hat die Lebenspunkte von {lightred}{1}{default} geändert!", + "fun_admin_speed_message": "{lightred}{0}{default} hat die Geschwindigkeit von {lightred}{1}{default} geändert!", + "fun_admin_gravity_message": "{lightred}{0}{default} hat die Schwerkraft von {lightred}{1}{default} geändert!", + "fun_admin_money_message": "{lightred}{0}{default} hat das Geld von {lightred}{1}{default} geändert!", + "fun_admin_god_message": "{lightred}{0}{default} hat den Gottmodus von {lightred}{1}{default} geändert!", + "fun_admin_noclip_message": "{lightred}{0}{default} hat den Noclip-Modus für {lightred}{1}{default} aktiviert/deaktiviert!", + "fun_admin_freeze_message": "{lightred}{0}{default} hat {lightred}{1}{default} eingefroren!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} hat {lightred}{1}{default} aufgetaut!", + "fun_admin_respawn_message": "{lightred}{0}{default} hat {lightred}{1}{default} wiederbelebt!", + + "fun_menu_god": "Gottmodus", + "fun_menu_noclip": "Noclip", + "fun_menu_respawn": "Wiederbeleben", + "fun_menu_give": "Waffe Geben", + "fun_menu_give_player": "Waffe Geben: {0}", + "fun_menu_strip": "Waffen Entfernen", + "fun_menu_freeze": "Einfrieren", + "fun_menu_hp": "HP Festlegen", + "fun_menu_hp_player": "HP Festlegen: {0}", + "fun_menu_speed": "Geschwindigkeit Festlegen", + "fun_menu_speed_player": "Geschwindigkeit Festlegen: {0}", + "fun_menu_gravity": "Schwerkraft Festlegen", + "fun_menu_gravity_player": "Schwerkraft Festlegen: {0}", + "fun_menu_money": "Geld Festlegen", + "fun_menu_money_player": "Geld Festlegen: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Geschwindigkeit {0}", + "fun_menu_gravity_value": "Schwerkraft {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} hat die Größe von {lightred}{1}{default} auf {lightred}{2}{default} geändert!", + "fun_menu_resize": "Größe Ändern", + "fun_menu_resize_player": "Größe Ändern: {0}", + "fun_menu_resize_value": "Größe {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/en.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/en.json new file mode 100644 index 0000000..cbf2e2e --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/en.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Fun Commands", + + "fun_admin_give_message": "{lightred}{0}{default} gave {lightred}{1}{default} a {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} took all of player {lightred}{1}{default} weapons!", + "fun_admin_hp_message": "{lightred}{0}{default} changed {lightred}{1}{default} hp amount!", + "fun_admin_speed_message": "{lightred}{0}{default} changed speed for {lightred}{1}{default}!", + "fun_admin_gravity_message": "{lightred}{0}{default} changed gravity for {lightred}{1}{default}!", + "fun_admin_money_message": "{lightred}{0}{default} changed money for {lightred}{1}{default}!", + "fun_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} toggled noclip for {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} froze {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} unfroze {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} respawned {lightred}{1}{default}!", + + "fun_menu_god": "God Mode", + "fun_menu_noclip": "No Clip", + "fun_menu_respawn": "Respawn", + "fun_menu_give": "Give Weapon", + "fun_menu_give_player": "Give Weapon: {0}", + "fun_menu_strip": "Strip Weapons", + "fun_menu_freeze": "Freeze", + "fun_menu_hp": "Set HP", + "fun_menu_hp_player": "Set HP: {0}", + "fun_menu_speed": "Set Speed", + "fun_menu_speed_player": "Set Speed: {0}", + "fun_menu_gravity": "Set Gravity", + "fun_menu_gravity_player": "Set Gravity: {0}", + "fun_menu_money": "Set Money", + "fun_menu_money_player": "Set Money: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Speed {0}", + "fun_menu_gravity_value": "Gravity {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} resized {lightred}{1}{default} to {lightred}{2}{default}!", + "fun_menu_resize": "Resize Player", + "fun_menu_resize_player": "Resize: {0}", + "fun_menu_resize_value": "Size {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/es.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/es.json new file mode 100644 index 0000000..0c21b32 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/es.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Comandos Divertidos", + + "fun_admin_give_message": "{lightred}{0}{default} dio {lightred}{1}{default} un {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} quitó todas las armas del jugador {lightred}{1}{default}!", + "fun_admin_hp_message": "{lightred}{0}{default} cambió la cantidad de HP de {lightred}{1}{default}!", + "fun_admin_speed_message": "{lightred}{0}{default} cambió la velocidad de {lightred}{1}{default}!", + "fun_admin_gravity_message": "{lightred}{0}{default} cambió la gravedad de {lightred}{1}{default}!", + "fun_admin_money_message": "{lightred}{0}{default} cambió el dinero de {lightred}{1}{default}!", + "fun_admin_god_message": "{lightred}{0}{default} cambió el modo dios de {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} alternó noclip para {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} congeló a {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} descongeló a {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} reapareció a {lightred}{1}{default}!", + + "fun_menu_god": "Modo Dios", + "fun_menu_noclip": "Noclip", + "fun_menu_respawn": "Reaparecer", + "fun_menu_give": "Dar Arma", + "fun_menu_give_player": "Dar Arma: {0}", + "fun_menu_strip": "Quitar Armas", + "fun_menu_freeze": "Congelar", + "fun_menu_hp": "Establecer HP", + "fun_menu_hp_player": "Establecer HP: {0}", + "fun_menu_speed": "Establecer Velocidad", + "fun_menu_speed_player": "Establecer Velocidad: {0}", + "fun_menu_gravity": "Establecer Gravedad", + "fun_menu_gravity_player": "Establecer Gravedad: {0}", + "fun_menu_money": "Establecer Dinero", + "fun_menu_money_player": "Establecer Dinero: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Velocidad {0}", + "fun_menu_gravity_value": "Gravedad {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} cambió el tamaño de {lightred}{1}{default} a {lightred}{2}{default}!", + "fun_menu_resize": "Cambiar Tamaño", + "fun_menu_resize_player": "Cambiar Tamaño: {0}", + "fun_menu_resize_value": "Tamaño {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/fa.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/fa.json new file mode 100644 index 0000000..cb8be4b --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/fa.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "دستورات سرگرمی", + + "fun_admin_give_message": "{lightred}{0}{default} {lightred}{2}{default} را به {lightred}{1}{default} داد!", + "fun_admin_strip_message": "{lightred}{0}{default} تمام سلاح‌های بازیکن {lightred}{1}{default} را گرفت!", + "fun_admin_hp_message": "{lightred}{0}{default} مقدار سلامت {lightred}{1}{default} را تغییر داد!", + "fun_admin_speed_message": "{lightred}{0}{default} سرعت {lightred}{1}{default} را تغییر داد!", + "fun_admin_gravity_message": "{lightred}{0}{default} جاذبه {lightred}{1}{default} را تغییر داد!", + "fun_admin_money_message": "{lightred}{0}{default} پول {lightred}{1}{default} را تغییر داد!", + "fun_admin_god_message": "{lightred}{0}{default} حالت خدا را برای {lightred}{1}{default} تغییر داد!", + "fun_admin_noclip_message": "{lightred}{0}{default} ناپدیدی را برای {lightred}{1}{default} فعال/غیرفعال کرد!", + "fun_admin_freeze_message": "{lightred}{0}{default} {lightred}{1}{default} را یخ‌زده کرد!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} {lightred}{1}{default} را از حالت یخ خارج کرد!", + "fun_admin_respawn_message": "{lightred}{0}{default} {lightred}{1}{default} را دوباره زنده کرد!", + + "fun_menu_god": "حالت خدا", + "fun_menu_noclip": "ناپدیدی", + "fun_menu_respawn": "احیا", + "fun_menu_give": "دادن سلاح", + "fun_menu_give_player": "دادن سلاح: {0}", + "fun_menu_strip": "گرفتن سلاح‌ها", + "fun_menu_freeze": "یخ زدن", + "fun_menu_hp": "تنظیم سلامت", + "fun_menu_hp_player": "تنظیم سلامت: {0}", + "fun_menu_speed": "تنظیم سرعت", + "fun_menu_speed_player": "تنظیم سرعت: {0}", + "fun_menu_gravity": "تنظیم جاذبه", + "fun_menu_gravity_player": "تنظیم جاذبه: {0}", + "fun_menu_money": "تنظیم پول", + "fun_menu_money_player": "تنظیم پول: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "سرعت {0}", + "fun_menu_gravity_value": "جاذبه {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} اندازه {lightred}{1}{default} را به {lightred}{2}{default} تغییر داد!", + "fun_menu_resize": "تغییر اندازه", + "fun_menu_resize_player": "تغییر اندازه: {0}", + "fun_menu_resize_value": "اندازه {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/fr.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/fr.json new file mode 100644 index 0000000..c2726d4 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/fr.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Commandes Amusantes", + + "fun_admin_give_message": "{lightred}{0}{default} a donné {lightred}{2}{default} à {lightred}{1}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} a retiré toutes les armes de {lightred}{1}{default}!", + "fun_admin_hp_message": "{lightred}{0}{default} a modifié la quantité de HP de {lightred}{1}{default}!", + "fun_admin_speed_message": "{lightred}{0}{default} a modifié la vitesse de {lightred}{1}{default}!", + "fun_admin_gravity_message": "{lightred}{0}{default} a modifié la gravité de {lightred}{1}{default}!", + "fun_admin_money_message": "{lightred}{0}{default} a modifié l'argent de {lightred}{1}{default}!", + "fun_admin_god_message": "{lightred}{0}{default} a modifié le mode dieu de {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} a activé/désactivé le noclip pour {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} a gelé {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} a dégivré {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} a réapparu {lightred}{1}{default}!", + + "fun_menu_god": "Mode Dieu", + "fun_menu_noclip": "Noclip", + "fun_menu_respawn": "Réapparition", + "fun_menu_give": "Donner Arme", + "fun_menu_give_player": "Donner Arme: {0}", + "fun_menu_strip": "Retirer Armes", + "fun_menu_freeze": "Geler", + "fun_menu_hp": "Définir HP", + "fun_menu_hp_player": "Définir HP: {0}", + "fun_menu_speed": "Définir Vitesse", + "fun_menu_speed_player": "Définir Vitesse: {0}", + "fun_menu_gravity": "Définir Gravité", + "fun_menu_gravity_player": "Définir Gravité: {0}", + "fun_menu_money": "Définir Argent", + "fun_menu_money_player": "Définir Argent: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Vitesse {0}", + "fun_menu_gravity_value": "Gravité {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} a redimensionné {lightred}{1}{default} à {lightred}{2}{default}!", + "fun_menu_resize": "Redimensionner", + "fun_menu_resize_player": "Redimensionner: {0}", + "fun_menu_resize_value": "Taille {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/lv.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/lv.json new file mode 100644 index 0000000..15099b2 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/lv.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Izklaidējošas Komandas", + + "fun_admin_give_message": "{lightred}{0}{default} iedeva {lightred}{1}{default} {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} noņēma visus {lightred}{1}{default} ieročus!", + "fun_admin_hp_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} HP daudzumu!", + "fun_admin_speed_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} ātrumu!", + "fun_admin_gravity_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} gravitāciju!", + "fun_admin_money_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} naudu!", + "fun_admin_god_message": "{lightred}{0}{default} mainīja dieva režīmu priekš {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} aktivizēja/deaktivizēja noclip priekš {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} sasaldēja {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} atkausēja {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} atdzīvināja {lightred}{1}{default}!", + + "fun_menu_god": "Dieva Režīms", + "fun_menu_noclip": "Noclip", + "fun_menu_respawn": "Atdzīvināt", + "fun_menu_give": "Dot Ieroci", + "fun_menu_give_player": "Dot Ieroci: {0}", + "fun_menu_strip": "Noņemt Ieročus", + "fun_menu_freeze": "Sasaldēt", + "fun_menu_hp": "Uzstādīt HP", + "fun_menu_hp_player": "Uzstādīt HP: {0}", + "fun_menu_speed": "Uzstādīt Ātrumu", + "fun_menu_speed_player": "Uzstādīt Ātrumu: {0}", + "fun_menu_gravity": "Uzstādīt Gravitāciju", + "fun_menu_gravity_player": "Uzstādīt Gravitāciju: {0}", + "fun_menu_money": "Uzstādīt Naudu", + "fun_menu_money_player": "Uzstādīt Naudu: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Ātrums {0}", + "fun_menu_gravity_value": "Gravitācija {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} izmēru uz {lightred}{2}{default}!", + "fun_menu_resize": "Mainīt Izmēru", + "fun_menu_resize_player": "Mainīt Izmēru: {0}", + "fun_menu_resize_value": "Izmērs {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pl.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pl.json new file mode 100644 index 0000000..a79a7c1 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pl.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Komendy Rozrywkowe", + + "fun_admin_give_message": "{lightred}{0}{default} dał {lightred}{1}{default} przedmiot {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} zabrał wszystkie bronie {lightred}{1}{default}!", + "fun_admin_hp_message": "{lightred}{0}{default} zmienił ilość hp dla {lightred}{1}{default}!", + "fun_admin_speed_message": "{lightred}{0}{default} zmienił prędkość dla {lightred}{1}{default}!", + "fun_admin_gravity_message": "{lightred}{0}{default} zmienił grawitację dla {lightred}{1}{default}!", + "fun_admin_money_message": "{lightred}{0}{default} zmienił pieniądze dla {lightred}{1}{default}!", + "fun_admin_god_message": "{lightred}{0}{default} zmienił tryb Boga dla {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} ustawił latanie dla {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} zamroził {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} odmroził {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} odrodził {lightred}{1}{default}!", + + "fun_menu_god": "Tryb Boga", + "fun_menu_noclip": "Latanie", + "fun_menu_respawn": "Odrodzenie", + "fun_menu_give": "Daj Broń", + "fun_menu_give_player": "Daj Broń: {0}", + "fun_menu_strip": "Zabierz Bronie", + "fun_menu_freeze": "Zamrożenie", + "fun_menu_hp": "Ustaw HP", + "fun_menu_hp_player": "Ustaw HP: {0}", + "fun_menu_speed": "Ustaw Prędkość", + "fun_menu_speed_player": "Ustaw Prędkość: {0}", + "fun_menu_gravity": "Ustaw Grawitację", + "fun_menu_gravity_player": "Ustaw Grawitację: {0}", + "fun_menu_money": "Ustaw Pieniądze", + "fun_menu_money_player": "Ustaw Pieniądze: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Prędkość {0}", + "fun_menu_gravity_value": "Grawitacja {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} zmienił rozmiar {lightred}{1}{default} na {lightred}{2}{default}!", + "fun_menu_resize": "Zmień Rozmiar", + "fun_menu_resize_player": "Zmień Rozmiar: {0}", + "fun_menu_resize_value": "Rozmiar {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pt-BR.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pt-BR.json new file mode 100644 index 0000000..a363d3b --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pt-BR.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Comandos Divertidos", + + "fun_admin_give_message": "{lightred}{0}{default} deu {lightred}{1}{default} um(a) {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} tirou todas as armas de {lightred}{1}{default}!", + "fun_admin_hp_message": "{lightred}{0}{default} mudou a quantidade de HP de {lightred}{1}{default}!", + "fun_admin_speed_message": "{lightred}{0}{default} mudou a velocidade de {lightred}{1}{default}!", + "fun_admin_gravity_message": "{lightred}{0}{default} mudou a gravidade de {lightred}{1}{default}!", + "fun_admin_money_message": "{lightred}{0}{default} mudou a quantidade de dinheiro de {lightred}{1}{default}!", + "fun_admin_god_message": "{lightred}{0}{default} mudou o modo Deus de {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} ativou/desativou o noclip para {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} congelou {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} descongelou {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} reanimou {lightred}{1}{default}!", + + "fun_menu_god": "Modo Deus", + "fun_menu_noclip": "Noclip", + "fun_menu_respawn": "Reanimar", + "fun_menu_give": "Dar Arma", + "fun_menu_give_player": "Dar Arma: {0}", + "fun_menu_strip": "Remover Armas", + "fun_menu_freeze": "Congelar", + "fun_menu_hp": "Definir HP", + "fun_menu_hp_player": "Definir HP: {0}", + "fun_menu_speed": "Definir Velocidade", + "fun_menu_speed_player": "Definir Velocidade: {0}", + "fun_menu_gravity": "Definir Gravidade", + "fun_menu_gravity_player": "Definir Gravidade: {0}", + "fun_menu_money": "Definir Dinheiro", + "fun_menu_money_player": "Definir Dinheiro: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Velocidade {0}", + "fun_menu_gravity_value": "Gravidade {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} redimensionou {lightred}{1}{default} para {lightred}{2}{default}!", + "fun_menu_resize": "Redimensionar", + "fun_menu_resize_player": "Redimensionar: {0}", + "fun_menu_resize_value": "Tamanho {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pt-PT.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pt-PT.json new file mode 100644 index 0000000..a363d3b --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/pt-PT.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Comandos Divertidos", + + "fun_admin_give_message": "{lightred}{0}{default} deu {lightred}{1}{default} um(a) {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} tirou todas as armas de {lightred}{1}{default}!", + "fun_admin_hp_message": "{lightred}{0}{default} mudou a quantidade de HP de {lightred}{1}{default}!", + "fun_admin_speed_message": "{lightred}{0}{default} mudou a velocidade de {lightred}{1}{default}!", + "fun_admin_gravity_message": "{lightred}{0}{default} mudou a gravidade de {lightred}{1}{default}!", + "fun_admin_money_message": "{lightred}{0}{default} mudou a quantidade de dinheiro de {lightred}{1}{default}!", + "fun_admin_god_message": "{lightred}{0}{default} mudou o modo Deus de {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} ativou/desativou o noclip para {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} congelou {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} descongelou {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} reanimou {lightred}{1}{default}!", + + "fun_menu_god": "Modo Deus", + "fun_menu_noclip": "Noclip", + "fun_menu_respawn": "Reanimar", + "fun_menu_give": "Dar Arma", + "fun_menu_give_player": "Dar Arma: {0}", + "fun_menu_strip": "Remover Armas", + "fun_menu_freeze": "Congelar", + "fun_menu_hp": "Definir HP", + "fun_menu_hp_player": "Definir HP: {0}", + "fun_menu_speed": "Definir Velocidade", + "fun_menu_speed_player": "Definir Velocidade: {0}", + "fun_menu_gravity": "Definir Gravidade", + "fun_menu_gravity_player": "Definir Gravidade: {0}", + "fun_menu_money": "Definir Dinheiro", + "fun_menu_money_player": "Definir Dinheiro: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Velocidade {0}", + "fun_menu_gravity_value": "Gravidade {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} redimensionou {lightred}{1}{default} para {lightred}{2}{default}!", + "fun_menu_resize": "Redimensionar", + "fun_menu_resize_player": "Redimensionar: {0}", + "fun_menu_resize_value": "Tamanho {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/ru.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/ru.json new file mode 100644 index 0000000..34ba26e --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/ru.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Развлекательные Команды", + + "fun_admin_give_message": "{lightred}{0}{default} дал {lightred}{1}{default} {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} забрал все оружие у {lightred}{1}{default}!", + "fun_admin_hp_message": "{lightred}{0}{default} изменил количество HP у {lightred}{1}{default}!", + "fun_admin_speed_message": "{lightred}{0}{default} изменил скорость {lightred}{1}{default}!", + "fun_admin_gravity_message": "{lightred}{0}{default} изменил гравитацию для {lightred}{1}{default}!", + "fun_admin_money_message": "{lightred}{0}{default} изменил количество денег у {lightred}{1}{default}!", + "fun_admin_god_message": "{lightred}{0}{default} изменил режим бога для {lightred}{1}{default}!", + "fun_admin_noclip_message": "{lightred}{0}{default} включил/выключил noclip для {lightred}{1}{default}!", + "fun_admin_freeze_message": "{lightred}{0}{default} заморозил {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} разморозил {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} возродил {lightred}{1}{default}!", + + "fun_menu_god": "Режим Бога", + "fun_menu_noclip": "Noclip", + "fun_menu_respawn": "Возрождение", + "fun_menu_give": "Выдать Оружие", + "fun_menu_give_player": "Выдать Оружие: {0}", + "fun_menu_strip": "Забрать Оружие", + "fun_menu_freeze": "Заморозить", + "fun_menu_hp": "Установить HP", + "fun_menu_hp_player": "Установить HP: {0}", + "fun_menu_speed": "Установить Скорость", + "fun_menu_speed_player": "Установить Скорость: {0}", + "fun_menu_gravity": "Установить Гравитацию", + "fun_menu_gravity_player": "Установить Гравитацию: {0}", + "fun_menu_money": "Установить Деньги", + "fun_menu_money_player": "Установить Деньги: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Скорость {0}", + "fun_menu_gravity_value": "Гравитация {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} изменил размер {lightred}{1}{default} на {lightred}{2}{default}!", + "fun_menu_resize": "Изменить Размер", + "fun_menu_resize_player": "Изменить Размер: {0}", + "fun_menu_resize_value": "Размер {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/tr.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/tr.json new file mode 100644 index 0000000..fbee819 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/tr.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "Eğlence Komutları", + + "fun_admin_give_message": "{lightred}{0}{default} {lightred}{1}{default}'e {lightred}{2}{default} verdi!", + "fun_admin_strip_message": "{lightred}{0}{default} {lightred}{1}{default}'in tüm silahlarını aldı!", + "fun_admin_hp_message": "{lightred}{0}{default} {lightred}{1}{default}'in HP miktarını değiştirdi!", + "fun_admin_speed_message": "{lightred}{0}{default} {lightred}{1}{default}'in hızını değiştirdi!", + "fun_admin_gravity_message": "{lightred}{0}{default} {lightred}{1}{default}'in yer çekimini değiştirdi!", + "fun_admin_money_message": "{lightred}{0}{default} {lightred}{1}{default}'in parasını değiştirdi!", + "fun_admin_god_message": "{lightred}{0}{default} {lightred}{1}{default}'in tanrı modunu değiştirdi!", + "fun_admin_noclip_message": "{lightred}{0}{default} {lightred}{1}{default} için noclip'i açtı/kapatı!", + "fun_admin_freeze_message": "{lightred}{0}{default} {lightred}{1}{default}'i dondurdu!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} {lightred}{1}{default}'in dondurmasını çözdü!", + "fun_admin_respawn_message": "{lightred}{0}{default} {lightred}{1}{default}'i yeniden doğurdu!", + + "fun_menu_god": "Tanrı Modu", + "fun_menu_noclip": "Noclip", + "fun_menu_respawn": "Yeniden Doğma", + "fun_menu_give": "Silah Ver", + "fun_menu_give_player": "Silah Ver: {0}", + "fun_menu_strip": "Silahları Al", + "fun_menu_freeze": "Dondur", + "fun_menu_hp": "HP Ayarla", + "fun_menu_hp_player": "HP Ayarla: {0}", + "fun_menu_speed": "Hız Ayarla", + "fun_menu_speed_player": "Hız Ayarla: {0}", + "fun_menu_gravity": "Yer Çekimi Ayarla", + "fun_menu_gravity_player": "Yer Çekimi Ayarla: {0}", + "fun_menu_money": "Para Ayarla", + "fun_menu_money_player": "Para Ayarla: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "Hız {0}", + "fun_menu_gravity_value": "Yer Çekimi {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} {lightred}{1}{default}'in boyutunu {lightred}{2}{default} olarak değiştirdi!", + "fun_menu_resize": "Boyut Değiştir", + "fun_menu_resize_player": "Boyut Değiştir: {0}", + "fun_menu_resize_value": "Boyut {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/zh-Hans.json b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/zh-Hans.json new file mode 100644 index 0000000..939b222 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/lang/zh-Hans.json @@ -0,0 +1,40 @@ +{ + "fun_category_name": "趣味命令", + + "fun_admin_give_message": "{lightred}{0}{default} 给了 {lightred}{1}{default} {lightred}{2}{default}!", + "fun_admin_strip_message": "{lightred}{0}{default} 移除了 {lightred}{1}{default} 的所有武器!", + "fun_admin_hp_message": "{lightred}{0}{default} 修改了 {lightred}{1}{default} 的生命值!", + "fun_admin_speed_message": "{lightred}{0}{default} 修改了 {lightred}{1}{default} 的速度!", + "fun_admin_gravity_message": "{lightred}{0}{default} 修改了 {lightred}{1}{default} 的重力!", + "fun_admin_money_message": "{lightred}{0}{default} 修改了 {lightred}{1}{default} 的金钱!", + "fun_admin_god_message": "{lightred}{0}{default} 切换了 {lightred}{1}{default} 的上帝模式!", + "fun_admin_noclip_message": "{lightred}{0}{default} 切换了 {lightred}{1}{default} 的穿墙模式!", + "fun_admin_freeze_message": "{lightred}{0}{default} 冻结了 {lightred}{1}{default}!", + "fun_admin_unfreeze_message": "{lightred}{0}{default} 解冻了 {lightred}{1}{default}!", + "fun_admin_respawn_message": "{lightred}{0}{default} 复活了 {lightred}{1}{default}!", + + "fun_menu_god": "上帝模式", + "fun_menu_noclip": "穿墙模式", + "fun_menu_respawn": "复活", + "fun_menu_give": "给予武器", + "fun_menu_give_player": "给予武器: {0}", + "fun_menu_strip": "移除武器", + "fun_menu_freeze": "冻结", + "fun_menu_hp": "设置生命值", + "fun_menu_hp_player": "设置生命值: {0}", + "fun_menu_speed": "设置速度", + "fun_menu_speed_player": "设置速度: {0}", + "fun_menu_gravity": "设置重力", + "fun_menu_gravity_player": "设置重力: {0}", + "fun_menu_money": "设置金钱", + "fun_menu_money_player": "设置金钱: {0}", + "fun_menu_hp_value": "{0} HP", + "fun_menu_speed_value": "速度 {0}", + "fun_menu_gravity_value": "重力 {0}", + "fun_menu_money_value": "${0}", + + "fun_admin_resize_message": "{lightred}{0}{default} 将 {lightred}{1}{default} 的大小改为 {lightred}{2}{default}!", + "fun_menu_resize": "调整大小", + "fun_menu_resize_player": "调整大小: {0}", + "fun_menu_resize_value": "大小 {0}" +} diff --git a/Modules/CS2-SimpleAdmin_FunCommands/README.md b/Modules/CS2-SimpleAdmin_FunCommands/README.md new file mode 100644 index 0000000..85df82d --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/README.md @@ -0,0 +1,384 @@ +# CS2-SimpleAdmin Fun Commands Module + +This module serves as a **reference implementation** for creating CS2-SimpleAdmin modules. It demonstrates best practices for menu creation, command registration, translation support, and API usage. + +## 📚 What This Module Teaches + +This module is designed to be educational and shows you how to: + +1. ✅ **Register commands dynamically** from configuration +2. ✅ **Create menu categories** and menu items +3. ✅ **Use per-player translations** with `ShowAdminActivityLocalized` +4. ✅ **Handle player targeting** and validation +5. ✅ **Implement proper cleanup** on module unload +6. ✅ **Structure code** using partial classes for organization +7. ✅ **Cache data** for performance (weapons cache) +8. ✅ **Use configuration** to enable/disable features + +## 🎯 Features + +This module provides fun admin commands: + +- **God Mode** (`css_god`) - Toggle god mode for players +- **No Clip** (`css_noclip`) - Enable no-clip mode +- **Freeze/Unfreeze** (`css_freeze`, `css_unfreeze`) - Freeze players in place +- **Respawn** (`css_respawn`) - Respawn dead players +- **Give Weapon** (`css_give`) - Give weapons to players +- **Strip Weapons** (`css_strip`) - Remove all player weapons +- **Set HP** (`css_hp`) - Set player health +- **Set Speed** (`css_speed`) - Modify player movement speed +- **Set Gravity** (`css_gravity`) - Change player gravity +- **Set Money** (`css_money`) - Set player money + +## 📁 File Structure + +``` +CS2-SimpleAdmin_FunCommands/ +├── CS2-SimpleAdmin_FunCommands.cs # Main plugin file - initialization, registration +├── Commands.cs # Command handlers +├── Actions.cs # Action methods (God, NoClip, Freeze, etc.) +├── Menus.cs # Menu creation using SimpleAdmin API +├── Config.cs # Configuration with command lists +└── lang/ # Translation files (13 languages) + ├── en.json + ├── pl.json + ├── ru.json + └── ... (10 more languages) +``` + +## 🔍 Code Organization Explained + +### 1. Main Plugin File (`CS2-SimpleAdmin_FunCommands.cs`) + +**Key Concepts Demonstrated:** + +```csharp +public partial class CS2_SimpleAdmin_FunCommands : BasePlugin, IPluginConfig +{ + // ✅ BEST PRACTICE: Use capability system to get API + private ICS2_SimpleAdminApi? _sharedApi; + private readonly PluginCapability _pluginCapability = new("simpleadmin:api"); + + // ✅ BEST PRACTICE: Cache expensive data + private static Dictionary? _weaponsCache; + + // ✅ BEST PRACTICE: Track menu registration state + private bool _menusRegistered = false; + + public override void OnAllPluginsLoaded(bool hotReload) + { + // Get the API + _sharedApi = _pluginCapability.Get(); + + // Register commands + RegisterFunCommands(); + + // ✅ BEST PRACTICE: Wait for SimpleAdmin to be ready before registering menus + _sharedApi.OnSimpleAdminReady += RegisterFunMenus; + RegisterFunMenus(); // Fallback for hot reload + } +} +``` + +**Why partial classes?** +- Separates concerns (commands, actions, menus) +- Makes code easier to navigate +- Each file has a specific purpose + +### 2. Configuration (`Config.cs`) + +**Key Concept:** Command lists for flexibility + +```csharp +public class Config : IBasePluginConfig +{ + // ✅ BEST PRACTICE: Allow multiple command aliases + public List NoclipCommands { get; set; } = ["css_noclip"]; + public List GodCommands { get; set; } = ["css_god"]; + // ... more command lists +} +``` + +**Benefits:** +- Users can disable features by emptying the list +- Users can add command aliases (e.g., `["css_god", "css_godmode"]`) +- Menus only register if commands exist + +### 3. Commands (`Commands.cs`) + +**Key Concepts Demonstrated:** + +```csharp +[CommandHelper(1, "<#userid or name>")] +[RequiresPermissions("@css/cheats")] +private void OnGodCommand(CCSPlayerController? caller, CommandInfo command) +{ + // ✅ BEST PRACTICE: Use API to get targets (handles target syntax) + var targets = _sharedApi!.GetTarget(command); + if (targets == null) return; + + // ✅ BEST PRACTICE: Filter for alive players + var playersToTarget = targets.Players.Where(player => + player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList(); + + // ✅ BEST PRACTICE: Check targeting permissions + playersToTarget.ForEach(player => + { + if (caller!.CanTarget(player)) + { + God(caller, player); + } + }); + + // ✅ BEST PRACTICE: Always log commands + _sharedApi.LogCommand(caller, command); +} +``` + +### 4. Actions (`Actions.cs`) + +**Key Concepts Demonstrated:** + +```csharp +private void God(CCSPlayerController? caller, CCSPlayerController player) +{ + // Perform the action + if (!GodPlayers.Add(player.Slot)) + { + GodPlayers.Remove(player.Slot); + } + + // ✅ BEST PRACTICE: Use per-player language support + var activityArgs = new object[] { "CALLER", player.PlayerName }; + if (caller == null || !_sharedApi!.IsAdminSilent(caller)) + { + if (Localizer != null) + { + // Each player sees message in their configured language! + _sharedApi!.ShowAdminActivityLocalized(Localizer, "fun_admin_god_message", callerName, false, activityArgs); + } + } + + // ✅ BEST PRACTICE: Log the action + _sharedApi!.LogCommand(caller, $"css_god {player.PlayerName}"); +} +``` + +### 5. Menus (`Menus.cs`) + +**Key Concepts Demonstrated:** + +#### Simple Player Selection Menu +```csharp +private object CreateGodModeMenu(CCSPlayerController admin) +{ + // ✅ BEST PRACTICE: Use CreateMenuWithPlayers for simple player selection + return _sharedApi!.CreateMenuWithPlayers("God Mode", "fun", admin, + player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player), + God); // Direct method reference +} +``` + +#### Nested Menu with Value Selection +```csharp +private object CreateSetHpMenu(CCSPlayerController admin) +{ + // ✅ BEST PRACTICE: Use CreateMenuWithBack for menus with back button + var menu = _sharedApi!.CreateMenuWithBack("Set HP", "fun", admin); + + var players = _sharedApi.GetValidPlayers().Where(p => + p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p)); + + foreach (var player in players) + { + // ✅ BEST PRACTICE: AddSubMenu automatically adds back button to submenu + _sharedApi.AddSubMenu(menu, playerName, p => CreateHpSelectionMenu(admin, player)); + } + + return menu; +} + +private object CreateHpSelectionMenu(CCSPlayerController admin, CCSPlayerController target) +{ + var hpMenu = _sharedApi!.CreateMenuWithBack($"Set HP: {target.PlayerName}", "fun", admin); + var hpValues = new[] { 1, 10, 25, 50, 100, 200, 500, 999 }; + + foreach (var hp in hpValues) + { + // ✅ BEST PRACTICE: AddMenuOption for simple actions + _sharedApi.AddMenuOption(hpMenu, $"{hp} HP", _ => + { + // ✅ BEST PRACTICE: Always validate before executing + if (target.IsValid) + { + target.SetHp(hp); + LogAndShowActivity(admin, target, "fun_admin_hp_message", "css_hp", hp.ToString()); + } + }); + } + + return hpMenu; +} +``` + +### 6. Translations + +**Key Concept:** Module-specific translations + +```json +// lang/en.json +{ + "fun_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!", + "fun_admin_hp_message": "{lightred}{0}{default} changed {lightred}{1}{default} hp amount!" +} +``` + +**Why module translations?** +- Your module is independent from SimpleAdmin +- You can update translations without affecting main plugin +- Each player sees messages in their language automatically + +## 🛠️ How to Use This as a Template + +### Step 1: Copy the Module +```bash +cp -r CS2-SimpleAdmin_FunCommands YourModuleName +``` + +### Step 2: Rename Files +- Rename `.csproj` file +- Rename all `.cs` files to match your module name +- Update namespace in all files + +### Step 3: Update References +- Change `namespace CS2_SimpleAdmin_FunCommands` to `namespace YourModuleName` +- Update plugin metadata (name, version, author, description) + +### Step 4: Modify Config +```csharp +public class Config : IBasePluginConfig +{ + public int Version { get; set; } = 1; + + // Add your own command lists + public List YourCommands { get; set; } = ["css_yourcommand"]; +} +``` + +### Step 5: Add Your Commands +Look at `Commands.cs` for examples of command handlers + +### Step 6: Add Your Menus +Look at `Menus.cs` for examples of menu creation + +### Step 7: Add Translations +Create language files in `lang/{language}.json` (e.g., `lang/en.json`, `lang/pl.json`) + +## 📖 Learning Path + +If you're new to module development, study files in this order: + +1. **Config.cs** - Understand configuration structure +2. **CS2-SimpleAdmin_FunCommands.cs** - See initialization and API acquisition +3. **Commands.cs** - Learn command registration and handling +4. **Actions.cs** - Understand action methods and translations +5. **Menus.cs** - Study menu creation patterns + +## 🎓 Best Practices Demonstrated + +### ✅ Command Registration +```csharp +// Dynamic registration based on config +if (Config.GodCommands.Count > 0) +{ + foreach (var command in Config.GodCommands) + { + _sharedApi.RegisterCommand(command, "Enable god mode", OnGodCommand); + } +} +``` + +### ✅ Target Validation +```csharp +// Always check if player can be targeted +if (!caller.CanTarget(player)) return; + +// Always validate player state +if (!player.IsValid) return; +``` + +### ✅ Translation Usage +```csharp +// Use module's localizer for per-player language support +if (Localizer != null) +{ + _sharedApi.ShowAdminActivityLocalized(Localizer, "translation_key", callerName, false, args); +} +``` + +### ✅ Cleanup on Unload +```csharp +public override void Unload(bool hotReload) +{ + // Unregister all commands + if (Config.GodCommands.Count > 0) + { + foreach (var command in Config.GodCommands) + { + _sharedApi.UnRegisterCommand(command); + } + } + + // Unregister all menus + if (Config.GodCommands.Count > 0) + _sharedApi.UnregisterMenu("fun", "god"); + + // Remove event handlers + _sharedApi.OnSimpleAdminReady -= RegisterFunMenus; +} +``` + +### ✅ Data Caching +```csharp +// Cache expensive operations +private static Dictionary GetWeaponsCache() +{ + if (_weaponsCache != null) return _weaponsCache; + + var weaponsArray = Enum.GetValues(typeof(CsItem)); + _weaponsCache = new Dictionary(); + // ... populate cache + return _weaponsCache; +} +``` + +## 🔗 Related Documentation + +- **[MODULE_DEVELOPMENT.md](../MODULE_DEVELOPMENT.md)** - Complete API reference +- **[TRANSLATION_EXAMPLE.md](../TRANSLATION_EXAMPLE.md)** - Translation usage guide +- **[CS2-SimpleAdminApi](../../CS2-SimpleAdminApi/)** - API interface definitions + +## 💡 Tips + +1. **Start Simple** - Begin with one command and one menu, then expand +2. **Test Thoroughly** - Test with multiple players, different permissions, and languages +3. **Handle Errors** - Always validate player state before actions +4. **Log Everything** - Use `_sharedApi.LogCommand()` for all admin actions +5. **Use Partial Classes** - Split your code into logical files +6. **Comment Your Code** - Especially if others will use it as reference + +## 🐛 Common Mistakes to Avoid + +- ❌ **Don't** forget to check `IsValid` before accessing player properties +- ❌ **Don't** skip permission checks (`CanTarget`, `RequiresPermissions`) +- ❌ **Don't** forget to unregister commands/menus in `Unload()` +- ❌ **Don't** use SimpleAdmin's translations - create your own +- ❌ **Don't** hardcode command names - use config lists instead + +## 🤝 Contributing + +If you improve this module or find better patterns, please contribute back so others can learn! + +## 📄 License + +This module is provided as-is for educational purposes. Feel free to use it as a template for your own modules. diff --git a/Modules/CS2-SimpleAdmin_FunCommands/global.json b/Modules/CS2-SimpleAdmin_FunCommands/global.json new file mode 100644 index 0000000..2ddda36 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_FunCommands/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-linux.tar.gz b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-linux.tar.gz deleted file mode 100644 index 61f7be3..0000000 Binary files a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-linux.tar.gz and /dev/null differ diff --git a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-windows.tar.gz b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-windows.tar.gz deleted file mode 100644 index 050a3a9..0000000 Binary files a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-windows.tar.gz and /dev/null differ diff --git a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.7-linux.tar.gz b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.7-linux.tar.gz new file mode 100644 index 0000000..6d70251 Binary files /dev/null and b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.7-linux.tar.gz differ diff --git a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.7-windows.tar.gz b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.7-windows.tar.gz new file mode 100644 index 0000000..d3a015d Binary files /dev/null and b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.7-windows.tar.gz differ diff --git a/Modules/MODULE_DEVELOPMENT.md b/Modules/MODULE_DEVELOPMENT.md new file mode 100644 index 0000000..2710d13 --- /dev/null +++ b/Modules/MODULE_DEVELOPMENT.md @@ -0,0 +1,602 @@ +# Module Development Guide - CS2-SimpleAdmin + +> **🎓 New to module development?** Start with the [Fun Commands Module](./CS2-SimpleAdmin_FunCommands/) - it's a fully documented reference implementation showing all best practices! + +This guide explains how to create modules for CS2-SimpleAdmin with custom commands, menus, and translations. + +## 📖 Table of Contents + +1. [Quick Start](#quick-start) +2. [Learning Resources](#learning-resources) +3. [API Methods Reference](#api-methods) +4. [Menu Patterns](#menu-patterns) +5. [Best Practices](#best-practices) +6. [Common Patterns](#common-patterns) +7. [Troubleshooting](#troubleshooting) + +## 🚀 Quick Start + +### Step 1: Study the Example Module + +The **[CS2-SimpleAdmin_FunCommands](./CS2-SimpleAdmin_FunCommands/)** module is your best learning resource. It demonstrates: + +✅ Command registration from config +✅ Dynamic menu creation +✅ Per-player translation support +✅ Proper cleanup on unload +✅ Code organization with partial classes +✅ All menu patterns you'll need + +**Start here:** Read [`CS2-SimpleAdmin_FunCommands/README.md`](./CS2-SimpleAdmin_FunCommands/README.md) + +### Step 2: Create Your Module Structure + +``` +YourModule/ +├── YourModule.csproj # Project file +├── YourModule.cs # Main plugin class +├── Commands.cs # Command handlers (optional) +├── Menus.cs # Menu creation (optional) +├── Config.cs # Configuration +└── lang/ # Translations + ├── en.json + ├── pl.json + └── ru.json +``` + +### Step 3: Minimal Working Example + +```csharp +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Capabilities; +using CS2_SimpleAdminApi; + +public class MyModule : BasePlugin +{ + private ICS2_SimpleAdminApi? _api; + private readonly PluginCapability _pluginCapability = new("simpleadmin:api"); + + public override void OnAllPluginsLoaded(bool hotReload) + { + _api = _pluginCapability.Get(); + if (_api == null) + { + Logger.LogError("CS2-SimpleAdmin API not found"); + return; + } + + // Register menus after API is ready + _api.OnSimpleAdminReady += RegisterMenus; + RegisterMenus(); // Fallback for hot reload + } + + private void RegisterMenus() + { + if (_api == null) return; + + // 1. Register a new category + _api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); + + // 2. Register menu items in the category + _api.RegisterMenu("mymodule", "action1", "Action 1", CreateAction1Menu, "@css/generic"); + _api.RegisterMenu("mymodule", "action2", "Action 2", CreateAction2Menu, "@css/kick"); + } + + private object CreateAction1Menu(CCSPlayerController admin) + { + // Create a menu with automatic back button + var menu = _api!.CreateMenuWithBack("Action 1 Menu", "mymodule", admin); + + // Add menu options + _api.AddMenuOption(menu, "Option 1", player => + { + player.PrintToChat("You selected Option 1"); + }); + + _api.AddMenuOption(menu, "Option 2", player => + { + player.PrintToChat("You selected Option 2"); + }); + + return menu; + } + + private object CreateAction2Menu(CCSPlayerController admin) + { + // Use the built-in player selection menu + return _api!.CreateMenuWithPlayers("Select Player", "mymodule", admin, + player => player.IsValid && admin.CanTarget(player), + (adminPlayer, targetPlayer) => + { + adminPlayer.PrintToChat($"You selected {targetPlayer.PlayerName}"); + }); + } + + public override void Unload(bool hotReload) + { + if (_api == null) return; + + // Clean up registered menus + _api.UnregisterMenu("mymodule", "action1"); + _api.UnregisterMenu("mymodule", "action2"); + _api.OnSimpleAdminReady -= RegisterMenus; + } +} +``` + +## 📚 Learning Resources + +### For Beginners + +1. **Start Here:** [`CS2-SimpleAdmin_FunCommands/README.md`](./CS2-SimpleAdmin_FunCommands/README.md) + - Explains every file and pattern + - Shows code organization + - Demonstrates all menu types + +2. **Read the Code:** Study these files in order: + - `Config.cs` - Simple configuration + - `CS2-SimpleAdmin_FunCommands.cs` - Plugin initialization + - `Commands.cs` - Command registration + - `Menus.cs` - Menu creation patterns + +3. **Translations:** [`TRANSLATION_EXAMPLE.md`](./TRANSLATION_EXAMPLE.md) + - How to use module translations + - Per-player language support + - Best practices + +### Key Concepts + +| Concept | What It Does | Example File | +|---------|-------------|--------------| +| **API Capability** | Get access to SimpleAdmin API | `CS2-SimpleAdmin_FunCommands.cs:37` | +| **Command Registration** | Register console commands | `Commands.cs:15-34` | +| **Menu Registration** | Add menus to admin menu | `CS2-SimpleAdmin_FunCommands.cs:130-141` | +| **Translations** | Per-player language support | `Actions.cs:20-31` | +| **Cleanup** | Unregister on plugin unload | `CS2-SimpleAdmin_FunCommands.cs:63-97` | + +## 🎯 Menu Patterns + +The FunCommands module demonstrates **3 essential menu patterns** you'll use in every module: + +### Pattern 1: Simple Player Selection +**When to use:** Select a player and immediately execute an action + +```csharp +// Example: God Mode menu +private object CreateGodModeMenu(CCSPlayerController admin) +{ + return _api.CreateMenuWithPlayers( + "God Mode", // Title + "yourmodule", // Category ID + admin, // Admin + player => player.IsValid && admin.CanTarget(player), // Filter + (adminPlayer, target) => // Action + { + // Execute action immediately + ToggleGodMode(target); + }); +} +``` + +**See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:21-29` + +### Pattern 2: Nested Menu (Player → Value) +**When to use:** Select a player, then select a value/option for that player + +```csharp +// Example: Set HP menu (player selection) +private object CreateSetHpMenu(CCSPlayerController admin) +{ + var menu = _api.CreateMenuWithBack("Set HP", "yourmodule", admin); + var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p)); + + foreach (var player in players) + { + // AddSubMenu automatically adds back button to submenu + _api.AddSubMenu(menu, player.PlayerName, + p => CreateHpValueMenu(admin, player)); + } + + return menu; +} + +// Example: Set HP menu (value selection) +private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target) +{ + var menu = _api.CreateMenuWithBack($"HP for {target.PlayerName}", "yourmodule", admin); + var values = new[] { 50, 100, 200, 500 }; + + foreach (var hp in values) + { + _api.AddMenuOption(menu, $"{hp} HP", _ => + { + if (target.IsValid) // Always validate! + { + target.PlayerPawn.Value.Health = hp; + } + }); + } + + return menu; +} +``` + +**See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:134-173` + +### Pattern 3: Nested Menu with Complex Data +**When to use:** Need to display more complex options (like weapons with icons, items with descriptions) + +```csharp +// Example: Give Weapon menu +private object CreateGiveWeaponMenu(CCSPlayerController admin) +{ + var menu = _api.CreateMenuWithBack("Give Weapon", "yourmodule", admin); + var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p)); + + foreach (var player in players) + { + _api.AddSubMenu(menu, player.PlayerName, + p => CreateWeaponSelectionMenu(admin, player)); + } + + return menu; +} + +private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target) +{ + var menu = _api.CreateMenuWithBack($"Weapons for {target.PlayerName}", "yourmodule", admin); + + // Use cached data for performance + foreach (var weapon in GetWeaponsCache()) + { + _api.AddMenuOption(menu, weapon.Value.ToString(), _ => + { + if (target.IsValid) + { + target.GiveNamedItem(weapon.Value); + } + }); + } + + return menu; +} +``` + +**See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:67-109` + +## 📋 API Methods Reference + +### 1. Category Management + +#### `RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")` + +Registers a new menu category that appears in the main admin menu. + +**Parameters:** +- `categoryId` - Unique identifier for the category (e.g., "fun", "vip", "economy") +- `categoryName` - Display name shown in menu (e.g., "Fun Commands") +- `permission` - Required permission to see the category (default: "@css/generic") + +**Example:** +```csharp +_api.RegisterMenuCategory("vip", "VIP Features", "@css/vip"); +``` + +### 2. Menu Registration + +#### `RegisterMenu(string categoryId, string menuId, string menuName, Func menuFactory, string? permission = null)` + +Registers a menu item within a category. + +**Parameters:** +- `categoryId` - The category to add this menu to +- `menuId` - Unique identifier for the menu +- `menuName` - Display name in the category menu +- `menuFactory` - Function that creates the menu when selected (receives admin player) +- `permission` - Optional permission required to see this menu item + +**Example:** +```csharp +_api.RegisterMenu("fun", "godmode", "God Mode", CreateGodModeMenu, "@css/cheats"); +``` + +#### `UnregisterMenu(string categoryId, string menuId)` + +Removes a menu item from a category. + +**Example:** +```csharp +_api.UnregisterMenu("fun", "godmode"); +``` + +### 3. Menu Creation + +#### `CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)` + +Creates a menu with an automatic "Back" button that returns to the category menu. + +**Parameters:** +- `title` - Menu title +- `categoryId` - Category this menu belongs to (for back navigation) +- `player` - The admin player viewing the menu + +**Returns:** `object` (MenuBuilder instance) + +**Example:** +```csharp +var menu = _api.CreateMenuWithBack("Weapon Selection", "fun", admin); +``` + +#### `CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin, Func filter, Action onSelect)` + +Creates a menu with a list of players, filtered and with automatic back button. + +**Parameters:** +- `title` - Menu title +- `categoryId` - Category for back navigation +- `admin` - The admin player viewing the menu +- `filter` - Function to filter which players appear in the menu +- `onSelect` - Action to execute when a player is selected (receives admin and target) + +**Returns:** `object` (MenuBuilder instance) + +**Example:** +```csharp +return _api.CreateMenuWithPlayers("Select Player to Kick", "admin", admin, + player => player.IsValid && admin.CanTarget(player), + (adminPlayer, targetPlayer) => + { + // Kick the selected player + Server.ExecuteCommand($"css_kick {targetPlayer.UserId}"); + }); +``` + +### 4. Menu Manipulation + +#### `AddMenuOption(object menu, string name, Action action, bool disabled = false, string? permission = null)` + +Adds a clickable option to a menu. + +**Parameters:** +- `menu` - The menu object (from CreateMenuWithBack) +- `name` - Display name of the option +- `action` - Action to execute when clicked (receives the player who clicked) +- `disabled` - Whether the option is disabled (grayed out) +- `permission` - Optional permission required to see this option + +**Example:** +```csharp +_api.AddMenuOption(menu, "Give AK-47", player => +{ + player.GiveNamedItem("weapon_ak47"); +}, permission: "@css/cheats"); +``` + +#### `AddSubMenu(object menu, string name, Func subMenuFactory, bool disabled = false, string? permission = null)` + +Adds a submenu option that opens another menu. **Automatically adds a back button to the submenu.** + +**Parameters:** +- `menu` - The parent menu +- `name` - Display name of the submenu option +- `subMenuFactory` - Function that creates the submenu (receives the player) +- `disabled` - Whether the option is disabled +- `permission` - Optional permission required + +**Example:** +```csharp +_api.AddSubMenu(menu, "Weapon Category", player => +{ + var weaponMenu = _api.CreateMenuWithBack("Weapons", "fun", player); + _api.AddMenuOption(weaponMenu, "AK-47", p => p.GiveNamedItem("weapon_ak47")); + _api.AddMenuOption(weaponMenu, "AWP", p => p.GiveNamedItem("weapon_awp")); + return weaponMenu; +}); +``` + +#### `OpenMenu(object menu, CCSPlayerController player)` + +Opens a menu for a specific player. + +**Example:** +```csharp +var menu = _api.CreateMenuWithBack("Custom Menu", "fun", player); +_api.AddMenuOption(menu, "Test", p => p.PrintToChat("Test!")); +_api.OpenMenu(menu, player); +``` + +## Advanced Examples + +### Nested Menus with Player Selection + +```csharp +private object CreateGiveWeaponMenu(CCSPlayerController admin) +{ + var menu = _api.CreateMenuWithBack("Give Weapon", "fun", admin); + var players = _api.GetValidPlayers() + .Where(p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p)); + + foreach (var player in players) + { + // Add submenu for each player - automatic back button will be added + _api.AddSubMenu(menu, player.PlayerName, p => CreateWeaponSelectionMenu(admin, player)); + } + + return menu; +} + +private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target) +{ + var weaponMenu = _api.CreateMenuWithBack($"Weapons for {target.PlayerName}", "fun", admin); + + var weapons = new[] { "weapon_ak47", "weapon_m4a1", "weapon_awp", "weapon_deagle" }; + foreach (var weapon in weapons) + { + _api.AddMenuOption(weaponMenu, weapon, _ => + { + if (target.IsValid) + { + target.GiveNamedItem(weapon); + admin.PrintToChat($"Gave {weapon} to {target.PlayerName}"); + } + }); + } + + return weaponMenu; +} +``` + +### Dynamic Menu with Value Selection + +```csharp +private object CreateSetHpMenu(CCSPlayerController admin) +{ + var menu = _api.CreateMenuWithBack("Set HP", "admin", admin); + var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p)); + + foreach (var player in players) + { + _api.AddSubMenu(menu, player.PlayerName, p => CreateHpValueMenu(admin, player)); + } + + return menu; +} + +private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target) +{ + var hpMenu = _api.CreateMenuWithBack($"HP for {target.PlayerName}", "admin", admin); + var hpValues = new[] { 1, 50, 100, 200, 500, 1000 }; + + foreach (var hp in hpValues) + { + _api.AddMenuOption(hpMenu, $"{hp} HP", _ => + { + if (target.IsValid && target.PlayerPawn?.Value != null) + { + target.PlayerPawn.Value.Health = hp; + admin.PrintToChat($"Set {target.PlayerName} HP to {hp}"); + } + }); + } + + return hpMenu; +} +``` + +### Permission-Based Menu Options + +```csharp +private object CreateAdminToolsMenu(CCSPlayerController admin) +{ + var menu = _api.CreateMenuWithBack("Admin Tools", "admin", admin); + + // Only root admins can see this + _api.AddMenuOption(menu, "Dangerous Action", player => + { + player.PrintToChat("Executing dangerous action..."); + }, permission: "@css/root"); + + // All admins can see this + _api.AddMenuOption(menu, "Safe Action", player => + { + player.PrintToChat("Executing safe action..."); + }, permission: "@css/generic"); + + return menu; +} +``` + +## Best Practices + +1. **Always check for API availability** + ```csharp + if (_api == null) return; + ``` + +2. **Validate player state before actions** + ```csharp + if (target.IsValid && target.PlayerPawn?.Value != null) + { + // Safe to perform action + } + ``` + +3. **Use descriptive category and menu IDs** + - Good: `"economy"`, `"vip_features"`, `"fun_commands"` + - Bad: `"cat1"`, `"menu"`, `"test"` + +4. **Clean up on unload** + ```csharp + public override void Unload(bool hotReload) + { + _api?.UnregisterMenu("mymodule", "mymenu"); + _api.OnSimpleAdminReady -= RegisterMenus; + } + ``` + +5. **Use appropriate permissions** + - `@css/generic` - All admins + - `@css/ban` - Admins who can ban + - `@css/kick` - Admins who can kick + - `@css/root` - Root admins only + - Custom permissions from your module + +6. **Handle hot reload** + ```csharp + _api.OnSimpleAdminReady += RegisterMenus; + RegisterMenus(); // Fallback for hot reload case + ``` + +## Automatic Back Button + +The menu system **automatically adds a "Back" button** to all submenus created with: +- `CreateMenuWithBack()` - Returns to the category menu +- `AddSubMenu()` - Returns to the parent menu + +You don't need to manually add back buttons - the system handles navigation automatically! + +## Getting Valid Players + +Use the API helper method to get valid, connected players: + +```csharp +var players = _api.GetValidPlayers(); + +// With filtering +var alivePlayers = _api.GetValidPlayers() + .Where(p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE); + +var targetablePlayers = _api.GetValidPlayers() + .Where(p => admin.CanTarget(p)); +``` + +## Complete Module Example + +See the `CS2-SimpleAdmin_FunCommands` module in the `Modules/` directory for a complete, production-ready example of: +- Category registration +- Multiple menu types +- Nested menus with automatic back buttons +- Player selection menus +- Permission-based access +- Proper cleanup on unload + +## Troubleshooting + +**Q: My category doesn't appear in the admin menu** +- Ensure you're calling `RegisterMenuCategory()` after the API is ready +- Check that the player has the required permission +- Verify the category has at least one menu registered with `RegisterMenu()` + +**Q: Back button doesn't work** +- Make sure you're using `CreateMenuWithBack()` instead of creating menus manually +- The `categoryId` parameter must match the category you registered +- Use `AddSubMenu()` for nested menus - it handles back navigation automatically + +**Q: Menu appears but is empty** +- Check that you're adding options with `AddMenuOption()` or `AddSubMenu()` +- Verify permission checks aren't filtering out all options +- Ensure player validation in filters isn't too restrictive + +**Q: API is null in OnAllPluginsLoaded** +- Wait for the `OnSimpleAdminReady` event instead of immediate registration +- Make sure CS2-SimpleAdmin is loaded before your module diff --git a/Modules/TRANSLATION_EXAMPLE.md b/Modules/TRANSLATION_EXAMPLE.md new file mode 100644 index 0000000..f16799b --- /dev/null +++ b/Modules/TRANSLATION_EXAMPLE.md @@ -0,0 +1,268 @@ +# Module Translation Guide + +> **🎓 New to translations?** This guide shows you how to add multi-language support to your module! + +## Why Use Module Translations? + +When you use SimpleAdmin API's translation system, **each player automatically sees messages in their preferred language**! + +**Example:** +- 🇬🇧 **Player with English:** "Admin gave PlayerName a weapon!" +- 🇵🇱 **Player with Polish:** "Admin dał PlayerName broń!" +- 🇷🇺 **Player with Russian:** "Admin дал PlayerName оружие!" + +**All from ONE line of code!** + +## Quick Start + +### Step 1: Create Your Translation Files + +Create a `lang` folder in your module with translation files for each language: + +``` +YourModule/ +└── lang/ + ├── en.json + ├── pl.json + └── ru.json +``` + +**Example: `lang/en.json`** +```json +{ + "yourmod_admin_action": "{lightred}{0}{default} performed action on {lightred}{1}{default}!" +} +``` + +**Example: `lang/pl.json`** +```json +{ + "yourmod_admin_action": "{lightred}{0}{default} wykonał akcję na {lightred}{1}{default}!" +} +``` + +### Step 2: Use in Your Code + +**✅ RECOMMENDED METHOD:** `ShowAdminActivityLocalized` + +```csharp +// Show activity with per-player language support +var args = new object[] { "CALLER", targetPlayer.PlayerName }; +if (admin == null || !_api.IsAdminSilent(admin)) +{ + if (Localizer != null) + { + // Each player sees this in their language! + _api.ShowAdminActivityLocalized( + Localizer, // Your module's localizer + "yourmod_admin_action", // Translation key + admin.PlayerName, // Caller name + false, // dontPublish + args); // Message arguments + } +} +``` + +That's it! SimpleAdmin handles the rest. + +## Complete Example + +```csharp +using CounterStrikeSharp.API.Core; +using CS2_SimpleAdminApi; + +public partial class MyModule : BasePlugin +{ + private ICS2_SimpleAdminApi? _api; + + private void GiveWeaponToPlayer(CCSPlayerController admin, CCSPlayerController target, string weaponName) + { + // Give the weapon + target.GiveNamedItem(weaponName); + + var callerName = admin.PlayerName; + + // Show activity using module's localizer - each player sees it in their language! + if (admin == null || !_api!.IsAdminSilent(admin)) + { + var args = new object[] { "CALLER", target.PlayerName, weaponName }; + if (Localizer != null) + { + _api!.ShowAdminActivityLocalized(Localizer, "yourmod_admin_give_message", callerName, false, args); + } + } + + // Log the command + _api!.LogCommand(admin, $"css_give {target.PlayerName} {weaponName}"); + } +} +``` + +## 🔑 Important: The "CALLER" Placeholder + +**Always use `"CALLER"` as the first argument** in your translation messages! + +The API automatically replaces `"CALLER"` based on the server's `ShowActivityType` configuration: + +| ShowActivityType | What Players See | +|-----------------|-----------------| +| `1` | Non-admins see "Admin", admins see real name | +| `2+` | Everyone sees real admin name | + +**Example:** +```json +{ + "yourmod_message": "{0} did something to {1}" + ↑ This will be replaced with "Admin" or admin's name +} +``` + +```csharp +var args = new object[] { "CALLER", targetPlayer.PlayerName }; +// ↑ API replaces this automatically +``` + +## 💡 Pro Tips + +### Tip 1: Use a Helper Method + +Create a reusable helper to reduce code duplication: + +```csharp +/// +/// Helper method to show activity and log command +/// Copy this to your module! +/// +private void LogAndShowActivity( + CCSPlayerController? caller, + CCSPlayerController target, + string translationKey, + string baseCommand, + params string[] extraArgs) +{ + var callerName = caller?.PlayerName ?? "Console"; + + // Build args: "CALLER" + target name + any extra args + var args = new List { "CALLER", target.PlayerName }; + args.AddRange(extraArgs); + + // Show activity with per-player language support + if (caller == null || !_api.IsAdminSilent(caller)) + { + if (Localizer != null) + { + _api.ShowAdminActivityLocalized( + Localizer, + translationKey, + callerName, + false, + args.ToArray()); + } + } + + // Build and log command + var logCommand = $"{baseCommand} {target.PlayerName}"; + if (extraArgs.Length > 0) + { + logCommand += $" {string.Join(" ", extraArgs)}"; + } + _api.LogCommand(caller, logCommand); +} +``` + +**Usage:** +```csharp +// Simple action +LogAndShowActivity(admin, target, "yourmod_kick_message", "css_kick"); + +// Action with parameters +LogAndShowActivity(admin, target, "yourmod_hp_message", "css_hp", "100"); +``` + +### Tip 2: Translation Key Naming Convention + +Use a consistent prefix for your module: + +```json +{ + "yourmod_admin_action1": "...", + "yourmod_admin_action2": "...", + "yourmod_error_notarget": "..." +} +``` + +This prevents conflicts with other modules and makes it clear which module owns the translation. + +### Tip 3: Color Formatting + +Use CounterStrikeSharp color tags in your translations: + +```json +{ + "yourmod_message": "{lightred}{0}{default} gave {green}{1}{default} a {yellow}{2}{default}!" +} +``` + +**Available colors:** +- `{default}`, `{white}`, `{darkred}`, `{green}`, `{lightyellow}` +- `{lightblue}`, `{olive}`, `{lime}`, `{red}`, `{purple}` +- `{grey}`, `{yellow}`, `{gold}`, `{silver}`, `{blue}` +- `{darkblue}`, `{bluegrey}`, `{magenta}`, `{lightred}`, `{orange}` + +## 📖 Real Example: Fun Commands Module + +The **[CS2-SimpleAdmin_FunCommands](./CS2-SimpleAdmin_FunCommands/)** module is a perfect reference: + +**Translation files:** `Modules/CS2-SimpleAdmin_FunCommands/lang/` +- Has 13 languages (en, pl, ru, de, fr, es, etc.) +- Shows proper key naming (`fun_admin_*`) +- Demonstrates color usage + +**Code examples:** `Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Actions.cs` +- Lines 20-31: God mode with translations +- Lines 48-59: NoClip with translations +- Lines 76-86: Freeze with translations + +**Helper method:** `Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Commands.cs:274-306` + +## ❌ Common Mistakes + +### Mistake 1: Forgetting "CALLER" +```csharp +// ❌ WRONG +var args = new object[] { admin.PlayerName, target.PlayerName }; + +// ✅ CORRECT +var args = new object[] { "CALLER", target.PlayerName }; +``` + +### Mistake 2: Using SimpleAdmin's Translations +```csharp +// ❌ WRONG - Uses SimpleAdmin's keys +_api.ShowAdminActivity("sa_admin_kick", ...) + +// ✅ CORRECT - Uses YOUR module's keys +_api.ShowAdminActivityLocalized(Localizer, "yourmod_kick", ...) +``` + +### Mistake 3: Not Checking Localizer +```csharp +// ❌ WRONG - Will crash if Localizer is null +_api.ShowAdminActivityLocalized(Localizer, "key", ...) + +// ✅ CORRECT - Check first +if (Localizer != null) +{ + _api.ShowAdminActivityLocalized(Localizer, "key", ...) +} +``` + +## 🔗 See Also + +- **[MODULE_DEVELOPMENT.md](./MODULE_DEVELOPMENT.md)** - Complete module development guide +- **[CS2-SimpleAdmin_FunCommands/README.md](./CS2-SimpleAdmin_FunCommands/README.md)** - Reference implementation +- **[CounterStrikeSharp Localization](https://docs.cssharp.dev/guides/localization/)** - Official CSS localization docs + +--- + +**Need help?** Study the FunCommands module - it demonstrates all these patterns correctly!