Skip to main content

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
// 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:

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:

foreach (var cmd in Config.MyCommands)
{
_api!.RegisterCommand(cmd, "Description", OnMyCommand);
}

Provide Sensible Defaults

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

// ✅ Good
if (_api == null)
{
Logger.LogError("API not available!");
return;
}

_api.RegisterCommand(...);

// ❌ Bad
_api!.RegisterCommand(...); // Can crash if null

Use OnSimpleAdminReady Pattern

// ✅ 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

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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

/// <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

// Test with:
// - Invalid players
// - Disconnected players
// - Players who changed teams
// - Null admins (console)
// - Silent admins
// - Players with higher immunity

Test Hot Reload

# Server console
css_plugins reload YourModule

Make sure everything works after reload!


Common Mistakes

❌ Forgetting to Unsubscribe

public override void Unload(bool hotReload)
{
// Missing unsubscribe = memory leak!
// _api.OnSimpleAdminReady -= RegisterMenus; ← FORGOT THIS
}

❌ Not Checking API Availability

// Crashes if SimpleAdmin not loaded!
_api.RegisterCommand(...); // ← No null check

❌ Hardcoding Strings

// 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

// 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

Shows:

  • ✅ Proper code organization
  • ✅ Configuration best practices
  • ✅ Menu creation with context
  • ✅ Per-player translations
  • ✅ Proper cleanup
  • ✅ Error handling

Next Steps