Modulentwicklung

Learn how to create your own CS2-SimpleAdmin modules.

Einführung

Creating modules for CS2-SimpleAdmin allows you to extend the plugin's functionality while keeping your code separate and maintainable.

Reference Implementation

The Fun-Befehle-Modul serves as a complete reference implementation. Study its code to learn best practices!

---

Voraussetzungen

Knowledge Required

Tools Needed

---

Schnellstart

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

---

Modulstruktur

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

---

Konfiguration

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 Befehle

Basic Befehl 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);
}

Befehl 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 Menüs

Register Menü 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;
}
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)
    );
}
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 Menüs

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;
}

---

Übersetzungen

Create Translation Dateien

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 Übersetzungen 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:

---

Working with API

Issue Strafen

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

---

Bewährte Vorgehensweisen

1. Always Check for Null

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

2. Validate Spieler State

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

3. Check Target Berechtigungen

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

4. Log Befehle

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

5. Use Per-Spieler Übersetzungen

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

6. Clean Up Ressourcen

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

---

Häufige Muster

Spieler 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();
}
// ✅ 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}");
}

---

Tests Your Modul

1. Build

dotnet build -c Release

2. Copy to Server

game/csgo/addons/counterstrikesharp/plugins/YourModuleName/

3. Test

4. Debug

Enable detailed logging:

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

---

Beispiel: Complete Mini-Modul

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"];
}

---

Nächste Schritte

---

Ressourcen

---

Brauchst du Hilfe?