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:
Dawid Bepierszcz
2025-10-20 01:27:01 +02:00
parent 21a5de6b3d
commit b0d8696756
74 changed files with 32732 additions and 279 deletions

View File

@@ -0,0 +1,465 @@
---
sidebar_position: 2
---
# Commands API
Complete reference for command registration and management.
## Command Registration
### RegisterCommand
Register a new command that integrates with CS2-SimpleAdmin.
```csharp
void RegisterCommand(string name, string? description, CommandInfo.CommandCallback callback)
```
**Parameters:**
- `name` - Command name (e.g., "css_mycommand")
- `description` - Command description (optional)
- `callback` - Method to call when command is executed
**Example:**
```csharp
_api.RegisterCommand("css_mycommand", "My custom command", OnMyCommand);
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Command logic here
}
```
**Throws:**
- `ArgumentException` - If command name is null or empty
- `ArgumentNullException` - If callback is null
---
### UnRegisterCommand
Unregister a previously registered command.
```csharp
void UnRegisterCommand(string commandName)
```
**Parameters:**
- `commandName` - Name of command to unregister
**Example:**
```csharp
_api.UnRegisterCommand("css_mycommand");
```
**Best Practice:**
Always unregister commands in your plugin's `Unload()` method:
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.UnRegisterCommand("css_mycommand");
}
```
---
## Target Parsing
### GetTarget
Parse player targets from command arguments.
```csharp
TargetResult? GetTarget(CommandInfo command)
```
**Parameters:**
- `command` - Command info containing arguments
**Returns:**
- `TargetResult` - Contains matched players
- `null` - If no targets found or error
**Example:**
```csharp
[CommandHelper(1, "<#userid or name>")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players)
{
// Do something with player
}
}
```
**Supported Target Syntax:**
- `@all` - All players
- `@ct` - All Counter-Terrorists
- `@t` - All Terrorists
- `@spec` - All spectators
- `@alive` - All alive players
- `@dead` - All dead players
- `@bot` - All bots
- `@human` - All human players
- `@me` - Command caller
- `#123` - Player by user ID
- `PlayerName` - Player by name (partial match)
---
## Command Logging
### LogCommand (with CommandInfo)
Log a command execution with full command info.
```csharp
void LogCommand(CCSPlayerController? caller, CommandInfo command)
```
**Parameters:**
- `caller` - Player who executed command (null for console)
- `command` - Command info object
**Example:**
```csharp
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Execute command logic
// Log the command
_api!.LogCommand(caller, command);
}
```
---
### LogCommand (with string)
Log a command execution with custom command string.
```csharp
void LogCommand(CCSPlayerController? caller, string command)
```
**Parameters:**
- `caller` - Player who executed command (null for console)
- `command` - Command string to log
**Example:**
```csharp
private void DoAction(CCSPlayerController? caller, CCSPlayerController target)
{
// Perform action
// Log with custom string
_api!.LogCommand(caller, $"css_mycommand {target.PlayerName}");
}
```
---
## Complete Example
### Basic Command
```csharp
private void RegisterCommands()
{
_api!.RegisterCommand("css_hello", "Say hello to a player", OnHelloCommand);
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnHelloCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid))
{
player.PrintToChat($"Hello {player.PlayerName}!");
}
_api.LogCommand(caller, command);
}
public override void Unload(bool hotReload)
{
_api?.UnRegisterCommand("css_hello");
}
```
---
### Command with Permission Check
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/ban")]
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
// Get targets
var targets = _api!.GetTarget(command);
if (targets == null) return;
// Filter for players caller can target
var validPlayers = targets.Players
.Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
.ToList();
foreach (var player in validPlayers)
{
// Issue ban
_api.IssuePenalty(player, caller, PenaltyType.Ban, "Banned via command", 1440);
}
_api.LogCommand(caller, command);
}
```
---
### Command with Arguments
```csharp
[CommandHelper(2, "<#userid or name> <value>")]
[RequiresPermissions("@css/slay")]
private void OnSetHpCommand(CCSPlayerController? caller, CommandInfo command)
{
// Parse HP value
if (!int.TryParse(command.GetArg(2), out int hp))
{
caller?.PrintToChat("Invalid HP value!");
return;
}
// Get targets
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && p.PawnIsAlive))
{
player.PlayerPawn?.Value?.SetHealth(hp);
}
_api.LogCommand(caller, $"css_sethp {hp}");
}
```
---
## Best Practices
### 1. Always Validate Targets
```csharp
var targets = _api!.GetTarget(command);
if (targets == null) return; // No targets found
// Filter for valid players
var validPlayers = targets.Players
.Where(p => p.IsValid && !p.IsBot)
.ToList();
if (validPlayers.Count == 0)
{
caller?.PrintToChat("No valid targets found!");
return;
}
```
### 2. Check Immunity
```csharp
foreach (var player in targets.Players)
{
// Check if caller can target this player (immunity check)
if (!caller!.CanTarget(player))
{
caller.PrintToChat($"You cannot target {player.PlayerName}!");
continue;
}
// Safe to target player
DoAction(player);
}
```
### 3. Always Log Commands
```csharp
// Log every admin command execution
_api.LogCommand(caller, command);
```
### 4. Use CommandHelper Attribute
```csharp
// Specify minimum args and usage
[CommandHelper(minArgs: 1, usage: "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnCommand(CCSPlayerController? caller, CommandInfo command)
{
// CounterStrikeSharp validates args automatically
}
```
### 5. Cleanup on Unload
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister ALL commands
_api.UnRegisterCommand("css_command1");
_api.UnRegisterCommand("css_command2");
}
```
---
## Common Patterns
### Multiple Aliases
```csharp
// Register same command with multiple aliases
var aliases = new[] { "css_mycommand", "css_mycmd", "css_mc" };
foreach (var alias in aliases)
{
_api.RegisterCommand(alias, "My command", OnMyCommand);
}
// Unregister all
public override void Unload(bool hotReload)
{
foreach (var alias in aliases)
{
_api?.UnRegisterCommand(alias);
}
}
```
### Command from Config
```csharp
// In Config.cs
public List<string> MyCommands { get; set; } = ["css_mycommand"];
// In Plugin
private void RegisterCommands()
{
foreach (var cmd in Config.MyCommands)
{
_api!.RegisterCommand(cmd, "Description", OnMyCommand);
}
}
// Allows users to add aliases or disable by clearing list
```
### Target Filtering
```csharp
// Get only alive players
var alivePlayers = targets.Players
.Where(p => p.IsValid && p.PawnIsAlive)
.ToList();
// Get only enemy team
var enemies = targets.Players
.Where(p => p.IsValid && p.Team != caller!.Team)
.ToList();
// Get targetable players
var targetable = targets.Players
.Where(p => p.IsValid && caller!.CanTarget(p))
.ToList();
```
---
## Error Handling
### Command Registration Errors
```csharp
try
{
_api.RegisterCommand("css_mycommand", "Description", OnMyCommand);
}
catch (ArgumentException ex)
{
Logger.LogError($"Failed to register command: {ex.Message}");
}
```
### Target Parsing Errors
```csharp
var targets = _api!.GetTarget(command);
if (targets == null)
{
// Target parsing failed
// Error message already sent to caller by SimpleAdmin
return;
}
if (targets.Players.Count == 0)
{
caller?.PrintToChat("No players matched your target!");
return;
}
```
---
## Performance Tips
### Cache Command Lists
```csharp
// Don't create new list every time
private readonly List<string> _commandAliases = new() { "css_cmd1", "css_cmd2" };
private void RegisterCommands()
{
foreach (var cmd in _commandAliases)
{
_api!.RegisterCommand(cmd, "Description", OnCommand);
}
}
```
### Efficient Target Filtering
```csharp
// ✅ Good - single LINQ query
var players = targets.Players
.Where(p => p.IsValid && !p.IsBot && p.PawnIsAlive && caller!.CanTarget(p))
.ToList();
// ❌ Bad - multiple iterations
var players = targets.Players.Where(p => p.IsValid).ToList();
players = players.Where(p => !p.IsBot).ToList();
players = players.Where(p => p.PawnIsAlive).ToList();
```
---
## Related APIs
- **[Menus API](menus)** - Create interactive menus
- **[Penalties API](penalties)** - Issue penalties from commands
- **[Utilities API](utilities)** - Helper functions for commands

View File

@@ -0,0 +1,642 @@
---
sidebar_position: 5
---
# Events API
Complete reference for CS2-SimpleAdmin event system.
## Event System Overview
CS2-SimpleAdmin exposes events that allow modules to react to plugin actions and state changes.
**All events use C# event delegates and should be subscribed to in `OnAllPluginsLoaded` and unsubscribed in `Unload`.**
---
## Plugin Lifecycle Events
### OnSimpleAdminReady
Fired when CS2-SimpleAdmin is fully initialized and ready.
```csharp
event Action? OnSimpleAdminReady
```
**When Fired:**
- After plugin load
- After database initialization
- After migrations complete
- Menu system ready
**Example:**
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
// Subscribe to ready event
_api.OnSimpleAdminReady += OnSimpleAdminReady;
// Also call directly for hot reload case
OnSimpleAdminReady();
}
private void OnSimpleAdminReady()
{
Logger.LogInformation("SimpleAdmin is ready!");
// Register menus (requires SimpleAdmin to be ready)
RegisterMenus();
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.OnSimpleAdminReady -= OnSimpleAdminReady;
}
```
**Best Practice:**
Always call your handler directly after subscribing to handle hot reload:
```csharp
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // ← Also call directly
```
---
## Penalty Events
### OnPlayerPenaltied
Fired when an **online player** receives a penalty.
```csharp
event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltied
```
**Parameters:**
1. `PlayerInfo player` - Player who received penalty
2. `PlayerInfo? admin` - Admin who issued penalty (null if console)
3. `PenaltyType penaltyType` - Type of penalty
4. `string reason` - Penalty reason
5. `int duration` - Duration in minutes (0 = permanent)
6. `int? penaltyId` - Database penalty ID (null if not stored)
7. `int? serverId` - Server ID (null in single-server mode)
**Example:**
```csharp
_api.OnPlayerPenaltied += OnPlayerPenaltied;
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
var adminName = admin?.PlayerName ?? "Console";
Logger.LogInformation($"{adminName} penaltied {player.PlayerName}: {type} ({duration}m) - {reason}");
// React to specific penalty types
switch (type)
{
case PenaltyType.Ban:
// Log ban to external system
LogBanToWebhook(player, admin, reason, duration);
break;
case PenaltyType.Warn:
// Check warning count
if (player.Warnings >= 3)
{
Logger.LogWarning($"{player.PlayerName} has {player.Warnings} warnings!");
}
break;
}
}
```
---
### OnPlayerPenaltiedAdded
Fired when a penalty is added to an **offline player** by SteamID.
```csharp
event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltiedAdded
```
**Parameters:**
1. `SteamID steamId` - Target player's SteamID
2. `PlayerInfo? admin` - Admin who issued penalty
3. `PenaltyType penaltyType` - Type of penalty
4. `string reason` - Penalty reason
5. `int duration` - Duration in minutes
6. `int? penaltyId` - Database penalty ID
7. `int? serverId` - Server ID
**Example:**
```csharp
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
private void OnPlayerPenaltiedAdded(
SteamID steamId,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
var adminName = admin?.PlayerName ?? "Console";
Logger.LogInformation($"Offline penalty: {adminName} -> SteamID {steamId}: {type} ({duration}m)");
// Log to external database or webhook
if (type == PenaltyType.Ban)
{
LogOfflineBan(steamId, admin, reason, duration);
}
}
```
---
## Admin Activity Events
### OnAdminShowActivity
Fired when an admin action is displayed to players.
```csharp
event Action<string, string?, bool, object>? OnAdminShowActivity
```
**Parameters:**
1. `string messageKey` - Translation key for the message
2. `string? callerName` - Admin name (null if console)
3. `bool dontPublish` - If true, don't broadcast to other systems
4. `object messageArgs` - Arguments for message formatting
**Example:**
```csharp
_api.OnAdminShowActivity += OnAdminShowActivity;
private void OnAdminShowActivity(
string messageKey,
string? callerName,
bool dontPublish,
object messageArgs)
{
if (dontPublish) return;
Logger.LogInformation($"Admin activity: {messageKey} by {callerName ?? "Console"}");
// Log to Discord, database, etc.
LogAdminAction(messageKey, callerName, messageArgs);
}
```
---
### OnAdminToggleSilent
Fired when an admin toggles silent mode.
```csharp
event Action<int, bool>? OnAdminToggleSilent
```
**Parameters:**
1. `int slot` - Player slot of admin
2. `bool status` - New silent status (true = silent, false = normal)
**Example:**
```csharp
_api.OnAdminToggleSilent += OnAdminToggleSilent;
private void OnAdminToggleSilent(int slot, bool status)
{
var player = Utilities.GetPlayerFromSlot(slot);
if (player == null) return;
var statusText = status ? "enabled" : "disabled";
Logger.LogInformation($"{player.PlayerName} {statusText} silent mode");
// Update UI or external systems
UpdateAdminStatus(player, status);
}
```
---
## Complete Examples
### Ban Logging System
```csharp
public class BanLogger
{
private ICS2_SimpleAdminApi? _api;
public void Initialize(ICS2_SimpleAdminApi api)
{
_api = api;
// Subscribe to both ban events
_api.OnPlayerPenaltied += OnPlayerPenaltied;
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
}
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
if (type != PenaltyType.Ban) return;
// Log to file
File.AppendAllText("bans.log",
$"[{DateTime.Now}] {player.PlayerName} ({player.SteamId}) " +
$"banned by {admin?.PlayerName ?? "Console"} " +
$"for {duration} minutes: {reason}\n");
// Send to Discord webhook
SendDiscordNotification(
$"🔨 **Ban Issued**\n" +
$"Player: {player.PlayerName}\n" +
$"Admin: {admin?.PlayerName ?? "Console"}\n" +
$"Duration: {FormatDuration(duration)}\n" +
$"Reason: {reason}"
);
}
private void OnPlayerPenaltiedAdded(
SteamID steamId,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
if (type != PenaltyType.Ban) return;
File.AppendAllText("bans.log",
$"[{DateTime.Now}] Offline ban: SteamID {steamId} " +
$"by {admin?.PlayerName ?? "Console"} " +
$"for {duration} minutes: {reason}\n");
}
private string FormatDuration(int minutes)
{
if (minutes == 0) return "Permanent";
if (minutes < 60) return $"{minutes} minutes";
if (minutes < 1440) return $"{minutes / 60} hours";
return $"{minutes / 1440} days";
}
}
```
---
### Warning Escalation System
```csharp
public class WarningEscalation
{
private ICS2_SimpleAdminApi? _api;
public void Initialize(ICS2_SimpleAdminApi api)
{
_api = api;
_api.OnPlayerPenaltied += OnPlayerPenaltied;
}
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
// Only handle warnings
if (type != PenaltyType.Warn) return;
Logger.LogInformation($"{player.PlayerName} now has {player.Warnings} warnings");
// Auto-escalate based on warning count
if (player.Warnings >= 3)
{
// 3 warnings = 1 hour ban
_api.IssuePenalty(
GetPlayerController(player.SteamId),
null,
PenaltyType.Ban,
"Automatic: 3 warnings",
60
);
}
else if (player.Warnings >= 5)
{
// 5 warnings = 1 day ban
_api.IssuePenalty(
GetPlayerController(player.SteamId),
null,
PenaltyType.Ban,
"Automatic: 5 warnings",
1440
);
}
}
}
```
---
### Admin Activity Monitor
```csharp
public class AdminMonitor
{
private readonly Dictionary<string, int> _adminActions = new();
public void Initialize(ICS2_SimpleAdminApi api)
{
api.OnAdminShowActivity += OnAdminShowActivity;
}
private void OnAdminShowActivity(
string messageKey,
string? callerName,
bool dontPublish,
object messageArgs)
{
if (callerName == null) return; // Ignore console actions
// Track admin actions
if (!_adminActions.ContainsKey(callerName))
{
_adminActions[callerName] = 0;
}
_adminActions[callerName]++;
// Log every 10th action
if (_adminActions[callerName] % 10 == 0)
{
Logger.LogInformation(
$"{callerName} has performed {_adminActions[callerName]} admin actions"
);
}
// Alert if admin is very active
if (_adminActions[callerName] > 100)
{
Logger.LogWarning($"{callerName} has performed many actions ({_adminActions[callerName]})");
}
}
}
```
---
## Best Practices
### 1. Always Unsubscribe
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
// Subscribe
_api.OnPlayerPenaltied += OnPlayerPenaltied;
_api.OnAdminShowActivity += OnAdminShowActivity;
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
// ALWAYS unsubscribe
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
_api.OnAdminShowActivity -= OnAdminShowActivity;
}
```
### 2. Handle Null Admins
```csharp
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin, // ← Can be null!
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
var adminName = admin?.PlayerName ?? "Console";
// Use adminName safely
}
```
### 3. Use Events for Integration
```csharp
// ✅ Good - React to penalties
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
if (type == PenaltyType.Ban)
{
NotifyExternalSystem(player, reason);
}
};
// ❌ Bad - Wrapping penalty methods
// Don't wrap IssuePenalty, use events instead
```
### 4. Check Event Parameters
```csharp
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
// Check nullable parameters
if (penaltyId.HasValue)
{
Logger.LogInformation($"Penalty ID: {penaltyId.Value}");
}
if (serverId.HasValue)
{
Logger.LogInformation($"Server ID: {serverId.Value}");
}
}
```
### 5. OnSimpleAdminReady Pattern
```csharp
// ✅ Good - Handles both normal load and hot reload
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus();
// ❌ Bad - Only works on normal load
_api.OnSimpleAdminReady += RegisterMenus;
```
---
## Common Patterns
### Event-Based Statistics
```csharp
public class ServerStatistics
{
private int _totalBans;
private int _totalMutes;
private int _totalWarnings;
public void Initialize(ICS2_SimpleAdminApi api)
{
api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
switch (type)
{
case PenaltyType.Ban:
_totalBans++;
break;
case PenaltyType.Mute:
case PenaltyType.Gag:
case PenaltyType.Silence:
_totalMutes++;
break;
case PenaltyType.Warn:
_totalWarnings++;
break;
}
};
}
public void PrintStatistics()
{
Logger.LogInformation($"Server Statistics:");
Logger.LogInformation($"Total Bans: {_totalBans}");
Logger.LogInformation($"Total Mutes: {_totalMutes}");
Logger.LogInformation($"Total Warnings: {_totalWarnings}");
}
}
```
### Conditional Event Handling
```csharp
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// Only handle bans
if (type != PenaltyType.Ban) return;
// Only handle permanent bans
if (duration != 0) return;
// Only handle admin-issued bans
if (admin == null) return;
// Process permanent admin bans
NotifyImportantBan(player, admin, reason);
};
```
---
## Performance Considerations
### Async Operations in Events
```csharp
// ⚠️ Be careful with async in event handlers
_api.OnPlayerPenaltied += async (player, admin, type, reason, duration, id, sid) =>
{
// Don't block the game thread
await Task.Run(() =>
{
// Long-running operation
LogToExternalDatabase(player, type, reason);
});
};
```
### Efficient Event Handlers
```csharp
// ✅ Good - Quick processing
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// Quick logging
Logger.LogInformation($"Ban: {player.PlayerName}");
};
// ❌ Bad - Heavy processing
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// Don't do expensive operations synchronously
SendEmailNotification(player); // ← This blocks the game thread!
};
```
---
## Troubleshooting
### Event Not Firing
**Check:**
1. Did you subscribe to the event?
2. Is `_api` not null?
3. Are you testing the right scenario?
4. Check server console for errors
### Memory Leaks
**Always unsubscribe:**
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unsubscribe ALL events
_api.OnSimpleAdminReady -= OnReady;
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
// ... etc
}
```
---
## Related APIs
- **[Penalties API](penalties)** - Issue penalties that trigger events
- **[Commands API](commands)** - Commands that may trigger admin activity
- **[Utilities API](utilities)** - Helper functions for event handlers

View File

@@ -0,0 +1,706 @@
---
sidebar_position: 3
---
# Menus API
Complete reference for creating interactive admin menus.
## Menu Categories
### RegisterMenuCategory
Register a new menu category in the admin menu.
```csharp
void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")
```
**Parameters:**
- `categoryId` - Unique identifier for the category
- `categoryName` - Display name shown in menu
- `permission` - Required permission to see category (default: "@css/generic")
**Example:**
```csharp
_api.RegisterMenuCategory("mycategory", "My Custom Category", "@css/generic");
```
**Best Practice:**
Register categories in the `OnSimpleAdminReady` event handler:
```csharp
_api.OnSimpleAdminReady += () =>
{
_api.RegisterMenuCategory("mycategory", Localizer?["category_name"] ?? "My Category");
};
```
---
## Menu Registration
### RegisterMenu (Basic)
Register a menu within a category.
```csharp
void RegisterMenu(
string categoryId,
string menuId,
string menuName,
Func<CCSPlayerController, object> menuFactory,
string? permission = null,
string? commandName = null
)
```
**Parameters:**
- `categoryId` - Category to add menu to
- `menuId` - Unique menu identifier
- `menuName` - Display name in menu
- `menuFactory` - Function that creates the menu
- `permission` - Required permission (optional)
- `commandName` - Command for permission override (optional, e.g., "css_god")
**Example:**
```csharp
_api.RegisterMenu(
"mycategory",
"mymenu",
"My Menu",
CreateMyMenu,
"@css/generic"
);
private object CreateMyMenu(CCSPlayerController player)
{
var menu = _api!.CreateMenuWithBack("My Menu", "mycategory", player);
// Add options...
return menu;
}
```
---
### RegisterMenu (with MenuContext) ⭐ RECOMMENDED
Register a menu with automatic context passing - **eliminates duplication!**
```csharp
void RegisterMenu(
string categoryId,
string menuId,
string menuName,
Func<CCSPlayerController, MenuContext, object> menuFactory,
string? permission = null,
string? commandName = null
)
```
**Parameters:**
- `categoryId` - Category to add menu to
- `menuId` - Unique menu identifier
- `menuName` - Display name in menu
- `menuFactory` - Function that receives player AND context
- `permission` - Required permission (optional)
- `commandName` - Command for permission override (optional)
**Example:**
```csharp
// ✅ NEW WAY - No duplication!
_api.RegisterMenu(
"fun",
"god",
"God Mode",
CreateGodMenu,
"@css/cheats",
"css_god"
);
private object CreateGodMenu(CCSPlayerController admin, MenuContext context)
{
// context contains: CategoryId, MenuId, MenuTitle, Permission, CommandName
return _api!.CreateMenuWithPlayers(
context, // ← Automatically uses "God Mode" and "fun"
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) => ToggleGod(admin, target)
);
}
// ❌ OLD WAY - Had to repeat "God Mode" and "fun"
private object CreateGodMenuOld(CCSPlayerController admin)
{
return _api!.CreateMenuWithPlayers(
"God Mode", // ← Repeated from RegisterMenu
"fun", // ← Repeated from RegisterMenu
admin,
filter,
action
);
}
```
**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"
}
```
---
### UnregisterMenu
Remove a menu from a category.
```csharp
void UnregisterMenu(string categoryId, string menuId)
```
**Example:**
```csharp
public override void Unload(bool hotReload)
{
_api?.UnregisterMenu("mycategory", "mymenu");
}
```
---
## Menu Creation
### CreateMenuWithBack
Create a menu with automatic back button.
```csharp
object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)
```
**Parameters:**
- `title` - Menu title
- `categoryId` - Category for back button navigation
- `player` - Player viewing the menu
**Example:**
```csharp
var menu = _api!.CreateMenuWithBack("My Menu", "mycategory", admin);
_api.AddMenuOption(menu, "Option 1", _ => DoAction1());
_api.AddMenuOption(menu, "Option 2", _ => DoAction2());
return menu;
```
---
### CreateMenuWithBack (with Context) ⭐ RECOMMENDED
Create a menu using context - **no duplication!**
```csharp
object CreateMenuWithBack(MenuContext context, CCSPlayerController player)
```
**Example:**
```csharp
private object CreateMenu(CCSPlayerController admin, MenuContext context)
{
// ✅ Uses context.MenuTitle and context.CategoryId automatically
var menu = _api!.CreateMenuWithBack(context, admin);
_api.AddMenuOption(menu, "Option 1", _ => DoAction1());
return menu;
}
```
---
### CreateMenuWithPlayers
Create a menu showing a list of players.
```csharp
object CreateMenuWithPlayers(
string title,
string categoryId,
CCSPlayerController admin,
Func<CCSPlayerController, bool> filter,
Action<CCSPlayerController, CCSPlayerController> onSelect
)
```
**Parameters:**
- `title` - Menu title
- `categoryId` - Category for back button
- `admin` - Admin viewing menu
- `filter` - Function to filter which players to show
- `onSelect` - Action when player is selected (admin, selectedPlayer)
**Example:**
```csharp
return _api!.CreateMenuWithPlayers(
"Select Player",
"mycategory",
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) =>
{
// Do something with selected player
DoAction(admin, target);
}
);
```
---
### CreateMenuWithPlayers (with Context) ⭐ RECOMMENDED
```csharp
object CreateMenuWithPlayers(
MenuContext context,
CCSPlayerController admin,
Func<CCSPlayerController, bool> filter,
Action<CCSPlayerController, CCSPlayerController> onSelect
)
```
**Example:**
```csharp
private object CreatePlayerMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context, // ← Automatically uses correct title and category!
admin,
player => player.PawnIsAlive && admin.CanTarget(player),
(admin, target) => PerformAction(admin, target)
);
}
```
---
## Menu Options
### AddMenuOption
Add a clickable option to a menu.
```csharp
void AddMenuOption(
object menu,
string name,
Action<CCSPlayerController> action,
bool disabled = false,
string? permission = null
)
```
**Parameters:**
- `menu` - Menu to add option to
- `name` - Option display text
- `action` - Function called when selected
- `disabled` - Whether option is disabled (default: false)
- `permission` - Required permission to see option (optional)
**Example:**
```csharp
var menu = _api!.CreateMenuWithBack("Actions", "mycategory", admin);
_api.AddMenuOption(menu, "Heal Player", _ =>
{
target.SetHp(100);
});
_api.AddMenuOption(menu, "Admin Only Option", _ =>
{
// Admin action
}, false, "@css/root");
return menu;
```
---
### AddSubMenu
Add a submenu option that opens another menu.
```csharp
void AddSubMenu(
object menu,
string name,
Func<CCSPlayerController, object> subMenuFactory,
bool disabled = false,
string? permission = null
)
```
**Parameters:**
- `menu` - Parent menu
- `name` - Submenu option display text
- `subMenuFactory` - Function that creates the submenu
- `disabled` - Whether option is disabled (default: false)
- `permission` - Required permission (optional)
**Example:**
```csharp
var menu = _api!.CreateMenuWithBack("Main Menu", "mycategory", admin);
_api.AddSubMenu(menu, "Player Actions", admin =>
{
return CreatePlayerActionsMenu(admin);
});
_api.AddSubMenu(menu, "Server Settings", admin =>
{
return CreateServerSettingsMenu(admin);
}, false, "@css/root");
return menu;
```
---
## Opening Menus
### OpenMenu
Display a menu to a player.
```csharp
void OpenMenu(object menu, CCSPlayerController player)
```
**Example:**
```csharp
var menu = CreateMyMenu(player);
_api!.OpenMenu(menu, player);
```
**Note:** Usually menus open automatically when selected, but this can be used for direct opening.
---
## Complete Examples
### Simple Player Selection Menu
```csharp
private void RegisterMenus()
{
_api!.RegisterMenuCategory("actions", "Player Actions", "@css/generic");
_api.RegisterMenu(
"actions",
"slay",
"Slay Player",
CreateSlayMenu,
"@css/slay"
);
}
private object CreateSlayMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.PawnIsAlive && admin.CanTarget(player),
(admin, target) =>
{
target.PlayerPawn?.Value?.CommitSuicide(false, true);
admin.PrintToChat($"Slayed {target.PlayerName}");
}
);
}
```
---
### Nested Menu with Value Selection
```csharp
private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => p.PawnIsAlive && admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateHpValueMenu(admin, player);
});
}
return menu;
}
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Set HP: {target.PlayerName}", "mycategory", admin);
var hpValues = new[] { 1, 10, 50, 100, 200, 500 };
foreach (var hp in hpValues)
{
_api.AddMenuOption(menu, $"{hp} HP", _ =>
{
if (target.IsValid && target.PawnIsAlive)
{
target.PlayerPawn?.Value?.SetHealth(hp);
admin.PrintToChat($"Set {target.PlayerName} HP to {hp}");
}
});
}
return menu;
}
```
---
### Menu with Permissions
```csharp
private object CreateAdminMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
// Everyone with menu access sees this
_api.AddMenuOption(menu, "Basic Action", _ => DoBasicAction());
// Only root admins see this
_api.AddMenuOption(menu, "Dangerous Action", _ =>
{
DoDangerousAction();
}, false, "@css/root");
// Submenu with permission
_api.AddSubMenu(menu, "Advanced Options", admin =>
{
return CreateAdvancedMenu(admin);
}, false, "@css/root");
return menu;
}
```
---
### Dynamic Menu with Current State
```csharp
private object CreateToggleMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => admin.CanTarget(p));
foreach (var player in players)
{
// Show current state in option name
bool hasGod = GodPlayers.Contains(player.Slot);
string status = hasGod ? "✓ ON" : "✗ OFF";
_api.AddMenuOption(menu, $"{player.PlayerName} ({status})", _ =>
{
if (hasGod)
GodPlayers.Remove(player.Slot);
else
GodPlayers.Add(player.Slot);
// Recreate menu to show updated state
var newMenu = CreateToggleMenu(admin, context);
_api.OpenMenu(newMenu, admin);
});
}
return menu;
}
```
---
## Best Practices
### 1. Use MenuContext
```csharp
// ✅ Good - Uses context
_api.RegisterMenu("cat", "id", "Title", CreateMenu, "@css/generic");
private object CreateMenu(CCSPlayerController admin, MenuContext context)
{
return _api.CreateMenuWithPlayers(context, admin, filter, action);
}
// ❌ Bad - Duplicates title and category
_api.RegisterMenu("cat", "id", "Title", CreateMenuOld, "@css/generic");
private object CreateMenuOld(CCSPlayerController admin)
{
return _api.CreateMenuWithPlayers("Title", "cat", admin, filter, action);
}
```
### 2. Register in OnSimpleAdminReady
```csharp
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Also call directly for hot reload
private void RegisterMenus()
{
if (_menusRegistered) return;
_api!.RegisterMenuCategory("category", "Category Name");
_api.RegisterMenu("category", "menu", "Menu Name", CreateMenu);
_menusRegistered = true;
}
```
### 3. Always Unregister
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.UnregisterMenu("category", "menu");
_api.OnSimpleAdminReady -= RegisterMenus;
}
```
### 4. Validate Player State
```csharp
private object CreateMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.IsValid && // Player exists
!player.IsBot && // Not a bot
player.PawnIsAlive && // Alive
admin.CanTarget(player), // Can be targeted
(admin, target) =>
{
// Extra validation before action
if (!target.IsValid || !target.PawnIsAlive)
return;
DoAction(admin, target);
}
);
}
```
### 5. Use Translations for Menu Names
```csharp
_api.RegisterMenuCategory(
"mycategory",
Localizer?["category_name"] ?? "Default Name",
"@css/generic"
);
_api.RegisterMenu(
"mycategory",
"mymenu",
Localizer?["menu_name"] ?? "Default Menu",
CreateMenu
);
```
---
## Permission Override
The `commandName` parameter allows server admins to override menu permissions via CounterStrikeSharp's admin system.
**Example:**
```csharp
_api.RegisterMenu(
"fun",
"god",
"God Mode",
CreateGodMenu,
"@css/cheats", // Default permission
"css_god" // Command name for override
);
```
**Admin config can override:**
```json
{
"css_god": ["@css/vip"]
}
```
Now VIPs will see the God Mode menu instead of requiring @css/cheats!
---
## Common Patterns
### Player List with Actions
```csharp
private object CreatePlayerListMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
foreach (var player in _api.GetValidPlayers())
{
if (!admin.CanTarget(player)) continue;
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
var actionMenu = _api.CreateMenuWithBack($"Actions: {player.PlayerName}", context.CategoryId, admin);
_api.AddMenuOption(actionMenu, "Slay", _ => player.CommitSuicide());
_api.AddMenuOption(actionMenu, "Kick", _ => KickPlayer(player));
_api.AddMenuOption(actionMenu, "Ban", _ => BanPlayer(admin, player));
return actionMenu;
});
}
return menu;
}
```
### Category-Based Organization
```csharp
private void RegisterAllMenus()
{
// Player management category
_api!.RegisterMenuCategory("players", "Player Management", "@css/generic");
_api.RegisterMenu("players", "kick", "Kick Player", CreateKickMenu, "@css/kick");
_api.RegisterMenu("players", "ban", "Ban Player", CreateBanMenu, "@css/ban");
// Server management category
_api.RegisterMenuCategory("server", "Server Management", "@css/generic");
_api.RegisterMenu("server", "map", "Change Map", CreateMapMenu, "@css/changemap");
_api.RegisterMenu("server", "settings", "Settings", CreateSettingsMenu, "@css/root");
}
```
---
## Related APIs
- **[Commands API](commands)** - Command integration
- **[Penalties API](penalties)** - Issue penalties from menus
- **[Utilities API](utilities)** - Helper functions for menus

View File

@@ -0,0 +1,621 @@
---
sidebar_position: 1
---
# API Overview
Complete reference for the CS2-SimpleAdmin API (ICS2_SimpleAdminApi).
## Introduction
The CS2-SimpleAdmin API is exposed via the `ICS2_SimpleAdminApi` interface, accessible through CounterStrikeSharp's capability system.
---
## Getting the API
### Using Capability System
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
public class YourPlugin : BasePlugin
{
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
new("simpleadmin:api");
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found!");
Unload(false);
return;
}
// API is ready to use
RegisterFeatures();
}
}
```
### Static Capability Reference
```csharp
// Alternative approach
var capability = ICS2_SimpleAdminApi.PluginCapability;
var api = capability?.Get();
```
---
## API Categories
The API is organized into logical categories:
| Category | Description | Learn More |
|----------|-------------|------------|
| **Commands** | Register/unregister commands, parse targets | [](commands) |
| **Menus** | Create admin menus with player selection | [](menus) |
| **Penalties** | Issue bans, mutes, gags, warnings | [](penalties) |
| **Events** | Subscribe to plugin events | [](events) |
| **Utilities** | Helper functions, player info, activity messages | [](utilities) |
---
## Quick Reference
### Command Management
```csharp
// Register command
_api.RegisterCommand(name, description, callback);
// Unregister command
_api.UnRegisterCommand(name);
// Parse player targets
var targets = _api.GetTarget(command);
// Log command
_api.LogCommand(caller, command);
```
**[Full Documentation →](commands)**
---
### Menu System
```csharp
// Register category
_api.RegisterMenuCategory(categoryId, categoryName, permission);
// Register menu
_api.RegisterMenu(categoryId, menuId, menuName, menuFactory, permission, commandName);
// Create menu with players
_api.CreateMenuWithPlayers(context, admin, filter, onSelect);
// Create menu with back button
_api.CreateMenuWithBack(context, admin);
// Add menu option
_api.AddMenuOption(menu, name, action, disabled, permission);
// Add submenu
_api.AddSubMenu(menu, name, subMenuFactory, disabled, permission);
// Open menu
_api.OpenMenu(menu, player);
// Unregister menu
_api.UnregisterMenu(categoryId, menuId);
```
**[Full Documentation →](menus)**
---
### Penalty Management
```csharp
// Issue penalty to online player
_api.IssuePenalty(player, admin, penaltyType, reason, duration);
// Issue penalty by SteamID
_api.IssuePenalty(steamId, admin, penaltyType, reason, duration);
// Get player info
var playerInfo = _api.GetPlayerInfo(player);
// Get mute status
var muteStatus = _api.GetPlayerMuteStatus(player);
```
**Penalty Types:**
- `PenaltyType.Ban` - Ban player
- `PenaltyType.Kick` - Kick player
- `PenaltyType.Gag` - Block text chat
- `PenaltyType.Mute` - Block voice chat
- `PenaltyType.Silence` - Block both
- `PenaltyType.Warn` - Issue warning
**[Full Documentation →](penalties)**
---
### Event System
```csharp
// Plugin ready event
_api.OnSimpleAdminReady += OnReady;
// Player penaltied
_api.OnPlayerPenaltied += OnPlayerPenaltied;
// Offline penalty added
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
// Admin activity
_api.OnAdminShowActivity += OnAdminActivity;
// Admin silent toggle
_api.OnAdminToggleSilent += OnAdminToggleSilent;
```
**[Full Documentation →](events)**
---
### Utility Functions
```csharp
// Get player info with penalties
var info = _api.GetPlayerInfo(player);
// Get database connection string
var connectionString = _api.GetConnectionString();
// Get server address
var serverAddress = _api.GetServerAddress();
// Get server ID
var serverId = _api.GetServerId();
// Get valid players
var players = _api.GetValidPlayers();
// Check if admin is silent
bool isSilent = _api.IsAdminSilent(player);
// Get all silent admins
var silentAdmins = _api.ListSilentAdminsSlots();
// Show admin activity
_api.ShowAdminActivity(messageKey, callerName, dontPublish, args);
// Show admin activity with custom translation
_api.ShowAdminActivityTranslated(translatedMessage, callerName, dontPublish);
// Show admin activity with module localizer (recommended)
_api.ShowAdminActivityLocalized(moduleLocalizer, messageKey, callerName, dontPublish, args);
```
**[Full Documentation →](utilities)**
---
## Common Patterns
### Basic Module Structure
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
namespace MyModule;
public class MyModule : BasePlugin
{
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
new("simpleadmin:api");
public override string ModuleName => "My Module";
public override string ModuleVersion => "1.0.0";
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
// Register features
RegisterCommands();
// Wait for SimpleAdmin ready
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Fallback for hot reload
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Cleanup
_api.UnRegisterCommand("css_mycommand");
_api.UnregisterMenu("category", "menu");
_api.OnSimpleAdminReady -= RegisterMenus;
}
}
```
---
### Command with Target Selection
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Parse targets
var targets = _api!.GetTarget(command);
if (targets == null) return;
// Filter valid players
var players = targets.Players
.Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
.ToList();
// Process each player
foreach (var player in players)
{
DoSomethingToPlayer(caller, player);
}
// Log command
_api.LogCommand(caller, command);
}
```
---
### Menu with Player Selection
```csharp
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
{
// Context contains categoryId, menuId, menuName, permission, commandName
return _api!.CreateMenuWithPlayers(
context, // Automatic title and category
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) =>
{
// Action when player selected
PerformAction(admin, target);
}
);
}
```
---
### Nested Menu
```csharp
private object CreatePlayerMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateActionMenu(admin, player);
});
}
return menu;
}
private object CreateActionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Actions for {target.PlayerName}", "category", admin);
_api.AddMenuOption(menu, "Action 1", _ => DoAction1(admin, target));
_api.AddMenuOption(menu, "Action 2", _ => DoAction2(admin, target));
return menu;
}
```
---
### Issue Penalty
```csharp
private void BanPlayer(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
{
// Issue ban
_api!.IssuePenalty(
target,
admin,
PenaltyType.Ban,
reason,
duration // minutes, 0 = permanent
);
// Show activity
if (admin == null || !_api.IsAdminSilent(admin))
{
_api.ShowAdminActivityLocalized(
Localizer,
"ban_message",
admin?.PlayerName,
false,
target.PlayerName,
duration
);
}
}
```
---
### Event Subscription
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
// Subscribe to events
_api.OnPlayerPenaltied += OnPlayerPenaltied;
}
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
Logger.LogInformation($"{player.PlayerName} received {type}: {reason} ({duration} min)");
// React to penalty
if (type == PenaltyType.Ban)
{
// Handle ban
}
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
}
```
---
## Best Practices
### 1. Always Check for Null
```csharp
if (_api == null)
{
Logger.LogError("API not available!");
return;
}
```
### 2. Use OnSimpleAdminReady Event
```csharp
_api.OnSimpleAdminReady += () =>
{
// Register menus only when SimpleAdmin is ready
RegisterMenus();
};
// Also call directly for hot reload case
RegisterMenus();
```
### 3. Clean Up on Unload
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister all commands
_api.UnRegisterCommand("css_mycommand");
// Unregister all menus
_api.UnregisterMenu("category", "menu");
// Unsubscribe all events
_api.OnSimpleAdminReady -= OnReady;
}
```
### 4. Validate Player State
```csharp
if (!player.IsValid || !player.PawnIsAlive)
{
return;
}
if (!caller.CanTarget(player))
{
return; // Immunity check
}
```
### 5. Use Per-Player Translations
```csharp
// Each player sees message in their configured language
_api.ShowAdminActivityLocalized(
Localizer, // Your module's localizer
"translation_key",
caller?.PlayerName,
false,
args
);
```
### 6. Log All Admin Actions
```csharp
_api.LogCommand(caller, command);
// or
_api.LogCommand(caller, $"css_mycommand {player.PlayerName}");
```
---
## API Versioning
The API uses semantic versioning:
- **Major** - Breaking changes
- **Minor** - New features, backwards compatible
- **Patch** - Bug fixes
**Current Version:** Check [GitHub Releases](https://github.com/daffyyyy/CS2-SimpleAdmin/releases)
---
## Thread Safety
The API is designed for single-threaded use within the CounterStrikeSharp game thread.
**Do NOT:**
- Call API methods from background threads
- Use async/await with API calls without proper synchronization
**Do:**
- Call API methods from event handlers
- Call API methods from commands
- Call API methods from timers
---
## Error Handling
The API uses exceptions for critical errors:
```csharp
try
{
_api.RegisterCommand("css_cmd", "Desc", callback);
}
catch (ArgumentException ex)
{
Logger.LogError($"Failed to register command: {ex.Message}");
}
```
**Common exceptions:**
- `ArgumentException` - Invalid arguments
- `InvalidOperationException` - Invalid state
- `KeyNotFoundException` - Player not found
---
## Performance Considerations
### Efficient Player Filtering
```csharp
// ✅ Good - single LINQ query
var players = _api.GetValidPlayers()
.Where(p => p.IsValid && admin.CanTarget(p))
.ToList();
// ❌ Bad - multiple iterations
var players = _api.GetValidPlayers();
players = players.Where(p => p.IsValid).ToList();
players = players.Where(p => admin.CanTarget(p)).ToList();
```
### Cache Expensive Operations
```csharp
// Cache menu creation if used multiple times
private object? _cachedMenu;
private object GetMenu(CCSPlayerController player)
{
if (_cachedMenu == null)
{
_cachedMenu = CreateMenu(player);
}
return _cachedMenu;
}
```
---
## Debugging
### Enable Detailed Logging
```csharp
Logger.LogInformation("Debug: API loaded");
Logger.LogWarning("Warning: Player not found");
Logger.LogError("Error: Failed to execute command");
```
### Check API Availability
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("❌ CS2-SimpleAdmin API not found!");
Logger.LogError("Make sure CS2-SimpleAdmin is installed and loaded.");
return;
}
Logger.LogInformation("✅ CS2-SimpleAdmin API loaded successfully");
}
```
---
## Next Steps
- **[Commands API](commands)** - Command registration and targeting
- **[Menus API](menus)** - Menu system details
- **[Penalties API](penalties)** - Penalty management
- **[Events API](events)** - Event subscription
- **[Utilities API](utilities)** - Helper functions
---
## Resources
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
- **[Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Reference implementation
- **[Module Development Guide](../module/getting-started)** - Create modules

View File

@@ -0,0 +1,610 @@
---
sidebar_position: 4
---
# Penalties API
Complete reference for issuing and managing player penalties.
## Penalty Types
```csharp
public enum PenaltyType
{
Ban, // Ban player from server
Kick, // Kick player from server
Gag, // Block text chat
Mute, // Block voice chat
Silence, // Block both text and voice
Warn // Issue warning
}
```
---
## Issue Penalties
### IssuePenalty (Online Player)
Issue a penalty to a currently connected player.
```csharp
void IssuePenalty(
CCSPlayerController player,
CCSPlayerController? admin,
PenaltyType penaltyType,
string reason,
int duration = -1
)
```
**Parameters:**
- `player` - Target player controller
- `admin` - Admin issuing penalty (null for console)
- `penaltyType` - Type of penalty
- `reason` - Reason for penalty
- `duration` - Duration in minutes (0 = permanent, -1 = default)
**Example:**
```csharp
// Ban player for 1 day
_api!.IssuePenalty(
player,
admin,
PenaltyType.Ban,
"Cheating",
1440 // 24 hours in minutes
);
// Permanent ban
_api.IssuePenalty(
player,
admin,
PenaltyType.Ban,
"Severe rule violation",
0
);
// Kick player
_api.IssuePenalty(
player,
admin,
PenaltyType.Kick,
"AFK"
);
// Gag for 30 minutes
_api.IssuePenalty(
player,
admin,
PenaltyType.Gag,
"Chat spam",
30
);
```
---
### IssuePenalty (Offline Player)
Issue a penalty to a player by SteamID (even if offline).
```csharp
void IssuePenalty(
SteamID steamid,
CCSPlayerController? admin,
PenaltyType penaltyType,
string reason,
int duration = -1
)
```
**Parameters:**
- `steamid` - Target player's SteamID
- `admin` - Admin issuing penalty (null for console)
- `penaltyType` - Type of penalty
- `reason` - Reason for penalty
- `duration` - Duration in minutes (0 = permanent, -1 = default)
**Example:**
```csharp
// Ban offline player
var steamId = new SteamID(76561198012345678);
_api!.IssuePenalty(
steamId,
admin,
PenaltyType.Ban,
"Ban evasion",
10080 // 7 days
);
// Mute offline player
_api.IssuePenalty(
steamId,
admin,
PenaltyType.Mute,
"Voice abuse",
1440
);
```
**Supported SteamID Formats:**
```csharp
// SteamID64
new SteamID(76561198012345678)
// Also works with SteamID string parsing
SteamID.FromString("STEAM_1:0:12345678")
SteamID.FromString("[U:1:12345678]")
```
---
## Get Player Information
### GetPlayerInfo
Get detailed player information including penalty counts.
```csharp
PlayerInfo GetPlayerInfo(CCSPlayerController player)
```
**Returns:** `PlayerInfo` object containing:
- `PlayerName` - Player's name
- `SteamId` - Steam ID (ulong)
- `IpAddress` - Player's IP address
- `Warnings` - Warning count
- `Bans` - Ban count
- `Mutes` - Mute count
- `Gags` - Gag count
**Example:**
```csharp
var playerInfo = _api!.GetPlayerInfo(player);
Console.WriteLine($"Player: {playerInfo.PlayerName}");
Console.WriteLine($"SteamID: {playerInfo.SteamId}");
Console.WriteLine($"Warnings: {playerInfo.Warnings}");
Console.WriteLine($"Total Bans: {playerInfo.Bans}");
// Check if player has penalties
if (playerInfo.Warnings >= 3)
{
_api.IssuePenalty(player, null, PenaltyType.Ban, "Too many warnings", 1440);
}
```
**Throws:**
- `KeyNotFoundException` - If player doesn't have a valid UserId
---
### GetPlayerMuteStatus
Get current mute/gag/silence status for a player.
```csharp
Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetPlayerMuteStatus(
CCSPlayerController player
)
```
**Returns:** Dictionary mapping penalty types to lists of active penalties
**Example:**
```csharp
var muteStatus = _api!.GetPlayerMuteStatus(player);
// Check if player is gagged
if (muteStatus.ContainsKey(PenaltyType.Gag))
{
var gagPenalties = muteStatus[PenaltyType.Gag];
foreach (var (endTime, duration, passed) in gagPenalties)
{
if (!passed)
{
var remaining = endTime - DateTime.UtcNow;
Console.WriteLine($"Gagged for {remaining.TotalMinutes:F0} more minutes");
}
}
}
// Check if player is muted
if (muteStatus.ContainsKey(PenaltyType.Mute))
{
Console.WriteLine("Player is currently muted");
}
// Check if player is silenced
if (muteStatus.ContainsKey(PenaltyType.Silence))
{
Console.WriteLine("Player is silenced (gag + mute)");
}
```
---
## Server Information
### GetConnectionString
Get the database connection string.
```csharp
string GetConnectionString()
```
**Example:**
```csharp
var connectionString = _api!.GetConnectionString();
// Use for custom database operations
```
---
### GetServerAddress
Get the server's IP address and port.
```csharp
string GetServerAddress()
```
**Example:**
```csharp
var serverAddress = _api!.GetServerAddress();
Console.WriteLine($"Server: {serverAddress}");
// Example output: "192.168.1.100:27015"
```
---
### GetServerId
Get the server's unique ID in the database.
```csharp
int? GetServerId()
```
**Returns:**
- `int` - Server ID if multi-server mode enabled
- `null` - If single-server mode
**Example:**
```csharp
var serverId = _api!.GetServerId();
if (serverId.HasValue)
{
Console.WriteLine($"Server ID: {serverId.Value}");
}
else
{
Console.WriteLine("Single-server mode");
}
```
---
## Complete Examples
### Ban with Validation
```csharp
private void BanPlayer(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
{
// Validate player
if (!target.IsValid)
{
admin?.PrintToChat("Invalid player!");
return;
}
// Check immunity
if (admin != null && !admin.CanTarget(target))
{
admin.PrintToChat($"You cannot ban {target.PlayerName} (higher immunity)!");
return;
}
// Get player info to check history
var playerInfo = _api!.GetPlayerInfo(target);
Logger.LogInformation(
$"{admin?.PlayerName ?? "Console"} banning {playerInfo.PlayerName} " +
$"(SteamID: {playerInfo.SteamId}, Previous bans: {playerInfo.Bans})"
);
// Issue ban
_api.IssuePenalty(target, admin, PenaltyType.Ban, reason, duration);
// Show activity
if (admin == null || !_api.IsAdminSilent(admin))
{
var durationText = duration == 0 ? "permanently" : $"for {duration} minutes";
Server.PrintToChatAll($"{admin?.PlayerName ?? "Console"} banned {target.PlayerName} {durationText}: {reason}");
}
}
```
---
### Progressive Punishment System
```csharp
private void HandlePlayerOffense(CCSPlayerController? admin, CCSPlayerController target, string reason)
{
var playerInfo = _api!.GetPlayerInfo(target);
// Progressive punishment based on warning count
if (playerInfo.Warnings == 0)
{
// First offense - warning
_api.IssuePenalty(target, admin, PenaltyType.Warn, reason);
target.PrintToChat("This is your first warning!");
}
else if (playerInfo.Warnings == 1)
{
// Second offense - gag for 30 minutes
_api.IssuePenalty(target, admin, PenaltyType.Gag, $"Second offense: {reason}", 30);
target.PrintToChat("Second warning! You are gagged for 30 minutes.");
}
else if (playerInfo.Warnings == 2)
{
// Third offense - 1 day ban
_api.IssuePenalty(target, admin, PenaltyType.Ban, $"Third offense: {reason}", 1440);
target.PrintToChat("Third offense! You are banned for 1 day.");
}
else
{
// More than 3 warnings - permanent ban
_api.IssuePenalty(target, admin, PenaltyType.Ban, $"Multiple offenses: {reason}", 0);
}
}
```
---
### Check Active Penalties Before Action
```csharp
private void AllowPlayerToChat(CCSPlayerController player)
{
var muteStatus = _api!.GetPlayerMuteStatus(player);
// Check if player is gagged
if (muteStatus.ContainsKey(PenaltyType.Gag))
{
player.PrintToChat("You are currently gagged and cannot use chat!");
return;
}
// Check if player is silenced (includes gag)
if (muteStatus.ContainsKey(PenaltyType.Silence))
{
player.PrintToChat("You are silenced and cannot communicate!");
return;
}
// Player can chat
ProcessChatMessage(player);
}
```
---
### Offline Player Ban
```csharp
private void BanOfflinePlayer(CCSPlayerController? admin, string steamIdString, int duration, string reason)
{
// Parse SteamID
if (!ulong.TryParse(steamIdString, out ulong steamId64))
{
admin?.PrintToChat("Invalid SteamID format!");
return;
}
var steamId = new SteamID(steamId64);
// Issue offline ban
_api!.IssuePenalty(steamId, admin, PenaltyType.Ban, reason, duration);
Logger.LogInformation(
$"{admin?.PlayerName ?? "Console"} banned offline player " +
$"(SteamID: {steamId64}) for {duration} minutes: {reason}"
);
admin?.PrintToChat($"Offline ban issued to SteamID {steamId64}");
}
```
---
### Multi-Account Detection
```csharp
[GameEventHandler]
public HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo info)
{
var player = @event.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
var playerInfo = _api!.GetPlayerInfo(player);
// Check if player has multiple accounts
if (playerInfo.Bans > 0)
{
// Notify admins
var admins = Utilities.GetPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, "@css/ban"));
foreach (var admin in admins)
{
admin.PrintToChat(
$"⚠ {player.PlayerName} has {playerInfo.Bans} previous ban(s)!"
);
}
}
return HookResult.Continue;
}
```
---
## Best Practices
### 1. Always Validate Players
```csharp
if (!target.IsValid || !target.PawnIsAlive)
{
return;
}
// Check immunity
if (admin != null && !admin.CanTarget(target))
{
admin.PrintToChat("Cannot target this player!");
return;
}
```
### 2. Provide Clear Reasons
```csharp
// ✅ Good - Specific reason
_api.IssuePenalty(player, admin, PenaltyType.Ban, "Aimbot detected in Round 12", 10080);
// ❌ Bad - Vague reason
_api.IssuePenalty(player, admin, PenaltyType.Ban, "cheating", 10080);
```
### 3. Log Penalty Actions
```csharp
_api.IssuePenalty(player, admin, PenaltyType.Ban, reason, duration);
Logger.LogInformation(
$"Penalty issued: {admin?.PlayerName ?? "Console"} -> {player.PlayerName} " +
$"| Type: {PenaltyType.Ban} | Duration: {duration}m | Reason: {reason}"
);
```
### 4. Handle Kick Separately
```csharp
// Kick doesn't need duration
_api.IssuePenalty(player, admin, PenaltyType.Kick, reason);
// NOT:
_api.IssuePenalty(player, admin, PenaltyType.Kick, reason, 0);
```
### 5. Check Active Penalties
```csharp
// Before issuing new penalty, check existing ones
var muteStatus = _api.GetPlayerMuteStatus(player);
if (muteStatus.ContainsKey(PenaltyType.Gag))
{
admin?.PrintToChat($"{player.PlayerName} is already gagged!");
return;
}
```
---
## Common Patterns
### Duration Helpers
```csharp
public static class PenaltyDurations
{
public const int OneHour = 60;
public const int OneDay = 1440;
public const int OneWeek = 10080;
public const int TwoWeeks = 20160;
public const int OneMonth = 43200;
public const int Permanent = 0;
}
// Usage
_api.IssuePenalty(player, admin, PenaltyType.Ban, reason, PenaltyDurations.OneWeek);
```
### Penalty History Display
```csharp
private void ShowPlayerHistory(CCSPlayerController admin, CCSPlayerController target)
{
var info = _api!.GetPlayerInfo(target);
admin.PrintToChat($"=== {info.PlayerName} History ===");
admin.PrintToChat($"Warnings: {info.Warnings}");
admin.PrintToChat($"Bans: {info.Bans}");
admin.PrintToChat($"Mutes: {info.Mutes}");
admin.PrintToChat($"Gags: {info.Gags}");
var muteStatus = _api.GetPlayerMuteStatus(target);
if (muteStatus.ContainsKey(PenaltyType.Gag))
admin.PrintToChat("Currently: GAGGED");
if (muteStatus.ContainsKey(PenaltyType.Mute))
admin.PrintToChat("Currently: MUTED");
if (muteStatus.ContainsKey(PenaltyType.Silence))
admin.PrintToChat("Currently: SILENCED");
}
```
---
## Error Handling
### Handle Invalid Players
```csharp
try
{
var playerInfo = _api!.GetPlayerInfo(player);
// Use playerInfo...
}
catch (KeyNotFoundException)
{
Logger.LogError($"Player info not found for {player?.PlayerName}");
return;
}
```
### Validate SteamID
```csharp
private bool TryParseSteamId(string input, out SteamID steamId)
{
steamId = default;
if (ulong.TryParse(input, out ulong steamId64))
{
steamId = new SteamID(steamId64);
return true;
}
return false;
}
```
---
## Related APIs
- **[Commands API](commands)** - Issue penalties from commands
- **[Menus API](menus)** - Issue penalties from menus
- **[Events API](events)** - React to penalty events
- **[Utilities API](utilities)** - Helper functions

View File

@@ -0,0 +1,585 @@
---
sidebar_position: 6
---
# Utilities API
Helper functions and utility methods for module development.
## Player Management
### GetValidPlayers
Get a list of all valid, connected players.
```csharp
List<CCSPlayerController> GetValidPlayers()
```
**Returns:** List of valid player controllers
**Example:**
```csharp
var players = _api!.GetValidPlayers();
foreach (var player in players)
{
Console.WriteLine($"Player: {player.PlayerName}");
}
// Filter for specific criteria
var alivePlayers = _api.GetValidPlayers()
.Where(p => p.PawnIsAlive)
.ToList();
var ctPlayers = _api.GetValidPlayers()
.Where(p => p.Team == CsTeam.CounterTerrorist)
.ToList();
```
**Note:** This method filters out invalid and bot players automatically.
---
## Admin Status
### IsAdminSilent
Check if an admin is in silent mode.
```csharp
bool IsAdminSilent(CCSPlayerController player)
```
**Parameters:**
- `player` - Player to check
**Returns:** `true` if player is in silent mode, `false` otherwise
**Example:**
```csharp
private void PerformAdminAction(CCSPlayerController admin, CCSPlayerController target)
{
// Do the action
DoAction(target);
// Only show activity if not silent
if (!_api!.IsAdminSilent(admin))
{
Server.PrintToChatAll($"{admin.PlayerName} performed action on {target.PlayerName}");
}
}
```
---
### ListSilentAdminsSlots
Get a list of player slots for all admins currently in silent mode.
```csharp
HashSet<int> ListSilentAdminsSlots()
```
**Returns:** HashSet of player slots
**Example:**
```csharp
var silentAdmins = _api!.ListSilentAdminsSlots();
Console.WriteLine($"Silent admins: {silentAdmins.Count}");
foreach (var slot in silentAdmins)
{
var player = Utilities.GetPlayerFromSlot(slot);
if (player != null)
{
Console.WriteLine($"- {player.PlayerName} (slot {slot})");
}
}
```
---
## Activity Messages
### ShowAdminActivity
Show an admin activity message to all players.
```csharp
void ShowAdminActivity(
string messageKey,
string? callerName = null,
bool dontPublish = false,
params object[] messageArgs
)
```
**Parameters:**
- `messageKey` - Translation key from SimpleAdmin's lang files
- `callerName` - Admin name (null for console)
- `dontPublish` - If true, don't trigger OnAdminShowActivity event
- `messageArgs` - Arguments for message formatting
**Example:**
```csharp
// Using SimpleAdmin's built-in translations
_api!.ShowAdminActivity(
"sa_admin_player_kick_message", // Translation key
admin?.PlayerName,
false,
player.PlayerName,
reason
);
```
**Limitations:**
- Only works with SimpleAdmin's own translation keys
- For module-specific messages, use `ShowAdminActivityLocalized`
---
### ShowAdminActivityTranslated
Show a pre-translated admin activity message.
```csharp
void ShowAdminActivityTranslated(
string translatedMessage,
string? callerName = null,
bool dontPublish = false
)
```
**Parameters:**
- `translatedMessage` - Already translated message
- `callerName` - Admin name
- `dontPublish` - If true, don't trigger event
**Example:**
```csharp
// Use when you've already translated the message
var message = Localizer?["my_action_message", player.PlayerName] ?? $"Action on {player.PlayerName}";
_api!.ShowAdminActivityTranslated(
message,
admin?.PlayerName,
false
);
```
**Use Case:**
- When you need custom message formatting
- When translation is already done
---
### ShowAdminActivityLocalized ⭐ RECOMMENDED
Show admin activity with per-player language support using module's localizer.
```csharp
void ShowAdminActivityLocalized(
object moduleLocalizer,
string messageKey,
string? callerName = null,
bool dontPublish = false,
params object[] messageArgs
)
```
**Parameters:**
- `moduleLocalizer` - Your module's `IStringLocalizer` instance
- `messageKey` - Translation key from your module's lang files
- `callerName` - Admin name
- `dontPublish` - If true, don't trigger event
- `messageArgs` - Message arguments
**Example:**
```csharp
// Each player sees message in their configured language!
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer, // Your module's localizer
"fun_admin_god_message", // From your lang/en.json
admin?.PlayerName,
false,
player.PlayerName
);
}
```
**lang/en.json:**
```json
{
"fun_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!"
}
```
**Why This is Best:**
- ✅ Each player sees message in their own language
- ✅ Uses your module's translations
- ✅ Supports color codes
- ✅ Per-player localization
---
## Complete Examples
### Action with Activity Message
```csharp
private void ToggleGodMode(CCSPlayerController? admin, CCSPlayerController target)
{
// Perform action
if (GodPlayers.Contains(target.Slot))
{
GodPlayers.Remove(target.Slot);
}
else
{
GodPlayers.Add(target.Slot);
}
// Show activity (respecting silent mode)
if (admin == null || !_api!.IsAdminSilent(admin))
{
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer,
"fun_admin_god_message",
admin?.PlayerName,
false,
target.PlayerName
);
}
}
// Log action
_api!.LogCommand(admin, $"css_god {target.PlayerName}");
}
```
---
### Broadcast to Non-Silent Admins
```csharp
private void NotifyAdmins(string message)
{
var silentAdmins = _api!.ListSilentAdminsSlots();
var players = _api.GetValidPlayers();
foreach (var player in players)
{
// Check if player is admin
if (!AdminManager.PlayerHasPermissions(player, "@css/generic"))
continue;
// Skip if admin is in silent mode
if (silentAdmins.Contains(player.Slot))
continue;
player.PrintToChat(message);
}
}
```
---
### Filter Players by Criteria
```csharp
private List<CCSPlayerController> GetTargetablePlayers(CCSPlayerController admin)
{
return _api!.GetValidPlayers()
.Where(p =>
p.IsValid &&
!p.IsBot &&
p.PawnIsAlive &&
admin.CanTarget(p))
.ToList();
}
private List<CCSPlayerController> GetAliveEnemies(CCSPlayerController player)
{
return _api!.GetValidPlayers()
.Where(p =>
p.Team != player.Team &&
p.PawnIsAlive)
.ToList();
}
private List<CCSPlayerController> GetAdmins()
{
return _api!.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, "@css/generic"))
.ToList();
}
```
---
## Best Practices
### 1. Use ShowAdminActivityLocalized
```csharp
// ✅ Good - Per-player language
_api.ShowAdminActivityLocalized(
Localizer,
"my_message_key",
admin?.PlayerName,
false,
args
);
// ❌ Bad - Single language for all
Server.PrintToChatAll($"{admin?.PlayerName} did something");
```
### 2. Respect Silent Mode
```csharp
// ✅ Good - Check silent mode
if (admin == null || !_api.IsAdminSilent(admin))
{
ShowActivity();
}
// ❌ Bad - Always show activity
ShowActivity(); // Ignores silent mode!
```
### 3. Validate Players from GetValidPlayers
```csharp
var players = _api.GetValidPlayers();
foreach (var player in players)
{
// Still good to check, especially for async operations
if (!player.IsValid) continue;
DoSomething(player);
}
```
### 4. Cache Silent Admin List if Checking Multiple Times
```csharp
// ✅ Good - Cache for multiple checks
var silentAdmins = _api.ListSilentAdminsSlots();
foreach (var admin in admins)
{
if (silentAdmins.Contains(admin.Slot)) continue;
NotifyAdmin(admin);
}
// ❌ Bad - Query for each admin
foreach (var admin in admins)
{
if (_api.IsAdminSilent(admin)) continue; // ← Repeated calls
NotifyAdmin(admin);
}
```
---
## Common Patterns
### Silent Mode Wrapper
```csharp
private void ShowActivityIfNotSilent(
CCSPlayerController? admin,
string messageKey,
params object[] args)
{
if (admin != null && _api!.IsAdminSilent(admin))
return;
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer,
messageKey,
admin?.PlayerName,
false,
args
);
}
}
// Usage
ShowActivityIfNotSilent(admin, "my_action", player.PlayerName);
```
---
### Get Online Admins
```csharp
private List<CCSPlayerController> GetOnlineAdmins(string permission = "@css/generic")
{
return _api!.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, permission))
.ToList();
}
// Usage
var admins = GetOnlineAdmins("@css/root");
foreach (var admin in admins)
{
admin.PrintToChat("Important admin message");
}
```
---
### Notify All Players Except Silent Admins
```csharp
private void BroadcastMessage(string message, bool excludeSilentAdmins = true)
{
var silentAdmins = excludeSilentAdmins
? _api!.ListSilentAdminsSlots()
: new HashSet<int>();
foreach (var player in _api.GetValidPlayers())
{
if (silentAdmins.Contains(player.Slot))
continue;
player.PrintToChat(message);
}
}
```
---
## Activity Message Formatting
### Color Codes in Messages
All activity messages support color codes:
```json
{
"my_message": "{lightred}Admin{default} banned {lightred}{0}{default} for {yellow}{1}{default}"
}
```
**Available Colors:**
- `{default}` - Default color
- `{white}` - White
- `{darkred}` - Dark red
- `{green}` - Green
- `{lightyellow}` - Light yellow
- `{lightblue}` - Light blue
- `{olive}` - Olive
- `{lime}` - Lime
- `{red}` - Red
- `{purple}` - Purple
- `{grey}` - Grey
- `{yellow}` - Yellow
- `{gold}` - Gold
- `{silver}` - Silver
- `{blue}` - Blue
- `{darkblue}` - Dark blue
- `{bluegrey}` - Blue grey
- `{magenta}` - Magenta
- `{lightred}` - Light red
- `{orange}` - Orange
---
### Message Arguments
```csharp
// lang/en.json
{
"ban_message": "{lightred}{0}{default} banned {lightred}{1}{default} for {yellow}{2}{default} minutes: {red}{3}"
}
// Code
_api.ShowAdminActivityLocalized(
Localizer,
"ban_message",
admin?.PlayerName,
false,
admin?.PlayerName, // {0}
target.PlayerName, // {1}
duration, // {2}
reason // {3}
);
```
---
## Performance Tips
### Minimize GetValidPlayers Calls
```csharp
// ✅ Good - Call once, filter multiple times
var allPlayers = _api.GetValidPlayers();
var alivePlayers = allPlayers.Where(p => p.PawnIsAlive).ToList();
var deadPlayers = allPlayers.Where(p => !p.PawnIsAlive).ToList();
// ❌ Bad - Multiple calls
var alivePlayers = _api.GetValidPlayers().Where(p => p.PawnIsAlive).ToList();
var deadPlayers = _api.GetValidPlayers().Where(p => !p.PawnIsAlive).ToList();
```
---
### Efficient Filtering
```csharp
// ✅ Good - Single LINQ query
var targets = _api.GetValidPlayers()
.Where(p => p.Team == CsTeam.Terrorist &&
p.PawnIsAlive &&
admin.CanTarget(p))
.ToList();
// ❌ Bad - Multiple iterations
var players = _api.GetValidPlayers();
players = players.Where(p => p.Team == CsTeam.Terrorist).ToList();
players = players.Where(p => p.PawnIsAlive).ToList();
players = players.Where(p => admin.CanTarget(p)).ToList();
```
---
## Troubleshooting
### Activity Messages Not Showing
**Check:**
1. Is `Localizer` not null?
2. Does translation key exist in lang files?
3. Is message correctly formatted?
4. Check `dontPublish` parameter
### Silent Mode Not Working
**Check:**
1. Is player actually in silent mode? (`css_hide` command)
2. Are you checking before showing activity?
3. Check slot vs player controller mismatch
---
## Related APIs
- **[Commands API](commands)** - Log commands
- **[Menus API](menus)** - Get players for menus
- **[Events API](events)** - Admin activity events
- **[Penalties API](penalties)** - Get player info

View File

@@ -0,0 +1,695 @@
---
sidebar_position: 8
---
# Plugin Architecture
Deep dive into CS2-SimpleAdmin's architecture and design patterns.
## Overview
CS2-SimpleAdmin follows a **layered architecture** with clear separation of concerns and well-defined responsibilities for each component.
---
## Architecture Layers
```
┌─────────────────────────────────────────┐
│ CounterStrikeSharp Integration Layer │ ← CS2_SimpleAdmin.cs
├─────────────────────────────────────────┤
│ Manager Layer │ ← /Managers/
│ • PermissionManager │
│ • BanManager │
│ • MuteManager │
│ • WarnManager │
│ • CacheManager │
│ • PlayerManager │
│ • ServerManager │
│ • DiscordManager │
├─────────────────────────────────────────┤
│ Database Layer │ ← /Database/
│ • IDatabaseProvider (Interface) │
│ • MySqlDatabaseProvider │
│ • SqliteDatabaseProvider │
│ • Migration System │
├─────────────────────────────────────────┤
│ Menu System │ ← /Menus/
│ • MenuManager (Singleton) │
│ • MenuBuilder (Factory) │
│ • Specific Menu Classes │
├─────────────────────────────────────────┤
│ Command System │ ← /Commands/
│ • RegisterCommands │
│ • Command Handlers (basebans, etc.) │
├─────────────────────────────────────────┤
│ Public API │ ← /Api/
│ • ICS2_SimpleAdminApi (Interface) │
│ • CS2_SimpleAdminApi (Implementation) │
└─────────────────────────────────────────┘
```
---
## Core Components
### 1. CounterStrikeSharp Integration Layer
**File:** `CS2_SimpleAdmin.cs`
**Responsibilities:**
- Plugin lifecycle management (Load/Unload)
- Event registration (`player_connect`, `player_disconnect`, etc.)
- Command routing
- Low-level game operations using `MemoryFunctionVoid`
- Timer management
**Key Methods:**
```csharp
public override void Load(bool hotReload)
public override void Unload(bool hotReload)
private HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo info)
private HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
```
---
### 2. Manager Layer
Each manager encapsulates specific domain logic:
#### PermissionManager
**File:** `/Managers/PermissionManager.cs`
**Responsibilities:**
- Load admin flags and groups from database
- Maintain in-memory `AdminCache` with lazy-loading
- Time-based cache expiry
- Immunity level management
**Key Patterns:**
- Caching for performance
- Lazy loading of admin data
- Periodic refresh
#### BanManager
**File:** `/Managers/BanManager.cs`
**Responsibilities:**
- Issue bans (SteamID, IP, or hybrid)
- Remove bans (unban)
- Handle ban expiration cleanup
- Multi-server ban synchronization
**Key Operations:**
```csharp
Task BanPlayer(...)
Task AddBanBySteamId(...)
Task RemoveBan(...)
```
#### MuteManager
**File:** `/Managers/MuteManager.cs`
**Responsibilities:**
- Three mute types: GAG (text), MUTE (voice), SILENCE (both)
- Duration-based mutes
- Expiration tracking
#### WarnManager
**File:** `/Managers/WarnManager.cs`
**Responsibilities:**
- Progressive warning system
- Auto-escalation to bans based on `WarnThreshold` config
- Warning history tracking
#### CacheManager
**File:** `/Managers/CacheManager.cs`
**Purpose:** Performance optimization layer
**Features:**
- In-memory ban cache with O(1) lookups by SteamID and IP
- Player IP history tracking for multi-account detection
- Reduces database queries on player join
**Data Structures:**
```csharp
Dictionary<ulong, BanInfo> _banCacheBySteamId
Dictionary<string, List<BanInfo>> _banCacheByIp
Dictionary<ulong, List<string>> _playerIpHistory
```
#### PlayerManager
**File:** `/Managers/PlayerManager.cs`
**Responsibilities:**
- Load player data on connect
- Check bans against cache
- Update IP history
- Semaphore limiting (max 5 concurrent loads)
**Key Pattern:**
```csharp
private readonly SemaphoreSlim _semaphore = new(5, 5);
public async Task LoadPlayerData(CCSPlayerController player)
{
await _semaphore.WaitAsync();
try
{
// Load player data
}
finally
{
_semaphore.Release();
}
}
```
#### ServerManager
**File:** `/Managers/ServerManager.cs`
**Responsibilities:**
- Load/register server metadata (IP, port, hostname, RCON)
- Multi-server mode support
- Server ID management
#### DiscordManager
**File:** `/Managers/DiscordManager.cs`
**Responsibilities:**
- Send webhook notifications for admin actions
- Configurable webhooks per penalty type
- Embed formatting with placeholders
---
### 3. Database Layer
**Files:** `/Database/`
**Provider Pattern** for database abstraction:
```csharp
public interface IDatabaseProvider
{
Task ExecuteAsync(string query, object? parameters = null);
Task<T> QueryFirstOrDefaultAsync<T>(string query, object? parameters = null);
Task<List<T>> QueryAsync<T>(string query, object? parameters = null);
// Query generation methods
string GetBanQuery(bool multiServer);
string GetMuteQuery(bool multiServer);
// ... more query methods
}
```
**Implementations:**
- `MySqlDatabaseProvider` - MySQL-specific SQL syntax
- `SqliteDatabaseProvider` - SQLite-specific SQL syntax
**Benefits:**
- Single codebase supports both MySQL and SQLite
- Easy to add new database providers
- Query methods accept `multiServer` boolean for scoping
**Migration System:**
**File:** `Database/Migration.cs`
- File-based migrations in `/Database/Migrations/{mysql,sqlite}/`
- Numbered files: `001_CreateTables.sql`, `002_AddColumn.sql`
- Tracking table: `sa_migrations`
- Auto-applies on plugin load
- Safe for multi-server environments
---
### 4. Menu System
**Files:** `/Menus/`
**MenuManager (Singleton Pattern):**
```csharp
public class MenuManager
{
public static MenuManager Instance { get; private set; }
private readonly Dictionary<string, MenuCategory> _categories = new();
private readonly Dictionary<string, Dictionary<string, MenuInfo>> _menus = new();
public void RegisterCategory(string id, string name, string permission);
public void RegisterMenu(string categoryId, string menuId, string name, ...);
public MenuBuilder CreateCategoryMenuPublic(MenuCategory category, CCSPlayerController player);
}
```
**MenuBuilder (Factory Pattern):**
```csharp
public class MenuBuilder
{
public MenuBuilder(string title);
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, ...);
public MenuBuilder AddSubMenu(string name, Func<CCSPlayerController, MenuBuilder> factory, ...);
public MenuBuilder WithBackAction(Action<CCSPlayerController> backAction);
public void OpenMenu(CCSPlayerController player);
}
```
**Specific Menu Classes:**
- `AdminMenu` - Main admin menu with categories
- `ManagePlayersMenu` - Player management menus
- `ManageServerMenu` - Server settings
- `DurationMenu` - Duration selection
- `ReasonMenu` - Reason selection
**Benefits:**
- Centralized menu management
- Permission-aware rendering
- Automatic back button handling
- Reusable menu components
---
### 5. Command System
**Files:** `/Commands/`
**Central Registration:**
**File:** `RegisterCommands.cs`
```csharp
public static class RegisterCommands
{
public static Dictionary<string, List<CommandDefinition>> _commandDefinitions = new();
public static void RegisterCommands(CS2_SimpleAdmin plugin)
{
// Load Commands.json
// Map commands to handler methods
// Register with CounterStrikeSharp
}
}
```
**Command Handlers:**
Organized by category:
- `basebans.cs` - Ban, unban, warn commands
- `basecomms.cs` - Gag, mute, silence commands
- `basecommands.cs` - Admin management, server commands
- `basechat.cs` - Chat commands (asay, csay, etc.)
- `playercommands.cs` - Player manipulation (slay, hp, etc.)
- `funcommands.cs` - Fun commands (god, noclip, etc.)
- `basevotes.cs` - Voting system
**Two-Tier Pattern:**
```csharp
// Entry command - parses arguments
[CommandHelper(2, "<#userid> <duration> [reason]")]
[RequiresPermissions("@css/ban")]
public void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = GetTarget(command);
int duration = ParseDuration(command.GetArg(2));
string reason = ParseReason(command);
foreach (var target in targets)
{
Ban(caller, target, duration, reason); // Core method
}
}
// Core method - database writes, events
private void Ban(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
{
// Write to database
BanManager.BanPlayer(target, admin, duration, reason);
// Update cache
CacheManager.AddBan(target);
// Trigger events
ApiInstance.OnPlayerPenaltiedEvent(target, admin, PenaltyType.Ban, reason, duration);
// Kick player
Server.ExecuteCommand($"kick {target.UserId}");
// Send Discord notification
DiscordManager.SendBanNotification(target, admin, duration, reason);
// Broadcast action
ShowAdminActivity("ban_message", admin?.PlayerName, target.PlayerName, duration, reason);
}
```
**Benefits:**
- Separation of parsing and execution
- Reusable core methods
- Consistent event triggering
- Easy to test
---
### 6. Public API
**Files:** `/Api/`
**Interface:** `ICS2_SimpleAdminApi.cs` (in CS2-SimpleAdminApi project)
**Implementation:** `CS2_SimpleAdminApi.cs`
**Capability System:**
```csharp
// In API interface
public static readonly PluginCapability<ICS2_SimpleAdminApi> PluginCapability = new("simpleadmin:api");
// In module
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
}
```
**Event Publishing:**
```csharp
// API exposes events
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, ...>? OnPlayerPenaltied;
// Core plugin triggers events
ApiInstance.OnPlayerPenaltiedEvent(player, admin, type, reason, duration, id);
// Modules subscribe
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// React to penalty
};
```
---
## Data Flow Patterns
### Player Join Flow
```
1. player_connect event
2. PlayerManager.LoadPlayerData()
3. Semaphore.WaitAsync() ← Max 5 concurrent
4. CacheManager.CheckBan(steamId, ip)
5a. BANNED → Kick player immediately
5b. CLEAN → Continue
6. Load active penalties from DB
7. Store in PlayersInfo dictionary
8. Update player IP history
```
### Ban Command Flow
```
1. OnBanCommand() ← Parse arguments
2. Ban() ← Core method
3. BanManager.BanPlayer() ← Write to DB
4. CacheManager.AddBan() ← Update cache
5. ApiInstance.OnPlayerPenaltiedEvent() ← Trigger event
6. Server.ExecuteCommand("kick") ← Kick player
7. DiscordManager.SendNotification() ← Discord webhook
8. ShowAdminActivity() ← Broadcast action
```
### Admin Permission Check Flow
```
1. Plugin Load
2. PermissionManager.LoadAdmins()
3. Build AdminCache ← SteamID → Flags/Immunity
4. Command Execution
5. RequiresPermissions attribute check
6. AdminManager.PlayerHasPermissions() ← Check cache
7a. HAS PERMISSION → Execute
7b. NO PERMISSION → Deny
```
---
## Design Patterns Used
### Singleton Pattern
```csharp
public class MenuManager
{
public static MenuManager Instance { get; private set; }
public static void Initialize(CS2_SimpleAdmin plugin)
{
Instance = new MenuManager(plugin);
}
}
```
**Used for:**
- MenuManager - Single menu registry
- Cache management - Single source of truth
### Factory Pattern
```csharp
public class MenuBuilder
{
public static MenuBuilder Create(string title) => new MenuBuilder(title);
public MenuBuilder AddOption(...) { /* ... */ return this; }
public MenuBuilder AddSubMenu(...) { /* ... */ return this; }
}
```
**Used for:**
- Menu creation
- Database provider creation
### Strategy Pattern
```csharp
public interface IDatabaseProvider
{
Task<List<BanInfo>> GetBans(bool multiServer);
}
public class MySqlDatabaseProvider : IDatabaseProvider { /* ... */ }
public class SqliteDatabaseProvider : IDatabaseProvider { /* ... */ }
```
**Used for:**
- Database abstraction
- Query generation per DB type
### Observer Pattern
```csharp
// Publisher
public event Action<PlayerInfo, ...>? OnPlayerPenaltied;
// Trigger
OnPlayerPenaltied?.Invoke(player, admin, type, reason, duration, id, sid);
// Subscribers
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// React
};
```
**Used for:**
- Event system
- Module communication
---
## Concurrency & Thread Safety
### Async/Await Patterns
All database operations use `async`/`await`:
```csharp
public async Task BanPlayer(...)
{
await _database.ExecuteAsync(query, parameters);
}
```
### Semaphore for Rate Limiting
```csharp
private readonly SemaphoreSlim _semaphore = new(5, 5);
public async Task LoadPlayerData(CCSPlayerController player)
{
await _semaphore.WaitAsync();
try
{
// Load data
}
finally
{
_semaphore.Release();
}
}
```
### Thread-Safe Collections
```csharp
private readonly ConcurrentDictionary<ulong, PlayerInfo> PlayersInfo = new();
```
---
## Memory Management
### In-Memory Caches
**AdminCache:**
```csharp
Dictionary<ulong, (List<string> Flags, int Immunity, DateTime Expiry)> AdminCache
```
**BanCache:**
```csharp
Dictionary<ulong, BanInfo> _banCacheBySteamId
Dictionary<string, List<BanInfo>> _banCacheByIp
```
**Benefits:**
- Reduces database load
- O(1) lookups
- TTL-based expiry
### Cleanup
```csharp
// On player disconnect
PlayersInfo.TryRemove(player.SteamID, out _);
// Periodic cache cleanup
AddTimer(3600f, CleanupExpiredCache, TimerFlags.REPEAT);
```
---
## Configuration System
### Multi-Level Configuration
1. **Main Config:** `CS2-SimpleAdmin.json`
2. **Commands Config:** `Commands.json`
3. **Module Configs:** Per-module JSON files
### Hot Reload Support
```csharp
public void OnConfigParsed(Config config)
{
Config = config;
// Reconfigure without restart
}
```
---
## Performance Optimizations
1. **Caching** - Minimize database queries
2. **Lazy Loading** - Load admin data on-demand
3. **Semaphore** - Limit concurrent operations
4. **Connection Pooling** - Reuse DB connections
5. **Indexed Queries** - Fast database lookups
6. **Memory Cleanup** - Remove disconnected player data
---
## Future Extensibility
### Plugin Capabilities
New modules can extend functionality:
```csharp
// New capability
var customCapability = new PluginCapability<ICustomFeature>("custom:feature");
Capabilities.RegisterPluginCapability(customCapability, () => _customFeature);
// Other plugins can use it
var feature = _customCapability.Get();
```
### Event-Driven Architecture
New events can be added without breaking changes:
```csharp
public event Action<NewEventArgs>? OnNewEvent;
```
---
## Testing Considerations
### Unit Testing
- Managers can be tested independently
- Mock `IDatabaseProvider` for testing
- Test command handlers with mock players
### Integration Testing
- Test on actual CS2 server
- Multi-server scenarios
- Database migration testing
---
## Related Documentation
- **[API Overview](api/overview)** - Public API details
- **[Module Development](module/getting-started)** - Create modules
- **[GitHub Source](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Browse code

View File

@@ -0,0 +1,379 @@
---
sidebar_position: 1
---
# Developer Introduction
Welcome to the CS2-SimpleAdmin developer documentation!
## Overview
This section contains technical documentation for developers who want to:
- Create modules using the CS2-SimpleAdmin API
- Contribute to the core plugin
- Integrate with CS2-SimpleAdmin from other plugins
- Understand the plugin architecture
---
## API Documentation
The CS2-SimpleAdmin API provides a rich set of features for module developers:
### Core Features
- **[Commands](api/commands)** - Register and manage commands
- **[Menus](api/menus)** - Create admin menus with player selection
- **[Penalties](api/penalties)** - Issue bans, mutes, gags, warnings
- **[Events](api/events)** - Subscribe to plugin events
- **[Utilities](api/utilities)** - Helper functions and player management
---
## Quick Links
### For Module Developers
- **[Module Development Guide](module/getting-started)** - Start creating modules
- **[Best Practices](module/best-practices)** - Write better code
- **[Examples](module/examples)** - Code examples and patterns
### For Core Contributors
- **[Architecture](architecture)** - Plugin structure and design
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
---
## Getting Started
### Prerequisites
- C# knowledge (intermediate level)
- .NET 8.0 SDK
- CounterStrikeSharp understanding
- CS2 dedicated server for testing
### Development Environment
**Recommended:**
- Visual Studio 2022 (Community or higher)
- VS Code with C# extension
- Git for version control
---
## CS2-SimpleAdminApi Interface
The main API interface provides all functionality:
```csharp
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
// Get the API
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
new("simpleadmin:api");
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found!");
return;
}
// Use the API
_api.RegisterCommand("css_mycommand", "Description", OnMyCommand);
}
```
---
## API Categories
### Command Management
Register custom commands that integrate with CS2-SimpleAdmin:
```csharp
_api.RegisterCommand("css_mycommand", "Description", callback);
_api.UnRegisterCommand("css_mycommand");
_api.GetTarget(command); // Parse player targets
```
**[Learn more →](api/commands)**
---
### Menu System
Create interactive menus with automatic back button handling:
```csharp
// Register category
_api.RegisterMenuCategory("mycategory", "My Category", "@css/generic");
// Register menu
_api.RegisterMenu("mycategory", "mymenu", "My Menu", CreateMenu, "@css/generic");
// Create menu with players
_api.CreateMenuWithPlayers(context, admin, filter, onSelect);
```
**[Learn more →](api/menus)**
---
### Penalty System
Issue and manage player penalties:
```csharp
// Ban player
_api.IssuePenalty(player, admin, PenaltyType.Ban, "Reason", 1440);
// Offline ban
_api.IssuePenalty(steamId, admin, PenaltyType.Ban, "Reason", 0);
// Check penalties
var status = _api.GetPlayerMuteStatus(player);
```
**[Learn more →](api/penalties)**
---
### Event System
React to plugin events:
```csharp
_api.OnSimpleAdminReady += () => { /* Plugin ready */ };
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, serverId) =>
{
// Player received penalty
};
```
**[Learn more →](api/events)**
---
### Utility Functions
Helper functions for common tasks:
```csharp
// Get player info
var playerInfo = _api.GetPlayerInfo(player);
// Get valid players
var players = _api.GetValidPlayers();
// Check admin status
if (_api.IsAdminSilent(admin)) { /* ... */ }
// Show admin activity
_api.ShowAdminActivity("message_key", callerName, false, args);
```
**[Learn more →](api/utilities)**
---
## Code Examples
### Simple Command
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var target in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
// Do something with target
}
_api.LogCommand(caller, command);
}
```
### Simple Menu
```csharp
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) => DoAction(admin, target)
);
}
```
---
## Best Practices
### Error Handling
```csharp
if (_api == null)
{
Logger.LogError("API not available!");
return;
}
if (!player.IsValid || !player.PawnIsAlive)
{
return;
}
```
### Resource Cleanup
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister commands
_api.UnRegisterCommand("css_mycommand");
// Unregister menus
_api.UnregisterMenu("mycategory", "mymenu");
// Unsubscribe events
_api.OnSimpleAdminReady -= OnReady;
}
```
### Translations
```csharp
// Use per-player language support
_api.ShowAdminActivityLocalized(
Localizer,
"translation_key",
caller?.PlayerName,
false,
args
);
```
---
## Reference Implementation
The **Fun Commands Module** serves as a complete reference implementation demonstrating all API features:
- Command registration from config
- Menu creation with context
- Per-player translations
- Proper cleanup
- Code organization
**[View Source Code](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)**
---
## Architecture Overview
CS2-SimpleAdmin follows a layered architecture:
**Layers:**
1. **CounterStrikeSharp Integration** - Game event handling
2. **Manager Layer** - Business logic (Bans, Mutes, Permissions)
3. **Database Layer** - MySQL/SQLite with migrations
4. **Menu System** - MenuManager with factory pattern
5. **Command System** - Dynamic registration
6. **Public API** - ICS2_SimpleAdminApi interface
**[Learn more →](architecture)**
---
## Contributing
### Ways to Contribute
1. **Report Bugs** - [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
2. **Suggest Features** - [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
3. **Submit Pull Requests** - Code contributions
4. **Create Modules** - Extend functionality
5. **Improve Documentation** - Help others learn
### Development Workflow
1. Fork the repository
2. Create feature branch
3. Make changes
4. Test thoroughly
5. Submit pull request
---
## Resources
### Documentation
- **[API Reference](api/overview)** - Complete API documentation
- **[Module Development](module/getting-started)** - Create modules
- **[Architecture](architecture)** - Plugin design
### External Resources
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS framework
- **[CS2 Docs](https://developer.valvesoftware.com/wiki/Counter-Strike_2)** - Game documentation
### Community
- **[GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
- **[Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)** - Bug reports
- **[Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)** - Questions and ideas
---
## Support
### Getting Help
1. **Check Documentation** - Most questions answered here
2. **Search Issues** - Someone may have had same problem
3. **Ask in Discussions** - Community help
4. **Create Issue** - For bugs or feature requests
### Reporting Bugs
Include:
- CS2-SimpleAdmin version
- CounterStrikeSharp version
- Error messages
- Steps to reproduce
- Expected vs actual behavior
---
## Next Steps
### For New Developers
1. **[Read API Overview](api/overview)** - Understand available features
2. **[Study Examples](module/examples)** - Learn from code
3. **[Create First Module](module/getting-started)** - Get hands-on
### For Advanced Developers
1. **[Read Architecture](architecture)** - Deep dive into structure
2. **[Review Source Code](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Understand implementation
3. **[Contribute](https://github.com/daffyyyy/CS2-SimpleAdmin/pulls)** - Help improve the plugin

View File

@@ -0,0 +1,540 @@
---
sidebar_position: 2
---
# Best Practices
Guidelines for writing high-quality CS2-SimpleAdmin modules.
## Code Organization
### Use Partial Classes
Split your code into logical files:
```
MyModule/
├── MyModule.cs # Main class, initialization
├── Commands.cs # Command handlers
├── Menus.cs # Menu creation
├── Actions.cs # Core logic
└── Config.cs # Configuration
```
```csharp
// MyModule.cs
public partial class MyModule : BasePlugin, IPluginConfig<Config>
{
// Initialization
}
// Commands.cs
public partial class MyModule
{
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Command logic
}
}
// Menus.cs
public partial class MyModule
{
private object CreateMyMenu(CCSPlayerController player, MenuContext context)
{
// Menu logic
}
}
```
**Benefits:**
- ✅ Easy to navigate
- ✅ Logical separation
- ✅ Better maintainability
---
## Configuration
### Use Command Lists
Allow users to customize aliases:
```csharp
public class Config : IBasePluginConfig
{
// ✅ Good - List allows multiple aliases
public List<string> MyCommands { get; set; } = ["css_mycommand"];
// ❌ Bad - Single string
public string MyCommand { get; set; } = "css_mycommand";
}
```
**Usage:**
```csharp
foreach (var cmd in Config.MyCommands)
{
_api!.RegisterCommand(cmd, "Description", OnMyCommand);
}
```
### Provide Sensible Defaults
```csharp
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
// Good defaults
public bool EnableFeature { get; set; } = true;
public int MaxValue { get; set; } = 100;
public List<string> Commands { get; set; } = ["css_default"];
}
```
---
## API Usage
### Always Check for Null
```csharp
// ✅ Good
if (_api == null)
{
Logger.LogError("API not available!");
return;
}
_api.RegisterCommand(...);
// ❌ Bad
_api!.RegisterCommand(...); // Can crash if null
```
### Use OnSimpleAdminReady Pattern
```csharp
// ✅ Good - Handles both normal load and hot reload
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Also call directly
// ❌ Bad - Only works on normal load
_api.OnSimpleAdminReady += RegisterMenus;
```
### Always Clean Up
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister ALL commands
foreach (var cmd in Config.MyCommands)
{
_api.UnRegisterCommand(cmd);
}
// Unregister ALL menus
_api.UnregisterMenu("category", "menu");
// Unsubscribe ALL events
_api.OnSimpleAdminReady -= RegisterMenus;
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
}
```
---
## Player Validation
### Validate Before Acting
```csharp
// ✅ Good - Multiple checks
if (!player.IsValid)
{
Logger.LogWarning("Player is invalid!");
return;
}
if (!player.PawnIsAlive)
{
caller?.PrintToChat("Target must be alive!");
return;
}
if (admin != null && !admin.CanTarget(player))
{
admin.PrintToChat("Cannot target this player!");
return;
}
// Safe to proceed
DoAction(player);
```
### Check State Changes
```csharp
// ✅ Good - Validate in callback
_api.AddMenuOption(menu, "Action", _ =>
{
// Validate again - player state may have changed
if (!target.IsValid || !target.PawnIsAlive)
return;
DoAction(target);
});
// ❌ Bad - No validation in callback
_api.AddMenuOption(menu, "Action", _ =>
{
DoAction(target); // Might crash!
});
```
---
## Translations
### Use MenuContext for Menus
```csharp
// ✅ Good - No duplication
_api.RegisterMenu("cat", "id", "Title", CreateMenu, "@css/generic");
private object CreateMenu(CCSPlayerController admin, MenuContext context)
{
return _api.CreateMenuWithPlayers(context, admin, filter, action);
}
// ❌ Bad - Duplicates title and category
_api.RegisterMenu("cat", "id", "Title", CreateMenuOld, "@css/generic");
private object CreateMenuOld(CCSPlayerController admin)
{
return _api.CreateMenuWithPlayers("Title", "cat", admin, filter, action);
}
```
### Use Per-Player Translations
```csharp
// ✅ Good - Each player sees their language
if (Localizer != null)
{
_api.ShowAdminActivityLocalized(
Localizer,
"translation_key",
admin?.PlayerName,
false,
args
);
}
// ❌ Bad - Single language for all
Server.PrintToChatAll($"{admin?.PlayerName} did something");
```
### Provide English Fallbacks
```csharp
// ✅ Good - Fallback if translation missing
_api.RegisterMenuCategory(
"mycat",
Localizer?["category_name"] ?? "Default Category Name",
"@css/generic"
);
// ❌ Bad - No fallback
_api.RegisterMenuCategory(
"mycat",
Localizer["category_name"], // Crashes if no translation!
"@css/generic"
);
```
---
## Performance
### Cache Expensive Operations
```csharp
// ✅ Good - Cache on first access
private static Dictionary<int, string>? _itemCache;
private static Dictionary<int, string> GetItemCache()
{
if (_itemCache != null) return _itemCache;
// Build cache once
_itemCache = new Dictionary<int, string>();
// ... populate
return _itemCache;
}
// ❌ Bad - Rebuild every time
private Dictionary<int, string> GetItems()
{
var items = new Dictionary<int, string>();
// ... expensive operation
return items;
}
```
### Efficient LINQ Queries
```csharp
// ✅ Good - Single query
var players = _api.GetValidPlayers()
.Where(p => p.IsValid && !p.IsBot && p.PawnIsAlive)
.ToList();
// ❌ Bad - Multiple iterations
var players = _api.GetValidPlayers();
players = players.Where(p => p.IsValid).ToList();
players = players.Where(p => !p.IsBot).ToList();
players = players.Where(p => p.PawnIsAlive).ToList();
```
---
## Error Handling
### Log Errors
```csharp
// ✅ Good - Detailed logging
try
{
DoAction();
}
catch (Exception ex)
{
Logger.LogError($"Failed to perform action: {ex.Message}");
Logger.LogError($"Stack trace: {ex.StackTrace}");
}
// ❌ Bad - Silent failure
try
{
DoAction();
}
catch
{
// Ignore
}
```
### Graceful Degradation
```csharp
// ✅ Good - Continue with reduced functionality
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("SimpleAdmin API not found - limited functionality!");
// Module still loads, just without SimpleAdmin integration
return;
}
// ❌ Bad - Crash the entire module
_api = _pluginCapability.Get() ?? throw new Exception("No API!");
```
---
## Security
### Validate Admin Permissions
```csharp
// ✅ Good - Check permissions
[RequiresPermissions("@css/ban")]
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
// Already validated by attribute
}
// ❌ Bad - No permission check
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
// Anyone can use this!
}
```
### Check Immunity
```csharp
// ✅ Good - Check immunity
if (admin != null && !admin.CanTarget(target))
{
admin.PrintToChat($"Cannot target {target.PlayerName}!");
return;
}
// ❌ Bad - Ignore immunity
DoAction(target); // Can target higher immunity!
```
### Sanitize Input
```csharp
// ✅ Good - Validate and sanitize
private void OnSetValueCommand(CCSPlayerController? caller, CommandInfo command)
{
if (!int.TryParse(command.GetArg(1), out int value))
{
caller?.PrintToChat("Invalid number!");
return;
}
if (value < 0 || value > 1000)
{
caller?.PrintToChat("Value must be between 0 and 1000!");
return;
}
SetValue(value);
}
// ❌ Bad - No validation
private void OnSetValueCommand(CCSPlayerController? caller, CommandInfo command)
{
var value = int.Parse(command.GetArg(1)); // Can crash!
SetValue(value); // No range check!
}
```
---
## Documentation
### Comment Complex Logic
```csharp
// ✅ Good - Explain why, not what
// We need to check immunity twice because player state can change
// between menu creation and action execution
if (!admin.CanTarget(player))
{
return;
}
// ❌ Bad - States the obvious
// Check if admin can target player
if (!admin.CanTarget(player))
{
return;
}
```
### XML Documentation
```csharp
/// <summary>
/// Toggles god mode for the specified player.
/// </summary>
/// <param name="admin">Admin performing the action (null for console)</param>
/// <param name="target">Player to toggle god mode for</param>
/// <returns>True if god mode is now enabled, false otherwise</returns>
public bool ToggleGodMode(CCSPlayerController? admin, CCSPlayerController target)
{
// Implementation
}
```
---
## Testing
### Test Edge Cases
```csharp
// Test with:
// - Invalid players
// - Disconnected players
// - Players who changed teams
// - Null admins (console)
// - Silent admins
// - Players with higher immunity
```
### Test Hot Reload
```bash
# Server console
css_plugins reload YourModule
```
Make sure everything works after reload!
---
## Common Mistakes
### ❌ Forgetting to Unsubscribe
```csharp
public override void Unload(bool hotReload)
{
// Missing unsubscribe = memory leak!
// _api.OnSimpleAdminReady -= RegisterMenus; ← FORGOT THIS
}
```
### ❌ Not Checking API Availability
```csharp
// Crashes if SimpleAdmin not loaded!
_api.RegisterCommand(...); // ← No null check
```
### ❌ Hardcoding Strings
```csharp
// Bad - not translatable
player.PrintToChat("You have been banned!");
// Good - uses translations
var message = Localizer?["ban_message"] ?? "You have been banned!";
player.PrintToChat(message);
```
### ❌ Blocking Game Thread
```csharp
// Bad - blocks game thread
Thread.Sleep(5000);
// Good - use CounterStrikeSharp timers
AddTimer(5.0f, () => DoAction());
```
---
## Reference Implementation
Study the **Fun Commands Module** for best practices:
**[View Source](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)**
Shows:
- ✅ Proper code organization
- ✅ Configuration best practices
- ✅ Menu creation with context
- ✅ Per-player translations
- ✅ Proper cleanup
- ✅ Error handling
---
## Next Steps
- **[Examples](examples)** - More code examples
- **[API Reference](../api/overview)** - Full API documentation
- **[GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Browse source code

View File

@@ -0,0 +1,552 @@
---
sidebar_position: 3
---
# Code Examples
Practical examples for common module development scenarios.
## Complete Mini Module
A fully working minimal module:
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CS2_SimpleAdminApi;
namespace HelloModule;
public class HelloModule : BasePlugin, IPluginConfig<Config>
{
public override string ModuleName => "Hello Module";
public override string ModuleVersion => "1.0.0";
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");
public Config Config { get; set; } = new();
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found!");
return;
}
// Register command
foreach (var cmd in Config.HelloCommands)
{
_api.RegisterCommand(cmd, "Say hello to a player", OnHelloCommand);
}
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnHelloCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
player.PrintToChat($"Hello {player.PlayerName}!");
caller?.PrintToChat($"Said hello to {player.PlayerName}");
}
_api.LogCommand(caller, command);
}
public void OnConfigParsed(Config config) => Config = config;
public override void Unload(bool hotReload)
{
if (_api == null) return;
foreach (var cmd in Config.HelloCommands)
{
_api.UnRegisterCommand(cmd);
}
}
}
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
public List<string> HelloCommands { get; set; } = ["css_hello"];
}
```
---
## Command Examples
### Simple Target Command
```csharp
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/slay")]
private void OnSlayCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
if (player.PawnIsAlive)
{
player.PlayerPawn?.Value?.CommitSuicide(false, true);
caller?.PrintToChat($"Slayed {player.PlayerName}");
}
}
_api.LogCommand(caller, command);
}
```
### Command with Value Parameter
```csharp
[CommandHelper(2, "<#userid or name> <value>")]
[RequiresPermissions("@css/slay")]
private void OnSetHpCommand(CCSPlayerController? caller, CommandInfo command)
{
// Parse HP value
if (!int.TryParse(command.GetArg(2), out int hp) || hp < 1 || hp > 999)
{
caller?.PrintToChat("Invalid HP! Use 1-999");
return;
}
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && p.PawnIsAlive && caller!.CanTarget(p)))
{
player.PlayerPawn?.Value?.SetHealth(hp);
caller?.PrintToChat($"Set {player.PlayerName} HP to {hp}");
}
_api.LogCommand(caller, $"css_sethp {hp}");
}
```
### Command with Penalty
```csharp
[CommandHelper(1, "<#userid or name> [duration] [reason]")]
[RequiresPermissions("@css/ban")]
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
// Parse duration (default: 60 minutes)
int duration = 60;
if (command.ArgCount > 2)
{
int.TryParse(command.GetArg(2), out duration);
}
// Get reason (default: "Banned")
string reason = command.ArgCount > 3
? string.Join(" ", command.ArgString.Split(' ').Skip(2))
: "Banned";
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
_api.IssuePenalty(player, caller, PenaltyType.Ban, reason, duration);
caller?.PrintToChat($"Banned {player.PlayerName} for {duration} minutes");
}
_api.LogCommand(caller, command);
}
```
---
## Menu Examples
### Simple Player Selection Menu
```csharp
private void RegisterMenus()
{
_api!.RegisterMenuCategory("actions", "Player Actions", "@css/generic");
_api.RegisterMenu(
"actions",
"kick",
"Kick Player",
CreateKickMenu,
"@css/kick"
);
}
private object CreateKickMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) =>
{
Server.ExecuteCommand($"css_kick #{target.UserId} Kicked via menu");
admin.PrintToChat($"Kicked {target.PlayerName}");
}
);
}
```
### Nested Menu (Player → Action)
```csharp
private object CreatePlayerActionsMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
foreach (var player in _api.GetValidPlayers().Where(p => admin.CanTarget(p)))
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateActionSelectMenu(admin, player);
});
}
return menu;
}
private object CreateActionSelectMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Actions: {target.PlayerName}", "actions", admin);
_api.AddMenuOption(menu, "Slay", _ =>
{
if (target.IsValid && target.PawnIsAlive)
{
target.PlayerPawn?.Value?.CommitSuicide(false, true);
admin.PrintToChat($"Slayed {target.PlayerName}");
}
});
_api.AddMenuOption(menu, "Kick", _ =>
{
if (target.IsValid)
{
Server.ExecuteCommand($"css_kick #{target.UserId}");
}
});
_api.AddMenuOption(menu, "Ban", _ =>
{
if (target.IsValid)
{
_api.IssuePenalty(target, admin, PenaltyType.Ban, "Banned via menu", 1440);
}
});
return menu;
}
```
### Menu with Value Selection
```csharp
private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var players = _api.GetValidPlayers()
.Where(p => p.PawnIsAlive && admin.CanTarget(p));
foreach (var player in players)
{
_api.AddSubMenu(menu, player.PlayerName, admin =>
{
return CreateHpValueMenu(admin, player);
});
}
return menu;
}
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Set HP: {target.PlayerName}", "actions", admin);
var hpValues = new[] { 1, 10, 50, 100, 200, 500 };
foreach (var hp in hpValues)
{
_api.AddMenuOption(menu, $"{hp} HP", _ =>
{
if (target.IsValid && target.PawnIsAlive)
{
target.PlayerPawn?.Value?.SetHealth(hp);
admin.PrintToChat($"Set {target.PlayerName} HP to {hp}");
}
});
}
return menu;
}
```
---
## Event Examples
### React to Bans
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
if (_api == null) return;
_api.OnPlayerPenaltied += OnPlayerPenaltied;
}
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
if (type != PenaltyType.Ban) return;
var adminName = admin?.PlayerName ?? "Console";
Logger.LogInformation($"Ban: {adminName} -> {player.PlayerName} ({duration}m): {reason}");
// Log to file
File.AppendAllText("bans.log",
$"[{DateTime.Now}] {player.PlayerName} banned by {adminName} for {duration}m: {reason}\n");
}
```
### Warning Escalation
```csharp
private void OnPlayerPenaltied(
PlayerInfo player,
PlayerInfo? admin,
PenaltyType type,
string reason,
int duration,
int? penaltyId,
int? serverId)
{
if (type != PenaltyType.Warn) return;
Logger.LogInformation($"{player.PlayerName} has {player.Warnings} warnings");
// Auto-ban at 3 warnings
if (player.Warnings >= 3)
{
var controller = Utilities.GetPlayers()
.FirstOrDefault(p => p.SteamID == player.SteamId);
if (controller != null)
{
_api!.IssuePenalty(
controller,
null,
PenaltyType.Ban,
"Automatic: 3 warnings",
1440 // 1 day
);
}
}
}
```
---
## Translation Examples
### Module with Translations
**lang/en.json:**
```json
{
"category_name": "My Module",
"menu_name": "My Action",
"action_message": "{lightred}{0}{default} performed action on {lightred}{1}{default}!",
"error_invalid_player": "{red}Error:{default} Invalid player!",
"success": "{green}Success!{default} Action completed."
}
```
**Code:**
```csharp
private void PerformAction(CCSPlayerController? admin, CCSPlayerController target)
{
// Perform action
DoSomething(target);
// Show activity with translation
if (admin == null || !_api!.IsAdminSilent(admin))
{
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer,
"action_message",
admin?.PlayerName,
false,
admin?.PlayerName ?? "Console",
target.PlayerName
);
}
}
// Send success message
admin?.PrintToChat(Localizer?["success"] ?? "Success!");
}
```
---
## Utility Examples
### Get Players by Team
```csharp
private List<CCSPlayerController> GetTeamPlayers(CsTeam team)
{
return _api!.GetValidPlayers()
.Where(p => p.Team == team)
.ToList();
}
// Usage
var ctPlayers = GetTeamPlayers(CsTeam.CounterTerrorist);
var tPlayers = GetTeamPlayers(CsTeam.Terrorist);
```
### Get Alive Players
```csharp
private List<CCSPlayerController> GetAlivePlayers()
{
return _api!.GetValidPlayers()
.Where(p => p.PawnIsAlive)
.ToList();
}
```
### Notify Admins
```csharp
private void NotifyAdmins(string message, string permission = "@css/generic")
{
var admins = _api!.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, permission));
foreach (var admin in admins)
{
admin.PrintToChat(message);
}
}
// Usage
NotifyAdmins("⚠ Important admin message", "@css/root");
```
---
## Timer Examples
### Delayed Action
```csharp
private void DelayedAction(CCSPlayerController player, float delay)
{
AddTimer(delay, () =>
{
if (player.IsValid && player.PawnIsAlive)
{
DoAction(player);
}
});
}
```
### Repeating Timer
```csharp
private void StartRepeatingAction()
{
AddTimer(1.0f, () =>
{
foreach (var player in _api!.GetValidPlayers())
{
if (player.PawnIsAlive)
{
UpdatePlayer(player);
}
}
}, TimerFlags.REPEAT);
}
```
---
## Configuration Examples
### Multiple Feature Toggles
```csharp
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
[JsonPropertyName("EnableFeature1")]
public bool EnableFeature1 { get; set; } = true;
[JsonPropertyName("EnableFeature2")]
public bool EnableFeature2 { get; set; } = false;
[JsonPropertyName("Feature1Commands")]
public List<string> Feature1Commands { get; set; } = ["css_feature1"];
[JsonPropertyName("Feature2Commands")]
public List<string> Feature2Commands { get; set; } = ["css_feature2"];
[JsonPropertyName("MaxValue")]
public int MaxValue { get; set; } = 100;
}
// Usage
private void RegisterCommands()
{
if (Config.EnableFeature1)
{
foreach (var cmd in Config.Feature1Commands)
{
_api!.RegisterCommand(cmd, "Feature 1", OnFeature1Command);
}
}
if (Config.EnableFeature2)
{
foreach (var cmd in Config.Feature2Commands)
{
_api!.RegisterCommand(cmd, "Feature 2", OnFeature2Command);
}
}
}
```
---
## Next Steps
- **[Best Practices](best-practices)** - Write better code
- **[Getting Started](getting-started)** - Create your first module
- **[API Reference](../api/overview)** - Full API documentation
- **[Fun Commands Source](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Complete reference implementation

View File

@@ -0,0 +1,282 @@
---
sidebar_position: 1
---
# Getting Started with Module Development
Step-by-step guide to creating your first CS2-SimpleAdmin module.
## Prerequisites
Before you begin:
- C# knowledge (intermediate level)
- .NET 8.0 SDK installed
- Visual Studio 2022 or VS Code
- Basic understanding of CounterStrikeSharp
- CS2 dedicated server for testing
---
## Step 1: Create Project
### Using .NET CLI
```bash
dotnet new classlib -n MyModule -f net8.0
cd MyModule
```
### Using Visual Studio
1. File → New → Project
2. Select "Class Library (.NET 8.0)"
3. Name: `MyModule`
4. Click Create
---
## Step 2: Add References
Edit `MyModule.csproj`:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="CounterStrikeSharp.API">
<HintPath>path/to/CounterStrikeSharp.API.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="CS2-SimpleAdminApi">
<HintPath>path/to/CS2-SimpleAdminApi.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
```
---
## Step 3: Create Main Plugin Class
Create `MyModule.cs`:
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CS2_SimpleAdminApi;
namespace MyModule;
public class MyModule : BasePlugin, IPluginConfig<Config>
{
public override string ModuleName => "My Module";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "Your Name";
public override string ModuleDescription => "My awesome module";
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
new("simpleadmin:api");
public Config Config { get; set; } = new();
public override void OnAllPluginsLoaded(bool hotReload)
{
// Get SimpleAdmin API
_api = _pluginCapability.Get();
if (_api == null)
{
Logger.LogError("CS2-SimpleAdmin API not found!");
return;
}
Logger.LogInformation("MyModule loaded successfully!");
// Register features
RegisterCommands();
// Register menus when ready
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Also call for hot reload
}
private void RegisterCommands()
{
if (_api == null) return;
foreach (var cmd in Config.MyCommands)
{
_api.RegisterCommand(cmd, "My command description", OnMyCommand);
}
}
private void RegisterMenus()
{
if (_api == null) return;
_api.RegisterMenuCategory("mymodule", "My Module", "@css/generic");
_api.RegisterMenu("mymodule", "mymenu", "My Menu", CreateMyMenu, "@css/generic");
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = _api!.GetTarget(command);
if (targets == null) return;
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
{
player.PrintToChat($"Hello from MyModule!");
}
_api.LogCommand(caller, command);
}
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
{
return _api!.CreateMenuWithPlayers(
context,
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) =>
{
target.PrintToChat("You were selected!");
}
);
}
public void OnConfigParsed(Config config)
{
Config = config;
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
// Unregister commands
foreach (var cmd in Config.MyCommands)
{
_api.UnRegisterCommand(cmd);
}
// Unregister menus
_api.UnregisterMenu("mymodule", "mymenu");
// Unsubscribe events
_api.OnSimpleAdminReady -= RegisterMenus;
}
}
```
---
## Step 4: Create Configuration
Create `Config.cs`:
```csharp
using CounterStrikeSharp.API.Core;
using System.Text.Json.Serialization;
public class Config : IBasePluginConfig
{
[JsonPropertyName("Version")]
public int Version { get; set; } = 1;
[JsonPropertyName("MyCommands")]
public List<string> MyCommands { get; set; } = ["css_mycommand"];
[JsonPropertyName("EnableFeature")]
public bool EnableFeature { get; set; } = true;
}
```
---
## Step 5: Build and Deploy
### Build
```bash
dotnet build -c Release
```
### Deploy
Copy files to server:
```
game/csgo/addons/counterstrikesharp/plugins/MyModule/
└── MyModule.dll
```
### Restart Server
```bash
# Server console
css_plugins reload
```
---
## Step 6: Test
1. Join your server
2. Open admin menu: `css_admin`
3. Look for "My Module" category
4. Test command: `css_mycommand @me`
---
## Next Steps
- **[Best Practices](best-practices)** - Write better code
- **[Examples](examples)** - More code examples
- **[API Reference](../api/overview)** - Full API documentation
- **[Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Reference implementation
---
## Common Issues
### API Not Found
**Error:** `CS2-SimpleAdmin API not found!`
**Solution:**
- Ensure CS2-SimpleAdmin is installed
- Check that CS2-SimpleAdminApi.dll is in shared folder
- Verify CS2-SimpleAdmin loads before your module
### Commands Not Working
**Check:**
- Command registered in `RegisterCommands()`
- Permission is correct
- Player has required permission
### Menu Not Showing
**Check:**
- `OnSimpleAdminReady` event subscribed
- Menu registered in category
- Permission is correct
- SimpleAdmin loaded successfully
---
## Resources
- **[Module Development Guide](../../modules/development)** - Detailed guide
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS framework