diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1b0b19..c2434cd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 10.0.x + dotnet-version: 8.0.x - name: Get Version id: get_version @@ -67,12 +67,28 @@ jobs: - name: Zip Main Build Output run: zip -r CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip ${{ env.OUTPUT_PATH }} + - name: Extract & Zip StatusBlocker Linux + run: | + mkdir -p statusblocker-linux && + tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-linux.tar.gz -C statusblocker-linux && + cd statusblocker-linux && + zip -r ../StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip ./* + + - name: Extract & Zip StatusBlocker Windows + run: | + mkdir -p statusblocker-windows && + tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-windows.tar.gz -C statusblocker-windows && + cd statusblocker-windows && + zip -r ../StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip ./* + - name: Upload all artifacts uses: actions/upload-artifact@v4 with: name: CS2-SimpleAdmin-Build-Artifacts path: | CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip + StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip + StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip publish: needs: build @@ -93,6 +109,8 @@ jobs: with: artifacts: | CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}.zip + StatusBlocker-linux-${{ needs.build.outputs.build_version }}.zip + StatusBlocker-windows-${{ needs.build.outputs.build_version }}.zip name: "CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}" tag: "build-${{ needs.build.outputs.build_version }}" body: | @@ -103,5 +121,8 @@ jobs: After the first launch, configure the plugin using the JSON config file at: addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json + - StatusBlocker: + Place the plugin files directly into the addons directory. + This plugin is a Metamod module for the StealthModule and does not require a subfolder. Remember to restart or reload your game server after installing and configuring the plugins. diff --git a/.gitignore b/.gitignore index 1850b2f..28e335d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,3 @@ Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.sln.DotSetti CS2-SimpleAdmin_BanSoundModule — kopia *.user CLAUDE.md -/Modules/CS2-SimpleAdmin_BanSoundModule -/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN diff --git a/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs b/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs index 868539e..1ef8886 100644 --- a/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs +++ b/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Commands; -using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Entities; @@ -206,14 +205,6 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryName, permission); } - public void RegisterMenuCategory(string categoryId, string categoryNameKey, string permission, object moduleLocalizer) - { - if (moduleLocalizer is not IStringLocalizer localizer) - throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance"); - - Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryNameKey, permission, localizer); - } - public void RegisterMenu(string categoryId, string menuId, string menuName, Func menuFactory, string? permission = null, string? commandName = null) { @@ -272,39 +263,6 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi } } - public void RegisterMenu(string categoryId, string menuId, string menuNameKey, - Func menuFactory, string? permission, string? commandName, object moduleLocalizer) - { - if (moduleLocalizer is not IStringLocalizer localizer) - throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance"); - - Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuNameKey, BuilderFactory, permission, commandName, localizer); - return; - - MenuBuilder BuilderFactory(CCSPlayerController player) - { - var context = new MenuContext(categoryId, menuId, menuNameKey, permission, commandName); - - if (menuFactory(player, context) 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) { @@ -331,30 +289,7 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi public object CreateMenuWithBack(MenuContext context, CCSPlayerController player) { - // Get translated title if module has localizer - string title = context.MenuTitle; - - if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category)) - { - // Check if this specific menu has a localizer - if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer)) - { - using (new WithTemporaryCulture(player.GetLanguage())) - { - title = menuLocalizer[context.MenuTitle] ?? context.MenuTitle; - } - } - // Fallback to category localizer - else if (category.ModuleLocalizer != null) - { - using (new WithTemporaryCulture(player.GetLanguage())) - { - title = category.ModuleLocalizer[context.MenuTitle] ?? context.MenuTitle; - } - } - } - - return CreateMenuWithBack(title, context.CategoryId, player); + return CreateMenuWithBack(context.MenuTitle, context.CategoryId, player); } public List GetValidPlayers() @@ -386,30 +321,7 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi public object CreateMenuWithPlayers(MenuContext context, CCSPlayerController admin, Func filter, Action onSelect) { - // Get translated title if module has localizer - string title = context.MenuTitle; - - if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category)) - { - // Check if this specific menu has a localizer - if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer)) - { - using (new WithTemporaryCulture(admin.GetLanguage())) - { - title = menuLocalizer[context.MenuTitle] ?? context.MenuTitle; - } - } - // Fallback to category localizer - else if (category.ModuleLocalizer != null) - { - using (new WithTemporaryCulture(admin.GetLanguage())) - { - title = category.ModuleLocalizer[context.MenuTitle] ?? context.MenuTitle; - } - } - } - - return CreateMenuWithPlayers(title, context.CategoryId, admin, filter, onSelect); + return CreateMenuWithPlayers(context.MenuTitle, context.CategoryId, admin, filter, onSelect); } public void AddMenuOption(object menu, string name, Action action, bool disabled = false, diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index a2a82ee..d193b3c 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -14,19 +14,20 @@ using MySqlConnector; namespace CS2_SimpleAdmin; -[MinimumApiVersion(369)] +[MinimumApiVersion(300)] public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig { internal static CS2_SimpleAdmin Instance { get; private set; } = new(); public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; - public override string ModuleAuthor => "daffyy"; - public override string ModuleVersion => "1.8.2b"; + public override string ModuleAuthor => "daffyy & Dliix66"; + public override string ModuleVersion => "1.7.8-beta-3"; public override void Load(bool hotReload) { Instance = this; + if (hotReload) { ServerLoaded = false; @@ -46,7 +47,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig p.IsValid && p is { Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToArray()) + foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsHLTV).ToArray()) { if (!player.IsBot) PlayerManager.LoadPlayerData(player, true); @@ -55,12 +56,14 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig(GameData.GetSignature("CBasePlayerController_SetPawn")); SimpleAdminApi = new Api.CS2_SimpleAdminApi(); Capabilities.RegisterPluginCapability(ICS2_SimpleAdminApi.PluginCapability, () => SimpleAdminApi); - + PlayersTimer?.Kill(); PlayersTimer = null; PlayerManager.CheckPlayersTimer(); @@ -81,9 +84,9 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig ReloadAdmins(null)); RegisterEvents(); AddTimer(0.5f, RegisterCommands.InitializeCommands); - AddTimer(3.0f, () => ReloadAdmins(null)); if (!CoreConfig.UnlockConCommands) { @@ -93,11 +96,20 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig - net10.0 + net8.0 CS2_SimpleAdmin enable enable @@ -19,16 +19,16 @@ - + none runtime compile; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + diff --git a/CS2-SimpleAdmin/Commands/basebans.cs b/CS2-SimpleAdmin/Commands/basebans.cs index f0a20ba..761ecbd 100644 --- a/CS2-SimpleAdmin/Commands/basebans.cs +++ b/CS2-SimpleAdmin/Commands/basebans.cs @@ -27,7 +27,7 @@ public partial class CS2_SimpleAdmin var targets = GetTarget(command); if (targets == null) return; - var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToList(); + var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, IsHLTV: false }).ToList(); if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0) { @@ -373,7 +373,7 @@ public partial class CS2_SimpleAdmin var targets = GetTarget(command); if (targets == null) return; - var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.Connected && !player.IsHLTV).ToList(); + var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected && !player.IsHLTV).ToList(); if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0) { diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index 9513cbc..8d95c33 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -501,13 +501,13 @@ public partial class CS2_SimpleAdmin Task.Run(async () => { - await PermissionManager.CreateGroupsJsonFile(); + await PermissionManager.CrateGroupsJsonFile(); await PermissionManager.CreateAdminsJsonFile(); var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json"); var groupsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/groups.json"); - await Server.NextWorldUpdateAsync(() => + await Server.NextWorldUpdateAsync(() => { AddTimer(1, () => { @@ -521,7 +521,7 @@ public partial class CS2_SimpleAdmin _logger?.LogInformation("Loaded admins!"); }); }); - }); + }); //_ = _adminManager.GiveAllGroupsFlags(); //_ = _adminManager.GiveAllFlags(); diff --git a/CS2-SimpleAdmin/Commands/playercommands.cs b/CS2-SimpleAdmin/Commands/playercommands.cs index 2330ccf..698c3f3 100644 --- a/CS2-SimpleAdmin/Commands/playercommands.cs +++ b/CS2-SimpleAdmin/Commands/playercommands.cs @@ -44,7 +44,7 @@ public partial class CS2_SimpleAdmin /// Optional command info for logging. internal static void Slay(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) { - if (!player.IsValid || player.Connected != PlayerConnectedState.Connected) return; + if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) return; if (!caller.CanTarget(player)) return; // Set default caller name if not provided @@ -93,7 +93,7 @@ public partial class CS2_SimpleAdmin playersToTarget.ForEach(player => { - if (player.Connected != PlayerConnectedState.Connected) + if (player.Connected != PlayerConnectedState.PlayerConnected) return; if (caller!.CanTarget(player)) @@ -207,7 +207,7 @@ public partial class CS2_SimpleAdmin internal static void ChangeTeam(CCSPlayerController? caller, CCSPlayerController player, string teamName, CsTeam teamNum, bool kill, CommandInfo? command = null) { // Check if the player is valid and connected - if (!player.IsValid || player.Connected != PlayerConnectedState.Connected) + if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) return; // Ensure the caller can target the player @@ -284,7 +284,7 @@ public partial class CS2_SimpleAdmin playersToTarget.ForEach(player => { // Check if the player is connected and can be targeted - if (player.Connected != PlayerConnectedState.Connected || !caller!.CanTarget(player)) + if (player.Connected != PlayerConnectedState.PlayerConnected || !caller!.CanTarget(player)) return; // Determine message key and arguments for the rename notification @@ -330,7 +330,7 @@ public partial class CS2_SimpleAdmin playersToTarget.ForEach(player => { // Check if the player is connected and can be targeted - if (player.Connected != PlayerConnectedState.Connected || !caller!.CanTarget(player)) + if (player.Connected != PlayerConnectedState.PlayerConnected || !caller!.CanTarget(player)) return; // Determine message key and arguments for the rename notification @@ -379,7 +379,7 @@ public partial class CS2_SimpleAdmin return; destinationPlayer = targets.Players.FirstOrDefault(p => - p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); + p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); if (destinationPlayer == null || !caller.CanTarget(destinationPlayer) || caller.PlayerPawn.Value == null) return; @@ -399,7 +399,7 @@ public partial class CS2_SimpleAdmin return; playersToTeleport = targets.Players - .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) + .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) .ToList(); if (!playersToTeleport.Any()) @@ -476,7 +476,7 @@ public partial class CS2_SimpleAdmin destinationPlayer = caller; playersToTeleport = targets.Players - .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) + .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) .ToList(); } else @@ -486,7 +486,7 @@ public partial class CS2_SimpleAdmin return; destinationPlayer = destination.Players.FirstOrDefault(p => - p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); + p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); if (destinationPlayer == null) return; @@ -497,7 +497,7 @@ public partial class CS2_SimpleAdmin return; playersToTeleport = targets.Players - .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p)) + .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p)) .ToList(); } diff --git a/CS2-SimpleAdmin/Database/IDatabaseProvider.cs b/CS2-SimpleAdmin/Database/IDatabaseProvider.cs index e2a4ae4..25bc129 100644 --- a/CS2-SimpleAdmin/Database/IDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/IDatabaseProvider.cs @@ -12,9 +12,6 @@ public interface IDatabaseProvider string GetBanSelectQuery(bool multiServer); string GetIpHistoryQuery(); string GetBanUpdateQuery(bool multiServer); - - // PlayerManager - string GetUpsertPlayerIpQuery(); // PermissionManager string GetAdminsQuery(); @@ -40,7 +37,6 @@ public interface IDatabaseProvider string GetUpdateBanStatusQuery(); string GetExpireBansQuery(bool multiServer); string GetExpireIpBansQuery(bool multiServer); - string GetExpireOldPlayerIpsQuery(); // MuteManager string GetAddMuteQuery(bool includePlayerName); diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/016_OptimizeTablesAndIndexes.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/016_OptimizeTablesAndIndexes.sql index a171504..c674d89 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Mysql/016_OptimizeTablesAndIndexes.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/016_OptimizeTablesAndIndexes.sql @@ -1,33 +1,33 @@ --- -- Migration 016: Optimize tables and indexes --- -- Add proper indexes for all tables to improve query performance +-- 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_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_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_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_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`); +-- 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`); +-- 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 e9ba400..33e57f2 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS `sa_players_ips` ( `steamid` INTEGER NOT NULL, - `address` INTEGER NOT NULL, + `address` INTEGER NOT NULL `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/013_AddNameColumnToSaPlayerIpsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/013_AddNameColumnToSaPlayerIpsTable.sql index 02185a2..87a80aa 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Sqlite/013_AddNameColumnToSaPlayerIpsTable.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/013_AddNameColumnToSaPlayerIpsTable.sql @@ -1,4 +1,4 @@ -DELETE FROM sa_players_ips; +TRUNCATE TABLE `sa_players_ips`; ALTER TABLE `sa_players_ips` ADD `name` VARCHAR(64) NULL DEFAULT NULL; CREATE INDEX IF NOT EXISTS `idx_sa_players_ips_used_at` ON `sa_players_ips` (`used_at` DESC); \ 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 index e69de29..749229a 100644 --- a/CS2-SimpleAdmin/Database/Migrations/Sqlite/016_OptimizeTablesAndIndexes.sql +++ 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 fa9a27e..9e79d93 100644 --- a/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs @@ -15,7 +15,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';"; await cmd.ExecuteNonQueryAsync(); - // cmd.CommandText = "SET time_zone = '+00:00';"; + cmd.CommandText = "SET time_zone = '+00:00';"; await cmd.ExecuteNonQueryAsync(); return connection; @@ -86,17 +86,6 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider return "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC"; } - public string GetUpsertPlayerIpQuery() - { - return """ - 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; - """; - } - public string GetBanUpdateQuery(bool multiServer) { return multiServer ? """ @@ -251,11 +240,6 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider ? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime" : "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid"; } - - public string GetExpireOldPlayerIpsQuery() - { - return "DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime"; - } public string GetAddMuteQuery(bool includePlayerName) => includePlayerName diff --git a/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs b/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs index d6186fb..bd63d7b 100644 --- a/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs @@ -83,17 +83,6 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider public string GetIpHistoryQuery() => "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC"; - public string GetUpsertPlayerIpQuery() - { - return """ - INSERT INTO sa_players_ips (steamid, name, address, used_at) - VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP) - ON CONFLICT(steamid, address) DO UPDATE SET - used_at = CURRENT_TIMESTAMP, - name = @playerName; - """; - } - public string GetBanUpdateQuery(bool multiServer) => multiServer ? """ @@ -166,9 +155,6 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider multiServer ? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime" : "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid"; - - public string GetExpireOldPlayerIpsQuery() => - "DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime"; public string GetAdminsQuery() => """ diff --git a/CS2-SimpleAdmin/Events.cs b/CS2-SimpleAdmin/Events.cs index 8d0149a..4a3755a 100644 --- a/CS2-SimpleAdmin/Events.cs +++ b/CS2-SimpleAdmin/Events.cs @@ -23,7 +23,7 @@ public partial class CS2_SimpleAdmin { RegisterListener(OnMapStart); // RegisterListener(OnClientConnect); - // RegisterListener(OnClientConnect); + RegisterListener(OnClientConnect); RegisterListener(OnClientConnected); RegisterListener(OnGameServerSteamAPIActivated); if (Config.OtherSettings.UserMessageGagChatType) @@ -77,7 +77,7 @@ public partial class CS2_SimpleAdmin new ServerManager().LoadServerData(); } - [GameEventHandler] + [GameEventHandler(HookMode.Pre)] public HookResult OnClientDisconnect(EventPlayerDisconnect @event, GameEventInfo info) { if (@event.Reason is 149 or 6) @@ -91,15 +91,14 @@ public partial class CS2_SimpleAdmin if (player == null || !player.IsValid || player.IsHLTV) return HookResult.Continue; - - CachedPlayers.Remove(player); + BotPlayers.Remove(player); + CachedPlayers.Remove(player); + SilentPlayers.Remove(player.Slot); if (player.IsBot) - { return HookResult.Continue; - } #if DEBUG Logger.LogCritical("[OnClientDisconnect] After Check"); @@ -177,9 +176,6 @@ public partial class CS2_SimpleAdmin if (player == null || !player.IsValid || player.IsBot) return; - if (!CachedPlayers.Contains(player)) - CachedPlayers.Add(player); - PlayerManager.LoadPlayerData(player); } @@ -253,13 +249,13 @@ public partial class CS2_SimpleAdmin player.DiePosition = null; } - AddTimer(0.5f, () => + AddTimer(0.41f, () => { foreach (var list in RenamedPlayers) { var player = Utilities.GetPlayerFromSteamId(list.Key); - if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.Connected) + if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) continue; if (player.PlayerName.Equals(list.Value)) @@ -328,7 +324,7 @@ public partial class CS2_SimpleAdmin ? Utilities.GetPlayerFromUserid(userId) : null; - if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.Connected) + if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.PlayerConnected) return HookResult.Continue; return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue; @@ -448,13 +444,13 @@ public partial class CS2_SimpleAdmin private void OnMapStart(string mapName) { + if (!ServerLoaded || ServerId == null) + AddTimer(2.0f, OnGameServerSteamAPIActivated); + if (Config.OtherSettings.ReloadAdminsEveryMapChange && ServerLoaded && ServerId != null) - ReloadAdmins(null); + AddTimer(5.0f, () => ReloadAdmins(null)); AddTimer(1.0f, ServerManager.CheckHibernationStatus); - - if (!ServerLoaded || ServerId == null) - AddTimer(1.5f, OnGameServerSteamAPIActivated); // AddTimer(34, () => // { @@ -462,18 +458,35 @@ public partial class CS2_SimpleAdmin // OnGameServerSteamAPIActivated(); // }); + GodPlayers.Clear(); SilentPlayers.Clear(); PlayerPenaltyManager.RemoveAllPenalties(); } + [GameEventHandler] + public HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info) + { + var player = @event.Userid; + + if (player is null || @event.Attacker is null || player.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE || player.PlayerPawn.Value == null) + return HookResult.Continue; + + if (!GodPlayers.Contains(player.Slot)) return HookResult.Continue; + + player.PlayerPawn.Value.Health = player.PlayerPawn.Value.MaxHealth; + player.PlayerPawn.Value.ArmorValue = 100; + + return HookResult.Continue; + } + [GameEventHandler] public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info) { var player = @event.Userid; if (player?.UserId == null || !player.IsValid || player.IsHLTV || - player.Connected != PlayerConnectedState.Connected || !PlayersInfo.ContainsKey(player.SteamID) || + player.Connected != PlayerConnectedState.PlayerConnected || !PlayersInfo.ContainsKey(player.SteamID) || @event.Attacker == null) return HookResult.Continue; @@ -499,13 +512,17 @@ public partial class CS2_SimpleAdmin public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info) { var player = @event.Userid; - if (player == null || !player.IsValid || player.IsBot || !SilentPlayers.Contains(player.Slot)) + if (player == null || !player.IsValid || player.IsBot) return HookResult.Continue; - if (@event is not { Oldteam: <= 1, Team: >= 1 }) return HookResult.Continue; - - SilentPlayers.Remove(player.Slot); - SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false); + if (!SilentPlayers.Contains(player.Slot)) + return HookResult.Continue; + + if (@event is { Oldteam: <= 1, Team: >= 1 }) + { + SilentPlayers.Remove(player.Slot); + SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false); + } return HookResult.Continue; } diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index aff807d..089e083 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -11,14 +11,12 @@ using CounterStrikeSharp.API.ValveConstants.Protobuf; using CS2_SimpleAdminApi; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Text.Json; -using System.Text.Json.Nodes; using System.Text.RegularExpressions; using CounterStrikeSharp.API.Core.Plugin.Host; using CounterStrikeSharp.API.Modules.Entities.Constants; @@ -66,7 +64,7 @@ internal static class Helper public static List GetValidPlayers() { - return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().Where(p => p.IsValid && p.Connected == PlayerConnectedState.Connected).ToList(); + return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().ToList(); } public static List GetValidPlayersWithBots() @@ -428,7 +426,7 @@ internal static class Helper var communityUrl = caller != null ? "<" + new SteamID(caller.SteamID).ToCommunityUrl() + ">" : ""; var callerName = caller != null ? caller.PlayerName : CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console"; - _ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command.GetCommandString])); + _ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(Helper.GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command.GetCommandString])); } private static void SendDiscordLogMessage(CCSPlayerController? caller, string command, IStringLocalizer? localizer) @@ -585,32 +583,27 @@ internal static class Helper } public static void DisplayCenterMessage( - CCSPlayerController player, - string messageKey, - string? callerName = null, + CCSPlayerController player, + string messageKey, + string? callerName = null, params object[] messageArgs) { if (CS2_SimpleAdmin._localizer == null) return; - // Determine the localized message key var localizedMessageKey = $"{messageKey}"; var formattedMessageArgs = messageArgs.Select(arg => arg?.ToString() ?? string.Empty).ToArray(); - // Replace placeholder based on showActivityType for (var i = 0; i < formattedMessageArgs.Length; i++) { - var arg = formattedMessageArgs[i]; // Convert argument to string if not null - // Replace "CALLER" placeholder in the argument string + var arg = formattedMessageArgs[i]; formattedMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch { 1 => arg.Replace("CALLER", CS2_SimpleAdmin._localizer["sa_admin"]), - 2 => arg.Replace("CALLER", callerName ?? "Console"), _ => arg }; } - // Print the localized message to the center of the screen for the player using (new WithTemporaryCulture(player.GetLanguage())) { player.PrintToCenter(CS2_SimpleAdmin._localizer[localizedMessageKey, formattedMessageArgs.Cast().ToArray()]); @@ -857,34 +850,26 @@ internal static class Helper } } - public static void UpdateConfig(BasePluginConfig config) + public static void UpdateConfig(T config) where T : BasePluginConfig, new() { // get newest config version - var configType = config.GetType(); - var newCfgVersion = ((BasePluginConfig)Activator.CreateInstance(configType)!).Version; + var newCfgVersion = new T().Version; // loaded config is up to date if (config.Version == newCfgVersion) return; - // Load existing JSON file and update version property - if (!File.Exists(CfgPath)) - return; + // update the version + config.Version = newCfgVersion; - var json = File.ReadAllText(CfgPath); - var node = JsonNode.Parse(json); - - if (node != null) - { - node["Version"] = newCfgVersion; - var updatedJsonContent = node.ToJsonString(new JsonSerializerOptions + // serialize the updated config back to json + var updatedJsonContent = JsonSerializer.Serialize(config, + new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); - - File.WriteAllText(CfgPath, updatedJsonContent); - } + File.WriteAllText(CfgPath, updatedJsonContent); } public static void TryLogCommandOnDiscord(CCSPlayerController? caller, string commandString) @@ -1011,10 +996,6 @@ public static class PluginInfo Console.WriteLine("|_______||___| |_| |_||___| |_______||_______||__| |__||______| |_| |_||___| |_| |__|"); Console.WriteLine(" >> Version: " + moduleVersion); Console.WriteLine(" >> GitHub: https://github.com/daffyyyy/CS2-SimpleAdmin"); - var _m1 = new List { 70,117,99,107,32,121,111,117,32,114,97,118,105,100,44,32,119,104,121,32,121,111,117,32,108,121,105,110,103,32,97,98,111,117,116,32,110,111,32,99,111,110,116,97,99,116,32,119,105,116,104,32,109,101,63,32,74,117,115,116,32,116,101,108,108,32,116,114,117,116,104,44,32,110,105,99,101,32,116,114,121,32,116,111,32,112,114,111,106,101,99,116,32,116,97,107,101,111,118,101,114 }; - var _m2 = new List { 32,32,58,68,32,40,97,108,108,32,119,104,111,32,116,114,105,101,100,32,116,111,32,99,111,110,116,97,99,116,32,119,105,116,104,32,109,101,44,32,106,117,115,116,32,119,114,105,116,101,32,109,101,115,115,97,103,101,32,116,111,32,109,101,32,120,68,41 }; - Console.WriteLine(" >> " + new string(_m1.Select(c => (char)c).ToArray())); - Console.WriteLine(" >> " + new string(_m2.Select(c => (char)c).ToArray())); Console.WriteLine(" "); } } @@ -1045,9 +1026,7 @@ public static class Time { public static DateTime ActualDateTime() { - if (CS2_SimpleAdmin.Instance.Config.DatabaseConfig.DatabaseType.ToLower().Equals("sqlite")) - return DateTime.UtcNow; - + return DateTime.UtcNow; string timezoneId = CS2_SimpleAdmin.Instance.Config.Timezone; DateTime utcNow = DateTime.UtcNow; diff --git a/CS2-SimpleAdmin/Managers/BanManager.cs b/CS2-SimpleAdmin/Managers/BanManager.cs index 5863b59..2aac526 100644 --- a/CS2-SimpleAdmin/Managers/BanManager.cs +++ b/CS2-SimpleAdmin/Managers/BanManager.cs @@ -431,9 +431,6 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans); sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId }); - - sql = databaseProvider.GetExpireOldPlayerIpsQuery(); - await connection.ExecuteAsync(sql, new { ipBansTime }); } } catch (Exception) diff --git a/CS2-SimpleAdmin/Managers/CacheManager.cs b/CS2-SimpleAdmin/Managers/CacheManager.cs index d02e036..4e580c6 100644 --- a/CS2-SimpleAdmin/Managers/CacheManager.cs +++ b/CS2-SimpleAdmin/Managers/CacheManager.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using CS2_SimpleAdmin.Database; using CS2_SimpleAdmin.Models; using Dapper; -using Microsoft.Extensions.Logging; using ZLinq; namespace CS2_SimpleAdmin.Managers; @@ -17,7 +16,6 @@ internal class CacheManager: IDisposable private HashSet _cachedIgnoredIps = []; private DateTime _lastUpdateTime = DateTime.MinValue; - private DateTime? _lastDatabaseTime = null; // Track actual time from database private bool _isInitialized; private bool _disposed; @@ -73,7 +71,7 @@ internal class CacheManager: IDisposable if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) { // Optimization: Load IP history and build cache in single pass - var ipHistory = await connection.QueryAsync( + 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"); var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; @@ -84,24 +82,24 @@ internal class CacheManager: IDisposable foreach (var record in ipHistory) { // When we encounter a new steamid, save the previous one - if ((ulong)record.Steamid != currentSteamId && currentSteamId != 0) + if (record.steamid != currentSteamId && currentSteamId != 0) { _playerIpsCache[currentSteamId] = currentIpSet; currentIpSet = new HashSet(new IpRecordComparer()); latestIpTimestamps.Clear(); } - currentSteamId = (ulong)record.Steamid; + currentSteamId = record.steamid; // Only keep the latest timestamp for each IP - if (!latestIpTimestamps.TryGetValue(record.Address, out var existingTimestamp) || - record.Used_at > existingTimestamp) + if (!latestIpTimestamps.TryGetValue(record.address, out var existingTimestamp) || + record.used_at > existingTimestamp) { - latestIpTimestamps[record.Address] = record.Used_at; + latestIpTimestamps[record.address] = record.used_at; currentIpSet.Add(new IpRecord( - record.Address, - record.Used_at, - string.IsNullOrEmpty(record.Name) ? unknownName : record.Name + record.address, + record.used_at, + string.IsNullOrEmpty(record.name) ? unknownName : record.name )); } } @@ -158,20 +156,13 @@ internal class CacheManager: IDisposable await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); IEnumerable updatedBans; - // Get current time from database in local timezone (CURRENT_TIMESTAMP uses session timezone, not UTC) - var currentDatabaseTime = await connection.QueryFirstAsync("SELECT CURRENT_TIMESTAMP"); - // 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) { - // Use previous database time or start from far past if first run - var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue; - - // Get recently updated bans by timestamp (using database time to avoid timezone issues) - var updatedBans_Query = (await connection.QueryAsync( + updatedBans = (await connection.QueryAsync( """ SELECT id AS Id, player_name AS PlayerName, @@ -180,68 +171,33 @@ internal class CacheManager: IDisposable status AS Status FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC """, - new { lastUpdate = lastCheckTime } - )).ToList(); - - // Detect changes: new bans or status changes - var updatedList = new List(); - foreach (var ban in updatedBans_Query) - { - if (!_banCache.TryGetValue(ban.Id, out var cachedBan)) - { - // New ban - updatedList.Add(ban); - } - else if (cachedBan.Status != ban.Status) - { - // Status changed - updatedList.Add(ban); - } - } + 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; - - // Update last check time to current database time - _lastDatabaseTime = currentDatabaseTime; } else { - // Use previous database time or start from far past if first run - var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue; - - // Get recently updated bans for this server by timestamp (using database time to avoid timezone issues) - var updatedBans_Query = (await connection.QueryAsync( + updatedBans = (await connection.QueryAsync( """ SELECT id AS Id, player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status - FROM `sa_bans` WHERE server_id = @serverId AND (updated_at > @lastUpdate OR created > @lastUpdate) ORDER BY updated_at DESC + FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC """, - new { serverId = CS2_SimpleAdmin.ServerId, lastUpdate = lastCheckTime } - )).ToList(); - - // Detect changes: new bans or status changes - var updatedList = new List(); - foreach (var ban in updatedBans_Query) - { - if (!_banCache.TryGetValue(ban.Id, out var cachedBan)) - { - // New ban - updatedList.Add(ban); - } - else if (cachedBan.Status != ban.Status) - { - // Status changed - updatedList.Add(ban); - } - } + new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId } + )); + // Optimization: Only fetch all IDs if there were updates + var updatedList = updatedBans.ToList(); if (updatedList.Count > 0) { allIds = (await connection.QueryAsync( @@ -250,9 +206,6 @@ internal class CacheManager: IDisposable )).ToHashSet(); } updatedBans = updatedList; - - // Update last check time to current database time - _lastDatabaseTime = currentDatabaseTime; } // Optimization: Only process deletions if we have the full ID list @@ -272,34 +225,34 @@ internal class CacheManager: IDisposable 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 _); + !_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue; + { + ipBans.RemoveAll(b => b.Id == id); + if (ipBans.Count == 0) + _ipIndex.TryRemove(ipUInt, out _); + } } } if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) { - var ipHistory = (await connection.QueryAsync( + var ipHistory = (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>( "SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300", new { lastUpdate = _lastUpdateTime })); - foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid)) + foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid)) { var ipSet = new HashSet( group - .GroupBy(x => x.Address) + .GroupBy(x => x.address) .Select(g => { - var latest = g.MaxBy(x => x.Used_at); + var latest = g.MaxBy(x => x.used_at); return new IpRecord( g.Key, - latest.Used_at, - !string.IsNullOrEmpty(latest.Name) - ? latest.Name + latest.used_at, + !string.IsNullOrEmpty(latest.name) + ? latest.name : CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown" ); }), @@ -307,7 +260,7 @@ internal class CacheManager: IDisposable ); _playerIpsCache.AddOrUpdate( - (ulong)group.Key, + group.Key, _ => ipSet, (_, existingSet) => { @@ -323,19 +276,16 @@ internal class CacheManager: IDisposable } // Update cache with new/modified bans - var needsRebuild = false; + var hasUpdates = false; foreach (var ban in updatedBans) { - if (_banCache.TryGetValue(ban.Id, out var oldBan) && oldBan.Status != ban.Status) - { - // Ban status changed (e.g., ACTIVE -> EXPIRED/UNBANNED), need to rebuild indexes - needsRebuild = true; - } _banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban); + hasUpdates = true; } - // Rebuild indexes if there were updates or status changes - if (updatedBans.Any() || needsRebuild) + // Always rebuild indexes if there were any updates + // This ensures status changes (ACTIVE -> UNBANNED) are reflected + if (hasUpdates) { RebuildIndexes(); } @@ -486,41 +436,32 @@ internal class CacheManager: IDisposable record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); if (record != null) { - // Double-check the ban is still active in cache (handle race conditions) - if (_banCache.TryGetValue(record.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE) + if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) || + (!record.PlayerSteamId.HasValue)) { - if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) || - (!record.PlayerSteamId.HasValue)) - { - _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); - } - - return true; + _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); } + + return true; } } - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress)) + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false; if (string.IsNullOrEmpty(ipAddress) || !IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) || _cachedIgnoredIps.Contains(ipUInt) || !_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false; - + record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); if (record == null) return false; - - // Double-check the ban is still active in cache (handle race conditions) - if (!_banCache.TryGetValue(record.Id, out var cachedBanIp) || cachedBanIp.StatusEnum != BanStatus.ACTIVE) - return false; - if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) || (!record.PlayerSteamId.HasValue && steamId.HasValue)) { _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); } - + return true; } @@ -606,100 +547,52 @@ internal class CacheManager: IDisposable var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE); if (activeBan != null) { - // Double-check the ban is still active in cache (handle race conditions) - if (_banCache.TryGetValue(activeBan.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE) - { - if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) - _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); - - return true; - } - } - } - - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress)) - return false; - - if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)) - return false; - - if (_cachedIgnoredIps.Contains(ipUInt)) - return false; - - // Direct ip ban (ban record has player_ip set) - if (_ipIndex.TryGetValue(ipUInt, out var ipBanRecords)) - { - var ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); - if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out var cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE) - { - var expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans; - if (expireOldIpBans <= 0 || ipBan.Created >= Time.ActualDateTime().AddDays(-expireOldIpBans)) - { - if (string.IsNullOrEmpty(ipBan.PlayerName)) - ipBan.PlayerName = playerName; - ipBan.PlayerSteamId ??= steamId; + if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp)) _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); - return true; - } - } - } - - // Multiaccount ban - check if other accounts using current ip are banned - if (!_playerIpsCache.IsEmpty) - { - foreach (var (otherSteamId, ipSet) in _playerIpsCache) - { - // Skip current player - if (otherSteamId == steamId) - continue; - - // Check if this ip is in the other accounts ip history - if (ipSet.All(record => record.Ip != ipUInt)) continue; - // Found another account using this ip - check if its banned - if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans)) continue; - var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE); - if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) || - cachedBan.StatusEnum != BanStatus.ACTIVE) continue; - _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); + return true; } } - // Multiaccount ban - check if this player used any ip where other banned accounts are connected - // Search sa_players_ips for all accounts sharing the same ips as current player - if (!CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false; - if (!_playerIpsCache.TryGetValue(steamId, out var playerIps)) + if (!_playerIpsCache.TryGetValue(steamId, out var ipData)) return false; - // For each ip the player used (current or historical) - foreach (var playerIpRecord in playerIps) + var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans); + var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; + + if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint)) { - // Search sa_players_ips for other accounts using this same ip (as uint) - foreach (var (otherSteamId, otherIpSet) in _playerIpsCache) + if (!_cachedIgnoredIps.Contains(ipAsUint)) { - if (otherSteamId == steamId) - continue; - - // Check if this other account used the player ip - if (otherIpSet.All(record => record.Ip != playerIpRecord.Ip)) - continue; - - // Check if this other account is banned - if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans)) - continue; - - var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE); - if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) || - cachedBan.StatusEnum != BanStatus.ACTIVE) - continue; - - _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); - return true; + ipData.Add(new IpRecord(ipAsUint, Time.ActualDateTime().AddSeconds(-2), unknownName)); } } + foreach (var ipRecord in ipData) + { + if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip)) + continue; + + if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords)) + continue; + + var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); + if (activeBan == null) + continue; + + if (string.IsNullOrEmpty(activeBan.PlayerName)) + activeBan.PlayerName = unknownName; + + activeBan.PlayerSteamId ??= steamId; + + _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); + + return true; + } + return false; } diff --git a/CS2-SimpleAdmin/Managers/PermissionManager.cs b/CS2-SimpleAdmin/Managers/PermissionManager.cs index 5826e74..c9f2b41 100644 --- a/CS2-SimpleAdmin/Managers/PermissionManager.cs +++ b/CS2-SimpleAdmin/Managers/PermissionManager.cs @@ -1,4 +1,4 @@ -using CounterStrikeSharp.API; +using CounterStrikeSharp.API; using CounterStrikeSharp.API.Modules.Entities; using Dapper; using Microsoft.Extensions.Logging; @@ -12,8 +12,54 @@ namespace CS2_SimpleAdmin.Managers; public class PermissionManager(IDatabaseProvider? databaseProvider) { + // Unused for now + //public static readonly ConcurrentDictionary> _adminCache = new ConcurrentDictionary>(); + // public static readonly ConcurrentDictionary AdminCache = new(); public static readonly ConcurrentDictionary Flags)> AdminCache = new(); + /* + public async Task, int)>> GetAdminFlags(string steamId) + { + DateTime now = Time.ActualDateTime(); + + await using MySqlConnection connection = await _database.GetConnectionAsync(); + + string sql = "SELECT flags, immunity, ends FROM sa_admins WHERE player_steamid = @PlayerSteamID AND (ends IS NULL OR ends > @CurrentTime) AND (server_id IS NULL OR server_id = @serverid)"; + List? activeFlags = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId }))?.ToList(); + + if (activeFlags == null) + { + return new List<(List, int)>(); + } + + List<(List, int)> filteredFlagsWithImmunity = []; + + foreach (dynamic flags in activeFlags) + { + if (flags is not IDictionary flagsDict) + { + continue; + } + + if (!flagsDict.TryGetValue("flags", out var flagsValueObj) || !flagsDict.TryGetValue("immunity", out var immunityValueObj)) + { + continue; + } + + if (!(flagsValueObj is string flagsValue) || !int.TryParse(immunityValueObj.ToString(), out var immunityValue)) + { + continue; + } + + //Console.WriteLine($"Flags: {flagsValue}, Immunity: {immunityValue}"); + + filteredFlagsWithImmunity.Add((flagsValue.Split(',').ToList(), immunityValue)); + } + + return filteredFlagsWithImmunity; + } + */ + /// /// Retrieves all players' flags and associated data asynchronously. /// @@ -54,7 +100,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) string playerName = g.Key.playerName as string ?? string.Empty; - // Dapper returns string here, not dynamic + // tutaj zakładamy, że Dapper zwraca już string (nie dynamic) var flags = g.Select(r => r.flag as string ?? string.Empty) .Distinct() .ToList(); @@ -72,6 +118,72 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) } } + + /* + public async Task, List>, int>>> GetAllGroupsFlags() + { + try + { + await using MySqlConnection connection = await _database.GetConnectionAsync(); + + string sql = "SELECT group_id FROM sa_groups_servers WHERE server_id = @serverid"; + var groupIds = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList(); + + sql = @" + SELECT g.group_id, f.flag + FROM sa_groups_flags f + JOIN sa_groups_servers g ON f.group_id = g.group_id + WHERE g.server_id = @serverid"; + + var groupFlagData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList(); + + if (groupIds.Count == 0 || groupFlagData.Count == 0) + { + return []; + } + + var groupInfoDictionary = new Dictionary, List>, int>>(); + + foreach (var groupId in groupIds) + { + groupInfoDictionary[groupId] = new Tuple, List>, int>([], [], 0); + } + + foreach (var row in groupFlagData) + { + var groupId = (int)row.group_id; + var flag = (string)row.flag; + + groupInfoDictionary[groupId].Item1.Add(flag); + } + + sql = @" + SELECT a.group_id, a.player_steamid, a.ends, g.immunity, g.name + FROM sa_admins a + JOIN sa_groups g ON a.group_id = g.id + WHERE a.group_id IN @groupIds"; + + var playerData = (await connection.QueryAsync(sql, new { groupIds })).ToList(); + + foreach (var row in playerData) + { + var groupId = (int)row.group_id; + var playerSteamid = (string)row.player_steamid; + var ends = row.ends as DateTime?; + var immunity = (int)row.immunity; + + groupInfoDictionary[groupId].Item2.Add(new Tuple(playerSteamid, ends)); + groupInfoDictionary[groupId] = new Tuple, List>, int>(groupInfoDictionary[groupId].Item1, groupInfoDictionary[groupId].Item2, immunity); + } + + return groupInfoDictionary; + } + catch { } + + return []; + } + */ + /// /// Retrieves all groups' data including flags and immunity asynchronously. /// @@ -80,9 +192,13 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) { if (databaseProvider == null) return []; + await using var connection = await databaseProvider.CreateConnectionAsync(); + ; try { - await using var connection = await databaseProvider.CreateConnectionAsync(); + // var sql = "SELECT group_id FROM sa_groups_servers WHERE (server_id = @serverid OR server_id IS NULL)"; + // var groupDataSql = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList(); + var sql = databaseProvider.GetGroupsQuery(); var groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList(); if (groupData.Count == 0) @@ -97,9 +213,11 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) var flag = (string)row.flag; var immunity = (int)row.immunity; + // Check if the group name already exists in the dictionary if (!groupInfoDictionary.TryGetValue(groupName, out (List, int) value)) { value = ([], immunity); + // If it doesn't exist, add a new entry with an empty list of flags and immunity groupInfoDictionary[groupName] = value; } @@ -120,7 +238,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) /// Creates a JSON file containing groups data asynchronously. /// [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - public async Task CreateGroupsJsonFile() + public async Task CrateGroupsJsonFile() { var groupsData = await GetAllGroupsData(); var jsonData = new Dictionary(); @@ -139,7 +257,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) var options = new JsonSerializerOptions { WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase + PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var json = JsonSerializer.Serialize(jsonData, options); @@ -147,6 +265,66 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) await File.WriteAllTextAsync(filePath, json); } + /* + public async Task GiveAllGroupsFlags() + { + Dictionary, List>, int>> groupFlags = await GetAllGroupsFlags(); + + foreach (var kvp in groupFlags) + { + var flags = kvp.Value.Item1; + var players = kvp.Value.Item2; + int immunity = kvp.Value.Item3; + + foreach (var playerTuple in players) + { + var steamIdStr = playerTuple.Item1; + var ends = playerTuple.Item2; + + if (!string.IsNullOrEmpty(steamIdStr) && SteamID.TryParse(steamIdStr, out var steamId) && steamId != null) + { + if (!_adminCache.ContainsKey(steamId)) + { + _adminCache.TryAdd(steamId, ends); + } + + Helper.GivePlayerFlags(steamId, flags, (uint)immunity); + // Often need to call 2 times + Helper.GivePlayerFlags(steamId, flags, (uint)immunity); + } + } + } + } + */ + /* + public async Task GiveAllFlags() + { + List<(string, string, List, int, DateTime?)> allPlayers = await GetAllPlayersFlags(); + + foreach (var record in allPlayers) + { + string steamIdStr = record.Item1; + List flags = record.Item2; + int immunity = record.Item3; + + DateTime? ends = record.Item4; + + if (!string.IsNullOrEmpty(steamIdStr) && SteamID.TryParse(steamIdStr, out var steamId) && steamId != null) + { + if (!_adminCache.ContainsKey(steamId)) + { + _adminCache.TryAdd(steamId, ends); + //_adminCacheTimestamps.Add(steamId, ends); + } + + Helper.GivePlayerFlags(steamId, flags, (uint)immunity); + // Often need to call 2 times + Helper.GivePlayerFlags(steamId, flags, (uint)immunity); + } + } + } + */ + /// /// Creates a JSON file containing admins data asynchronously. /// @@ -158,12 +336,25 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) .Where(player => SteamID.TryParse(player.identity.ToString(), out _)) .ToList(); + // foreach (var player in allPlayers) + // { + // var (steamId, name, flags, immunity, ends) = player; + // + // Console.WriteLine($"Player SteamID: {steamId}"); + // Console.WriteLine($"Player Name: {name}"); + // Console.WriteLine($"Flags: {string.Join(", ", flags)}"); + // Console.WriteLine($"Immunity: {immunity}"); + // Console.WriteLine($"Ends: {(ends.HasValue ? ends.Value.ToString("yyyy-MM-dd HH:mm:ss") : "Never")}"); + // Console.WriteLine(); + // } + var jsonData = validPlayers - .GroupBy(player => player.name) + .GroupBy(player => player.name) // Group by player name .ToDictionary( - group => group.Key, + group => group.Key, // Use the player name as key object (group) => { + // Consolidate data for players with same name var consolidatedData = group.Aggregate( new { @@ -174,13 +365,16 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) }, (acc, player) => { + // Merge identities if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString())) { acc = acc with { identity = player.identity.ToString() }; } + // Combine immunities by maximum value acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) }; + // Combine flags and groups acc = acc with { flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(), @@ -189,12 +383,12 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) return acc; }); - + Server.NextWorldUpdate(() => { var keysToRemove = new List(); - foreach (var steamId in AdminCache.Keys.ToList()) + foreach (var steamId in AdminCache.Keys.ToList()) { var data = AdminManager.GetPlayerAdminData(steamId); if (data != null) @@ -228,9 +422,40 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) } }); + // Server.NextFrameAsync(() => + // { + // for (var index = 0; index < AdminCache.Keys.ToList().Count; index++) + // { + // var steamId = AdminCache.Keys.ToList()[index]; + // + // var data = AdminManager.GetPlayerAdminData(steamId); + // if (data != null) + // { + // AdminManager.RemovePlayerPermissions(steamId, AdminCache[steamId].Flags.ToArray()); + // AdminManager.RemovePlayerFromGroup(steamId, true, AdminCache[steamId].Flags.ToArray()); + // } + // + // if (!AdminCache.TryRemove(steamId, out _)) continue; + // + // if (data == null) continue; + // if (data.Flags.ToList().Count != 0 && data.Groups.ToList().Count != 0) + // continue; + // + // AdminManager.ClearPlayerPermissions(steamId); + // AdminManager.RemovePlayerAdminData(steamId); + // } + // + // foreach (var player in group) + // { + // SteamID.TryParse(player.identity, out var steamId); + // if (steamId == null) continue; + // AdminCache.TryAdd(steamId, (player.ends, player.flags)); + // } + // }); + return consolidatedData; }); - + var options = new JsonSerializerOptions { WriteIndented = true, @@ -239,7 +464,10 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) var json = JsonSerializer.Serialize(jsonData, options); var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json"); + await File.WriteAllTextAsync(filePath, json); + + //await File.WriteAllTextAsync(CS2_SimpleAdmin.Instance.ModuleDirectory + "/data/admins.json", json); } /// @@ -252,6 +480,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) if (databaseProvider == null) return; if (string.IsNullOrEmpty(playerSteamId)) return; + //_adminCache.TryRemove(playerSteamId, out _); + try { await using var connection = await databaseProvider.CreateConnectionAsync(); @@ -280,12 +510,18 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return; var now = Time.ActualDateTime(); - DateTime? futureTime = time != 0 ? now.AddMinutes(time) : null; + DateTime? futureTime; + + if (time != 0) + futureTime = now.AddMinutes(time); + else + futureTime = null; try { await using var connection = await databaseProvider.CreateConnectionAsync(); + // Insert admin into sa_admins table var insertAdminSql = databaseProvider.GetAddAdminQuery(); var adminId = await connection.ExecuteScalarAsync(insertAdminSql, new { @@ -297,8 +533,28 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) serverid = globalAdmin ? null : CS2_SimpleAdmin.ServerId }); + // Insert flags into sa_admins_flags table foreach (var flag in flagsList) { + // if (flag.StartsWith($"#")) + // { + // // const string sql = "SELECT id FROM `sa_groups` WHERE name = @groupName"; + // // var groupId = await connection.QuerySingleOrDefaultAsync(sql, new { groupName = flag }); + // + // var sql = databaseProvider.GetGroupIdByNameQuery(); + // var groupId = await connection.QuerySingleOrDefaultAsync(sql, new { groupName = flag, CS2_SimpleAdmin.ServerId }); + // + // if (groupId != null) + // { + // var updateAdminGroup = "UPDATE `sa_admins` SET group_id = @groupId WHERE id = @adminId"; + // await connection.ExecuteAsync(updateAdminGroup, new + // { + // groupId, + // adminId + // }); + // } + // } + var insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery(); await connection.ExecuteAsync(insertFlagsSql, new { @@ -331,10 +587,10 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return; + await using var connection = await databaseProvider.CreateConnectionAsync(); try { - await using var connection = await databaseProvider.CreateConnectionAsync(); - + // Insert group into sa_groups table var insertGroup = databaseProvider.GetAddGroupQuery(); var groupId = await connection.ExecuteScalarAsync(insertGroup, new { @@ -342,9 +598,11 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) immunity }); + // Insert flags into sa_groups_flags table foreach (var flag in flagsList) { var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery(); + await connection.ExecuteAsync(insertFlagsSql, new { groupId, @@ -358,6 +616,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) { CS2_SimpleAdmin.Instance.ReloadAdmins(null); }); + } catch (Exception ex) { @@ -375,9 +634,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) if (string.IsNullOrEmpty(groupName)) return; + await using var connection = await databaseProvider.CreateConnectionAsync(); try { - await using var connection = await databaseProvider.CreateConnectionAsync(); var sql = databaseProvider.GetDeleteGroupQuery(); await connection.ExecuteAsync(sql, new { groupName }); } @@ -406,4 +665,4 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) CS2_SimpleAdmin._logger?.LogCritical("Unable to remove expired admins"); } } -} +} \ No newline at end of file diff --git a/CS2-SimpleAdmin/Managers/PlayerManager.cs b/CS2-SimpleAdmin/Managers/PlayerManager.cs index 1736112..aff7930 100644 --- a/CS2-SimpleAdmin/Managers/PlayerManager.cs +++ b/CS2-SimpleAdmin/Managers/PlayerManager.cs @@ -13,7 +13,7 @@ namespace CS2_SimpleAdmin.Managers; internal class PlayerManager { - private readonly SemaphoreSlim _loadPlayerSemaphore = new(6); + private readonly SemaphoreSlim _loadPlayerSemaphore = new(5); private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config; /// @@ -52,40 +52,81 @@ internal class PlayerManager { await _loadPlayerSemaphore.WaitAsync(); - // Save ip address before ban check - await SavePlayerIpAddress(steamId, playerName, ipAddress); - - // Always check bans first, regardless of PlayersInfo state - var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch + if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId)) { - 0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null), - _ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp - ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, - ipAddress) - : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress) - }; - - CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Player {playerName} ({steamId}) IP: {ipAddress} - BanType: {CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType} - CheckMultiAccounts: {CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp} - isBanned: {isBanned}"); - - if (isBanned) - { - CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] KICKING {playerName} ({steamId})"); - await Server.NextWorldUpdateAsync(() => + var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch { - CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Executing kick for {playerName}"); - Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED); - }); + 0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null), + _ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp + ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, + ipAddress) + : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress) + }; - return; + // CS2_SimpleAdmin._logger?.LogInformation($"Player {playerName} ({steamId} - {ipAddress}) is banned? {isBanned.ToString()}"); + + if (isBanned) + { + await Server.NextWorldUpdateAsync(() => + { + // CS2_SimpleAdmin._logger?.LogInformation($"Kicking {playerName}"); + Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED); + }); + + return; + } } - if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId)) + if (fullConnect) { var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress); CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo; - if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null) + await Server.NextWorldUpdateAsync(() => { + if (!CS2_SimpleAdmin.CachedPlayers.Contains(player)) + CS2_SimpleAdmin.CachedPlayers.Add(player); + }); + + if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null && + CS2_SimpleAdmin.PlayersInfo[steamId] != null) + { + try + { + await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); + + // Eliminates the need for SELECT COUNT and duplicate UPDATE queries + var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64; + var ipUint = 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}"); + } + playerInfo.AccountsAssociated = CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable() .Select(x => (x.SteamId, x.PlayerName)).ToList() ?? []; @@ -172,7 +213,7 @@ internal class PlayerManager AdminManager.PlayerHasPermissions( new SteamID(p.SteamID), "@css/ban")) && - p.Connected == PlayerConnectedState.Connected && + p.Connected == PlayerConnectedState.PlayerConnected && !CS2_SimpleAdmin.AdminDisabledJoinComms .Contains(p.SteamID))) { @@ -219,45 +260,12 @@ internal class PlayerManager _loadPlayerSemaphore.Release(); } }); - if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name)) { player.Rename(name); } } - /// - /// Saves player's IP address to the database for multi-account detection. - /// This is called before ban checks to ensure IP is recorded even if player is banned. - /// - private async Task SavePlayerIpAddress(ulong steamId, string playerName, string? ipAddress) - { - if (!_config.OtherSettings.CheckMultiAccountsByIp || ipAddress == null || CS2_SimpleAdmin.DatabaseProvider == null) - return; - - try - { - await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); - - var steamId64 = steamId; - var ipUint = IpHelper.IpToUint(ipAddress); - - var upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery(); - - await connection.ExecuteAsync(upsertQuery, new - { - SteamID = steamId64, - playerName, - IPAddress = ipUint - }); - } - catch (Exception ex) - { - CS2_SimpleAdmin._logger?.LogError( - $"Unable to save ip address for {playerName} ({ipAddress}): {ex.Message}"); - } - } - /// /// Periodically checks the status of online players and applies timers for speed, gravity, /// and penalty expiration validation. @@ -275,16 +283,6 @@ internal class PlayerManager /// public void CheckPlayersTimer() { - CS2_SimpleAdmin.Instance.AddTimer(5f, () => - { - foreach (var (steamid, name) in CS2_SimpleAdmin.RenamedPlayers) - { - var player = Helper.GetPlayerFromSteamid64(steamid); - if (player == null || !player.IsValid || player.PlayerName == name) continue; - player.Rename(name); - } - }, TimerFlags.REPEAT); - CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () => { #if DEBUG @@ -295,6 +293,9 @@ internal class PlayerManager // 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) @@ -369,12 +370,7 @@ internal class PlayerManager foreach (var player in bannedPlayers) { if (!player.UserId.HasValue) continue; - await Server.NextWorldUpdateAsync(() => - { - if (Helper.GetPlayerFromSteamid64(player.SteamID) != null) - Helper.KickPlayer((int)player.UserId, - NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED); - }); + await Server.NextWorldUpdateAsync(() => Helper.KickPlayer((int)player.UserId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED)); } } diff --git a/CS2-SimpleAdmin/Managers/ServerManager.cs b/CS2-SimpleAdmin/Managers/ServerManager.cs index ec80aaa..e2401bc 100644 --- a/CS2-SimpleAdmin/Managers/ServerManager.cs +++ b/CS2-SimpleAdmin/Managers/ServerManager.cs @@ -31,7 +31,7 @@ public class ServerManager /// public void LoadServerData() { - CS2_SimpleAdmin.Instance.AddTimer(1.0f, () => + CS2_SimpleAdmin.Instance.AddTimer(2.0f, () => { if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return; @@ -103,12 +103,14 @@ public class ServerManager CS2_SimpleAdmin.ServerId = serverId; CS2_SimpleAdmin._logger?.LogInformation("Loaded server with ip {ip}", ipAddress); -CS2_SimpleAdmin.ServerLoaded = true; - - if (CS2_SimpleAdmin.Instance.CacheManager != null) + if (CS2_SimpleAdmin.ServerId != null) { - await CS2_SimpleAdmin.Instance.CacheManager.InitializeCacheAsync(); + await Server.NextWorldUpdateAsync(() => CS2_SimpleAdmin.Instance.ReloadAdmins(null)); } + + CS2_SimpleAdmin.ServerLoaded = true; + if (CS2_SimpleAdmin.Instance.CacheManager != null) + await CS2_SimpleAdmin.Instance.CacheManager.InitializeCacheAsync(); } catch (Exception ex) { diff --git a/CS2-SimpleAdmin/Managers/WarnManager.cs b/CS2-SimpleAdmin/Managers/WarnManager.cs index 7db3d1d..0f58b84 100644 --- a/CS2-SimpleAdmin/Managers/WarnManager.cs +++ b/CS2-SimpleAdmin/Managers/WarnManager.cs @@ -33,7 +33,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider) playerName = player.Name, adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", - warnReason = reason, + muteReason = reason, duration = time, ends = futureTime, created = now, @@ -42,7 +42,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider) return warnId; } - catch(Exception) + catch { return null; } @@ -73,7 +73,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider) playerSteamid = playerSteamId, adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", - warnReason = reason, + muteReason = reason, duration = time, ends = futureTime, created = now, diff --git a/CS2-SimpleAdmin/Menus/BasicMenu.cs b/CS2-SimpleAdmin/Menus/BasicMenu.cs index 1234079..b0fba78 100644 --- a/CS2-SimpleAdmin/Menus/BasicMenu.cs +++ b/CS2-SimpleAdmin/Menus/BasicMenu.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Entities.Constants; @@ -13,33 +12,32 @@ public abstract class BasicMenu { /// /// Initializes all menus in the system by registering them with the MenuManager. - /// Register with translation keys instead of static names - translation happens per-player. /// public static void Initialize() { var manager = MenuManager.Instance; - // Players category menus - using translation keys - manager.RegisterMenu("players", "slap", "sa_slap", CreateSlapMenu, "@css/slay"); - manager.RegisterMenu("players", "slay", "sa_slay", CreateSlayMenu, "@css/slay"); - manager.RegisterMenu("players", "kick", "sa_kick", CreateKickMenu, "@css/kick"); - manager.RegisterMenu("players", "warn", "sa_warn", CreateWarnMenu, "@css/kick"); - manager.RegisterMenu("players", "ban", "sa_ban", CreateBanMenu, "@css/ban"); - manager.RegisterMenu("players", "gag", "sa_gag", CreateGagMenu, "@css/chat"); - manager.RegisterMenu("players", "mute", "sa_mute", CreateMuteMenu, "@css/chat"); - manager.RegisterMenu("players", "silence", "sa_silence", CreateSilenceMenu, "@css/chat"); - manager.RegisterMenu("players", "team", "sa_team_force", CreateForceTeamMenu, "@css/kick"); + // 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 - using translation keys - manager.RegisterMenu("server", "plugins", "sa_menu_pluginsmanager_title", CreatePluginsMenu, "@css/root"); - manager.RegisterMenu("server", "changemap", "sa_changemap", CreateChangeMapMenu, "@css/changemap"); - manager.RegisterMenu("server", "restart", "sa_restart_game", CreateRestartGameMenu, "@css/generic"); - manager.RegisterMenu("server", "custom", "sa_menu_custom_commands", CreateCustomCommandsMenu, "@css/generic"); + // 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 - using translation keys - manager.RegisterMenu("admin", "add", "sa_admin_add", CreateAddAdminMenu, "@css/root"); - manager.RegisterMenu("admin", "remove", "sa_admin_remove", CreateRemoveAdminMenu, "@css/root"); - manager.RegisterMenu("admin", "reload", "sa_admin_reload", CreateReloadAdminsMenu, "@css/root"); + // 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"); } /// @@ -51,15 +49,14 @@ public abstract class BasicMenu private static MenuBuilder CreateSlapMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var slapMenu = new MenuBuilder("sa_slap", admin, localizer); + var slapMenu = new MenuBuilder(localizer?["sa_slap"] ?? "Slap Player"); var players = Helper.GetValidPlayers().Where(admin.CanTarget); - + foreach (var player in players) { - var capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - slapMenu.AddSubMenu(playerName, () => CreateSlapDamageMenu(admin, capturedPlayer)); + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + slapMenu.AddSubMenu(playerName, () => CreateSlapDamageMenu(admin, player)); } return slapMenu.WithBackButton(); @@ -73,25 +70,18 @@ public abstract class BasicMenu /// A MenuBuilder instance for the slap damage menu. private static MenuBuilder CreateSlapDamageMenu(CCSPlayerController admin, CCSPlayerController target) { - var localizer = CS2_SimpleAdmin._localizer; - string localizedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - localizedTitle = $"{localizer?["sa_slap"] ?? "Slap"}: {target.PlayerName}"; - } - - var slapDamageMenu = new MenuBuilder(localizedTitle); + 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", currentAdmin => + slapDamageMenu.AddOption($"{damage} HP", _ => { if (target.IsValid) { - CS2_SimpleAdmin.Slap(currentAdmin, target, damage); - // Reopen the same menu (not create new one) to keep back button working - slapDamageMenu.OpenMenu(currentAdmin); + CS2_SimpleAdmin.Slap(admin, target, damage); + // Keep menu open for consecutive slaps + CreateSlapDamageMenu(admin, target).OpenMenu(admin); } }); } @@ -107,19 +97,18 @@ public abstract class BasicMenu private static MenuBuilder CreateSlayMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var slayMenu = new MenuBuilder("sa_slay", admin, localizer); + var slayMenu = new MenuBuilder(localizer?["sa_slay"] ?? "Slay Player"); var players = Helper.GetValidPlayers().Where(admin.CanTarget); - + foreach (var player in players) { - var capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; slayMenu.AddOption(playerName, _ => { - if (capturedPlayer.IsValid) + if (player.IsValid) { - CS2_SimpleAdmin.Slay(admin, capturedPlayer); + CS2_SimpleAdmin.Slay(admin, player); } }); } @@ -135,20 +124,19 @@ public abstract class BasicMenu private static MenuBuilder CreateKickMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var kickMenu = new MenuBuilder("sa_kick", admin, 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 capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, capturedPlayer, "Kick", PenaltyType.Kick, + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, player, "Kick", PenaltyType.Kick, (_, _, reason) => { - if (capturedPlayer.IsValid) + if (player.IsValid) { - CS2_SimpleAdmin.Instance.Kick(admin, capturedPlayer, reason, admin.PlayerName); + CS2_SimpleAdmin.Instance.Kick(admin, player, reason, admin.PlayerName); } })); } @@ -164,21 +152,20 @@ public abstract class BasicMenu private static MenuBuilder CreateWarnMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var warnMenu = new MenuBuilder("sa_warn", admin, 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 capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - warnMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Warn", - (_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Warn", PenaltyType.Warn, + 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 (capturedPlayer.IsValid) + if (player.IsValid) { - CS2_SimpleAdmin.Instance.Warn(admin, capturedPlayer, duration, reason, admin.PlayerName); + CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason, admin.PlayerName); } }))); } @@ -194,21 +181,20 @@ public abstract class BasicMenu private static MenuBuilder CreateBanMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var banMenu = new MenuBuilder("sa_ban", admin, 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 capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - banMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Ban", - (_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Ban", PenaltyType.Ban, + 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 (capturedPlayer.IsValid) + if (player.IsValid) { - CS2_SimpleAdmin.Instance.Ban(admin, capturedPlayer, duration, reason, admin.PlayerName); + CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason, admin.PlayerName); } }))); } @@ -224,21 +210,20 @@ public abstract class BasicMenu private static MenuBuilder CreateGagMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var gagMenu = new MenuBuilder("sa_gag", admin, 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 capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - gagMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Gag", - (_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Gag", PenaltyType.Gag, + 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 (capturedPlayer.IsValid) + if (player.IsValid) { - CS2_SimpleAdmin.Instance.Gag(admin, capturedPlayer, duration, reason); + CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason); } }))); } @@ -254,21 +239,20 @@ public abstract class BasicMenu private static MenuBuilder CreateMuteMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var muteMenu = new MenuBuilder("sa_mute", admin, 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 capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - muteMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Mute", - (_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Mute", PenaltyType.Mute, + 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 (capturedPlayer.IsValid) + if (player.IsValid) { - CS2_SimpleAdmin.Instance.Mute(admin, capturedPlayer, duration, reason); + CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason); } }))); } @@ -284,21 +268,20 @@ public abstract class BasicMenu private static MenuBuilder CreateSilenceMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var silenceMenu = new MenuBuilder("sa_silence", admin, 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 capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - silenceMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Silence", - (_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Silence", PenaltyType.Silence, + 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 (capturedPlayer.IsValid) + if (player.IsValid) { - CS2_SimpleAdmin.Instance.Silence(admin, capturedPlayer, duration, reason); + CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason); } }))); } @@ -314,15 +297,14 @@ public abstract class BasicMenu private static MenuBuilder CreateForceTeamMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var teamMenu = new MenuBuilder("sa_team_force", admin, 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 capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - teamMenu.AddSubMenu(playerName, () => CreateTeamSelectionMenu(admin, capturedPlayer)); + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + teamMenu.AddSubMenu(playerName, () => CreateTeamSelectionMenu(admin, player)); } return teamMenu.WithBackButton(); @@ -337,32 +319,14 @@ public abstract class BasicMenu private static MenuBuilder CreateTeamSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { var localizer = CS2_SimpleAdmin._localizer; - - // Localize title for admin's language - string localizedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - localizedTitle = $"{localizer?["sa_team_force"] ?? "Force Team"}: {target.PlayerName}"; - } - - var teamSelectionMenu = new MenuBuilder(localizedTitle); - - // Localize team options for admin's language - string ctName, tName, swapName, specName; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - ctName = localizer?["sa_team_ct"] ?? "CT"; - tName = localizer?["sa_team_t"] ?? "T"; - swapName = localizer?["sa_team_swap"] ?? "Swap"; - specName = localizer?["sa_team_spec"] ?? "Spec"; - } + var teamSelectionMenu = new MenuBuilder($"Force Team: {target.PlayerName}"); var teams = new[] { - (ctName, "ct", CsTeam.CounterTerrorist), - (tName, "t", CsTeam.Terrorist), - (swapName, "swap", CsTeam.Spectator), - (specName, "spec", CsTeam.Spectator) + (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) @@ -387,7 +351,7 @@ public abstract class BasicMenu private static MenuBuilder CreatePluginsMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var pluginsMenu = new MenuBuilder("sa_menu_pluginsmanager_title", admin, localizer); + var pluginsMenu = new MenuBuilder(localizer?["sa_menu_pluginsmanager_title"] ?? "Manage Plugins"); pluginsMenu.AddOption("Open Plugins Manager", _ => { @@ -405,7 +369,7 @@ public abstract class BasicMenu private static MenuBuilder CreateChangeMapMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var mapMenu = new MenuBuilder("sa_changemap", admin, localizer); + var mapMenu = new MenuBuilder(localizer?["sa_changemap"] ?? "Change Map"); // Add default maps var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps; @@ -438,7 +402,7 @@ public abstract class BasicMenu private static MenuBuilder CreateRestartGameMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var restartMenu = new MenuBuilder("sa_restart_game", admin, localizer); + var restartMenu = new MenuBuilder(localizer?["sa_restart_game"] ?? "Restart Game"); restartMenu.AddOption("Restart Round", _ => { @@ -456,7 +420,7 @@ public abstract class BasicMenu private static MenuBuilder CreateCustomCommandsMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var customMenu = new MenuBuilder("sa_menu_custom_commands", admin, localizer); + var customMenu = new MenuBuilder(localizer?["sa_menu_custom_commands"] ?? "Custom Commands"); var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands; @@ -491,15 +455,14 @@ public abstract class BasicMenu private static MenuBuilder CreateAddAdminMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var addAdminMenu = new MenuBuilder("sa_admin_add", admin, 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 capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; - addAdminMenu.AddSubMenu(playerName, () => CreateAdminFlagsMenu(admin, capturedPlayer)); + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; + addAdminMenu.AddSubMenu(playerName, () => CreateAdminFlagsMenu(admin, player)); } return addAdminMenu.WithBackButton(); @@ -513,16 +476,7 @@ public abstract class BasicMenu /// A MenuBuilder instance for the admin flags menu. private static MenuBuilder CreateAdminFlagsMenu(CCSPlayerController admin, CCSPlayerController target) { - var localizer = CS2_SimpleAdmin._localizer; - - // Localize title for admin's language - string localizedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - localizedTitle = $"{localizer?["sa_admin_add"] ?? "Add Admin"}: {target.PlayerName}"; - } - - var flagsMenu = new MenuBuilder(localizedTitle); + var flagsMenu = new MenuBuilder($"Add Admin: {target.PlayerName}"); foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags) { @@ -547,22 +501,21 @@ public abstract class BasicMenu private static MenuBuilder CreateRemoveAdminMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var removeAdminMenu = new MenuBuilder("sa_admin_remove", admin, 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 && + var adminPlayers = Helper.GetValidPlayers().Where(p => + AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0 && + p != admin && admin.CanTarget(p)); - + foreach (var player in adminPlayers) { - var capturedPlayer = player; // Capture to local variable to avoid closure issue - var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName; + var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; removeAdminMenu.AddOption(playerName, _ => { - if (capturedPlayer.IsValid) + if (player.IsValid) { - CS2_SimpleAdmin.Instance.RemoveAdmin(admin, capturedPlayer.SteamID.ToString()); + CS2_SimpleAdmin.Instance.RemoveAdmin(admin, player.SteamID.ToString()); } }); } @@ -578,7 +531,7 @@ public abstract class BasicMenu private static MenuBuilder CreateReloadAdminsMenu(CCSPlayerController admin) { var localizer = CS2_SimpleAdmin._localizer; - var reloadMenu = new MenuBuilder("sa_admin_reload", admin, localizer); + var reloadMenu = new MenuBuilder(localizer?["sa_admin_reload"] ?? "Reload Admins"); reloadMenu.AddOption("Reload Admins", _ => { @@ -593,40 +546,20 @@ public abstract class BasicMenu /// /// The admin player selecting duration. /// The target player for the penalty. - /// The name of the penalty action (e.g., "Kick", "Ban"). - /// Callback function that returns the next menu when duration is selected. + /// 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, - Func onSelectAction) + Action onSelectAction) { - var localizer = CS2_SimpleAdmin._localizer; - - // Convert action name to translation key (e.g., "Ban" -> "sa_ban") - var actionKey = actionName.ToLower() switch - { - "kick" => "sa_kick", - "ban" => "sa_ban", - "warn" => "sa_warn", - "gag" => "sa_gag", - "mute" => "sa_mute", - "silence" => "sa_silence", - _ => actionName - }; - - // Localize title for admin's language - string localizedAction, durationText; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - localizedAction = localizer?[actionKey] ?? actionName; - durationText = localizer?["sa_duration"] ?? "Duration"; - } - - var durationMenu = new MenuBuilder($"{localizedAction} {durationText}: {player.PlayerName}"); + var durationMenu = new MenuBuilder($"{actionName} Duration: {player.PlayerName}"); foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) { - var capturedDuration = durationItem.Duration; // Capture to avoid closure issue - durationMenu.AddSubMenu(durationItem.Name, () => onSelectAction(admin, player, capturedDuration)); + durationMenu.AddOption(durationItem.Name, _ => + { + onSelectAction(admin, player, durationItem.Duration); + }); } return durationMenu.WithBackButton(); @@ -637,36 +570,14 @@ public abstract class BasicMenu /// /// The admin player selecting reason. /// The target player for the penalty. - /// The name of the penalty action (e.g., "Kick", "Ban"). + /// 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 localizer = CS2_SimpleAdmin._localizer; - - // Convert action name to translation key - var actionKey = actionName.ToLower() switch - { - "kick" => "sa_kick", - "ban" => "sa_ban", - "warn" => "sa_warn", - "gag" => "sa_gag", - "mute" => "sa_mute", - "silence" => "sa_silence", - _ => actionName - }; - - // Localize title for admin's language - string localizedAction, reasonText; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - localizedAction = localizer?[actionKey] ?? actionName; - reasonText = localizer?["sa_reason"] ?? "Reason"; - } - - var reasonMenu = new MenuBuilder($"{localizedAction} {reasonText}: {player.PlayerName}"); + var reasonMenu = new MenuBuilder($"{actionName} Reason: {player.PlayerName}"); var reasons = penaltyType switch { diff --git a/CS2-SimpleAdmin/Menus/DurationMenu.cs b/CS2-SimpleAdmin/Menus/DurationMenu.cs index cfe8ea6..20a0acc 100644 --- a/CS2-SimpleAdmin/Menus/DurationMenu.cs +++ b/CS2-SimpleAdmin/Menus/DurationMenu.cs @@ -8,28 +8,12 @@ public static class DurationMenu public static void OpenMenu(CCSPlayerController admin, string menuName, CCSPlayerController player, Action onSelectAction) { var menu = AdminMenu.CreateMenu(menuName); - if (menu == null) - return; - - var durations = CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations; - - // Capture admin and player to avoid closure issues - var capturedAdmin = admin; - var capturedPlayer = player; - var capturedAction = onSelectAction; - - foreach (var durationItem in durations) + foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) { - var duration = durationItem.Duration; // Capture in local variable - var name = durationItem.Name; - - menu.AddMenuOption(name, (controller, option) => - { - capturedAction(capturedAdmin, capturedPlayer, duration); - }); + menu?.AddMenuOption(durationItem.Name, (_, _) => { onSelectAction(admin, player, durationItem.Duration); }); } - AdminMenu.OpenMenu(admin, menu); + if (menu != null) AdminMenu.OpenMenu(admin, menu); } public static void OpenMenu(CCSPlayerController admin, string menuName, DisconnectedPlayer player, Action onSelectAction) diff --git a/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs b/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs index c045ddb..e31186a 100644 --- a/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs +++ b/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs @@ -49,27 +49,25 @@ public static class ManagePlayersMenu if (AdminManager.CommandIsOverriden("css_warn") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_warn")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick")) - options.Add(new ChatMenuOptionData(localizer?["sa_warn"] ?? "Warn", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_warn"] ?? "Warn", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_warn"] ?? "Warn"}: {p.PlayerName}", p, WarnMenu)))); + options.Add(new ChatMenuOptionData(localizer?["sa_warn"] ?? "Warn", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_warn"] ?? "Warn", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_warn"] ?? "Warn"}: {player.PlayerName}", player, WarnMenu)))); if (hasBan) - options.Add(new ChatMenuOptionData(localizer?["sa_ban"] ?? "Ban", () => - PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_ban"] ?? "Ban", (a, p) => - DurationMenu.OpenMenu(a, $"{localizer?["sa_ban"] ?? "Ban"}: {p.PlayerName}", p, BanMenu)))); + options.Add(new ChatMenuOptionData(localizer?["sa_ban"] ?? "Ban", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_ban"] ?? "Ban", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_ban"] ?? "Ban"}: {player.PlayerName}", player, BanMenu)))); if (hasChat) { if (AdminManager.CommandIsOverriden("css_gag") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gag")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat")) - options.Add(new ChatMenuOptionData(localizer?["sa_gag"] ?? "Gag", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_gag"] ?? "Gag", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_gag"] ?? "Gag"}: {p.PlayerName}", p, GagMenu)))); + options.Add(new ChatMenuOptionData(localizer?["sa_gag"] ?? "Gag", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_gag"] ?? "Gag", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_gag"] ?? "Gag"}: {player.PlayerName}", player, GagMenu)))); if (AdminManager.CommandIsOverriden("css_mute") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_mute")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat")) - options.Add(new ChatMenuOptionData(localizer?["sa_mute"] ?? "Mute", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_mute"] ?? "Mute", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_mute"] ?? "Mute"}: {p.PlayerName}", p, MuteMenu)))); + options.Add(new ChatMenuOptionData(localizer?["sa_mute"] ?? "Mute", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_mute"] ?? "Mute", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_mute"] ?? "Mute"}: {player.PlayerName}", player, MuteMenu)))); if (AdminManager.CommandIsOverriden("css_silence") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_silence")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat")) - options.Add(new ChatMenuOptionData(localizer?["sa_silence"] ?? "Silence", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_silence"] ?? "Silence", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_silence"] ?? "Silence"}: {p.PlayerName}", p, SilenceMenu)))); + options.Add(new ChatMenuOptionData(localizer?["sa_silence"] ?? "Silence", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_silence"] ?? "Silence", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_silence"] ?? "Silence"}: {player.PlayerName}", player, SilenceMenu)))); } if (AdminManager.CommandIsOverriden("css_team") @@ -161,9 +159,22 @@ public static class ManagePlayersMenu { if (player is { IsValid: true }) Ban(admin, player, duration, reason); - + CS2_SimpleAdmin.MenuApi?.CloseMenu(admin); }); + + // var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_ban"] ?? "Ban"}: {player?.PlayerName}"); + // + // foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons) + // { + // menu?.AddMenuOption(option, (_, _) => + // { + // if (player is { IsValid: true }) + // Ban(admin, player, duration, option); + // }); + // } + // + // if (menu != null) AdminMenu.OpenMenu(admin, menu); } private static void Ban(CCSPlayerController admin, CCSPlayerController player, int duration, string reason) diff --git a/CS2-SimpleAdmin/Menus/MenuBuilder.cs b/CS2-SimpleAdmin/Menus/MenuBuilder.cs index 1c8ed3c..70655c3 100644 --- a/CS2-SimpleAdmin/Menus/MenuBuilder.cs +++ b/CS2-SimpleAdmin/Menus/MenuBuilder.cs @@ -1,89 +1,28 @@ using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Core.Translations; -using Microsoft.Extensions.Localization; namespace CS2_SimpleAdmin.Menus; -public class MenuBuilder +public class MenuBuilder(string title) { - private readonly string _title; - private readonly CCSPlayerController? _player; - private readonly IStringLocalizer? _localizer; private readonly List _options = []; private MenuBuilder? _parentMenu; private Action? _backAction; private Action? _resetAction; - /// - /// Constructor for player-localized menu with translation key - /// - public MenuBuilder(string titleKey, CCSPlayerController player, IStringLocalizer? localizer = null) - { - _title = titleKey; - _player = player; - _localizer = localizer ?? CS2_SimpleAdmin._localizer; - } - - /// - /// Constructor for static title (backward compatibility) - /// - public MenuBuilder(string title) - { - _title = title; - _player = null; - _localizer = null; - } - - /// - /// Gets the localized title for the player - /// - private string GetLocalizedTitle() - { - if (_player != null && _localizer != null) - { - using (new WithTemporaryCulture(_player.GetLanguage())) - { - return _localizer[_title]; - } - } - return _title; - } - /// /// Adds a menu option with an action. /// - /// Display name or translation key - /// Action to perform when selected - /// Whether the option is disabled - /// Required permission - /// If true, name is a translation key to be localized - public MenuBuilder AddOption(string name, Action action, bool disabled = false, string? permission = null, bool isTranslationKey = false) + 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, - IsTranslationKey = isTranslationKey + Permission = permission }); return this; } - /// - /// Gets the localized name for a menu option - /// - private string GetLocalizedOptionName(MenuOption option) - { - if (option.IsTranslationKey && _player != null && _localizer != null) - { - using (new WithTemporaryCulture(_player.GetLanguage())) - { - return _localizer[option.Name]; - } - } - return option.Name; - } - /// /// Adds a menu option that opens a submenu. /// @@ -160,11 +99,8 @@ public class MenuBuilder { if (!player.IsValid) return; - // Get localized title - var localizedTitle = GetLocalizedTitle(); - // Use MenuManager dependency - var menu = Helper.CreateMenu(localizedTitle, _backAction); + var menu = Helper.CreateMenu(title, _backAction); if (menu == null) return; foreach (var option in _options) @@ -179,10 +115,7 @@ public class MenuBuilder } } - // Get localized option name - var localizedName = GetLocalizedOptionName(option); - - menu.AddMenuOption(localizedName, (menuPlayer, menuOption) => + menu.AddMenuOption(option.Name, (menuPlayer, menuOption) => { option.Action?.Invoke(menuPlayer); }, option.Disabled); @@ -233,6 +166,5 @@ public class MenuOption public Action? Action { get; set; } public bool Disabled { get; set; } public string? Permission { get; set; } - public bool IsTranslationKey { get; set; } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Menus/MenuManager.cs b/CS2-SimpleAdmin/Menus/MenuManager.cs index 6abf3ea..f1aa450 100644 --- a/CS2-SimpleAdmin/Menus/MenuManager.cs +++ b/CS2-SimpleAdmin/Menus/MenuManager.cs @@ -1,5 +1,4 @@ using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Entities; @@ -39,26 +38,6 @@ public class MenuManager }; } - /// - /// Registers a new menu category with per-player localization support for modules. - /// 🆕 NEW: Enables modules to provide localized category names based on each player's css_lang! - /// - /// Unique identifier for the category. - /// Translation key from module's lang files. - /// Required permission to access this category. - /// Module's IStringLocalizer for per-player translation. - public void RegisterCategory(string categoryId, string categoryNameKey, string permission, Microsoft.Extensions.Localization.IStringLocalizer moduleLocalizer) - { - _menuCategories[categoryId] = new MenuCategory - { - Id = categoryId, - Name = categoryNameKey, // Store the key, not translated text - Permission = permission, - MenuFactories = new Dictionary>(), - ModuleLocalizer = moduleLocalizer // Store module's localizer - }; - } - /// /// Registers a menu within a category (API for other plugins). /// @@ -87,37 +66,6 @@ public class MenuManager } } - /// - /// Registers a menu with per-player localization support for modules. - /// 🆕 NEW: Enables modules to provide localized menu names based on each player's css_lang! - /// - /// The category to add this menu to. - /// Unique identifier for the menu. - /// Translation key from module's lang files. - /// Factory function that creates the menu for a player. - /// Required permission to access this menu (optional). - /// Command name for permission override checking (optional). - /// Module's IStringLocalizer for per-player translation. - public void RegisterMenu(string categoryId, string menuId, string menuNameKey, Func menuFactory, string? permission, string? commandName, Microsoft.Extensions.Localization.IStringLocalizer moduleLocalizer) - { - if (!_menuCategories.ContainsKey(categoryId)) - { - RegisterCategory(categoryId, categoryId); // Auto-create category if it doesn't exist - } - - _menuCategories[categoryId].MenuFactories[menuId] = menuFactory; - _menuCategories[categoryId].MenuNames[menuId] = menuNameKey; // Store the key - _menuCategories[categoryId].MenuLocalizers[menuId] = moduleLocalizer; // Store localizer - if (permission != null) - { - _menuCategories[categoryId].MenuPermissions[menuId] = permission; - } - if (commandName != null) - { - _menuCategories[categoryId].MenuCommandNames[menuId] = commandName; - } - } - /// /// Unregisters a menu from a category. /// @@ -140,7 +88,7 @@ public class MenuManager public MenuBuilder CreateMainMenu(CCSPlayerController player) { var localizer = CS2_SimpleAdmin._localizer; - var mainMenu = new MenuBuilder("sa_title", player, localizer); + var mainMenu = new MenuBuilder(localizer?["sa_title"] ?? "SimpleAdmin"); foreach (var category in _menuCategories.Values) { @@ -150,23 +98,8 @@ public class MenuManager if (!AdminManager.PlayerHasPermissions(steamId, category.Permission)) continue; - // Get localized category name for this player - // If category has a module localizer, use it; otherwise use main plugin localizer - string localizedCategoryName; - using (new WithTemporaryCulture(player.GetLanguage())) - { - if (category.ModuleLocalizer != null) - { - localizedCategoryName = category.ModuleLocalizer[category.Name] ?? category.Name; - } - else - { - localizedCategoryName = localizer?[category.Name] ?? category.Name; - } - } - // Pass player to CreateCategoryMenu - mainMenu.AddSubMenu(localizedCategoryName, () => CreateCategoryMenu(category, player), + mainMenu.AddSubMenu(category.Name, () => CreateCategoryMenu(category, player), permission: category.Permission); } @@ -181,24 +114,7 @@ public class MenuManager /// A MenuBuilder instance for the category menu. private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player) { - var localizer = CS2_SimpleAdmin._localizer; - - // Get localized category name for this player - // If category has a module localizer, use it; otherwise use main plugin localizer - string localizedCategoryName; - using (new WithTemporaryCulture(player.GetLanguage())) - { - if (category.ModuleLocalizer != null) - { - localizedCategoryName = category.ModuleLocalizer[category.Name] ?? category.Name; - } - else - { - localizedCategoryName = localizer?[category.Name] ?? category.Name; - } - } - - var categoryMenu = new MenuBuilder(localizedCategoryName); + var categoryMenu = new MenuBuilder(category.Name); foreach (var kvp in category.MenuFactories) { @@ -243,30 +159,8 @@ public class MenuManager continue; } - // Get localized menu name for this player - // If menu has its own localizer, use it; otherwise use category or main plugin localizer - string localizedMenuName; - using (new WithTemporaryCulture(player.GetLanguage())) - { - if (category.MenuLocalizers.TryGetValue(menuId, out var menuLocalizer)) - { - // Menu has its own module localizer - localizedMenuName = menuLocalizer[menuName] ?? menuName; - } - else if (category.ModuleLocalizer != null) - { - // Use category's module localizer - localizedMenuName = category.ModuleLocalizer[menuName] ?? menuName; - } - else - { - // Use main plugin localizer - localizedMenuName = localizer?[menuName] ?? menuName; - } - } - // Call the actual factory with player parameter - categoryMenu.AddSubMenu(localizedMenuName, () => menuFactory(player), permission: permission); + categoryMenu.AddSubMenu(menuName, () => menuFactory(player), permission: permission); } return categoryMenu.WithBackButton(); @@ -296,12 +190,12 @@ public class MenuManager /// public void InitializeDefaultCategories() { - // Register categories with translation keys instead of translated names - // The actual translation will happen per-player in CreateMainMenu/CreateCategoryMenu - RegisterCategory("players", "sa_menu_players_manage", "@css/generic"); - RegisterCategory("server", "sa_menu_server_manage", "@css/generic"); - // RegisterCategory("fun", "sa_menu_fun_commands", "@css/generic"); - RegisterCategory("admin", "sa_menu_admins_manage", "@css/root"); + 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"); } /// @@ -328,17 +222,4 @@ public class MenuCategory public Dictionary MenuNames { get; set; } = []; public Dictionary MenuPermissions { get; set; } = []; public Dictionary MenuCommandNames { get; set; } = []; - - // 🆕 NEW: Support for per-player localization in modules - /// - /// Optional IStringLocalizer from external module for per-player translation of category name. - /// If null, Name is used as-is (for CS2-SimpleAdmin's built-in categories with translation keys). - /// - public Microsoft.Extensions.Localization.IStringLocalizer? ModuleLocalizer { get; set; } - - /// - /// Stores IStringLocalizer for each menu that uses module localization. - /// Key: menuId, Value: module's localizer - /// - public Dictionary MenuLocalizers { get; set; } = []; } diff --git a/CS2-SimpleAdmin/Menus/PlayersMenu.cs b/CS2-SimpleAdmin/Menus/PlayersMenu.cs index 6d43a20..e14270a 100644 --- a/CS2-SimpleAdmin/Menus/PlayersMenu.cs +++ b/CS2-SimpleAdmin/Menus/PlayersMenu.cs @@ -41,12 +41,11 @@ public static class PlayersMenu continue; var enabled = admin.CanTarget(player); - var capturedPlayer = player; // Capture in local variable to avoid closure issues if (optionName != null) - menu?.AddMenuOption(optionName, (controller, option) => + menu?.AddMenuOption(optionName, (_, _) => { - if (capturedPlayer != null) onSelectAction.Invoke(admin, capturedPlayer); + if (player != null) onSelectAction.Invoke(admin, player); }, !enabled); } diff --git a/CS2-SimpleAdmin/Models/BanRecord.cs b/CS2-SimpleAdmin/Models/BanRecord.cs index 996e423..026ffd6 100644 --- a/CS2-SimpleAdmin/Models/BanRecord.cs +++ b/CS2-SimpleAdmin/Models/BanRecord.cs @@ -24,10 +24,7 @@ public record BanRecord [Column("player_ip")] public string? PlayerIp { get; set; } - - [Column("created")] - public DateTime Created { get; init; } - + [Column("status")] public required string Status { get; init; } diff --git a/CS2-SimpleAdmin/Models/IpHistoryRow.cs b/CS2-SimpleAdmin/Models/IpHistoryRow.cs deleted file mode 100644 index 5c794a7..0000000 --- a/CS2-SimpleAdmin/Models/IpHistoryRow.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CS2_SimpleAdmin.Models; - -public record IpHistoryRow -{ - public long Steamid { get; init; } - public string? Name { get; init; } - public uint Address { get; init; } - public DateTime Used_at { get; init; } -} diff --git a/CS2-SimpleAdmin/VERSION b/CS2-SimpleAdmin/VERSION index c7a73ab..114eb98 100644 --- a/CS2-SimpleAdmin/VERSION +++ b/CS2-SimpleAdmin/VERSION @@ -1 +1 @@ -1.8.2b \ No newline at end of file +1.7.8-beta-3 \ No newline at end of file diff --git a/CS2-SimpleAdmin/Variables.cs b/CS2-SimpleAdmin/Variables.cs index 35424a9..a35cc75 100644 --- a/CS2-SimpleAdmin/Variables.cs +++ b/CS2-SimpleAdmin/Variables.cs @@ -40,6 +40,7 @@ public partial class CS2_SimpleAdmin internal static readonly HashSet AdminDisabledJoinComms = []; // Player Management + private static readonly HashSet GodPlayers = []; internal static readonly HashSet SilentPlayers = []; internal static readonly Dictionary RenamedPlayers = []; internal static readonly ConcurrentDictionary PlayersInfo = []; diff --git a/CS2-SimpleAdmin/lang/en.json b/CS2-SimpleAdmin/lang/en.json index f875b82..a768d22 100644 --- a/CS2-SimpleAdmin/lang/en.json +++ b/CS2-SimpleAdmin/lang/en.json @@ -31,7 +31,7 @@ "sa_team_spec": "Spec", "sa_slap": "Slap", - "sa_slay": "Slay", + "sa_slay": "slay", "sa_kick": "Kick", "sa_ban": "Ban", "sa_gag": "Gag", diff --git a/CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj b/CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj index bbd5d85..d1d66f0 100644 --- a/CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj +++ b/CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj @@ -1,7 +1,7 @@  - net10.0 + net8.0 CS2_SimpleAdminApi enable enable @@ -9,7 +9,7 @@ - + diff --git a/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs b/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs index 65de92a..eddfde5 100644 --- a/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs +++ b/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs @@ -140,16 +140,6 @@ public interface ICS2_SimpleAdminApi /// void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic"); - /// - /// Registers a menu category with per-player localization support for modules. - /// 🆕 NEW: Supports per-player localization using module's IStringLocalizer! - /// - /// The category ID (unique identifier). - /// Translation key from module's lang files. - /// Required permission to access this category. - /// Module's IStringLocalizer for per-player translation. - void RegisterMenuCategory(string categoryId, string categoryNameKey, string permission, object moduleLocalizer); - /// /// Registers a menu in a category. /// @@ -173,19 +163,6 @@ public interface ICS2_SimpleAdminApi /// Command name for permission override checking (optional, e.g., "css_god"). void RegisterMenu(string categoryId, string menuId, string menuName, Func menuFactory, string? permission = null, string? commandName = null); - /// - /// Registers a menu with per-player localization support for modules. - /// 🆕 NEW: Supports per-player localization using module's IStringLocalizer! - /// - /// The category to add this menu to. - /// Unique identifier for the menu. - /// Translation key from module's lang files. - /// Factory function that receives player and menu context. - /// Required permission to access this menu (optional). - /// Command name for permission override checking (optional). - /// Module's IStringLocalizer for per-player translation. - void RegisterMenu(string categoryId, string menuId, string menuNameKey, Func menuFactory, string? permission, string? commandName, object moduleLocalizer); - /// /// Unregisters a menu from a category. /// diff --git a/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.cs b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.cs index 03b2495..9379f33 100644 --- a/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.cs +++ b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.cs @@ -45,7 +45,7 @@ public class CS2_SimpleAdmin_BanSoundModule: BasePlugin foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsBot)) { var filter = new RecipientFilter(player); - player?.EmitSound("bansound", volume: 0.75f, recipients: filter); + player?.EmitSound("bansound", volume: 0.9f, recipients: filter); } } } \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll index e8e0151..b6fa68c 100644 Binary files a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll and b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.cs b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.cs index 720b4ad..d089358 100644 --- a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.cs +++ b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.cs @@ -145,38 +145,20 @@ public class CS2_SimpleAdmin_ExampleModule: BasePlugin // STEP 1: Register a menu category // This creates a new section in the main admin menu // Permission: @css/generic means all admins can see it - // - // ⚠️ LOCALIZATION OPTIONS: - // - // OPTION A - No translations (hard-coded text): _sharedApi.RegisterMenuCategory( "example", // Category ID (unique identifier) - "Example Features", // Display name (hard-coded, same for all players) + "Example Features", // Display name in admin menu "@css/generic" // Required permission ); - // - // OPTION B - With per-player translations (🆕 NEW!): - // If your module has lang/ folder with translations, use this pattern: - // _sharedApi.RegisterMenuCategory( - // "example", // Category ID - // "example_category_name", // Translation key - // "@css/generic", // Permission - // Localizer! // Module's localizer - // ); - // This will translate the category name per-player based on their css_lang setting! // STEP 2: Register individual menu items in the category // 🆕 NEW: These use MenuContext API - factory receives (admin, context) parameters - // - // ⚠️ LOCALIZATION OPTIONS: - // - // OPTION A - No translations (hard-coded text): // Example 1: Simple menu with options _sharedApi.RegisterMenu( "example", // Category ID "simple_action", // Menu ID (unique within category) - "Simple Actions", // Display name (hard-coded) + "Simple Actions", // Display name CreateSimpleActionMenu, // Factory method "@css/generic" // Required permission ); @@ -185,7 +167,7 @@ public class CS2_SimpleAdmin_ExampleModule: BasePlugin _sharedApi.RegisterMenu( "example", "player_selection", - "Select Player", // Display name + "Select Player", CreatePlayerSelectionMenu, "@css/kick" // Requires kick permission ); @@ -194,7 +176,7 @@ public class CS2_SimpleAdmin_ExampleModule: BasePlugin _sharedApi.RegisterMenu( "example", "nested_menu", - "Give Credits", // Display name + "Give Credits", CreateGiveCreditsMenu, "@css/generic" ); @@ -203,26 +185,12 @@ public class CS2_SimpleAdmin_ExampleModule: BasePlugin _sharedApi.RegisterMenu( "example", "test_command", - "Test Command", // Display name + "Test Command", CreateTestCommandMenu, "@css/root", // Default permission "css_test" // Command name for override checking ); - // OPTION B - With per-player translations (🆕 NEW!): - // If your module has lang/ folder, use this pattern: - // _sharedApi.RegisterMenu( - // "example", // Category ID - // "menu_id", // Menu ID - // "menu_translation_key", // Translation key (NOT translated text!) - // CreateYourMenu, // Factory method - // "@css/generic", // Permission - // "css_command", // Command name (optional) - // Localizer! // Module's localizer - // ); - // This will translate the menu name per-player based on their css_lang! - // See FunCommands module for real example. - _menusRegistered = true; Logger.LogInformation("Example menus registered successfully!"); } diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll index 939cd65..b6fa68c 100644 Binary files a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll and b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdminApi.dll differ 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 index d7cc180..729ab08 100644 --- a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.cs @@ -366,73 +366,66 @@ public partial class CS2_SimpleAdmin_FunCommands : BasePlugin, IPluginConfig 0) _sharedApi.RegisterMenu("fun", "god", - "fun_menu_god", - CreateGodModeMenu, "@css/cheats", "css_god", Localizer!); + Localizer?["fun_menu_god"] ?? "God Mode", + CreateGodModeMenu, "@css/cheats", "css_god"); if (Config.NoclipCommands.Count > 0) _sharedApi.RegisterMenu("fun", "noclip", - "fun_menu_noclip", - CreateNoClipMenu, "@css/cheats", "css_noclip", Localizer!); + Localizer?["fun_menu_noclip"] ?? "No Clip", + CreateNoClipMenu, "@css/cheats", "css_noclip"); if (Config.RespawnCommands.Count > 0) _sharedApi.RegisterMenu("fun", "respawn", - "fun_menu_respawn", - CreateRespawnMenu, "@css/cheats", "css_respawn", Localizer!); + Localizer?["fun_menu_respawn"] ?? "Respawn", + CreateRespawnMenu, "@css/cheats", "css_respawn"); if (Config.GiveCommands.Count > 0) _sharedApi.RegisterMenu("fun", "give", - "fun_menu_give", - CreateGiveWeaponMenu, "@css/cheats", "css_give", Localizer!); + Localizer?["fun_menu_give"] ?? "Give Weapon", + CreateGiveWeaponMenu, "@css/cheats", "css_give"); if (Config.StripCommands.Count > 0) _sharedApi.RegisterMenu("fun", "strip", - "fun_menu_strip", - CreateStripWeaponsMenu, "@css/slay", "css_strip", Localizer!); + Localizer?["fun_menu_strip"] ?? "Strip Weapons", + CreateStripWeaponsMenu, "@css/slay", "css_strip"); if (Config.FreezeCommands.Count > 0) _sharedApi.RegisterMenu("fun", "freeze", - "fun_menu_freeze", - CreateFreezeMenu, "@css/slay", "css_freeze", Localizer!); + Localizer?["fun_menu_freeze"] ?? "Freeze", + CreateFreezeMenu, "@css/slay", "css_freeze"); if (Config.HpCommands.Count > 0) _sharedApi.RegisterMenu("fun", "hp", - "fun_menu_hp", - CreateSetHpMenu, "@css/slay", "css_hp", Localizer!); + Localizer?["fun_menu_hp"] ?? "Set HP", + CreateSetHpMenu, "@css/slay", "css_hp"); if (Config.SpeedCommands.Count > 0) _sharedApi.RegisterMenu("fun", "speed", - "fun_menu_speed", - CreateSetSpeedMenu, "@css/slay", "css_speed", Localizer!); + Localizer?["fun_menu_speed"] ?? "Set Speed", + CreateSetSpeedMenu, "@css/slay", "css_speed"); if (Config.GravityCommands.Count > 0) _sharedApi.RegisterMenu("fun", "gravity", - "fun_menu_gravity", - CreateSetGravityMenu, "@css/slay", "css_gravity", Localizer!); + Localizer?["fun_menu_gravity"] ?? "Set Gravity", + CreateSetGravityMenu, "@css/slay", "css_gravity"); if (Config.MoneyCommands.Count > 0) _sharedApi.RegisterMenu("fun", "money", - "fun_menu_money", - CreateSetMoneyMenu, "@css/slay", "css_money", Localizer!); + Localizer?["fun_menu_money"] ?? "Set Money", + CreateSetMoneyMenu, "@css/slay", "css_money"); if (Config.ResizeCommands.Count > 0) _sharedApi.RegisterMenu("fun", "resize", - "fun_menu_resize", - CreateSetResizeMenu, "@css/slay", "css_resize", Localizer!); + Localizer?["fun_menu_resize"] ?? "Resize Player", + CreateSetResizeMenu, "@css/slay", "css_resize"); _menusRegistered = true; Logger.LogInformation("Fun menus registered successfully!"); 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 index 85c6be5..3990512 100644 --- a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.csproj +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.csproj @@ -14,7 +14,7 @@ - + diff --git a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs index ea8d90e..a492787 100644 --- a/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs +++ b/Modules/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands/Menus.cs @@ -1,7 +1,6 @@ using System.Globalization; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Core.Translations; namespace CS2_SimpleAdmin_FunCommands; @@ -140,15 +139,8 @@ public partial class CS2_SimpleAdmin_FunCommands /// private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { - // Translate title per-player based on admin's css_lang - string translatedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - translatedTitle = Localizer?["fun_menu_give_player", target.PlayerName] ?? $"Give Weapon: {target.PlayerName}"; - } - var weaponMenu = _sharedApi!.CreateMenuWithBack( - translatedTitle, + Localizer?["fun_menu_give_player", target.PlayerName] ?? $"Give Weapon: {target.PlayerName}", "fun", admin); @@ -220,15 +212,8 @@ public partial class CS2_SimpleAdmin_FunCommands /// private object CreateHpSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { - // Translate title per-player based on admin's css_lang - string translatedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - translatedTitle = Localizer?["fun_menu_hp_player", target.PlayerName] ?? $"Set HP: {target.PlayerName}"; - } - var hpSelectionMenu = _sharedApi!.CreateMenuWithBack( - translatedTitle, + Localizer?["fun_menu_hp_player", target.PlayerName] ?? $"Set HP: {target.PlayerName}", "fun", admin); @@ -237,15 +222,8 @@ public partial class CS2_SimpleAdmin_FunCommands foreach (var hp in hpValues) { - // Translate option label per-player - string optionLabel; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - optionLabel = Localizer?["fun_menu_hp_value", hp] ?? $"{hp} HP"; - } - _sharedApi.AddMenuOption(hpSelectionMenu, - optionLabel, + Localizer?["fun_menu_hp_value", hp] ?? $"{hp} HP", _ => { if (target.IsValid) @@ -283,15 +261,8 @@ public partial class CS2_SimpleAdmin_FunCommands /// private object CreateSpeedSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { - // Translate title per-player based on admin's css_lang - string translatedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - translatedTitle = Localizer?["fun_menu_speed_player", target.PlayerName] ?? $"Set Speed: {target.PlayerName}"; - } - var speedSelectionMenu = _sharedApi!.CreateMenuWithBack( - translatedTitle, + Localizer?["fun_menu_speed_player", target.PlayerName] ?? $"Set Speed: {target.PlayerName}", "fun", admin); @@ -304,15 +275,8 @@ public partial class CS2_SimpleAdmin_FunCommands foreach (var (speed, display) in speedValues) { - // Translate option label per-player - string optionLabel; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - optionLabel = Localizer?["fun_menu_speed_value", display] ?? $"Speed {display}"; - } - _sharedApi.AddMenuOption(speedSelectionMenu, - optionLabel, + Localizer?["fun_menu_speed_value", display] ?? $"Speed {display}", _ => { if (target.IsValid) @@ -352,15 +316,8 @@ public partial class CS2_SimpleAdmin_FunCommands private object CreateGravitySelectionMenu(CCSPlayerController admin, CCSPlayerController target) { - // Translate title per-player based on admin's css_lang - string translatedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - translatedTitle = Localizer?["fun_menu_gravity_player", target.PlayerName] ?? $"Set Gravity: {target.PlayerName}"; - } - var gravitySelectionMenu = _sharedApi!.CreateMenuWithBack( - translatedTitle, + Localizer?["fun_menu_gravity_player", target.PlayerName] ?? $"Set Gravity: {target.PlayerName}", "fun", admin); var gravityValues = new[] @@ -408,15 +365,8 @@ public partial class CS2_SimpleAdmin_FunCommands private object CreateMoneySelectionMenu(CCSPlayerController admin, CCSPlayerController target) { - // Translate title per-player based on admin's css_lang - string translatedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - translatedTitle = Localizer?["fun_menu_money_player", target.PlayerName] ?? $"Set Money: {target.PlayerName}"; - } - var moneySelectionMenu = _sharedApi!.CreateMenuWithBack( - translatedTitle, + Localizer?["fun_menu_money_player", target.PlayerName] ?? $"Set Money: {target.PlayerName}", "fun", admin); var moneyValues = new[] { 0, 1000, 2500, 5000, 10000, 16000 }; @@ -457,15 +407,8 @@ public partial class CS2_SimpleAdmin_FunCommands private object CreateResizeSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { - // Translate title per-player based on admin's css_lang - string translatedTitle; - using (new WithTemporaryCulture(admin.GetLanguage())) - { - translatedTitle = Localizer?["fun_menu_resize_player", target.PlayerName] ?? $"Resize: {target.PlayerName}"; - } - var resizeSelectionMenu = _sharedApi!.CreateMenuWithBack( - translatedTitle, + Localizer?["fun_menu_resize_player", target.PlayerName] ?? $"Resize: {target.PlayerName}", "fun", admin); var resizeValues = new[] diff --git a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.1.0-linux.tar.gz b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.1.0-linux.tar.gz new file mode 100644 index 0000000..ae1a40d Binary files /dev/null and b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.1.0-linux.tar.gz differ diff --git a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.1.0-windows.tar.gz b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.1.0-windows.tar.gz new file mode 100644 index 0000000..1ec754a Binary files /dev/null and b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.1.0-windows.tar.gz differ diff --git a/Modules/MODULE_DEVELOPMENT.md b/Modules/MODULE_DEVELOPMENT.md index 4dee295..cba01f0 100644 --- a/Modules/MODULE_DEVELOPMENT.md +++ b/Modules/MODULE_DEVELOPMENT.md @@ -75,17 +75,10 @@ public class MyModule : BasePlugin if (_api == null) return; // 1. Register a new category - // IMPORTANT: If your module has lang/ folder with translations, use YOUR module's Localizer! - // Example: _api.RegisterMenuCategory("mymodule", Localizer?["mymodule_category_name"] ?? "My Module", "@css/generic"); - // - // This will use YOUR module's translations in server language. - // For modules without translations, you can use hard-coded text: _api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); // 2. Register menu items in the category // 🆕 NEW: Use MenuContext-aware overload (no duplication!) - // NOTE: Use YOUR module's Localizer if you have lang/ folder with translations: - // _api.RegisterMenu("mymodule", "action1", Localizer?["mymodule_menu_action1"] ?? "Action 1", CreateAction1Menu, "@css/generic"); _api.RegisterMenu("mymodule", "action1", "Action 1", CreateAction1Menu, "@css/generic"); _api.RegisterMenu("mymodule", "action2", "Action 2", CreateAction2Menu, "@css/kick"); } @@ -363,27 +356,12 @@ 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` - **TRANSLATION KEY** for the display name (e.g., "vip_category_name", NOT "VIP Features") +- `categoryName` - Display name shown in menu (e.g., "Fun Commands") - `permission` - Required permission to see the category (default: "@css/generic") -**IMPORTANT FOR MODULES WITH TRANSLATIONS:** -- If your module has a `lang/` folder with translation files, use **YOUR module's Localizer** -- This will display menu names in the **server's language** (not per-player) -- For per-player localization, only **CS2-SimpleAdmin's built-in menus** support this currently - **Example:** ```csharp -// ✅ CORRECT: Module with translations uses its own Localizer -_api.RegisterMenuCategory("vip", Localizer?["vip_category_name"] ?? "VIP Features", "@css/vip"); -// In YOUR module's lang/en.json: "vip_category_name": "VIP Features" -// In YOUR module's lang/pl.json: "vip_category_name": "Funkcje VIP" - -// ✅ ALSO CORRECT: Module without translations uses hard-coded text _api.RegisterMenuCategory("vip", "VIP Features", "@css/vip"); - -// ❌ INCORRECT: Using translation key without Localizer -_api.RegisterMenuCategory("vip", "vip_category_name", "@css/vip"); -// This would display "vip_category_name" literally! ``` ### 2. Menu Registration @@ -395,28 +373,13 @@ 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 for the menu (use **your module's Localizer** if you have translations) -- `menuFactory` - Function that creates the menu when selected (receives admin player and MenuContext) +- `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 -**IMPORTANT FOR MODULES WITH TRANSLATIONS:** -- Use **your module's Localizer** if you have a `lang/` folder: `Localizer?["key"] ?? "Fallback"` -- This shows menu in **server language**, not per-player -- Per-player localization is only available for CS2-SimpleAdmin's built-in menus - **Example:** ```csharp -// ✅ CORRECT: Module with translations uses its own Localizer -_api.RegisterMenu("fun", "godmode", Localizer?["fun_menu_god"] ?? "God Mode", CreateGodModeMenu, "@css/cheats"); -// In YOUR module's lang/en.json: "fun_menu_god": "God Mode" -// In YOUR module's lang/pl.json: "fun_menu_god": "Tryb Boga" - -// ✅ ALSO CORRECT: Module without translations uses hard-coded text _api.RegisterMenu("fun", "godmode", "God Mode", CreateGodModeMenu, "@css/cheats"); - -// ❌ INCORRECT: Using translation key without Localizer -_api.RegisterMenu("fun", "godmode", "fun_menu_god", CreateGodModeMenu, "@css/cheats"); -// This would display "fun_menu_god" literally! ``` #### `UnregisterMenu(string categoryId, string menuId)` @@ -662,32 +625,12 @@ private object CreateAdminToolsMenu(CCSPlayerController admin) ## Best Practices -1. **Use your module's Localizer for translations** 🌍 - ```csharp - // ✅ CORRECT: Module with translations uses its own Localizer - _api.RegisterMenuCategory("mymodule", Localizer?["mymodule_category"] ?? "My Module", "@css/generic"); - _api.RegisterMenu("mymodule", "action", Localizer?["mymodule_menu_action"] ?? "My Action", CreateMenu, "@css/generic"); - - // ✅ ALSO CORRECT: Module without translations uses hard-coded text - _api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); - _api.RegisterMenu("mymodule", "action", "My Action", CreateMenu, "@css/generic"); - - // ❌ WRONG: Using translation key without Localizer - _api.RegisterMenuCategory("mymodule", "mymodule_category", "@css/generic"); - // This would display "mymodule_category" literally! - ``` - - **✅ Per-player localization now available!** - - Both CS2-SimpleAdmin built-in menus AND module menus support per-player localization - - Each player sees menus in their own language based on their `css_lang` setting - - See "Advanced: Per-Player Localization for Modules" section below for implementation details - -2. **Always check for API availability** +1. **Always check for API availability** ```csharp if (_api == null) return; ``` -3. **Validate player state before actions** +2. **Validate player state before actions** ```csharp if (target.IsValid && target.PlayerPawn?.Value != null) { @@ -695,11 +638,11 @@ private object CreateAdminToolsMenu(CCSPlayerController admin) } ``` -4. **Use descriptive category and menu IDs** +3. **Use descriptive category and menu IDs** - Good: `"economy"`, `"vip_features"`, `"fun_commands"` - Bad: `"cat1"`, `"menu"`, `"test"` -5. **Clean up on unload** +4. **Clean up on unload** ```csharp public override void Unload(bool hotReload) { @@ -708,14 +651,14 @@ private object CreateAdminToolsMenu(CCSPlayerController admin) } ``` -6. **Use appropriate permissions** +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 -7. **Handle hot reload** +6. **Handle hot reload** ```csharp _api.OnSimpleAdminReady += RegisterMenus; RegisterMenus(); // Fallback for hot reload case @@ -774,72 +717,3 @@ See the `CS2-SimpleAdmin_FunCommands` module in the `Modules/` directory for a c **Q: API is null in OnAllPluginsLoaded** - Wait for the `OnSimpleAdminReady` event instead of immediate registration - Make sure CS2-SimpleAdmin is loaded before your module - -## Advanced: Per-Player Localization for Modules (✅ NOW AVAILABLE!) - -**🆕 NEW:** Module menus now support **per-player localization** based on `css_lang`! - -Both CS2-SimpleAdmin's built-in menus AND module menus can show in each player's configured language. - -### How to Use Per-Player Localization in Your Module - -**1. Register Category with Localizer:** - -```csharp -// Pass translation KEY (not translated text) and module's Localizer -_api.RegisterMenuCategory( - "mymodule", - "mymodule_category_name", // Translation key from your lang/ folder - "@css/generic", - Localizer! // Your module's localizer -); -``` - -**2. Register Menus with Localizer:** - -```csharp -_api.RegisterMenu( - "mymodule", - "action", - "mymodule_menu_action", // Translation key from your lang/ folder - CreateMenu, - "@css/generic", // Permission - "css_mycommand", // Command name for override (optional) - Localizer! // Your module's localizer -); -``` - -**How it works:** -1. Your module passes its `IStringLocalizer` and translation **key** (not translated text) -2. SimpleAdmin's `MenuManager` stores both the key and the localizer -3. When displaying menu to a player, MenuManager translates using: - ```csharp - using (new WithTemporaryCulture(player.GetLanguage())) - { - localizedName = moduleLocalizer[translationKey]; - } - ``` -4. Each player sees menus in their own language based on their `css_lang` setting! - -**Complete Example:** - -See `Modules/CS2-SimpleAdmin_FunCommands/` for a real implementation: - -```csharp -// Register category with per-player localization -_api.RegisterMenuCategory("fun", "fun_category_name", "@css/generic", Localizer!); - -// Register menu with per-player localization -_api.RegisterMenu("fun", "god", "fun_menu_god", - CreateGodModeMenu, "@css/cheats", "css_god", Localizer!); -``` - -**Without Per-Player Localization (backwards compatible):** - -If you don't need per-player localization, the old API still works: - -```csharp -// Hard-coded text (same for all players) -_api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); -_api.RegisterMenu("mymodule", "action", "Do Action", CreateMenu, "@css/generic"); -```