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
AdminCachewith 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
WarnThresholdconfig - 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 syntaxSqliteDatabaseProvider- SQLite-specific SQL syntax
Benefits:
- Single codebase supports both MySQL and SQLite
- Easy to add new database providers
- Query methods accept
multiServerboolean 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 categoriesManagePlayersMenu- Player management menusManageServerMenu- Server settingsDurationMenu- Duration selectionReasonMenu- 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 commandsbasecomms.cs- Gag, mute, silence commandsbasecommands.cs- Admin management, server commandsbasechat.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
- Main Config:
CS2-SimpleAdmin.json - Commands Config:
Commands.json - Module Configs: Per-module JSON files
Hot Reload Support
public void OnConfigParsed(Config config)
{
Config = config;
// Reconfigure without restart
}
Performance Optimizations
- Caching - Minimize database queries
- Lazy Loading - Load admin data on-demand
- Semaphore - Limit concurrent operations
- Connection Pooling - Reuse DB connections
- Indexed Queries - Fast database lookups
- 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
IDatabaseProviderfor testing - Test command handlers with mock players
Integration Testing
- Test on actual CS2 server
- Multi-server scenarios
- Database migration testing
Related Documentation
- API Overview - Public API details
- Module Development - Create modules
- GitHub Source - Browse code