SimpleAdmin/CS2-SimpleAdmin/Menus/MenuManager.cs
Astra cf1090cbd2
Some checks are pending
Build and Publish / build (push) Waiting to run
Build and Publish / publish (push) Blocked by required conditions
zio fork 1.8.2b-zio9: admin menu honours command overrides, @zio/penalties
Every player and server entry in the admin menu names its command, and an overridden entry is
checked with the override's check_type ("any" passes with one flag), the way CSS checks the typed
command; upstream required every flag. css_penalties for someone else needs @zio/penalties or
@css/root instead of @css/kick.
2026-09-29 12:44:15 +01:00

345 lines
15 KiB
C#

using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
public class MenuManager
{
private static MenuManager? _instance;
public static MenuManager Instance => _instance ??= new MenuManager();
private readonly Dictionary<string, Func<CCSPlayerController, MenuBuilder>> _menuFactories = [];
private readonly Dictionary<string, MenuCategory> _menuCategories = [];
/// <summary>
/// Provides public access to menu categories (for API usage).
/// </summary>
/// <returns>Dictionary of menu categories keyed by category ID.</returns>
public Dictionary<string, MenuCategory> GetMenuCategories()
{
return _menuCategories;
}
/// <summary>
/// Registers a new menu category with specified permissions.
/// </summary>
/// <param name="categoryId">Unique identifier for the category.</param>
/// <param name="categoryName">Display name of the category.</param>
/// <param name="permission">Required permission to access this category (default: @css/generic).</param>
public void RegisterCategory(string categoryId, string categoryName, string permission = "@css/generic")
{
_menuCategories[categoryId] = new MenuCategory
{
Id = categoryId,
Name = categoryName,
Permission = permission,
MenuFactories = new Dictionary<string, Func<CCSPlayerController, MenuBuilder>>()
};
}
/// <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>
/// Registers a menu within a category (API for other plugins).
/// </summary>
/// <param name="categoryId">The category to add this menu to.</param>
/// <param name="menuId">Unique identifier for the menu.</param>
/// <param name="menuName">Display name of the menu.</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, e.g., "css_god").</param>
public void RegisterMenu(string categoryId, string menuId, string menuName, Func<CCSPlayerController, MenuBuilder> menuFactory, string? permission = null, string? commandName = null)
{
if (!_menuCategories.ContainsKey(categoryId))
{
RegisterCategory(categoryId, categoryId); // Auto-create category if it doesn't exist
}
_menuCategories[categoryId].MenuFactories[menuId] = menuFactory;
_menuCategories[categoryId].MenuNames[menuId] = menuName;
if (permission != null)
{
_menuCategories[categoryId].MenuPermissions[menuId] = permission;
}
if (commandName != null)
{
_menuCategories[categoryId].MenuCommandNames[menuId] = commandName;
}
}
/// <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>
/// Unregisters a menu from a category.
/// </summary>
/// <param name="categoryId">The category containing the menu.</param>
/// <param name="menuId">The menu to unregister.</param>
public void UnregisterMenu(string categoryId, string menuId)
{
if (!_menuCategories.TryGetValue(categoryId, out var category)) return;
category.MenuFactories.Remove(menuId);
_menuCategories[categoryId].MenuNames.Remove(menuId);
_menuCategories[categoryId].MenuPermissions.Remove(menuId);
_menuCategories[categoryId].MenuCommandNames.Remove(menuId);
}
/// <summary>
/// Creates the main admin menu for a player with accessible categories.
/// </summary>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the main menu.</returns>
public MenuBuilder CreateMainMenu(CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
var mainMenu = new MenuBuilder("sa_title", player, localizer);
foreach (var category in _menuCategories.Values)
{
if (category.MenuFactories.Count <= 0) continue;
// Check category permissions
var steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, category.Permission))
continue;
// Get localized category name for this player
// If category has a module localizer, use it; otherwise use main plugin localizer
string localizedCategoryName;
using (new WithTemporaryCulture(player.GetLanguage()))
{
if (category.ModuleLocalizer != null)
{
localizedCategoryName = category.ModuleLocalizer[category.Name] ?? category.Name;
}
else
{
localizedCategoryName = localizer?[category.Name] ?? category.Name;
}
}
// Pass player to CreateCategoryMenu
mainMenu.AddSubMenu(localizedCategoryName, () => CreateCategoryMenu(category, player),
permission: category.Permission);
}
return mainMenu;
}
/// <summary>
/// Creates a category submenu containing all registered menus in that category.
/// </summary>
/// <param name="category">The menu category to create.</param>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the category menu.</returns>
private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
// Get localized category name for this player
// If category has a module localizer, use it; otherwise use main plugin localizer
string localizedCategoryName;
using (new WithTemporaryCulture(player.GetLanguage()))
{
if (category.ModuleLocalizer != null)
{
localizedCategoryName = category.ModuleLocalizer[category.Name] ?? category.Name;
}
else
{
localizedCategoryName = localizer?[category.Name] ?? category.Name;
}
}
var categoryMenu = new MenuBuilder(localizedCategoryName);
foreach (var kvp in category.MenuFactories)
{
var menuId = kvp.Key;
var menuFactory = kvp.Value;
var menuName = category.MenuNames.TryGetValue(menuId, out var name) ? name : menuId;
var permission = category.MenuPermissions.TryGetValue(menuId, out var perm) ? perm : null;
var commandName = category.MenuCommandNames.TryGetValue(menuId, out var cmd) ? cmd : null;
// Check permissions with command override support
var steamId = new SteamID(player.SteamID);
// If commandName is provided, check for permission overrides
if (!string.IsNullOrEmpty(commandName))
{
bool hasPermission;
// Check if command has overridden permissions
if (AdminManager.CommandIsOverriden(commandName))
{
// zio: honour the override's check_type ("any" passes with one flag), as CSS does
// for the typed command, instead of requiring every flag.
hasPermission = Helper.CanRunOverriddenCommand(player, commandName);
}
else if (!string.IsNullOrEmpty(permission))
{
// Use default permission if no override exists
hasPermission = AdminManager.PlayerHasPermissions(steamId, permission);
}
else
{
// No permission required
hasPermission = true;
}
if (!hasPermission)
continue;
}
// Fallback to standard permission check if no commandName provided
else if (!string.IsNullOrEmpty(permission))
{
if (!AdminManager.PlayerHasPermissions(steamId, permission))
continue;
}
// Get localized menu name for this player
// If menu has its own localizer, use it; otherwise use category or main plugin localizer
string localizedMenuName;
using (new WithTemporaryCulture(player.GetLanguage()))
{
if (category.MenuLocalizers.TryGetValue(menuId, out var menuLocalizer))
{
// Menu has its own module localizer
localizedMenuName = menuLocalizer[menuName] ?? menuName;
}
else if (category.ModuleLocalizer != null)
{
// Use category's module localizer
localizedMenuName = category.ModuleLocalizer[menuName] ?? menuName;
}
else
{
// Use main plugin localizer
localizedMenuName = localizer?[menuName] ?? menuName;
}
}
// Call the actual factory with player parameter
categoryMenu.AddSubMenu(localizedMenuName, () => menuFactory(player), permission: permission);
}
return categoryMenu.WithBackButton();
}
/// <summary>
/// Opens the main admin menu for a player.
/// </summary>
/// <param name="player">The player to open the menu for.</param>
public void OpenMainMenu(CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
var steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, "@css/generic"))
{
player.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command"));
return;
}
CreateMainMenu(player).OpenMenu(player);
}
/// <summary>
/// Initializes default menu categories (Players, Server, Admin).
/// </summary>
public void InitializeDefaultCategories()
{
// Register categories with translation keys instead of translated names
// The actual translation will happen per-player in CreateMainMenu/CreateCategoryMenu
RegisterCategory("players", "sa_menu_players_manage", "@css/generic");
RegisterCategory("server", "sa_menu_server_manage", "@css/generic");
// RegisterCategory("fun", "sa_menu_fun_commands", "@css/generic");
RegisterCategory("admin", "sa_menu_admins_manage", "@css/root");
}
/// <summary>
/// Public method for creating category menus (for API usage).
/// </summary>
/// <param name="category">The menu category to create.</param>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the category menu.</returns>
public MenuBuilder CreateCategoryMenuPublic(MenuCategory category, CCSPlayerController player)
{
return CreateCategoryMenu(category, player);
}
}
/// <summary>
/// Represents a menu category containing multiple menus.
/// </summary>
public class MenuCategory
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Permission { get; set; } = "@css/generic";
public Dictionary<string, Func<CCSPlayerController, MenuBuilder>> MenuFactories { get; set; } = [];
public Dictionary<string, string> MenuNames { get; set; } = [];
public Dictionary<string, string> MenuPermissions { 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; } = [];
}