mirror of
https://github.com/daffyyyy/CS2-SimpleAdmin.git
synced 2026-02-20 11:08:22 +00:00
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.
This commit is contained in:
540
CS2-SimpleAdmin-docs/docs/developer/module/best-practices.md
Normal file
540
CS2-SimpleAdmin-docs/docs/developer/module/best-practices.md
Normal file
@@ -0,0 +1,540 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
|
||||
```csharp
|
||||
// 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:
|
||||
|
||||
```csharp
|
||||
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:**
|
||||
```csharp
|
||||
foreach (var cmd in Config.MyCommands)
|
||||
{
|
||||
_api!.RegisterCommand(cmd, "Description", OnMyCommand);
|
||||
}
|
||||
```
|
||||
|
||||
### Provide Sensible Defaults
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
// ✅ Good
|
||||
if (_api == null)
|
||||
{
|
||||
Logger.LogError("API not available!");
|
||||
return;
|
||||
}
|
||||
|
||||
_api.RegisterCommand(...);
|
||||
|
||||
// ❌ Bad
|
||||
_api!.RegisterCommand(...); // Can crash if null
|
||||
```
|
||||
|
||||
### Use OnSimpleAdminReady Pattern
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
// Test with:
|
||||
// - Invalid players
|
||||
// - Disconnected players
|
||||
// - Players who changed teams
|
||||
// - Null admins (console)
|
||||
// - Silent admins
|
||||
// - Players with higher immunity
|
||||
```
|
||||
|
||||
### Test Hot Reload
|
||||
|
||||
```bash
|
||||
# Server console
|
||||
css_plugins reload YourModule
|
||||
```
|
||||
|
||||
Make sure everything works after reload!
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Forgetting to Unsubscribe
|
||||
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
// Missing unsubscribe = memory leak!
|
||||
// _api.OnSimpleAdminReady -= RegisterMenus; ← FORGOT THIS
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Not Checking API Availability
|
||||
|
||||
```csharp
|
||||
// Crashes if SimpleAdmin not loaded!
|
||||
_api.RegisterCommand(...); // ← No null check
|
||||
```
|
||||
|
||||
### ❌ Hardcoding Strings
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
// 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](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)**
|
||||
|
||||
Shows:
|
||||
- ✅ Proper code organization
|
||||
- ✅ Configuration best practices
|
||||
- ✅ Menu creation with context
|
||||
- ✅ Per-player translations
|
||||
- ✅ Proper cleanup
|
||||
- ✅ Error handling
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Examples](examples)** - More code examples
|
||||
- **[API Reference](../api/overview)** - Full API documentation
|
||||
- **[GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Browse source code
|
||||
552
CS2-SimpleAdmin-docs/docs/developer/module/examples.md
Normal file
552
CS2-SimpleAdmin-docs/docs/developer/module/examples.md
Normal file
@@ -0,0 +1,552 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Code Examples
|
||||
|
||||
Practical examples for common module development scenarios.
|
||||
|
||||
## Complete Mini Module
|
||||
|
||||
A fully working minimal module:
|
||||
|
||||
```csharp
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Capabilities;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CS2_SimpleAdminApi;
|
||||
|
||||
namespace HelloModule;
|
||||
|
||||
public class HelloModule : BasePlugin, IPluginConfig<Config>
|
||||
{
|
||||
public override string ModuleName => "Hello 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
|
||||
foreach (var cmd in Config.HelloCommands)
|
||||
{
|
||||
_api.RegisterCommand(cmd, "Say hello to a player", OnHelloCommand);
|
||||
}
|
||||
}
|
||||
|
||||
[CommandHelper(1, "<#userid or name>")]
|
||||
[RequiresPermissions("@css/generic")]
|
||||
private void OnHelloCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
|
||||
{
|
||||
player.PrintToChat($"Hello {player.PlayerName}!");
|
||||
caller?.PrintToChat($"Said hello to {player.PlayerName}");
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, command);
|
||||
}
|
||||
|
||||
public void OnConfigParsed(Config config) => Config = config;
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
foreach (var cmd in Config.HelloCommands)
|
||||
{
|
||||
_api.UnRegisterCommand(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Config : IBasePluginConfig
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public List<string> HelloCommands { get; set; } = ["css_hello"];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command Examples
|
||||
|
||||
### Simple Target Command
|
||||
|
||||
```csharp
|
||||
[CommandHelper(1, "<#userid or name>")]
|
||||
[RequiresPermissions("@css/slay")]
|
||||
private void OnSlayCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
|
||||
{
|
||||
if (player.PawnIsAlive)
|
||||
{
|
||||
player.PlayerPawn?.Value?.CommitSuicide(false, true);
|
||||
caller?.PrintToChat($"Slayed {player.PlayerName}");
|
||||
}
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, command);
|
||||
}
|
||||
```
|
||||
|
||||
### Command with Value Parameter
|
||||
|
||||
```csharp
|
||||
[CommandHelper(2, "<#userid or name> <value>")]
|
||||
[RequiresPermissions("@css/slay")]
|
||||
private void OnSetHpCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
// Parse HP value
|
||||
if (!int.TryParse(command.GetArg(2), out int hp) || hp < 1 || hp > 999)
|
||||
{
|
||||
caller?.PrintToChat("Invalid HP! Use 1-999");
|
||||
return;
|
||||
}
|
||||
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
foreach (var player in targets.Players.Where(p => p.IsValid && p.PawnIsAlive && caller!.CanTarget(p)))
|
||||
{
|
||||
player.PlayerPawn?.Value?.SetHealth(hp);
|
||||
caller?.PrintToChat($"Set {player.PlayerName} HP to {hp}");
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, $"css_sethp {hp}");
|
||||
}
|
||||
```
|
||||
|
||||
### Command with Penalty
|
||||
|
||||
```csharp
|
||||
[CommandHelper(1, "<#userid or name> [duration] [reason]")]
|
||||
[RequiresPermissions("@css/ban")]
|
||||
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
// Parse duration (default: 60 minutes)
|
||||
int duration = 60;
|
||||
if (command.ArgCount > 2)
|
||||
{
|
||||
int.TryParse(command.GetArg(2), out duration);
|
||||
}
|
||||
|
||||
// Get reason (default: "Banned")
|
||||
string reason = command.ArgCount > 3
|
||||
? string.Join(" ", command.ArgString.Split(' ').Skip(2))
|
||||
: "Banned";
|
||||
|
||||
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
|
||||
{
|
||||
_api.IssuePenalty(player, caller, PenaltyType.Ban, reason, duration);
|
||||
caller?.PrintToChat($"Banned {player.PlayerName} for {duration} minutes");
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, command);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Menu Examples
|
||||
|
||||
### Simple Player Selection Menu
|
||||
|
||||
```csharp
|
||||
private void RegisterMenus()
|
||||
{
|
||||
_api!.RegisterMenuCategory("actions", "Player Actions", "@css/generic");
|
||||
|
||||
_api.RegisterMenu(
|
||||
"actions",
|
||||
"kick",
|
||||
"Kick Player",
|
||||
CreateKickMenu,
|
||||
"@css/kick"
|
||||
);
|
||||
}
|
||||
|
||||
private object CreateKickMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
context,
|
||||
admin,
|
||||
player => player.IsValid && admin.CanTarget(player),
|
||||
(admin, target) =>
|
||||
{
|
||||
Server.ExecuteCommand($"css_kick #{target.UserId} Kicked via menu");
|
||||
admin.PrintToChat($"Kicked {target.PlayerName}");
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Menu (Player → Action)
|
||||
|
||||
```csharp
|
||||
private object CreatePlayerActionsMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack(context, admin);
|
||||
|
||||
foreach (var player in _api.GetValidPlayers().Where(p => admin.CanTarget(p)))
|
||||
{
|
||||
_api.AddSubMenu(menu, player.PlayerName, admin =>
|
||||
{
|
||||
return CreateActionSelectMenu(admin, player);
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private object CreateActionSelectMenu(CCSPlayerController admin, CCSPlayerController target)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack($"Actions: {target.PlayerName}", "actions", admin);
|
||||
|
||||
_api.AddMenuOption(menu, "Slay", _ =>
|
||||
{
|
||||
if (target.IsValid && target.PawnIsAlive)
|
||||
{
|
||||
target.PlayerPawn?.Value?.CommitSuicide(false, true);
|
||||
admin.PrintToChat($"Slayed {target.PlayerName}");
|
||||
}
|
||||
});
|
||||
|
||||
_api.AddMenuOption(menu, "Kick", _ =>
|
||||
{
|
||||
if (target.IsValid)
|
||||
{
|
||||
Server.ExecuteCommand($"css_kick #{target.UserId}");
|
||||
}
|
||||
});
|
||||
|
||||
_api.AddMenuOption(menu, "Ban", _ =>
|
||||
{
|
||||
if (target.IsValid)
|
||||
{
|
||||
_api.IssuePenalty(target, admin, PenaltyType.Ban, "Banned via menu", 1440);
|
||||
}
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
```
|
||||
|
||||
### Menu with Value Selection
|
||||
|
||||
```csharp
|
||||
private object CreateSetHpMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack(context, admin);
|
||||
|
||||
var players = _api.GetValidPlayers()
|
||||
.Where(p => p.PawnIsAlive && admin.CanTarget(p));
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
_api.AddSubMenu(menu, player.PlayerName, admin =>
|
||||
{
|
||||
return CreateHpValueMenu(admin, player);
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private object CreateHpValueMenu(CCSPlayerController admin, CCSPlayerController target)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack($"Set HP: {target.PlayerName}", "actions", admin);
|
||||
|
||||
var hpValues = new[] { 1, 10, 50, 100, 200, 500 };
|
||||
|
||||
foreach (var hp in hpValues)
|
||||
{
|
||||
_api.AddMenuOption(menu, $"{hp} HP", _ =>
|
||||
{
|
||||
if (target.IsValid && target.PawnIsAlive)
|
||||
{
|
||||
target.PlayerPawn?.Value?.SetHealth(hp);
|
||||
admin.PrintToChat($"Set {target.PlayerName} HP to {hp}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Examples
|
||||
|
||||
### React to Bans
|
||||
|
||||
```csharp
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_api = _pluginCapability.Get();
|
||||
if (_api == null) return;
|
||||
|
||||
_api.OnPlayerPenaltied += OnPlayerPenaltied;
|
||||
}
|
||||
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
if (type != PenaltyType.Ban) return;
|
||||
|
||||
var adminName = admin?.PlayerName ?? "Console";
|
||||
Logger.LogInformation($"Ban: {adminName} -> {player.PlayerName} ({duration}m): {reason}");
|
||||
|
||||
// Log to file
|
||||
File.AppendAllText("bans.log",
|
||||
$"[{DateTime.Now}] {player.PlayerName} banned by {adminName} for {duration}m: {reason}\n");
|
||||
}
|
||||
```
|
||||
|
||||
### Warning Escalation
|
||||
|
||||
```csharp
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
if (type != PenaltyType.Warn) return;
|
||||
|
||||
Logger.LogInformation($"{player.PlayerName} has {player.Warnings} warnings");
|
||||
|
||||
// Auto-ban at 3 warnings
|
||||
if (player.Warnings >= 3)
|
||||
{
|
||||
var controller = Utilities.GetPlayers()
|
||||
.FirstOrDefault(p => p.SteamID == player.SteamId);
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
_api!.IssuePenalty(
|
||||
controller,
|
||||
null,
|
||||
PenaltyType.Ban,
|
||||
"Automatic: 3 warnings",
|
||||
1440 // 1 day
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Translation Examples
|
||||
|
||||
### Module with Translations
|
||||
|
||||
**lang/en.json:**
|
||||
```json
|
||||
{
|
||||
"category_name": "My Module",
|
||||
"menu_name": "My Action",
|
||||
"action_message": "{lightred}{0}{default} performed action on {lightred}{1}{default}!",
|
||||
"error_invalid_player": "{red}Error:{default} Invalid player!",
|
||||
"success": "{green}Success!{default} Action completed."
|
||||
}
|
||||
```
|
||||
|
||||
**Code:**
|
||||
```csharp
|
||||
private void PerformAction(CCSPlayerController? admin, CCSPlayerController target)
|
||||
{
|
||||
// Perform action
|
||||
DoSomething(target);
|
||||
|
||||
// Show activity with translation
|
||||
if (admin == null || !_api!.IsAdminSilent(admin))
|
||||
{
|
||||
if (Localizer != null)
|
||||
{
|
||||
_api!.ShowAdminActivityLocalized(
|
||||
Localizer,
|
||||
"action_message",
|
||||
admin?.PlayerName,
|
||||
false,
|
||||
admin?.PlayerName ?? "Console",
|
||||
target.PlayerName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Send success message
|
||||
admin?.PrintToChat(Localizer?["success"] ?? "Success!");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utility Examples
|
||||
|
||||
### Get Players by Team
|
||||
|
||||
```csharp
|
||||
private List<CCSPlayerController> GetTeamPlayers(CsTeam team)
|
||||
{
|
||||
return _api!.GetValidPlayers()
|
||||
.Where(p => p.Team == team)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Usage
|
||||
var ctPlayers = GetTeamPlayers(CsTeam.CounterTerrorist);
|
||||
var tPlayers = GetTeamPlayers(CsTeam.Terrorist);
|
||||
```
|
||||
|
||||
### Get Alive Players
|
||||
|
||||
```csharp
|
||||
private List<CCSPlayerController> GetAlivePlayers()
|
||||
{
|
||||
return _api!.GetValidPlayers()
|
||||
.Where(p => p.PawnIsAlive)
|
||||
.ToList();
|
||||
}
|
||||
```
|
||||
|
||||
### Notify Admins
|
||||
|
||||
```csharp
|
||||
private void NotifyAdmins(string message, string permission = "@css/generic")
|
||||
{
|
||||
var admins = _api!.GetValidPlayers()
|
||||
.Where(p => AdminManager.PlayerHasPermissions(p, permission));
|
||||
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
admin.PrintToChat(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
NotifyAdmins("⚠ Important admin message", "@css/root");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timer Examples
|
||||
|
||||
### Delayed Action
|
||||
|
||||
```csharp
|
||||
private void DelayedAction(CCSPlayerController player, float delay)
|
||||
{
|
||||
AddTimer(delay, () =>
|
||||
{
|
||||
if (player.IsValid && player.PawnIsAlive)
|
||||
{
|
||||
DoAction(player);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Repeating Timer
|
||||
|
||||
```csharp
|
||||
private void StartRepeatingAction()
|
||||
{
|
||||
AddTimer(1.0f, () =>
|
||||
{
|
||||
foreach (var player in _api!.GetValidPlayers())
|
||||
{
|
||||
if (player.PawnIsAlive)
|
||||
{
|
||||
UpdatePlayer(player);
|
||||
}
|
||||
}
|
||||
}, TimerFlags.REPEAT);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Multiple Feature Toggles
|
||||
|
||||
```csharp
|
||||
public class Config : IBasePluginConfig
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("EnableFeature1")]
|
||||
public bool EnableFeature1 { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("EnableFeature2")]
|
||||
public bool EnableFeature2 { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("Feature1Commands")]
|
||||
public List<string> Feature1Commands { get; set; } = ["css_feature1"];
|
||||
|
||||
[JsonPropertyName("Feature2Commands")]
|
||||
public List<string> Feature2Commands { get; set; } = ["css_feature2"];
|
||||
|
||||
[JsonPropertyName("MaxValue")]
|
||||
public int MaxValue { get; set; } = 100;
|
||||
}
|
||||
|
||||
// Usage
|
||||
private void RegisterCommands()
|
||||
{
|
||||
if (Config.EnableFeature1)
|
||||
{
|
||||
foreach (var cmd in Config.Feature1Commands)
|
||||
{
|
||||
_api!.RegisterCommand(cmd, "Feature 1", OnFeature1Command);
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.EnableFeature2)
|
||||
{
|
||||
foreach (var cmd in Config.Feature2Commands)
|
||||
{
|
||||
_api!.RegisterCommand(cmd, "Feature 2", OnFeature2Command);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Best Practices](best-practices)** - Write better code
|
||||
- **[Getting Started](getting-started)** - Create your first module
|
||||
- **[API Reference](../api/overview)** - Full API documentation
|
||||
- **[Fun Commands Source](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Complete reference implementation
|
||||
282
CS2-SimpleAdmin-docs/docs/developer/module/getting-started.md
Normal file
282
CS2-SimpleAdmin-docs/docs/developer/module/getting-started.md
Normal file
@@ -0,0 +1,282 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Getting Started with Module Development
|
||||
|
||||
Step-by-step guide to creating your first CS2-SimpleAdmin module.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin:
|
||||
|
||||
- C# knowledge (intermediate level)
|
||||
- .NET 8.0 SDK installed
|
||||
- Visual Studio 2022 or VS Code
|
||||
- Basic understanding of CounterStrikeSharp
|
||||
- CS2 dedicated server for testing
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Project
|
||||
|
||||
### Using .NET CLI
|
||||
|
||||
```bash
|
||||
dotnet new classlib -n MyModule -f net8.0
|
||||
cd MyModule
|
||||
```
|
||||
|
||||
### Using Visual Studio
|
||||
|
||||
1. File → New → Project
|
||||
2. Select "Class Library (.NET 8.0)"
|
||||
3. Name: `MyModule`
|
||||
4. Click Create
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Add References
|
||||
|
||||
Edit `MyModule.csproj`:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="CounterStrikeSharp.API">
|
||||
<HintPath>path/to/CounterStrikeSharp.API.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
|
||||
<Reference Include="CS2-SimpleAdminApi">
|
||||
<HintPath>path/to/CS2-SimpleAdminApi.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create Main Plugin Class
|
||||
|
||||
Create `MyModule.cs`:
|
||||
|
||||
```csharp
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Capabilities;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CS2_SimpleAdminApi;
|
||||
|
||||
namespace MyModule;
|
||||
|
||||
public class MyModule : BasePlugin, IPluginConfig<Config>
|
||||
{
|
||||
public override string ModuleName => "My Module";
|
||||
public override string ModuleVersion => "1.0.0";
|
||||
public override string ModuleAuthor => "Your Name";
|
||||
public override string ModuleDescription => "My awesome module";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Logger.LogInformation("MyModule loaded successfully!");
|
||||
|
||||
// Register features
|
||||
RegisterCommands();
|
||||
|
||||
// Register menus when ready
|
||||
_api.OnSimpleAdminReady += RegisterMenus;
|
||||
RegisterMenus(); // Also call for hot reload
|
||||
}
|
||||
|
||||
private void RegisterCommands()
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
foreach (var cmd in Config.MyCommands)
|
||||
{
|
||||
_api.RegisterCommand(cmd, "My command description", OnMyCommand);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterMenus()
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
_api.RegisterMenuCategory("mymodule", "My Module", "@css/generic");
|
||||
_api.RegisterMenu("mymodule", "mymenu", "My Menu", CreateMyMenu, "@css/generic");
|
||||
}
|
||||
|
||||
[CommandHelper(1, "<#userid or name>")]
|
||||
[RequiresPermissions("@css/generic")]
|
||||
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
foreach (var player in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
|
||||
{
|
||||
player.PrintToChat($"Hello from MyModule!");
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, command);
|
||||
}
|
||||
|
||||
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
context,
|
||||
admin,
|
||||
player => player.IsValid && admin.CanTarget(player),
|
||||
(admin, target) =>
|
||||
{
|
||||
target.PrintToChat("You were selected!");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public void OnConfigParsed(Config config)
|
||||
{
|
||||
Config = config;
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
// Unregister commands
|
||||
foreach (var cmd in Config.MyCommands)
|
||||
{
|
||||
_api.UnRegisterCommand(cmd);
|
||||
}
|
||||
|
||||
// Unregister menus
|
||||
_api.UnregisterMenu("mymodule", "mymenu");
|
||||
|
||||
// Unsubscribe events
|
||||
_api.OnSimpleAdminReady -= RegisterMenus;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create Configuration
|
||||
|
||||
Create `Config.cs`:
|
||||
|
||||
```csharp
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Build and Deploy
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
dotnet build -c Release
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
Copy files to server:
|
||||
```
|
||||
game/csgo/addons/counterstrikesharp/plugins/MyModule/
|
||||
└── MyModule.dll
|
||||
```
|
||||
|
||||
### Restart Server
|
||||
|
||||
```bash
|
||||
# Server console
|
||||
css_plugins reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Test
|
||||
|
||||
1. Join your server
|
||||
2. Open admin menu: `css_admin`
|
||||
3. Look for "My Module" category
|
||||
4. Test command: `css_mycommand @me`
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Best Practices](best-practices)** - Write better code
|
||||
- **[Examples](examples)** - More code examples
|
||||
- **[API Reference](../api/overview)** - Full API documentation
|
||||
- **[Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Reference implementation
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### API Not Found
|
||||
|
||||
**Error:** `CS2-SimpleAdmin API not found!`
|
||||
|
||||
**Solution:**
|
||||
- Ensure CS2-SimpleAdmin is installed
|
||||
- Check that CS2-SimpleAdminApi.dll is in shared folder
|
||||
- Verify CS2-SimpleAdmin loads before your module
|
||||
|
||||
### Commands Not Working
|
||||
|
||||
**Check:**
|
||||
- Command registered in `RegisterCommands()`
|
||||
- Permission is correct
|
||||
- Player has required permission
|
||||
|
||||
### Menu Not Showing
|
||||
|
||||
**Check:**
|
||||
- `OnSimpleAdminReady` event subscribed
|
||||
- Menu registered in category
|
||||
- Permission is correct
|
||||
- SimpleAdmin loaded successfully
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **[Module Development Guide](../../modules/development)** - Detailed guide
|
||||
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
|
||||
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS framework
|
||||
Reference in New Issue
Block a user