Files
CS2-SimpleAdmin/CS2-SimpleAdmin-docs/docs/modules/development.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

17 KiB

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

dotnet new classlib -n YourModuleName -f net8.0
cd YourModuleName

2. Add References

Edit your .csproj file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <!-- CounterStrikeSharp -->
    <Reference Include="CounterStrikeSharp.API">
      <HintPath>path/to/CounterStrikeSharp.API.dll</HintPath>
      <Private>false</Private>
    </Reference>

    <!-- CS2-SimpleAdmin API -->
    <Reference Include="CS2-SimpleAdminApi">
      <HintPath>path/to/CS2-SimpleAdminApi.dll</HintPath>
      <Private>false</Private>
    </Reference>
  </ItemGroup>
</Project>

3. Create Main Plugin Class

using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CS2_SimpleAdminApi;

namespace YourModuleName;

public class YourModule : BasePlugin, IPluginConfig<Config>
{
    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<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;
        }

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

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:

// YourModule.cs
public partial class YourModule : BasePlugin, IPluginConfig<Config>
{
    // 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

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;

    [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

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:

public override void Unload(bool hotReload)
{
    if (_api == null) return;

    foreach (var cmd in Config.MyCommands)
    {
        _api.UnRegisterCommand(cmd);
    }
}

Creating Menus

Register Menu Category

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)

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

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

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:

{
  "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

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

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

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

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

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

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

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

2. Validate Player State

if (!player.IsValid || !player.PawnIsAlive)
{
    return;
}

3. Check Target Permissions

if (!caller.CanTarget(target))
{
    // caller can't target this player (immunity)
    return;
}

4. Log Commands

_api.LogCommand(caller, command);
// or
_api.LogCommand(caller, $"css_mycommand {player.PlayerName}");

5. Use Per-Player Translations

// Each player sees message in their language!
_api.ShowAdminActivityLocalized(
    Localizer,
    "translation_key",
    callerName,
    false,
    args
);

6. Clean Up Resources

public override void Unload(bool hotReload)
{
    // Unregister commands
    // Unregister menus
    // Unsubscribe events
    // Dispose resources
}

Common Patterns

Player Targeting Helper

private List<CCSPlayerController> GetTargets(CommandInfo command, CCSPlayerController? caller)
{
    var targets = _api!.GetTarget(command);
    if (targets == null) return new List<CCSPlayerController>();

    return targets.Players
        .Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
        .ToList();
}

Menu Context Pattern (NEW!)

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

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

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:

Logger.LogInformation("Debug: ...");
Logger.LogWarning("Warning: ...");
Logger.LogError("Error: ...");

Example: Complete Mini-Module

Here's a complete working example:

using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CS2_SimpleAdminApi;

namespace ExampleModule;

public class ExampleModule : BasePlugin, IPluginConfig<Config>
{
    public override string ModuleName => "Example 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
        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<string> ExampleCommands { get; set; } = ["css_example"];
}

Next Steps


Resources


Need Help?