Compare commits

..

6 Commits

Author SHA1 Message Date
Dawid Bepierszcz
c2e8b4a898 Refactor player cache and ban checks, update version
Improves player cache handling and ban status checks for race condition safety. Removes unused GodPlayers logic and related event handler. Refactors event handlers for disconnect and team changes, and fixes warn reason field naming. Updates version to 1.7.8-beta-7.
2025-12-02 17:37:14 +01:00
Dawid Bepierszcz
9723a4faee :(
:(
2025-11-13 01:40:50 +01:00
Dawid Bepierszcz
4865b76262 Improve ban cache sync and update dependencies
Enhanced CacheManager to use database time for ban updates, improving multi-server consistency and handling of status changes. Increased PlayerManager's semaphore limit and improved player/bans refresh logic to ensure status changes are detected even when the server is empty. Updated author and version metadata, commented out duplicate event registration, and bumped CounterStrikeSharp.API dependency to 1.0.346.
2025-11-13 01:33:38 +01:00
Dawid Bepierszcz
0dded66e5d Fix closure issues in menus and update dependencies
Captured player and duration variables in menu callbacks to prevent closure-related bugs. Updated package versions in project files and bumped plugin version to 1.7.8-beta-5. Improved player validation and message localization logic.
2025-11-06 02:24:43 +01:00
Dawid Bepierszcz
038641dbdf Comment out MySQL index migration and remove Sqlite optimization
Commented out all index creation statements in the MySQL migration 016 for table and index optimization. Removed the corresponding Sqlite migration 016 entirely. Also replaced TRUNCATE TABLE with DELETE FROM in Sqlite migration 013 for sa_players_ips to improve compatibility.
2025-10-30 18:17:47 +01:00
Dawid Bepierszcz
a03964c08a Add per-player menu localization and refactor menus
Introduces per-player localization for menu categories and items using translation keys and IStringLocalizer, allowing modules and the main plugin to display menu names in the player's language. Refactors menu registration and builder logic to use translation keys, updates API and documentation, and adds database provider upsert query abstraction for player IPs. Also updates version to 1.7.8-beta-4 and corrects a translation string typo.
2025-10-30 01:41:08 +01:00
33 changed files with 1021 additions and 368 deletions

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Entities;
@@ -205,6 +206,14 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryName, permission); 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, public void RegisterMenu(string categoryId, string menuId, string menuName,
Func<CCSPlayerController, object> menuFactory, string? permission = null, string? commandName = null) Func<CCSPlayerController, object> menuFactory, string? permission = null, string? commandName = null)
{ {
@@ -263,6 +272,39 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
} }
} }
public void RegisterMenu(string categoryId, string menuId, string menuNameKey,
Func<CCSPlayerController, MenuContext, object> 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) public void UnregisterMenu(string categoryId, string menuId)
{ {
@@ -289,7 +331,30 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public object CreateMenuWithBack(MenuContext context, CCSPlayerController player) public object CreateMenuWithBack(MenuContext context, CCSPlayerController player)
{ {
return CreateMenuWithBack(context.MenuTitle, context.CategoryId, 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);
} }
public List<CCSPlayerController> GetValidPlayers() public List<CCSPlayerController> GetValidPlayers()
@@ -321,7 +386,30 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public object CreateMenuWithPlayers(MenuContext context, CCSPlayerController admin, public object CreateMenuWithPlayers(MenuContext context, CCSPlayerController admin,
Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect) Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)
{ {
return CreateMenuWithPlayers(context.MenuTitle, context.CategoryId, admin, filter, 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);
} }
public void AddMenuOption(object menu, string name, Action<CCSPlayerController> action, bool disabled = false, public void AddMenuOption(object menu, string name, Action<CCSPlayerController> action, bool disabled = false,

View File

@@ -21,13 +21,12 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
public override string ModuleAuthor => "daffyy & Dliix66"; public override string ModuleAuthor => "daffyy";
public override string ModuleVersion => "1.7.8-beta-3"; public override string ModuleVersion => "1.7.8-beta-7";
public override void Load(bool hotReload) public override void Load(bool hotReload)
{ {
Instance = this; Instance = this;
if (hotReload) if (hotReload)
{ {
ServerLoaded = false; ServerLoaded = false;
@@ -47,7 +46,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
CachedPlayers.Clear(); CachedPlayers.Clear();
BotPlayers.Clear(); BotPlayers.Clear();
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsHLTV).ToArray()) foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && p.Connected == PlayerConnectedState.PlayerConnected && !p.IsHLTV).ToArray())
{ {
if (!player.IsBot) if (!player.IsBot)
PlayerManager.LoadPlayerData(player, true); PlayerManager.LoadPlayerData(player, true);
@@ -261,6 +260,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
CacheManager = null; CacheManager = null;
PlayersTimer?.Kill(); PlayersTimer?.Kill();
PlayersTimer = null; PlayersTimer = null;
UnregisterEvents(); UnregisterEvents();
if (hotReload) if (hotReload)

View File

@@ -19,16 +19,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.340"> <PackageReference Include="CounterStrikeSharp.API" Version="1.0.346">
<PrivateAssets>none</PrivateAssets> <PrivateAssets>none</PrivateAssets>
<ExcludeAssets>runtime</ExcludeAssets> <ExcludeAssets>runtime</ExcludeAssets>
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Dapper" Version="2.1.66" /> <PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="MySqlConnector" Version="2.4.0" /> <PackageReference Include="MySqlConnector" Version="2.5.0-beta.1" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Linq.Async" Version="6.0.3" /> <PackageReference Include="System.Linq.Async" Version="7.0.0-preview.1.g24680b5469" />
<PackageReference Include="ZLinq" Version="1.5.2" /> <PackageReference Include="ZLinq" Version="1.5.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -13,6 +13,9 @@ public interface IDatabaseProvider
string GetIpHistoryQuery(); string GetIpHistoryQuery();
string GetBanUpdateQuery(bool multiServer); string GetBanUpdateQuery(bool multiServer);
// PlayerManager
string GetUpsertPlayerIpQuery();
// PermissionManager // PermissionManager
string GetAdminsQuery(); string GetAdminsQuery();
string GetDeleteAdminQuery(bool globalDelete); string GetDeleteAdminQuery(bool globalDelete);

View File

@@ -1,33 +1,33 @@
-- Migration 016: Optimize tables and indexes -- -- Migration 016: Optimize tables and indexes
-- Add proper indexes for all tables to improve query performance -- -- Add proper indexes for all tables to improve query performance
-- Optimize sa_players_ips table indexes -- -- Optimize sa_players_ips table indexes
-- Add index on used_at for efficient date-based queries -- -- 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); -- ALTER TABLE `sa_players_ips` ADD INDEX IF NOT EXISTS `idx_used_at` (`used_at` DESC);
-- Optimize sa_bans table indexes -- -- Optimize sa_bans table indexes
-- Add composite indexes for common query patterns -- -- 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_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_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_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_server_status` ON `sa_bans` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_created` ON `sa_bans` (`created` DESC); -- CREATE INDEX IF NOT EXISTS `idx_bans_created` ON `sa_bans` (`created` DESC);
-- Optimize sa_admins table indexes -- -- 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_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_server_ends` ON `sa_admins` (`server_id`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_admins_ends` ON `sa_admins` (`ends`); -- CREATE INDEX IF NOT EXISTS `idx_admins_ends` ON `sa_admins` (`ends`);
-- Optimize sa_mutes table indexes (in addition to migration 014) -- -- Optimize sa_mutes table indexes (in addition to migration 014)
-- Add index for expire queries -- -- 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_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_server_status` ON `sa_mutes` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_created` ON `sa_mutes` (`created` DESC); -- CREATE INDEX IF NOT EXISTS `idx_mutes_created` ON `sa_mutes` (`created` DESC);
-- Optimize sa_warns table indexes (if exists) -- -- 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_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_status_ends` ON `sa_warns` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_warns_server_status` ON `sa_warns` (`server_id`, `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 -- -- Add index on sa_servers for faster lookups
CREATE INDEX IF NOT EXISTS `idx_servers_hostname` ON `sa_servers` (`hostname`); -- CREATE INDEX IF NOT EXISTS `idx_servers_hostname` ON `sa_servers` (`hostname`);

View File

@@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS `sa_players_ips` ( CREATE TABLE IF NOT EXISTS `sa_players_ips` (
`steamid` INTEGER NOT NULL, `steamid` INTEGER NOT NULL,
`address` INTEGER NOT NULL `address` INTEGER NOT NULL,
`used_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `used_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`steamid`, `address`) PRIMARY KEY (`steamid`, `address`)
); );

View File

@@ -1,4 +1,4 @@
TRUNCATE TABLE `sa_players_ips`; DELETE FROM sa_players_ips;
ALTER TABLE `sa_players_ips` ADD `name` VARCHAR(64) NULL DEFAULT NULL; 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); CREATE INDEX IF NOT EXISTS `idx_sa_players_ips_used_at` ON `sa_players_ips` (`used_at` DESC);

View File

@@ -1,33 +0,0 @@
-- 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`);

View File

@@ -15,7 +15,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';"; cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
cmd.CommandText = "SET time_zone = '+00:00';"; // cmd.CommandText = "SET time_zone = '+00:00';";
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
return connection; return connection;
@@ -86,6 +86,17 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
return "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC"; 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) public string GetBanUpdateQuery(bool multiServer)
{ {
return multiServer ? """ return multiServer ? """

View File

@@ -83,6 +83,17 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
public string GetIpHistoryQuery() => public string GetIpHistoryQuery() =>
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC"; "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) => public string GetBanUpdateQuery(bool multiServer) =>
multiServer multiServer
? """ ? """

View File

@@ -23,7 +23,7 @@ public partial class CS2_SimpleAdmin
{ {
RegisterListener<Listeners.OnMapStart>(OnMapStart); RegisterListener<Listeners.OnMapStart>(OnMapStart);
// RegisterListener<Listeners.OnClientConnect>(OnClientConnect); // RegisterListener<Listeners.OnClientConnect>(OnClientConnect);
RegisterListener<Listeners.OnClientConnect>(OnClientConnect); // RegisterListener<Listeners.OnClientConnect>(OnClientConnect);
RegisterListener<Listeners.OnClientConnected>(OnClientConnected); RegisterListener<Listeners.OnClientConnected>(OnClientConnected);
RegisterListener<Listeners.OnGameServerSteamAPIActivated>(OnGameServerSteamAPIActivated); RegisterListener<Listeners.OnGameServerSteamAPIActivated>(OnGameServerSteamAPIActivated);
if (Config.OtherSettings.UserMessageGagChatType) if (Config.OtherSettings.UserMessageGagChatType)
@@ -77,7 +77,7 @@ public partial class CS2_SimpleAdmin
new ServerManager().LoadServerData(); new ServerManager().LoadServerData();
} }
[GameEventHandler(HookMode.Pre)] [GameEventHandler]
public HookResult OnClientDisconnect(EventPlayerDisconnect @event, GameEventInfo info) public HookResult OnClientDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
{ {
if (@event.Reason is 149 or 6) if (@event.Reason is 149 or 6)
@@ -92,13 +92,14 @@ public partial class CS2_SimpleAdmin
if (player == null || !player.IsValid || player.IsHLTV) if (player == null || !player.IsValid || player.IsHLTV)
return HookResult.Continue; return HookResult.Continue;
BotPlayers.Remove(player);
CachedPlayers.Remove(player); CachedPlayers.Remove(player);
BotPlayers.Remove(player);
SilentPlayers.Remove(player.Slot); SilentPlayers.Remove(player.Slot);
if (player.IsBot) if (player.IsBot)
{
return HookResult.Continue; return HookResult.Continue;
}
#if DEBUG #if DEBUG
Logger.LogCritical("[OnClientDisconnect] After Check"); Logger.LogCritical("[OnClientDisconnect] After Check");
@@ -176,6 +177,9 @@ public partial class CS2_SimpleAdmin
if (player == null || !player.IsValid || player.IsBot) if (player == null || !player.IsValid || player.IsBot)
return; return;
if (!CachedPlayers.Contains(player))
CachedPlayers.Add(player);
PlayerManager.LoadPlayerData(player); PlayerManager.LoadPlayerData(player);
} }
@@ -458,28 +462,11 @@ public partial class CS2_SimpleAdmin
// OnGameServerSteamAPIActivated(); // OnGameServerSteamAPIActivated();
// }); // });
GodPlayers.Clear();
SilentPlayers.Clear(); SilentPlayers.Clear();
PlayerPenaltyManager.RemoveAllPenalties(); 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] [GameEventHandler]
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info) public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{ {
@@ -512,17 +499,13 @@ public partial class CS2_SimpleAdmin
public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info) public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info)
{ {
var player = @event.Userid; var player = @event.Userid;
if (player == null || !player.IsValid || player.IsBot) if (player == null || !player.IsValid || player.IsBot || !SilentPlayers.Contains(player.Slot))
return HookResult.Continue; return HookResult.Continue;
if (!SilentPlayers.Contains(player.Slot)) if (@event is not { Oldteam: <= 1, Team: >= 1 }) return HookResult.Continue;
return HookResult.Continue;
if (@event is { Oldteam: <= 1, Team: >= 1 })
{
SilentPlayers.Remove(player.Slot); SilentPlayers.Remove(player.Slot);
SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false); SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false);
}
return HookResult.Continue; return HookResult.Continue;
} }

View File

@@ -64,7 +64,7 @@ internal static class Helper
public static List<CCSPlayerController> GetValidPlayers() public static List<CCSPlayerController> GetValidPlayers()
{ {
return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().ToList(); return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().Where(p => p.IsValid && p.Connected == PlayerConnectedState.PlayerConnected).ToList();
} }
public static List<CCSPlayerController> GetValidPlayersWithBots() public static List<CCSPlayerController> GetValidPlayersWithBots()
@@ -426,7 +426,7 @@ internal static class Helper
var communityUrl = caller != null ? "<" + new SteamID(caller.SteamID).ToCommunityUrl() + ">" : "<https://steamcommunity.com/profiles/0>"; var communityUrl = caller != null ? "<" + new SteamID(caller.SteamID).ToCommunityUrl() + ">" : "<https://steamcommunity.com/profiles/0>";
var callerName = caller != null ? caller.PlayerName : CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console"; var callerName = caller != null ? caller.PlayerName : CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console";
_ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(Helper.GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command.GetCommandString])); _ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command.GetCommandString]));
} }
private static void SendDiscordLogMessage(CCSPlayerController? caller, string command, IStringLocalizer? localizer) private static void SendDiscordLogMessage(CCSPlayerController? caller, string command, IStringLocalizer? localizer)
@@ -590,20 +590,25 @@ internal static class Helper
{ {
if (CS2_SimpleAdmin._localizer == null) return; if (CS2_SimpleAdmin._localizer == null) return;
// Determine the localized message key
var localizedMessageKey = $"{messageKey}"; var localizedMessageKey = $"{messageKey}";
var formattedMessageArgs = messageArgs.Select(arg => arg?.ToString() ?? string.Empty).ToArray(); var formattedMessageArgs = messageArgs.Select(arg => arg?.ToString() ?? string.Empty).ToArray();
// Replace placeholder based on showActivityType
for (var i = 0; i < formattedMessageArgs.Length; i++) for (var i = 0; i < formattedMessageArgs.Length; i++)
{ {
var arg = formattedMessageArgs[i]; var arg = formattedMessageArgs[i]; // Convert argument to string if not null
// Replace "CALLER" placeholder in the argument string
formattedMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch formattedMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{ {
1 => arg.Replace("CALLER", CS2_SimpleAdmin._localizer["sa_admin"]), 1 => arg.Replace("CALLER", CS2_SimpleAdmin._localizer["sa_admin"]),
2 => arg.Replace("CALLER", callerName ?? "Console"),
_ => arg _ => arg
}; };
} }
// Print the localized message to the center of the screen for the player
using (new WithTemporaryCulture(player.GetLanguage())) using (new WithTemporaryCulture(player.GetLanguage()))
{ {
player.PrintToCenter(CS2_SimpleAdmin._localizer[localizedMessageKey, formattedMessageArgs.Cast<object>().ToArray()]); player.PrintToCenter(CS2_SimpleAdmin._localizer[localizedMessageKey, formattedMessageArgs.Cast<object>().ToArray()]);
@@ -1026,7 +1031,9 @@ public static class Time
{ {
public static DateTime ActualDateTime() 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; string timezoneId = CS2_SimpleAdmin.Instance.Config.Timezone;
DateTime utcNow = DateTime.UtcNow; DateTime utcNow = DateTime.UtcNow;

View File

@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using CS2_SimpleAdmin.Database; using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Models; using CS2_SimpleAdmin.Models;
using Dapper; using Dapper;
using Microsoft.Extensions.Logging;
using ZLinq; using ZLinq;
namespace CS2_SimpleAdmin.Managers; namespace CS2_SimpleAdmin.Managers;
@@ -16,6 +17,7 @@ internal class CacheManager: IDisposable
private HashSet<uint> _cachedIgnoredIps = []; private HashSet<uint> _cachedIgnoredIps = [];
private DateTime _lastUpdateTime = DateTime.MinValue; private DateTime _lastUpdateTime = DateTime.MinValue;
private DateTime? _lastDatabaseTime = null; // Track actual time from database
private bool _isInitialized; private bool _isInitialized;
private bool _disposed; private bool _disposed;
@@ -156,13 +158,20 @@ internal class CacheManager: IDisposable
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<BanRecord> updatedBans; IEnumerable<BanRecord> updatedBans;
// Get current time from database in local timezone (CURRENT_TIMESTAMP uses session timezone, not UTC)
var currentDatabaseTime = await connection.QueryFirstAsync<DateTime>("SELECT CURRENT_TIMESTAMP");
// Optimization: Only get IDs for comparison if we need to check for deletions // Optimization: Only get IDs for comparison if we need to check for deletions
// Most of the time bans are just added/updated, not deleted // Most of the time bans are just added/updated, not deleted
HashSet<int>? allIds = null; HashSet<int>? allIds = null;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{ {
updatedBans = (await connection.QueryAsync<BanRecord>( // 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<BanRecord>(
""" """
SELECT id AS Id, SELECT id AS Id,
player_name AS PlayerName, player_name AS PlayerName,
@@ -171,33 +180,68 @@ internal class CacheManager: IDisposable
status AS Status status AS Status
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
""", """,
new { lastUpdate = _lastUpdateTime } new { lastUpdate = lastCheckTime }
)); )).ToList();
// Detect changes: new bans or status changes
var updatedList = new List<BanRecord>();
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);
}
}
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0) if (updatedList.Count > 0)
{ {
allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet(); allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
} }
updatedBans = updatedList; updatedBans = updatedList;
// Update last check time to current database time
_lastDatabaseTime = currentDatabaseTime;
} }
else else
{ {
updatedBans = (await connection.QueryAsync<BanRecord>( // 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<BanRecord>(
""" """
SELECT id AS Id, SELECT id AS Id,
player_name AS PlayerName, player_name AS PlayerName,
player_steamid AS PlayerSteamId, player_steamid AS PlayerSteamId,
player_ip AS PlayerIp, player_ip AS PlayerIp,
status AS Status status AS Status
FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC FROM `sa_bans` WHERE server_id = @serverId AND (updated_at > @lastUpdate OR created > @lastUpdate) ORDER BY updated_at DESC
""", """,
new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId } new { serverId = CS2_SimpleAdmin.ServerId, lastUpdate = lastCheckTime }
)); )).ToList();
// Detect changes: new bans or status changes
var updatedList = new List<BanRecord>();
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);
}
}
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0) if (updatedList.Count > 0)
{ {
allIds = (await connection.QueryAsync<int>( allIds = (await connection.QueryAsync<int>(
@@ -206,6 +250,9 @@ internal class CacheManager: IDisposable
)).ToHashSet(); )).ToHashSet();
} }
updatedBans = updatedList; updatedBans = updatedList;
// Update last check time to current database time
_lastDatabaseTime = currentDatabaseTime;
} }
// Optimization: Only process deletions if we have the full ID list // Optimization: Only process deletions if we have the full ID list
@@ -276,16 +323,19 @@ internal class CacheManager: IDisposable
} }
// Update cache with new/modified bans // Update cache with new/modified bans
var hasUpdates = false; var needsRebuild = false;
foreach (var ban in updatedBans) 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); _banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban);
hasUpdates = true;
} }
// Always rebuild indexes if there were any updates // Rebuild indexes if there were updates or status changes
// This ensures status changes (ACTIVE -> UNBANNED) are reflected if (updatedBans.Any() || needsRebuild)
if (hasUpdates)
{ {
RebuildIndexes(); RebuildIndexes();
} }
@@ -435,6 +485,9 @@ internal class CacheManager: IDisposable
{ {
record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record != null) 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)) || if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue)) (!record.PlayerSteamId.HasValue))
@@ -445,8 +498,9 @@ internal class CacheManager: IDisposable
return true; return true;
} }
} }
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
return false; return false;
if (string.IsNullOrEmpty(ipAddress) || if (string.IsNullOrEmpty(ipAddress) ||
@@ -456,6 +510,11 @@ internal class CacheManager: IDisposable
record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record == null) return false; 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)) || if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue && steamId.HasValue)) (!record.PlayerSteamId.HasValue && steamId.HasValue))
{ {
@@ -547,14 +606,18 @@ internal class CacheManager: IDisposable
var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE); var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan != null) if (activeBan != null)
{ {
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp)) // 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)); _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true; return true;
} }
} }
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
return false; return false;
if (!_playerIpsCache.TryGetValue(steamId, out var ipData)) if (!_playerIpsCache.TryGetValue(steamId, out var ipData))
@@ -583,6 +646,10 @@ internal class CacheManager: IDisposable
if (activeBan == null) if (activeBan == null)
continue; continue;
// Double-check the ban is still active in cache (handle race conditions)
if (!_banCache.TryGetValue(activeBan.Id, out var cachedBan) || cachedBan.StatusEnum != BanStatus.ACTIVE)
continue;
if (string.IsNullOrEmpty(activeBan.PlayerName)) if (string.IsNullOrEmpty(activeBan.PlayerName))
activeBan.PlayerName = unknownName; activeBan.PlayerName = unknownName;

View File

@@ -13,7 +13,7 @@ namespace CS2_SimpleAdmin.Managers;
internal class PlayerManager internal class PlayerManager
{ {
private readonly SemaphoreSlim _loadPlayerSemaphore = new(5); private readonly SemaphoreSlim _loadPlayerSemaphore = new(10);
private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config; private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config;
/// <summary> /// <summary>
@@ -51,7 +51,6 @@ internal class PlayerManager
try try
{ {
await _loadPlayerSemaphore.WaitAsync(); await _loadPlayerSemaphore.WaitAsync();
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId)) if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
{ {
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
@@ -82,12 +81,6 @@ internal class PlayerManager
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress); var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo; CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
await Server.NextWorldUpdateAsync(() =>
{
if (!CS2_SimpleAdmin.CachedPlayers.Contains(player))
CS2_SimpleAdmin.CachedPlayers.Add(player);
});
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null && if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null &&
CS2_SimpleAdmin.PlayersInfo[steamId] != null) CS2_SimpleAdmin.PlayersInfo[steamId] != null)
{ {
@@ -99,14 +92,8 @@ internal class PlayerManager
var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64; var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64;
var ipUint = IpHelper.IpToUint(ipAddress); var ipUint = IpHelper.IpToUint(ipAddress);
// MySQL: INSERT ... ON DUPLICATE KEY UPDATE pattern // Use database-specific UPSERT query (handles MySQL vs SQLite syntax differences)
const string upsertQuery = """ var upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
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 await connection.ExecuteAsync(upsertQuery, new
{ {
@@ -260,6 +247,7 @@ internal class PlayerManager
_loadPlayerSemaphore.Release(); _loadPlayerSemaphore.Release();
} }
}); });
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name)) if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name))
{ {
player.Rename(name); player.Rename(name);
@@ -293,9 +281,6 @@ internal class PlayerManager
// Optimization: Get players once and avoid allocating anonymous types // Optimization: Get players once and avoid allocating anonymous types
var validPlayers = Helper.GetValidPlayers(); var validPlayers = Helper.GetValidPlayers();
if (validPlayers.Count == 0)
return;
// Use ValueTuple instead of anonymous type - better performance and less allocations // 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); var tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
foreach (var p in validPlayers) foreach (var p in validPlayers)
@@ -370,7 +355,12 @@ internal class PlayerManager
foreach (var player in bannedPlayers) foreach (var player in bannedPlayers)
{ {
if (!player.UserId.HasValue) continue; if (!player.UserId.HasValue) continue;
await Server.NextWorldUpdateAsync(() => Helper.KickPlayer((int)player.UserId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED)); await Server.NextWorldUpdateAsync(() =>
{
if (Helper.GetPlayerFromSteamid64(player.SteamID) != null)
Helper.KickPlayer((int)player.UserId,
NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
});
} }
} }

View File

@@ -33,7 +33,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
playerName = player.Name, playerName = player.Name,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason, warnReason = reason,
duration = time, duration = time,
ends = futureTime, ends = futureTime,
created = now, created = now,
@@ -42,7 +42,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
return warnId; return warnId;
} }
catch catch(Exception e)
{ {
return null; return null;
} }
@@ -73,7 +73,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
playerSteamid = playerSteamId, playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason, warnReason = reason,
duration = time, duration = time,
ends = futureTime, ends = futureTime,
created = now, created = now,

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API; using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants; using CounterStrikeSharp.API.Modules.Entities.Constants;
@@ -12,32 +13,33 @@ public abstract class BasicMenu
{ {
/// <summary> /// <summary>
/// Initializes all menus in the system by registering them with the MenuManager. /// Initializes all menus in the system by registering them with the MenuManager.
/// Register with translation keys instead of static names - translation happens per-player.
/// </summary> /// </summary>
public static void Initialize() public static void Initialize()
{ {
var manager = MenuManager.Instance; var manager = MenuManager.Instance;
// Players category menus // Players category menus - using translation keys
manager.RegisterMenu("players", "slap", "Slap Player", CreateSlapMenu, "@css/slay"); manager.RegisterMenu("players", "slap", "sa_slap", CreateSlapMenu, "@css/slay");
manager.RegisterMenu("players", "slay", "Slay Player", CreateSlayMenu, "@css/slay"); manager.RegisterMenu("players", "slay", "sa_slay", CreateSlayMenu, "@css/slay");
manager.RegisterMenu("players", "kick", "Kick Player", CreateKickMenu, "@css/kick"); manager.RegisterMenu("players", "kick", "sa_kick", CreateKickMenu, "@css/kick");
manager.RegisterMenu("players", "warn", "Warn Player", CreateWarnMenu, "@css/kick"); manager.RegisterMenu("players", "warn", "sa_warn", CreateWarnMenu, "@css/kick");
manager.RegisterMenu("players", "ban", "Ban Player", CreateBanMenu, "@css/ban"); manager.RegisterMenu("players", "ban", "sa_ban", CreateBanMenu, "@css/ban");
manager.RegisterMenu("players", "gag", "Gag Player", CreateGagMenu, "@css/chat"); manager.RegisterMenu("players", "gag", "sa_gag", CreateGagMenu, "@css/chat");
manager.RegisterMenu("players", "mute", "Mute Player", CreateMuteMenu, "@css/chat"); manager.RegisterMenu("players", "mute", "sa_mute", CreateMuteMenu, "@css/chat");
manager.RegisterMenu("players", "silence", "Silence Player", CreateSilenceMenu, "@css/chat"); manager.RegisterMenu("players", "silence", "sa_silence", CreateSilenceMenu, "@css/chat");
manager.RegisterMenu("players", "team", "Force Team", CreateForceTeamMenu, "@css/kick"); manager.RegisterMenu("players", "team", "sa_team_force", CreateForceTeamMenu, "@css/kick");
// Server category menus // Server category menus - using translation keys
manager.RegisterMenu("server", "plugins", "Manage Plugins", CreatePluginsMenu, "@css/root"); manager.RegisterMenu("server", "plugins", "sa_menu_pluginsmanager_title", CreatePluginsMenu, "@css/root");
manager.RegisterMenu("server", "changemap", "Change Map", CreateChangeMapMenu, "@css/changemap"); manager.RegisterMenu("server", "changemap", "sa_changemap", CreateChangeMapMenu, "@css/changemap");
manager.RegisterMenu("server", "restart", "Restart Game", CreateRestartGameMenu, "@css/generic"); manager.RegisterMenu("server", "restart", "sa_restart_game", CreateRestartGameMenu, "@css/generic");
manager.RegisterMenu("server", "custom", "Custom Commands", CreateCustomCommandsMenu, "@css/generic"); manager.RegisterMenu("server", "custom", "sa_menu_custom_commands", CreateCustomCommandsMenu, "@css/generic");
// Admin category menus // Admin category menus - using translation keys
manager.RegisterMenu("admin", "add", "Add Admin", CreateAddAdminMenu, "@css/root"); manager.RegisterMenu("admin", "add", "sa_admin_add", CreateAddAdminMenu, "@css/root");
manager.RegisterMenu("admin", "remove", "Remove Admin", CreateRemoveAdminMenu, "@css/root"); manager.RegisterMenu("admin", "remove", "sa_admin_remove", CreateRemoveAdminMenu, "@css/root");
manager.RegisterMenu("admin", "reload", "Reload Admins", CreateReloadAdminsMenu, "@css/root"); manager.RegisterMenu("admin", "reload", "sa_admin_reload", CreateReloadAdminsMenu, "@css/root");
} }
/// <summary> /// <summary>
@@ -49,14 +51,15 @@ public abstract class BasicMenu
private static MenuBuilder CreateSlapMenu(CCSPlayerController admin) private static MenuBuilder CreateSlapMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var slapMenu = new MenuBuilder(localizer?["sa_slap"] ?? "Slap Player"); var slapMenu = new MenuBuilder("sa_slap", admin, localizer);
var players = Helper.GetValidPlayers().Where(admin.CanTarget); var players = Helper.GetValidPlayers().Where(admin.CanTarget);
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
slapMenu.AddSubMenu(playerName, () => CreateSlapDamageMenu(admin, player)); var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
slapMenu.AddSubMenu(playerName, () => CreateSlapDamageMenu(admin, capturedPlayer));
} }
return slapMenu.WithBackButton(); return slapMenu.WithBackButton();
@@ -70,18 +73,25 @@ public abstract class BasicMenu
/// <returns>A MenuBuilder instance for the slap damage menu.</returns> /// <returns>A MenuBuilder instance for the slap damage menu.</returns>
private static MenuBuilder CreateSlapDamageMenu(CCSPlayerController admin, CCSPlayerController target) private static MenuBuilder CreateSlapDamageMenu(CCSPlayerController admin, CCSPlayerController target)
{ {
var slapDamageMenu = new MenuBuilder($"Slap: {target.PlayerName}"); 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 damages = new[] { 0, 1, 5, 10, 50, 100 }; var damages = new[] { 0, 1, 5, 10, 50, 100 };
foreach (var damage in damages) foreach (var damage in damages)
{ {
slapDamageMenu.AddOption($"{damage} HP", _ => slapDamageMenu.AddOption($"{damage} HP", currentAdmin =>
{ {
if (target.IsValid) if (target.IsValid)
{ {
CS2_SimpleAdmin.Slap(admin, target, damage); CS2_SimpleAdmin.Slap(currentAdmin, target, damage);
// Keep menu open for consecutive slaps // Reopen the same menu (not create new one) to keep back button working
CreateSlapDamageMenu(admin, target).OpenMenu(admin); slapDamageMenu.OpenMenu(currentAdmin);
} }
}); });
} }
@@ -97,18 +107,19 @@ public abstract class BasicMenu
private static MenuBuilder CreateSlayMenu(CCSPlayerController admin) private static MenuBuilder CreateSlayMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var slayMenu = new MenuBuilder(localizer?["sa_slay"] ?? "Slay Player"); var slayMenu = new MenuBuilder("sa_slay", admin, localizer);
var players = Helper.GetValidPlayers().Where(admin.CanTarget); var players = Helper.GetValidPlayers().Where(admin.CanTarget);
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
slayMenu.AddOption(playerName, _ => slayMenu.AddOption(playerName, _ =>
{ {
if (player.IsValid) if (capturedPlayer.IsValid)
{ {
CS2_SimpleAdmin.Slay(admin, player); CS2_SimpleAdmin.Slay(admin, capturedPlayer);
} }
}); });
} }
@@ -124,19 +135,20 @@ public abstract class BasicMenu
private static MenuBuilder CreateKickMenu(CCSPlayerController admin) private static MenuBuilder CreateKickMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var kickMenu = new MenuBuilder(localizer?["sa_kick"] ?? "Kick Player"); var kickMenu = new MenuBuilder("sa_kick", admin, localizer);
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, player, "Kick", PenaltyType.Kick, var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, capturedPlayer, "Kick", PenaltyType.Kick,
(_, _, reason) => (_, _, reason) =>
{ {
if (player.IsValid) if (capturedPlayer.IsValid)
{ {
CS2_SimpleAdmin.Instance.Kick(admin, player, reason, admin.PlayerName); CS2_SimpleAdmin.Instance.Kick(admin, capturedPlayer, reason, admin.PlayerName);
} }
})); }));
} }
@@ -152,20 +164,21 @@ public abstract class BasicMenu
private static MenuBuilder CreateWarnMenu(CCSPlayerController admin) private static MenuBuilder CreateWarnMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var warnMenu = new MenuBuilder(localizer?["sa_warn"] ?? "Warn Player"); var warnMenu = new MenuBuilder("sa_warn", admin, localizer);
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
warnMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Warn", var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
(_, _, duration) => CreateReasonMenu(admin, player, "Warn", PenaltyType.Warn, warnMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Warn",
(_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Warn", PenaltyType.Warn,
(_, _, reason) => (_, _, reason) =>
{ {
if (player.IsValid) if (capturedPlayer.IsValid)
{ {
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason, admin.PlayerName); CS2_SimpleAdmin.Instance.Warn(admin, capturedPlayer, duration, reason, admin.PlayerName);
} }
}))); })));
} }
@@ -181,20 +194,21 @@ public abstract class BasicMenu
private static MenuBuilder CreateBanMenu(CCSPlayerController admin) private static MenuBuilder CreateBanMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var banMenu = new MenuBuilder(localizer?["sa_ban"] ?? "Ban Player"); var banMenu = new MenuBuilder("sa_ban", admin, localizer);
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
banMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Ban", var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
(_, _, duration) => CreateReasonMenu(admin, player, "Ban", PenaltyType.Ban, banMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Ban",
(_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Ban", PenaltyType.Ban,
(_, _, reason) => (_, _, reason) =>
{ {
if (player.IsValid) if (capturedPlayer.IsValid)
{ {
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason, admin.PlayerName); CS2_SimpleAdmin.Instance.Ban(admin, capturedPlayer, duration, reason, admin.PlayerName);
} }
}))); })));
} }
@@ -210,20 +224,21 @@ public abstract class BasicMenu
private static MenuBuilder CreateGagMenu(CCSPlayerController admin) private static MenuBuilder CreateGagMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var gagMenu = new MenuBuilder(localizer?["sa_gag"] ?? "Gag Player"); var gagMenu = new MenuBuilder("sa_gag", admin, localizer);
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
gagMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Gag", var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
(_, _, duration) => CreateReasonMenu(admin, player, "Gag", PenaltyType.Gag, gagMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Gag",
(_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Gag", PenaltyType.Gag,
(_, _, reason) => (_, _, reason) =>
{ {
if (player.IsValid) if (capturedPlayer.IsValid)
{ {
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason); CS2_SimpleAdmin.Instance.Gag(admin, capturedPlayer, duration, reason);
} }
}))); })));
} }
@@ -239,20 +254,21 @@ public abstract class BasicMenu
private static MenuBuilder CreateMuteMenu(CCSPlayerController admin) private static MenuBuilder CreateMuteMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var muteMenu = new MenuBuilder(localizer?["sa_mute"] ?? "Mute Player"); var muteMenu = new MenuBuilder("sa_mute", admin, localizer);
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
muteMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Mute", var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
(_, _, duration) => CreateReasonMenu(admin, player, "Mute", PenaltyType.Mute, muteMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Mute",
(_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Mute", PenaltyType.Mute,
(_, _, reason) => (_, _, reason) =>
{ {
if (player.IsValid) if (capturedPlayer.IsValid)
{ {
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason); CS2_SimpleAdmin.Instance.Mute(admin, capturedPlayer, duration, reason);
} }
}))); })));
} }
@@ -268,20 +284,21 @@ public abstract class BasicMenu
private static MenuBuilder CreateSilenceMenu(CCSPlayerController admin) private static MenuBuilder CreateSilenceMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var silenceMenu = new MenuBuilder(localizer?["sa_silence"] ?? "Silence Player"); var silenceMenu = new MenuBuilder("sa_silence", admin, localizer);
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
silenceMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Silence", var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
(_, _, duration) => CreateReasonMenu(admin, player, "Silence", PenaltyType.Silence, silenceMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, capturedPlayer, "Silence",
(_, _, duration) => CreateReasonMenu(admin, capturedPlayer, "Silence", PenaltyType.Silence,
(_, _, reason) => (_, _, reason) =>
{ {
if (player.IsValid) if (capturedPlayer.IsValid)
{ {
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason); CS2_SimpleAdmin.Instance.Silence(admin, capturedPlayer, duration, reason);
} }
}))); })));
} }
@@ -297,14 +314,15 @@ public abstract class BasicMenu
private static MenuBuilder CreateForceTeamMenu(CCSPlayerController admin) private static MenuBuilder CreateForceTeamMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var teamMenu = new MenuBuilder(localizer?["sa_team_force"] ?? "Force Team"); var teamMenu = new MenuBuilder("sa_team_force", admin, localizer);
var players = Helper.GetValidPlayers().Where(p => admin.CanTarget(p)); var players = Helper.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
teamMenu.AddSubMenu(playerName, () => CreateTeamSelectionMenu(admin, player)); var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
teamMenu.AddSubMenu(playerName, () => CreateTeamSelectionMenu(admin, capturedPlayer));
} }
return teamMenu.WithBackButton(); return teamMenu.WithBackButton();
@@ -319,14 +337,32 @@ public abstract class BasicMenu
private static MenuBuilder CreateTeamSelectionMenu(CCSPlayerController admin, CCSPlayerController target) private static MenuBuilder CreateTeamSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var teamSelectionMenu = new MenuBuilder($"Force Team: {target.PlayerName}");
// 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 teams = new[] var teams = new[]
{ {
(localizer?["sa_team_ct"] ?? "CT", "ct", CsTeam.CounterTerrorist), (ctName, "ct", CsTeam.CounterTerrorist),
(localizer?["sa_team_t"] ?? "T", "t", CsTeam.Terrorist), (tName, "t", CsTeam.Terrorist),
(localizer?["sa_team_swap"] ?? "Swap", "swap", CsTeam.Spectator), (swapName, "swap", CsTeam.Spectator),
(localizer?["sa_team_spec"] ?? "Spec", "spec", CsTeam.Spectator) (specName, "spec", CsTeam.Spectator)
}; };
foreach (var (name, teamName, teamNum) in teams) foreach (var (name, teamName, teamNum) in teams)
@@ -351,7 +387,7 @@ public abstract class BasicMenu
private static MenuBuilder CreatePluginsMenu(CCSPlayerController admin) private static MenuBuilder CreatePluginsMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var pluginsMenu = new MenuBuilder(localizer?["sa_menu_pluginsmanager_title"] ?? "Manage Plugins"); var pluginsMenu = new MenuBuilder("sa_menu_pluginsmanager_title", admin, localizer);
pluginsMenu.AddOption("Open Plugins Manager", _ => pluginsMenu.AddOption("Open Plugins Manager", _ =>
{ {
@@ -369,7 +405,7 @@ public abstract class BasicMenu
private static MenuBuilder CreateChangeMapMenu(CCSPlayerController admin) private static MenuBuilder CreateChangeMapMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var mapMenu = new MenuBuilder(localizer?["sa_changemap"] ?? "Change Map"); var mapMenu = new MenuBuilder("sa_changemap", admin, localizer);
// Add default maps // Add default maps
var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps; var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps;
@@ -402,7 +438,7 @@ public abstract class BasicMenu
private static MenuBuilder CreateRestartGameMenu(CCSPlayerController admin) private static MenuBuilder CreateRestartGameMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var restartMenu = new MenuBuilder(localizer?["sa_restart_game"] ?? "Restart Game"); var restartMenu = new MenuBuilder("sa_restart_game", admin, localizer);
restartMenu.AddOption("Restart Round", _ => restartMenu.AddOption("Restart Round", _ =>
{ {
@@ -420,7 +456,7 @@ public abstract class BasicMenu
private static MenuBuilder CreateCustomCommandsMenu(CCSPlayerController admin) private static MenuBuilder CreateCustomCommandsMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var customMenu = new MenuBuilder(localizer?["sa_menu_custom_commands"] ?? "Custom Commands"); var customMenu = new MenuBuilder("sa_menu_custom_commands", admin, localizer);
var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands; var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
@@ -455,14 +491,15 @@ public abstract class BasicMenu
private static MenuBuilder CreateAddAdminMenu(CCSPlayerController admin) private static MenuBuilder CreateAddAdminMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var addAdminMenu = new MenuBuilder(localizer?["sa_admin_add"] ?? "Add Admin"); var addAdminMenu = new MenuBuilder("sa_admin_add", admin, localizer);
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p)); var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players) foreach (var player in players)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
addAdminMenu.AddSubMenu(playerName, () => CreateAdminFlagsMenu(admin, player)); var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
addAdminMenu.AddSubMenu(playerName, () => CreateAdminFlagsMenu(admin, capturedPlayer));
} }
return addAdminMenu.WithBackButton(); return addAdminMenu.WithBackButton();
@@ -476,7 +513,16 @@ public abstract class BasicMenu
/// <returns>A MenuBuilder instance for the admin flags menu.</returns> /// <returns>A MenuBuilder instance for the admin flags menu.</returns>
private static MenuBuilder CreateAdminFlagsMenu(CCSPlayerController admin, CCSPlayerController target) private static MenuBuilder CreateAdminFlagsMenu(CCSPlayerController admin, CCSPlayerController target)
{ {
var flagsMenu = new MenuBuilder($"Add Admin: {target.PlayerName}"); 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);
foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags) foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags)
{ {
@@ -501,7 +547,7 @@ public abstract class BasicMenu
private static MenuBuilder CreateRemoveAdminMenu(CCSPlayerController admin) private static MenuBuilder CreateRemoveAdminMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var removeAdminMenu = new MenuBuilder(localizer?["sa_admin_remove"] ?? "Remove Admin"); var removeAdminMenu = new MenuBuilder("sa_admin_remove", admin, localizer);
var adminPlayers = Helper.GetValidPlayers().Where(p => var adminPlayers = Helper.GetValidPlayers().Where(p =>
AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0 && AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0 &&
@@ -510,12 +556,13 @@ public abstract class BasicMenu
foreach (var player in adminPlayers) foreach (var player in adminPlayers)
{ {
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName; var capturedPlayer = player; // Capture to local variable to avoid closure issue
var playerName = capturedPlayer.PlayerName.Length > 26 ? capturedPlayer.PlayerName[..26] : capturedPlayer.PlayerName;
removeAdminMenu.AddOption(playerName, _ => removeAdminMenu.AddOption(playerName, _ =>
{ {
if (player.IsValid) if (capturedPlayer.IsValid)
{ {
CS2_SimpleAdmin.Instance.RemoveAdmin(admin, player.SteamID.ToString()); CS2_SimpleAdmin.Instance.RemoveAdmin(admin, capturedPlayer.SteamID.ToString());
} }
}); });
} }
@@ -531,7 +578,7 @@ public abstract class BasicMenu
private static MenuBuilder CreateReloadAdminsMenu(CCSPlayerController admin) private static MenuBuilder CreateReloadAdminsMenu(CCSPlayerController admin)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var reloadMenu = new MenuBuilder(localizer?["sa_admin_reload"] ?? "Reload Admins"); var reloadMenu = new MenuBuilder("sa_admin_reload", admin, localizer);
reloadMenu.AddOption("Reload Admins", _ => reloadMenu.AddOption("Reload Admins", _ =>
{ {
@@ -546,20 +593,40 @@ public abstract class BasicMenu
/// </summary> /// </summary>
/// <param name="admin">The admin player selecting duration.</param> /// <param name="admin">The admin player selecting duration.</param>
/// <param name="player">The target player for the penalty.</param> /// <param name="player">The target player for the penalty.</param>
/// <param name="actionName">The name of the penalty action.</param> /// <param name="actionName">The name of the penalty action (e.g., "Kick", "Ban").</param>
/// <param name="onSelectAction">Callback action executed when duration is selected.</param> /// <param name="onSelectAction">Callback function that returns the next menu when duration is selected.</param>
/// <returns>A MenuBuilder instance for the duration menu.</returns> /// <returns>A MenuBuilder instance for the duration menu.</returns>
private static MenuBuilder CreateDurationMenu(CCSPlayerController admin, CCSPlayerController player, string actionName, private static MenuBuilder CreateDurationMenu(CCSPlayerController admin, CCSPlayerController player, string actionName,
Action<CCSPlayerController, CCSPlayerController, int> onSelectAction) Func<CCSPlayerController, CCSPlayerController, int, MenuBuilder> onSelectAction)
{ {
var durationMenu = new MenuBuilder($"{actionName} Duration: {player.PlayerName}"); 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}");
foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations)
{ {
durationMenu.AddOption(durationItem.Name, _ => var capturedDuration = durationItem.Duration; // Capture to avoid closure issue
{ durationMenu.AddSubMenu(durationItem.Name, () => onSelectAction(admin, player, capturedDuration));
onSelectAction(admin, player, durationItem.Duration);
});
} }
return durationMenu.WithBackButton(); return durationMenu.WithBackButton();
@@ -570,14 +637,36 @@ public abstract class BasicMenu
/// </summary> /// </summary>
/// <param name="admin">The admin player selecting reason.</param> /// <param name="admin">The admin player selecting reason.</param>
/// <param name="player">The target player for the penalty.</param> /// <param name="player">The target player for the penalty.</param>
/// <param name="actionName">The name of the penalty action.</param> /// <param name="actionName">The name of the penalty action (e.g., "Kick", "Ban").</param>
/// <param name="penaltyType">The type of penalty to determine which reason list to use.</param> /// <param name="penaltyType">The type of penalty to determine which reason list to use.</param>
/// <param name="onSelectAction">Callback action executed when reason is selected.</param> /// <param name="onSelectAction">Callback action executed when reason is selected.</param>
/// <returns>A MenuBuilder instance for the reason menu.</returns> /// <returns>A MenuBuilder instance for the reason menu.</returns>
private static MenuBuilder CreateReasonMenu(CCSPlayerController admin, CCSPlayerController player, string actionName, private static MenuBuilder CreateReasonMenu(CCSPlayerController admin, CCSPlayerController player, string actionName,
PenaltyType penaltyType, Action<CCSPlayerController, CCSPlayerController, string> onSelectAction) PenaltyType penaltyType, Action<CCSPlayerController, CCSPlayerController, string> onSelectAction)
{ {
var reasonMenu = new MenuBuilder($"{actionName} Reason: {player.PlayerName}"); 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 reasons = penaltyType switch var reasons = penaltyType switch
{ {

View File

@@ -8,12 +8,28 @@ public static class DurationMenu
public static void OpenMenu(CCSPlayerController admin, string menuName, CCSPlayerController player, Action<CCSPlayerController, CCSPlayerController, int> onSelectAction) public static void OpenMenu(CCSPlayerController admin, string menuName, CCSPlayerController player, Action<CCSPlayerController, CCSPlayerController, int> onSelectAction)
{ {
var menu = AdminMenu.CreateMenu(menuName); var menu = AdminMenu.CreateMenu(menuName);
foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations) 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)
{ {
menu?.AddMenuOption(durationItem.Name, (_, _) => { onSelectAction(admin, player, durationItem.Duration); }); var duration = durationItem.Duration; // Capture in local variable
var name = durationItem.Name;
menu.AddMenuOption(name, (controller, option) =>
{
capturedAction(capturedAdmin, capturedPlayer, duration);
});
} }
if (menu != null) AdminMenu.OpenMenu(admin, menu); AdminMenu.OpenMenu(admin, menu);
} }
public static void OpenMenu(CCSPlayerController admin, string menuName, DisconnectedPlayer player, Action<CCSPlayerController, DisconnectedPlayer, int> onSelectAction) public static void OpenMenu(CCSPlayerController admin, string menuName, DisconnectedPlayer player, Action<CCSPlayerController, DisconnectedPlayer, int> onSelectAction)

View File

@@ -49,25 +49,27 @@ public static class ManagePlayersMenu
if (AdminManager.CommandIsOverriden("css_warn") if (AdminManager.CommandIsOverriden("css_warn")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_warn")) ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_warn"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick"))
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)))); 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))));
if (hasBan) if (hasBan)
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)))); 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))));
if (hasChat) if (hasChat)
{ {
if (AdminManager.CommandIsOverriden("css_gag") if (AdminManager.CommandIsOverriden("css_gag")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gag")) ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gag"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
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)))); 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))));
if (AdminManager.CommandIsOverriden("css_mute") if (AdminManager.CommandIsOverriden("css_mute")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_mute")) ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_mute"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
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)))); 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))));
if (AdminManager.CommandIsOverriden("css_silence") if (AdminManager.CommandIsOverriden("css_silence")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_silence")) ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_silence"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
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)))); 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))));
} }
if (AdminManager.CommandIsOverriden("css_team") if (AdminManager.CommandIsOverriden("css_team")
@@ -162,19 +164,6 @@ public static class ManagePlayersMenu
CS2_SimpleAdmin.MenuApi?.CloseMenu(admin); 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) private static void Ban(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)

View File

@@ -1,28 +1,89 @@
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using Microsoft.Extensions.Localization;
namespace CS2_SimpleAdmin.Menus; namespace CS2_SimpleAdmin.Menus;
public class MenuBuilder(string title) public class MenuBuilder
{ {
private readonly string _title;
private readonly CCSPlayerController? _player;
private readonly IStringLocalizer? _localizer;
private readonly List<MenuOption> _options = []; private readonly List<MenuOption> _options = [];
private MenuBuilder? _parentMenu; private MenuBuilder? _parentMenu;
private Action<CCSPlayerController>? _backAction; private Action<CCSPlayerController>? _backAction;
private Action<CCSPlayerController>? _resetAction; private Action<CCSPlayerController>? _resetAction;
/// <summary>
/// Constructor for player-localized menu with translation key
/// </summary>
public MenuBuilder(string titleKey, CCSPlayerController player, IStringLocalizer? localizer = null)
{
_title = titleKey;
_player = player;
_localizer = localizer ?? CS2_SimpleAdmin._localizer;
}
/// <summary>
/// Constructor for static title (backward compatibility)
/// </summary>
public MenuBuilder(string title)
{
_title = title;
_player = null;
_localizer = null;
}
/// <summary>
/// Gets the localized title for the player
/// </summary>
private string GetLocalizedTitle()
{
if (_player != null && _localizer != null)
{
using (new WithTemporaryCulture(_player.GetLanguage()))
{
return _localizer[_title];
}
}
return _title;
}
/// <summary> /// <summary>
/// Adds a menu option with an action. /// Adds a menu option with an action.
/// </summary> /// </summary>
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null) /// <param name="name">Display name or translation key</param>
/// <param name="action">Action to perform when selected</param>
/// <param name="disabled">Whether the option is disabled</param>
/// <param name="permission">Required permission</param>
/// <param name="isTranslationKey">If true, name is a translation key to be localized</param>
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null, bool isTranslationKey = false)
{ {
_options.Add(new MenuOption _options.Add(new MenuOption
{ {
Name = name, Name = name,
Action = action, Action = action,
Disabled = disabled, Disabled = disabled,
Permission = permission Permission = permission,
IsTranslationKey = isTranslationKey
}); });
return this; return this;
} }
/// <summary>
/// Gets the localized name for a menu option
/// </summary>
private string GetLocalizedOptionName(MenuOption option)
{
if (option.IsTranslationKey && _player != null && _localizer != null)
{
using (new WithTemporaryCulture(_player.GetLanguage()))
{
return _localizer[option.Name];
}
}
return option.Name;
}
/// <summary> /// <summary>
/// Adds a menu option that opens a submenu. /// Adds a menu option that opens a submenu.
/// </summary> /// </summary>
@@ -99,8 +160,11 @@ public class MenuBuilder(string title)
{ {
if (!player.IsValid) return; if (!player.IsValid) return;
// Get localized title
var localizedTitle = GetLocalizedTitle();
// Use MenuManager dependency // Use MenuManager dependency
var menu = Helper.CreateMenu(title, _backAction); var menu = Helper.CreateMenu(localizedTitle, _backAction);
if (menu == null) return; if (menu == null) return;
foreach (var option in _options) foreach (var option in _options)
@@ -115,7 +179,10 @@ public class MenuBuilder(string title)
} }
} }
menu.AddMenuOption(option.Name, (menuPlayer, menuOption) => // Get localized option name
var localizedName = GetLocalizedOptionName(option);
menu.AddMenuOption(localizedName, (menuPlayer, menuOption) =>
{ {
option.Action?.Invoke(menuPlayer); option.Action?.Invoke(menuPlayer);
}, option.Disabled); }, option.Disabled);
@@ -166,5 +233,6 @@ public class MenuOption
public Action<CCSPlayerController>? Action { get; set; } public Action<CCSPlayerController>? Action { get; set; }
public bool Disabled { get; set; } public bool Disabled { get; set; }
public string? Permission { get; set; } public string? Permission { get; set; }
public bool IsTranslationKey { get; set; }
} }

View File

@@ -1,4 +1,5 @@
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Entities;
@@ -38,6 +39,26 @@ public class MenuManager
}; };
} }
/// <summary>
/// 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!
/// </summary>
/// <param name="categoryId">Unique identifier for the category.</param>
/// <param name="categoryNameKey">Translation key from module's lang files.</param>
/// <param name="permission">Required permission to access this category.</param>
/// <param name="moduleLocalizer">Module's IStringLocalizer for per-player translation.</param>
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<string, Func<CCSPlayerController, MenuBuilder>>(),
ModuleLocalizer = moduleLocalizer // Store module's localizer
};
}
/// <summary> /// <summary>
/// Registers a menu within a category (API for other plugins). /// Registers a menu within a category (API for other plugins).
/// </summary> /// </summary>
@@ -66,6 +87,37 @@ public class MenuManager
} }
} }
/// <summary>
/// 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!
/// </summary>
/// <param name="categoryId">The category to add this menu to.</param>
/// <param name="menuId">Unique identifier for the menu.</param>
/// <param name="menuNameKey">Translation key from module's lang files.</param>
/// <param name="menuFactory">Factory function that creates the menu for a player.</param>
/// <param name="permission">Required permission to access this menu (optional).</param>
/// <param name="commandName">Command name for permission override checking (optional).</param>
/// <param name="moduleLocalizer">Module's IStringLocalizer for per-player translation.</param>
public void RegisterMenu(string categoryId, string menuId, string menuNameKey, Func<CCSPlayerController, MenuBuilder> 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;
}
}
/// <summary> /// <summary>
/// Unregisters a menu from a category. /// Unregisters a menu from a category.
/// </summary> /// </summary>
@@ -88,7 +140,7 @@ public class MenuManager
public MenuBuilder CreateMainMenu(CCSPlayerController player) public MenuBuilder CreateMainMenu(CCSPlayerController player)
{ {
var localizer = CS2_SimpleAdmin._localizer; var localizer = CS2_SimpleAdmin._localizer;
var mainMenu = new MenuBuilder(localizer?["sa_title"] ?? "SimpleAdmin"); var mainMenu = new MenuBuilder("sa_title", player, localizer);
foreach (var category in _menuCategories.Values) foreach (var category in _menuCategories.Values)
{ {
@@ -98,8 +150,23 @@ public class MenuManager
if (!AdminManager.PlayerHasPermissions(steamId, category.Permission)) if (!AdminManager.PlayerHasPermissions(steamId, category.Permission))
continue; 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 // Pass player to CreateCategoryMenu
mainMenu.AddSubMenu(category.Name, () => CreateCategoryMenu(category, player), mainMenu.AddSubMenu(localizedCategoryName, () => CreateCategoryMenu(category, player),
permission: category.Permission); permission: category.Permission);
} }
@@ -114,7 +181,24 @@ public class MenuManager
/// <returns>A MenuBuilder instance for the category menu.</returns> /// <returns>A MenuBuilder instance for the category menu.</returns>
private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player) private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player)
{ {
var categoryMenu = new MenuBuilder(category.Name); 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);
foreach (var kvp in category.MenuFactories) foreach (var kvp in category.MenuFactories)
{ {
@@ -159,8 +243,30 @@ public class MenuManager
continue; 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 // Call the actual factory with player parameter
categoryMenu.AddSubMenu(menuName, () => menuFactory(player), permission: permission); categoryMenu.AddSubMenu(localizedMenuName, () => menuFactory(player), permission: permission);
} }
return categoryMenu.WithBackButton(); return categoryMenu.WithBackButton();
@@ -190,12 +296,12 @@ public class MenuManager
/// </summary> /// </summary>
public void InitializeDefaultCategories() public void InitializeDefaultCategories()
{ {
var localizer = CS2_SimpleAdmin._localizer; // Register categories with translation keys instead of translated names
// The actual translation will happen per-player in CreateMainMenu/CreateCategoryMenu
RegisterCategory("players", localizer?["sa_menu_players_manage"] ?? "Manage Players", "@css/generic"); RegisterCategory("players", "sa_menu_players_manage", "@css/generic");
RegisterCategory("server", localizer?["sa_menu_server_manage"] ?? "Server Management", "@css/generic"); RegisterCategory("server", "sa_menu_server_manage", "@css/generic");
// RegisterCategory("fun", localizer?["sa_menu_fun_commands"] ?? "Fun Commands", "@css/generic"); // RegisterCategory("fun", "sa_menu_fun_commands", "@css/generic");
RegisterCategory("admin", localizer?["sa_menu_admins_manage"] ?? "Admin Management", "@css/root"); RegisterCategory("admin", "sa_menu_admins_manage", "@css/root");
} }
/// <summary> /// <summary>
@@ -222,4 +328,17 @@ public class MenuCategory
public Dictionary<string, string> MenuNames { get; set; } = []; public Dictionary<string, string> MenuNames { get; set; } = [];
public Dictionary<string, string> MenuPermissions { get; set; } = []; public Dictionary<string, string> MenuPermissions { get; set; } = [];
public Dictionary<string, string> MenuCommandNames { get; set; } = []; public Dictionary<string, string> MenuCommandNames { get; set; } = [];
// 🆕 NEW: Support for per-player localization in modules
/// <summary>
/// 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).
/// </summary>
public Microsoft.Extensions.Localization.IStringLocalizer? ModuleLocalizer { get; set; }
/// <summary>
/// Stores IStringLocalizer for each menu that uses module localization.
/// Key: menuId, Value: module's localizer
/// </summary>
public Dictionary<string, Microsoft.Extensions.Localization.IStringLocalizer> MenuLocalizers { get; set; } = [];
} }

View File

@@ -41,11 +41,12 @@ public static class PlayersMenu
continue; continue;
var enabled = admin.CanTarget(player); var enabled = admin.CanTarget(player);
var capturedPlayer = player; // Capture in local variable to avoid closure issues
if (optionName != null) if (optionName != null)
menu?.AddMenuOption(optionName, (_, _) => menu?.AddMenuOption(optionName, (controller, option) =>
{ {
if (player != null) onSelectAction.Invoke(admin, player); if (capturedPlayer != null) onSelectAction.Invoke(admin, capturedPlayer);
}, },
!enabled); !enabled);