Skip to main content

Plugin Architecture

Deep dive into CS2-SimpleAdmin's architecture and design patterns.

Overview

CS2-SimpleAdmin follows a layered architecture with clear separation of concerns and well-defined responsibilities for each component.


Architecture Layers

┌─────────────────────────────────────────┐
│ CounterStrikeSharp Integration Layer │ ← CS2_SimpleAdmin.cs
├─────────────────────────────────────────┤
│ Manager Layer │ ← /Managers/
│ • PermissionManager │
│ • BanManager │
│ • MuteManager │
│ • WarnManager │
│ • CacheManager │
│ • PlayerManager │
│ • ServerManager │
│ • DiscordManager │
├─────────────────────────────────────────┤
│ Database Layer │ ← /Database/
│ • IDatabaseProvider (Interface) │
│ • MySqlDatabaseProvider │
│ • SqliteDatabaseProvider │
│ • Migration System │
├─────────────────────────────────────────┤
│ Menu System │ ← /Menus/
│ • MenuManager (Singleton) │
│ • MenuBuilder (Factory) │
│ • Specific Menu Classes │
├─────────────────────────────────────────┤
│ Command System │ ← /Commands/
│ • RegisterCommands │
│ • Command Handlers (basebans, etc.) │
├─────────────────────────────────────────┤
│ Public API │ ← /Api/
│ • ICS2_SimpleAdminApi (Interface) │
│ • CS2_SimpleAdminApi (Implementation) │
└─────────────────────────────────────────┘

Core Components

1. CounterStrikeSharp Integration Layer

File: CS2_SimpleAdmin.cs

Responsibilities:

  • Plugin lifecycle management (Load/Unload)
  • Event registration (player_connect, player_disconnect, etc.)
  • Command routing
  • Low-level game operations using MemoryFunctionVoid
  • Timer management

Key Methods:

public override void Load(bool hotReload)
public override void Unload(bool hotReload)
private HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo info)
private HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)

2. Manager Layer

Each manager encapsulates specific domain logic:

PermissionManager

File: /Managers/PermissionManager.cs

Responsibilities:

  • Load admin flags and groups from database
  • Maintain in-memory AdminCache with lazy-loading
  • Time-based cache expiry
  • Immunity level management

Key Patterns:

  • Caching for performance
  • Lazy loading of admin data
  • Periodic refresh

BanManager

File: /Managers/BanManager.cs

Responsibilities:

  • Issue bans (SteamID, IP, or hybrid)
  • Remove bans (unban)
  • Handle ban expiration cleanup
  • Multi-server ban synchronization

Key Operations:

Task BanPlayer(...)
Task AddBanBySteamId(...)
Task RemoveBan(...)

MuteManager

File: /Managers/MuteManager.cs

Responsibilities:

  • Three mute types: GAG (text), MUTE (voice), SILENCE (both)
  • Duration-based mutes
  • Expiration tracking

WarnManager

File: /Managers/WarnManager.cs

Responsibilities:

  • Progressive warning system
  • Auto-escalation to bans based on WarnThreshold config
  • Warning history tracking

CacheManager

File: /Managers/CacheManager.cs

Purpose: Performance optimization layer

Features:

  • In-memory ban cache with O(1) lookups by SteamID and IP
  • Player IP history tracking for multi-account detection
  • Reduces database queries on player join

Data Structures:

Dictionary<ulong, BanInfo> _banCacheBySteamId
Dictionary<string, List<BanInfo>> _banCacheByIp
Dictionary<ulong, List<string>> _playerIpHistory

PlayerManager

File: /Managers/PlayerManager.cs

Responsibilities:

  • Load player data on connect
  • Check bans against cache
  • Update IP history
  • Semaphore limiting (max 5 concurrent loads)

Key Pattern:

private readonly SemaphoreSlim _semaphore = new(5, 5);

public async Task LoadPlayerData(CCSPlayerController player)
{
await _semaphore.WaitAsync();
try
{
// Load player data
}
finally
{
_semaphore.Release();
}
}

ServerManager

File: /Managers/ServerManager.cs

Responsibilities:

  • Load/register server metadata (IP, port, hostname, RCON)
  • Multi-server mode support
  • Server ID management

DiscordManager

File: /Managers/DiscordManager.cs

Responsibilities:

  • Send webhook notifications for admin actions
  • Configurable webhooks per penalty type
  • Embed formatting with placeholders

3. Database Layer

Files: /Database/

Provider Pattern for database abstraction:

public interface IDatabaseProvider
{
Task ExecuteAsync(string query, object? parameters = null);
Task<T> QueryFirstOrDefaultAsync<T>(string query, object? parameters = null);
Task<List<T>> QueryAsync<T>(string query, object? parameters = null);

// Query generation methods
string GetBanQuery(bool multiServer);
string GetMuteQuery(bool multiServer);
// ... more query methods
}

Implementations:

  • MySqlDatabaseProvider - MySQL-specific SQL syntax
  • SqliteDatabaseProvider - SQLite-specific SQL syntax

Benefits:

  • Single codebase supports both MySQL and SQLite
  • Easy to add new database providers
  • Query methods accept multiServer boolean for scoping

Migration System:

File: Database/Migration.cs

  • File-based migrations in /Database/Migrations/{mysql,sqlite}/
  • Numbered files: 001_CreateTables.sql, 002_AddColumn.sql
  • Tracking table: sa_migrations
  • Auto-applies on plugin load
  • Safe for multi-server environments

4. Menu System

Files: /Menus/

MenuManager (Singleton Pattern):

public class MenuManager
{
public static MenuManager Instance { get; private set; }

private readonly Dictionary<string, MenuCategory> _categories = new();
private readonly Dictionary<string, Dictionary<string, MenuInfo>> _menus = new();

public void RegisterCategory(string id, string name, string permission);
public void RegisterMenu(string categoryId, string menuId, string name, ...);
public MenuBuilder CreateCategoryMenuPublic(MenuCategory category, CCSPlayerController player);
}

MenuBuilder (Factory Pattern):

public class MenuBuilder
{
public MenuBuilder(string title);
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, ...);
public MenuBuilder AddSubMenu(string name, Func<CCSPlayerController, MenuBuilder> factory, ...);
public MenuBuilder WithBackAction(Action<CCSPlayerController> backAction);
public void OpenMenu(CCSPlayerController player);
}

Specific Menu Classes:

  • AdminMenu - Main admin menu with categories
  • ManagePlayersMenu - Player management menus
  • ManageServerMenu - Server settings
  • DurationMenu - Duration selection
  • ReasonMenu - Reason selection

Benefits:

  • Centralized menu management
  • Permission-aware rendering
  • Automatic back button handling
  • Reusable menu components

5. Command System

Files: /Commands/

Central Registration:

File: RegisterCommands.cs

public static class RegisterCommands
{
public static Dictionary<string, List<CommandDefinition>> _commandDefinitions = new();

public static void RegisterCommands(CS2_SimpleAdmin plugin)
{
// Load Commands.json
// Map commands to handler methods
// Register with CounterStrikeSharp
}
}

Command Handlers:

Organized by category:

  • basebans.cs - Ban, unban, warn commands
  • basecomms.cs - Gag, mute, silence commands
  • basecommands.cs - Admin management, server commands
  • basechat.cs - Chat commands (asay, csay, etc.)
  • playercommands.cs - Player manipulation (slay, hp, etc.)
  • funcommands.cs - Fun commands (god, noclip, etc.)
  • basevotes.cs - Voting system

Two-Tier Pattern:

// Entry command - parses arguments
[CommandHelper(2, "<#userid> <duration> [reason]")]
[RequiresPermissions("@css/ban")]
public void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
var targets = GetTarget(command);
int duration = ParseDuration(command.GetArg(2));
string reason = ParseReason(command);

foreach (var target in targets)
{
Ban(caller, target, duration, reason); // Core method
}
}

// Core method - database writes, events
private void Ban(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
{
// Write to database
BanManager.BanPlayer(target, admin, duration, reason);

// Update cache
CacheManager.AddBan(target);

// Trigger events
ApiInstance.OnPlayerPenaltiedEvent(target, admin, PenaltyType.Ban, reason, duration);

// Kick player
Server.ExecuteCommand($"kick {target.UserId}");

// Send Discord notification
DiscordManager.SendBanNotification(target, admin, duration, reason);

// Broadcast action
ShowAdminActivity("ban_message", admin?.PlayerName, target.PlayerName, duration, reason);
}

Benefits:

  • Separation of parsing and execution
  • Reusable core methods
  • Consistent event triggering
  • Easy to test

6. Public API

Files: /Api/

Interface: ICS2_SimpleAdminApi.cs (in CS2-SimpleAdminApi project)

Implementation: CS2_SimpleAdminApi.cs

Capability System:

// In API interface
public static readonly PluginCapability<ICS2_SimpleAdminApi> PluginCapability = new("simpleadmin:api");

// In module
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability = new("simpleadmin:api");

public override void OnAllPluginsLoaded(bool hotReload)
{
_api = _pluginCapability.Get();
}

Event Publishing:

// API exposes events
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, ...>? OnPlayerPenaltied;

// Core plugin triggers events
ApiInstance.OnPlayerPenaltiedEvent(player, admin, type, reason, duration, id);

// Modules subscribe
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// React to penalty
};

Data Flow Patterns

Player Join Flow

1. player_connect event

2. PlayerManager.LoadPlayerData()

3. Semaphore.WaitAsync() ← Max 5 concurrent

4. CacheManager.CheckBan(steamId, ip)

5a. BANNED → Kick player immediately
5b. CLEAN → Continue

6. Load active penalties from DB

7. Store in PlayersInfo dictionary

8. Update player IP history

Ban Command Flow

1. OnBanCommand() ← Parse arguments

2. Ban() ← Core method

3. BanManager.BanPlayer() ← Write to DB

4. CacheManager.AddBan() ← Update cache

5. ApiInstance.OnPlayerPenaltiedEvent() ← Trigger event

6. Server.ExecuteCommand("kick") ← Kick player

7. DiscordManager.SendNotification() ← Discord webhook

8. ShowAdminActivity() ← Broadcast action

Admin Permission Check Flow

1. Plugin Load

2. PermissionManager.LoadAdmins()

3. Build AdminCache ← SteamID → Flags/Immunity

4. Command Execution

5. RequiresPermissions attribute check

6. AdminManager.PlayerHasPermissions() ← Check cache

7a. HAS PERMISSION → Execute
7b. NO PERMISSION → Deny

Design Patterns Used

Singleton Pattern

public class MenuManager
{
public static MenuManager Instance { get; private set; }

public static void Initialize(CS2_SimpleAdmin plugin)
{
Instance = new MenuManager(plugin);
}
}

Used for:

  • MenuManager - Single menu registry
  • Cache management - Single source of truth

Factory Pattern

public class MenuBuilder
{
public static MenuBuilder Create(string title) => new MenuBuilder(title);

public MenuBuilder AddOption(...) { /* ... */ return this; }
public MenuBuilder AddSubMenu(...) { /* ... */ return this; }
}

Used for:

  • Menu creation
  • Database provider creation

Strategy Pattern

public interface IDatabaseProvider
{
Task<List<BanInfo>> GetBans(bool multiServer);
}

public class MySqlDatabaseProvider : IDatabaseProvider { /* ... */ }
public class SqliteDatabaseProvider : IDatabaseProvider { /* ... */ }

Used for:

  • Database abstraction
  • Query generation per DB type

Observer Pattern

// Publisher
public event Action<PlayerInfo, ...>? OnPlayerPenaltied;

// Trigger
OnPlayerPenaltied?.Invoke(player, admin, type, reason, duration, id, sid);

// Subscribers
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
{
// React
};

Used for:

  • Event system
  • Module communication

Concurrency & Thread Safety

Async/Await Patterns

All database operations use async/await:

public async Task BanPlayer(...)
{
await _database.ExecuteAsync(query, parameters);
}

Semaphore for Rate Limiting

private readonly SemaphoreSlim _semaphore = new(5, 5);

public async Task LoadPlayerData(CCSPlayerController player)
{
await _semaphore.WaitAsync();
try
{
// Load data
}
finally
{
_semaphore.Release();
}
}

Thread-Safe Collections

private readonly ConcurrentDictionary<ulong, PlayerInfo> PlayersInfo = new();

Memory Management

In-Memory Caches

AdminCache:

Dictionary<ulong, (List<string> Flags, int Immunity, DateTime Expiry)> AdminCache

BanCache:

Dictionary<ulong, BanInfo> _banCacheBySteamId
Dictionary<string, List<BanInfo>> _banCacheByIp

Benefits:

  • Reduces database load
  • O(1) lookups
  • TTL-based expiry

Cleanup

// On player disconnect
PlayersInfo.TryRemove(player.SteamID, out _);

// Periodic cache cleanup
AddTimer(3600f, CleanupExpiredCache, TimerFlags.REPEAT);

Configuration System

Multi-Level Configuration

  1. Main Config: CS2-SimpleAdmin.json
  2. Commands Config: Commands.json
  3. Module Configs: Per-module JSON files

Hot Reload Support

public void OnConfigParsed(Config config)
{
Config = config;
// Reconfigure without restart
}

Performance Optimizations

  1. Caching - Minimize database queries
  2. Lazy Loading - Load admin data on-demand
  3. Semaphore - Limit concurrent operations
  4. Connection Pooling - Reuse DB connections
  5. Indexed Queries - Fast database lookups
  6. Memory Cleanup - Remove disconnected player data

Future Extensibility

Plugin Capabilities

New modules can extend functionality:

// New capability
var customCapability = new PluginCapability<ICustomFeature>("custom:feature");
Capabilities.RegisterPluginCapability(customCapability, () => _customFeature);

// Other plugins can use it
var feature = _customCapability.Get();

Event-Driven Architecture

New events can be added without breaking changes:

public event Action<NewEventArgs>? OnNewEvent;

Testing Considerations

Unit Testing

  • Managers can be tested independently
  • Mock IDatabaseProvider for testing
  • Test command handlers with mock players

Integration Testing

  • Test on actual CS2 server
  • Multi-server scenarios
  • Database migration testing