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:

---

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:

---

Next Steps