Files
CS2-SimpleAdmin/CS2-SimpleAdmin-docs/docs/developer/module/best-practices.md
Dawid Bepierszcz b0d8696756 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.
2025-10-20 01:27:01 +02:00

10 KiB

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