Add CS2-SimpleAdmin documentation site

Introduces a new documentation site for CS2-SimpleAdmin using Docusaurus, including developer API references, tutorials, user guides, and module documentation. Removes the CleanModule example and updates FunCommands and ExampleModule. Also updates main plugin and API files to support new documentation and module structure.
This commit is contained in:
Dawid Bepierszcz
2025-10-20 01:27:01 +02:00
parent 21a5de6b3d
commit b0d8696756
74 changed files with 32732 additions and 279 deletions

View File

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

View File

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

View File

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