mirror of
https://github.com/daffyyyy/CS2-SimpleAdmin.git
synced 2026-02-17 10:31:01 +00:00
Add CS2-SimpleAdmin documentation site
Introduces a new documentation site for CS2-SimpleAdmin using Docusaurus, including developer API references, tutorials, user guides, and module documentation. Removes the CleanModule example and updates FunCommands and ExampleModule. Also updates main plugin and API files to support new documentation and module structure.
This commit is contained in:
Binary file not shown.
@@ -1,59 +0,0 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Core.Capabilities;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CS2_SimpleAdmin_CleanModule;
|
||||
|
||||
public class CS2_SimpleAdmin_CleanModule: BasePlugin
|
||||
{
|
||||
public override string ModuleName => "[CS2-SimpleAdmin] Clean module";
|
||||
public override string ModuleDescription => "Module allows you to remove all weapons lying on the ground";
|
||||
public override string ModuleVersion => "v1.0.0";
|
||||
public override string ModuleAuthor => "daffyy";
|
||||
|
||||
private static ICS2_SimpleAdminApi? _sharedApi;
|
||||
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
|
||||
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_sharedApi = _pluginCapability.Get();
|
||||
|
||||
if (_sharedApi == null)
|
||||
{
|
||||
Logger.LogError("CS2-SimpleAdmin SharedApi not found");
|
||||
Unload(false);
|
||||
}
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_clean")]
|
||||
[ConsoleCommand("css_clear")]
|
||||
[RequiresPermissions("@css/cheat")]
|
||||
public void OnCleanCommand(CCSPlayerController? caller, CommandInfo commandInfo)
|
||||
{
|
||||
var weapons = Utilities.FindAllEntitiesByDesignerName<CCSWeaponBaseGun>("weapon_");
|
||||
var defusers = Utilities.FindAllEntitiesByDesignerName<CSceneEntity>("item_cutters");
|
||||
|
||||
foreach (var weapon in weapons)
|
||||
{
|
||||
if (!weapon.IsValid || weapon.State != CSWeaponState_t.WEAPON_NOT_CARRIED)
|
||||
continue;
|
||||
|
||||
weapon.Remove();
|
||||
}
|
||||
|
||||
foreach (var defuser in defusers)
|
||||
{
|
||||
if (!defuser.IsValid || defuser.OwnerEntity.Value != null)
|
||||
continue;
|
||||
|
||||
defuser.Remove();
|
||||
}
|
||||
|
||||
_sharedApi?.LogCommand(caller, commandInfo);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>CS2_SimpleAdmin_CleanModule</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.266" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="CS2-SimpleAdminApi">
|
||||
<HintPath>CS2-SimpleAdminApi.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CS2-SimpleAdmin_CleanModule", "CS2-SimpleAdmin_CleanModule.csproj", "{D940F3E9-0E3F-467A-B336-149E3A624FB6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D940F3E9-0E3F-467A-B336-149E3A624FB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D940F3E9-0E3F-467A-B336-149E3A624FB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D940F3E9-0E3F-467A-B336-149E3A624FB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D940F3E9-0E3F-467A-B336-149E3A624FB6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,173 +4,532 @@ using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Core.Capabilities;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CounterStrikeSharp.API.Modules.Entities.Constants;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CS2_SimpleAdmin_ExampleModule;
|
||||
|
||||
/// <summary>
|
||||
/// COMPLETE EXAMPLE MODULE FOR CS2-SIMPLEADMIN
|
||||
///
|
||||
/// This module demonstrates:
|
||||
/// 1. ✅ Getting CS2-SimpleAdmin API via capability system
|
||||
/// 2. ✅ Using API methods (GetServerId, GetConnectionString, IssuePenalty)
|
||||
/// 3. ✅ Listening to events (OnPlayerPenaltied, OnPlayerPenaltiedAdded)
|
||||
/// 4. ✅ Registering console commands
|
||||
/// 5. ✅ Creating menu categories and menu items
|
||||
/// 6. ✅ Using NEW MenuContext API to eliminate code duplication
|
||||
/// 7. ✅ Proper cleanup on module unload
|
||||
///
|
||||
/// Study this file to learn how to create your own CS2-SimpleAdmin modules!
|
||||
/// </summary>
|
||||
public class CS2_SimpleAdmin_ExampleModule: BasePlugin
|
||||
{
|
||||
public override string ModuleName => "[CS2-SimpleAdmin] Example module";
|
||||
public override string ModuleVersion => "v1.0.1";
|
||||
public override string ModuleAuthor => "daffyy";
|
||||
// ========================================
|
||||
// PLUGIN METADATA
|
||||
// ========================================
|
||||
public override string ModuleName => "[CS2-SimpleAdmin] Example Module";
|
||||
public override string ModuleVersion => "v1.1.0";
|
||||
public override string ModuleAuthor => "daffyy & Example Contributors";
|
||||
|
||||
// ========================================
|
||||
// PRIVATE FIELDS
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Server ID from SimpleAdmin (null for single-server mode)
|
||||
/// Useful for multi-server setups to identify which server this is
|
||||
/// </summary>
|
||||
private int? _serverId;
|
||||
private string _dbConnectionString = string.Empty;
|
||||
|
||||
private static ICS2_SimpleAdminApi? _sharedApi;
|
||||
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
|
||||
|
||||
/// <summary>
|
||||
/// Database connection string from SimpleAdmin
|
||||
/// Use this if your module needs direct database access
|
||||
/// </summary>
|
||||
private string _dbConnectionString = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Reference to CS2-SimpleAdmin API
|
||||
/// Use this to call API methods and register menus
|
||||
/// </summary>
|
||||
private static ICS2_SimpleAdminApi? _sharedApi;
|
||||
|
||||
/// <summary>
|
||||
/// Capability for getting the SimpleAdmin API
|
||||
/// This is the recommended way to get access to another plugin's API
|
||||
/// </summary>
|
||||
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
|
||||
|
||||
/// <summary>
|
||||
/// Flag to prevent duplicate menu registration
|
||||
/// Important for hot reload scenarios
|
||||
/// </summary>
|
||||
private bool _menusRegistered = false;
|
||||
|
||||
// ========================================
|
||||
// PLUGIN LIFECYCLE
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Called when all plugins are loaded (including hot reload)
|
||||
/// BEST PRACTICE: Use this instead of Load() to ensure all dependencies are available
|
||||
/// </summary>
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_sharedApi = _pluginCapability.Get();
|
||||
|
||||
if (_sharedApi == null)
|
||||
// STEP 1: Get the SimpleAdmin API using capability system
|
||||
try
|
||||
{
|
||||
Logger.LogError("CS2-SimpleAdmin SharedApi not found");
|
||||
_sharedApi = _pluginCapability.Get();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.LogError("CS2-SimpleAdmin API not found - make sure CS2-SimpleAdmin is loaded!");
|
||||
Unload(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// STEP 2: Get server information from SimpleAdmin
|
||||
_serverId = _sharedApi.GetServerId();
|
||||
_dbConnectionString = _sharedApi.GetConnectionString();
|
||||
Logger.LogInformation($"{ModuleName} started with serverId {_serverId}");
|
||||
|
||||
_sharedApi.OnPlayerPenaltied += OnPlayerPenaltied;
|
||||
_sharedApi.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
|
||||
|
||||
// STEP 3: Subscribe to SimpleAdmin events
|
||||
// These events fire when penalties (ban, kick, mute, etc.) are issued
|
||||
_sharedApi.OnPlayerPenaltied += OnPlayerPenaltied; // When penalty is issued to ONLINE player
|
||||
_sharedApi.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded; // When penalty is issued to OFFLINE player
|
||||
|
||||
// STEP 4: Register menus
|
||||
// BEST PRACTICE: Wait for SimpleAdmin to be ready before registering menus
|
||||
// This handles both normal load and hot reload scenarios
|
||||
_sharedApi.OnSimpleAdminReady += RegisterExampleMenus;
|
||||
RegisterExampleMenus(); // Fallback for hot reload case
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when the plugin is being unloaded
|
||||
/// BEST PRACTICE: Always clean up your registrations to prevent memory leaks
|
||||
/// </summary>
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_sharedApi == null) return;
|
||||
|
||||
// Unsubscribe from events
|
||||
_sharedApi.OnPlayerPenaltied -= OnPlayerPenaltied;
|
||||
_sharedApi.OnPlayerPenaltiedAdded -= OnPlayerPenaltiedAdded;
|
||||
_sharedApi.OnSimpleAdminReady -= RegisterExampleMenus;
|
||||
|
||||
// Unregister menus
|
||||
_sharedApi.UnregisterMenu("example", "simple_action");
|
||||
_sharedApi.UnregisterMenu("example", "player_selection");
|
||||
_sharedApi.UnregisterMenu("example", "nested_menu");
|
||||
_sharedApi.UnregisterMenu("example", "test_command");
|
||||
|
||||
Logger.LogInformation($"{ModuleName} unloaded successfully");
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MENU REGISTRATION
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Registers all example menus in the admin menu
|
||||
/// BEST PRACTICE: Use this pattern to prevent duplicate registrations
|
||||
/// </summary>
|
||||
private void RegisterExampleMenus()
|
||||
{
|
||||
if (_sharedApi == null || _menusRegistered) return;
|
||||
|
||||
try
|
||||
{
|
||||
// STEP 1: Register a menu category
|
||||
// This creates a new section in the main admin menu
|
||||
// Permission: @css/generic means all admins can see it
|
||||
_sharedApi.RegisterMenuCategory(
|
||||
"example", // Category ID (unique identifier)
|
||||
"Example Features", // Display name in admin menu
|
||||
"@css/generic" // Required permission
|
||||
);
|
||||
|
||||
// STEP 2: Register individual menu items in the category
|
||||
// 🆕 NEW: These use MenuContext API - factory receives (admin, context) parameters
|
||||
|
||||
// Example 1: Simple menu with options
|
||||
_sharedApi.RegisterMenu(
|
||||
"example", // Category ID
|
||||
"simple_action", // Menu ID (unique within category)
|
||||
"Simple Actions", // Display name
|
||||
CreateSimpleActionMenu, // Factory method
|
||||
"@css/generic" // Required permission
|
||||
);
|
||||
|
||||
// Example 2: Player selection menu
|
||||
_sharedApi.RegisterMenu(
|
||||
"example",
|
||||
"player_selection",
|
||||
"Select Player",
|
||||
CreatePlayerSelectionMenu,
|
||||
"@css/kick" // Requires kick permission
|
||||
);
|
||||
|
||||
// Example 3: Nested menu (Player → Value)
|
||||
_sharedApi.RegisterMenu(
|
||||
"example",
|
||||
"nested_menu",
|
||||
"Give Credits",
|
||||
CreateGiveCreditsMenu,
|
||||
"@css/generic"
|
||||
);
|
||||
|
||||
// Example 4: Menu with permission override support
|
||||
_sharedApi.RegisterMenu(
|
||||
"example",
|
||||
"test_command",
|
||||
"Test Command",
|
||||
CreateTestCommandMenu,
|
||||
"@css/root", // Default permission
|
||||
"css_test" // Command name for override checking
|
||||
);
|
||||
|
||||
_menusRegistered = true;
|
||||
Logger.LogInformation("Example menus registered successfully!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to register example menus: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MENU FACTORY METHODS
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// PATTERN 1: Simple menu with static options
|
||||
/// 🆕 NEW: Uses MenuContext to eliminate duplication!
|
||||
/// </summary>
|
||||
private object CreateSimpleActionMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// Create menu with automatic back button
|
||||
// 🆕 NEW: Use context instead of repeating title and category!
|
||||
var menu = _sharedApi!.CreateMenuWithBack(context, admin);
|
||||
|
||||
// Add menu options
|
||||
_sharedApi.AddMenuOption(menu, "Print Server Info", player =>
|
||||
{
|
||||
player.PrintToChat($"Server ID: {_serverId}");
|
||||
player.PrintToChat($"Server IP: {_sharedApi?.GetServerAddress()}");
|
||||
});
|
||||
|
||||
_sharedApi.AddMenuOption(menu, "Get My Stats", player =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var playerInfo = _sharedApi?.GetPlayerInfo(player);
|
||||
player.PrintToChat($"Total Bans: {playerInfo?.TotalBans ?? 0}");
|
||||
player.PrintToChat($"Total Kicks: {playerInfo?.TotalKicks ?? 0}");
|
||||
player.PrintToChat($"Total Warns: {playerInfo?.TotalWarns ?? 0}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error getting player info: {ex.Message}");
|
||||
player.PrintToChat("Error retrieving your stats");
|
||||
}
|
||||
});
|
||||
|
||||
_sharedApi.AddMenuOption(menu, "Check Silent Mode", player =>
|
||||
{
|
||||
var isSilent = _sharedApi?.IsAdminSilent(player) ?? false;
|
||||
player.PrintToChat($"Silent mode: {(isSilent ? "ON" : "OFF")}");
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PATTERN 2: Player selection menu with immediate action
|
||||
/// 🆕 NEW: Uses MenuContext API - cleaner and less error-prone!
|
||||
/// </summary>
|
||||
private object CreatePlayerSelectionMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// 🆕 NEW: CreateMenuWithPlayers now uses context instead of title/category
|
||||
return _sharedApi!.CreateMenuWithPlayers(
|
||||
context, // ← Contains title and category automatically!
|
||||
admin,
|
||||
// Filter: Only show valid players that admin can target
|
||||
player => player.IsValid && admin.CanTarget(player),
|
||||
// Action: What happens when a player is selected
|
||||
(adminPlayer, targetPlayer) =>
|
||||
{
|
||||
adminPlayer.PrintToChat($"You selected: {targetPlayer.PlayerName}");
|
||||
|
||||
// Example: Show player info
|
||||
try
|
||||
{
|
||||
var playerInfo = _sharedApi?.GetPlayerInfo(targetPlayer);
|
||||
adminPlayer.PrintToChat($"{targetPlayer.PlayerName} - Bans: {playerInfo?.TotalBans}, Warns: {playerInfo?.TotalWarns}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Could not get info for {targetPlayer.PlayerName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PATTERN 3: Nested menu (Player → Value selection)
|
||||
/// 🆕 NEW: First level menu uses MenuContext
|
||||
/// </summary>
|
||||
private object CreateGiveCreditsMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// Create menu with back button
|
||||
// 🆕 NEW: Uses context - no more repeating title/category!
|
||||
var menu = _sharedApi!.CreateMenuWithBack(context, admin);
|
||||
|
||||
// Get all valid, targetable players
|
||||
var players = _sharedApi.GetValidPlayers().Where(p =>
|
||||
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
var playerName = player.PlayerName.Length > 26
|
||||
? player.PlayerName[..26]
|
||||
: player.PlayerName;
|
||||
|
||||
// AddSubMenu automatically adds a "Back" button to the submenu
|
||||
_sharedApi.AddSubMenu(menu, playerName, p => CreateCreditAmountMenu(admin, player));
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submenu for selecting credit amount
|
||||
/// Note: Submenus create dynamic titles, so they don't receive MenuContext
|
||||
/// </summary>
|
||||
private object CreateCreditAmountMenu(CCSPlayerController admin, CCSPlayerController target)
|
||||
{
|
||||
// Dynamic title includes target's name
|
||||
var menu = _sharedApi!.CreateMenuWithBack(
|
||||
$"Credits for {target.PlayerName}",
|
||||
"example", // Category for back navigation
|
||||
admin
|
||||
);
|
||||
|
||||
// Predefined credit amounts
|
||||
var creditAmounts = new[] { 100, 500, 1000, 5000, 10000 };
|
||||
|
||||
foreach (var amount in creditAmounts)
|
||||
{
|
||||
_sharedApi.AddMenuOption(menu, $"{amount} Credits", _ =>
|
||||
{
|
||||
// BEST PRACTICE: Always validate player is still valid before action
|
||||
if (target.IsValid)
|
||||
{
|
||||
Server.PrintToChatAll($"{admin.PlayerName} gave {amount} credits to {target.PlayerName}");
|
||||
Logger.LogInformation($"Admin {admin.PlayerName} gave {amount} credits to {target.PlayerName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
admin.PrintToChat("Player is no longer available");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example menu with permission override support
|
||||
/// </summary>
|
||||
private object CreateTestCommandMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
var menu = _sharedApi!.CreateMenuWithBack(context, admin);
|
||||
|
||||
// You can access context properties if needed
|
||||
_sharedApi.AddMenuOption(menu, "Show Context Info", player =>
|
||||
{
|
||||
player.PrintToChat($"Category: {context.CategoryId}");
|
||||
player.PrintToChat($"Menu ID: {context.MenuId}");
|
||||
player.PrintToChat($"Title: {context.MenuTitle}");
|
||||
player.PrintToChat($"Permission: {context.Permission}");
|
||||
player.PrintToChat($"Command: {context.CommandName}");
|
||||
});
|
||||
|
||||
_sharedApi.AddMenuOption(menu, "Test Action", player =>
|
||||
{
|
||||
player.PrintToChat("Test action executed!");
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CONSOLE COMMANDS
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Example command: Kick yourself
|
||||
/// Demonstrates using IssuePenalty API for online players
|
||||
/// </summary>
|
||||
[ConsoleCommand("css_kickme")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
public void KickMeCommand(CCSPlayerController? caller, CommandInfo commandInfo)
|
||||
{
|
||||
if (caller == null) return;
|
||||
|
||||
_sharedApi?.IssuePenalty(caller, null, PenaltyType.Kick, "test");
|
||||
|
||||
// Issue a kick penalty to the caller
|
||||
// Parameters: player, admin (null = console), penaltyType, reason
|
||||
_sharedApi?.IssuePenalty(caller, null, PenaltyType.Kick, "You kicked yourself!");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_serverAddress")]
|
||||
|
||||
/// <summary>
|
||||
/// Example command: Get server address
|
||||
/// Demonstrates using GetServerAddress API
|
||||
/// </summary>
|
||||
[ConsoleCommand("css_serveraddress")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
public void ServerAddressCommand(CCSPlayerController? caller, CommandInfo commandInfo)
|
||||
{
|
||||
commandInfo.ReplyToCommand($"Our server IP: {_sharedApi?.GetServerAddress()}");
|
||||
}
|
||||
|
||||
[ConsoleCommand("css_getMyInfo")]
|
||||
commandInfo.ReplyToCommand($"Server IP: {_sharedApi?.GetServerAddress()}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example command: Get player statistics
|
||||
/// Demonstrates using GetPlayerInfo API
|
||||
/// </summary>
|
||||
[ConsoleCommand("css_getmyinfo")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)]
|
||||
public void GetMyInfoCommand(CCSPlayerController? caller, CommandInfo commandInfo)
|
||||
{
|
||||
if (caller == null) return;
|
||||
|
||||
var playerInfo = _sharedApi?.GetPlayerInfo(caller);
|
||||
commandInfo.ReplyToCommand($"Your total bans: {playerInfo?.TotalBans}");
|
||||
commandInfo.ReplyToCommand($"Your total gags: {playerInfo?.TotalGags}");
|
||||
commandInfo.ReplyToCommand($"Your total mutes: {playerInfo?.TotalMutes}");
|
||||
commandInfo.ReplyToCommand($"Your total silences: {playerInfo?.SteamId}");
|
||||
|
||||
try
|
||||
{
|
||||
var playerInfo = _sharedApi?.GetPlayerInfo(caller);
|
||||
commandInfo.ReplyToCommand($"Your Statistics:");
|
||||
commandInfo.ReplyToCommand($" Total Bans: {playerInfo?.TotalBans ?? 0}");
|
||||
commandInfo.ReplyToCommand($" Total Kicks: {playerInfo?.TotalKicks ?? 0}");
|
||||
commandInfo.ReplyToCommand($" Total Gags: {playerInfo?.TotalGags ?? 0}");
|
||||
commandInfo.ReplyToCommand($" Total Mutes: {playerInfo?.TotalMutes ?? 0}");
|
||||
commandInfo.ReplyToCommand($" Total Warns: {playerInfo?.TotalWarns ?? 0}");
|
||||
commandInfo.ReplyToCommand($" SteamID: {playerInfo?.SteamId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error in GetMyInfoCommand: {ex.Message}");
|
||||
commandInfo.ReplyToCommand("Error retrieving your information");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Example command: Add ban to offline player
|
||||
/// Demonstrates using IssuePenalty API with SteamID for offline players
|
||||
/// SERVER ONLY - dangerous command!
|
||||
/// </summary>
|
||||
[ConsoleCommand("css_testaddban")]
|
||||
[CommandHelper(whoCanExecute: CommandUsage.SERVER_ONLY)]
|
||||
public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo commandInfo)
|
||||
{
|
||||
_sharedApi?.IssuePenalty(new SteamID(76561197960287930), null, PenaltyType.Ban, "My super reason", 10);
|
||||
// Issue a ban to an offline player by SteamID
|
||||
// Parameters: steamID, admin (null = console), penaltyType, reason, duration (minutes)
|
||||
_sharedApi?.IssuePenalty(
|
||||
new SteamID(76561197960287930), // Target SteamID
|
||||
null, // Admin (null = console)
|
||||
PenaltyType.Ban, // Penalty type
|
||||
"Test ban from example module", // Reason
|
||||
10 // Duration (10 minutes)
|
||||
);
|
||||
|
||||
Logger.LogInformation("Test ban issued via API");
|
||||
}
|
||||
|
||||
private void OnPlayerPenaltied(PlayerInfo player, PlayerInfo? admin, PenaltyType penaltyType,
|
||||
string reason, int duration, int? penaltyId, int? serverId)
|
||||
// ========================================
|
||||
// EVENT HANDLERS
|
||||
// ========================================
|
||||
|
||||
/// <summary>
|
||||
/// Called when a penalty is issued to an ONLINE player
|
||||
/// Use this to react to bans/kicks/mutes happening in real-time
|
||||
/// </summary>
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player, // The player who received the penalty
|
||||
PlayerInfo? admin, // The admin who issued it (null = console)
|
||||
PenaltyType penaltyType,// Type of penalty (Ban, Kick, Mute, etc.)
|
||||
string reason, // Reason for the penalty
|
||||
int duration, // Duration in minutes (-1 = permanent)
|
||||
int? penaltyId, // Database ID of the penalty
|
||||
int? serverId // Server ID where it was issued
|
||||
)
|
||||
{
|
||||
// Example: Announce bans to all players
|
||||
if (penaltyType == PenaltyType.Ban)
|
||||
{
|
||||
Server.PrintToChatAll($"{player.Name} is a dog");
|
||||
var adminName = admin?.Name ?? "Console";
|
||||
var durationText = (duration == -1 || duration == 0) ? "permanently" : $"for {duration} minutes";
|
||||
Server.PrintToChatAll($"{player.Name} was banned {durationText} by {adminName}");
|
||||
}
|
||||
|
||||
// Log all penalties
|
||||
var adminNameLog = admin?.Name ?? "Console";
|
||||
switch (penaltyType)
|
||||
{
|
||||
case PenaltyType.Ban:
|
||||
Logger.LogInformation($"Ban issued to {player.Name} by {adminNameLog} (ID: {penaltyId}, Duration: {duration}m, Reason: {reason})");
|
||||
break;
|
||||
case PenaltyType.Kick:
|
||||
Logger.LogInformation($"Kick issued to {player.Name} by {adminNameLog} (Reason: {reason})");
|
||||
break;
|
||||
case PenaltyType.Gag:
|
||||
Logger.LogInformation($"Gag issued to {player.Name} by {adminNameLog} (ID: {penaltyId}, Duration: {duration}m)");
|
||||
break;
|
||||
case PenaltyType.Mute:
|
||||
Logger.LogInformation($"Mute issued to {player.Name} by {adminNameLog} (ID: {penaltyId}, Duration: {duration}m)");
|
||||
break;
|
||||
case PenaltyType.Silence:
|
||||
Logger.LogInformation($"Silence issued to {player.Name} by {adminNameLog} (ID: {penaltyId}, Duration: {duration}m)");
|
||||
break;
|
||||
case PenaltyType.Warn:
|
||||
Logger.LogInformation($"Warning issued to {player.Name} by {adminNameLog} (ID: {penaltyId}, Reason: {reason})");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a penalty is issued to an OFFLINE player
|
||||
/// Use this to react to bans/mutes added via SteamID (player not on server)
|
||||
/// </summary>
|
||||
private void OnPlayerPenaltiedAdded(
|
||||
SteamID steamId, // SteamID of the penalized player
|
||||
PlayerInfo? admin, // The admin who issued it (null = console)
|
||||
PenaltyType penaltyType,// Type of penalty
|
||||
string reason, // Reason for the penalty
|
||||
int duration, // Duration in minutes (-1 = permanent)
|
||||
int? penaltyId, // Database ID of the penalty
|
||||
int? serverId // Server ID where it was issued
|
||||
)
|
||||
{
|
||||
// Log offline penalty additions
|
||||
var adminName = admin?.Name ?? "Console";
|
||||
|
||||
switch (penaltyType)
|
||||
{
|
||||
case PenaltyType.Ban:
|
||||
{
|
||||
Logger.LogInformation("Ban issued");
|
||||
Logger.LogInformation($"Id = {penaltyId}");
|
||||
Logger.LogInformation($"Ban added for offline player {steamId} by {adminName} (ID: {penaltyId}, Duration: {duration}m, Reason: {reason})");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Kick:
|
||||
{
|
||||
Logger.LogInformation("Kick issued");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Gag:
|
||||
{
|
||||
Logger.LogInformation("Gag issued");
|
||||
Logger.LogInformation($"Id = {penaltyId}");
|
||||
Logger.LogInformation($"Gag added for offline player {steamId} by {adminName} (ID: {penaltyId}, Duration: {duration}m)");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Mute:
|
||||
{
|
||||
Logger.LogInformation("Mute issued");
|
||||
Logger.LogInformation($"Mute added for offline player {steamId} by {adminName} (ID: {penaltyId}, Duration: {duration}m)");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Silence:
|
||||
{
|
||||
Logger.LogInformation("Silence issued");
|
||||
Logger.LogInformation($"Silence added for offline player {steamId} by {adminName} (ID: {penaltyId}, Duration: {duration}m)");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Warn:
|
||||
{
|
||||
Logger.LogInformation("Warn issued");
|
||||
Logger.LogInformation($"Warning added for offline player {steamId} by {adminName} (ID: {penaltyId}, Reason: {reason})");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
|
||||
}
|
||||
|
||||
Console.WriteLine(player.Name);
|
||||
Console.WriteLine(admin?.Name ?? "Console");
|
||||
Console.WriteLine(player.SteamId.ToString());
|
||||
Console.WriteLine(reason);
|
||||
}
|
||||
|
||||
private void OnPlayerPenaltiedAdded(SteamID steamId, PlayerInfo? admin, PenaltyType penaltyType,
|
||||
string reason, int duration, int? penaltyId, int? serverId)
|
||||
{
|
||||
switch (penaltyType)
|
||||
{
|
||||
case PenaltyType.Ban:
|
||||
{
|
||||
Logger.LogInformation("Ban added");
|
||||
Logger.LogInformation($"Id = {penaltyId}");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Kick:
|
||||
{
|
||||
Logger.LogInformation("Kick added");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Gag:
|
||||
{
|
||||
Logger.LogInformation("Gag added");
|
||||
Logger.LogInformation($"Id = {penaltyId}");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Mute:
|
||||
{
|
||||
Logger.LogInformation("Mute added");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Silence:
|
||||
{
|
||||
Logger.LogInformation("Silence added");
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Warn:
|
||||
{
|
||||
Logger.LogInformation("Warn added");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
|
||||
}
|
||||
|
||||
Console.WriteLine(admin?.Name ?? "Console");
|
||||
Console.WriteLine(steamId.ToString());
|
||||
Console.WriteLine(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -370,7 +370,7 @@ public partial class CS2_SimpleAdmin_FunCommands : BasePlugin, IPluginConfig<Con
|
||||
|
||||
// Register menus with command names for permission override support
|
||||
// Server admins can override default permissions via CounterStrikeSharp admin system
|
||||
// Example: If "css_god" is overridden to "@css/vip", only VIPs will see the God Mode menu
|
||||
// Example: If "css_god" is overdden to "@css/vip", only VIPs will see the God Mode menu
|
||||
|
||||
if (Config.GodCommands.Count > 0)
|
||||
_sharedApi.RegisterMenu("fun", "god",
|
||||
|
||||
@@ -27,6 +27,35 @@ namespace CS2_SimpleAdmin_FunCommands;
|
||||
/// "css_god"); // Command name for override checking
|
||||
///
|
||||
/// This means developers don't need to manually check permissions in their menu factory methods!
|
||||
///
|
||||
/// MENU CONTEXT API (NEW!):
|
||||
/// ========================
|
||||
/// Menu factory methods now receive a MenuContext parameter that contains:
|
||||
/// - CategoryId: The category this menu belongs to (e.g., "fun")
|
||||
/// - MenuId: The unique identifier for this menu (e.g., "god")
|
||||
/// - MenuTitle: The display title from registration (e.g., "God Mode")
|
||||
/// - Permission: The default permission (e.g., "@css/cheats")
|
||||
/// - CommandName: The command name for override checking (e.g., "css_god")
|
||||
///
|
||||
/// This eliminates duplication when creating menus - you no longer need to repeat
|
||||
/// the title and category in both RegisterMenu() and CreateMenuWithPlayers()!
|
||||
///
|
||||
/// Before (old API):
|
||||
/// private object CreateGodModeMenu(CCSPlayerController admin)
|
||||
/// {
|
||||
/// return _sharedApi.CreateMenuWithPlayers(
|
||||
/// "God Mode", // ← Duplicated from RegisterMenu
|
||||
/// "fun", // ← Duplicated from RegisterMenu
|
||||
/// admin, filter, action);
|
||||
/// }
|
||||
///
|
||||
/// After (new API with MenuContext):
|
||||
/// private object CreateGodModeMenu(CCSPlayerController admin, MenuContext context)
|
||||
/// {
|
||||
/// return _sharedApi.CreateMenuWithPlayers(
|
||||
/// context, // ← Contains both title and category automatically!
|
||||
/// admin, filter, action);
|
||||
/// }
|
||||
/// </summary>
|
||||
public partial class CS2_SimpleAdmin_FunCommands
|
||||
{
|
||||
@@ -39,22 +68,21 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
/// <summary>
|
||||
/// Creates a simple player selection menu for god mode.
|
||||
/// PATTERN: CreateMenuWithPlayers with method reference
|
||||
/// IMPROVED: Uses MenuContext to eliminate duplication of title and category
|
||||
/// </summary>
|
||||
private object CreateGodModeMenu(CCSPlayerController admin)
|
||||
private object CreateGodModeMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
return _sharedApi!.CreateMenuWithPlayers(
|
||||
Localizer?["fun_menu_god"] ?? "God Mode", // Menu title from translation
|
||||
"fun", // Category ID (for back button navigation)
|
||||
context, // ← Context contains title & category automatically!
|
||||
admin, // Admin opening the menu
|
||||
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player), // Filter: only alive, targetable players
|
||||
God); // Action to execute (method reference)
|
||||
}
|
||||
|
||||
private object CreateNoClipMenu(CCSPlayerController admin)
|
||||
private object CreateNoClipMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
return _sharedApi!.CreateMenuWithPlayers(
|
||||
Localizer?["fun_menu_noclip"] ?? "No Clip",
|
||||
"fun",
|
||||
context,
|
||||
admin,
|
||||
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player),
|
||||
NoClip);
|
||||
@@ -63,13 +91,13 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
/// <summary>
|
||||
/// Creates a player selection menu for respawn command.
|
||||
/// PATTERN: CreateMenuWithPlayers with method reference
|
||||
/// IMPROVED: Uses MenuContext to eliminate duplication
|
||||
/// </summary>
|
||||
private object CreateRespawnMenu(CCSPlayerController admin)
|
||||
private object CreateRespawnMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
return _sharedApi!.CreateMenuWithPlayers(
|
||||
Localizer?["fun_menu_respawn"] ?? "Respawn", // Menu title from translation
|
||||
"fun", // Category ID
|
||||
admin, // Admin
|
||||
context,
|
||||
admin,
|
||||
admin.CanTarget, // Filter: only targetable players (no LifeState check - can respawn dead players)
|
||||
Respawn); // Use the Respawn method which includes death position teleport
|
||||
}
|
||||
@@ -83,12 +111,12 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
/// <summary>
|
||||
/// Creates a nested menu: Player selection → Weapon selection.
|
||||
/// PATTERN: CreateMenuWithBack + foreach + AddSubMenu
|
||||
/// IMPROVED: Uses MenuContext - no more duplication of title/category!
|
||||
/// </summary>
|
||||
private object CreateGiveWeaponMenu(CCSPlayerController admin)
|
||||
private object CreateGiveWeaponMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
var menu = _sharedApi!.CreateMenuWithBack(
|
||||
Localizer?["fun_menu_give"] ?? "Give Weapon",
|
||||
"fun",
|
||||
context, // ← Context contains title & category!
|
||||
admin);
|
||||
var players = _sharedApi.GetValidPlayers().Where(p =>
|
||||
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
|
||||
@@ -134,11 +162,10 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
return weaponMenu;
|
||||
}
|
||||
|
||||
private object CreateStripWeaponsMenu(CCSPlayerController admin)
|
||||
private object CreateStripWeaponsMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
return _sharedApi!.CreateMenuWithPlayers(
|
||||
Localizer?["fun_menu_strip"] ?? "Strip Weapons",
|
||||
"fun",
|
||||
context,
|
||||
admin,
|
||||
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player),
|
||||
(adminPlayer, targetPlayer) =>
|
||||
@@ -148,11 +175,10 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
});
|
||||
}
|
||||
|
||||
private object CreateFreezeMenu(CCSPlayerController admin)
|
||||
private object CreateFreezeMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
return _sharedApi!.CreateMenuWithPlayers(
|
||||
Localizer?["fun_menu_freeze"] ?? "Freeze",
|
||||
"fun",
|
||||
context,
|
||||
admin,
|
||||
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player),
|
||||
(adminPlayer, targetPlayer) => { Freeze(adminPlayer, targetPlayer, -1); });
|
||||
@@ -161,13 +187,12 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
/// <summary>
|
||||
/// Creates a nested menu for setting player HP with predefined values.
|
||||
/// PATTERN: Same as Give Weapon (player selection → value selection)
|
||||
/// This is a common pattern you'll use frequently!
|
||||
/// IMPROVED: Uses MenuContext - cleaner and less error-prone!
|
||||
/// </summary>
|
||||
private object CreateSetHpMenu(CCSPlayerController admin)
|
||||
private object CreateSetHpMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
var menu = _sharedApi!.CreateMenuWithBack(
|
||||
Localizer?["fun_menu_hp"] ?? "Set HP",
|
||||
"fun",
|
||||
context,
|
||||
admin);
|
||||
var players = _sharedApi.GetValidPlayers().Where(p =>
|
||||
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
|
||||
@@ -212,11 +237,10 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
return hpSelectionMenu;
|
||||
}
|
||||
|
||||
private object CreateSetSpeedMenu(CCSPlayerController admin)
|
||||
private object CreateSetSpeedMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
var menu = _sharedApi!.CreateMenuWithBack(
|
||||
Localizer?["fun_menu_speed"] ?? "Set Speed",
|
||||
"fun",
|
||||
context,
|
||||
admin);
|
||||
var players = _sharedApi.GetValidPlayers().Where(p =>
|
||||
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
|
||||
@@ -273,11 +297,10 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
return speedSelectionMenu;
|
||||
}
|
||||
|
||||
private object CreateSetGravityMenu(CCSPlayerController admin)
|
||||
private object CreateSetGravityMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
var menu = _sharedApi!.CreateMenuWithBack(
|
||||
Localizer?["fun_menu_gravity"] ?? "Set Gravity",
|
||||
"fun",
|
||||
context,
|
||||
admin);
|
||||
var players = _sharedApi.GetValidPlayers().Where(p =>
|
||||
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
|
||||
@@ -324,11 +347,10 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
return gravitySelectionMenu;
|
||||
}
|
||||
|
||||
private object CreateSetMoneyMenu(CCSPlayerController admin)
|
||||
private object CreateSetMoneyMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
var menu = _sharedApi!.CreateMenuWithBack(
|
||||
Localizer?["fun_menu_money"] ?? "Set Money",
|
||||
"fun",
|
||||
context,
|
||||
admin);
|
||||
var players = _sharedApi.GetValidPlayers().Where(p => admin.CanTarget(p));
|
||||
|
||||
@@ -366,11 +388,10 @@ public partial class CS2_SimpleAdmin_FunCommands
|
||||
return moneySelectionMenu;
|
||||
}
|
||||
|
||||
private object CreateSetResizeMenu(CCSPlayerController admin)
|
||||
private object CreateSetResizeMenu(CCSPlayerController admin, CS2_SimpleAdminApi.MenuContext context)
|
||||
{
|
||||
var menu = _sharedApi!.CreateMenuWithBack(
|
||||
Localizer?["fun_menu_resize"] ?? "Resize Player",
|
||||
"fun",
|
||||
context,
|
||||
admin);
|
||||
var players = _sharedApi.GetValidPlayers().Where(p =>
|
||||
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
|
||||
|
||||
@@ -168,23 +168,35 @@ private void God(CCSPlayerController? caller, CCSPlayerController player)
|
||||
|
||||
**Key Concepts Demonstrated:**
|
||||
|
||||
#### Simple Player Selection Menu
|
||||
#### Simple Player Selection Menu (NEW API with MenuContext!)
|
||||
|
||||
```csharp
|
||||
private object CreateGodModeMenu(CCSPlayerController admin)
|
||||
// 🆕 NEW: Factory receives MenuContext - no more duplication!
|
||||
private object CreateGodModeMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// ✅ BEST PRACTICE: Use CreateMenuWithPlayers for simple player selection
|
||||
return _sharedApi!.CreateMenuWithPlayers("God Mode", "fun", admin,
|
||||
// ✅ BEST PRACTICE: Use context instead of repeating title and category
|
||||
return _sharedApi!.CreateMenuWithPlayers(
|
||||
context, // ← Contains "God Mode" title and "fun" category automatically!
|
||||
admin,
|
||||
player => player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(player),
|
||||
God); // Direct method reference
|
||||
God); // Direct method reference
|
||||
}
|
||||
```
|
||||
|
||||
#### Nested Menu with Value Selection
|
||||
**Why MenuContext is better:**
|
||||
- ❌ **Before:** You had to type `"God Mode"` and `"fun"` twice (in `RegisterMenu` and `CreateMenuWithPlayers`)
|
||||
- ✅ **After:** Context contains these values automatically - no duplication!
|
||||
- ✅ Less error-prone (can't accidentally use wrong category)
|
||||
- ✅ Easier to refactor (change title in one place)
|
||||
|
||||
#### Nested Menu with Value Selection (NEW API!)
|
||||
|
||||
```csharp
|
||||
private object CreateSetHpMenu(CCSPlayerController admin)
|
||||
// 🆕 NEW: Uses MenuContext to eliminate duplication
|
||||
private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// ✅ BEST PRACTICE: Use CreateMenuWithBack for menus with back button
|
||||
var menu = _sharedApi!.CreateMenuWithBack("Set HP", "fun", admin);
|
||||
// ✅ BEST PRACTICE: Use context instead of manual title/category
|
||||
var menu = _sharedApi!.CreateMenuWithBack(context, admin);
|
||||
|
||||
var players = _sharedApi.GetValidPlayers().Where(p =>
|
||||
p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && admin.CanTarget(p));
|
||||
@@ -200,6 +212,7 @@ private object CreateSetHpMenu(CCSPlayerController admin)
|
||||
|
||||
private object CreateHpSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
|
||||
{
|
||||
// Note: Submenus don't receive context - they create their own titles dynamically
|
||||
var hpMenu = _sharedApi!.CreateMenuWithBack($"Set HP: {target.PlayerName}", "fun", admin);
|
||||
var hpValues = new[] { 1, 10, 25, 50, 100, 200, 500, 999 };
|
||||
|
||||
@@ -221,6 +234,31 @@ private object CreateHpSelectionMenu(CCSPlayerController admin, CCSPlayerControl
|
||||
}
|
||||
```
|
||||
|
||||
**Comparison: Old vs New API**
|
||||
|
||||
```csharp
|
||||
// ❌ OLD API - lots of duplication
|
||||
_sharedApi.RegisterMenu("fun", "god", "God Mode", CreateGodModeMenu, "@css/cheats", "css_god");
|
||||
|
||||
private object CreateGodModeMenu(CCSPlayerController admin)
|
||||
{
|
||||
return _sharedApi.CreateMenuWithPlayers(
|
||||
"God Mode", // ← Repeated from RegisterMenu
|
||||
"fun", // ← Repeated from RegisterMenu
|
||||
admin, filter, action);
|
||||
}
|
||||
|
||||
// ✅ NEW API - no duplication!
|
||||
_sharedApi.RegisterMenu("fun", "god", "God Mode", CreateGodModeMenu, "@css/cheats", "css_god");
|
||||
|
||||
private object CreateGodModeMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
return _sharedApi.CreateMenuWithPlayers(
|
||||
context, // ← Contains title and category automatically!
|
||||
admin, filter, action);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Translations
|
||||
|
||||
**Key Concept:** Module-specific translations
|
||||
|
||||
@@ -6,12 +6,12 @@ This guide explains how to create modules for CS2-SimpleAdmin with custom comman
|
||||
|
||||
## 📖 Table of Contents
|
||||
|
||||
1. [Quick Start](#quick-start)
|
||||
2. [Learning Resources](#learning-resources)
|
||||
3. [API Methods Reference](#api-methods)
|
||||
4. [Menu Patterns](#menu-patterns)
|
||||
5. [Best Practices](#best-practices)
|
||||
6. [Common Patterns](#common-patterns)
|
||||
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
|
||||
@@ -44,7 +44,7 @@ YourModule/
|
||||
└── ru.json
|
||||
```
|
||||
|
||||
### Step 3: Minimal Working Example
|
||||
### Step 3: Minimal Working Example (NEW MenuContext API!)
|
||||
|
||||
```csharp
|
||||
using CounterStrikeSharp.API.Core;
|
||||
@@ -78,14 +78,17 @@ public class MyModule : BasePlugin
|
||||
_api.RegisterMenuCategory("mymodule", "My Module", "@css/generic");
|
||||
|
||||
// 2. Register menu items in the category
|
||||
// 🆕 NEW: Use MenuContext-aware overload (no duplication!)
|
||||
_api.RegisterMenu("mymodule", "action1", "Action 1", CreateAction1Menu, "@css/generic");
|
||||
_api.RegisterMenu("mymodule", "action2", "Action 2", CreateAction2Menu, "@css/kick");
|
||||
}
|
||||
|
||||
private object CreateAction1Menu(CCSPlayerController admin)
|
||||
// 🆕 NEW: Factory now receives MenuContext parameter
|
||||
private object CreateAction1Menu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// Create a menu with automatic back button
|
||||
var menu = _api!.CreateMenuWithBack("Action 1 Menu", "mymodule", admin);
|
||||
// Use context instead of repeating title and category!
|
||||
var menu = _api!.CreateMenuWithBack(context, admin);
|
||||
|
||||
// Add menu options
|
||||
_api.AddMenuOption(menu, "Option 1", player =>
|
||||
@@ -101,10 +104,13 @@ public class MyModule : BasePlugin
|
||||
return menu;
|
||||
}
|
||||
|
||||
private object CreateAction2Menu(CCSPlayerController admin)
|
||||
// 🆕 NEW: MenuContext eliminates duplication here too!
|
||||
private object CreateAction2Menu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// Use the built-in player selection menu
|
||||
return _api!.CreateMenuWithPlayers("Select Player", "mymodule", admin,
|
||||
// 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) =>
|
||||
{
|
||||
@@ -158,16 +164,15 @@ public class MyModule : BasePlugin
|
||||
|
||||
The FunCommands module demonstrates **3 essential menu patterns** you'll use in every module:
|
||||
|
||||
### Pattern 1: Simple Player Selection
|
||||
### Pattern 1: Simple Player Selection (NEW MenuContext API!)
|
||||
**When to use:** Select a player and immediately execute an action
|
||||
|
||||
```csharp
|
||||
// Example: God Mode menu
|
||||
private object CreateGodModeMenu(CCSPlayerController admin)
|
||||
// 🆕 NEW: Factory receives MenuContext - eliminates duplication!
|
||||
private object CreateGodModeMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
return _api.CreateMenuWithPlayers(
|
||||
"God Mode", // Title
|
||||
"yourmodule", // Category ID
|
||||
context, // ← Contains title & category automatically!
|
||||
admin, // Admin
|
||||
player => player.IsValid && admin.CanTarget(player), // Filter
|
||||
(adminPlayer, target) => // Action
|
||||
@@ -178,16 +183,20 @@ private object CreateGodModeMenu(CCSPlayerController admin)
|
||||
}
|
||||
```
|
||||
|
||||
**See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:21-29`
|
||||
**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!
|
||||
|
||||
### Pattern 2: Nested Menu (Player → Value)
|
||||
**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
|
||||
// Example: Set HP menu (player selection)
|
||||
private object CreateSetHpMenu(CCSPlayerController admin)
|
||||
// 🆕 NEW: Uses MenuContext to eliminate duplication
|
||||
private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
var menu = _api.CreateMenuWithBack("Set HP", "yourmodule", admin);
|
||||
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)
|
||||
@@ -201,6 +210,7 @@ private object CreateSetHpMenu(CCSPlayerController admin)
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -221,7 +231,12 @@ private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController
|
||||
}
|
||||
```
|
||||
|
||||
**See:** `CS2-SimpleAdmin_FunCommands/Menus.cs:134-173`
|
||||
**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)
|
||||
@@ -264,6 +279,73 @@ private object CreateWeaponSelectionMenu(CCSPlayerController admin, CCSPlayerCon
|
||||
|
||||
**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
|
||||
@@ -312,35 +394,57 @@ _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:**
|
||||
**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:**
|
||||
**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<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)`
|
||||
#### `CreateMenuWithPlayers(MenuContext context, CCSPlayerController admin, Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)` 🆕 NEW!
|
||||
|
||||
Creates a menu with a list of players, filtered and with automatic back button.
|
||||
|
||||
**Parameters:**
|
||||
**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:**
|
||||
**Example (Old API):**
|
||||
```csharp
|
||||
return _api.CreateMenuWithPlayers("Select Player to Kick", "admin", admin,
|
||||
player => player.IsValid && admin.CanTarget(player),
|
||||
@@ -351,6 +455,19 @@ return _api.CreateMenuWithPlayers("Select Player to Kick", "admin", admin,
|
||||
});
|
||||
```
|
||||
|
||||
**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<CCSPlayerController> action, bool disabled = false, string? permission = null)`
|
||||
|
||||
Reference in New Issue
Block a user