---
sidebar_position: 3
---
# Module Development
Learn how to create your own CS2-SimpleAdmin modules.
## Introduction
Creating modules for CS2-SimpleAdmin allows you to extend the plugin's functionality while keeping your code separate and maintainable.
:::tip Reference Implementation
The **[Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** serves as a complete reference implementation. Study its code to learn best practices!
:::
---
## Prerequisites
### Knowledge Required
- C# programming (intermediate level)
- .NET 8.0
- CounterStrikeSharp basics
- Understanding of CS2-SimpleAdmin structure
### Tools Needed
- Visual Studio 2022 or VS Code
- .NET 8.0 SDK
- CS2 server for testing
---
## Quick Start
### 1. Create Project
```bash
dotnet new classlib -n YourModuleName -f net8.0
cd YourModuleName
```
### 2. Add References
Edit your `.csproj` file:
```xml
net8.0
enable
enable
path/to/CounterStrikeSharp.API.dll
false
path/to/CS2-SimpleAdminApi.dll
false
```
### 3. Create Main Plugin Class
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;
namespace YourModuleName;
public class YourModule : BasePlugin, IPluginConfig
{
public override string ModuleName => "Your Module Name";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "Your Name";
public override string ModuleDescription => "Description";
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability _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;
}
// Register your commands and menus
RegisterCommands();
_api.OnSimpleAdminReady += RegisterMenus;
RegisterMenus(); // Fallback for hot reload
}
public void OnConfigParsed(Config config)
{
Config = config;
}
private void RegisterCommands()
{
// Register commands here
}
private void RegisterMenus()
{
// Register menus here
}
}
```
---
## Module Structure
### Recommended File Organization
```
YourModuleName/
├── YourModule.cs # Main plugin class
├── Config.cs # Configuration
├── Commands.cs # Command handlers (partial class)
├── Menus.cs # Menu creation (partial class)
├── Actions.cs # Core logic (partial class)
├── lang/ # Translations
│ ├── en.json
│ ├── pl.json
│ └── ...
└── YourModuleName.csproj
```
### Using Partial Classes
Split your code for better organization:
```csharp
// YourModule.cs
public partial class YourModule : BasePlugin, IPluginConfig
{
// Plugin initialization
}
// Commands.cs
public partial class YourModule
{
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Command logic
}
}
// Menus.cs
public partial class YourModule
{
private object CreateMyMenu(CCSPlayerController player, MenuContext context)
{
// Menu creation
}
}
```
---
## Configuration
### Create Config Class
```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 MyCommands { get; set; } = ["css_mycommand"];
[JsonPropertyName("EnableFeature")]
public bool EnableFeature { get; set; } = true;
[JsonPropertyName("MaxValue")]
public int MaxValue { get; set; } = 100;
}
```
### Config Best Practices
1. **Use command lists** - Allow users to add aliases or disable features
2. **Provide defaults** - Sensible default values
3. **Version your config** - Track config changes
4. **Document settings** - Clear property names
---
## Registering Commands
### Basic Command Registration
```csharp
private void RegisterCommands()
{
if (_api == null) return;
foreach (var cmd in Config.MyCommands)
{
_api.RegisterCommand(cmd, "Command description", OnMyCommand);
}
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Get target players
var targets = _api!.GetTarget(command);
if (targets == null) return;
// Filter for valid players
var players = targets.Players
.Where(p => p.IsValid && !p.IsBot)
.ToList();
// Process each player
foreach (var player in players)
{
if (caller!.CanTarget(player))
{
DoSomething(caller, player);
}
}
// Log the command
_api.LogCommand(caller, command);
}
```
### Command Cleanup
Always unregister commands when unloading:
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
foreach (var cmd in Config.MyCommands)
{
_api.UnRegisterCommand(cmd);
}
}
```
---
## Creating Menus
### Register Menu Category
```csharp
private void RegisterMenus()
{
if (_api == null || _menusRegistered) return;
// Register category
_api.RegisterMenuCategory(
"mycategory",
Localizer?["category_name"] ?? "My Category",
"@css/generic"
);
// Register menu
_api.RegisterMenu(
"mycategory",
"mymenu",
Localizer?["menu_name"] ?? "My Menu",
CreateMyMenu,
"@css/generic",
"css_mycommand" // For permission override
);
_menusRegistered = true;
}
```
### Menu with Player Selection (NEW API)
```csharp
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
{
// Context contains: CategoryId, MenuId, MenuTitle, Permission, CommandName
// No need to repeat "mycategory" and "My Menu" here!
return _api!.CreateMenuWithPlayers(
context, // ← Automatically uses menu title and category
admin,
player => player.IsValid && admin.CanTarget(player),
(admin, target) => DoSomethingToPlayer(admin, target)
);
}
```
### Menu with Custom Options
```csharp
private object CreateValueSelectionMenu(CCSPlayerController admin, MenuContext context)
{
var menu = _api!.CreateMenuWithBack(context, admin);
var values = new[] { 10, 25, 50, 100, 200 };
foreach (var value in values)
{
_api.AddMenuOption(menu, $"{value} points", player =>
{
GivePoints(player, value);
});
}
return menu;
}
```
### Nested Menus
```csharp
private object CreatePlayerSelectionMenu(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 CreateValueMenu(admin, player);
});
}
return menu;
}
private object CreateValueMenu(CCSPlayerController admin, CCSPlayerController target)
{
var menu = _api!.CreateMenuWithBack($"Select value for {target.PlayerName}", "mycategory", admin);
// Add options...
return menu;
}
```
---
## Translations
### Create Translation Files
Create `lang/en.json`:
```json
{
"command_success": "{green}Success! {default}Action performed on {lightred}{0}",
"command_failed": "{red}Failed! {default}Could not perform action",
"menu_title": "My Custom Menu"
}
```
### Use Translations in Code
```csharp
// In commands
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
{
// Using module's own localizer for per-player language
if (Localizer != null)
{
_api!.ShowAdminActivityLocalized(
Localizer,
"command_success",
caller?.PlayerName,
false,
target.PlayerName
);
}
}
```
### Multiple Language Support
Create files for each language:
- `lang/en.json` - English
- `lang/pl.json` - Polish
- `lang/ru.json` - Russian
- `lang/de.json` - German
- etc.
---
## Working with API
### Issue Penalties
```csharp
// Ban online player
_api!.IssuePenalty(
player,
admin,
PenaltyType.Ban,
"Cheating",
1440 // 1 day in minutes
);
// Ban offline player by SteamID
_api!.IssuePenalty(
new SteamID(76561198012345678),
admin,
PenaltyType.Ban,
"Ban evasion",
0 // Permanent
);
// Other penalty types
_api!.IssuePenalty(player, admin, PenaltyType.Gag, "Chat spam", 30);
_api!.IssuePenalty(player, admin, PenaltyType.Mute, "Mic spam", 60);
_api!.IssuePenalty(player, admin, PenaltyType.Silence, "Total abuse", 120);
_api!.IssuePenalty(player, admin, PenaltyType.Warn, "Rule break");
```
### Get Player Information
```csharp
// Get player info with penalty data
var playerInfo = _api!.GetPlayerInfo(player);
Console.WriteLine($"Player: {playerInfo.PlayerName}");
Console.WriteLine($"SteamID: {playerInfo.SteamId}");
Console.WriteLine($"Warnings: {playerInfo.Warnings}");
// Get player mute status
var muteStatus = _api!.GetPlayerMuteStatus(player);
if (muteStatus.ContainsKey(PenaltyType.Gag))
{
Console.WriteLine("Player is gagged");
}
```
### Check Admin Status
```csharp
// Check if admin is in silent mode
if (_api!.IsAdminSilent(admin))
{
// Don't broadcast this action
}
// Get all silent admins
var silentAdmins = _api!.ListSilentAdminsSlots();
```
---
## Events
### Subscribe to Events
```csharp
public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
// Subscribe to events
_api.OnSimpleAdminReady += OnSimpleAdminReady;
_api.OnPlayerPenaltied += OnPlayerPenaltied;
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
_api.OnAdminShowActivity += OnAdminShowActivity;
}
private void OnSimpleAdminReady()
{
Logger.LogInformation("SimpleAdmin is ready!");
RegisterMenus();
}
private void OnPlayerPenaltied(PlayerInfo player, PlayerInfo? admin,
PenaltyType type, string reason, int duration, int? penaltyId, int? serverId)
{
Logger.LogInformation($"{player.PlayerName} received {type} for {reason}");
}
private void OnPlayerPenaltiedAdded(SteamID steamId, PlayerInfo? admin,
PenaltyType type, string reason, int duration, int? penaltyId, int? serverId)
{
Logger.LogInformation($"Offline ban added to {steamId}");
}
private void OnAdminShowActivity(string messageKey, string? callerName,
bool dontPublish, object messageArgs)
{
// React to admin activity
}
```
### Unsubscribe on Unload
```csharp
public override void Unload(bool hotReload)
{
if (_api == null) return;
_api.OnSimpleAdminReady -= OnSimpleAdminReady;
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
// ... unsubscribe all events
}
```
---
## Best Practices
### 1. Always Check for Null
```csharp
if (_api == null)
{
Logger.LogError("API not available!");
return;
}
```
### 2. Validate Player State
```csharp
if (!player.IsValid || !player.PawnIsAlive)
{
return;
}
```
### 3. Check Target Permissions
```csharp
if (!caller.CanTarget(target))
{
// caller can't target this player (immunity)
return;
}
```
### 4. Log Commands
```csharp
_api.LogCommand(caller, command);
// or
_api.LogCommand(caller, $"css_mycommand {player.PlayerName}");
```
### 5. Use Per-Player Translations
```csharp
// Each player sees message in their language!
_api.ShowAdminActivityLocalized(
Localizer,
"translation_key",
callerName,
false,
args
);
```
### 6. Clean Up Resources
```csharp
public override void Unload(bool hotReload)
{
// Unregister commands
// Unregister menus
// Unsubscribe events
// Dispose resources
}
```
---
## Common Patterns
### Player Targeting Helper
```csharp
private List GetTargets(CommandInfo command, CCSPlayerController? caller)
{
var targets = _api!.GetTarget(command);
if (targets == null) return new List();
return targets.Players
.Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
.ToList();
}
```
### Menu Context Pattern (NEW!)
```csharp
// ✅ NEW: Use context to avoid duplication
private object CreateMenu(CCSPlayerController player, MenuContext context)
{
// context.MenuTitle, context.CategoryId already set!
return _api!.CreateMenuWithPlayers(context, player, filter, action);
}
// ❌ OLD: Had to repeat title and category
private object CreateMenu(CCSPlayerController player)
{
return _api!.CreateMenuWithPlayers("My Menu", "mycategory", player, filter, action);
}
```
### Action with Activity Message
```csharp
private void DoAction(CCSPlayerController? caller, CCSPlayerController target)
{
// Perform action
// ...
// Show activity
if (caller == null || !_api!.IsAdminSilent(caller))
{
_api!.ShowAdminActivityLocalized(
Localizer,
"action_message",
caller?.PlayerName,
false,
target.PlayerName
);
}
// Log action
_api!.LogCommand(caller, $"css_action {target.PlayerName}");
}
```
---
## Testing Your Module
### 1. Build
```bash
dotnet build -c Release
```
### 2. Copy to Server
```
game/csgo/addons/counterstrikesharp/plugins/YourModuleName/
```
### 3. Test
- Start server
- Check console for load messages
- Test commands
- Test menus
- Check translations
### 4. Debug
Enable detailed logging:
```csharp
Logger.LogInformation("Debug: ...");
Logger.LogWarning("Warning: ...");
Logger.LogError("Error: ...");
```
---
## Example: Complete Mini-Module
Here's a complete working example:
```csharp
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CS2_SimpleAdminApi;
namespace ExampleModule;
public class ExampleModule : BasePlugin, IPluginConfig
{
public override string ModuleName => "Example Module";
public override string ModuleVersion => "1.0.0";
private ICS2_SimpleAdminApi? _api;
private readonly PluginCapability _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
if (Config.ExampleCommands.Count > 0)
{
foreach (var cmd in Config.ExampleCommands)
{
_api.RegisterCommand(cmd, "Example command", OnExampleCommand);
}
}
}
public void OnConfigParsed(Config config)
{
Config = config;
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/generic")]
private void OnExampleCommand(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 to target
caller?.PrintToChat($"Performed action on {target.PlayerName}");
}
_api.LogCommand(caller, command);
}
public override void Unload(bool hotReload)
{
if (_api == null) return;
foreach (var cmd in Config.ExampleCommands)
{
_api.UnRegisterCommand(cmd);
}
}
}
public class Config : IBasePluginConfig
{
public int Version { get; set; } = 1;
public List ExampleCommands { get; set; } = ["css_example"];
}
```
---
## Next Steps
- **[Study Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Complete reference
- **[Read API Documentation](../developer/api/overview)** - Full API reference
- **[Check Examples](../developer/module/examples)** - More code examples
---
## Resources
- **[CS2-SimpleAdmin GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS documentation
- **[Module Development Guide](../developer/module/getting-started)** - Detailed guide
---
## Need Help?
- **Issues:** [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
- **Discussions:** [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
- **Examples:** Study official modules for reference