# Module Development Guide - CS2-SimpleAdmin > **🎓 New to module development?** Start with the [Fun Commands Module](./CS2-SimpleAdmin_FunCommands/) - it's a fully documented reference implementation showing all best practices! This guide explains how to create modules for CS2-SimpleAdmin with custom commands, menus, and translations. ## 📖 Table of Contents 1. [Quick Start](#-quick-start) 2. [Learning Resources](#-learning-resources) 3. [MenuContext API (New!)](#-menucontext-api-new) 4. [API Methods Reference](#-api-methods-reference) 5. [Menu Patterns](#-menu-patterns) 6. [Best Practices](#best-practices) 7. [Troubleshooting](#troubleshooting) ## 🚀 Quick Start ### Step 1: Study the Example Module The **[CS2-SimpleAdmin_FunCommands](./CS2-SimpleAdmin_FunCommands/)** module is your best learning resource. It demonstrates: ✅ Command registration from config ✅ Dynamic menu creation ✅ Per-player translation support ✅ Proper cleanup on unload ✅ Code organization with partial classes ✅ All menu patterns you'll need **Start here:** Read [`CS2-SimpleAdmin_FunCommands/README.md`](./CS2-SimpleAdmin_FunCommands/README.md) ### Step 2: Create Your Module Structure ``` YourModule/ ├── YourModule.csproj # Project file ├── YourModule.cs # Main plugin class ├── Commands.cs # Command handlers (optional) ├── Menus.cs # Menu creation (optional) ├── Config.cs # Configuration └── lang/ # Translations ├── en.json ├── pl.json └── ru.json ``` ### Step 3: Minimal Working Example (NEW MenuContext API!) ```csharp using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Capabilities; using CS2_SimpleAdminApi; public class MyModule : BasePlugin { private ICS2_SimpleAdminApi? _api; private readonly PluginCapability _pluginCapability = new("simpleadmin:api"); public override void OnAllPluginsLoaded(bool hotReload) { _api = _pluginCapability.Get(); if (_api == null) { Logger.LogError("CS2-SimpleAdmin API not found"); return; } // Register menus after API is ready _api.OnSimpleAdminReady += RegisterMenus; RegisterMenus(); // Fallback for hot reload } private void RegisterMenus() { if (_api == null) return; // 1. Register a new category // IMPORTANT: If your module has lang/ folder with translations, use YOUR module's Localizer! // Example: _api.RegisterMenuCategory("mymodule", Localizer?["mymodule_category_name"] ?? "My Module", "@css/generic"); // // This will use YOUR module's translations in server language. // For modules without translations, you can use hard-coded text: _api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); // 2. Register menu items in the category // 🆕 NEW: Use MenuContext-aware overload (no duplication!) // NOTE: Use YOUR module's Localizer if you have lang/ folder with translations: // _api.RegisterMenu("mymodule", "action1", Localizer?["mymodule_menu_action1"] ?? "Action 1", CreateAction1Menu, "@css/generic"); _api.RegisterMenu("mymodule", "action1", "Action 1", CreateAction1Menu, "@css/generic"); _api.RegisterMenu("mymodule", "action2", "Action 2", CreateAction2Menu, "@css/kick"); } // 🆕 NEW: Factory now receives MenuContext parameter private object CreateAction1Menu(CCSPlayerController admin, MenuContext context) { // Create a menu with automatic back button // Use context instead of repeating title and category! var menu = _api!.CreateMenuWithBack(context, admin); // Add menu options _api.AddMenuOption(menu, "Option 1", player => { player.PrintToChat("You selected Option 1"); }); _api.AddMenuOption(menu, "Option 2", player => { player.PrintToChat("You selected Option 2"); }); return menu; } // 🆕 NEW: MenuContext eliminates duplication here too! private object CreateAction2Menu(CCSPlayerController admin, MenuContext context) { // Use the built-in player selection menu with context return _api!.CreateMenuWithPlayers( context, // ← Contains title & category automatically! admin, player => player.IsValid && admin.CanTarget(player), (adminPlayer, targetPlayer) => { adminPlayer.PrintToChat($"You selected {targetPlayer.PlayerName}"); }); } public override void Unload(bool hotReload) { if (_api == null) return; // Clean up registered menus _api.UnregisterMenu("mymodule", "action1"); _api.UnregisterMenu("mymodule", "action2"); _api.OnSimpleAdminReady -= RegisterMenus; } } ``` ## 📚 Learning Resources ### For Beginners 1. **Start Here:** [`CS2-SimpleAdmin_FunCommands/README.md`](./CS2-SimpleAdmin_FunCommands/README.md) - Explains every file and pattern - Shows code organization - Demonstrates all menu types 2. **Read the Code:** Study these files in order: - `Config.cs` - Simple configuration - `CS2-SimpleAdmin_FunCommands.cs` - Plugin initialization - `Commands.cs` - Command registration - `Menus.cs` - Menu creation patterns 3. **Translations:** [`TRANSLATION_EXAMPLE.md`](./TRANSLATION_EXAMPLE.md) - How to use module translations - Per-player language support - Best practices ### Key Concepts | Concept | What It Does | Example File | |---------|-------------|--------------| | **API Capability** | Get access to SimpleAdmin API | `CS2-SimpleAdmin_FunCommands.cs:37` | | **Command Registration** | Register console commands | `Commands.cs:15-34` | | **Menu Registration** | Add menus to admin menu | `CS2-SimpleAdmin_FunCommands.cs:130-141` | | **Translations** | Per-player language support | `Actions.cs:20-31` | | **Cleanup** | Unregister on plugin unload | `CS2-SimpleAdmin_FunCommands.cs:63-97` | ## 🎯 Menu Patterns The FunCommands module demonstrates **3 essential menu patterns** you'll use in every module: ### Pattern 1: Simple Player Selection (NEW MenuContext API!) **When to use:** Select a player and immediately execute an action ```csharp // 🆕 NEW: Factory receives MenuContext - eliminates duplication! private object CreateGodModeMenu(CCSPlayerController admin, MenuContext context) { return _api.CreateMenuWithPlayers( context, // ← Contains title & category automatically! admin, // Admin player => player.IsValid && admin.CanTarget(player), // Filter (adminPlayer, target) => // Action { // Execute action immediately ToggleGodMode(target); }); } ``` **Why MenuContext is better:** - ❌ **Old way:** `"God Mode"` and `"yourmodule"` had to be typed twice (in RegisterMenu and CreateMenuWithPlayers) - ✅ **New way:** Context contains both automatically - no duplication, no mistakes! **See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:44-51` ### Pattern 2: Nested Menu (Player → Value) - NEW MenuContext API! **When to use:** Select a player, then select a value/option for that player ```csharp // 🆕 NEW: Uses MenuContext to eliminate duplication private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context) { var menu = _api.CreateMenuWithBack(context, admin); // ← No more repeating title & category! var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p)); foreach (var player in players) { // AddSubMenu automatically adds back button to submenu _api.AddSubMenu(menu, player.PlayerName, p => CreateHpValueMenu(admin, player)); } return menu; } // Example: Set HP menu (value selection) // Note: Submenus don't receive context - they create dynamic titles private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target) { var menu = _api.CreateMenuWithBack($"HP for {target.PlayerName}", "yourmodule", admin); var values = new[] { 50, 100, 200, 500 }; foreach (var hp in values) { _api.AddMenuOption(menu, $"{hp} HP", _ => { if (target.IsValid) // Always validate! { target.PlayerPawn.Value.Health = hp; } }); } return menu; } ``` **Benefits of MenuContext:** - ✅ Change menu title in one place (RegisterMenu) and it updates everywhere - ✅ Can't accidentally mistype category ID - ✅ Access to permission and command name from context if needed **See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:163-178` ### Pattern 3: Nested Menu with Complex Data **When to use:** Need to display more complex options (like weapons with icons, items with descriptions) ```csharp // Example: Give Weapon menu private object CreateGiveWeaponMenu(CCSPlayerController admin) { var menu = _api.CreateMenuWithBack("Give Weapon", "yourmodule", admin); var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p)); foreach (var player in players) { _api.AddSubMenu(menu, player.PlayerName, p => CreateWeaponSelectionMenu(admin, player)); } return menu; } private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { var menu = _api.CreateMenuWithBack($"Weapons for {target.PlayerName}", "yourmodule", admin); // Use cached data for performance foreach (var weapon in GetWeaponsCache()) { _api.AddMenuOption(menu, weapon.Value.ToString(), _ => { if (target.IsValid) { target.GiveNamedItem(weapon.Value); } }); } return menu; } ``` **See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:67-109` ## 🆕 MenuContext API (New!) ### What is MenuContext? `MenuContext` is a new feature that eliminates code duplication when creating menus. When you register a menu, you provide information like title, category, and permissions. Previously, you had to repeat this information in your menu factory method. Now, this information is automatically passed to your factory via `MenuContext`. ### MenuContext Properties ```csharp public class MenuContext { public string CategoryId { get; } // e.g., "fun" public string MenuId { get; } // e.g., "god" public string MenuTitle { get; } // e.g., "God Mode" public string? Permission { get; } // e.g., "@css/cheats" public string? CommandName { get; } // e.g., "css_god" } ``` ### Before vs After Comparison ```csharp // ❌ OLD API - Duplication everywhere _api.RegisterMenu("fun", "god", "God Mode", CreateGodModeMenu, "@css/cheats"); private object CreateGodModeMenu(CCSPlayerController admin) { return _api.CreateMenuWithPlayers( "God Mode", // ← Duplicated from RegisterMenu "fun", // ← Duplicated from RegisterMenu admin, filter, action); } // ✅ NEW API - No duplication! _api.RegisterMenu("fun", "god", "God Mode", CreateGodModeMenu, "@css/cheats"); private object CreateGodModeMenu(CCSPlayerController admin, MenuContext context) { return _api.CreateMenuWithPlayers( context, // ← Contains all info automatically! admin, filter, action); } ``` ### When to Use MenuContext | Menu Creation Method | Old Signature | New Signature (Recommended) | |---------------------|---------------|----------------------------| | `CreateMenuWithBack` | `(string title, string categoryId, ...)` | `(MenuContext context, ...)` | | `CreateMenuWithPlayers` | `(string title, string categoryId, ...)` | `(MenuContext context, ...)` | **Rule of thumb:** If you're creating a menu directly from a registered menu factory, use `MenuContext`. For dynamic submenus (e.g., player-specific menus), use the old API. ### Backward Compatibility The old API still works! Both signatures are supported: ```csharp // ✅ Old API - still works _api.RegisterMenu("cat", "id", "Title", (CCSPlayerController admin) => CreateOldStyleMenu(admin)); // ✅ New API - recommended _api.RegisterMenu("cat", "id", "Title", (CCSPlayerController admin, MenuContext ctx) => CreateNewStyleMenu(admin, ctx)); ``` ## 📋 API Methods Reference ### 1. Category Management #### `RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")` Registers a new menu category that appears in the main admin menu. **Parameters:** - `categoryId` - Unique identifier for the category (e.g., "fun", "vip", "economy") - `categoryName` - **TRANSLATION KEY** for the display name (e.g., "vip_category_name", NOT "VIP Features") - `permission` - Required permission to see the category (default: "@css/generic") **IMPORTANT FOR MODULES WITH TRANSLATIONS:** - If your module has a `lang/` folder with translation files, use **YOUR module's Localizer** - This will display menu names in the **server's language** (not per-player) - For per-player localization, only **CS2-SimpleAdmin's built-in menus** support this currently **Example:** ```csharp // ✅ CORRECT: Module with translations uses its own Localizer _api.RegisterMenuCategory("vip", Localizer?["vip_category_name"] ?? "VIP Features", "@css/vip"); // In YOUR module's lang/en.json: "vip_category_name": "VIP Features" // In YOUR module's lang/pl.json: "vip_category_name": "Funkcje VIP" // ✅ ALSO CORRECT: Module without translations uses hard-coded text _api.RegisterMenuCategory("vip", "VIP Features", "@css/vip"); // ❌ INCORRECT: Using translation key without Localizer _api.RegisterMenuCategory("vip", "vip_category_name", "@css/vip"); // This would display "vip_category_name" literally! ``` ### 2. Menu Registration #### `RegisterMenu(string categoryId, string menuId, string menuName, Func menuFactory, string? permission = null)` Registers a menu item within a category. **Parameters:** - `categoryId` - The category to add this menu to - `menuId` - Unique identifier for the menu - `menuName` - Display name for the menu (use **your module's Localizer** if you have translations) - `menuFactory` - Function that creates the menu when selected (receives admin player and MenuContext) - `permission` - Optional permission required to see this menu item **IMPORTANT FOR MODULES WITH TRANSLATIONS:** - Use **your module's Localizer** if you have a `lang/` folder: `Localizer?["key"] ?? "Fallback"` - This shows menu in **server language**, not per-player - Per-player localization is only available for CS2-SimpleAdmin's built-in menus **Example:** ```csharp // ✅ CORRECT: Module with translations uses its own Localizer _api.RegisterMenu("fun", "godmode", Localizer?["fun_menu_god"] ?? "God Mode", CreateGodModeMenu, "@css/cheats"); // In YOUR module's lang/en.json: "fun_menu_god": "God Mode" // In YOUR module's lang/pl.json: "fun_menu_god": "Tryb Boga" // ✅ ALSO CORRECT: Module without translations uses hard-coded text _api.RegisterMenu("fun", "godmode", "God Mode", CreateGodModeMenu, "@css/cheats"); // ❌ INCORRECT: Using translation key without Localizer _api.RegisterMenu("fun", "godmode", "fun_menu_god", CreateGodModeMenu, "@css/cheats"); // This would display "fun_menu_god" literally! ``` #### `UnregisterMenu(string categoryId, string menuId)` Removes a menu item from a category. **Example:** ```csharp _api.UnregisterMenu("fun", "godmode"); ``` ### 3. Menu Creation #### `CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)` #### `CreateMenuWithBack(MenuContext context, CCSPlayerController player)` 🆕 NEW! Creates a menu with an automatic "Back" button that returns to the category menu. **Parameters (Old API):** - `title` - Menu title - `categoryId` - Category this menu belongs to (for back navigation) - `player` - The admin player viewing the menu **Parameters (New API - Recommended):** - `context` - MenuContext containing title, category, and other metadata - `player` - The admin player viewing the menu **Returns:** `object` (MenuBuilder instance) **Example (Old API):** ```csharp var menu = _api.CreateMenuWithBack("Weapon Selection", "fun", admin); ``` **Example (New API - Recommended):** ```csharp private object CreateWeaponMenu(CCSPlayerController admin, MenuContext context) { var menu = _api.CreateMenuWithBack(context, admin); // ← Uses context! // ... add options return menu; } ``` #### `CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin, Func filter, Action onSelect)` #### `CreateMenuWithPlayers(MenuContext context, CCSPlayerController admin, Func filter, Action onSelect)` 🆕 NEW! Creates a menu with a list of players, filtered and with automatic back button. **Parameters (Old API):** - `title` - Menu title - `categoryId` - Category for back navigation - `admin` - The admin player viewing the menu - `filter` - Function to filter which players appear in the menu - `onSelect` - Action to execute when a player is selected (receives admin and target) **Parameters (New API - Recommended):** - `context` - MenuContext containing title and category - `admin` - The admin player viewing the menu - `filter` - Function to filter which players appear in the menu - `onSelect` - Action to execute when a player is selected (receives admin and target) **Returns:** `object` (MenuBuilder instance) **Example (Old API):** ```csharp return _api.CreateMenuWithPlayers("Select Player to Kick", "admin", admin, player => player.IsValid && admin.CanTarget(player), (adminPlayer, targetPlayer) => { // Kick the selected player Server.ExecuteCommand($"css_kick {targetPlayer.UserId}"); }); ``` **Example (New API - Recommended):** ```csharp private object CreateKickMenu(CCSPlayerController admin, MenuContext context) { return _api.CreateMenuWithPlayers(context, admin, player => player.IsValid && admin.CanTarget(player), (adminPlayer, targetPlayer) => { Server.ExecuteCommand($"css_kick {targetPlayer.UserId}"); }); } ``` ### 4. Menu Manipulation #### `AddMenuOption(object menu, string name, Action action, bool disabled = false, string? permission = null)` Adds a clickable option to a menu. **Parameters:** - `menu` - The menu object (from CreateMenuWithBack) - `name` - Display name of the option - `action` - Action to execute when clicked (receives the player who clicked) - `disabled` - Whether the option is disabled (grayed out) - `permission` - Optional permission required to see this option **Example:** ```csharp _api.AddMenuOption(menu, "Give AK-47", player => { player.GiveNamedItem("weapon_ak47"); }, permission: "@css/cheats"); ``` #### `AddSubMenu(object menu, string name, Func subMenuFactory, bool disabled = false, string? permission = null)` Adds a submenu option that opens another menu. **Automatically adds a back button to the submenu.** **Parameters:** - `menu` - The parent menu - `name` - Display name of the submenu option - `subMenuFactory` - Function that creates the submenu (receives the player) - `disabled` - Whether the option is disabled - `permission` - Optional permission required **Example:** ```csharp _api.AddSubMenu(menu, "Weapon Category", player => { var weaponMenu = _api.CreateMenuWithBack("Weapons", "fun", player); _api.AddMenuOption(weaponMenu, "AK-47", p => p.GiveNamedItem("weapon_ak47")); _api.AddMenuOption(weaponMenu, "AWP", p => p.GiveNamedItem("weapon_awp")); return weaponMenu; }); ``` #### `OpenMenu(object menu, CCSPlayerController player)` Opens a menu for a specific player. **Example:** ```csharp var menu = _api.CreateMenuWithBack("Custom Menu", "fun", player); _api.AddMenuOption(menu, "Test", p => p.PrintToChat("Test!")); _api.OpenMenu(menu, player); ``` ## Advanced Examples ### Nested Menus with Player Selection ```csharp private object CreateGiveWeaponMenu(CCSPlayerController admin) { var menu = _api.CreateMenuWithBack("Give Weapon", "fun", admin); var players = _api.GetValidPlayers() .Where(p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p)); foreach (var player in players) { // Add submenu for each player - automatic back button will be added _api.AddSubMenu(menu, player.PlayerName, p => CreateWeaponSelectionMenu(admin, player)); } return menu; } private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerController target) { var weaponMenu = _api.CreateMenuWithBack($"Weapons for {target.PlayerName}", "fun", admin); var weapons = new[] { "weapon_ak47", "weapon_m4a1", "weapon_awp", "weapon_deagle" }; foreach (var weapon in weapons) { _api.AddMenuOption(weaponMenu, weapon, _ => { if (target.IsValid) { target.GiveNamedItem(weapon); admin.PrintToChat($"Gave {weapon} to {target.PlayerName}"); } }); } return weaponMenu; } ``` ### Dynamic Menu with Value Selection ```csharp private object CreateSetHpMenu(CCSPlayerController admin) { var menu = _api.CreateMenuWithBack("Set HP", "admin", admin); var players = _api.GetValidPlayers().Where(p => admin.CanTarget(p)); foreach (var player in players) { _api.AddSubMenu(menu, player.PlayerName, p => CreateHpValueMenu(admin, player)); } return menu; } private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target) { var hpMenu = _api.CreateMenuWithBack($"HP for {target.PlayerName}", "admin", admin); var hpValues = new[] { 1, 50, 100, 200, 500, 1000 }; foreach (var hp in hpValues) { _api.AddMenuOption(hpMenu, $"{hp} HP", _ => { if (target.IsValid && target.PlayerPawn?.Value != null) { target.PlayerPawn.Value.Health = hp; admin.PrintToChat($"Set {target.PlayerName} HP to {hp}"); } }); } return hpMenu; } ``` ### Permission-Based Menu Options ```csharp private object CreateAdminToolsMenu(CCSPlayerController admin) { var menu = _api.CreateMenuWithBack("Admin Tools", "admin", admin); // Only root admins can see this _api.AddMenuOption(menu, "Dangerous Action", player => { player.PrintToChat("Executing dangerous action..."); }, permission: "@css/root"); // All admins can see this _api.AddMenuOption(menu, "Safe Action", player => { player.PrintToChat("Executing safe action..."); }, permission: "@css/generic"); return menu; } ``` ## Best Practices 1. **Use your module's Localizer for translations** 🌍 ```csharp // ✅ CORRECT: Module with translations uses its own Localizer _api.RegisterMenuCategory("mymodule", Localizer?["mymodule_category"] ?? "My Module", "@css/generic"); _api.RegisterMenu("mymodule", "action", Localizer?["mymodule_menu_action"] ?? "My Action", CreateMenu, "@css/generic"); // ✅ ALSO CORRECT: Module without translations uses hard-coded text _api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); _api.RegisterMenu("mymodule", "action", "My Action", CreateMenu, "@css/generic"); // ❌ WRONG: Using translation key without Localizer _api.RegisterMenuCategory("mymodule", "mymodule_category", "@css/generic"); // This would display "mymodule_category" literally! ``` **✅ Per-player localization now available!** - Both CS2-SimpleAdmin built-in menus AND module menus support per-player localization - Each player sees menus in their own language based on their `css_lang` setting - See "Advanced: Per-Player Localization for Modules" section below for implementation details 2. **Always check for API availability** ```csharp if (_api == null) return; ``` 3. **Validate player state before actions** ```csharp if (target.IsValid && target.PlayerPawn?.Value != null) { // Safe to perform action } ``` 4. **Use descriptive category and menu IDs** - Good: `"economy"`, `"vip_features"`, `"fun_commands"` - Bad: `"cat1"`, `"menu"`, `"test"` 5. **Clean up on unload** ```csharp public override void Unload(bool hotReload) { _api?.UnregisterMenu("mymodule", "mymenu"); _api.OnSimpleAdminReady -= RegisterMenus; } ``` 6. **Use appropriate permissions** - `@css/generic` - All admins - `@css/ban` - Admins who can ban - `@css/kick` - Admins who can kick - `@css/root` - Root admins only - Custom permissions from your module 7. **Handle hot reload** ```csharp _api.OnSimpleAdminReady += RegisterMenus; RegisterMenus(); // Fallback for hot reload case ``` ## Automatic Back Button The menu system **automatically adds a "Back" button** to all submenus created with: - `CreateMenuWithBack()` - Returns to the category menu - `AddSubMenu()` - Returns to the parent menu You don't need to manually add back buttons - the system handles navigation automatically! ## Getting Valid Players Use the API helper method to get valid, connected players: ```csharp var players = _api.GetValidPlayers(); // With filtering var alivePlayers = _api.GetValidPlayers() .Where(p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE); var targetablePlayers = _api.GetValidPlayers() .Where(p => admin.CanTarget(p)); ``` ## Complete Module Example See the `CS2-SimpleAdmin_FunCommands` module in the `Modules/` directory for a complete, production-ready example of: - Category registration - Multiple menu types - Nested menus with automatic back buttons - Player selection menus - Permission-based access - Proper cleanup on unload ## Troubleshooting **Q: My category doesn't appear in the admin menu** - Ensure you're calling `RegisterMenuCategory()` after the API is ready - Check that the player has the required permission - Verify the category has at least one menu registered with `RegisterMenu()` **Q: Back button doesn't work** - Make sure you're using `CreateMenuWithBack()` instead of creating menus manually - The `categoryId` parameter must match the category you registered - Use `AddSubMenu()` for nested menus - it handles back navigation automatically **Q: Menu appears but is empty** - Check that you're adding options with `AddMenuOption()` or `AddSubMenu()` - Verify permission checks aren't filtering out all options - Ensure player validation in filters isn't too restrictive **Q: API is null in OnAllPluginsLoaded** - Wait for the `OnSimpleAdminReady` event instead of immediate registration - Make sure CS2-SimpleAdmin is loaded before your module ## Advanced: Per-Player Localization for Modules (✅ NOW AVAILABLE!) **🆕 NEW:** Module menus now support **per-player localization** based on `css_lang`! Both CS2-SimpleAdmin's built-in menus AND module menus can show in each player's configured language. ### How to Use Per-Player Localization in Your Module **1. Register Category with Localizer:** ```csharp // Pass translation KEY (not translated text) and module's Localizer _api.RegisterMenuCategory( "mymodule", "mymodule_category_name", // Translation key from your lang/ folder "@css/generic", Localizer! // Your module's localizer ); ``` **2. Register Menus with Localizer:** ```csharp _api.RegisterMenu( "mymodule", "action", "mymodule_menu_action", // Translation key from your lang/ folder CreateMenu, "@css/generic", // Permission "css_mycommand", // Command name for override (optional) Localizer! // Your module's localizer ); ``` **How it works:** 1. Your module passes its `IStringLocalizer` and translation **key** (not translated text) 2. SimpleAdmin's `MenuManager` stores both the key and the localizer 3. When displaying menu to a player, MenuManager translates using: ```csharp using (new WithTemporaryCulture(player.GetLanguage())) { localizedName = moduleLocalizer[translationKey]; } ``` 4. Each player sees menus in their own language based on their `css_lang` setting! **Complete Example:** See `Modules/CS2-SimpleAdmin_FunCommands/` for a real implementation: ```csharp // Register category with per-player localization _api.RegisterMenuCategory("fun", "fun_category_name", "@css/generic", Localizer!); // Register menu with per-player localization _api.RegisterMenu("fun", "god", "fun_menu_god", CreateGodModeMenu, "@css/cheats", "css_god", Localizer!); ``` **Without Per-Player Localization (backwards compatible):** If you don't need per-player localization, the old API still works: ```csharp // Hard-coded text (same for all players) _api.RegisterMenuCategory("mymodule", "My Module", "@css/generic"); _api.RegisterMenu("mymodule", "action", "Do Action", CreateMenu, "@css/generic"); ```