mirror of
https://github.com/daffyyyy/CS2-SimpleAdmin.git
synced 2026-02-22 11:42:26 +00:00
Add CS2-SimpleAdmin documentation site
Introduces a new documentation site for CS2-SimpleAdmin using Docusaurus, including developer API references, tutorials, user guides, and module documentation. Removes the CleanModule example and updates FunCommands and ExampleModule. Also updates main plugin and API files to support new documentation and module structure.
This commit is contained in:
465
CS2-SimpleAdmin-docs/docs/developer/api/commands.md
Normal file
465
CS2-SimpleAdmin-docs/docs/developer/api/commands.md
Normal 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
|
||||
642
CS2-SimpleAdmin-docs/docs/developer/api/events.md
Normal file
642
CS2-SimpleAdmin-docs/docs/developer/api/events.md
Normal 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
|
||||
706
CS2-SimpleAdmin-docs/docs/developer/api/menus.md
Normal file
706
CS2-SimpleAdmin-docs/docs/developer/api/menus.md
Normal 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
|
||||
621
CS2-SimpleAdmin-docs/docs/developer/api/overview.md
Normal file
621
CS2-SimpleAdmin-docs/docs/developer/api/overview.md
Normal 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
|
||||
610
CS2-SimpleAdmin-docs/docs/developer/api/penalties.md
Normal file
610
CS2-SimpleAdmin-docs/docs/developer/api/penalties.md
Normal 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
|
||||
585
CS2-SimpleAdmin-docs/docs/developer/api/utilities.md
Normal file
585
CS2-SimpleAdmin-docs/docs/developer/api/utilities.md
Normal 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
|
||||
695
CS2-SimpleAdmin-docs/docs/developer/architecture.md
Normal file
695
CS2-SimpleAdmin-docs/docs/developer/architecture.md
Normal 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
|
||||
379
CS2-SimpleAdmin-docs/docs/developer/intro.md
Normal file
379
CS2-SimpleAdmin-docs/docs/developer/intro.md
Normal 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
|
||||
540
CS2-SimpleAdmin-docs/docs/developer/module/best-practices.md
Normal file
540
CS2-SimpleAdmin-docs/docs/developer/module/best-practices.md
Normal 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
|
||||
552
CS2-SimpleAdmin-docs/docs/developer/module/examples.md
Normal file
552
CS2-SimpleAdmin-docs/docs/developer/module/examples.md
Normal 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
|
||||
282
CS2-SimpleAdmin-docs/docs/developer/module/getting-started.md
Normal file
282
CS2-SimpleAdmin-docs/docs/developer/module/getting-started.md
Normal 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
|
||||
Reference in New Issue
Block a user