Migrate menus and votes to Panorama UI Kit
Replace MenuManagerCS2 with Panorama UI Kit for the admin menus and css_vote. Layout names (menu, vote, banner, toast) are configurable under MenuConfig and preloaded on startup; MenuType is removed and ConfigVersion bumped to 26. - Drop MenuManagerApi.dll and the old chat menu classes - Reference PanoramaUiKit.Shared.dll, bump CounterStrikeSharp to 1.0.376 - Add .editorconfig and CODING_CONVENTIONS.md, apply them codebase-wide - Remove the CS2-SimpleAdmin-docs site - Rewrite README for the fork
60
.editorconfig
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{csproj,props,targets,xml,json,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.cs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# See CODING_CONVENTIONS.md
|
||||
|
||||
# Explicit types, never var
|
||||
csharp_style_var_for_built_in_types = false:warning
|
||||
csharp_style_var_when_type_is_apparent = false:warning
|
||||
csharp_style_var_elsewhere = false:warning
|
||||
dotnet_diagnostic.IDE0007.severity = none
|
||||
dotnet_diagnostic.IDE0008.severity = warning
|
||||
|
||||
# Braces always, no single-line if
|
||||
csharp_prefer_braces = true:warning
|
||||
dotnet_diagnostic.IDE0011.severity = warning
|
||||
|
||||
# No ternary
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = false:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = false:silent
|
||||
dotnet_diagnostic.IDE0045.severity = none
|
||||
dotnet_diagnostic.IDE0046.severity = none
|
||||
|
||||
# Usings
|
||||
dotnet_sort_system_directives_first = true
|
||||
dotnet_separate_import_directive_groups = false
|
||||
csharp_using_directive_placement = outside_namespace:warning
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
|
||||
# Namespaces
|
||||
csharp_style_namespace_declarations = file_scoped:suggestion
|
||||
|
||||
# Allman braces
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
|
||||
# Modifiers / this.
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
|
||||
# Unused
|
||||
dotnet_diagnostic.IDE0059.severity = suggestion
|
||||
dotnet_diagnostic.IDE0060.severity = suggestion
|
||||
58
CODING_CONVENTIONS.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Coding conventions
|
||||
|
||||
Rules for all C# in this repository. `.editorconfig` enforces most of them: run
|
||||
`dotnet format CS2-SimpleAdmin.sln` before committing.
|
||||
|
||||
## Types
|
||||
|
||||
- Always write the explicit type. No `var`.
|
||||
```csharp
|
||||
List<CCSPlayerController> players = Helper.GetValidPlayers();
|
||||
foreach (CCSPlayerController player in players) { ... }
|
||||
```
|
||||
- The only exception is an anonymous type, which has no name (Dapper parameters): `var parameters = new { ... };`.
|
||||
- Target-typed `new()` and collection expressions (`[]`) are fine: the type is already written on the left.
|
||||
|
||||
## Conditions
|
||||
|
||||
- No ternary operator (`c ? a : b`). Write an `if`, or extract a small helper that returns.
|
||||
```csharp
|
||||
string status = "OFF";
|
||||
if (loaded)
|
||||
{
|
||||
status = "ON";
|
||||
}
|
||||
```
|
||||
- Always use braces, even for one statement. No single-line `if (x) return;`.
|
||||
- `switch` expressions on a value are fine (`penaltyType switch { ... }`).
|
||||
- `??` and `?.` are fine.
|
||||
|
||||
## Layout
|
||||
|
||||
- Allman braces, 4 spaces, LF line endings.
|
||||
- File-scoped namespaces.
|
||||
- `using` directives outside the namespace, `System` first, no unused ones.
|
||||
- Early return to keep nesting shallow.
|
||||
|
||||
## Naming
|
||||
|
||||
- PascalCase for types, methods, properties, constants. camelCase for locals and parameters. `_camelCase` for private fields.
|
||||
- A lambda parameter you do not use is `_`. Never assign to a discard (`_ = ...`) inside a lambda whose parameter is `_`: the assignment goes to the parameter.
|
||||
|
||||
## Duplication
|
||||
|
||||
- When the same expression shows up in several commands, add a helper in `Helper` (see `GetCallerName`,
|
||||
`GetReasonOrUnknown`, `GetArgsReason`, `GetAdminInfo`, `GetLogName`, `Truncate`, `HasCommandPermission`).
|
||||
|
||||
## Comments
|
||||
|
||||
- Comment why, not what. No commented-out code: git keeps the history.
|
||||
- XML `<summary>` on public members and on anything non-obvious.
|
||||
|
||||
## UI
|
||||
|
||||
- All UI goes through the Panorama UI Kit (`Menus/UiKit.cs`). Never call CounterStrikeSharp chat/center menus directly.
|
||||
- Menus are built with `MenuBuilder`. A sub-menu returns `builder.WithBackButton()`; the kit draws the back button.
|
||||
- An option closes the menu after its action, unless it passes `keepOpen: true` or its action opens another menu.
|
||||
- Panorama does not render chat color tags (`{red}`, `ChatColors`): keep them out of UI text.
|
||||
- Native calls are not thread-safe: after an `await`, come back with `Server.NextWorldUpdateAsync` before touching the UI.
|
||||
20
CS2-SimpleAdmin-docs/.gitignore
vendored
|
|
@ -1,20 +0,0 @@
|
|||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# Website
|
||||
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
## Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```bash
|
||||
USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```bash
|
||||
GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||
|
|
@ -1,465 +0,0 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Commands API
|
||||
|
||||
Complete reference for command registration and management.
|
||||
|
||||
## Command Registration
|
||||
|
||||
### RegisterCommand
|
||||
|
||||
Register a new command that integrates with CS2-SimpleAdmin.
|
||||
|
||||
```csharp
|
||||
void RegisterCommand(string name, string? description, CommandInfo.CommandCallback callback)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `name` - Command name (e.g., "css_mycommand")
|
||||
- `description` - Command description (optional)
|
||||
- `callback` - Method to call when command is executed
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.RegisterCommand("css_mycommand", "My custom command", OnMyCommand);
|
||||
|
||||
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
// Command logic here
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- `ArgumentException` - If command name is null or empty
|
||||
- `ArgumentNullException` - If callback is null
|
||||
|
||||
---
|
||||
|
||||
### UnRegisterCommand
|
||||
|
||||
Unregister a previously registered command.
|
||||
|
||||
```csharp
|
||||
void UnRegisterCommand(string commandName)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `commandName` - Name of command to unregister
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.UnRegisterCommand("css_mycommand");
|
||||
```
|
||||
|
||||
**Best Practice:**
|
||||
Always unregister commands in your plugin's `Unload()` method:
|
||||
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
_api.UnRegisterCommand("css_mycommand");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Target Parsing
|
||||
|
||||
### GetTarget
|
||||
|
||||
Parse player targets from command arguments.
|
||||
|
||||
```csharp
|
||||
TargetResult? GetTarget(CommandInfo command)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `command` - Command info containing arguments
|
||||
|
||||
**Returns:**
|
||||
- `TargetResult` - Contains matched players
|
||||
- `null` - If no targets found or error
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
[CommandHelper(1, "<#userid or name>")]
|
||||
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
foreach (var player in targets.Players)
|
||||
{
|
||||
// Do something with player
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Supported Target Syntax:**
|
||||
- `@all` - All players
|
||||
- `@ct` - All Counter-Terrorists
|
||||
- `@t` - All Terrorists
|
||||
- `@spec` - All spectators
|
||||
- `@alive` - All alive players
|
||||
- `@dead` - All dead players
|
||||
- `@bot` - All bots
|
||||
- `@human` - All human players
|
||||
- `@me` - Command caller
|
||||
- `#123` - Player by user ID
|
||||
- `PlayerName` - Player by name (partial match)
|
||||
|
||||
---
|
||||
|
||||
## Command Logging
|
||||
|
||||
### LogCommand (with CommandInfo)
|
||||
|
||||
Log a command execution with full command info.
|
||||
|
||||
```csharp
|
||||
void LogCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `caller` - Player who executed command (null for console)
|
||||
- `command` - Command info object
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
// Execute command logic
|
||||
|
||||
// Log the command
|
||||
_api!.LogCommand(caller, command);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### LogCommand (with string)
|
||||
|
||||
Log a command execution with custom command string.
|
||||
|
||||
```csharp
|
||||
void LogCommand(CCSPlayerController? caller, string command)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `caller` - Player who executed command (null for console)
|
||||
- `command` - Command string to log
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
private void DoAction(CCSPlayerController? caller, CCSPlayerController target)
|
||||
{
|
||||
// Perform action
|
||||
|
||||
// Log with custom string
|
||||
_api!.LogCommand(caller, $"css_mycommand {target.PlayerName}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
### Basic Command
|
||||
|
||||
```csharp
|
||||
private void RegisterCommands()
|
||||
{
|
||||
_api!.RegisterCommand("css_hello", "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))
|
||||
{
|
||||
player.PrintToChat($"Hello {player.PlayerName}!");
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, command);
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
_api?.UnRegisterCommand("css_hello");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Command with Permission Check
|
||||
|
||||
```csharp
|
||||
[CommandHelper(1, "<#userid or name>")]
|
||||
[RequiresPermissions("@css/ban")]
|
||||
private void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
// Get targets
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
// Filter for players caller can target
|
||||
var validPlayers = targets.Players
|
||||
.Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
|
||||
.ToList();
|
||||
|
||||
foreach (var player in validPlayers)
|
||||
{
|
||||
// Issue ban
|
||||
_api.IssuePenalty(player, caller, PenaltyType.Ban, "Banned via command", 1440);
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, command);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Command with Arguments
|
||||
|
||||
```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))
|
||||
{
|
||||
caller?.PrintToChat("Invalid HP value!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get targets
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
foreach (var player in targets.Players.Where(p => p.IsValid && p.PawnIsAlive))
|
||||
{
|
||||
player.PlayerPawn?.Value?.SetHealth(hp);
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, $"css_sethp {hp}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Validate Targets
|
||||
|
||||
```csharp
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return; // No targets found
|
||||
|
||||
// Filter for valid players
|
||||
var validPlayers = targets.Players
|
||||
.Where(p => p.IsValid && !p.IsBot)
|
||||
.ToList();
|
||||
|
||||
if (validPlayers.Count == 0)
|
||||
{
|
||||
caller?.PrintToChat("No valid targets found!");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Check Immunity
|
||||
|
||||
```csharp
|
||||
foreach (var player in targets.Players)
|
||||
{
|
||||
// Check if caller can target this player (immunity check)
|
||||
if (!caller!.CanTarget(player))
|
||||
{
|
||||
caller.PrintToChat($"You cannot target {player.PlayerName}!");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safe to target player
|
||||
DoAction(player);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Always Log Commands
|
||||
|
||||
```csharp
|
||||
// Log every admin command execution
|
||||
_api.LogCommand(caller, command);
|
||||
```
|
||||
|
||||
### 4. Use CommandHelper Attribute
|
||||
|
||||
```csharp
|
||||
// Specify minimum args and usage
|
||||
[CommandHelper(minArgs: 1, usage: "<#userid or name>")]
|
||||
[RequiresPermissions("@css/generic")]
|
||||
private void OnCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
// CounterStrikeSharp validates args automatically
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Cleanup on Unload
|
||||
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
// Unregister ALL commands
|
||||
_api.UnRegisterCommand("css_command1");
|
||||
_api.UnRegisterCommand("css_command2");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Multiple Aliases
|
||||
|
||||
```csharp
|
||||
// Register same command with multiple aliases
|
||||
var aliases = new[] { "css_mycommand", "css_mycmd", "css_mc" };
|
||||
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
_api.RegisterCommand(alias, "My command", OnMyCommand);
|
||||
}
|
||||
|
||||
// Unregister all
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
_api?.UnRegisterCommand(alias);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command from Config
|
||||
|
||||
```csharp
|
||||
// In Config.cs
|
||||
public List<string> MyCommands { get; set; } = ["css_mycommand"];
|
||||
|
||||
// In Plugin
|
||||
private void RegisterCommands()
|
||||
{
|
||||
foreach (var cmd in Config.MyCommands)
|
||||
{
|
||||
_api!.RegisterCommand(cmd, "Description", OnMyCommand);
|
||||
}
|
||||
}
|
||||
|
||||
// Allows users to add aliases or disable by clearing list
|
||||
```
|
||||
|
||||
### Target Filtering
|
||||
|
||||
```csharp
|
||||
// Get only alive players
|
||||
var alivePlayers = targets.Players
|
||||
.Where(p => p.IsValid && p.PawnIsAlive)
|
||||
.ToList();
|
||||
|
||||
// Get only enemy team
|
||||
var enemies = targets.Players
|
||||
.Where(p => p.IsValid && p.Team != caller!.Team)
|
||||
.ToList();
|
||||
|
||||
// Get targetable players
|
||||
var targetable = targets.Players
|
||||
.Where(p => p.IsValid && caller!.CanTarget(p))
|
||||
.ToList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Command Registration Errors
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
_api.RegisterCommand("css_mycommand", "Description", OnMyCommand);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Logger.LogError($"Failed to register command: {ex.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
### Target Parsing Errors
|
||||
|
||||
```csharp
|
||||
var targets = _api!.GetTarget(command);
|
||||
|
||||
if (targets == null)
|
||||
{
|
||||
// Target parsing failed
|
||||
// Error message already sent to caller by SimpleAdmin
|
||||
return;
|
||||
}
|
||||
|
||||
if (targets.Players.Count == 0)
|
||||
{
|
||||
caller?.PrintToChat("No players matched your target!");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Cache Command Lists
|
||||
|
||||
```csharp
|
||||
// Don't create new list every time
|
||||
private readonly List<string> _commandAliases = new() { "css_cmd1", "css_cmd2" };
|
||||
|
||||
private void RegisterCommands()
|
||||
{
|
||||
foreach (var cmd in _commandAliases)
|
||||
{
|
||||
_api!.RegisterCommand(cmd, "Description", OnCommand);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Efficient Target Filtering
|
||||
|
||||
```csharp
|
||||
// ✅ Good - single LINQ query
|
||||
var players = targets.Players
|
||||
.Where(p => p.IsValid && !p.IsBot && p.PawnIsAlive && caller!.CanTarget(p))
|
||||
.ToList();
|
||||
|
||||
// ❌ Bad - multiple iterations
|
||||
var players = targets.Players.Where(p => p.IsValid).ToList();
|
||||
players = players.Where(p => !p.IsBot).ToList();
|
||||
players = players.Where(p => p.PawnIsAlive).ToList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related APIs
|
||||
|
||||
- **[Menus API](menus)** - Create interactive menus
|
||||
- **[Penalties API](penalties)** - Issue penalties from commands
|
||||
- **[Utilities API](utilities)** - Helper functions for commands
|
||||
|
|
@ -1,642 +0,0 @@
|
|||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Events API
|
||||
|
||||
Complete reference for CS2-SimpleAdmin event system.
|
||||
|
||||
## Event System Overview
|
||||
|
||||
CS2-SimpleAdmin exposes events that allow modules to react to plugin actions and state changes.
|
||||
|
||||
**All events use C# event delegates and should be subscribed to in `OnAllPluginsLoaded` and unsubscribed in `Unload`.**
|
||||
|
||||
---
|
||||
|
||||
## Plugin Lifecycle Events
|
||||
|
||||
### OnSimpleAdminReady
|
||||
|
||||
Fired when CS2-SimpleAdmin is fully initialized and ready.
|
||||
|
||||
```csharp
|
||||
event Action? OnSimpleAdminReady
|
||||
```
|
||||
|
||||
**When Fired:**
|
||||
- After plugin load
|
||||
- After database initialization
|
||||
- After migrations complete
|
||||
- Menu system ready
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_api = _pluginCapability.Get();
|
||||
if (_api == null) return;
|
||||
|
||||
// Subscribe to ready event
|
||||
_api.OnSimpleAdminReady += OnSimpleAdminReady;
|
||||
|
||||
// Also call directly for hot reload case
|
||||
OnSimpleAdminReady();
|
||||
}
|
||||
|
||||
private void OnSimpleAdminReady()
|
||||
{
|
||||
Logger.LogInformation("SimpleAdmin is ready!");
|
||||
|
||||
// Register menus (requires SimpleAdmin to be ready)
|
||||
RegisterMenus();
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
_api.OnSimpleAdminReady -= OnSimpleAdminReady;
|
||||
}
|
||||
```
|
||||
|
||||
**Best Practice:**
|
||||
Always call your handler directly after subscribing to handle hot reload:
|
||||
```csharp
|
||||
_api.OnSimpleAdminReady += RegisterMenus;
|
||||
RegisterMenus(); // ← Also call directly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Penalty Events
|
||||
|
||||
### OnPlayerPenaltied
|
||||
|
||||
Fired when an **online player** receives a penalty.
|
||||
|
||||
```csharp
|
||||
event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltied
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
1. `PlayerInfo player` - Player who received penalty
|
||||
2. `PlayerInfo? admin` - Admin who issued penalty (null if console)
|
||||
3. `PenaltyType penaltyType` - Type of penalty
|
||||
4. `string reason` - Penalty reason
|
||||
5. `int duration` - Duration in minutes (0 = permanent)
|
||||
6. `int? penaltyId` - Database penalty ID (null if not stored)
|
||||
7. `int? serverId` - Server ID (null in single-server mode)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.OnPlayerPenaltied += OnPlayerPenaltied;
|
||||
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
var adminName = admin?.PlayerName ?? "Console";
|
||||
Logger.LogInformation($"{adminName} penaltied {player.PlayerName}: {type} ({duration}m) - {reason}");
|
||||
|
||||
// React to specific penalty types
|
||||
switch (type)
|
||||
{
|
||||
case PenaltyType.Ban:
|
||||
// Log ban to external system
|
||||
LogBanToWebhook(player, admin, reason, duration);
|
||||
break;
|
||||
|
||||
case PenaltyType.Warn:
|
||||
// Check warning count
|
||||
if (player.Warnings >= 3)
|
||||
{
|
||||
Logger.LogWarning($"{player.PlayerName} has {player.Warnings} warnings!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### OnPlayerPenaltiedAdded
|
||||
|
||||
Fired when a penalty is added to an **offline player** by SteamID.
|
||||
|
||||
```csharp
|
||||
event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltiedAdded
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
1. `SteamID steamId` - Target player's SteamID
|
||||
2. `PlayerInfo? admin` - Admin who issued penalty
|
||||
3. `PenaltyType penaltyType` - Type of penalty
|
||||
4. `string reason` - Penalty reason
|
||||
5. `int duration` - Duration in minutes
|
||||
6. `int? penaltyId` - Database penalty ID
|
||||
7. `int? serverId` - Server ID
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
|
||||
|
||||
private void OnPlayerPenaltiedAdded(
|
||||
SteamID steamId,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
var adminName = admin?.PlayerName ?? "Console";
|
||||
Logger.LogInformation($"Offline penalty: {adminName} -> SteamID {steamId}: {type} ({duration}m)");
|
||||
|
||||
// Log to external database or webhook
|
||||
if (type == PenaltyType.Ban)
|
||||
{
|
||||
LogOfflineBan(steamId, admin, reason, duration);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin Activity Events
|
||||
|
||||
### OnAdminShowActivity
|
||||
|
||||
Fired when an admin action is displayed to players.
|
||||
|
||||
```csharp
|
||||
event Action<string, string?, bool, object>? OnAdminShowActivity
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
1. `string messageKey` - Translation key for the message
|
||||
2. `string? callerName` - Admin name (null if console)
|
||||
3. `bool dontPublish` - If true, don't broadcast to other systems
|
||||
4. `object messageArgs` - Arguments for message formatting
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.OnAdminShowActivity += OnAdminShowActivity;
|
||||
|
||||
private void OnAdminShowActivity(
|
||||
string messageKey,
|
||||
string? callerName,
|
||||
bool dontPublish,
|
||||
object messageArgs)
|
||||
{
|
||||
if (dontPublish) return;
|
||||
|
||||
Logger.LogInformation($"Admin activity: {messageKey} by {callerName ?? "Console"}");
|
||||
|
||||
// Log to Discord, database, etc.
|
||||
LogAdminAction(messageKey, callerName, messageArgs);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### OnAdminToggleSilent
|
||||
|
||||
Fired when an admin toggles silent mode.
|
||||
|
||||
```csharp
|
||||
event Action<int, bool>? OnAdminToggleSilent
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
1. `int slot` - Player slot of admin
|
||||
2. `bool status` - New silent status (true = silent, false = normal)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.OnAdminToggleSilent += OnAdminToggleSilent;
|
||||
|
||||
private void OnAdminToggleSilent(int slot, bool status)
|
||||
{
|
||||
var player = Utilities.GetPlayerFromSlot(slot);
|
||||
if (player == null) return;
|
||||
|
||||
var statusText = status ? "enabled" : "disabled";
|
||||
Logger.LogInformation($"{player.PlayerName} {statusText} silent mode");
|
||||
|
||||
// Update UI or external systems
|
||||
UpdateAdminStatus(player, status);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Ban Logging System
|
||||
|
||||
```csharp
|
||||
public class BanLogger
|
||||
{
|
||||
private ICS2_SimpleAdminApi? _api;
|
||||
|
||||
public void Initialize(ICS2_SimpleAdminApi api)
|
||||
{
|
||||
_api = api;
|
||||
|
||||
// Subscribe to both ban events
|
||||
_api.OnPlayerPenaltied += OnPlayerPenaltied;
|
||||
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
|
||||
}
|
||||
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
if (type != PenaltyType.Ban) return;
|
||||
|
||||
// Log to file
|
||||
File.AppendAllText("bans.log",
|
||||
$"[{DateTime.Now}] {player.PlayerName} ({player.SteamId}) " +
|
||||
$"banned by {admin?.PlayerName ?? "Console"} " +
|
||||
$"for {duration} minutes: {reason}\n");
|
||||
|
||||
// Send to Discord webhook
|
||||
SendDiscordNotification(
|
||||
$"🔨 **Ban Issued**\n" +
|
||||
$"Player: {player.PlayerName}\n" +
|
||||
$"Admin: {admin?.PlayerName ?? "Console"}\n" +
|
||||
$"Duration: {FormatDuration(duration)}\n" +
|
||||
$"Reason: {reason}"
|
||||
);
|
||||
}
|
||||
|
||||
private void OnPlayerPenaltiedAdded(
|
||||
SteamID steamId,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
if (type != PenaltyType.Ban) return;
|
||||
|
||||
File.AppendAllText("bans.log",
|
||||
$"[{DateTime.Now}] Offline ban: SteamID {steamId} " +
|
||||
$"by {admin?.PlayerName ?? "Console"} " +
|
||||
$"for {duration} minutes: {reason}\n");
|
||||
}
|
||||
|
||||
private string FormatDuration(int minutes)
|
||||
{
|
||||
if (minutes == 0) return "Permanent";
|
||||
if (minutes < 60) return $"{minutes} minutes";
|
||||
if (minutes < 1440) return $"{minutes / 60} hours";
|
||||
return $"{minutes / 1440} days";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Warning Escalation System
|
||||
|
||||
```csharp
|
||||
public class WarningEscalation
|
||||
{
|
||||
private ICS2_SimpleAdminApi? _api;
|
||||
|
||||
public void Initialize(ICS2_SimpleAdminApi api)
|
||||
{
|
||||
_api = api;
|
||||
_api.OnPlayerPenaltied += OnPlayerPenaltied;
|
||||
}
|
||||
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
// Only handle warnings
|
||||
if (type != PenaltyType.Warn) return;
|
||||
|
||||
Logger.LogInformation($"{player.PlayerName} now has {player.Warnings} warnings");
|
||||
|
||||
// Auto-escalate based on warning count
|
||||
if (player.Warnings >= 3)
|
||||
{
|
||||
// 3 warnings = 1 hour ban
|
||||
_api.IssuePenalty(
|
||||
GetPlayerController(player.SteamId),
|
||||
null,
|
||||
PenaltyType.Ban,
|
||||
"Automatic: 3 warnings",
|
||||
60
|
||||
);
|
||||
}
|
||||
else if (player.Warnings >= 5)
|
||||
{
|
||||
// 5 warnings = 1 day ban
|
||||
_api.IssuePenalty(
|
||||
GetPlayerController(player.SteamId),
|
||||
null,
|
||||
PenaltyType.Ban,
|
||||
"Automatic: 5 warnings",
|
||||
1440
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Admin Activity Monitor
|
||||
|
||||
```csharp
|
||||
public class AdminMonitor
|
||||
{
|
||||
private readonly Dictionary<string, int> _adminActions = new();
|
||||
|
||||
public void Initialize(ICS2_SimpleAdminApi api)
|
||||
{
|
||||
api.OnAdminShowActivity += OnAdminShowActivity;
|
||||
}
|
||||
|
||||
private void OnAdminShowActivity(
|
||||
string messageKey,
|
||||
string? callerName,
|
||||
bool dontPublish,
|
||||
object messageArgs)
|
||||
{
|
||||
if (callerName == null) return; // Ignore console actions
|
||||
|
||||
// Track admin actions
|
||||
if (!_adminActions.ContainsKey(callerName))
|
||||
{
|
||||
_adminActions[callerName] = 0;
|
||||
}
|
||||
|
||||
_adminActions[callerName]++;
|
||||
|
||||
// Log every 10th action
|
||||
if (_adminActions[callerName] % 10 == 0)
|
||||
{
|
||||
Logger.LogInformation(
|
||||
$"{callerName} has performed {_adminActions[callerName]} admin actions"
|
||||
);
|
||||
}
|
||||
|
||||
// Alert if admin is very active
|
||||
if (_adminActions[callerName] > 100)
|
||||
{
|
||||
Logger.LogWarning($"{callerName} has performed many actions ({_adminActions[callerName]})");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Unsubscribe
|
||||
|
||||
```csharp
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_api = _pluginCapability.Get();
|
||||
if (_api == null) return;
|
||||
|
||||
// Subscribe
|
||||
_api.OnPlayerPenaltied += OnPlayerPenaltied;
|
||||
_api.OnAdminShowActivity += OnAdminShowActivity;
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
// ALWAYS unsubscribe
|
||||
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
|
||||
_api.OnAdminShowActivity -= OnAdminShowActivity;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Handle Null Admins
|
||||
|
||||
```csharp
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player,
|
||||
PlayerInfo? admin, // ← Can be null!
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
var adminName = admin?.PlayerName ?? "Console";
|
||||
// Use adminName safely
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Events for Integration
|
||||
|
||||
```csharp
|
||||
// ✅ Good - React to penalties
|
||||
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
|
||||
{
|
||||
if (type == PenaltyType.Ban)
|
||||
{
|
||||
NotifyExternalSystem(player, reason);
|
||||
}
|
||||
};
|
||||
|
||||
// ❌ Bad - Wrapping penalty methods
|
||||
// Don't wrap IssuePenalty, use events instead
|
||||
```
|
||||
|
||||
### 4. Check Event Parameters
|
||||
|
||||
```csharp
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
// Check nullable parameters
|
||||
if (penaltyId.HasValue)
|
||||
{
|
||||
Logger.LogInformation($"Penalty ID: {penaltyId.Value}");
|
||||
}
|
||||
|
||||
if (serverId.HasValue)
|
||||
{
|
||||
Logger.LogInformation($"Server ID: {serverId.Value}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. OnSimpleAdminReady Pattern
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Handles both normal load and hot reload
|
||||
_api.OnSimpleAdminReady += RegisterMenus;
|
||||
RegisterMenus();
|
||||
|
||||
// ❌ Bad - Only works on normal load
|
||||
_api.OnSimpleAdminReady += RegisterMenus;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Event-Based Statistics
|
||||
|
||||
```csharp
|
||||
public class ServerStatistics
|
||||
{
|
||||
private int _totalBans;
|
||||
private int _totalMutes;
|
||||
private int _totalWarnings;
|
||||
|
||||
public void Initialize(ICS2_SimpleAdminApi api)
|
||||
{
|
||||
api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case PenaltyType.Ban:
|
||||
_totalBans++;
|
||||
break;
|
||||
case PenaltyType.Mute:
|
||||
case PenaltyType.Gag:
|
||||
case PenaltyType.Silence:
|
||||
_totalMutes++;
|
||||
break;
|
||||
case PenaltyType.Warn:
|
||||
_totalWarnings++;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void PrintStatistics()
|
||||
{
|
||||
Logger.LogInformation($"Server Statistics:");
|
||||
Logger.LogInformation($"Total Bans: {_totalBans}");
|
||||
Logger.LogInformation($"Total Mutes: {_totalMutes}");
|
||||
Logger.LogInformation($"Total Warnings: {_totalWarnings}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Event Handling
|
||||
|
||||
```csharp
|
||||
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
|
||||
{
|
||||
// Only handle bans
|
||||
if (type != PenaltyType.Ban) return;
|
||||
|
||||
// Only handle permanent bans
|
||||
if (duration != 0) return;
|
||||
|
||||
// Only handle admin-issued bans
|
||||
if (admin == null) return;
|
||||
|
||||
// Process permanent admin bans
|
||||
NotifyImportantBan(player, admin, reason);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Async Operations in Events
|
||||
|
||||
```csharp
|
||||
// ⚠️ Be careful with async in event handlers
|
||||
_api.OnPlayerPenaltied += async (player, admin, type, reason, duration, id, sid) =>
|
||||
{
|
||||
// Don't block the game thread
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// Long-running operation
|
||||
LogToExternalDatabase(player, type, reason);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Efficient Event Handlers
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Quick processing
|
||||
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
|
||||
{
|
||||
// Quick logging
|
||||
Logger.LogInformation($"Ban: {player.PlayerName}");
|
||||
};
|
||||
|
||||
// ❌ Bad - Heavy processing
|
||||
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, sid) =>
|
||||
{
|
||||
// Don't do expensive operations synchronously
|
||||
SendEmailNotification(player); // ← This blocks the game thread!
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Event Not Firing
|
||||
|
||||
**Check:**
|
||||
1. Did you subscribe to the event?
|
||||
2. Is `_api` not null?
|
||||
3. Are you testing the right scenario?
|
||||
4. Check server console for errors
|
||||
|
||||
### Memory Leaks
|
||||
|
||||
**Always unsubscribe:**
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
// Unsubscribe ALL events
|
||||
_api.OnSimpleAdminReady -= OnReady;
|
||||
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
|
||||
// ... etc
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related APIs
|
||||
|
||||
- **[Penalties API](penalties)** - Issue penalties that trigger events
|
||||
- **[Commands API](commands)** - Commands that may trigger admin activity
|
||||
- **[Utilities API](utilities)** - Helper functions for event handlers
|
||||
|
|
@ -1,706 +0,0 @@
|
|||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Menus API
|
||||
|
||||
Complete reference for creating interactive admin menus.
|
||||
|
||||
## Menu Categories
|
||||
|
||||
### RegisterMenuCategory
|
||||
|
||||
Register a new menu category in the admin menu.
|
||||
|
||||
```csharp
|
||||
void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `categoryId` - Unique identifier for the category
|
||||
- `categoryName` - Display name shown in menu
|
||||
- `permission` - Required permission to see category (default: "@css/generic")
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.RegisterMenuCategory("mycategory", "My Custom Category", "@css/generic");
|
||||
```
|
||||
|
||||
**Best Practice:**
|
||||
Register categories in the `OnSimpleAdminReady` event handler:
|
||||
|
||||
```csharp
|
||||
_api.OnSimpleAdminReady += () =>
|
||||
{
|
||||
_api.RegisterMenuCategory("mycategory", Localizer?["category_name"] ?? "My Category");
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Menu Registration
|
||||
|
||||
### RegisterMenu (Basic)
|
||||
|
||||
Register a menu within a category.
|
||||
|
||||
```csharp
|
||||
void RegisterMenu(
|
||||
string categoryId,
|
||||
string menuId,
|
||||
string menuName,
|
||||
Func<CCSPlayerController, object> menuFactory,
|
||||
string? permission = null,
|
||||
string? commandName = null
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `categoryId` - Category to add menu to
|
||||
- `menuId` - Unique menu identifier
|
||||
- `menuName` - Display name in menu
|
||||
- `menuFactory` - Function that creates the menu
|
||||
- `permission` - Required permission (optional)
|
||||
- `commandName` - Command for permission override (optional, e.g., "css_god")
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.RegisterMenu(
|
||||
"mycategory",
|
||||
"mymenu",
|
||||
"My Menu",
|
||||
CreateMyMenu,
|
||||
"@css/generic"
|
||||
);
|
||||
|
||||
private object CreateMyMenu(CCSPlayerController player)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack("My Menu", "mycategory", player);
|
||||
// Add options...
|
||||
return menu;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### RegisterMenu (with MenuContext) ⭐ RECOMMENDED
|
||||
|
||||
Register a menu with automatic context passing - **eliminates duplication!**
|
||||
|
||||
```csharp
|
||||
void RegisterMenu(
|
||||
string categoryId,
|
||||
string menuId,
|
||||
string menuName,
|
||||
Func<CCSPlayerController, MenuContext, object> menuFactory,
|
||||
string? permission = null,
|
||||
string? commandName = null
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `categoryId` - Category to add menu to
|
||||
- `menuId` - Unique menu identifier
|
||||
- `menuName` - Display name in menu
|
||||
- `menuFactory` - Function that receives player AND context
|
||||
- `permission` - Required permission (optional)
|
||||
- `commandName` - Command for permission override (optional)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// ✅ NEW WAY - No duplication!
|
||||
_api.RegisterMenu(
|
||||
"fun",
|
||||
"god",
|
||||
"God Mode",
|
||||
CreateGodMenu,
|
||||
"@css/cheats",
|
||||
"css_god"
|
||||
);
|
||||
|
||||
private object CreateGodMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// context contains: CategoryId, MenuId, MenuTitle, Permission, CommandName
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
context, // ← Automatically uses "God Mode" and "fun"
|
||||
admin,
|
||||
player => player.IsValid && admin.CanTarget(player),
|
||||
(admin, target) => ToggleGod(admin, target)
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ OLD WAY - Had to repeat "God Mode" and "fun"
|
||||
private object CreateGodMenuOld(CCSPlayerController admin)
|
||||
{
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
"God Mode", // ← Repeated from RegisterMenu
|
||||
"fun", // ← Repeated from RegisterMenu
|
||||
admin,
|
||||
filter,
|
||||
action
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**MenuContext Properties:**
|
||||
```csharp
|
||||
public class MenuContext
|
||||
{
|
||||
public string CategoryId { get; } // e.g., "fun"
|
||||
public string MenuId { get; } // e.g., "god"
|
||||
public string MenuTitle { get; } // e.g., "God Mode"
|
||||
public string? Permission { get; } // e.g., "@css/cheats"
|
||||
public string? CommandName { get; } // e.g., "css_god"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### UnregisterMenu
|
||||
|
||||
Remove a menu from a category.
|
||||
|
||||
```csharp
|
||||
void UnregisterMenu(string categoryId, string menuId)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
_api?.UnregisterMenu("mycategory", "mymenu");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Menu Creation
|
||||
|
||||
### CreateMenuWithBack
|
||||
|
||||
Create a menu with automatic back button.
|
||||
|
||||
```csharp
|
||||
object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `title` - Menu title
|
||||
- `categoryId` - Category for back button navigation
|
||||
- `player` - Player viewing the menu
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var menu = _api!.CreateMenuWithBack("My Menu", "mycategory", admin);
|
||||
_api.AddMenuOption(menu, "Option 1", _ => DoAction1());
|
||||
_api.AddMenuOption(menu, "Option 2", _ => DoAction2());
|
||||
return menu;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CreateMenuWithBack (with Context) ⭐ RECOMMENDED
|
||||
|
||||
Create a menu using context - **no duplication!**
|
||||
|
||||
```csharp
|
||||
object CreateMenuWithBack(MenuContext context, CCSPlayerController player)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
private object CreateMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// ✅ Uses context.MenuTitle and context.CategoryId automatically
|
||||
var menu = _api!.CreateMenuWithBack(context, admin);
|
||||
|
||||
_api.AddMenuOption(menu, "Option 1", _ => DoAction1());
|
||||
return menu;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CreateMenuWithPlayers
|
||||
|
||||
Create a menu showing a list of players.
|
||||
|
||||
```csharp
|
||||
object CreateMenuWithPlayers(
|
||||
string title,
|
||||
string categoryId,
|
||||
CCSPlayerController admin,
|
||||
Func<CCSPlayerController, bool> filter,
|
||||
Action<CCSPlayerController, CCSPlayerController> onSelect
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `title` - Menu title
|
||||
- `categoryId` - Category for back button
|
||||
- `admin` - Admin viewing menu
|
||||
- `filter` - Function to filter which players to show
|
||||
- `onSelect` - Action when player is selected (admin, selectedPlayer)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
"Select Player",
|
||||
"mycategory",
|
||||
admin,
|
||||
player => player.IsValid && admin.CanTarget(player),
|
||||
(admin, target) =>
|
||||
{
|
||||
// Do something with selected player
|
||||
DoAction(admin, target);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CreateMenuWithPlayers (with Context) ⭐ RECOMMENDED
|
||||
|
||||
```csharp
|
||||
object CreateMenuWithPlayers(
|
||||
MenuContext context,
|
||||
CCSPlayerController admin,
|
||||
Func<CCSPlayerController, bool> filter,
|
||||
Action<CCSPlayerController, CCSPlayerController> onSelect
|
||||
)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
private object CreatePlayerMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
context, // ← Automatically uses correct title and category!
|
||||
admin,
|
||||
player => player.PawnIsAlive && admin.CanTarget(player),
|
||||
(admin, target) => PerformAction(admin, target)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Menu Options
|
||||
|
||||
### AddMenuOption
|
||||
|
||||
Add a clickable option to a menu.
|
||||
|
||||
```csharp
|
||||
void AddMenuOption(
|
||||
object menu,
|
||||
string name,
|
||||
Action<CCSPlayerController> action,
|
||||
bool disabled = false,
|
||||
string? permission = null
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `menu` - Menu to add option to
|
||||
- `name` - Option display text
|
||||
- `action` - Function called when selected
|
||||
- `disabled` - Whether option is disabled (default: false)
|
||||
- `permission` - Required permission to see option (optional)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var menu = _api!.CreateMenuWithBack("Actions", "mycategory", admin);
|
||||
|
||||
_api.AddMenuOption(menu, "Heal Player", _ =>
|
||||
{
|
||||
target.SetHp(100);
|
||||
});
|
||||
|
||||
_api.AddMenuOption(menu, "Admin Only Option", _ =>
|
||||
{
|
||||
// Admin action
|
||||
}, false, "@css/root");
|
||||
|
||||
return menu;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### AddSubMenu
|
||||
|
||||
Add a submenu option that opens another menu.
|
||||
|
||||
```csharp
|
||||
void AddSubMenu(
|
||||
object menu,
|
||||
string name,
|
||||
Func<CCSPlayerController, object> subMenuFactory,
|
||||
bool disabled = false,
|
||||
string? permission = null
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `menu` - Parent menu
|
||||
- `name` - Submenu option display text
|
||||
- `subMenuFactory` - Function that creates the submenu
|
||||
- `disabled` - Whether option is disabled (default: false)
|
||||
- `permission` - Required permission (optional)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var menu = _api!.CreateMenuWithBack("Main Menu", "mycategory", admin);
|
||||
|
||||
_api.AddSubMenu(menu, "Player Actions", admin =>
|
||||
{
|
||||
return CreatePlayerActionsMenu(admin);
|
||||
});
|
||||
|
||||
_api.AddSubMenu(menu, "Server Settings", admin =>
|
||||
{
|
||||
return CreateServerSettingsMenu(admin);
|
||||
}, false, "@css/root");
|
||||
|
||||
return menu;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Opening Menus
|
||||
|
||||
### OpenMenu
|
||||
|
||||
Display a menu to a player.
|
||||
|
||||
```csharp
|
||||
void OpenMenu(object menu, CCSPlayerController player)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var menu = CreateMyMenu(player);
|
||||
_api!.OpenMenu(menu, player);
|
||||
```
|
||||
|
||||
**Note:** Usually menus open automatically when selected, but this can be used for direct opening.
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Simple Player Selection Menu
|
||||
|
||||
```csharp
|
||||
private void RegisterMenus()
|
||||
{
|
||||
_api!.RegisterMenuCategory("actions", "Player Actions", "@css/generic");
|
||||
|
||||
_api.RegisterMenu(
|
||||
"actions",
|
||||
"slay",
|
||||
"Slay Player",
|
||||
CreateSlayMenu,
|
||||
"@css/slay"
|
||||
);
|
||||
}
|
||||
|
||||
private object CreateSlayMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
context,
|
||||
admin,
|
||||
player => player.PawnIsAlive && admin.CanTarget(player),
|
||||
(admin, target) =>
|
||||
{
|
||||
target.PlayerPawn?.Value?.CommitSuicide(false, true);
|
||||
admin.PrintToChat($"Slayed {target.PlayerName}");
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Nested 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}", "mycategory", 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;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Menu with Permissions
|
||||
|
||||
```csharp
|
||||
private object CreateAdminMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack(context, admin);
|
||||
|
||||
// Everyone with menu access sees this
|
||||
_api.AddMenuOption(menu, "Basic Action", _ => DoBasicAction());
|
||||
|
||||
// Only root admins see this
|
||||
_api.AddMenuOption(menu, "Dangerous Action", _ =>
|
||||
{
|
||||
DoDangerousAction();
|
||||
}, false, "@css/root");
|
||||
|
||||
// Submenu with permission
|
||||
_api.AddSubMenu(menu, "Advanced Options", admin =>
|
||||
{
|
||||
return CreateAdvancedMenu(admin);
|
||||
}, false, "@css/root");
|
||||
|
||||
return menu;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Dynamic Menu with Current State
|
||||
|
||||
```csharp
|
||||
private object CreateToggleMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack(context, admin);
|
||||
|
||||
var players = _api.GetValidPlayers()
|
||||
.Where(p => admin.CanTarget(p));
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
// Show current state in option name
|
||||
bool hasGod = GodPlayers.Contains(player.Slot);
|
||||
string status = hasGod ? "✓ ON" : "✗ OFF";
|
||||
|
||||
_api.AddMenuOption(menu, $"{player.PlayerName} ({status})", _ =>
|
||||
{
|
||||
if (hasGod)
|
||||
GodPlayers.Remove(player.Slot);
|
||||
else
|
||||
GodPlayers.Add(player.Slot);
|
||||
|
||||
// Recreate menu to show updated state
|
||||
var newMenu = CreateToggleMenu(admin, context);
|
||||
_api.OpenMenu(newMenu, admin);
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use MenuContext
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Uses context
|
||||
_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);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register in OnSimpleAdminReady
|
||||
|
||||
```csharp
|
||||
_api.OnSimpleAdminReady += RegisterMenus;
|
||||
RegisterMenus(); // Also call directly for hot reload
|
||||
|
||||
private void RegisterMenus()
|
||||
{
|
||||
if (_menusRegistered) return;
|
||||
|
||||
_api!.RegisterMenuCategory("category", "Category Name");
|
||||
_api.RegisterMenu("category", "menu", "Menu Name", CreateMenu);
|
||||
|
||||
_menusRegistered = true;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Always Unregister
|
||||
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
_api.UnregisterMenu("category", "menu");
|
||||
_api.OnSimpleAdminReady -= RegisterMenus;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Validate Player State
|
||||
|
||||
```csharp
|
||||
private object CreateMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
context,
|
||||
admin,
|
||||
player => player.IsValid && // Player exists
|
||||
!player.IsBot && // Not a bot
|
||||
player.PawnIsAlive && // Alive
|
||||
admin.CanTarget(player), // Can be targeted
|
||||
(admin, target) =>
|
||||
{
|
||||
// Extra validation before action
|
||||
if (!target.IsValid || !target.PawnIsAlive)
|
||||
return;
|
||||
|
||||
DoAction(admin, target);
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Use Translations for Menu Names
|
||||
|
||||
```csharp
|
||||
_api.RegisterMenuCategory(
|
||||
"mycategory",
|
||||
Localizer?["category_name"] ?? "Default Name",
|
||||
"@css/generic"
|
||||
);
|
||||
|
||||
_api.RegisterMenu(
|
||||
"mycategory",
|
||||
"mymenu",
|
||||
Localizer?["menu_name"] ?? "Default Menu",
|
||||
CreateMenu
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission Override
|
||||
|
||||
The `commandName` parameter allows server admins to override menu permissions via CounterStrikeSharp's admin system.
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
_api.RegisterMenu(
|
||||
"fun",
|
||||
"god",
|
||||
"God Mode",
|
||||
CreateGodMenu,
|
||||
"@css/cheats", // Default permission
|
||||
"css_god" // Command name for override
|
||||
);
|
||||
```
|
||||
|
||||
**Admin config can override:**
|
||||
```json
|
||||
{
|
||||
"css_god": ["@css/vip"]
|
||||
}
|
||||
```
|
||||
|
||||
Now VIPs will see the God Mode menu instead of requiring @css/cheats!
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Player List with Actions
|
||||
|
||||
```csharp
|
||||
private object CreatePlayerListMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack(context, admin);
|
||||
|
||||
foreach (var player in _api.GetValidPlayers())
|
||||
{
|
||||
if (!admin.CanTarget(player)) continue;
|
||||
|
||||
_api.AddSubMenu(menu, player.PlayerName, admin =>
|
||||
{
|
||||
var actionMenu = _api.CreateMenuWithBack($"Actions: {player.PlayerName}", context.CategoryId, admin);
|
||||
|
||||
_api.AddMenuOption(actionMenu, "Slay", _ => player.CommitSuicide());
|
||||
_api.AddMenuOption(actionMenu, "Kick", _ => KickPlayer(player));
|
||||
_api.AddMenuOption(actionMenu, "Ban", _ => BanPlayer(admin, player));
|
||||
|
||||
return actionMenu;
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
```
|
||||
|
||||
### Category-Based Organization
|
||||
|
||||
```csharp
|
||||
private void RegisterAllMenus()
|
||||
{
|
||||
// Player management category
|
||||
_api!.RegisterMenuCategory("players", "Player Management", "@css/generic");
|
||||
_api.RegisterMenu("players", "kick", "Kick Player", CreateKickMenu, "@css/kick");
|
||||
_api.RegisterMenu("players", "ban", "Ban Player", CreateBanMenu, "@css/ban");
|
||||
|
||||
// Server management category
|
||||
_api.RegisterMenuCategory("server", "Server Management", "@css/generic");
|
||||
_api.RegisterMenu("server", "map", "Change Map", CreateMapMenu, "@css/changemap");
|
||||
_api.RegisterMenu("server", "settings", "Settings", CreateSettingsMenu, "@css/root");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related APIs
|
||||
|
||||
- **[Commands API](commands)** - Command integration
|
||||
- **[Penalties API](penalties)** - Issue penalties from menus
|
||||
- **[Utilities API](utilities)** - Helper functions for menus
|
||||
|
|
@ -1,621 +0,0 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# API Overview
|
||||
|
||||
Complete reference for the CS2-SimpleAdmin API (ICS2_SimpleAdminApi).
|
||||
|
||||
## Introduction
|
||||
|
||||
The CS2-SimpleAdmin API is exposed via the `ICS2_SimpleAdminApi` interface, accessible through CounterStrikeSharp's capability system.
|
||||
|
||||
---
|
||||
|
||||
## Getting the API
|
||||
|
||||
### Using Capability System
|
||||
|
||||
```csharp
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Capabilities;
|
||||
using CS2_SimpleAdminApi;
|
||||
|
||||
public class YourPlugin : BasePlugin
|
||||
{
|
||||
private ICS2_SimpleAdminApi? _api;
|
||||
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
|
||||
new("simpleadmin:api");
|
||||
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_api = _pluginCapability.Get();
|
||||
|
||||
if (_api == null)
|
||||
{
|
||||
Logger.LogError("CS2-SimpleAdmin API not found!");
|
||||
Unload(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// API is ready to use
|
||||
RegisterFeatures();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Static Capability Reference
|
||||
|
||||
```csharp
|
||||
// Alternative approach
|
||||
var capability = ICS2_SimpleAdminApi.PluginCapability;
|
||||
var api = capability?.Get();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Categories
|
||||
|
||||
The API is organized into logical categories:
|
||||
|
||||
| Category | Description | Learn More |
|
||||
|----------|-------------|------------|
|
||||
| **Commands** | Register/unregister commands, parse targets | [→](commands) |
|
||||
| **Menus** | Create admin menus with player selection | [→](menus) |
|
||||
| **Penalties** | Issue bans, mutes, gags, warnings | [→](penalties) |
|
||||
| **Events** | Subscribe to plugin events | [→](events) |
|
||||
| **Utilities** | Helper functions, player info, activity messages | [→](utilities) |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Command Management
|
||||
|
||||
```csharp
|
||||
// Register command
|
||||
_api.RegisterCommand(name, description, callback);
|
||||
|
||||
// Unregister command
|
||||
_api.UnRegisterCommand(name);
|
||||
|
||||
// Parse player targets
|
||||
var targets = _api.GetTarget(command);
|
||||
|
||||
// Log command
|
||||
_api.LogCommand(caller, command);
|
||||
```
|
||||
|
||||
**[Full Documentation →](commands)**
|
||||
|
||||
---
|
||||
|
||||
### Menu System
|
||||
|
||||
```csharp
|
||||
// Register category
|
||||
_api.RegisterMenuCategory(categoryId, categoryName, permission);
|
||||
|
||||
// Register menu
|
||||
_api.RegisterMenu(categoryId, menuId, menuName, menuFactory, permission, commandName);
|
||||
|
||||
// Create menu with players
|
||||
_api.CreateMenuWithPlayers(context, admin, filter, onSelect);
|
||||
|
||||
// Create menu with back button
|
||||
_api.CreateMenuWithBack(context, admin);
|
||||
|
||||
// Add menu option
|
||||
_api.AddMenuOption(menu, name, action, disabled, permission);
|
||||
|
||||
// Add submenu
|
||||
_api.AddSubMenu(menu, name, subMenuFactory, disabled, permission);
|
||||
|
||||
// Open menu
|
||||
_api.OpenMenu(menu, player);
|
||||
|
||||
// Unregister menu
|
||||
_api.UnregisterMenu(categoryId, menuId);
|
||||
```
|
||||
|
||||
**[Full Documentation →](menus)**
|
||||
|
||||
---
|
||||
|
||||
### Penalty Management
|
||||
|
||||
```csharp
|
||||
// Issue penalty to online player
|
||||
_api.IssuePenalty(player, admin, penaltyType, reason, duration);
|
||||
|
||||
// Issue penalty by SteamID
|
||||
_api.IssuePenalty(steamId, admin, penaltyType, reason, duration);
|
||||
|
||||
// Get player info
|
||||
var playerInfo = _api.GetPlayerInfo(player);
|
||||
|
||||
// Get mute status
|
||||
var muteStatus = _api.GetPlayerMuteStatus(player);
|
||||
```
|
||||
|
||||
**Penalty Types:**
|
||||
- `PenaltyType.Ban` - Ban player
|
||||
- `PenaltyType.Kick` - Kick player
|
||||
- `PenaltyType.Gag` - Block text chat
|
||||
- `PenaltyType.Mute` - Block voice chat
|
||||
- `PenaltyType.Silence` - Block both
|
||||
- `PenaltyType.Warn` - Issue warning
|
||||
|
||||
**[Full Documentation →](penalties)**
|
||||
|
||||
---
|
||||
|
||||
### Event System
|
||||
|
||||
```csharp
|
||||
// Plugin ready event
|
||||
_api.OnSimpleAdminReady += OnReady;
|
||||
|
||||
// Player penaltied
|
||||
_api.OnPlayerPenaltied += OnPlayerPenaltied;
|
||||
|
||||
// Offline penalty added
|
||||
_api.OnPlayerPenaltiedAdded += OnPlayerPenaltiedAdded;
|
||||
|
||||
// Admin activity
|
||||
_api.OnAdminShowActivity += OnAdminActivity;
|
||||
|
||||
// Admin silent toggle
|
||||
_api.OnAdminToggleSilent += OnAdminToggleSilent;
|
||||
```
|
||||
|
||||
**[Full Documentation →](events)**
|
||||
|
||||
---
|
||||
|
||||
### Utility Functions
|
||||
|
||||
```csharp
|
||||
// Get player info with penalties
|
||||
var info = _api.GetPlayerInfo(player);
|
||||
|
||||
// Get database connection string
|
||||
var connectionString = _api.GetConnectionString();
|
||||
|
||||
// Get server address
|
||||
var serverAddress = _api.GetServerAddress();
|
||||
|
||||
// Get server ID
|
||||
var serverId = _api.GetServerId();
|
||||
|
||||
// Get valid players
|
||||
var players = _api.GetValidPlayers();
|
||||
|
||||
// Check if admin is silent
|
||||
bool isSilent = _api.IsAdminSilent(player);
|
||||
|
||||
// Get all silent admins
|
||||
var silentAdmins = _api.ListSilentAdminsSlots();
|
||||
|
||||
// Show admin activity
|
||||
_api.ShowAdminActivity(messageKey, callerName, dontPublish, args);
|
||||
|
||||
// Show admin activity with custom translation
|
||||
_api.ShowAdminActivityTranslated(translatedMessage, callerName, dontPublish);
|
||||
|
||||
// Show admin activity with module localizer (recommended)
|
||||
_api.ShowAdminActivityLocalized(moduleLocalizer, messageKey, callerName, dontPublish, args);
|
||||
```
|
||||
|
||||
**[Full Documentation →](utilities)**
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Basic Module Structure
|
||||
|
||||
```csharp
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Capabilities;
|
||||
using CS2_SimpleAdminApi;
|
||||
|
||||
namespace MyModule;
|
||||
|
||||
public class MyModule : BasePlugin
|
||||
{
|
||||
private ICS2_SimpleAdminApi? _api;
|
||||
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
|
||||
new("simpleadmin:api");
|
||||
|
||||
public override string ModuleName => "My Module";
|
||||
public override string ModuleVersion => "1.0.0";
|
||||
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_api = _pluginCapability.Get();
|
||||
if (_api == null) return;
|
||||
|
||||
// Register features
|
||||
RegisterCommands();
|
||||
|
||||
// Wait for SimpleAdmin ready
|
||||
_api.OnSimpleAdminReady += RegisterMenus;
|
||||
RegisterMenus(); // Fallback for hot reload
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
// Cleanup
|
||||
_api.UnRegisterCommand("css_mycommand");
|
||||
_api.UnregisterMenu("category", "menu");
|
||||
_api.OnSimpleAdminReady -= RegisterMenus;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Command with Target Selection
|
||||
|
||||
```csharp
|
||||
[CommandHelper(1, "<#userid or name>")]
|
||||
[RequiresPermissions("@css/generic")]
|
||||
private void OnMyCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
// Parse targets
|
||||
var targets = _api!.GetTarget(command);
|
||||
if (targets == null) return;
|
||||
|
||||
// Filter valid players
|
||||
var players = targets.Players
|
||||
.Where(p => p.IsValid && !p.IsBot && caller!.CanTarget(p))
|
||||
.ToList();
|
||||
|
||||
// Process each player
|
||||
foreach (var player in players)
|
||||
{
|
||||
DoSomethingToPlayer(caller, player);
|
||||
}
|
||||
|
||||
// Log command
|
||||
_api.LogCommand(caller, command);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Menu with Player Selection
|
||||
|
||||
```csharp
|
||||
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
// Context contains categoryId, menuId, menuName, permission, commandName
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
context, // Automatic title and category
|
||||
admin,
|
||||
player => player.IsValid && admin.CanTarget(player),
|
||||
(admin, target) =>
|
||||
{
|
||||
// Action when player selected
|
||||
PerformAction(admin, target);
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Nested Menu
|
||||
|
||||
```csharp
|
||||
private object CreatePlayerMenu(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 CreateActionMenu(admin, player);
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private object CreateActionMenu(CCSPlayerController admin, CCSPlayerController target)
|
||||
{
|
||||
var menu = _api!.CreateMenuWithBack($"Actions for {target.PlayerName}", "category", admin);
|
||||
|
||||
_api.AddMenuOption(menu, "Action 1", _ => DoAction1(admin, target));
|
||||
_api.AddMenuOption(menu, "Action 2", _ => DoAction2(admin, target));
|
||||
|
||||
return menu;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue Penalty
|
||||
|
||||
```csharp
|
||||
private void BanPlayer(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
|
||||
{
|
||||
// Issue ban
|
||||
_api!.IssuePenalty(
|
||||
target,
|
||||
admin,
|
||||
PenaltyType.Ban,
|
||||
reason,
|
||||
duration // minutes, 0 = permanent
|
||||
);
|
||||
|
||||
// Show activity
|
||||
if (admin == null || !_api.IsAdminSilent(admin))
|
||||
{
|
||||
_api.ShowAdminActivityLocalized(
|
||||
Localizer,
|
||||
"ban_message",
|
||||
admin?.PlayerName,
|
||||
false,
|
||||
target.PlayerName,
|
||||
duration
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Event Subscription
|
||||
|
||||
```csharp
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_api = _pluginCapability.Get();
|
||||
if (_api == null) return;
|
||||
|
||||
// Subscribe to events
|
||||
_api.OnPlayerPenaltied += OnPlayerPenaltied;
|
||||
}
|
||||
|
||||
private void OnPlayerPenaltied(
|
||||
PlayerInfo player,
|
||||
PlayerInfo? admin,
|
||||
PenaltyType type,
|
||||
string reason,
|
||||
int duration,
|
||||
int? penaltyId,
|
||||
int? serverId)
|
||||
{
|
||||
Logger.LogInformation($"{player.PlayerName} received {type}: {reason} ({duration} min)");
|
||||
|
||||
// React to penalty
|
||||
if (type == PenaltyType.Ban)
|
||||
{
|
||||
// Handle ban
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
_api.OnPlayerPenaltied -= OnPlayerPenaltied;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Check for Null
|
||||
|
||||
```csharp
|
||||
if (_api == null)
|
||||
{
|
||||
Logger.LogError("API not available!");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use OnSimpleAdminReady Event
|
||||
|
||||
```csharp
|
||||
_api.OnSimpleAdminReady += () =>
|
||||
{
|
||||
// Register menus only when SimpleAdmin is ready
|
||||
RegisterMenus();
|
||||
};
|
||||
|
||||
// Also call directly for hot reload case
|
||||
RegisterMenus();
|
||||
```
|
||||
|
||||
### 3. Clean Up on Unload
|
||||
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
// Unregister all commands
|
||||
_api.UnRegisterCommand("css_mycommand");
|
||||
|
||||
// Unregister all menus
|
||||
_api.UnregisterMenu("category", "menu");
|
||||
|
||||
// Unsubscribe all events
|
||||
_api.OnSimpleAdminReady -= OnReady;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Validate Player State
|
||||
|
||||
```csharp
|
||||
if (!player.IsValid || !player.PawnIsAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return; // Immunity check
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Use Per-Player Translations
|
||||
|
||||
```csharp
|
||||
// Each player sees message in their configured language
|
||||
_api.ShowAdminActivityLocalized(
|
||||
Localizer, // Your module's localizer
|
||||
"translation_key",
|
||||
caller?.PlayerName,
|
||||
false,
|
||||
args
|
||||
);
|
||||
```
|
||||
|
||||
### 6. Log All Admin Actions
|
||||
|
||||
```csharp
|
||||
_api.LogCommand(caller, command);
|
||||
// or
|
||||
_api.LogCommand(caller, $"css_mycommand {player.PlayerName}");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Versioning
|
||||
|
||||
The API uses semantic versioning:
|
||||
- **Major** - Breaking changes
|
||||
- **Minor** - New features, backwards compatible
|
||||
- **Patch** - Bug fixes
|
||||
|
||||
**Current Version:** Check [GitHub Releases](https://github.com/daffyyyy/CS2-SimpleAdmin/releases)
|
||||
|
||||
---
|
||||
|
||||
## Thread Safety
|
||||
|
||||
The API is designed for single-threaded use within the CounterStrikeSharp game thread.
|
||||
|
||||
**Do NOT:**
|
||||
- Call API methods from background threads
|
||||
- Use async/await with API calls without proper synchronization
|
||||
|
||||
**Do:**
|
||||
- Call API methods from event handlers
|
||||
- Call API methods from commands
|
||||
- Call API methods from timers
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
The API uses exceptions for critical errors:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
_api.RegisterCommand("css_cmd", "Desc", callback);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Logger.LogError($"Failed to register command: {ex.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
**Common exceptions:**
|
||||
- `ArgumentException` - Invalid arguments
|
||||
- `InvalidOperationException` - Invalid state
|
||||
- `KeyNotFoundException` - Player not found
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Efficient Player Filtering
|
||||
|
||||
```csharp
|
||||
// ✅ Good - single LINQ query
|
||||
var players = _api.GetValidPlayers()
|
||||
.Where(p => p.IsValid && admin.CanTarget(p))
|
||||
.ToList();
|
||||
|
||||
// ❌ Bad - multiple iterations
|
||||
var players = _api.GetValidPlayers();
|
||||
players = players.Where(p => p.IsValid).ToList();
|
||||
players = players.Where(p => admin.CanTarget(p)).ToList();
|
||||
```
|
||||
|
||||
### Cache Expensive Operations
|
||||
|
||||
```csharp
|
||||
// Cache menu creation if used multiple times
|
||||
private object? _cachedMenu;
|
||||
|
||||
private object GetMenu(CCSPlayerController player)
|
||||
{
|
||||
if (_cachedMenu == null)
|
||||
{
|
||||
_cachedMenu = CreateMenu(player);
|
||||
}
|
||||
return _cachedMenu;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable Detailed Logging
|
||||
|
||||
```csharp
|
||||
Logger.LogInformation("Debug: API loaded");
|
||||
Logger.LogWarning("Warning: Player not found");
|
||||
Logger.LogError("Error: Failed to execute command");
|
||||
```
|
||||
|
||||
### Check API Availability
|
||||
|
||||
```csharp
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_api = _pluginCapability.Get();
|
||||
|
||||
if (_api == null)
|
||||
{
|
||||
Logger.LogError("❌ CS2-SimpleAdmin API not found!");
|
||||
Logger.LogError("Make sure CS2-SimpleAdmin is installed and loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("✅ CS2-SimpleAdmin API loaded successfully");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Commands API](commands)** - Command registration and targeting
|
||||
- **[Menus API](menus)** - Menu system details
|
||||
- **[Penalties API](penalties)** - Penalty management
|
||||
- **[Events API](events)** - Event subscription
|
||||
- **[Utilities API](utilities)** - Helper functions
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
|
||||
- **[Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Reference implementation
|
||||
- **[Module Development Guide](../module/getting-started)** - Create modules
|
||||
|
|
@ -1,610 +0,0 @@
|
|||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Penalties API
|
||||
|
||||
Complete reference for issuing and managing player penalties.
|
||||
|
||||
## Penalty Types
|
||||
|
||||
```csharp
|
||||
public enum PenaltyType
|
||||
{
|
||||
Ban, // Ban player from server
|
||||
Kick, // Kick player from server
|
||||
Gag, // Block text chat
|
||||
Mute, // Block voice chat
|
||||
Silence, // Block both text and voice
|
||||
Warn // Issue warning
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issue Penalties
|
||||
|
||||
### IssuePenalty (Online Player)
|
||||
|
||||
Issue a penalty to a currently connected player.
|
||||
|
||||
```csharp
|
||||
void IssuePenalty(
|
||||
CCSPlayerController player,
|
||||
CCSPlayerController? admin,
|
||||
PenaltyType penaltyType,
|
||||
string reason,
|
||||
int duration = -1
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `player` - Target player controller
|
||||
- `admin` - Admin issuing penalty (null for console)
|
||||
- `penaltyType` - Type of penalty
|
||||
- `reason` - Reason for penalty
|
||||
- `duration` - Duration in minutes (0 = permanent, -1 = default)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// Ban player for 1 day
|
||||
_api!.IssuePenalty(
|
||||
player,
|
||||
admin,
|
||||
PenaltyType.Ban,
|
||||
"Cheating",
|
||||
1440 // 24 hours in minutes
|
||||
);
|
||||
|
||||
// Permanent ban
|
||||
_api.IssuePenalty(
|
||||
player,
|
||||
admin,
|
||||
PenaltyType.Ban,
|
||||
"Severe rule violation",
|
||||
0
|
||||
);
|
||||
|
||||
// Kick player
|
||||
_api.IssuePenalty(
|
||||
player,
|
||||
admin,
|
||||
PenaltyType.Kick,
|
||||
"AFK"
|
||||
);
|
||||
|
||||
// Gag for 30 minutes
|
||||
_api.IssuePenalty(
|
||||
player,
|
||||
admin,
|
||||
PenaltyType.Gag,
|
||||
"Chat spam",
|
||||
30
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### IssuePenalty (Offline Player)
|
||||
|
||||
Issue a penalty to a player by SteamID (even if offline).
|
||||
|
||||
```csharp
|
||||
void IssuePenalty(
|
||||
SteamID steamid,
|
||||
CCSPlayerController? admin,
|
||||
PenaltyType penaltyType,
|
||||
string reason,
|
||||
int duration = -1
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `steamid` - Target player's SteamID
|
||||
- `admin` - Admin issuing penalty (null for console)
|
||||
- `penaltyType` - Type of penalty
|
||||
- `reason` - Reason for penalty
|
||||
- `duration` - Duration in minutes (0 = permanent, -1 = default)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// Ban offline player
|
||||
var steamId = new SteamID(76561198012345678);
|
||||
|
||||
_api!.IssuePenalty(
|
||||
steamId,
|
||||
admin,
|
||||
PenaltyType.Ban,
|
||||
"Ban evasion",
|
||||
10080 // 7 days
|
||||
);
|
||||
|
||||
// Mute offline player
|
||||
_api.IssuePenalty(
|
||||
steamId,
|
||||
admin,
|
||||
PenaltyType.Mute,
|
||||
"Voice abuse",
|
||||
1440
|
||||
);
|
||||
```
|
||||
|
||||
**Supported SteamID Formats:**
|
||||
```csharp
|
||||
// SteamID64
|
||||
new SteamID(76561198012345678)
|
||||
|
||||
// Also works with SteamID string parsing
|
||||
SteamID.FromString("STEAM_1:0:12345678")
|
||||
SteamID.FromString("[U:1:12345678]")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get Player Information
|
||||
|
||||
### GetPlayerInfo
|
||||
|
||||
Get detailed player information including penalty counts.
|
||||
|
||||
```csharp
|
||||
PlayerInfo GetPlayerInfo(CCSPlayerController player)
|
||||
```
|
||||
|
||||
**Returns:** `PlayerInfo` object containing:
|
||||
- `PlayerName` - Player's name
|
||||
- `SteamId` - Steam ID (ulong)
|
||||
- `IpAddress` - Player's IP address
|
||||
- `Warnings` - Warning count
|
||||
- `Bans` - Ban count
|
||||
- `Mutes` - Mute count
|
||||
- `Gags` - Gag count
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var playerInfo = _api!.GetPlayerInfo(player);
|
||||
|
||||
Console.WriteLine($"Player: {playerInfo.PlayerName}");
|
||||
Console.WriteLine($"SteamID: {playerInfo.SteamId}");
|
||||
Console.WriteLine($"Warnings: {playerInfo.Warnings}");
|
||||
Console.WriteLine($"Total Bans: {playerInfo.Bans}");
|
||||
|
||||
// Check if player has penalties
|
||||
if (playerInfo.Warnings >= 3)
|
||||
{
|
||||
_api.IssuePenalty(player, null, PenaltyType.Ban, "Too many warnings", 1440);
|
||||
}
|
||||
```
|
||||
|
||||
**Throws:**
|
||||
- `KeyNotFoundException` - If player doesn't have a valid UserId
|
||||
|
||||
---
|
||||
|
||||
### GetPlayerMuteStatus
|
||||
|
||||
Get current mute/gag/silence status for a player.
|
||||
|
||||
```csharp
|
||||
Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetPlayerMuteStatus(
|
||||
CCSPlayerController player
|
||||
)
|
||||
```
|
||||
|
||||
**Returns:** Dictionary mapping penalty types to lists of active penalties
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var muteStatus = _api!.GetPlayerMuteStatus(player);
|
||||
|
||||
// Check if player is gagged
|
||||
if (muteStatus.ContainsKey(PenaltyType.Gag))
|
||||
{
|
||||
var gagPenalties = muteStatus[PenaltyType.Gag];
|
||||
|
||||
foreach (var (endTime, duration, passed) in gagPenalties)
|
||||
{
|
||||
if (!passed)
|
||||
{
|
||||
var remaining = endTime - DateTime.UtcNow;
|
||||
Console.WriteLine($"Gagged for {remaining.TotalMinutes:F0} more minutes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if player is muted
|
||||
if (muteStatus.ContainsKey(PenaltyType.Mute))
|
||||
{
|
||||
Console.WriteLine("Player is currently muted");
|
||||
}
|
||||
|
||||
// Check if player is silenced
|
||||
if (muteStatus.ContainsKey(PenaltyType.Silence))
|
||||
{
|
||||
Console.WriteLine("Player is silenced (gag + mute)");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Information
|
||||
|
||||
### GetConnectionString
|
||||
|
||||
Get the database connection string.
|
||||
|
||||
```csharp
|
||||
string GetConnectionString()
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var connectionString = _api!.GetConnectionString();
|
||||
// Use for custom database operations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GetServerAddress
|
||||
|
||||
Get the server's IP address and port.
|
||||
|
||||
```csharp
|
||||
string GetServerAddress()
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var serverAddress = _api!.GetServerAddress();
|
||||
Console.WriteLine($"Server: {serverAddress}");
|
||||
// Example output: "192.168.1.100:27015"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GetServerId
|
||||
|
||||
Get the server's unique ID in the database.
|
||||
|
||||
```csharp
|
||||
int? GetServerId()
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
- `int` - Server ID if multi-server mode enabled
|
||||
- `null` - If single-server mode
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var serverId = _api!.GetServerId();
|
||||
|
||||
if (serverId.HasValue)
|
||||
{
|
||||
Console.WriteLine($"Server ID: {serverId.Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Single-server mode");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Ban with Validation
|
||||
|
||||
```csharp
|
||||
private void BanPlayer(CCSPlayerController? admin, CCSPlayerController target, int duration, string reason)
|
||||
{
|
||||
// Validate player
|
||||
if (!target.IsValid)
|
||||
{
|
||||
admin?.PrintToChat("Invalid player!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check immunity
|
||||
if (admin != null && !admin.CanTarget(target))
|
||||
{
|
||||
admin.PrintToChat($"You cannot ban {target.PlayerName} (higher immunity)!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get player info to check history
|
||||
var playerInfo = _api!.GetPlayerInfo(target);
|
||||
|
||||
Logger.LogInformation(
|
||||
$"{admin?.PlayerName ?? "Console"} banning {playerInfo.PlayerName} " +
|
||||
$"(SteamID: {playerInfo.SteamId}, Previous bans: {playerInfo.Bans})"
|
||||
);
|
||||
|
||||
// Issue ban
|
||||
_api.IssuePenalty(target, admin, PenaltyType.Ban, reason, duration);
|
||||
|
||||
// Show activity
|
||||
if (admin == null || !_api.IsAdminSilent(admin))
|
||||
{
|
||||
var durationText = duration == 0 ? "permanently" : $"for {duration} minutes";
|
||||
Server.PrintToChatAll($"{admin?.PlayerName ?? "Console"} banned {target.PlayerName} {durationText}: {reason}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Progressive Punishment System
|
||||
|
||||
```csharp
|
||||
private void HandlePlayerOffense(CCSPlayerController? admin, CCSPlayerController target, string reason)
|
||||
{
|
||||
var playerInfo = _api!.GetPlayerInfo(target);
|
||||
|
||||
// Progressive punishment based on warning count
|
||||
if (playerInfo.Warnings == 0)
|
||||
{
|
||||
// First offense - warning
|
||||
_api.IssuePenalty(target, admin, PenaltyType.Warn, reason);
|
||||
target.PrintToChat("This is your first warning!");
|
||||
}
|
||||
else if (playerInfo.Warnings == 1)
|
||||
{
|
||||
// Second offense - gag for 30 minutes
|
||||
_api.IssuePenalty(target, admin, PenaltyType.Gag, $"Second offense: {reason}", 30);
|
||||
target.PrintToChat("Second warning! You are gagged for 30 minutes.");
|
||||
}
|
||||
else if (playerInfo.Warnings == 2)
|
||||
{
|
||||
// Third offense - 1 day ban
|
||||
_api.IssuePenalty(target, admin, PenaltyType.Ban, $"Third offense: {reason}", 1440);
|
||||
target.PrintToChat("Third offense! You are banned for 1 day.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// More than 3 warnings - permanent ban
|
||||
_api.IssuePenalty(target, admin, PenaltyType.Ban, $"Multiple offenses: {reason}", 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Check Active Penalties Before Action
|
||||
|
||||
```csharp
|
||||
private void AllowPlayerToChat(CCSPlayerController player)
|
||||
{
|
||||
var muteStatus = _api!.GetPlayerMuteStatus(player);
|
||||
|
||||
// Check if player is gagged
|
||||
if (muteStatus.ContainsKey(PenaltyType.Gag))
|
||||
{
|
||||
player.PrintToChat("You are currently gagged and cannot use chat!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player is silenced (includes gag)
|
||||
if (muteStatus.ContainsKey(PenaltyType.Silence))
|
||||
{
|
||||
player.PrintToChat("You are silenced and cannot communicate!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Player can chat
|
||||
ProcessChatMessage(player);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Offline Player Ban
|
||||
|
||||
```csharp
|
||||
private void BanOfflinePlayer(CCSPlayerController? admin, string steamIdString, int duration, string reason)
|
||||
{
|
||||
// Parse SteamID
|
||||
if (!ulong.TryParse(steamIdString, out ulong steamId64))
|
||||
{
|
||||
admin?.PrintToChat("Invalid SteamID format!");
|
||||
return;
|
||||
}
|
||||
|
||||
var steamId = new SteamID(steamId64);
|
||||
|
||||
// Issue offline ban
|
||||
_api!.IssuePenalty(steamId, admin, PenaltyType.Ban, reason, duration);
|
||||
|
||||
Logger.LogInformation(
|
||||
$"{admin?.PlayerName ?? "Console"} banned offline player " +
|
||||
$"(SteamID: {steamId64}) for {duration} minutes: {reason}"
|
||||
);
|
||||
|
||||
admin?.PrintToChat($"Offline ban issued to SteamID {steamId64}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Multi-Account Detection
|
||||
|
||||
```csharp
|
||||
[GameEventHandler]
|
||||
public HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo info)
|
||||
{
|
||||
var player = @event.Userid;
|
||||
if (player == null || !player.IsValid) return HookResult.Continue;
|
||||
|
||||
var playerInfo = _api!.GetPlayerInfo(player);
|
||||
|
||||
// Check if player has multiple accounts
|
||||
if (playerInfo.Bans > 0)
|
||||
{
|
||||
// Notify admins
|
||||
var admins = Utilities.GetPlayers()
|
||||
.Where(p => AdminManager.PlayerHasPermissions(p, "@css/ban"));
|
||||
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
admin.PrintToChat(
|
||||
$"⚠ {player.PlayerName} has {playerInfo.Bans} previous ban(s)!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Validate Players
|
||||
|
||||
```csharp
|
||||
if (!target.IsValid || !target.PawnIsAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check immunity
|
||||
if (admin != null && !admin.CanTarget(target))
|
||||
{
|
||||
admin.PrintToChat("Cannot target this player!");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Provide Clear Reasons
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Specific reason
|
||||
_api.IssuePenalty(player, admin, PenaltyType.Ban, "Aimbot detected in Round 12", 10080);
|
||||
|
||||
// ❌ Bad - Vague reason
|
||||
_api.IssuePenalty(player, admin, PenaltyType.Ban, "cheating", 10080);
|
||||
```
|
||||
|
||||
### 3. Log Penalty Actions
|
||||
|
||||
```csharp
|
||||
_api.IssuePenalty(player, admin, PenaltyType.Ban, reason, duration);
|
||||
|
||||
Logger.LogInformation(
|
||||
$"Penalty issued: {admin?.PlayerName ?? "Console"} -> {player.PlayerName} " +
|
||||
$"| Type: {PenaltyType.Ban} | Duration: {duration}m | Reason: {reason}"
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Handle Kick Separately
|
||||
|
||||
```csharp
|
||||
// Kick doesn't need duration
|
||||
_api.IssuePenalty(player, admin, PenaltyType.Kick, reason);
|
||||
|
||||
// NOT:
|
||||
_api.IssuePenalty(player, admin, PenaltyType.Kick, reason, 0);
|
||||
```
|
||||
|
||||
### 5. Check Active Penalties
|
||||
|
||||
```csharp
|
||||
// Before issuing new penalty, check existing ones
|
||||
var muteStatus = _api.GetPlayerMuteStatus(player);
|
||||
|
||||
if (muteStatus.ContainsKey(PenaltyType.Gag))
|
||||
{
|
||||
admin?.PrintToChat($"{player.PlayerName} is already gagged!");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Duration Helpers
|
||||
|
||||
```csharp
|
||||
public static class PenaltyDurations
|
||||
{
|
||||
public const int OneHour = 60;
|
||||
public const int OneDay = 1440;
|
||||
public const int OneWeek = 10080;
|
||||
public const int TwoWeeks = 20160;
|
||||
public const int OneMonth = 43200;
|
||||
public const int Permanent = 0;
|
||||
}
|
||||
|
||||
// Usage
|
||||
_api.IssuePenalty(player, admin, PenaltyType.Ban, reason, PenaltyDurations.OneWeek);
|
||||
```
|
||||
|
||||
### Penalty History Display
|
||||
|
||||
```csharp
|
||||
private void ShowPlayerHistory(CCSPlayerController admin, CCSPlayerController target)
|
||||
{
|
||||
var info = _api!.GetPlayerInfo(target);
|
||||
|
||||
admin.PrintToChat($"=== {info.PlayerName} History ===");
|
||||
admin.PrintToChat($"Warnings: {info.Warnings}");
|
||||
admin.PrintToChat($"Bans: {info.Bans}");
|
||||
admin.PrintToChat($"Mutes: {info.Mutes}");
|
||||
admin.PrintToChat($"Gags: {info.Gags}");
|
||||
|
||||
var muteStatus = _api.GetPlayerMuteStatus(target);
|
||||
|
||||
if (muteStatus.ContainsKey(PenaltyType.Gag))
|
||||
admin.PrintToChat("Currently: GAGGED");
|
||||
if (muteStatus.ContainsKey(PenaltyType.Mute))
|
||||
admin.PrintToChat("Currently: MUTED");
|
||||
if (muteStatus.ContainsKey(PenaltyType.Silence))
|
||||
admin.PrintToChat("Currently: SILENCED");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Handle Invalid Players
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
var playerInfo = _api!.GetPlayerInfo(player);
|
||||
// Use playerInfo...
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
Logger.LogError($"Player info not found for {player?.PlayerName}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### Validate SteamID
|
||||
|
||||
```csharp
|
||||
private bool TryParseSteamId(string input, out SteamID steamId)
|
||||
{
|
||||
steamId = default;
|
||||
|
||||
if (ulong.TryParse(input, out ulong steamId64))
|
||||
{
|
||||
steamId = new SteamID(steamId64);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related APIs
|
||||
|
||||
- **[Commands API](commands)** - Issue penalties from commands
|
||||
- **[Menus API](menus)** - Issue penalties from menus
|
||||
- **[Events API](events)** - React to penalty events
|
||||
- **[Utilities API](utilities)** - Helper functions
|
||||
|
|
@ -1,585 +0,0 @@
|
|||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Utilities API
|
||||
|
||||
Helper functions and utility methods for module development.
|
||||
|
||||
## Player Management
|
||||
|
||||
### GetValidPlayers
|
||||
|
||||
Get a list of all valid, connected players.
|
||||
|
||||
```csharp
|
||||
List<CCSPlayerController> GetValidPlayers()
|
||||
```
|
||||
|
||||
**Returns:** List of valid player controllers
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var players = _api!.GetValidPlayers();
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
Console.WriteLine($"Player: {player.PlayerName}");
|
||||
}
|
||||
|
||||
// Filter for specific criteria
|
||||
var alivePlayers = _api.GetValidPlayers()
|
||||
.Where(p => p.PawnIsAlive)
|
||||
.ToList();
|
||||
|
||||
var ctPlayers = _api.GetValidPlayers()
|
||||
.Where(p => p.Team == CsTeam.CounterTerrorist)
|
||||
.ToList();
|
||||
```
|
||||
|
||||
**Note:** This method filters out invalid and bot players automatically.
|
||||
|
||||
---
|
||||
|
||||
## Admin Status
|
||||
|
||||
### IsAdminSilent
|
||||
|
||||
Check if an admin is in silent mode.
|
||||
|
||||
```csharp
|
||||
bool IsAdminSilent(CCSPlayerController player)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `player` - Player to check
|
||||
|
||||
**Returns:** `true` if player is in silent mode, `false` otherwise
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
private void PerformAdminAction(CCSPlayerController admin, CCSPlayerController target)
|
||||
{
|
||||
// Do the action
|
||||
DoAction(target);
|
||||
|
||||
// Only show activity if not silent
|
||||
if (!_api!.IsAdminSilent(admin))
|
||||
{
|
||||
Server.PrintToChatAll($"{admin.PlayerName} performed action on {target.PlayerName}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ListSilentAdminsSlots
|
||||
|
||||
Get a list of player slots for all admins currently in silent mode.
|
||||
|
||||
```csharp
|
||||
HashSet<int> ListSilentAdminsSlots()
|
||||
```
|
||||
|
||||
**Returns:** HashSet of player slots
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var silentAdmins = _api!.ListSilentAdminsSlots();
|
||||
|
||||
Console.WriteLine($"Silent admins: {silentAdmins.Count}");
|
||||
|
||||
foreach (var slot in silentAdmins)
|
||||
{
|
||||
var player = Utilities.GetPlayerFromSlot(slot);
|
||||
if (player != null)
|
||||
{
|
||||
Console.WriteLine($"- {player.PlayerName} (slot {slot})");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activity Messages
|
||||
|
||||
### ShowAdminActivity
|
||||
|
||||
Show an admin activity message to all players.
|
||||
|
||||
```csharp
|
||||
void ShowAdminActivity(
|
||||
string messageKey,
|
||||
string? callerName = null,
|
||||
bool dontPublish = false,
|
||||
params object[] messageArgs
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `messageKey` - Translation key from SimpleAdmin's lang files
|
||||
- `callerName` - Admin name (null for console)
|
||||
- `dontPublish` - If true, don't trigger OnAdminShowActivity event
|
||||
- `messageArgs` - Arguments for message formatting
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// Using SimpleAdmin's built-in translations
|
||||
_api!.ShowAdminActivity(
|
||||
"sa_admin_player_kick_message", // Translation key
|
||||
admin?.PlayerName,
|
||||
false,
|
||||
player.PlayerName,
|
||||
reason
|
||||
);
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- Only works with SimpleAdmin's own translation keys
|
||||
- For module-specific messages, use `ShowAdminActivityLocalized`
|
||||
|
||||
---
|
||||
|
||||
### ShowAdminActivityTranslated
|
||||
|
||||
Show a pre-translated admin activity message.
|
||||
|
||||
```csharp
|
||||
void ShowAdminActivityTranslated(
|
||||
string translatedMessage,
|
||||
string? callerName = null,
|
||||
bool dontPublish = false
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `translatedMessage` - Already translated message
|
||||
- `callerName` - Admin name
|
||||
- `dontPublish` - If true, don't trigger event
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// Use when you've already translated the message
|
||||
var message = Localizer?["my_action_message", player.PlayerName] ?? $"Action on {player.PlayerName}";
|
||||
|
||||
_api!.ShowAdminActivityTranslated(
|
||||
message,
|
||||
admin?.PlayerName,
|
||||
false
|
||||
);
|
||||
```
|
||||
|
||||
**Use Case:**
|
||||
- When you need custom message formatting
|
||||
- When translation is already done
|
||||
|
||||
---
|
||||
|
||||
### ShowAdminActivityLocalized ⭐ RECOMMENDED
|
||||
|
||||
Show admin activity with per-player language support using module's localizer.
|
||||
|
||||
```csharp
|
||||
void ShowAdminActivityLocalized(
|
||||
object moduleLocalizer,
|
||||
string messageKey,
|
||||
string? callerName = null,
|
||||
bool dontPublish = false,
|
||||
params object[] messageArgs
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `moduleLocalizer` - Your module's `IStringLocalizer` instance
|
||||
- `messageKey` - Translation key from your module's lang files
|
||||
- `callerName` - Admin name
|
||||
- `dontPublish` - If true, don't trigger event
|
||||
- `messageArgs` - Message arguments
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
// Each player sees message in their configured language!
|
||||
if (Localizer != null)
|
||||
{
|
||||
_api!.ShowAdminActivityLocalized(
|
||||
Localizer, // Your module's localizer
|
||||
"fun_admin_god_message", // From your lang/en.json
|
||||
admin?.PlayerName,
|
||||
false,
|
||||
player.PlayerName
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**lang/en.json:**
|
||||
```json
|
||||
{
|
||||
"fun_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!"
|
||||
}
|
||||
```
|
||||
|
||||
**Why This is Best:**
|
||||
- ✅ Each player sees message in their own language
|
||||
- ✅ Uses your module's translations
|
||||
- ✅ Supports color codes
|
||||
- ✅ Per-player localization
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Action with Activity Message
|
||||
|
||||
```csharp
|
||||
private void ToggleGodMode(CCSPlayerController? admin, CCSPlayerController target)
|
||||
{
|
||||
// Perform action
|
||||
if (GodPlayers.Contains(target.Slot))
|
||||
{
|
||||
GodPlayers.Remove(target.Slot);
|
||||
}
|
||||
else
|
||||
{
|
||||
GodPlayers.Add(target.Slot);
|
||||
}
|
||||
|
||||
// Show activity (respecting silent mode)
|
||||
if (admin == null || !_api!.IsAdminSilent(admin))
|
||||
{
|
||||
if (Localizer != null)
|
||||
{
|
||||
_api!.ShowAdminActivityLocalized(
|
||||
Localizer,
|
||||
"fun_admin_god_message",
|
||||
admin?.PlayerName,
|
||||
false,
|
||||
target.PlayerName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Log action
|
||||
_api!.LogCommand(admin, $"css_god {target.PlayerName}");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Broadcast to Non-Silent Admins
|
||||
|
||||
```csharp
|
||||
private void NotifyAdmins(string message)
|
||||
{
|
||||
var silentAdmins = _api!.ListSilentAdminsSlots();
|
||||
|
||||
var players = _api.GetValidPlayers();
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
// Check if player is admin
|
||||
if (!AdminManager.PlayerHasPermissions(player, "@css/generic"))
|
||||
continue;
|
||||
|
||||
// Skip if admin is in silent mode
|
||||
if (silentAdmins.Contains(player.Slot))
|
||||
continue;
|
||||
|
||||
player.PrintToChat(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Filter Players by Criteria
|
||||
|
||||
```csharp
|
||||
private List<CCSPlayerController> GetTargetablePlayers(CCSPlayerController admin)
|
||||
{
|
||||
return _api!.GetValidPlayers()
|
||||
.Where(p =>
|
||||
p.IsValid &&
|
||||
!p.IsBot &&
|
||||
p.PawnIsAlive &&
|
||||
admin.CanTarget(p))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<CCSPlayerController> GetAliveEnemies(CCSPlayerController player)
|
||||
{
|
||||
return _api!.GetValidPlayers()
|
||||
.Where(p =>
|
||||
p.Team != player.Team &&
|
||||
p.PawnIsAlive)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<CCSPlayerController> GetAdmins()
|
||||
{
|
||||
return _api!.GetValidPlayers()
|
||||
.Where(p => AdminManager.PlayerHasPermissions(p, "@css/generic"))
|
||||
.ToList();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use ShowAdminActivityLocalized
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Per-player language
|
||||
_api.ShowAdminActivityLocalized(
|
||||
Localizer,
|
||||
"my_message_key",
|
||||
admin?.PlayerName,
|
||||
false,
|
||||
args
|
||||
);
|
||||
|
||||
// ❌ Bad - Single language for all
|
||||
Server.PrintToChatAll($"{admin?.PlayerName} did something");
|
||||
```
|
||||
|
||||
### 2. Respect Silent Mode
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Check silent mode
|
||||
if (admin == null || !_api.IsAdminSilent(admin))
|
||||
{
|
||||
ShowActivity();
|
||||
}
|
||||
|
||||
// ❌ Bad - Always show activity
|
||||
ShowActivity(); // Ignores silent mode!
|
||||
```
|
||||
|
||||
### 3. Validate Players from GetValidPlayers
|
||||
|
||||
```csharp
|
||||
var players = _api.GetValidPlayers();
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
// Still good to check, especially for async operations
|
||||
if (!player.IsValid) continue;
|
||||
|
||||
DoSomething(player);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Cache Silent Admin List if Checking Multiple Times
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Cache for multiple checks
|
||||
var silentAdmins = _api.ListSilentAdminsSlots();
|
||||
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
if (silentAdmins.Contains(admin.Slot)) continue;
|
||||
NotifyAdmin(admin);
|
||||
}
|
||||
|
||||
// ❌ Bad - Query for each admin
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
if (_api.IsAdminSilent(admin)) continue; // ← Repeated calls
|
||||
NotifyAdmin(admin);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Silent Mode Wrapper
|
||||
|
||||
```csharp
|
||||
private void ShowActivityIfNotSilent(
|
||||
CCSPlayerController? admin,
|
||||
string messageKey,
|
||||
params object[] args)
|
||||
{
|
||||
if (admin != null && _api!.IsAdminSilent(admin))
|
||||
return;
|
||||
|
||||
if (Localizer != null)
|
||||
{
|
||||
_api!.ShowAdminActivityLocalized(
|
||||
Localizer,
|
||||
messageKey,
|
||||
admin?.PlayerName,
|
||||
false,
|
||||
args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
ShowActivityIfNotSilent(admin, "my_action", player.PlayerName);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Get Online Admins
|
||||
|
||||
```csharp
|
||||
private List<CCSPlayerController> GetOnlineAdmins(string permission = "@css/generic")
|
||||
{
|
||||
return _api!.GetValidPlayers()
|
||||
.Where(p => AdminManager.PlayerHasPermissions(p, permission))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Usage
|
||||
var admins = GetOnlineAdmins("@css/root");
|
||||
foreach (var admin in admins)
|
||||
{
|
||||
admin.PrintToChat("Important admin message");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Notify All Players Except Silent Admins
|
||||
|
||||
```csharp
|
||||
private void BroadcastMessage(string message, bool excludeSilentAdmins = true)
|
||||
{
|
||||
var silentAdmins = excludeSilentAdmins
|
||||
? _api!.ListSilentAdminsSlots()
|
||||
: new HashSet<int>();
|
||||
|
||||
foreach (var player in _api.GetValidPlayers())
|
||||
{
|
||||
if (silentAdmins.Contains(player.Slot))
|
||||
continue;
|
||||
|
||||
player.PrintToChat(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activity Message Formatting
|
||||
|
||||
### Color Codes in Messages
|
||||
|
||||
All activity messages support color codes:
|
||||
|
||||
```json
|
||||
{
|
||||
"my_message": "{lightred}Admin{default} banned {lightred}{0}{default} for {yellow}{1}{default}"
|
||||
}
|
||||
```
|
||||
|
||||
**Available Colors:**
|
||||
- `{default}` - Default color
|
||||
- `{white}` - White
|
||||
- `{darkred}` - Dark red
|
||||
- `{green}` - Green
|
||||
- `{lightyellow}` - Light yellow
|
||||
- `{lightblue}` - Light blue
|
||||
- `{olive}` - Olive
|
||||
- `{lime}` - Lime
|
||||
- `{red}` - Red
|
||||
- `{purple}` - Purple
|
||||
- `{grey}` - Grey
|
||||
- `{yellow}` - Yellow
|
||||
- `{gold}` - Gold
|
||||
- `{silver}` - Silver
|
||||
- `{blue}` - Blue
|
||||
- `{darkblue}` - Dark blue
|
||||
- `{bluegrey}` - Blue grey
|
||||
- `{magenta}` - Magenta
|
||||
- `{lightred}` - Light red
|
||||
- `{orange}` - Orange
|
||||
|
||||
---
|
||||
|
||||
### Message Arguments
|
||||
|
||||
```csharp
|
||||
// lang/en.json
|
||||
{
|
||||
"ban_message": "{lightred}{0}{default} banned {lightred}{1}{default} for {yellow}{2}{default} minutes: {red}{3}"
|
||||
}
|
||||
|
||||
// Code
|
||||
_api.ShowAdminActivityLocalized(
|
||||
Localizer,
|
||||
"ban_message",
|
||||
admin?.PlayerName,
|
||||
false,
|
||||
admin?.PlayerName, // {0}
|
||||
target.PlayerName, // {1}
|
||||
duration, // {2}
|
||||
reason // {3}
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Minimize GetValidPlayers Calls
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Call once, filter multiple times
|
||||
var allPlayers = _api.GetValidPlayers();
|
||||
var alivePlayers = allPlayers.Where(p => p.PawnIsAlive).ToList();
|
||||
var deadPlayers = allPlayers.Where(p => !p.PawnIsAlive).ToList();
|
||||
|
||||
// ❌ Bad - Multiple calls
|
||||
var alivePlayers = _api.GetValidPlayers().Where(p => p.PawnIsAlive).ToList();
|
||||
var deadPlayers = _api.GetValidPlayers().Where(p => !p.PawnIsAlive).ToList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Efficient Filtering
|
||||
|
||||
```csharp
|
||||
// ✅ Good - Single LINQ query
|
||||
var targets = _api.GetValidPlayers()
|
||||
.Where(p => p.Team == CsTeam.Terrorist &&
|
||||
p.PawnIsAlive &&
|
||||
admin.CanTarget(p))
|
||||
.ToList();
|
||||
|
||||
// ❌ Bad - Multiple iterations
|
||||
var players = _api.GetValidPlayers();
|
||||
players = players.Where(p => p.Team == CsTeam.Terrorist).ToList();
|
||||
players = players.Where(p => p.PawnIsAlive).ToList();
|
||||
players = players.Where(p => admin.CanTarget(p)).ToList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Activity Messages Not Showing
|
||||
|
||||
**Check:**
|
||||
1. Is `Localizer` not null?
|
||||
2. Does translation key exist in lang files?
|
||||
3. Is message correctly formatted?
|
||||
4. Check `dontPublish` parameter
|
||||
|
||||
### Silent Mode Not Working
|
||||
|
||||
**Check:**
|
||||
1. Is player actually in silent mode? (`css_hide` command)
|
||||
2. Are you checking before showing activity?
|
||||
3. Check slot vs player controller mismatch
|
||||
|
||||
---
|
||||
|
||||
## Related APIs
|
||||
|
||||
- **[Commands API](commands)** - Log commands
|
||||
- **[Menus API](menus)** - Get players for menus
|
||||
- **[Events API](events)** - Admin activity events
|
||||
- **[Penalties API](penalties)** - Get player info
|
||||
|
|
@ -1,695 +0,0 @@
|
|||
---
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
# 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:**
|
||||
```csharp
|
||||
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:**
|
||||
```csharp
|
||||
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:**
|
||||
```csharp
|
||||
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:**
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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):**
|
||||
|
||||
```csharp
|
||||
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):**
|
||||
|
||||
```csharp
|
||||
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`
|
||||
|
||||
```csharp
|
||||
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:**
|
||||
|
||||
```csharp
|
||||
// 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:**
|
||||
|
||||
```csharp
|
||||
// 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:**
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
// 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`:
|
||||
|
||||
```csharp
|
||||
public async Task BanPlayer(...)
|
||||
{
|
||||
await _database.ExecuteAsync(query, parameters);
|
||||
}
|
||||
```
|
||||
|
||||
### Semaphore for Rate Limiting
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
private readonly ConcurrentDictionary<ulong, PlayerInfo> PlayersInfo = new();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Management
|
||||
|
||||
### In-Memory Caches
|
||||
|
||||
**AdminCache:**
|
||||
```csharp
|
||||
Dictionary<ulong, (List<string> Flags, int Immunity, DateTime Expiry)> AdminCache
|
||||
```
|
||||
|
||||
**BanCache:**
|
||||
```csharp
|
||||
Dictionary<ulong, BanInfo> _banCacheBySteamId
|
||||
Dictionary<string, List<BanInfo>> _banCacheByIp
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Reduces database load
|
||||
- O(1) lookups
|
||||
- TTL-based expiry
|
||||
|
||||
### Cleanup
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
// 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:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- **[API Overview](api/overview)** - Public API details
|
||||
- **[Module Development](module/getting-started)** - Create modules
|
||||
- **[GitHub Source](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Browse code
|
||||
|
|
@ -1,379 +0,0 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Developer Introduction
|
||||
|
||||
Welcome to the CS2-SimpleAdmin developer documentation!
|
||||
|
||||
## Overview
|
||||
|
||||
This section contains technical documentation for developers who want to:
|
||||
|
||||
- Create modules using the CS2-SimpleAdmin API
|
||||
- Contribute to the core plugin
|
||||
- Integrate with CS2-SimpleAdmin from other plugins
|
||||
- Understand the plugin architecture
|
||||
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
|
||||
The CS2-SimpleAdmin API provides a rich set of features for module developers:
|
||||
|
||||
### Core Features
|
||||
|
||||
- **[Commands](api/commands)** - Register and manage commands
|
||||
- **[Menus](api/menus)** - Create admin menus with player selection
|
||||
- **[Penalties](api/penalties)** - Issue bans, mutes, gags, warnings
|
||||
- **[Events](api/events)** - Subscribe to plugin events
|
||||
- **[Utilities](api/utilities)** - Helper functions and player management
|
||||
|
||||
---
|
||||
|
||||
## Quick Links
|
||||
|
||||
### For Module Developers
|
||||
|
||||
- **[Module Development Guide](module/getting-started)** - Start creating modules
|
||||
- **[Best Practices](module/best-practices)** - Write better code
|
||||
- **[Examples](module/examples)** - Code examples and patterns
|
||||
|
||||
### For Core Contributors
|
||||
|
||||
- **[Architecture](architecture)** - Plugin structure and design
|
||||
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- C# knowledge (intermediate level)
|
||||
- .NET 8.0 SDK
|
||||
- CounterStrikeSharp understanding
|
||||
- CS2 dedicated server for testing
|
||||
|
||||
### Development Environment
|
||||
|
||||
**Recommended:**
|
||||
- Visual Studio 2022 (Community or higher)
|
||||
- VS Code with C# extension
|
||||
- Git for version control
|
||||
|
||||
---
|
||||
|
||||
## CS2-SimpleAdminApi Interface
|
||||
|
||||
The main API interface provides all functionality:
|
||||
|
||||
```csharp
|
||||
using CounterStrikeSharp.API.Core.Capabilities;
|
||||
using CS2_SimpleAdminApi;
|
||||
|
||||
// Get the API
|
||||
private ICS2_SimpleAdminApi? _api;
|
||||
private readonly PluginCapability<ICS2_SimpleAdminApi> _pluginCapability =
|
||||
new("simpleadmin:api");
|
||||
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
_api = _pluginCapability.Get();
|
||||
|
||||
if (_api == null)
|
||||
{
|
||||
Logger.LogError("CS2-SimpleAdmin API not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the API
|
||||
_api.RegisterCommand("css_mycommand", "Description", OnMyCommand);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Categories
|
||||
|
||||
### Command Management
|
||||
|
||||
Register custom commands that integrate with CS2-SimpleAdmin:
|
||||
|
||||
```csharp
|
||||
_api.RegisterCommand("css_mycommand", "Description", callback);
|
||||
_api.UnRegisterCommand("css_mycommand");
|
||||
_api.GetTarget(command); // Parse player targets
|
||||
```
|
||||
|
||||
**[Learn more →](api/commands)**
|
||||
|
||||
---
|
||||
|
||||
### Menu System
|
||||
|
||||
Create interactive menus with automatic back button handling:
|
||||
|
||||
```csharp
|
||||
// Register category
|
||||
_api.RegisterMenuCategory("mycategory", "My Category", "@css/generic");
|
||||
|
||||
// Register menu
|
||||
_api.RegisterMenu("mycategory", "mymenu", "My Menu", CreateMenu, "@css/generic");
|
||||
|
||||
// Create menu with players
|
||||
_api.CreateMenuWithPlayers(context, admin, filter, onSelect);
|
||||
```
|
||||
|
||||
**[Learn more →](api/menus)**
|
||||
|
||||
---
|
||||
|
||||
### Penalty System
|
||||
|
||||
Issue and manage player penalties:
|
||||
|
||||
```csharp
|
||||
// Ban player
|
||||
_api.IssuePenalty(player, admin, PenaltyType.Ban, "Reason", 1440);
|
||||
|
||||
// Offline ban
|
||||
_api.IssuePenalty(steamId, admin, PenaltyType.Ban, "Reason", 0);
|
||||
|
||||
// Check penalties
|
||||
var status = _api.GetPlayerMuteStatus(player);
|
||||
```
|
||||
|
||||
**[Learn more →](api/penalties)**
|
||||
|
||||
---
|
||||
|
||||
### Event System
|
||||
|
||||
React to plugin events:
|
||||
|
||||
```csharp
|
||||
_api.OnSimpleAdminReady += () => { /* Plugin ready */ };
|
||||
_api.OnPlayerPenaltied += (player, admin, type, reason, duration, id, serverId) =>
|
||||
{
|
||||
// Player received penalty
|
||||
};
|
||||
```
|
||||
|
||||
**[Learn more →](api/events)**
|
||||
|
||||
---
|
||||
|
||||
### Utility Functions
|
||||
|
||||
Helper functions for common tasks:
|
||||
|
||||
```csharp
|
||||
// Get player info
|
||||
var playerInfo = _api.GetPlayerInfo(player);
|
||||
|
||||
// Get valid players
|
||||
var players = _api.GetValidPlayers();
|
||||
|
||||
// Check admin status
|
||||
if (_api.IsAdminSilent(admin)) { /* ... */ }
|
||||
|
||||
// Show admin activity
|
||||
_api.ShowAdminActivity("message_key", callerName, false, args);
|
||||
```
|
||||
|
||||
**[Learn more →](api/utilities)**
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Simple Command
|
||||
|
||||
```csharp
|
||||
[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 target in targets.Players.Where(p => p.IsValid && caller!.CanTarget(p)))
|
||||
{
|
||||
// Do something with target
|
||||
}
|
||||
|
||||
_api.LogCommand(caller, command);
|
||||
}
|
||||
```
|
||||
|
||||
### Simple Menu
|
||||
|
||||
```csharp
|
||||
private object CreateMyMenu(CCSPlayerController admin, MenuContext context)
|
||||
{
|
||||
return _api!.CreateMenuWithPlayers(
|
||||
context,
|
||||
admin,
|
||||
player => player.IsValid && admin.CanTarget(player),
|
||||
(admin, target) => DoAction(admin, target)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Error Handling
|
||||
|
||||
```csharp
|
||||
if (_api == null)
|
||||
{
|
||||
Logger.LogError("API not available!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.IsValid || !player.PawnIsAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Cleanup
|
||||
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
// Unregister commands
|
||||
_api.UnRegisterCommand("css_mycommand");
|
||||
|
||||
// Unregister menus
|
||||
_api.UnregisterMenu("mycategory", "mymenu");
|
||||
|
||||
// Unsubscribe events
|
||||
_api.OnSimpleAdminReady -= OnReady;
|
||||
}
|
||||
```
|
||||
|
||||
### Translations
|
||||
|
||||
```csharp
|
||||
// Use per-player language support
|
||||
_api.ShowAdminActivityLocalized(
|
||||
Localizer,
|
||||
"translation_key",
|
||||
caller?.PlayerName,
|
||||
false,
|
||||
args
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
The **Fun Commands Module** serves as a complete reference implementation demonstrating all API features:
|
||||
|
||||
- Command registration from config
|
||||
- Menu creation with context
|
||||
- Per-player translations
|
||||
- Proper cleanup
|
||||
- Code organization
|
||||
|
||||
**[View Source Code](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)**
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
CS2-SimpleAdmin follows a layered architecture:
|
||||
|
||||
**Layers:**
|
||||
1. **CounterStrikeSharp Integration** - Game event handling
|
||||
2. **Manager Layer** - Business logic (Bans, Mutes, Permissions)
|
||||
3. **Database Layer** - MySQL/SQLite with migrations
|
||||
4. **Menu System** - MenuManager with factory pattern
|
||||
5. **Command System** - Dynamic registration
|
||||
6. **Public API** - ICS2_SimpleAdminApi interface
|
||||
|
||||
**[Learn more →](architecture)**
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
### Ways to Contribute
|
||||
|
||||
1. **Report Bugs** - [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
|
||||
2. **Suggest Features** - [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
|
||||
3. **Submit Pull Requests** - Code contributions
|
||||
4. **Create Modules** - Extend functionality
|
||||
5. **Improve Documentation** - Help others learn
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Fork the repository
|
||||
2. Create feature branch
|
||||
3. Make changes
|
||||
4. Test thoroughly
|
||||
5. Submit pull request
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
### Documentation
|
||||
|
||||
- **[API Reference](api/overview)** - Complete API documentation
|
||||
- **[Module Development](module/getting-started)** - Create modules
|
||||
- **[Architecture](architecture)** - Plugin design
|
||||
|
||||
### External Resources
|
||||
|
||||
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS framework
|
||||
- **[CS2 Docs](https://developer.valvesoftware.com/wiki/Counter-Strike_2)** - Game documentation
|
||||
|
||||
### Community
|
||||
|
||||
- **[GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
|
||||
- **[Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)** - Bug reports
|
||||
- **[Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)** - Questions and ideas
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
### Getting Help
|
||||
|
||||
1. **Check Documentation** - Most questions answered here
|
||||
2. **Search Issues** - Someone may have had same problem
|
||||
3. **Ask in Discussions** - Community help
|
||||
4. **Create Issue** - For bugs or feature requests
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
Include:
|
||||
- CS2-SimpleAdmin version
|
||||
- CounterStrikeSharp version
|
||||
- Error messages
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### For New Developers
|
||||
|
||||
1. **[Read API Overview](api/overview)** - Understand available features
|
||||
2. **[Study Examples](module/examples)** - Learn from code
|
||||
3. **[Create First Module](module/getting-started)** - Get hands-on
|
||||
|
||||
### For Advanced Developers
|
||||
|
||||
1. **[Read Architecture](architecture)** - Deep dive into structure
|
||||
2. **[Review Source Code](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Understand implementation
|
||||
3. **[Contribute](https://github.com/daffyyyy/CS2-SimpleAdmin/pulls)** - Help improve the plugin
|
||||
|
|
@ -1,540 +0,0 @@
|
|||
---
|
||||
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
|
||||
|
|
@ -1,552 +0,0 @@
|
|||
---
|
||||
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
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
---
|
||||
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
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Tutorial Intro
|
||||
|
||||
Let's discover **Docusaurus in less than 5 minutes**.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Get started by **creating a new site**.
|
||||
|
||||
Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**.
|
||||
|
||||
### What you'll need
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) version 20.0 or above:
|
||||
- When installing Node.js, you are recommended to check all checkboxes related to dependencies.
|
||||
|
||||
## Generate a new site
|
||||
|
||||
Generate a new Docusaurus site using the **classic template**.
|
||||
|
||||
The classic template will automatically be added to your project after you run the command:
|
||||
|
||||
```bash
|
||||
npm init docusaurus@latest my-website classic
|
||||
```
|
||||
|
||||
You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.
|
||||
|
||||
The command also installs all necessary dependencies you need to run Docusaurus.
|
||||
|
||||
## Start your site
|
||||
|
||||
Run the development server:
|
||||
|
||||
```bash
|
||||
cd my-website
|
||||
npm run start
|
||||
```
|
||||
|
||||
The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there.
|
||||
|
||||
The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/.
|
||||
|
||||
Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes.
|
||||
|
|
@ -1,799 +0,0 @@
|
|||
---
|
||||
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](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** 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
|
||||
|
||||
```bash
|
||||
dotnet new classlib -n YourModuleName -f net8.0
|
||||
cd YourModuleName
|
||||
```
|
||||
|
||||
### 2. Add References
|
||||
|
||||
Edit your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
### Recommended File Organization
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```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;
|
||||
|
||||
[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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
if (_api == null) return;
|
||||
|
||||
foreach (var cmd in Config.MyCommands)
|
||||
{
|
||||
_api.UnRegisterCommand(cmd);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating Menus
|
||||
|
||||
### Register Menu Category
|
||||
|
||||
```csharp
|
||||
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)
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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`:
|
||||
|
||||
```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
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
if (_api == null)
|
||||
{
|
||||
Logger.LogError("API not available!");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Validate Player State
|
||||
|
||||
```csharp
|
||||
if (!player.IsValid || !player.PawnIsAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Check Target Permissions
|
||||
|
||||
```csharp
|
||||
if (!caller.CanTarget(target))
|
||||
{
|
||||
// caller can't target this player (immunity)
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Log Commands
|
||||
|
||||
```csharp
|
||||
_api.LogCommand(caller, command);
|
||||
// or
|
||||
_api.LogCommand(caller, $"css_mycommand {player.PlayerName}");
|
||||
```
|
||||
|
||||
### 5. Use Per-Player Translations
|
||||
|
||||
```csharp
|
||||
// Each player sees message in their language!
|
||||
_api.ShowAdminActivityLocalized(
|
||||
Localizer,
|
||||
"translation_key",
|
||||
callerName,
|
||||
false,
|
||||
args
|
||||
);
|
||||
```
|
||||
|
||||
### 6. Clean Up Resources
|
||||
|
||||
```csharp
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
// Unregister commands
|
||||
// Unregister menus
|
||||
// Unsubscribe events
|
||||
// Dispose resources
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Player Targeting Helper
|
||||
|
||||
```csharp
|
||||
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!)
|
||||
|
||||
```csharp
|
||||
// ✅ 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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```bash
|
||||
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:
|
||||
```csharp
|
||||
Logger.LogInformation("Debug: ...");
|
||||
Logger.LogWarning("Warning: ...");
|
||||
Logger.LogError("Error: ...");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example: Complete Mini-Module
|
||||
|
||||
Here's a complete working example:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
- **[Study Fun Commands Module](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** - Complete reference
|
||||
- **[Read API Documentation](../developer/api/overview)** - Full API reference
|
||||
- **[Check Examples](../developer/module/examples)** - More code examples
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **[CS2-SimpleAdmin GitHub](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code
|
||||
- **[CounterStrikeSharp Docs](https://docs.cssharp.dev/)** - CSS documentation
|
||||
- **[Module Development Guide](../developer/module/getting-started)** - Detailed guide
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **Issues:** [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
|
||||
- **Discussions:** [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
|
||||
- **Examples:** Study official modules for reference
|
||||
|
|
@ -1,691 +0,0 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Fun Commands Module
|
||||
|
||||
Add entertaining and powerful player manipulation commands to your server.
|
||||
|
||||
## Overview
|
||||
|
||||
The Fun Commands module extends CS2-SimpleAdmin with commands for god mode, noclip, freeze, respawn, weapon management, and player attribute modification.
|
||||
|
||||
**Module Name:** `CS2-SimpleAdmin_FunCommands`
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- ⭐ God Mode - Make players invincible
|
||||
- 👻 No Clip - Allow players to fly through walls
|
||||
- 🧊 Freeze/Unfreeze - Freeze players in place
|
||||
- 🔄 Respawn - Bring dead players back
|
||||
- 🔫 Give Weapons - Provide any weapon to players
|
||||
- 🗑️ Strip Weapons - Remove all weapons
|
||||
- ❤️ Set HP - Modify player health
|
||||
- ⚡ Set Speed - Change movement speed
|
||||
- 🌙 Set Gravity - Modify gravity
|
||||
- 💰 Set Money - Adjust player money
|
||||
- 📏 Resize Player - Change player model size
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- CS2-SimpleAdmin installed and working
|
||||
- CS2-SimpleAdminApi.dll in shared folder
|
||||
|
||||
### Install Steps
|
||||
|
||||
1. **Download** the module from releases
|
||||
|
||||
2. **Extract** to your server:
|
||||
```
|
||||
game/csgo/addons/counterstrikesharp/plugins/CS2-SimpleAdmin_FunCommands/
|
||||
```
|
||||
|
||||
3. **Restart** your server or reload plugins:
|
||||
```
|
||||
css_plugins reload
|
||||
```
|
||||
|
||||
4. **Verify** the module loaded:
|
||||
- Check server console for load message
|
||||
- Try `css_admin` and look for "Fun Commands" menu
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
### God Mode
|
||||
|
||||
Toggle god mode (invincibility) for a player.
|
||||
|
||||
```bash
|
||||
css_god <#userid or name>
|
||||
css_godmode <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/cheats`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_god #123
|
||||
css_god PlayerName
|
||||
css_god @all # Toggle god mode for everyone
|
||||
```
|
||||
|
||||
**Effects:**
|
||||
- Player takes no damage
|
||||
- Toggles on/off with each use
|
||||
|
||||
---
|
||||
|
||||
### No Clip
|
||||
|
||||
Enable noclip mode (fly through walls).
|
||||
|
||||
```bash
|
||||
css_noclip <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/cheats`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_noclip #123
|
||||
css_noclip PlayerName
|
||||
```
|
||||
|
||||
**Effects:**
|
||||
- Player can fly
|
||||
- Can pass through walls
|
||||
- Gravity disabled
|
||||
- Toggles on/off with each use
|
||||
|
||||
---
|
||||
|
||||
### Freeze
|
||||
|
||||
Freeze a player in place.
|
||||
|
||||
```bash
|
||||
css_freeze <#userid or name> [duration]
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `duration` - Freeze duration in seconds (optional, default: permanent until unfreeze)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_freeze #123 # Freeze permanently
|
||||
css_freeze PlayerName 30 # Freeze for 30 seconds
|
||||
css_freeze @t 10 # Freeze all terrorists for 10 seconds
|
||||
```
|
||||
|
||||
**Effects:**
|
||||
- Player cannot move
|
||||
- Player cannot shoot
|
||||
- Auto-unfreezes after duration (if specified)
|
||||
|
||||
---
|
||||
|
||||
### Unfreeze
|
||||
|
||||
Unfreeze a frozen player.
|
||||
|
||||
```bash
|
||||
css_unfreeze <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_unfreeze #123
|
||||
css_unfreeze PlayerName
|
||||
css_unfreeze @all # Unfreeze everyone
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Respawn
|
||||
|
||||
Respawn a dead player at last death position.
|
||||
|
||||
```bash
|
||||
css_respawn <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/cheats`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_respawn #123
|
||||
css_respawn PlayerName
|
||||
css_respawn @dead # Respawn all dead players
|
||||
```
|
||||
|
||||
**Effects:**
|
||||
- Player spawns at death point
|
||||
- Gets default weapons
|
||||
- Joins their team
|
||||
|
||||
---
|
||||
|
||||
### Give Weapon
|
||||
|
||||
Give a weapon to a player.
|
||||
|
||||
```bash
|
||||
css_give <#userid or name> <weapon>
|
||||
```
|
||||
|
||||
**Permission:** `@css/cheats`
|
||||
|
||||
**Weapon names:**
|
||||
|
||||
**Rifles:**
|
||||
- `weapon_ak47` or `ak47`
|
||||
- `weapon_m4a1` or `m4a1`
|
||||
- `weapon_m4a1_silencer` or `m4a1_silencer`
|
||||
- `weapon_awp` or `awp`
|
||||
- `weapon_aug` or `aug`
|
||||
- `weapon_sg556` or `sg556`
|
||||
- `weapon_ssg08` or `ssg08` (Scout)
|
||||
- `weapon_g3sg1` or `g3sg1`
|
||||
- `weapon_scar20` or `scar20`
|
||||
|
||||
**SMGs:**
|
||||
- `weapon_mp5sd` or `mp5sd`
|
||||
- `weapon_mp7` or `mp7`
|
||||
- `weapon_mp9` or `mp9`
|
||||
- `weapon_mac10` or `mac10`
|
||||
- `weapon_p90` or `p90`
|
||||
- `weapon_ump45` or `ump45`
|
||||
- `weapon_bizon` or `bizon`
|
||||
|
||||
**Heavy:**
|
||||
- `weapon_nova` or `nova`
|
||||
- `weapon_xm1014` or `xm1014`
|
||||
- `weapon_mag7` or `mag7`
|
||||
- `weapon_sawedoff` or `sawedoff`
|
||||
- `weapon_m249` or `m249`
|
||||
- `weapon_negev` or `negev`
|
||||
|
||||
**Pistols:**
|
||||
- `weapon_deagle` or `deagle`
|
||||
- `weapon_elite` or `elite` (Dual Berettas)
|
||||
- `weapon_fiveseven` or `fiveseven`
|
||||
- `weapon_glock` or `glock`
|
||||
- `weapon_hkp2000` or `hkp2000`
|
||||
- `weapon_p250` or `p250`
|
||||
- `weapon_usp_silencer` or `usp_silencer`
|
||||
- `weapon_tec9` or `tec9`
|
||||
- `weapon_cz75a` or `cz75a`
|
||||
- `weapon_revolver` or `revolver`
|
||||
|
||||
**Grenades:**
|
||||
- `weapon_flashbang` or `flashbang`
|
||||
- `weapon_hegrenade` or `hegrenade`
|
||||
- `weapon_smokegrenade` or `smokegrenade`
|
||||
- `weapon_molotov` or `molotov`
|
||||
- `weapon_incgrenade` or `incgrenade`
|
||||
- `weapon_decoy` or `decoy`
|
||||
|
||||
**Equipment:**
|
||||
- `weapon_knife` or `knife`
|
||||
- `weapon_taser` or `taser`
|
||||
- `item_defuser` or `defuser`
|
||||
- `item_kevlar` or `kevlar`
|
||||
- `item_assaultsuit` or `assaultsuit`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_give #123 awp
|
||||
css_give PlayerName ak47
|
||||
css_give @ct m4a1
|
||||
css_give @all deagle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Strip Weapons
|
||||
|
||||
Remove all weapons from a player.
|
||||
|
||||
```bash
|
||||
css_strip <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_strip #123
|
||||
css_strip PlayerName
|
||||
css_strip @t # Disarm all terrorists
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Set HP
|
||||
|
||||
Set a player's health.
|
||||
|
||||
```bash
|
||||
css_hp <#userid or name> <health>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `health` - Health amount (1-999+)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_hp #123 100 # Full health
|
||||
css_hp PlayerName 200 # 200 HP
|
||||
css_hp @all 1 # 1 HP everyone
|
||||
```
|
||||
|
||||
**Common values:**
|
||||
- `1` - 1 HP (one-shot mode)
|
||||
- `100` - Normal health
|
||||
- `200` - Double health
|
||||
- `500` - Tank mode
|
||||
|
||||
---
|
||||
|
||||
### Set Speed
|
||||
|
||||
Modify a player's movement speed.
|
||||
|
||||
```bash
|
||||
css_speed <#userid or name> <speed>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `speed` - Speed multiplier (0.1 - 10.0)
|
||||
- `1.0` = Normal speed
|
||||
- `2.0` = Double speed
|
||||
- `0.5` = Half speed
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_speed #123 1.5 # 50% faster
|
||||
css_speed PlayerName 0.5 # Slow motion
|
||||
css_speed @all 2.0 # Everyone fast
|
||||
css_speed #123 1.0 # Reset to normal
|
||||
```
|
||||
|
||||
**Common values:**
|
||||
- `0.5` - Slow motion mode
|
||||
- `1.0` - Normal (reset)
|
||||
- `1.5` - Fast mode
|
||||
- `2.0` - Super fast
|
||||
- `3.0` - Extremely fast
|
||||
|
||||
---
|
||||
|
||||
### Set Gravity
|
||||
|
||||
Modify a player's gravity.
|
||||
|
||||
```bash
|
||||
css_gravity <#userid or name> <gravity>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `gravity` - Gravity multiplier (0.1 - 10.0)
|
||||
- `1.0` = Normal gravity
|
||||
- `0.5` = Moon jump
|
||||
- `2.0` = Heavy
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_gravity #123 0.5 # Moon jump
|
||||
css_gravity PlayerName 0.1 # Super jump
|
||||
css_gravity @all 2.0 # Heavy gravity
|
||||
css_gravity #123 1.0 # Reset to normal
|
||||
```
|
||||
|
||||
**Common values:**
|
||||
- `0.1` - Super high jumps
|
||||
- `0.5` - Moon gravity
|
||||
- `1.0` - Normal (reset)
|
||||
- `2.0` - Heavy/fast falling
|
||||
|
||||
---
|
||||
|
||||
### Set Money
|
||||
|
||||
Set a player's money amount.
|
||||
|
||||
```bash
|
||||
css_money <#userid or name> <amount>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `amount` - Money amount (0-65535)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_money #123 16000 # Max money
|
||||
css_money PlayerName 0 # Remove all money
|
||||
css_money @ct 10000 # Give all CTs $10,000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Resize Player
|
||||
|
||||
Change a player's model size.
|
||||
|
||||
```bash
|
||||
css_resize <#userid or name> <scale>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `scale` - Size scale (0.1 - 10.0)
|
||||
- `1.0` = Normal size
|
||||
- `0.5` = Half size
|
||||
- `2.0` = Double size
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_resize #123 0.5 # Tiny player
|
||||
css_resize PlayerName 2.0 # Giant player
|
||||
css_resize #123 1.0 # Reset to normal
|
||||
```
|
||||
|
||||
**Common values:**
|
||||
- `0.5` - Tiny mode
|
||||
- `1.0` - Normal (reset)
|
||||
- `1.5` - Big
|
||||
- `2.0` - Giant
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration file location:
|
||||
```
|
||||
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin_FunCommands/CS2-SimpleAdmin_FunCommands.json
|
||||
```
|
||||
|
||||
### Default Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": 1,
|
||||
"GodCommands": ["css_god", "css_godmode"],
|
||||
"NoclipCommands": ["css_noclip"],
|
||||
"FreezeCommands": ["css_freeze"],
|
||||
"UnfreezeCommands": ["css_unfreeze"],
|
||||
"RespawnCommands": ["css_respawn"],
|
||||
"GiveCommands": ["css_give"],
|
||||
"StripCommands": ["css_strip"],
|
||||
"HpCommands": ["css_hp"],
|
||||
"SpeedCommands": ["css_speed"],
|
||||
"GravityCommands": ["css_gravity"],
|
||||
"MoneyCommands": ["css_money"],
|
||||
"ResizeCommands": ["css_resize"]
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing Commands
|
||||
|
||||
**Add aliases:**
|
||||
```json
|
||||
"GodCommands": ["css_god", "css_godmode", "css_immortal"]
|
||||
```
|
||||
|
||||
**Disable feature:**
|
||||
```json
|
||||
"GodCommands": []
|
||||
```
|
||||
|
||||
**Rename command:**
|
||||
```json
|
||||
"NoclipCommands": ["css_fly"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Admin Menu Integration
|
||||
|
||||
The module automatically adds a "Fun Commands" category to the admin menu with these options:
|
||||
|
||||
- God Mode
|
||||
- No Clip
|
||||
- Freeze
|
||||
- Respawn
|
||||
- Give Weapon
|
||||
- Strip Weapons
|
||||
- Set HP
|
||||
- Set Speed
|
||||
- Set Gravity
|
||||
- Set Money
|
||||
- Resize Player
|
||||
|
||||
**Access menu:**
|
||||
```bash
|
||||
css_admin # Navigate to "Fun Commands"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission System
|
||||
|
||||
### Permission Override
|
||||
|
||||
Admins can override command permissions using CounterStrikeSharp's admin system.
|
||||
|
||||
**Example:**
|
||||
If you want VIPs to use god mode:
|
||||
|
||||
1. **In admin config**, add permission override for `css_god`:
|
||||
```json
|
||||
{
|
||||
"css_god": ["@css/vip"]
|
||||
}
|
||||
```
|
||||
|
||||
2. **VIPs will now see God Mode** in the menu
|
||||
|
||||
---
|
||||
|
||||
## Permissions Required
|
||||
|
||||
| Command | Default Permission | Description |
|
||||
|---------|------------------|-------------|
|
||||
| `css_god` | `@css/cheats` | God mode |
|
||||
| `css_noclip` | `@css/cheats` | No clip |
|
||||
| `css_freeze` | `@css/slay` | Freeze players |
|
||||
| `css_unfreeze` | `@css/slay` | Unfreeze players |
|
||||
| `css_respawn` | `@css/cheats` | Respawn players |
|
||||
| `css_give` | `@css/cheats` | Give weapons |
|
||||
| `css_strip` | `@css/slay` | Strip weapons |
|
||||
| `css_hp` | `@css/slay` | Set health |
|
||||
| `css_speed` | `@css/slay` | Set speed |
|
||||
| `css_gravity` | `@css/slay` | Set gravity |
|
||||
| `css_money` | `@css/slay` | Set money |
|
||||
| `css_resize` | `@css/slay` | Resize player |
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Fun Rounds
|
||||
|
||||
```bash
|
||||
# Low gravity, high speed round
|
||||
css_gravity @all 0.3
|
||||
css_speed @all 1.5
|
||||
|
||||
# One-shot mode
|
||||
css_hp @all 1
|
||||
css_give @all deagle
|
||||
|
||||
# Tiny players
|
||||
css_resize @all 0.5
|
||||
```
|
||||
|
||||
### Admin Events
|
||||
|
||||
```bash
|
||||
# Hide and seek (seekers)
|
||||
css_speed @ct 1.5
|
||||
css_hp @ct 200
|
||||
|
||||
# Hide and seek (hiders)
|
||||
css_resize @t 0.5
|
||||
css_speed @t 0.8
|
||||
```
|
||||
|
||||
### Testing & Debug
|
||||
|
||||
```bash
|
||||
# Test map navigation
|
||||
css_noclip @me
|
||||
css_god @me
|
||||
|
||||
# Test weapon balance
|
||||
css_give @me awp
|
||||
css_hp @me 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Competitive Balance
|
||||
|
||||
1. **Don't use during serious matches** - Breaks game balance
|
||||
2. **Announce fun rounds** - Let players know it's for fun
|
||||
3. **Reset after use** - Return to normal settings
|
||||
4. **Save for appropriate times** - End of night, special events
|
||||
|
||||
### Reset Commands
|
||||
|
||||
Always reset modifications after fun rounds:
|
||||
|
||||
```bash
|
||||
css_speed @all 1.0
|
||||
css_gravity @all 1.0
|
||||
css_resize @all 1.0
|
||||
```
|
||||
|
||||
### Permission Management
|
||||
|
||||
1. **Limit @css/cheats** - Only trusted admins
|
||||
2. **@css/slay is safer** - For HP/speed/gravity
|
||||
3. **Monitor usage** - Check logs for abuse
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Speed/Gravity not persisting
|
||||
|
||||
**Solution:**
|
||||
- These are maintained by a repeating timer
|
||||
- If they reset, reapply them
|
||||
- Check server console for timer errors
|
||||
|
||||
### God mode not working
|
||||
|
||||
**Check:**
|
||||
- Is player alive?
|
||||
- Check console for errors
|
||||
- Try toggling off and on
|
||||
|
||||
### Can't give weapons
|
||||
|
||||
**Check:**
|
||||
- Correct weapon name
|
||||
- Player is alive
|
||||
- Player has inventory space
|
||||
|
||||
### Noclip doesn't work
|
||||
|
||||
**Check:**
|
||||
- Player must be alive
|
||||
- sv_cheats doesn't need to be enabled
|
||||
- Check console for errors
|
||||
|
||||
---
|
||||
|
||||
## Module Development
|
||||
|
||||
This module serves as a **reference implementation** for creating CS2-SimpleAdmin modules.
|
||||
|
||||
**Key concepts demonstrated:**
|
||||
- Command registration from configuration
|
||||
- Menu creation with SimpleAdmin API
|
||||
- Per-player translation support
|
||||
- Proper cleanup on module unload
|
||||
- Code organization using partial classes
|
||||
|
||||
**[View source code](https://github.com/daffyyyy/CS2-SimpleAdmin/tree/main/Modules/CS2-SimpleAdmin_FunCommands)** for implementation details.
|
||||
|
||||
---
|
||||
|
||||
## Translations
|
||||
|
||||
The module includes translations for 13 languages:
|
||||
|
||||
- English (en)
|
||||
- Polish (pl)
|
||||
- Russian (ru)
|
||||
- Portuguese (pt)
|
||||
- And 9 more...
|
||||
|
||||
Translation files location:
|
||||
```
|
||||
plugins/CS2-SimpleAdmin_FunCommands/lang/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- **[Player Commands](../user/commands/playercommands)** - Core player commands
|
||||
- **[Module Development](development)** - Create your own modules
|
||||
- **[API Reference](../developer/api/overview)** - CS2-SimpleAdmin API
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
**v1.0.0** - Initial release
|
||||
- God mode
|
||||
- Noclip
|
||||
- Freeze/Unfreeze
|
||||
- Respawn
|
||||
- Give/Strip weapons
|
||||
- HP/Speed/Gravity/Money
|
||||
- Resize player
|
||||
- Admin menu integration
|
||||
- 13 language support
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
**Issues:** [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
|
||||
|
||||
**Questions:** [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
|
||||
|
|
@ -1,260 +0,0 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Modules Introduction
|
||||
|
||||
Extend CS2-SimpleAdmin functionality with powerful modules.
|
||||
|
||||
## What are Modules?
|
||||
|
||||
Modules are extensions that add new features to CS2-SimpleAdmin. They use the CS2-SimpleAdmin API to integrate seamlessly with the core plugin.
|
||||
|
||||
## Official Modules
|
||||
|
||||
### Fun Commands Module
|
||||
|
||||
Adds entertainment and player manipulation commands like god mode, noclip, freeze, and more.
|
||||
|
||||
**[Learn more →](funcommands)**
|
||||
|
||||
---
|
||||
|
||||
## Benefits of Modules
|
||||
|
||||
### 🔌 Easy Integration
|
||||
- Built on CS2-SimpleAdmin API
|
||||
- Automatic menu registration
|
||||
- Command system integration
|
||||
|
||||
### 🎨 Feature Separation
|
||||
- Keep core plugin lightweight
|
||||
- Add only features you need
|
||||
- Easy to enable/disable
|
||||
|
||||
### 🔧 Customizable
|
||||
- Configure each module independently
|
||||
- Disable unwanted commands
|
||||
- Customize permissions
|
||||
|
||||
### 📦 Simple Installation
|
||||
- Drop module files in folder
|
||||
- Restart server
|
||||
- Module auto-loads
|
||||
|
||||
---
|
||||
|
||||
## Installing Modules
|
||||
|
||||
### Standard Installation
|
||||
|
||||
1. **Download the module** from releases or build from source
|
||||
|
||||
2. **Extract to plugins folder:**
|
||||
```
|
||||
game/csgo/addons/counterstrikesharp/plugins/ModuleName/
|
||||
```
|
||||
|
||||
3. **Restart server** or reload plugins:
|
||||
```
|
||||
css_plugins reload
|
||||
```
|
||||
|
||||
4. **Configure** (if needed):
|
||||
```
|
||||
addons/counterstrikesharp/configs/plugins/ModuleName/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Structure
|
||||
|
||||
Typical module structure:
|
||||
|
||||
```
|
||||
plugins/
|
||||
└── CS2-SimpleAdmin_ModuleName/
|
||||
├── CS2-SimpleAdmin_ModuleName.dll
|
||||
├── CS2-SimpleAdmin_ModuleName.json (config)
|
||||
└── lang/ (translations)
|
||||
├── en.json
|
||||
├── pl.json
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Configuration
|
||||
|
||||
Each module has its own configuration file:
|
||||
|
||||
```
|
||||
addons/counterstrikesharp/configs/plugins/ModuleName/ModuleName.json
|
||||
```
|
||||
|
||||
### Common Configuration Pattern
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": 1,
|
||||
"CommandName": ["css_command", "css_alias"],
|
||||
"OtherSettings": {
|
||||
"EnableFeature": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Command lists allow multiple aliases
|
||||
- Empty command list = feature disabled
|
||||
- Module-specific settings
|
||||
|
||||
---
|
||||
|
||||
## Available Modules
|
||||
|
||||
### Core Modules
|
||||
|
||||
| Module | Description | Status |
|
||||
|--------|-------------|--------|
|
||||
| **[Fun Commands](funcommands)** | God mode, noclip, freeze, speed, gravity | ✅ Official |
|
||||
|
||||
### Community Modules
|
||||
|
||||
Check the [GitHub repository](https://github.com/daffyyyy/CS2-SimpleAdmin) for community-contributed modules.
|
||||
|
||||
---
|
||||
|
||||
## Developing Modules
|
||||
|
||||
Want to create your own module?
|
||||
|
||||
**[See Module Development Guide →](development)**
|
||||
|
||||
**[See Developer Documentation →](../developer/intro)**
|
||||
|
||||
---
|
||||
|
||||
## Module vs Core Plugin
|
||||
|
||||
### When to use Core Plugin:
|
||||
- Essential admin functions
|
||||
- Punishment system
|
||||
- Permission management
|
||||
- Database operations
|
||||
|
||||
### When to use Modules:
|
||||
- Optional features
|
||||
- Server-specific functionality
|
||||
- Experimental features
|
||||
- Custom integrations
|
||||
|
||||
---
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
### Required for All Modules:
|
||||
- CS2-SimpleAdmin (core plugin)
|
||||
- CS2-SimpleAdminApi.dll
|
||||
|
||||
### Module-Specific:
|
||||
Check each module's documentation for specific requirements.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Modules
|
||||
|
||||
### Module doesn't load
|
||||
|
||||
**Check:**
|
||||
1. Is CS2-SimpleAdmin loaded?
|
||||
2. Is CS2-SimpleAdminApi.dll in shared folder?
|
||||
3. Check server console for errors
|
||||
4. Verify module files are complete
|
||||
|
||||
### Module commands not working
|
||||
|
||||
**Check:**
|
||||
1. Is command enabled in module config?
|
||||
2. Do you have required permissions?
|
||||
3. Check Commands.json for conflicts
|
||||
4. Verify module loaded successfully
|
||||
|
||||
### Module conflicts
|
||||
|
||||
**Check:**
|
||||
- Multiple modules providing same command
|
||||
- Check server console for warnings
|
||||
- Disable conflicting module
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Module Management
|
||||
|
||||
1. **Use only needed modules** - Don't overload server
|
||||
2. **Keep modules updated** - Check for updates regularly
|
||||
3. **Test before production** - Test modules on dev server first
|
||||
4. **Review permissions** - Understand what each module can do
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Monitor resource usage** - Some modules may impact performance
|
||||
2. **Configure wisely** - Disable unused features
|
||||
3. **Check logs** - Monitor for errors
|
||||
|
||||
---
|
||||
|
||||
## Module Updates
|
||||
|
||||
### Updating Modules
|
||||
|
||||
1. **Backup current version**
|
||||
2. **Download new version**
|
||||
3. **Replace files** in plugins folder
|
||||
4. **Check configuration** - New config options may exist
|
||||
5. **Restart server**
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
Some updates may have breaking changes:
|
||||
- Check module changelog
|
||||
- Review new configuration options
|
||||
- Test thoroughly
|
||||
|
||||
---
|
||||
|
||||
## Community Contributions
|
||||
|
||||
### Sharing Modules
|
||||
|
||||
Created a module? Share it with the community!
|
||||
|
||||
1. **Publish on GitHub**
|
||||
2. **Document thoroughly**
|
||||
3. **Provide examples**
|
||||
4. **Include README**
|
||||
|
||||
### Using Community Modules
|
||||
|
||||
1. **Review code** - Ensure it's safe
|
||||
2. **Check compatibility** - Verify CS2-SimpleAdmin version
|
||||
3. **Test thoroughly** - Don't trust blindly
|
||||
4. **Report issues** - Help improve modules
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Explore Fun Commands Module](funcommands)** - Add entertainment features
|
||||
- **[Learn Module Development](development)** - Create your own modules
|
||||
- **[Read API Documentation](../developer/intro)** - Understand the API
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **Issues** - [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
|
||||
- **Discussions** - [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
|
||||
- **Examples** - Check official modules for reference
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"label": "Tutorial - Basics",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "5 minutes to learn the most important Docusaurus concepts."
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Congratulations!
|
||||
|
||||
You have just learned the **basics of Docusaurus** and made some changes to the **initial template**.
|
||||
|
||||
Docusaurus has **much more to offer**!
|
||||
|
||||
Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**.
|
||||
|
||||
Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610)
|
||||
|
||||
## What's next?
|
||||
|
||||
- Read the [official documentation](https://docusaurus.io/)
|
||||
- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config)
|
||||
- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration)
|
||||
- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout)
|
||||
- Add a [search bar](https://docusaurus.io/docs/search)
|
||||
- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase)
|
||||
- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Create a Blog Post
|
||||
|
||||
Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed...
|
||||
|
||||
## Create your first Post
|
||||
|
||||
Create a file at `blog/2021-02-28-greetings.md`:
|
||||
|
||||
```md title="blog/2021-02-28-greetings.md"
|
||||
---
|
||||
slug: greetings
|
||||
title: Greetings!
|
||||
authors:
|
||||
- name: Joel Marcey
|
||||
title: Co-creator of Docusaurus 1
|
||||
url: https://github.com/JoelMarcey
|
||||
image_url: https://github.com/JoelMarcey.png
|
||||
- name: Sébastien Lorber
|
||||
title: Docusaurus maintainer
|
||||
url: https://sebastienlorber.com
|
||||
image_url: https://github.com/slorber.png
|
||||
tags: [greetings]
|
||||
---
|
||||
|
||||
Congratulations, you have made your first post!
|
||||
|
||||
Feel free to play around and edit this post as much as you like.
|
||||
```
|
||||
|
||||
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Create a Document
|
||||
|
||||
Documents are **groups of pages** connected through:
|
||||
|
||||
- a **sidebar**
|
||||
- **previous/next navigation**
|
||||
- **versioning**
|
||||
|
||||
## Create your first Doc
|
||||
|
||||
Create a Markdown file at `docs/hello.md`:
|
||||
|
||||
```md title="docs/hello.md"
|
||||
# Hello
|
||||
|
||||
This is my **first Docusaurus document**!
|
||||
```
|
||||
|
||||
A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello).
|
||||
|
||||
## Configure the Sidebar
|
||||
|
||||
Docusaurus automatically **creates a sidebar** from the `docs` folder.
|
||||
|
||||
Add metadata to customize the sidebar label and position:
|
||||
|
||||
```md title="docs/hello.md" {1-4}
|
||||
---
|
||||
sidebar_label: 'Hi!'
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
This is my **first Docusaurus document**!
|
||||
```
|
||||
|
||||
It is also possible to create your sidebar explicitly in `sidebars.js`:
|
||||
|
||||
```js title="sidebars.js"
|
||||
export default {
|
||||
tutorialSidebar: [
|
||||
'intro',
|
||||
// highlight-next-line
|
||||
'hello',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['tutorial-basics/create-a-document'],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Create a Page
|
||||
|
||||
Add **Markdown or React** files to `src/pages` to create a **standalone page**:
|
||||
|
||||
- `src/pages/index.js` → `localhost:3000/`
|
||||
- `src/pages/foo.md` → `localhost:3000/foo`
|
||||
- `src/pages/foo/bar.js` → `localhost:3000/foo/bar`
|
||||
|
||||
## Create your first React Page
|
||||
|
||||
Create a file at `src/pages/my-react-page.js`:
|
||||
|
||||
```jsx title="src/pages/my-react-page.js"
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
|
||||
export default function MyReactPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>My React page</h1>
|
||||
<p>This is a React page</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page).
|
||||
|
||||
## Create your first Markdown Page
|
||||
|
||||
Create a file at `src/pages/my-markdown-page.md`:
|
||||
|
||||
```mdx title="src/pages/my-markdown-page.md"
|
||||
# My Markdown page
|
||||
|
||||
This is a Markdown page
|
||||
```
|
||||
|
||||
A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page).
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Deploy your site
|
||||
|
||||
Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**).
|
||||
|
||||
It builds your site as simple **static HTML, JavaScript and CSS files**.
|
||||
|
||||
## Build your site
|
||||
|
||||
Build your site **for production**:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The static files are generated in the `build` folder.
|
||||
|
||||
## Deploy your site
|
||||
|
||||
Test your production build locally:
|
||||
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**).
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Markdown Features
|
||||
|
||||
Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**.
|
||||
|
||||
## Front Matter
|
||||
|
||||
Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
|
||||
|
||||
```text title="my-doc.md"
|
||||
// highlight-start
|
||||
---
|
||||
id: my-doc-id
|
||||
title: My document title
|
||||
description: My document description
|
||||
slug: /my-custom-url
|
||||
---
|
||||
// highlight-end
|
||||
|
||||
## Markdown heading
|
||||
|
||||
Markdown text with [links](./hello.md)
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
Regular Markdown links are supported, using url paths or relative file paths.
|
||||
|
||||
```md
|
||||
Let's see how to [Create a page](/create-a-page).
|
||||
```
|
||||
|
||||
```md
|
||||
Let's see how to [Create a page](./create-a-page.md).
|
||||
```
|
||||
|
||||
**Result:** Let's see how to [Create a page](./create-a-page.md).
|
||||
|
||||
## Images
|
||||
|
||||
Regular Markdown images are supported.
|
||||
|
||||
You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`):
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||

|
||||
|
||||
You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
## Code Blocks
|
||||
|
||||
Markdown code blocks are supported with Syntax highlighting.
|
||||
|
||||
````md
|
||||
```jsx title="src/components/HelloDocusaurus.js"
|
||||
function HelloDocusaurus() {
|
||||
return <h1>Hello, Docusaurus!</h1>;
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
```jsx title="src/components/HelloDocusaurus.js"
|
||||
function HelloDocusaurus() {
|
||||
return <h1>Hello, Docusaurus!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## Admonitions
|
||||
|
||||
Docusaurus has a special syntax to create admonitions and callouts:
|
||||
|
||||
```md
|
||||
:::tip My tip
|
||||
|
||||
Use this awesome feature option
|
||||
|
||||
:::
|
||||
|
||||
:::danger Take care
|
||||
|
||||
This action is dangerous
|
||||
|
||||
:::
|
||||
```
|
||||
|
||||
:::tip My tip
|
||||
|
||||
Use this awesome feature option
|
||||
|
||||
:::
|
||||
|
||||
:::danger Take care
|
||||
|
||||
This action is dangerous
|
||||
|
||||
:::
|
||||
|
||||
## MDX and React Components
|
||||
|
||||
[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**:
|
||||
|
||||
```jsx
|
||||
export const Highlight = ({children, color}) => (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '20px',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
alert(`You clicked the color ${color} with label ${children}`)
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
|
||||
|
||||
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
|
||||
```
|
||||
|
||||
export const Highlight = ({children, color}) => (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderRadius: '20px',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
alert(`You clicked the color ${color} with label ${children}`);
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
|
||||
|
||||
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"label": "Tutorial - Extras",
|
||||
"position": 3,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
|
@ -1,55 +0,0 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Manage Docs Versions
|
||||
|
||||
Docusaurus can manage multiple versions of your docs.
|
||||
|
||||
## Create a docs version
|
||||
|
||||
Release a version 1.0 of your project:
|
||||
|
||||
```bash
|
||||
npm run docusaurus docs:version 1.0
|
||||
```
|
||||
|
||||
The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created.
|
||||
|
||||
Your docs now have 2 versions:
|
||||
|
||||
- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs
|
||||
- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs**
|
||||
|
||||
## Add a Version Dropdown
|
||||
|
||||
To navigate seamlessly across versions, add a version dropdown.
|
||||
|
||||
Modify the `docusaurus.config.js` file:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
export default {
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
items: [
|
||||
// highlight-start
|
||||
{
|
||||
type: 'docsVersionDropdown',
|
||||
},
|
||||
// highlight-end
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The docs version dropdown appears in your navbar:
|
||||
|
||||

|
||||
|
||||
## Update an existing version
|
||||
|
||||
It is possible to edit versioned docs in their respective folder:
|
||||
|
||||
- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello`
|
||||
- `docs/hello.md` updates `http://localhost:3000/docs/next/hello`
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Translate your site
|
||||
|
||||
Let's translate `docs/intro.md` to French.
|
||||
|
||||
## Configure i18n
|
||||
|
||||
Modify `docusaurus.config.js` to add support for the `fr` locale:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
export default {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Translate a doc
|
||||
|
||||
Copy the `docs/intro.md` file to the `i18n/fr` folder:
|
||||
|
||||
```bash
|
||||
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/
|
||||
|
||||
cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md
|
||||
```
|
||||
|
||||
Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French.
|
||||
|
||||
## Start your localized site
|
||||
|
||||
Start your site on the French locale:
|
||||
|
||||
```bash
|
||||
npm run start -- --locale fr
|
||||
```
|
||||
|
||||
Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated.
|
||||
|
||||
:::caution
|
||||
|
||||
In development, you can only use one locale at a time.
|
||||
|
||||
:::
|
||||
|
||||
## Add a Locale Dropdown
|
||||
|
||||
To navigate seamlessly across languages, add a locale dropdown.
|
||||
|
||||
Modify the `docusaurus.config.js` file:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
export default {
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
items: [
|
||||
// highlight-start
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
},
|
||||
// highlight-end
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The locale dropdown now appears in your navbar:
|
||||
|
||||

|
||||
|
||||
## Build your localized site
|
||||
|
||||
Build your site for a specific locale:
|
||||
|
||||
```bash
|
||||
npm run build -- --locale fr
|
||||
```
|
||||
|
||||
Or build your site to include all the locales at once:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Ban Commands
|
||||
|
||||
Commands for managing player bans.
|
||||
|
||||
## Ban Player
|
||||
|
||||
Ban a player currently on the server.
|
||||
|
||||
```bash
|
||||
css_ban <#userid or name> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/ban`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_ban @all 60 "Timeout for everyone"
|
||||
css_ban #123 1440 "Hacking - 1 day ban"
|
||||
css_ban PlayerName 0 "Permanent ban for cheating"
|
||||
css_ban @ct 30 "CT team timeout"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Time in minutes (0 = permanent)
|
||||
- Supports player targeting (@all, @ct, @t, #userid, name)
|
||||
- Reason is optional but recommended
|
||||
|
||||
---
|
||||
|
||||
## Add Ban (Offline Player)
|
||||
|
||||
Ban a player by SteamID even if they're not online.
|
||||
|
||||
```bash
|
||||
css_addban <steamid> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/ban`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_addban STEAM_1:0:12345678 1440 "Ban evasion"
|
||||
css_addban 76561198012345678 10080 "Hacking - 7 day ban"
|
||||
css_addban STEAM_1:1:87654321 0 "Permanent ban"
|
||||
```
|
||||
|
||||
**Supported SteamID formats:**
|
||||
- SteamID64: `76561198012345678`
|
||||
- SteamID: `STEAM_1:0:12345678`
|
||||
- SteamID3: `[U:1:12345678]`
|
||||
|
||||
---
|
||||
|
||||
## Ban IP Address
|
||||
|
||||
Ban an IP address.
|
||||
|
||||
```bash
|
||||
css_banip <ip> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/ban`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_banip 192.168.1.100 1440 "Ban evasion attempt"
|
||||
css_banip 10.0.0.5 0 "Persistent troublemaker"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Useful for preventing ban evasion
|
||||
- Can be combined with SteamID bans
|
||||
- Check config for `BanType` setting (SteamID, IP, or Both)
|
||||
|
||||
---
|
||||
|
||||
## Unban Player
|
||||
|
||||
Remove a ban from a player.
|
||||
|
||||
```bash
|
||||
css_unban <steamid or name or ip> [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/unban`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_unban 76561198012345678 "Appeal accepted"
|
||||
css_unban STEAM_1:0:12345678 "Ban lifted"
|
||||
css_unban 192.168.1.100 "Wrong person banned"
|
||||
css_unban PlayerName "Mistake"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Works with SteamID, IP, or player name
|
||||
- Unban reason is logged
|
||||
- Can unban offline players
|
||||
|
||||
---
|
||||
|
||||
## Warn Player
|
||||
|
||||
Issue a warning to a player.
|
||||
|
||||
```bash
|
||||
css_warn <#userid or name> [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_warn #123 "Mic spam"
|
||||
css_warn PlayerName "Language"
|
||||
css_warn @all "Final warning"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Warnings can accumulate
|
||||
- Auto-escalation to bans based on `WarnThreshold` config
|
||||
- Example: 3 warnings = 1 hour ban, 4 warnings = 2 hour ban
|
||||
|
||||
**Warning Threshold Configuration:**
|
||||
```json
|
||||
"WarnThreshold": {
|
||||
"3": "css_addban STEAMID64 60 \"3 warnings\"",
|
||||
"4": "css_ban #USERID 120 \"4 warnings\""
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unwarn Player
|
||||
|
||||
Remove a warning from a player.
|
||||
|
||||
```bash
|
||||
css_unwarn <steamid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_unwarn 76561198012345678
|
||||
css_unwarn PlayerName
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Removes the most recent warning
|
||||
- Helps manage warning thresholds
|
||||
- Can be used for offline players
|
||||
|
||||
---
|
||||
|
||||
## Permission Requirements
|
||||
|
||||
| Command | Required Permission | Description |
|
||||
|---------|-------------------|-------------|
|
||||
| `css_ban` | `@css/ban` | Ban online players |
|
||||
| `css_addban` | `@css/ban` | Ban offline players by SteamID |
|
||||
| `css_banip` | `@css/ban` | Ban IP addresses |
|
||||
| `css_unban` | `@css/unban` | Remove bans |
|
||||
| `css_warn` | `@css/kick` | Issue warnings |
|
||||
| `css_unwarn` | `@css/kick` | Remove warnings |
|
||||
|
||||
## Ban Types
|
||||
|
||||
Configure ban behavior in `CS2-SimpleAdmin.json`:
|
||||
|
||||
```json
|
||||
"BanType": 1
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `1` - SteamID only (default)
|
||||
- `2` - IP only
|
||||
- `3` - Both SteamID and IP
|
||||
|
||||
## Time Durations
|
||||
|
||||
Common time values:
|
||||
|
||||
| Duration | Minutes | Description |
|
||||
|----------|---------|-------------|
|
||||
| 1 minute | 1 | Very short timeout |
|
||||
| 5 minutes | 5 | Short timeout |
|
||||
| 15 minutes | 15 | Medium timeout |
|
||||
| 1 hour | 60 | Standard timeout |
|
||||
| 1 day | 1440 | Daily ban |
|
||||
| 1 week | 10080 | Weekly ban |
|
||||
| 2 weeks | 20160 | Bi-weekly ban |
|
||||
| 1 month | 43200 | Monthly ban |
|
||||
| Permanent | 0 | Never expires |
|
||||
|
||||
## Player Targeting
|
||||
|
||||
All ban commands support advanced targeting:
|
||||
|
||||
- `@all` - Target all players
|
||||
- `@ct` - Target all Counter-Terrorists
|
||||
- `@t` - Target all Terrorists
|
||||
- `@spec` - Target all spectators
|
||||
- `#123` - Target by userid
|
||||
- `PlayerName` - Target by name (partial match)
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Banning
|
||||
|
||||
1. **Always provide a reason** - Helps with appeals and record keeping
|
||||
2. **Use appropriate durations** - Don't permaban for minor offenses
|
||||
3. **Check ban history** - Use `css_who` to see if player has priors
|
||||
4. **Consider warnings first** - Give players a chance to improve
|
||||
|
||||
### Warning System
|
||||
|
||||
1. **Be consistent** - Use warnings for minor offenses
|
||||
2. **Configure thresholds** - Set up auto-escalation in config
|
||||
3. **Communicate clearly** - Let players know why they're warned
|
||||
4. **Review regularly** - Check warning history with `css_warns`
|
||||
|
||||
### Multi-Account Detection
|
||||
|
||||
When `CheckMultiAccountsByIp` is enabled:
|
||||
- Plugin detects multiple accounts from same IP
|
||||
- Sends Discord notifications if configured
|
||||
- Helps identify ban evasion
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Ban doesn't work
|
||||
|
||||
**Check:**
|
||||
- Do you have `@css/ban` permission?
|
||||
- Is the SteamID format correct?
|
||||
- Check server console for errors
|
||||
|
||||
### Player rejoins after ban
|
||||
|
||||
**Check:**
|
||||
- Is `MultiServerMode` enabled if using multiple servers?
|
||||
- Is the database shared across servers?
|
||||
- Check ban type configuration (SteamID vs IP)
|
||||
|
||||
### Warning threshold not working
|
||||
|
||||
**Check:**
|
||||
- Is `WarnThreshold` configured correctly?
|
||||
- Are the command formats correct in config?
|
||||
- Check server console for execution errors
|
||||
|
||||
## Related Commands
|
||||
|
||||
- **[Communication Commands](basecomms)** - Mute, gag, silence
|
||||
- **[Player Commands](playercommands)** - Kick, slay, etc.
|
||||
- **[Base Commands](basecommands)** - Admin management
|
||||
|
|
@ -1,442 +0,0 @@
|
|||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Chat Commands
|
||||
|
||||
Admin chat and messaging commands.
|
||||
|
||||
## Admin Chat
|
||||
|
||||
### Admin Say (Private)
|
||||
|
||||
Send a message to all online admins only.
|
||||
|
||||
```bash
|
||||
css_asay <message>
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Features:**
|
||||
- Only admins see the message
|
||||
- Useful for admin coordination
|
||||
- Colored differently from regular chat
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_asay "Player is suspicious, keep an eye on them"
|
||||
css_asay "I'm going AFK for 5 minutes"
|
||||
css_asay "Need help with a situation"
|
||||
```
|
||||
|
||||
**Message format:**
|
||||
```
|
||||
[ADMIN] YourName: message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Public Announcements
|
||||
|
||||
### CSS Say (Colored)
|
||||
|
||||
Send a colored message to all players.
|
||||
|
||||
```bash
|
||||
css_cssay <message>
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Features:**
|
||||
- Colorful formatted message
|
||||
- Visible to all players
|
||||
- Stands out from regular chat
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_cssay "Server will restart in 5 minutes!"
|
||||
css_cssay "Welcome to our server!"
|
||||
css_cssay "Rules: No cheating, be respectful"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Say (With Prefix)
|
||||
|
||||
Send a message to all players with admin prefix.
|
||||
|
||||
```bash
|
||||
css_say <message>
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Features:**
|
||||
- Message shows with "(ADMIN)" prefix
|
||||
- Visible to all players
|
||||
- Authority message format
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_say "Please be respectful in chat"
|
||||
css_say "Cheating will result in permanent ban"
|
||||
css_say "Type !rules for server rules"
|
||||
```
|
||||
|
||||
**Message format:**
|
||||
```
|
||||
(ADMIN) YourName: message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Private Say (Whisper)
|
||||
|
||||
Send a private message to a specific player.
|
||||
|
||||
```bash
|
||||
css_psay <#userid or name> <message>
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Features:**
|
||||
- Only the target player sees the message
|
||||
- Useful for private warnings or help
|
||||
- Doesn't clutter public chat
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_psay #123 "Please stop mic spamming"
|
||||
css_psay PlayerName "You need to join a team"
|
||||
css_psay @all "This is a private message to everyone"
|
||||
```
|
||||
|
||||
**Target receives:**
|
||||
```
|
||||
(ADMIN to you) AdminName: message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Center Say
|
||||
|
||||
Display a message in the center of all players' screens.
|
||||
|
||||
```bash
|
||||
css_csay <message>
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Features:**
|
||||
- Large text in center of screen
|
||||
- Impossible to miss
|
||||
- Useful for important announcements
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_csay "ROUND STARTS IN 10 SECONDS"
|
||||
css_csay "FREEZE! Don't move!"
|
||||
css_csay "Server restarting NOW"
|
||||
```
|
||||
|
||||
**Display:**
|
||||
- Shows in center of screen
|
||||
- Large, bold text
|
||||
- Auto-fades after a few seconds
|
||||
|
||||
---
|
||||
|
||||
### HUD Say
|
||||
|
||||
Display a message on players' HUD (screen overlay).
|
||||
|
||||
```bash
|
||||
css_hsay <message>
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Features:**
|
||||
- Message appears on screen overlay
|
||||
- Less intrusive than center say
|
||||
- Stays visible longer
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_hsay "Tournament starting soon!"
|
||||
css_hsay "New map voting available"
|
||||
css_hsay "Visit our website: example.com"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Announcements
|
||||
|
||||
```bash
|
||||
# Server restart warning
|
||||
css_cssay "Server will restart in 10 minutes! Save your progress!"
|
||||
|
||||
# Center screen countdown
|
||||
css_csay "5"
|
||||
# Wait...
|
||||
css_csay "4"
|
||||
css_csay "3"
|
||||
css_csay "2"
|
||||
css_csay "1"
|
||||
css_csay "GO!"
|
||||
```
|
||||
|
||||
### Player Communication
|
||||
|
||||
```bash
|
||||
# Private warning
|
||||
css_psay PlayerName "This is your first warning for chat spam"
|
||||
|
||||
# Public announcement
|
||||
css_say "Everyone please be quiet for the next round"
|
||||
|
||||
# Admin coordination
|
||||
css_asay "I'm spectating the suspicious player in T spawn"
|
||||
```
|
||||
|
||||
### Event Management
|
||||
|
||||
```bash
|
||||
# Tournament announcement
|
||||
css_cssay "⚠ TOURNAMENT STARTING IN 5 MINUTES ⚠"
|
||||
css_hsay "Teams, please get ready!"
|
||||
css_csay "TOURNAMENT BEGINS NOW!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Color Codes
|
||||
|
||||
Many chat commands support color codes:
|
||||
|
||||
```
|
||||
{default} - Default chat color
|
||||
{white} - White
|
||||
{darkred} - Dark red
|
||||
{green} - Green
|
||||
{lightyellow} - Light yellow
|
||||
{lightblue} - Light blue
|
||||
{olive} - Olive
|
||||
{lime} - Lime
|
||||
{red} - Red
|
||||
{purple} - Purple
|
||||
{grey} - Grey
|
||||
{yellow} - Yellow
|
||||
{gold} - Gold
|
||||
{silver} - Silver
|
||||
{blue} - Blue
|
||||
{darkblue} - Dark blue
|
||||
{bluegrey} - Blue grey
|
||||
{magenta} - Magenta
|
||||
{lightred} - Light red
|
||||
{orange} - Orange
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
css_say "{red}WARNING: {default}No camping in spawn!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message Targeting
|
||||
|
||||
Some commands support player targeting for private messages:
|
||||
|
||||
### Supported Targets
|
||||
|
||||
- `@all` - All players (private message to each)
|
||||
- `@ct` - All Counter-Terrorists
|
||||
- `@t` - All Terrorists
|
||||
- `@spec` - All spectators
|
||||
- `#123` - Specific userid
|
||||
- `PlayerName` - Player by name
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Private message to all CTs
|
||||
css_psay @ct "Defend bombsite A this round"
|
||||
|
||||
# Private message to all terrorists
|
||||
css_psay @t "Rush B with smoke and flash"
|
||||
|
||||
# Message to spectators
|
||||
css_psay @spec "Type !join to play"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Each Command
|
||||
|
||||
**css_asay** (Admin Say):
|
||||
- Admin coordination
|
||||
- Discussing player behavior
|
||||
- Planning admin actions
|
||||
- Private admin discussions
|
||||
|
||||
**css_cssay** (Colored Say):
|
||||
- Important server announcements
|
||||
- Event notifications
|
||||
- Eye-catching messages
|
||||
- Server information
|
||||
|
||||
**css_say** (Say):
|
||||
- General admin announcements
|
||||
- Rule reminders
|
||||
- Warnings to all players
|
||||
- Admin presence
|
||||
|
||||
**css_psay** (Private Say):
|
||||
- Private warnings
|
||||
- Individual help
|
||||
- Direct player communication
|
||||
- Discretion needed
|
||||
|
||||
**css_csay** (Center Say):
|
||||
- Emergency announcements
|
||||
- Cannot-miss messages
|
||||
- Round starts/events
|
||||
- Countdowns
|
||||
|
||||
**css_hsay** (HUD Say):
|
||||
- Persistent information
|
||||
- Less urgent announcements
|
||||
- Server info
|
||||
- Website/Discord links
|
||||
|
||||
---
|
||||
|
||||
## Communication Guidelines
|
||||
|
||||
### Professional Communication
|
||||
|
||||
1. **Be clear and concise** - Don't spam long messages
|
||||
2. **Use appropriate command** - Don't center-spam trivial messages
|
||||
3. **Check for typos** - You represent the server
|
||||
4. **Avoid excessive colors** - Can be hard to read
|
||||
|
||||
### Spam Prevention
|
||||
|
||||
1. **Don't overuse center say** - Very intrusive for players
|
||||
2. **Space out announcements** - Don't flood chat
|
||||
3. **Use HUD say for persistent info** - Less annoying
|
||||
4. **Coordinate with other admins** - Avoid duplicate messages
|
||||
|
||||
### Effective Messaging
|
||||
|
||||
**Good Examples:**
|
||||
```bash
|
||||
css_cssay "🎯 New map voting system available! Type !mapvote"
|
||||
css_psay PlayerName "Hey! Please enable your mic or use team chat"
|
||||
css_asay "Checking player demos for possible aimbot"
|
||||
```
|
||||
|
||||
**Poor Examples:**
|
||||
```bash
|
||||
css_csay "hi" # Don't use center say for trivial messages
|
||||
css_cssay "a" "b" "c" "d" # Don't spam center messages
|
||||
css_say "asdfasdfasdf" # Unprofessional
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Silent Mode
|
||||
|
||||
Admins can use silent mode to hide their activity:
|
||||
|
||||
```bash
|
||||
css_hide # Toggle silent mode
|
||||
```
|
||||
|
||||
When in silent mode:
|
||||
- Chat messages still work
|
||||
- Admin name might be hidden (depends on config)
|
||||
- Useful for undercover moderation
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Show Activity Type
|
||||
|
||||
Controls how admin actions are displayed:
|
||||
|
||||
```json
|
||||
"ShowActivityType": 2
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `0` - Hide all admin activity
|
||||
- `1` - Anonymous ("An admin says...")
|
||||
- `2` - Show name ("AdminName says...")
|
||||
|
||||
---
|
||||
|
||||
## Chat Restrictions
|
||||
|
||||
### Respecting Gags
|
||||
|
||||
Remember that:
|
||||
- `css_asay` works even if admin is gagged (admin chat)
|
||||
- Other commands respect communication penalties
|
||||
- Can't use chat commands while silenced
|
||||
|
||||
### Permission Requirements
|
||||
|
||||
All chat commands require `@css/chat` permission:
|
||||
|
||||
```bash
|
||||
css_addadmin STEAMID "Name" "@css/chat" 50 0
|
||||
```
|
||||
|
||||
Or add to a group with chat permission:
|
||||
```bash
|
||||
css_addgroup "#moderators" "@css/chat,@css/kick" 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Messages not showing
|
||||
|
||||
**Check:**
|
||||
- Do you have `@css/chat` permission?
|
||||
- Are you silenced/gagged?
|
||||
- Check console for errors
|
||||
- Verify player is connected (for css_psay)
|
||||
|
||||
### Colors not working
|
||||
|
||||
**Check:**
|
||||
- Use correct color code syntax: `{red}`
|
||||
- Some commands may not support colors
|
||||
- Different chat systems handle colors differently
|
||||
|
||||
### Players can't see center/HUD messages
|
||||
|
||||
**Check:**
|
||||
- CS2 client-side chat settings
|
||||
- Conflicting HUD plugins
|
||||
- Server console for errors
|
||||
|
||||
---
|
||||
|
||||
## Related Commands
|
||||
|
||||
- **[Communication Commands](basecomms)** - Gag, mute, silence players
|
||||
- **[Base Commands](basecommands)** - Admin management
|
||||
- **[Ban Commands](basebans)** - Player punishment
|
||||
|
|
@ -1,656 +0,0 @@
|
|||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Base Commands
|
||||
|
||||
Core admin commands for server management and admin system.
|
||||
|
||||
## Player Information
|
||||
|
||||
### Show Penalties
|
||||
|
||||
View your own active penalties.
|
||||
|
||||
```bash
|
||||
css_penalties
|
||||
css_mypenalties
|
||||
css_comms
|
||||
```
|
||||
|
||||
**Permission:** None (all players)
|
||||
|
||||
**Shows:**
|
||||
- Active bans
|
||||
- Active communication restrictions (gag, mute, silence)
|
||||
- Warning count
|
||||
- Duration remaining
|
||||
|
||||
---
|
||||
|
||||
### Hide Penalty Notifications
|
||||
|
||||
Hide penalty notifications when you connect to the server.
|
||||
|
||||
```bash
|
||||
css_hidecomms
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Notes:**
|
||||
- Toggle on/off
|
||||
- Admins won't see penalty notifications on join
|
||||
- Useful for admin privacy
|
||||
|
||||
---
|
||||
|
||||
## Admin Menu
|
||||
|
||||
### Open Admin Menu
|
||||
|
||||
Opens the main admin menu interface.
|
||||
|
||||
```bash
|
||||
css_admin
|
||||
```
|
||||
|
||||
**Permission:** `@css/generic`
|
||||
|
||||
**Features:**
|
||||
- Player management
|
||||
- Server management
|
||||
- Ban/kick/mute players via menu
|
||||
- Map changing
|
||||
- Custom server commands
|
||||
|
||||
---
|
||||
|
||||
### Admin Help
|
||||
|
||||
Print the admin help file.
|
||||
|
||||
```bash
|
||||
css_adminhelp
|
||||
```
|
||||
|
||||
**Permission:** `@css/generic`
|
||||
|
||||
**Shows:**
|
||||
- Available commands for your permission level
|
||||
- Command syntax
|
||||
- Permission requirements
|
||||
|
||||
---
|
||||
|
||||
## Admin Management
|
||||
|
||||
### Add Admin
|
||||
|
||||
Add a new admin to the database.
|
||||
|
||||
```bash
|
||||
css_addadmin <steamid> <name> <flags/groups> <immunity> <duration>
|
||||
```
|
||||
|
||||
**Permission:** `@css/root`
|
||||
|
||||
**Parameters:**
|
||||
- `steamid` - Player's SteamID (any format)
|
||||
- `name` - Admin name (for identification)
|
||||
- `flags/groups` - Permission flags or group name
|
||||
- `immunity` - Immunity level (0-100, higher = more protection)
|
||||
- `duration` - Duration in minutes (0 = permanent)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Add permanent admin with root access
|
||||
css_addadmin 76561198012345678 "AdminName" "@css/root" 99 0
|
||||
|
||||
# Add moderator for 30 days
|
||||
css_addadmin STEAM_1:0:12345678 "ModName" "@css/kick,@css/ban" 50 43200
|
||||
|
||||
# Add admin using group
|
||||
css_addadmin 76561198012345678 "AdminName" "#moderators" 60 0
|
||||
|
||||
# Add admin to all servers (-g flag)
|
||||
css_addadmin 76561198012345678 "AdminName" "@css/root" 99 0 -g
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `-g` - Add to all servers (global admin)
|
||||
|
||||
---
|
||||
|
||||
### Delete Admin
|
||||
|
||||
Remove an admin from the database.
|
||||
|
||||
```bash
|
||||
css_deladmin <steamid>
|
||||
```
|
||||
|
||||
**Permission:** `@css/root`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Remove admin from current server
|
||||
css_deladmin 76561198012345678
|
||||
|
||||
# Remove admin from all servers (-g flag)
|
||||
css_deladmin 76561198012345678 -g
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Add Admin Group
|
||||
|
||||
Create a new admin group.
|
||||
|
||||
```bash
|
||||
css_addgroup <group_name> <flags> <immunity>
|
||||
```
|
||||
|
||||
**Permission:** `@css/root`
|
||||
|
||||
**Parameters:**
|
||||
- `group_name` - Name of the group (e.g., "#moderators")
|
||||
- `flags` - Permission flags for the group
|
||||
- `immunity` - Default immunity level for group members
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Create moderator group
|
||||
css_addgroup "#moderators" "@css/kick,@css/ban,@css/chat" 50
|
||||
|
||||
# Create VIP group
|
||||
css_addgroup "#vip" "@css/vip" 10
|
||||
|
||||
# Create global group (-g flag)
|
||||
css_addgroup "#owner" "@css/root" 99 -g
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `-g` - Create group on all servers
|
||||
|
||||
---
|
||||
|
||||
### Delete Admin Group
|
||||
|
||||
Remove an admin group from the database.
|
||||
|
||||
```bash
|
||||
css_delgroup <group_name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/root`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Delete group from current server
|
||||
css_delgroup "#moderators"
|
||||
|
||||
# Delete group from all servers (-g flag)
|
||||
css_delgroup "#moderators" -g
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Reload Admins
|
||||
|
||||
Reload admin permissions from the database.
|
||||
|
||||
```bash
|
||||
css_reloadadmins
|
||||
```
|
||||
|
||||
**Permission:** `@css/root`
|
||||
|
||||
**When to use:**
|
||||
- After adding/removing admins via database
|
||||
- After modifying admin permissions
|
||||
- After group changes
|
||||
- Troubleshooting permission issues
|
||||
|
||||
**Note:** Admins are automatically reloaded periodically and on map change (if configured).
|
||||
|
||||
---
|
||||
|
||||
## Player Information
|
||||
|
||||
### Hide in Scoreboard
|
||||
|
||||
Toggle admin stealth mode (hide from scoreboard).
|
||||
|
||||
```bash
|
||||
css_hide
|
||||
css_stealth
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Features:**
|
||||
- Hides you from the scoreboard
|
||||
- Makes admin actions anonymous
|
||||
- Useful for undercover moderation
|
||||
|
||||
---
|
||||
|
||||
### Who is This Player
|
||||
|
||||
Show detailed information about a player.
|
||||
|
||||
```bash
|
||||
css_who <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/generic`
|
||||
|
||||
**Shows:**
|
||||
- Player name and SteamID
|
||||
- IP address (if you have `@css/showip` permission)
|
||||
- Connection time
|
||||
- Active penalties
|
||||
- Warning count
|
||||
- Ban history
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_who #123
|
||||
css_who PlayerName
|
||||
css_who @me
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Show Disconnected Players
|
||||
|
||||
Show recently disconnected players.
|
||||
|
||||
```bash
|
||||
css_disconnected
|
||||
css_last
|
||||
css_last10 # Show last 10 (config value)
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Shows:**
|
||||
- Player name
|
||||
- SteamID
|
||||
- Disconnect time
|
||||
- Disconnect reason
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
"DisconnectedPlayersHistoryCount": 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Show Warns for Player
|
||||
|
||||
Open warn list for a specific player.
|
||||
|
||||
```bash
|
||||
css_warns <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Shows:**
|
||||
- All warnings for the player
|
||||
- Warning reasons
|
||||
- Admins who issued warnings
|
||||
- Warning timestamps
|
||||
- Total warning count
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_warns #123
|
||||
css_warns PlayerName
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Show Penalty History
|
||||
|
||||
Print every past and current penalty for a player to your console.
|
||||
|
||||
```bash
|
||||
css_history <#userid or name or steamid64> [bans|gags|mutes|silences|warns]
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Shows:**
|
||||
- Bans, gags, mutes, silences and warns, newest first
|
||||
- Status (active, expired, unbanned, unmuted), duration and reason
|
||||
- Admin who issued it
|
||||
- Who lifted it, when and why, for unbans and unmutes
|
||||
|
||||
**Notes:**
|
||||
- Accepts a SteamID64, so it works for players who are not online
|
||||
- The optional second argument narrows the list to one penalty type
|
||||
- Also available in the admin menu under Players > History
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_history #123
|
||||
css_history PlayerName bans
|
||||
css_history 76561198012345678
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Show Online Players
|
||||
|
||||
Show information about all online players.
|
||||
|
||||
```bash
|
||||
css_players
|
||||
```
|
||||
|
||||
**Permission:** `@css/generic`
|
||||
|
||||
**Shows:**
|
||||
- List of all connected players
|
||||
- UserIDs
|
||||
- Names
|
||||
- Teams
|
||||
- Connection status
|
||||
|
||||
---
|
||||
|
||||
## Server Management
|
||||
|
||||
### Kick Player
|
||||
|
||||
Kick a player from the server.
|
||||
|
||||
```bash
|
||||
css_kick <#userid or name> [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_kick #123 "AFK"
|
||||
css_kick PlayerName "Rule violation"
|
||||
css_kick @spec "Cleaning spectators"
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
"KickTime": 5
|
||||
```
|
||||
Delay in seconds before kicking (allows player to see the reason).
|
||||
|
||||
---
|
||||
|
||||
### Change Map
|
||||
|
||||
Change to a different map.
|
||||
|
||||
```bash
|
||||
css_map <mapname>
|
||||
css_changemap <mapname>
|
||||
```
|
||||
|
||||
**Permission:** `@css/changemap`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_map de_dust2
|
||||
css_changemap de_mirage
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
"DefaultMaps": [
|
||||
"de_dust2",
|
||||
"de_mirage",
|
||||
"de_inferno"
|
||||
]
|
||||
```
|
||||
|
||||
Maps in this list appear in the map change menu.
|
||||
|
||||
---
|
||||
|
||||
### Change Workshop Map
|
||||
|
||||
Change to a workshop map by ID or name.
|
||||
|
||||
```bash
|
||||
css_wsmap <name or id>
|
||||
css_changewsmap <name or id>
|
||||
css_workshop <name or id>
|
||||
```
|
||||
|
||||
**Permission:** `@css/changemap`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_wsmap 123456789
|
||||
css_wsmap aim_map
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
"WorkshopMaps": {
|
||||
"aim_map": "123456789",
|
||||
"surf_map": "987654321"
|
||||
}
|
||||
```
|
||||
|
||||
Maps configured here can be changed by name instead of ID.
|
||||
|
||||
---
|
||||
|
||||
### Change CVar
|
||||
|
||||
Change a server console variable.
|
||||
|
||||
```bash
|
||||
css_cvar <cvar> <value>
|
||||
```
|
||||
|
||||
**Permission:** `@css/cvar`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_cvar sv_cheats 1
|
||||
css_cvar mp_roundtime 5
|
||||
css_cvar mp_maxmoney 16000
|
||||
```
|
||||
|
||||
**Warning:** This is a powerful command. Only grant to trusted admins.
|
||||
|
||||
---
|
||||
|
||||
### Execute RCON Command
|
||||
|
||||
Execute any command as the server.
|
||||
|
||||
```bash
|
||||
css_rcon <command>
|
||||
```
|
||||
|
||||
**Permission:** `@css/rcon`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_rcon status
|
||||
css_rcon changelevel de_dust2
|
||||
css_rcon sv_cheats 1
|
||||
```
|
||||
|
||||
**Warning:** Extremely powerful command. Only grant to server owners.
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
"DisableDangerousCommands": true
|
||||
```
|
||||
|
||||
When enabled, prevents execution of dangerous commands via css_rcon.
|
||||
|
||||
---
|
||||
|
||||
### Restart Game
|
||||
|
||||
Restart the current game/round.
|
||||
|
||||
```bash
|
||||
css_rr
|
||||
css_rg
|
||||
css_restart
|
||||
css_restartgame
|
||||
```
|
||||
|
||||
**Permission:** `@css/generic`
|
||||
|
||||
**Notes:**
|
||||
- Restarts the current round
|
||||
- Score is reset
|
||||
- Players remain connected
|
||||
|
||||
---
|
||||
|
||||
## Permission Flags
|
||||
|
||||
Common permission flags used in CS2-SimpleAdmin:
|
||||
|
||||
| Flag | Description | Common Use |
|
||||
|------|-------------|------------|
|
||||
| `@css/generic` | Generic admin access | Basic admin menu, info commands |
|
||||
| `@css/chat` | Chat management | Gag, mute, silence |
|
||||
| `@css/kick` | Kick players | Kick, warnings, player info |
|
||||
| `@css/ban` | Ban players | Ban, banip, addban |
|
||||
| `@css/unban` | Unban players | Remove bans |
|
||||
| `@css/permban` | Permanent bans | Issue permanent bans |
|
||||
| `@css/changemap` | Change maps | Map changing |
|
||||
| `@css/cvar` | Change cvars | Server variable modification |
|
||||
| `@css/rcon` | Execute rcon | Full server control |
|
||||
| `@css/root` | Root access | All permissions, admin management |
|
||||
| `@css/slay` | Slay/respawn | Player manipulation |
|
||||
| `@css/cheats` | Cheat commands | God mode, noclip, give weapons |
|
||||
| `@css/showip` | View IPs | See player IP addresses |
|
||||
|
||||
---
|
||||
|
||||
## Immunity System
|
||||
|
||||
Immunity prevents lower-level admins from targeting higher-level admins.
|
||||
|
||||
**How it works:**
|
||||
- Each admin has an immunity value (0-100)
|
||||
- Higher immunity = more protection
|
||||
- Admins can only target players with lower immunity
|
||||
|
||||
**Example:**
|
||||
- Admin A has immunity 50
|
||||
- Admin B has immunity 30
|
||||
- Admin A can ban Admin B
|
||||
- Admin B cannot ban Admin A
|
||||
|
||||
**Best Practice:**
|
||||
- Owner: 99
|
||||
- Senior admins: 80-90
|
||||
- Moderators: 50-70
|
||||
- Trial mods: 20-40
|
||||
- Regular players: 0
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Reload Admins on Map Change
|
||||
|
||||
```json
|
||||
"ReloadAdminsEveryMapChange": false
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `true` - Reload admin permissions every map change
|
||||
- `false` - Only reload when explicitly requested (better performance)
|
||||
|
||||
### Show Activity Type
|
||||
|
||||
```json
|
||||
"ShowActivityType": 2
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `0` - Hide all admin activity
|
||||
- `1` - Show activity anonymously ("An admin banned PlayerName")
|
||||
- `2` - Show admin name ("AdminName banned PlayerName")
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Admin Management
|
||||
|
||||
1. **Use groups** - Easier to manage than individual permissions
|
||||
2. **Set appropriate immunity** - Prevent abuse
|
||||
3. **Time-limited admin** - For trial moderators
|
||||
4. **Document changes** - Keep track of who has what permissions
|
||||
|
||||
### Permission Assignment
|
||||
|
||||
**Recommended hierarchy:**
|
||||
```
|
||||
Root (@css/root, immunity 99):
|
||||
- Server owners only
|
||||
|
||||
Senior Admin (@css/ban,@css/kick,@css/chat,@css/changemap, immunity 80):
|
||||
- Trusted long-term admins
|
||||
|
||||
Moderator (@css/kick,@css/chat, immunity 50):
|
||||
- Regular moderators
|
||||
|
||||
Trial Mod (@css/kick, immunity 20):
|
||||
- New moderators on probation
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
1. **Limit @css/rcon** - Only to server owner
|
||||
2. **Limit @css/cvar** - Only to senior admins
|
||||
3. **Monitor admin actions** - Review logs regularly
|
||||
4. **Use time-limited admin** - For temporary staff
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Admin permissions not working
|
||||
|
||||
**Check:**
|
||||
1. Is admin correctly added with `css_addadmin`?
|
||||
2. Run `css_reloadadmins`
|
||||
3. Check database connection
|
||||
4. Verify SteamID format
|
||||
|
||||
### Can't target another admin
|
||||
|
||||
**Check:**
|
||||
- Your immunity level vs target's immunity
|
||||
- You need equal or higher immunity to target
|
||||
|
||||
### Commands not available
|
||||
|
||||
**Check:**
|
||||
- Your permission flags
|
||||
- Commands.json for disabled commands
|
||||
- Server console for errors
|
||||
|
||||
---
|
||||
|
||||
## Related Commands
|
||||
|
||||
- **[Ban Commands](basebans)** - Player punishment
|
||||
- **[Communication Commands](basecomms)** - Chat/voice management
|
||||
- **[Player Commands](playercommands)** - Player manipulation
|
||||
|
|
@ -1,396 +0,0 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Communication Commands
|
||||
|
||||
Commands for managing player communication (voice and text chat).
|
||||
|
||||
## Overview
|
||||
|
||||
CS2-SimpleAdmin provides three types of communication restrictions:
|
||||
|
||||
- **Gag** - Blocks text chat only
|
||||
- **Mute** - Blocks voice chat only
|
||||
- **Silence** - Blocks both text and voice chat
|
||||
|
||||
---
|
||||
|
||||
## Gag Commands
|
||||
|
||||
### Gag Player
|
||||
|
||||
Prevent a player from using text chat.
|
||||
|
||||
```bash
|
||||
css_gag <#userid or name> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_gag #123 30 "Chat spam"
|
||||
css_gag PlayerName 1440 "Advertising"
|
||||
css_gag @all 5 "Everyone quiet for 5 minutes"
|
||||
```
|
||||
|
||||
### Add Gag (Offline Player)
|
||||
|
||||
Gag a player by SteamID even if they're offline.
|
||||
|
||||
```bash
|
||||
css_addgag <steamid> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_addgag 76561198012345678 60 "Chat abuse"
|
||||
css_addgag STEAM_1:0:12345678 1440 "Spam"
|
||||
```
|
||||
|
||||
### Ungag Player
|
||||
|
||||
Remove a gag from a player.
|
||||
|
||||
```bash
|
||||
css_ungag <steamid or name> [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_ungag PlayerName "Appeal accepted"
|
||||
css_ungag 76561198012345678 "Mistake"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mute Commands
|
||||
|
||||
### Mute Player
|
||||
|
||||
Prevent a player from using voice chat.
|
||||
|
||||
```bash
|
||||
css_mute <#userid or name> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_mute #123 30 "Mic spam"
|
||||
css_mute PlayerName 60 "Loud music"
|
||||
css_mute @t 5 "T team timeout"
|
||||
```
|
||||
|
||||
### Add Mute (Offline Player)
|
||||
|
||||
Mute a player by SteamID even if they're offline.
|
||||
|
||||
```bash
|
||||
css_addmute <steamid> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_addmute 76561198012345678 120 "Voice abuse"
|
||||
css_addmute STEAM_1:0:12345678 1440 "Mic spam"
|
||||
```
|
||||
|
||||
### Unmute Player
|
||||
|
||||
Remove a mute from a player.
|
||||
|
||||
```bash
|
||||
css_unmute <steamid or name> [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_unmute PlayerName "Behavior improved"
|
||||
css_unmute 76561198012345678 "Time served"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Silence Commands
|
||||
|
||||
### Silence Player
|
||||
|
||||
Block both text and voice chat from a player.
|
||||
|
||||
```bash
|
||||
css_silence <#userid or name> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_silence #123 60 "Complete communication ban"
|
||||
css_silence PlayerName 1440 "Severe abuse"
|
||||
```
|
||||
|
||||
### Add Silence (Offline Player)
|
||||
|
||||
Silence a player by SteamID even if they're offline.
|
||||
|
||||
```bash
|
||||
css_addsilence <steamid> [time in minutes/0 perm] [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_addsilence 76561198012345678 120 "Total communication ban"
|
||||
css_addsilence STEAM_1:0:12345678 0 "Permanent silence"
|
||||
```
|
||||
|
||||
### Unsilence Player
|
||||
|
||||
Remove a silence from a player.
|
||||
|
||||
```bash
|
||||
css_unsilence <steamid or name> [reason]
|
||||
```
|
||||
|
||||
**Permission:** `@css/chat`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_unsilence PlayerName "Punishment complete"
|
||||
css_unsilence 76561198012345678 "Appeal granted"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission Requirements
|
||||
|
||||
All communication commands require the `@css/chat` permission.
|
||||
|
||||
| Command | Action | Offline Support |
|
||||
|---------|--------|----------------|
|
||||
| `css_gag` | Block text chat | No |
|
||||
| `css_addgag` | Block text chat | Yes |
|
||||
| `css_ungag` | Remove text block | Yes |
|
||||
| `css_mute` | Block voice chat | No |
|
||||
| `css_addmute` | Block voice chat | Yes |
|
||||
| `css_unmute` | Remove voice block | Yes |
|
||||
| `css_silence` | Block both | No |
|
||||
| `css_addsilence` | Block both | Yes |
|
||||
| `css_unsilence` | Remove both blocks | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Communication Penalty Types
|
||||
|
||||
### When to Use Each Type
|
||||
|
||||
**Gag (Text Only):**
|
||||
- Chat spam
|
||||
- Advertising in chat
|
||||
- Offensive messages
|
||||
- Spectator camera abuse messages
|
||||
|
||||
**Mute (Voice Only):**
|
||||
- Mic spam
|
||||
- Loud music/noise
|
||||
- Voice abuse
|
||||
- Excessive talking
|
||||
|
||||
**Silence (Both):**
|
||||
- Severe abuse cases
|
||||
- Players who switch between chat and voice to evade
|
||||
- Complete communication bans
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### UserMessage Gag Type
|
||||
|
||||
In `CS2-SimpleAdmin.json`:
|
||||
|
||||
```json
|
||||
"UserMessageGagChatType": false
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `false` - Standard gag implementation (default)
|
||||
- `true` - Alternative gag using UserMessage system
|
||||
|
||||
**Note:** Try switching this if gag commands don't work as expected.
|
||||
|
||||
### Notify Penalties on Connect
|
||||
|
||||
```json
|
||||
"NotifyPenaltiesToAdminOnConnect": true
|
||||
```
|
||||
|
||||
When enabled, admins see active communication penalties when they join:
|
||||
```
|
||||
[CS2-SimpleAdmin] PlayerName is gagged (30 minutes remaining)
|
||||
[CS2-SimpleAdmin] PlayerName is muted (1 hour remaining)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checking Penalties
|
||||
|
||||
### View Own Penalties
|
||||
|
||||
Players can check their own communication penalties:
|
||||
|
||||
```bash
|
||||
css_penalties
|
||||
css_mypenalties
|
||||
css_comms
|
||||
```
|
||||
|
||||
Shows:
|
||||
- Active gags, mutes, and silences
|
||||
- Duration remaining
|
||||
- Reason for penalty
|
||||
- Admin who issued it
|
||||
|
||||
### Admin View of Penalties
|
||||
|
||||
Use the admin menu or player info command:
|
||||
|
||||
```bash
|
||||
css_who <#userid or name>
|
||||
```
|
||||
|
||||
Shows complete penalty history including communication restrictions.
|
||||
|
||||
---
|
||||
|
||||
## Time Durations
|
||||
|
||||
Common duration values:
|
||||
|
||||
| Duration | Minutes | Use Case |
|
||||
|----------|---------|----------|
|
||||
| 1 minute | 1 | Quick warning |
|
||||
| 5 minutes | 5 | Minor spam |
|
||||
| 15 minutes | 15 | Standard timeout |
|
||||
| 30 minutes | 30 | Repeated offense |
|
||||
| 1 hour | 60 | Moderate abuse |
|
||||
| 6 hours | 360 | Serious abuse |
|
||||
| 1 day | 1440 | Severe abuse |
|
||||
| 1 week | 10080 | Extreme cases |
|
||||
| Permanent | 0 | Reserved for worst cases |
|
||||
|
||||
---
|
||||
|
||||
## Player Targeting
|
||||
|
||||
All communication commands support advanced targeting:
|
||||
|
||||
- `@all` - Target all players
|
||||
- `@ct` - Target all Counter-Terrorists
|
||||
- `@t` - Target all Terrorists
|
||||
- `@spec` - Target all spectators
|
||||
- `#123` - Target by userid
|
||||
- `PlayerName` - Target by name
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_gag @all 1 "Quiet for one minute"
|
||||
css_mute @t 5 "T team voice timeout"
|
||||
css_silence @ct 10 "CT team complete silence"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Communication Management
|
||||
|
||||
1. **Start with warnings** - Not all chat issues need immediate gag
|
||||
2. **Use appropriate durations** - Match severity to punishment
|
||||
3. **Provide reasons** - Helps players understand what they did wrong
|
||||
4. **Consider silence carefully** - Complete communication ban is harsh
|
||||
|
||||
### Gag vs Mute vs Silence
|
||||
|
||||
**Progressive Approach:**
|
||||
1. Verbal warning
|
||||
2. Gag or mute (specific to offense)
|
||||
3. Longer gag/mute for repeat offense
|
||||
4. Silence for continued abuse
|
||||
5. Temporary ban for extreme cases
|
||||
|
||||
### Documentation
|
||||
|
||||
1. **Always provide reasons** - Required for appeals
|
||||
2. **Be specific** - "Mic spam" not just "abuse"
|
||||
3. **Keep records** - Use admin logs for repeat offenders
|
||||
|
||||
---
|
||||
|
||||
## Discord Integration
|
||||
|
||||
Communication penalties can send Discord notifications when configured:
|
||||
|
||||
```json
|
||||
"DiscordPenaltyGagSettings": [...],
|
||||
"DiscordPenaltyMuteSettings": [...],
|
||||
"DiscordPenaltySilenceSettings": [...]
|
||||
```
|
||||
|
||||
Notifications include:
|
||||
- Player name and SteamID
|
||||
- Penalty type and duration
|
||||
- Reason provided
|
||||
- Admin who issued it
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Gag doesn't work
|
||||
|
||||
**Try:**
|
||||
1. Switch `UserMessageGagChatType` in config
|
||||
2. Ensure player is actually gagged (check with `css_who`)
|
||||
3. Check for conflicting plugins
|
||||
|
||||
### Mute doesn't block voice
|
||||
|
||||
**Check:**
|
||||
- Is sv_talk_enemy_dead configured correctly?
|
||||
- Are there voice management plugins conflicting?
|
||||
- Check server console for errors
|
||||
|
||||
### Penalties not persistent across maps
|
||||
|
||||
**Solution:**
|
||||
- Penalties should persist automatically
|
||||
- Check database connection
|
||||
- Verify MultiServerMode if using multiple servers
|
||||
|
||||
### Player can't see their penalties
|
||||
|
||||
**Check:**
|
||||
- Command aliases in Commands.json
|
||||
- Ensure `css_penalties` is enabled
|
||||
- Check player chat permissions
|
||||
|
||||
---
|
||||
|
||||
## Related Commands
|
||||
|
||||
- **[Ban Commands](basebans)** - For more serious offenses
|
||||
- **[Player Commands](playercommands)** - Kick, team switch
|
||||
- **[Base Commands](basecommands)** - Admin management
|
||||
|
|
@ -1,436 +0,0 @@
|
|||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Vote Commands
|
||||
|
||||
Commands for creating polls and votes on your server.
|
||||
|
||||
## Create Vote
|
||||
|
||||
Create a custom poll for players to vote on.
|
||||
|
||||
```bash
|
||||
css_vote <question> [option1] [option2] [option3] ...
|
||||
```
|
||||
|
||||
**Permission:** `@css/generic`
|
||||
|
||||
**Parameters:**
|
||||
- `question` - The question to ask players
|
||||
- `option1, option2, ...` - Vote options (at least 2 required)
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple Yes/No Vote
|
||||
|
||||
```bash
|
||||
css_vote "Should we change map?" "Yes" "No"
|
||||
```
|
||||
|
||||
**Player sees:**
|
||||
```
|
||||
Vote: Should we change map?
|
||||
1. Yes
|
||||
2. No
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Multiple Options
|
||||
|
||||
```bash
|
||||
css_vote "Which map should we play next?" "de_dust2" "de_mirage" "de_inferno" "de_nuke"
|
||||
```
|
||||
|
||||
**Player sees:**
|
||||
```
|
||||
Vote: Which map should we play next?
|
||||
1. de_dust2
|
||||
2. de_mirage
|
||||
3. de_inferno
|
||||
4. de_nuke
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rule Vote
|
||||
|
||||
```bash
|
||||
css_vote "Should we allow AWPs?" "Yes" "No" "Only one per team"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Activity Vote
|
||||
|
||||
```bash
|
||||
css_vote "What should we do?" "Surf" "Deathrun" "Competitive" "Fun Round"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How Voting Works
|
||||
|
||||
### Player Participation
|
||||
|
||||
Players vote by:
|
||||
1. Opening their chat
|
||||
2. Typing the number of their choice
|
||||
3. Or using vote menu (if available)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Player: 1 (votes for option 1)
|
||||
Player: 2 (votes for option 2)
|
||||
```
|
||||
|
||||
### Vote Duration
|
||||
|
||||
- Default vote time: ~30 seconds
|
||||
- Vote timer shows on screen
|
||||
- Results shown when vote ends
|
||||
|
||||
### Vote Results
|
||||
|
||||
After voting ends, results are displayed:
|
||||
```
|
||||
Vote Results:
|
||||
1. Yes - 12 votes (60%)
|
||||
2. No - 8 votes (40%)
|
||||
|
||||
Winner: Yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Map Voting
|
||||
|
||||
```bash
|
||||
css_vote "Next map?" "de_dust2" "de_mirage" "de_inferno"
|
||||
```
|
||||
|
||||
### Rule Changes
|
||||
|
||||
```bash
|
||||
css_vote "Enable friendly fire?" "Yes" "No"
|
||||
css_vote "Restart round?" "Yes" "No"
|
||||
```
|
||||
|
||||
### Player Punishment
|
||||
|
||||
```bash
|
||||
css_vote "Ban PlayerName for cheating?" "Yes" "No"
|
||||
css_vote "Kick AFK player?" "Yes" "No"
|
||||
```
|
||||
|
||||
### Fun Rounds
|
||||
|
||||
```bash
|
||||
css_vote "Fun round type?" "Knife only" "Deagle only" "Zeus only" "Normal"
|
||||
```
|
||||
|
||||
### Server Settings
|
||||
|
||||
```bash
|
||||
css_vote "Round time?" "2 minutes" "3 minutes" "5 minutes"
|
||||
css_vote "Max players?" "10v10" "5v5" "7v7"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Question Clarity
|
||||
|
||||
**Good Questions:**
|
||||
- Clear and concise
|
||||
- Specific
|
||||
- Easy to understand
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
✅ css_vote "Change to de_dust2?" "Yes" "No"
|
||||
❌ css_vote "Map?" "Yes" "No" # Unclear what map
|
||||
|
||||
✅ css_vote "Restart this round?" "Yes" "No"
|
||||
❌ css_vote "Restart?" "Yes" "No" # Restart what?
|
||||
```
|
||||
|
||||
### Option Limits
|
||||
|
||||
**Recommendations:**
|
||||
- 2-5 options ideal
|
||||
- Too many options confuse players
|
||||
- Keep options brief
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
✅ css_vote "Next map?" "dust2" "mirage" "inferno"
|
||||
❌ css_vote "Next map?" "de_dust2" "de_mirage" "de_inferno" "de_nuke" "de_vertigo" "de_ancient" "de_anubis"
|
||||
```
|
||||
|
||||
### Timing
|
||||
|
||||
**When to use votes:**
|
||||
- End of round
|
||||
- Between maps
|
||||
- During downtime
|
||||
- Not during active gameplay
|
||||
|
||||
**When NOT to use votes:**
|
||||
- Mid-round
|
||||
- During clutch situations
|
||||
- Too frequently
|
||||
|
||||
### Vote Spam Prevention
|
||||
|
||||
Don't spam votes:
|
||||
```bash
|
||||
❌ Multiple votes in quick succession
|
||||
❌ Overlapping votes
|
||||
❌ Votes every round
|
||||
```
|
||||
|
||||
Wait for current vote to finish before starting another.
|
||||
|
||||
---
|
||||
|
||||
## Vote Types
|
||||
|
||||
### Administrative Votes
|
||||
|
||||
**Map change:**
|
||||
```bash
|
||||
css_vote "Change map now?" "Yes" "No"
|
||||
```
|
||||
|
||||
**Server restart:**
|
||||
```bash
|
||||
css_vote "Restart server?" "Yes" "No"
|
||||
```
|
||||
|
||||
**Rule enforcement:**
|
||||
```bash
|
||||
css_vote "Kick PlayerName?" "Yes" "No"
|
||||
```
|
||||
|
||||
### Gameplay Votes
|
||||
|
||||
**Weapon restrictions:**
|
||||
```bash
|
||||
css_vote "Disable AWP?" "Yes" "No"
|
||||
```
|
||||
|
||||
**Team scramble:**
|
||||
```bash
|
||||
css_vote "Scramble teams?" "Yes" "No"
|
||||
```
|
||||
|
||||
**Round rules:**
|
||||
```bash
|
||||
css_vote "Knife round first?" "Yes" "No"
|
||||
```
|
||||
|
||||
### Event Votes
|
||||
|
||||
**Tournament:**
|
||||
```bash
|
||||
css_vote "Start tournament?" "Yes, start now" "Wait 5 minutes" "No, cancel"
|
||||
```
|
||||
|
||||
**Custom game mode:**
|
||||
```bash
|
||||
css_vote "Game mode?" "Hide and Seek" "Gungame" "Surf" "Normal"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
### Technical Limits
|
||||
|
||||
- Maximum ~10 options (depends on menu system)
|
||||
- One vote at a time
|
||||
- Requires active players to participate
|
||||
|
||||
### Permission Required
|
||||
|
||||
Only admins with `@css/generic` permission can start votes.
|
||||
|
||||
To grant permission:
|
||||
```bash
|
||||
css_addadmin STEAMID "Name" "@css/generic" 50 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vote Results Handling
|
||||
|
||||
### Manual Enforcement
|
||||
|
||||
Votes don't automatically execute actions. Admins must:
|
||||
|
||||
1. **See the results**
|
||||
2. **Manually execute the winning option**
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Start vote
|
||||
css_vote "Change to de_dust2?" "Yes" "No"
|
||||
|
||||
# If "Yes" wins, manually change map
|
||||
css_map de_dust2
|
||||
```
|
||||
|
||||
### Why Manual?
|
||||
|
||||
- Prevents abuse
|
||||
- Allows admin oversight
|
||||
- Gives control over execution
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Combining with Commands
|
||||
|
||||
Use votes to decide, then execute:
|
||||
|
||||
```bash
|
||||
# Vote on map
|
||||
css_vote "Next map?" "dust2" "mirage" "inferno"
|
||||
# If dust2 wins:
|
||||
css_map de_dust2
|
||||
|
||||
# Vote on player kick
|
||||
css_vote "Kick PlayerName?" "Yes" "No"
|
||||
# If Yes wins:
|
||||
css_kick PlayerName "Voted to be kicked"
|
||||
```
|
||||
|
||||
### Sequential Votes
|
||||
|
||||
Run multiple votes for complex decisions:
|
||||
|
||||
```bash
|
||||
# First vote: Mode
|
||||
css_vote "Game mode?" "Competitive" "Casual"
|
||||
|
||||
# If Competitive wins, second vote:
|
||||
css_vote "Round time?" "2 min" "3 min" "5 min"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Check if vote commands are enabled in:
|
||||
```
|
||||
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/Commands.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"Commands": {
|
||||
"css_vote": {
|
||||
"Aliases": [
|
||||
"css_vote"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To disable votes, remove all aliases:
|
||||
```json
|
||||
{
|
||||
"Commands": {
|
||||
"css_vote": {
|
||||
"Aliases": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Vote doesn't start
|
||||
|
||||
**Check:**
|
||||
- Do you have `@css/generic` permission?
|
||||
- Is command enabled in Commands.json?
|
||||
- Are there at least 2 options?
|
||||
|
||||
### Players can't vote
|
||||
|
||||
**Check:**
|
||||
- Vote menu is showing
|
||||
- Players know how to vote (type number in chat)
|
||||
- Vote hasn't already ended
|
||||
|
||||
### Vote results not showing
|
||||
|
||||
**Check:**
|
||||
- Wait for vote to complete
|
||||
- Check server console
|
||||
- Ensure voting system is working
|
||||
|
||||
---
|
||||
|
||||
## Permission Requirements
|
||||
|
||||
| Command | Permission | Description |
|
||||
|---------|------------|-------------|
|
||||
| `css_vote` | `@css/generic` | Create votes/polls |
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
### Effective Polling
|
||||
|
||||
1. **Ask clear questions** - No ambiguity
|
||||
2. **Limit options** - 2-4 is ideal
|
||||
3. **Time it right** - Between rounds
|
||||
4. **Follow through** - Execute winning option
|
||||
5. **Don't overuse** - Votes lose impact if spammed
|
||||
|
||||
### Community Engagement
|
||||
|
||||
Use votes to:
|
||||
- Involve community in decisions
|
||||
- Gauge player preferences
|
||||
- Create democratic server atmosphere
|
||||
- Get feedback on changes
|
||||
|
||||
### Example Scenarios
|
||||
|
||||
**New map test:**
|
||||
```bash
|
||||
css_vote "Try new map cs_office?" "Yes" "No"
|
||||
```
|
||||
|
||||
**Event planning:**
|
||||
```bash
|
||||
css_vote "Tournament this weekend?" "Saturday" "Sunday" "No thanks"
|
||||
```
|
||||
|
||||
**Rule feedback:**
|
||||
```bash
|
||||
css_vote "Keep no-AWP rule?" "Yes" "No" "Only limit to 2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Commands
|
||||
|
||||
- **[Base Commands](basecommands)** - Server management
|
||||
- **[Chat Commands](basechat)** - Announcements
|
||||
- **[Player Commands](playercommands)** - Player actions
|
||||
|
|
@ -1,516 +0,0 @@
|
|||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Player Commands
|
||||
|
||||
Commands for managing and manipulating players on your server.
|
||||
|
||||
:::note
|
||||
Many of these commands are included in the base plugin. For extended fun commands (god mode, noclip, freeze, etc.), see the [Fun Commands Module](../../modules/funcommands).
|
||||
:::
|
||||
|
||||
## Player Management
|
||||
|
||||
### Slay Player
|
||||
|
||||
Kill a player instantly.
|
||||
|
||||
```bash
|
||||
css_slay <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_slay #123
|
||||
css_slay PlayerName
|
||||
css_slay @ct # Slay all CTs
|
||||
css_slay @t # Slay all terrorists
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Punishment for rule breaking
|
||||
- Ending rounds quickly
|
||||
- Removing camping players
|
||||
|
||||
---
|
||||
|
||||
### Slap Player
|
||||
|
||||
Slap a player, dealing damage and pushing them.
|
||||
|
||||
```bash
|
||||
css_slap <#userid or name> [damage]
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `damage` - HP damage to deal (default: 0)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_slap #123 # Slap with no damage
|
||||
css_slap PlayerName 10 # Slap for 10 HP damage
|
||||
css_slap @all 5 # Slap everyone for 5 damage
|
||||
```
|
||||
|
||||
**Effects:**
|
||||
- Player is pushed in a random direction
|
||||
- Optional damage dealt
|
||||
- Makes slap sound
|
||||
|
||||
**Use cases:**
|
||||
- Funny punishment
|
||||
- Getting player attention
|
||||
- Moving AFK players
|
||||
|
||||
---
|
||||
|
||||
## Player Attributes
|
||||
|
||||
### Set Player Health
|
||||
|
||||
Set a player's health points.
|
||||
|
||||
```bash
|
||||
css_hp <#userid or name> <health>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_hp #123 100 # Set to full health
|
||||
css_hp PlayerName 1 # Set to 1 HP
|
||||
css_hp @ct 200 # Give all CTs 200 HP
|
||||
```
|
||||
|
||||
**Valid range:** 1 - 999+
|
||||
|
||||
---
|
||||
|
||||
### Set Player Speed
|
||||
|
||||
Modify a player's movement speed.
|
||||
|
||||
```bash
|
||||
css_speed <#userid or name> <speed>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `speed` - Speed multiplier (1.0 = normal, 2.0 = double speed, 0.5 = half speed)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_speed #123 1.5 # 150% speed
|
||||
css_speed PlayerName 0.5 # 50% speed (slow motion)
|
||||
css_speed @all 2.0 # Double speed for everyone
|
||||
css_speed #123 1.0 # Reset to normal speed
|
||||
```
|
||||
|
||||
**Common values:**
|
||||
- `0.5` - Slow motion
|
||||
- `1.0` - Normal (default)
|
||||
- `1.5` - Fast
|
||||
- `2.0` - Very fast
|
||||
- `3.0` - Extremely fast
|
||||
|
||||
**Note:** Speed persists across respawns until reset.
|
||||
|
||||
---
|
||||
|
||||
### Set Player Gravity
|
||||
|
||||
Modify a player's gravity.
|
||||
|
||||
```bash
|
||||
css_gravity <#userid or name> <gravity>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Parameters:**
|
||||
- `gravity` - Gravity multiplier (1.0 = normal, 0.5 = moon jump, 2.0 = heavy)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_gravity #123 0.5 # Moon jump
|
||||
css_gravity PlayerName 2.0 # Heavy gravity
|
||||
css_gravity @all 0.1 # Super jump for everyone
|
||||
css_gravity #123 1.0 # Reset to normal
|
||||
```
|
||||
|
||||
**Common values:**
|
||||
- `0.1` - Super high jumps
|
||||
- `0.5` - Moon gravity
|
||||
- `1.0` - Normal (default)
|
||||
- `2.0` - Heavy/fast falling
|
||||
|
||||
**Note:** Gravity persists across respawns until reset.
|
||||
|
||||
---
|
||||
|
||||
### Set Player Money
|
||||
|
||||
Set a player's money amount.
|
||||
|
||||
```bash
|
||||
css_money <#userid or name> <amount>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_money #123 16000 # Max money
|
||||
css_money PlayerName 0 # Remove all money
|
||||
css_money @ct 10000 # Give all CTs $10,000
|
||||
```
|
||||
|
||||
**Valid range:** 0 - 65535 (CS2 engine limit)
|
||||
|
||||
---
|
||||
|
||||
## Team Management
|
||||
|
||||
### Switch Player Team
|
||||
|
||||
Move a player to a different team.
|
||||
|
||||
```bash
|
||||
css_team <#userid or name> [ct/t/spec] [-k]
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Parameters:**
|
||||
- `ct` - Counter-Terrorist team
|
||||
- `t` - Terrorist team
|
||||
- `spec` - Spectators
|
||||
- `-k` - Kill player during switch (optional)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_team #123 ct # Move to CT
|
||||
css_team PlayerName t # Move to T
|
||||
css_team @spec t # Move all spectators to T
|
||||
css_team #123 ct -k # Move to CT and kill
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
"TeamSwitchType": 1
|
||||
```
|
||||
|
||||
Determines team switch behavior.
|
||||
|
||||
---
|
||||
|
||||
### Rename Player
|
||||
|
||||
Temporarily rename a player.
|
||||
|
||||
```bash
|
||||
css_rename <#userid or name> <new name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_rename #123 "NewName"
|
||||
css_rename PlayerName "RenamedPlayer"
|
||||
css_rename @all "Everyone"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Rename is temporary (resets on reconnect)
|
||||
- For permanent rename, use `css_prename`
|
||||
|
||||
---
|
||||
|
||||
### Permanent Rename
|
||||
|
||||
Permanently force a player's name.
|
||||
|
||||
```bash
|
||||
css_prename <#userid or name> <new name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/ban`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_prename #123 "EnforcedName"
|
||||
css_prename PlayerName "NewIdentity"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Name is enforced even after reconnect, map change and server restart
|
||||
- Stored in the database (`sa_renames`) and shared by every server using that database
|
||||
- Player cannot change it; `css_prename <player>` with no name removes the rename
|
||||
- Useful for offensive names
|
||||
|
||||
---
|
||||
|
||||
## Weapon Management
|
||||
|
||||
### Give Weapon
|
||||
|
||||
Give a weapon to a player.
|
||||
|
||||
```bash
|
||||
css_give <#userid or name> <weapon>
|
||||
```
|
||||
|
||||
**Permission:** `@css/cheats`
|
||||
|
||||
**Weapon names:**
|
||||
- Rifles: `ak47`, `m4a1`, `m4a1_silencer`, `aug`, `sg556`, `awp`
|
||||
- SMGs: `mp5sd`, `mp7`, `mp9`, `p90`, `ump45`
|
||||
- Heavy: `nova`, `xm1014`, `mag7`, `m249`, `negev`
|
||||
- Pistols: `deagle`, `elite`, `fiveseven`, `glock`, `hkp2000`, `p250`, `tec9`, `usp_silencer`
|
||||
- Grenades: `flashbang`, `hegrenade`, `smokegrenade`, `molotov`, `incgrenade`, `decoy`
|
||||
- Equipment: `kevlar`, `assaultsuit`, `defuser`, `knife`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_give #123 awp
|
||||
css_give PlayerName ak47
|
||||
css_give @ct m4a1
|
||||
css_give @all deagle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Strip Weapons
|
||||
|
||||
Remove all weapons from a player.
|
||||
|
||||
```bash
|
||||
css_strip <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/slay`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_strip #123
|
||||
css_strip PlayerName
|
||||
css_strip @t # Disarm all terrorists
|
||||
```
|
||||
|
||||
**Effects:**
|
||||
- Removes all weapons
|
||||
- Leaves player with knife only
|
||||
- Removes grenades and equipment
|
||||
|
||||
---
|
||||
|
||||
## Teleportation
|
||||
|
||||
### Teleport to Player
|
||||
|
||||
Teleport yourself to another player.
|
||||
|
||||
```bash
|
||||
css_tp <#userid or name>
|
||||
css_tpto <#userid or name>
|
||||
css_goto <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_tp #123
|
||||
css_goto PlayerName
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Checking player behavior
|
||||
- Admin help
|
||||
- Spectating suspicious players
|
||||
|
||||
---
|
||||
|
||||
### Teleport Player to You
|
||||
|
||||
Bring a player to your location.
|
||||
|
||||
```bash
|
||||
css_bring <#userid or name>
|
||||
css_tphere <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/kick`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_bring #123
|
||||
css_tphere PlayerName
|
||||
css_bring @all # Bring everyone to you
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Moving stuck players
|
||||
- Gathering players
|
||||
- Admin events
|
||||
|
||||
---
|
||||
|
||||
### Respawn Player
|
||||
|
||||
Respawn a dead player.
|
||||
|
||||
```bash
|
||||
css_respawn <#userid or name>
|
||||
```
|
||||
|
||||
**Permission:** `@css/cheats`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
css_respawn #123
|
||||
css_respawn PlayerName
|
||||
css_respawn @ct # Respawn all dead CTs
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Player spawns at spawn point
|
||||
- Equipped with default weapons
|
||||
- Can break competitive balance
|
||||
|
||||
---
|
||||
|
||||
## Player Targeting
|
||||
|
||||
All player commands support advanced targeting:
|
||||
|
||||
### Target Syntax
|
||||
|
||||
- `@all` - All players
|
||||
- `@ct` - All Counter-Terrorists
|
||||
- `@t` - All Terrorists
|
||||
- `@spec` - All spectators
|
||||
- `@alive` - All alive players
|
||||
- `@dead` - All dead players
|
||||
- `@bot` - All bots
|
||||
- `@human` - All human players
|
||||
- `@me` - Yourself
|
||||
- `#123` - Specific user ID
|
||||
- `PlayerName` - By name (partial match supported)
|
||||
|
||||
### Multiple Targets
|
||||
|
||||
```bash
|
||||
css_slay @ct # Kills all CTs
|
||||
css_hp @all 200 # Give everyone 200 HP
|
||||
css_speed @t 2.0 # Make all Ts fast
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission Requirements
|
||||
|
||||
| Command | Required Permission | Description |
|
||||
|---------|-------------------|-------------|
|
||||
| `css_slay` | `@css/slay` | Kill players |
|
||||
| `css_slap` | `@css/slay` | Slap players |
|
||||
| `css_hp` | `@css/slay` | Set health |
|
||||
| `css_speed` | `@css/slay` | Modify speed |
|
||||
| `css_gravity` | `@css/slay` | Modify gravity |
|
||||
| `css_money` | `@css/slay` | Set money |
|
||||
| `css_team` | `@css/kick` | Change team |
|
||||
| `css_rename` | `@css/kick` | Temporary rename |
|
||||
| `css_prename` | `@css/ban` | Permanent rename |
|
||||
| `css_give` | `@css/cheats` | Give weapons |
|
||||
| `css_strip` | `@css/slay` | Remove weapons |
|
||||
| `css_tp` | `@css/kick` | Teleport to player |
|
||||
| `css_bring` | `@css/kick` | Bring player |
|
||||
| `css_respawn` | `@css/cheats` | Respawn players |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Punishment Commands
|
||||
|
||||
**Slay:**
|
||||
- Use for rule violations
|
||||
- Better than kick for minor issues
|
||||
- Allows player to stay and learn
|
||||
|
||||
**Slap:**
|
||||
- Lighter punishment
|
||||
- Good for warnings
|
||||
- Can be funny/entertaining
|
||||
|
||||
### Gameplay Modification
|
||||
|
||||
**HP/Speed/Gravity:**
|
||||
- Use for events/fun rounds
|
||||
- Don't abuse during competitive play
|
||||
- Reset to normal after use
|
||||
|
||||
**Respawn:**
|
||||
- Very disruptive to gameplay
|
||||
- Use sparingly
|
||||
- Good for fixing bugs/mistakes
|
||||
|
||||
### Team Management
|
||||
|
||||
**Team switching:**
|
||||
- Balance teams fairly
|
||||
- Don't abuse for winning
|
||||
- Use `-k` flag for competitive integrity
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Team Switch Behavior
|
||||
|
||||
```json
|
||||
"TeamSwitchType": 1
|
||||
```
|
||||
|
||||
Controls how team switching works.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Speed/Gravity not persisting
|
||||
|
||||
**Solution:** These are maintained by a timer. If they reset:
|
||||
- Check server console for errors
|
||||
- Ensure plugin is loaded correctly
|
||||
- Try reapplying the modification
|
||||
|
||||
### Can't teleport
|
||||
|
||||
**Check:**
|
||||
- Target player is connected
|
||||
- You have correct permissions
|
||||
- Both players are valid
|
||||
|
||||
### Give weapon not working
|
||||
|
||||
**Check:**
|
||||
- Weapon name is correct
|
||||
- Player is alive
|
||||
- Player has inventory space
|
||||
|
||||
---
|
||||
|
||||
## Related Commands
|
||||
|
||||
- **[Fun Commands Module](../../modules/funcommands)** - Extended fun commands (freeze, god mode, noclip)
|
||||
- **[Ban Commands](basebans)** - Punishment commands
|
||||
- **[Base Commands](basecommands)** - Server management
|
||||
|
|
@ -1,397 +0,0 @@
|
|||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
Learn how to configure CS2-SimpleAdmin to suit your server's needs.
|
||||
|
||||
## Configuration File Location
|
||||
|
||||
The main configuration file is located at:
|
||||
```
|
||||
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json
|
||||
```
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
The configuration file is divided into several sections:
|
||||
|
||||
### Database Configuration
|
||||
|
||||
Configure your database connection:
|
||||
|
||||
```json
|
||||
"DatabaseConfig": {
|
||||
"DatabaseType": "SQLite",
|
||||
"SqliteFilePath": "cs2-simpleadmin.sqlite",
|
||||
"DatabaseHost": "",
|
||||
"DatabasePort": 3306,
|
||||
"DatabaseUser": "",
|
||||
"DatabasePassword": "",
|
||||
"DatabaseName": "",
|
||||
"DatabaseSSlMode": "preferred"
|
||||
}
|
||||
```
|
||||
|
||||
**Database Types:**
|
||||
- `SQLite` - Local database file (good for single server)
|
||||
- `MySQL` - MySQL/MariaDB server (required for multi-server setups)
|
||||
|
||||
**MySQL Example:**
|
||||
```json
|
||||
"DatabaseConfig": {
|
||||
"DatabaseType": "MySQL",
|
||||
"DatabaseHost": "localhost",
|
||||
"DatabasePort": 3306,
|
||||
"DatabaseUser": "cs2admin",
|
||||
"DatabasePassword": "your_password",
|
||||
"DatabaseName": "cs2_simpleadmin",
|
||||
"DatabaseSSlMode": "preferred"
|
||||
}
|
||||
```
|
||||
|
||||
### Other Settings
|
||||
|
||||
General plugin settings:
|
||||
|
||||
```json
|
||||
"OtherSettings": {
|
||||
"ShowActivityType": 2,
|
||||
"TeamSwitchType": 1,
|
||||
"KickTime": 5,
|
||||
"BanType": 1,
|
||||
"TimeMode": 1,
|
||||
"DisableDangerousCommands": true,
|
||||
"MaxBanDuration": 10080,
|
||||
"MaxMuteDuration": 10080,
|
||||
"ExpireOldIpBans": 0,
|
||||
"ReloadAdminsEveryMapChange": false,
|
||||
"DisconnectedPlayersHistoryCount": 10,
|
||||
"NotifyPenaltiesToAdminOnConnect": true,
|
||||
"ShowBanMenuIfNoTime": true,
|
||||
"UserMessageGagChatType": false,
|
||||
"CheckMultiAccountsByIp": true,
|
||||
"AdditionalCommandsToLog": [],
|
||||
"IgnoredIps": []
|
||||
}
|
||||
```
|
||||
|
||||
**Settings Explained:**
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `ShowActivityType` | How to display admin actions (0=hide, 1=anonymous, 2=show name) | 2 |
|
||||
| `TeamSwitchType` | Team switch behavior | 1 |
|
||||
| `KickTime` | Delay before kicking player (seconds) | 5 |
|
||||
| `BanType` | Ban type (1=SteamID, 2=IP, 3=Both) | 1 |
|
||||
| `TimeMode` | Time display mode | 1 |
|
||||
| `DisableDangerousCommands` | Disable potentially dangerous commands | true |
|
||||
| `MaxBanDuration` | Maximum ban duration in minutes (0=unlimited) | 10080 |
|
||||
| `MaxMuteDuration` | Maximum mute duration in minutes (0=unlimited) | 10080 |
|
||||
| `ExpireOldIpBans` | Auto-expire IP bans after X days (0=disabled) | 0 |
|
||||
| `ReloadAdminsEveryMapChange` | Reload admin permissions on map change | false |
|
||||
| `DisconnectedPlayersHistoryCount` | Number of disconnected players to track | 10 |
|
||||
| `NotifyPenaltiesToAdminOnConnect` | Show penalties to admins when they connect | true |
|
||||
| `ShowBanMenuIfNoTime` | Show ban menu even without time parameter | true |
|
||||
| `UserMessageGagChatType` | Use UserMessage for gag (alternative chat blocking) | false |
|
||||
| `CheckMultiAccountsByIp` | Detect multiple accounts from same IP | true |
|
||||
| `AdditionalCommandsToLog` | Array of additional commands to log | [] |
|
||||
| `IgnoredIps` | IPs to ignore in multi-account detection | [] |
|
||||
|
||||
### Metrics and Updates
|
||||
|
||||
```json
|
||||
"EnableMetrics": true,
|
||||
"EnableUpdateCheck": true
|
||||
```
|
||||
|
||||
- `EnableMetrics` - Send anonymous usage statistics
|
||||
- `EnableUpdateCheck` - Check for plugin updates on load
|
||||
|
||||
### Timezone
|
||||
|
||||
Set your server's timezone for accurate timestamps:
|
||||
|
||||
```json
|
||||
"Timezone": "UTC"
|
||||
```
|
||||
|
||||
See the [list of timezones](#timezone-list) below.
|
||||
|
||||
### Warning Thresholds
|
||||
|
||||
Configure automatic actions when players reach warning thresholds:
|
||||
|
||||
```json
|
||||
"WarnThreshold": {
|
||||
"998": "css_addban STEAMID64 60 \"3/4 Warn\"",
|
||||
"999": "css_ban #USERID 120 \"4/4 Warn\""
|
||||
}
|
||||
```
|
||||
|
||||
**Example:** Automatically ban a player for 60 minutes when they receive their 3rd warning.
|
||||
|
||||
### Multi-Server Mode
|
||||
|
||||
Enable if you're running multiple servers with a shared database:
|
||||
|
||||
```json
|
||||
"MultiServerMode": true
|
||||
```
|
||||
|
||||
When enabled:
|
||||
- Bans are shared across all servers
|
||||
- Admin permissions can be global or server-specific
|
||||
- Player data is synchronized
|
||||
|
||||
### Discord Integration
|
||||
|
||||
Send notifications to Discord webhooks:
|
||||
|
||||
```json
|
||||
"Discord": {
|
||||
"DiscordLogWebhook": "https://discord.com/api/webhooks/...",
|
||||
"DiscordPenaltyBanSettings": [...],
|
||||
"DiscordPenaltyMuteSettings": [...],
|
||||
"DiscordPenaltyGagSettings": [...],
|
||||
"DiscordPenaltySilenceSettings": [...],
|
||||
"DiscordPenaltyWarnSettings": [...],
|
||||
"DiscordAssociatedAccountsSettings": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**Webhook Settings:**
|
||||
Each penalty type can have its own webhook configuration:
|
||||
|
||||
```json
|
||||
"DiscordPenaltyBanSettings": [
|
||||
{
|
||||
"name": "Color",
|
||||
"value": "#FF0000"
|
||||
},
|
||||
{
|
||||
"name": "Webhook",
|
||||
"value": "https://discord.com/api/webhooks/YOUR_WEBHOOK_HERE"
|
||||
},
|
||||
{
|
||||
"name": "ThumbnailUrl",
|
||||
"value": "https://example.com/ban-icon.png"
|
||||
},
|
||||
{
|
||||
"name": "ImageUrl",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "Footer",
|
||||
"value": "CS2-SimpleAdmin"
|
||||
},
|
||||
{
|
||||
"name": "Time",
|
||||
"value": "{relative}"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Available Placeholders:**
|
||||
- `{relative}` - Relative timestamp
|
||||
- `{fixed}` - Fixed timestamp
|
||||
|
||||
### Map Configuration
|
||||
|
||||
Configure default maps and workshop maps:
|
||||
|
||||
```json
|
||||
"DefaultMaps": [
|
||||
"de_dust2",
|
||||
"de_mirage",
|
||||
"de_inferno"
|
||||
],
|
||||
"WorkshopMaps": {
|
||||
"aim_map": "123456789",
|
||||
"surf_map": "987654321"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Server Commands
|
||||
|
||||
Add custom commands to the admin menu:
|
||||
|
||||
```json
|
||||
"CustomServerCommands": [
|
||||
{
|
||||
"Flag": "@css/root",
|
||||
"DisplayName": "Reload Admins",
|
||||
"Command": "css_reloadadmins"
|
||||
},
|
||||
{
|
||||
"Flag": "@css/cheats",
|
||||
"DisplayName": "Enable sv_cheats",
|
||||
"Command": "sv_cheats 1"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Menu Configuration
|
||||
|
||||
Configure menu appearance and options:
|
||||
|
||||
```json
|
||||
"MenuConfig": {
|
||||
"MenuType": "selectable",
|
||||
"Durations": [
|
||||
{ "name": "1 minute", "duration": 1 },
|
||||
{ "name": "5 minutes", "duration": 5 },
|
||||
{ "name": "15 minutes", "duration": 15 },
|
||||
{ "name": "1 hour", "duration": 60 },
|
||||
{ "name": "1 day", "duration": 1440 },
|
||||
{ "name": "7 days", "duration": 10080 },
|
||||
{ "name": "14 days", "duration": 20160 },
|
||||
{ "name": "30 days", "duration": 43200 },
|
||||
{ "name": "Permanent", "duration": 0 }
|
||||
],
|
||||
"BanReasons": [
|
||||
"Hacking",
|
||||
"Voice Abuse",
|
||||
"Chat Abuse",
|
||||
"Admin disrespect",
|
||||
"Other"
|
||||
],
|
||||
"KickReasons": [
|
||||
"Voice Abuse",
|
||||
"Chat Abuse",
|
||||
"Admin disrespect",
|
||||
"Other"
|
||||
],
|
||||
"WarnReasons": [
|
||||
"Voice Abuse",
|
||||
"Chat Abuse",
|
||||
"Admin disrespect",
|
||||
"Other"
|
||||
],
|
||||
"MuteReasons": [
|
||||
"Advertising",
|
||||
"Spamming",
|
||||
"Spectator camera abuse",
|
||||
"Hate",
|
||||
"Admin disrespect",
|
||||
"Other"
|
||||
],
|
||||
"AdminFlags": [
|
||||
{ "name": "Generic", "flag": "@css/generic" },
|
||||
{ "name": "Chat", "flag": "@css/chat" },
|
||||
{ "name": "Change Map", "flag": "@css/changemap" },
|
||||
{ "name": "Slay", "flag": "@css/slay" },
|
||||
{ "name": "Kick", "flag": "@css/kick" },
|
||||
{ "name": "Ban", "flag": "@css/ban" },
|
||||
{ "name": "Perm Ban", "flag": "@css/permban" },
|
||||
{ "name": "Unban", "flag": "@css/unban" },
|
||||
{ "name": "Show IP", "flag": "@css/showip" },
|
||||
{ "name": "Cvar", "flag": "@css/cvar" },
|
||||
{ "name": "Rcon", "flag": "@css/rcon" },
|
||||
{ "name": "Root (all flags)", "flag": "@css/root" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Timezone List
|
||||
|
||||
<details>
|
||||
<summary>Click to expand timezone list</summary>
|
||||
|
||||
```
|
||||
UTC
|
||||
America/New_York
|
||||
America/Chicago
|
||||
America/Denver
|
||||
America/Los_Angeles
|
||||
Europe/London
|
||||
Europe/Paris
|
||||
Europe/Berlin
|
||||
Europe/Warsaw
|
||||
Europe/Moscow
|
||||
Asia/Tokyo
|
||||
Asia/Shanghai
|
||||
Asia/Dubai
|
||||
Australia/Sydney
|
||||
Pacific/Auckland
|
||||
... (and many more)
|
||||
```
|
||||
|
||||
For a complete list, see the info.txt file in the documentation folder.
|
||||
|
||||
</details>
|
||||
|
||||
## Commands Configuration
|
||||
|
||||
You can customize command aliases in:
|
||||
```
|
||||
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/Commands.json
|
||||
```
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"Commands": {
|
||||
"css_ban": {
|
||||
"Aliases": [
|
||||
"css_ban",
|
||||
"css_ban2"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to:
|
||||
- **Disable commands** - Remove all aliases from the array
|
||||
- **Add aliases** - Add multiple command variations
|
||||
- **Rename commands** - Change the command name while keeping functionality
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security
|
||||
|
||||
1. **Use MySQL in production** - SQLite is not suitable for multi-server setups
|
||||
2. **Set MaxBanDuration** - Prevent accidental permanent bans
|
||||
3. **Enable DisableDangerousCommands** - Protect against accidental server crashes
|
||||
4. **Use strong database passwords** - If using MySQL
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Set ReloadAdminsEveryMapChange to false** - Unless you frequently modify admin permissions
|
||||
2. **Limit DisconnectedPlayersHistoryCount** - Reduce memory usage
|
||||
3. **Use database indices** - Migrations create these automatically
|
||||
|
||||
### Multi-Server Setup
|
||||
|
||||
1. **Enable MultiServerMode** - Share data across servers
|
||||
2. **Use MySQL** - Required for multi-server
|
||||
3. **Configure server IDs** - Each server gets a unique ID automatically
|
||||
4. **Test penalties** - Ensure bans work across all servers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Changes not taking effect
|
||||
|
||||
**Solution:** Reload the plugin or restart the server:
|
||||
```
|
||||
css_plugins reload CS2-SimpleAdmin
|
||||
```
|
||||
|
||||
### Discord webhooks not working
|
||||
|
||||
**Solution:**
|
||||
- Verify webhook URL is correct
|
||||
- Check that the webhook is not deleted in Discord
|
||||
- Ensure server has internet access
|
||||
|
||||
### TimeMode issues
|
||||
|
||||
**Solution:** Set your timezone correctly in the configuration
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Learn admin commands](commands/basebans)** - Browse available commands
|
||||
- **[Set up admins](#)** - Add your admin team
|
||||
- **[Configure modules](../modules/intro)** - Extend functionality
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
This guide will help you install CS2-SimpleAdmin on your Counter-Strike 2 server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing CS2-SimpleAdmin, ensure you have the following dependencies installed:
|
||||
|
||||
### Required Dependencies
|
||||
|
||||
1. **[CounterStrikeSharp](https://github.com/roflmuffin/CounterStrikeSharp/)** (v1.0.340+)
|
||||
- The core framework for CS2 server plugins
|
||||
|
||||
2. **[AnyBaseLibCS2](https://github.com/NickFox007/AnyBaseLibCS2)**
|
||||
- Required by PlayerSettings
|
||||
|
||||
3. **[PlayerSettings](https://github.com/NickFox007/PlayerSettingsCS2)**
|
||||
- Required by MenuManager
|
||||
|
||||
4. **[MenuManagerCS2](https://github.com/NickFox007/MenuManagerCS2)**
|
||||
- Provides the menu system
|
||||
|
||||
### Database Requirements
|
||||
|
||||
You'll need either:
|
||||
- **MySQL** server (recommended for production)
|
||||
- **SQLite** (built-in, good for testing)
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Download the Plugin
|
||||
|
||||
Download the latest release from the [GitHub Releases page](https://github.com/daffyyyy/CS2-SimpleAdmin/releases).
|
||||
|
||||
You can either:
|
||||
- Download the pre-built release ZIP file
|
||||
- Clone the repository and build from source
|
||||
|
||||
### 2. Extract Files
|
||||
|
||||
Extract the downloaded files to your server's CounterStrikeSharp directory:
|
||||
|
||||
```
|
||||
game/csgo/addons/counterstrikesharp/plugins/CS2-SimpleAdmin/
|
||||
```
|
||||
|
||||
Your directory structure should look like this:
|
||||
|
||||
```
|
||||
csgo/
|
||||
└── addons/
|
||||
└── counterstrikesharp/
|
||||
├── plugins/
|
||||
│ └── CS2-SimpleAdmin/
|
||||
│ ├── CS2-SimpleAdmin.dll
|
||||
│ ├── lang/
|
||||
│ └── ... (other files)
|
||||
└── shared/
|
||||
└── CS2-SimpleAdminApi/
|
||||
└── CS2-SimpleAdminApi.dll
|
||||
```
|
||||
|
||||
### 3. First Launch
|
||||
|
||||
Start your server. On the first launch, CS2-SimpleAdmin will:
|
||||
|
||||
1. Create a configuration file at:
|
||||
```
|
||||
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json
|
||||
```
|
||||
|
||||
2. Create a database (if using SQLite):
|
||||
```
|
||||
addons/counterstrikesharp/plugins/CS2-SimpleAdmin/cs2-simpleadmin.sqlite
|
||||
```
|
||||
|
||||
3. Apply database migrations automatically
|
||||
|
||||
### 4. Configure the Plugin
|
||||
|
||||
Edit the generated configuration file to match your server setup.
|
||||
|
||||
See the [Configuration Guide](configuration) for detailed information.
|
||||
|
||||
### 5. Restart Your Server
|
||||
|
||||
After editing the configuration, restart your server or reload the plugin:
|
||||
|
||||
```bash
|
||||
css_plugins reload CS2-SimpleAdmin
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
If you want to build CS2-SimpleAdmin from source:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 8.0 SDK
|
||||
- Git
|
||||
|
||||
### Build Steps
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/daffyyyy/CS2-SimpleAdmin.git
|
||||
cd CS2-SimpleAdmin
|
||||
```
|
||||
|
||||
2. **Restore dependencies:**
|
||||
```bash
|
||||
dotnet restore CS2-SimpleAdmin.sln
|
||||
```
|
||||
|
||||
3. **Build the solution:**
|
||||
```bash
|
||||
dotnet build CS2-SimpleAdmin.sln -c Release
|
||||
```
|
||||
|
||||
4. **Build output location:**
|
||||
```
|
||||
CS2-SimpleAdmin/bin/Release/net8.0/
|
||||
CS2-SimpleAdminApi/bin/Release/net8.0/
|
||||
```
|
||||
|
||||
5. **Copy to server:**
|
||||
- Copy `CS2-SimpleAdmin.dll` and its dependencies to `plugins/CS2-SimpleAdmin/`
|
||||
- Copy `CS2-SimpleAdminApi.dll` to `shared/CS2-SimpleAdminApi/`
|
||||
|
||||
## Verification
|
||||
|
||||
To verify the installation was successful:
|
||||
|
||||
1. **Check server console** for the plugin load message:
|
||||
```
|
||||
[CS2-SimpleAdmin] Plugin loaded successfully
|
||||
```
|
||||
|
||||
2. **Run an admin command** in-game:
|
||||
```
|
||||
css_admin
|
||||
```
|
||||
|
||||
3. **Check the logs** at:
|
||||
```
|
||||
addons/counterstrikesharp/logs/CS2-SimpleAdmin*.txt
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin doesn't load
|
||||
|
||||
**Solution:** Ensure all required dependencies are installed:
|
||||
- CounterStrikeSharp (latest version)
|
||||
- AnyBaseLibCS2
|
||||
- PlayerSettings
|
||||
- MenuManagerCS2
|
||||
|
||||
### Database connection errors
|
||||
|
||||
**Solution:**
|
||||
- For MySQL: Verify database credentials in the config file
|
||||
- For SQLite: Ensure the plugin has write permissions in its directory
|
||||
|
||||
### Commands not working
|
||||
|
||||
**Solution:**
|
||||
- Check that you have admin permissions configured
|
||||
- Verify the commands are enabled in `Commands.json`
|
||||
- Check server console for error messages
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Configure your plugin](configuration)** - Set up database, permissions, and features
|
||||
- **[Learn the commands](commands/basebans)** - Browse available admin commands
|
||||
- **[Add admins](#)** - Set up your admin team
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check the [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues) for similar problems
|
||||
2. Review server logs for error messages
|
||||
3. Ask for help on [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
Welcome to **CS2-SimpleAdmin** - a comprehensive administration plugin for Counter-Strike 2 servers built with C# (.NET 8.0) for CounterStrikeSharp.
|
||||
|
||||
## What is CS2-SimpleAdmin?
|
||||
|
||||
CS2-SimpleAdmin is a powerful server administration tool that provides comprehensive features for managing your Counter-Strike 2 server. It offers:
|
||||
|
||||
- **Player Management** - Ban, kick, mute, gag, and warn players
|
||||
- **Admin System** - Flexible permission system with flags and groups
|
||||
- **Multi-Server Support** - Manage multiple servers with a shared database
|
||||
- **Discord Integration** - Send notifications to Discord webhooks
|
||||
- **Menu System** - Easy-to-use admin menus
|
||||
- **Extensive Commands** - Over 50 admin commands
|
||||
- **Module Support** - Extend functionality with custom modules
|
||||
|
||||
## Key Features
|
||||
|
||||
### 🛡️ Comprehensive Penalty System
|
||||
- **Bans** - Ban players by SteamID or IP address
|
||||
- **Mutes** - Mute voice communication
|
||||
- **Gags** - Gag text chat
|
||||
- **Silence** - Block both voice and text
|
||||
- **Warnings** - Progressive warning system with auto-escalation
|
||||
|
||||
### 👥 Flexible Admin System
|
||||
- Permission-based access control using flags
|
||||
- Admin groups for easy management
|
||||
- Immunity levels to prevent abuse
|
||||
- Server-specific or global admin assignments
|
||||
|
||||
### 🗄️ Database Support
|
||||
- **MySQL** - For production environments
|
||||
- **SQLite** - For quick setup and testing
|
||||
- Automatic migration system
|
||||
- Multi-server mode with shared data
|
||||
|
||||
### 🎮 User-Friendly Interface
|
||||
- Interactive admin menus
|
||||
- In-game admin panel
|
||||
- Player selection menus
|
||||
- Duration and reason selection
|
||||
|
||||
### 🔧 Extensibility
|
||||
- Public API for module development
|
||||
- Event system for custom integrations
|
||||
- Command registration system
|
||||
- Menu builder API
|
||||
|
||||
## Requirements
|
||||
|
||||
Before installing CS2-SimpleAdmin, make sure you have:
|
||||
|
||||
- [CounterStrikeSharp](https://github.com/roflmuffin/CounterStrikeSharp/) (v1.0.340+)
|
||||
- [AnyBaseLibCS2](https://github.com/NickFox007/AnyBaseLibCS2)
|
||||
- [PlayerSettings](https://github.com/NickFox007/PlayerSettingsCS2)
|
||||
- [MenuManagerCS2](https://github.com/NickFox007/MenuManagerCS2)
|
||||
- MySQL database (or use SQLite for testing)
|
||||
|
||||
## Quick Links
|
||||
|
||||
- **[Installation Guide](installation)** - Get started with CS2-SimpleAdmin
|
||||
- **[Configuration](configuration)** - Learn how to configure the plugin
|
||||
- **[Commands](commands/basebans)** - Browse all available commands
|
||||
- **[GitHub Repository](https://github.com/daffyyyy/CS2-SimpleAdmin)** - Source code and releases
|
||||
|
||||
## Community & Support
|
||||
|
||||
Need help or want to contribute?
|
||||
|
||||
- **Issues** - Report bugs on [GitHub Issues](https://github.com/daffyyyy/CS2-SimpleAdmin/issues)
|
||||
- **Discussions** - Ask questions on [GitHub Discussions](https://github.com/daffyyyy/CS2-SimpleAdmin/discussions)
|
||||
- **Pull Requests** - Contribute improvements
|
||||
|
||||
## License
|
||||
|
||||
CS2-SimpleAdmin is open-source software. Check the [GitHub repository](https://github.com/daffyyyy/CS2-SimpleAdmin) for license details.
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
// @ts-check
|
||||
// `@type` JSDoc annotations allow editor autocompletion and type checking
|
||||
// (when paired with `@ts-check`).
|
||||
// There are various equivalent ways to declare your Docusaurus config.
|
||||
// See: https://docusaurus.io/docs/api/docusaurus-config
|
||||
|
||||
import {themes as prismThemes} from 'prism-react-renderer';
|
||||
|
||||
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'CS2-SimpleAdmin',
|
||||
tagline: 'Comprehensive administration plugin for Counter-Strike 2 servers',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future
|
||||
future: {
|
||||
v4: true, // Improve compatibility with the upcoming Docusaurus v4
|
||||
},
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://cs2-simpleadmin.daffyy.dev',
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: '/',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: 'daffyyyy', // Usually your GitHub org/user name.
|
||||
projectName: 'CS2-SimpleAdmin', // Usually your repo name.
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
|
||||
// Even if you don't use internationalization, you can use this field to set
|
||||
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||
// may want to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: './sidebars.js',
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
// editUrl:
|
||||
// '',
|
||||
},
|
||||
blog: false, // Disable blog
|
||||
theme: {
|
||||
customCss: './src/css/custom.css',
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
// Replace with your project's social card
|
||||
image: 'img/docusaurus-social-card.jpg',
|
||||
metadata: [
|
||||
{name: 'keywords', content: 'CS2, Counter-Strike 2, admin plugin, server management, bans, mutes, CounterStrikeSharp'},
|
||||
{name: 'description', content: 'Comprehensive administration plugin for Counter-Strike 2 servers with ban management, multi-server support, and extensible API'},
|
||||
{name: 'author', content: 'daffyyyy'},
|
||||
{property: 'og:title', content: 'CS2-SimpleAdmin - Admin Plugin for Counter-Strike 2'},
|
||||
{property: 'og:description', content: 'Comprehensive administration plugin for CS2 servers. Manage bans, mutes, warnings, and permissions with multi-server support.'},
|
||||
{property: 'og:type', content: 'website'},
|
||||
{property: 'og:url', content: 'https://cs2-simpleadmin.daffyy.dev'},
|
||||
{property: 'og:image', content: 'https://cs2-simpleadmin.daffyy.dev/img/docusaurus-social-card.jpg'},
|
||||
{name: 'twitter:card', content: 'summary_large_image'},
|
||||
{name: 'twitter:title', content: 'CS2-SimpleAdmin - Admin Plugin for Counter-Strike 2'},
|
||||
{name: 'twitter:description', content: 'Comprehensive administration plugin for CS2 servers with ban management and multi-server support.'},
|
||||
{name: 'twitter:image', content: 'https://cs2-simpleadmin.daffyy.dev/img/docusaurus-social-card.jpg'},
|
||||
],
|
||||
colorMode: {
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
navbar: {
|
||||
title: 'CS2-SimpleAdmin',
|
||||
logo: {
|
||||
alt: 'CS2-SimpleAdmin Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'userSidebar',
|
||||
position: 'left',
|
||||
label: 'User Guide',
|
||||
},
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'modulesSidebar',
|
||||
position: 'left',
|
||||
label: 'Modules',
|
||||
},
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'developerSidebar',
|
||||
position: 'left',
|
||||
label: 'Developer',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Documentation',
|
||||
items: [
|
||||
{
|
||||
label: 'User Guide',
|
||||
to: '/docs/user/intro',
|
||||
},
|
||||
{
|
||||
label: 'Modules',
|
||||
to: '/docs/modules/intro',
|
||||
},
|
||||
{
|
||||
label: 'Developer',
|
||||
to: '/docs/developer/intro',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'GitHub Issues',
|
||||
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin/issues',
|
||||
},
|
||||
{
|
||||
label: 'GitHub Discussions',
|
||||
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin/discussions',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin',
|
||||
},
|
||||
{
|
||||
label: 'Releases',
|
||||
href: 'https://github.com/daffyyyy/CS2-SimpleAdmin/releases',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} CS2-SimpleAdmin. Built with Docusaurus.`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
additionalLanguages: ['csharp', 'json', 'bash'],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
17981
CS2-SimpleAdmin-docs/package-lock.json
generated
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"name": "cs-2-simple-admin-docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.9.2",
|
||||
"@docusaurus/preset-classic": "3.9.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.9.2",
|
||||
"@docusaurus/types": "3.9.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
|
||||
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
|
||||
@type {import('@docusaurus/plugin-content-docs').SidebarsConfig}
|
||||
*/
|
||||
const sidebars = {
|
||||
userSidebar: [
|
||||
'user/intro',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
'user/installation',
|
||||
'user/configuration',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Commands',
|
||||
items: [
|
||||
'user/commands/basebans',
|
||||
'user/commands/basecomms',
|
||||
'user/commands/basecommands',
|
||||
'user/commands/basechat',
|
||||
'user/commands/playercommands',
|
||||
'user/commands/basevotes',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
modulesSidebar: [
|
||||
'modules/intro',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Official Modules',
|
||||
items: [
|
||||
'modules/funcommands',
|
||||
],
|
||||
},
|
||||
'modules/development',
|
||||
],
|
||||
|
||||
developerSidebar: [
|
||||
'developer/intro',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'API Reference',
|
||||
items: [
|
||||
'developer/api/overview',
|
||||
'developer/api/commands',
|
||||
'developer/api/menus',
|
||||
'developer/api/penalties',
|
||||
'developer/api/events',
|
||||
'developer/api/utilities',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Module Development',
|
||||
items: [
|
||||
'developer/module/getting-started',
|
||||
'developer/module/best-practices',
|
||||
'developer/module/examples',
|
||||
],
|
||||
},
|
||||
'developer/architecture',
|
||||
],
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import Heading from '@theme/Heading';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
const FeatureList = [
|
||||
{
|
||||
title: 'Comprehensive Admin Tools',
|
||||
img: require('@site/static/img/index_1.png').default,
|
||||
description: (
|
||||
<>
|
||||
Full suite of admin commands for managing players, bans, mutes, warnings,
|
||||
and server settings. Everything you need to moderate your CS2 server.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Multi-Server Support',
|
||||
img: require('@site/static/img/index_2.png').default,
|
||||
description: (
|
||||
<>
|
||||
Manage multiple servers with synchronized admin permissions and penalties.
|
||||
Share bans, mutes, and admin groups across your entire server network.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Extensible API',
|
||||
img: require('@site/static/img/index_3.png').default,
|
||||
description: (
|
||||
<>
|
||||
Build custom modules using the public API. Create your own commands,
|
||||
menus, and integrate with CS2-SimpleAdmin's permission and penalty systems.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({img, title, description}) {
|
||||
return (
|
||||
<div className={clsx('col col--4')}>
|
||||
<div className="text--center">
|
||||
<img src={img} className={styles.featureSvg} alt={title} />
|
||||
</div>
|
||||
<div className="text--center padding-horiz--md">
|
||||
<Heading as="h3">{title}</Heading>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomepageFeatures() {
|
||||
return (
|
||||
<section className={styles.features}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
{FeatureList.map((props, idx) => (
|
||||
<Feature key={idx} {...props} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #ff8c00;
|
||||
--ifm-color-primary-dark: #e67e00;
|
||||
--ifm-color-primary-darker: #d97700;
|
||||
--ifm-color-primary-darkest: #b36200;
|
||||
--ifm-color-primary-light: #ff9a1a;
|
||||
--ifm-color-primary-lighter: #ffa328;
|
||||
--ifm-color-primary-lightest: #ffb54d;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #ff9500;
|
||||
--ifm-color-primary-dark: #e68600;
|
||||
--ifm-color-primary-darker: #cc7700;
|
||||
--ifm-color-primary-darkest: #b36200;
|
||||
--ifm-color-primary-light: #ffa31a;
|
||||
--ifm-color-primary-lighter: #ffad33;
|
||||
--ifm-color-primary-lightest: #ffc266;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Layout from '@theme/Layout';
|
||||
import HomepageFeatures from '@site/src/components/HomepageFeatures';
|
||||
|
||||
import Heading from '@theme/Heading';
|
||||
import styles from './index.module.css';
|
||||
|
||||
function HomepageHeader() {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
return (
|
||||
<header className={clsx('hero hero--primary', styles.heroBanner)}>
|
||||
<iframe
|
||||
className={styles.videoBackground}
|
||||
src="https://www.youtube.com/embed/4qEdIXLdxMo?autoplay=1&mute=1&loop=1&playlist=4qEdIXLdxMo&controls=0&showinfo=0&rel=0&modestbranding=1&playsinline=1"
|
||||
title="Background Video"
|
||||
frameBorder="0"
|
||||
allow="autoplay; encrypted-media"
|
||||
allowFullScreen
|
||||
/>
|
||||
<div className={styles.videoOverlay}></div>
|
||||
<div className="container">
|
||||
<Heading as="h1" className="hero__title">
|
||||
{siteConfig.title}
|
||||
</Heading>
|
||||
<p className="hero__subtitle">{siteConfig.tagline}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/user/intro">
|
||||
Get Started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
return (
|
||||
<Layout
|
||||
title={`${siteConfig.title} - Admin Plugin for CS2`}
|
||||
description="CS2-SimpleAdmin is a comprehensive administration plugin for Counter-Strike 2 servers. Manage bans, mutes, warnings, and permissions with multi-server support and extensible API.">
|
||||
<HomepageHeader />
|
||||
<main>
|
||||
<HomepageFeatures />
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* CSS files with the .module.css suffix will be treated as CSS modules
|
||||
* and scoped locally.
|
||||
*/
|
||||
|
||||
.heroBanner {
|
||||
padding: 8rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 600px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.videoBackground {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
height: 56.25vw; /* 16:9 Aspect Ratio */
|
||||
min-height: 100vh;
|
||||
min-width: 177.77vh; /* 16:9 Aspect Ratio */
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.videoOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.heroBanner :global(.container) {
|
||||
position: relative;
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
.heroBanner :global(.hero__title) {
|
||||
color: white !important;
|
||||
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.9);
|
||||
font-size: 3.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.heroBanner :global(.hero__subtitle) {
|
||||
color: white !important;
|
||||
text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.9);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.heroBanner .buttons {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.heroBanner {
|
||||
padding: 4rem 2rem;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.videoBackground {
|
||||
width: 200vw;
|
||||
height: 112.5vw;
|
||||
}
|
||||
|
||||
.heroBanner :global(.hero__title) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.heroBanner :global(.hero__subtitle) {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.buttons :global(.button) {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
||||
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
BIN
CS2-SimpleAdmin/3rd_party/MenuManagerApi.dll
vendored
BIN
CS2-SimpleAdmin/3rd_party/PanoramaUiKit.Shared.dll
vendored
Normal file
|
|
@ -18,9 +18,12 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
|
||||
public PlayerInfo GetPlayerInfo(CCSPlayerController player)
|
||||
{
|
||||
return !player.UserId.HasValue
|
||||
? throw new KeyNotFoundException("Player with specific UserId not found")
|
||||
: CS2_SimpleAdmin.PlayersInfo[player.SteamID];
|
||||
if (!player.UserId.HasValue)
|
||||
{
|
||||
throw new KeyNotFoundException("Player with specific UserId not found");
|
||||
}
|
||||
|
||||
return CS2_SimpleAdmin.PlayersInfo[player.SteamID];
|
||||
}
|
||||
|
||||
public string GetConnectionString() => CS2_SimpleAdmin.Instance.DbConnectionString;
|
||||
|
|
@ -57,35 +60,35 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
switch (penaltyType)
|
||||
{
|
||||
case PenaltyType.Ban:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Kick:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Kick(admin, player, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Kick(admin, player, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Gag:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Mute:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Silence:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Warn:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
|
||||
}
|
||||
|
|
@ -97,30 +100,30 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
switch (penaltyType)
|
||||
{
|
||||
case PenaltyType.Ban:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddBan(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddBan(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Gag:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddGag(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddGag(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Mute:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddMute(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddMute(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Silence:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddSilence(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddSilence(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
case PenaltyType.Warn:
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddWarn(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddWarn(admin, steamid, duration, reason);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
|
||||
}
|
||||
|
|
@ -149,27 +152,31 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
public void RegisterCommand(string name, string? description, CommandInfo.CommandCallback callback)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Command name cannot be null or empty.", nameof(name));
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(callback);
|
||||
|
||||
var definition = new CommandDefinition(name, description ?? "No description", callback);
|
||||
if (!RegisterCommands._commandDefinitions.TryGetValue(name, out var list))
|
||||
CommandDefinition definition = new CommandDefinition(name, description ?? "No description", callback);
|
||||
if (!RegisterCommands._commandDefinitions.TryGetValue(name, out IList<CommandDefinition>? list))
|
||||
{
|
||||
list = new List<CommandDefinition>();
|
||||
RegisterCommands._commandDefinitions[name] = list;
|
||||
}
|
||||
|
||||
|
||||
list.Add(definition);
|
||||
}
|
||||
|
||||
public void UnRegisterCommand(string commandName)
|
||||
{
|
||||
var definitions = RegisterCommands._commandDefinitions[commandName];
|
||||
IList<CommandDefinition> definitions = RegisterCommands._commandDefinitions[commandName];
|
||||
if (definitions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var definition in definitions)
|
||||
foreach (CommandDefinition definition in definitions)
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.RemoveCommand(commandName, definition.Callback);
|
||||
}
|
||||
|
|
@ -196,7 +203,9 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
bool dontPublish = false, params object[] messageArgs)
|
||||
{
|
||||
if (moduleLocalizer is not IStringLocalizer localizer)
|
||||
{
|
||||
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
|
||||
}
|
||||
|
||||
Helper.ShowAdminActivityLocalized(localizer, messageKey, callerName, dontPublish, messageArgs);
|
||||
}
|
||||
|
|
@ -209,7 +218,9 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
public void RegisterMenuCategory(string categoryId, string categoryNameKey, string permission, object moduleLocalizer)
|
||||
{
|
||||
if (moduleLocalizer is not IStringLocalizer localizer)
|
||||
{
|
||||
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
|
||||
}
|
||||
|
||||
Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryNameKey, permission, localizer);
|
||||
}
|
||||
|
|
@ -223,22 +234,11 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
MenuBuilder BuilderFactory(CCSPlayerController player)
|
||||
{
|
||||
if (menuFactory(player) is not MenuBuilder menuBuilder)
|
||||
throw new InvalidOperationException("Menu factory must return MenuBuilder");
|
||||
|
||||
// Dodaj automatyczną obsługę przycisku 'Wróć'
|
||||
menuBuilder.WithBackAction(p =>
|
||||
{
|
||||
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
|
||||
{
|
||||
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
Menus.MenuManager.Instance.OpenMainMenu(p);
|
||||
}
|
||||
});
|
||||
throw new InvalidOperationException("Menu factory must return MenuBuilder");
|
||||
}
|
||||
|
||||
return menuBuilder;
|
||||
return menuBuilder.WithBackButton();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -250,25 +250,14 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
|
||||
MenuBuilder BuilderFactory(CCSPlayerController player)
|
||||
{
|
||||
var context = new MenuContext(categoryId, menuId, menuName, permission, commandName);
|
||||
MenuContext context = new MenuContext(categoryId, menuId, menuName, permission, commandName);
|
||||
|
||||
if (menuFactory(player, context) is not MenuBuilder menuBuilder)
|
||||
throw new InvalidOperationException("Menu factory must return MenuBuilder");
|
||||
|
||||
// Dodaj automatyczną obsługę przycisku 'Wróć'
|
||||
menuBuilder.WithBackAction(p =>
|
||||
{
|
||||
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
|
||||
{
|
||||
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
Menus.MenuManager.Instance.OpenMainMenu(p);
|
||||
}
|
||||
});
|
||||
throw new InvalidOperationException("Menu factory must return MenuBuilder");
|
||||
}
|
||||
|
||||
return menuBuilder;
|
||||
return menuBuilder.WithBackButton();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,32 +265,23 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
Func<CCSPlayerController, MenuContext, object> menuFactory, string? permission, string? commandName, object moduleLocalizer)
|
||||
{
|
||||
if (moduleLocalizer is not IStringLocalizer localizer)
|
||||
{
|
||||
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
|
||||
}
|
||||
|
||||
Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuNameKey, BuilderFactory, permission, commandName, localizer);
|
||||
return;
|
||||
|
||||
MenuBuilder BuilderFactory(CCSPlayerController player)
|
||||
{
|
||||
var context = new MenuContext(categoryId, menuId, menuNameKey, permission, commandName);
|
||||
MenuContext context = new MenuContext(categoryId, menuId, menuNameKey, permission, commandName);
|
||||
|
||||
if (menuFactory(player, context) is not MenuBuilder menuBuilder)
|
||||
throw new InvalidOperationException("Menu factory must return MenuBuilder");
|
||||
|
||||
// Dodaj automatyczną obsługę przycisku 'Wróć'
|
||||
menuBuilder.WithBackAction(p =>
|
||||
{
|
||||
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
|
||||
{
|
||||
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
Menus.MenuManager.Instance.OpenMainMenu(p);
|
||||
}
|
||||
});
|
||||
throw new InvalidOperationException("Menu factory must return MenuBuilder");
|
||||
}
|
||||
|
||||
return menuBuilder;
|
||||
return menuBuilder.WithBackButton();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -313,20 +293,7 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
|
||||
public object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)
|
||||
{
|
||||
var builder = new MenuBuilder(title);
|
||||
builder.WithBackAction(p =>
|
||||
{
|
||||
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
|
||||
{
|
||||
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
Menus.MenuManager.Instance.OpenMainMenu(p);
|
||||
}
|
||||
});
|
||||
|
||||
return builder;
|
||||
return new MenuBuilder(title).WithBackButton();
|
||||
}
|
||||
|
||||
public object CreateMenuWithBack(MenuContext context, CCSPlayerController player)
|
||||
|
|
@ -334,10 +301,10 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
// Get translated title if module has localizer
|
||||
string title = context.MenuTitle;
|
||||
|
||||
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category))
|
||||
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out MenuCategory? category))
|
||||
{
|
||||
// Check if this specific menu has a localizer
|
||||
if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer))
|
||||
if (category.MenuLocalizers.TryGetValue(context.MenuId, out IStringLocalizer? menuLocalizer))
|
||||
{
|
||||
using (new WithTemporaryCulture(player.GetLanguage()))
|
||||
{
|
||||
|
|
@ -365,12 +332,12 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
public object CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin,
|
||||
Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)
|
||||
{
|
||||
var menu = (MenuBuilder)CreateMenuWithBack(title, categoryId, admin);
|
||||
var players = Helper.GetValidPlayers().Where(filter);
|
||||
MenuBuilder menu = (MenuBuilder)CreateMenuWithBack(title, categoryId, admin);
|
||||
IEnumerable<CCSPlayerController> players = Helper.GetValidPlayers().Where(filter);
|
||||
|
||||
foreach (var player in players)
|
||||
foreach (CCSPlayerController? player in players)
|
||||
{
|
||||
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
|
||||
string playerName = Helper.Truncate(player.PlayerName, 26);
|
||||
menu.AddOption(playerName, _ =>
|
||||
{
|
||||
if (player.IsValid)
|
||||
|
|
@ -389,10 +356,10 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
// Get translated title if module has localizer
|
||||
string title = context.MenuTitle;
|
||||
|
||||
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category))
|
||||
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out MenuCategory? category))
|
||||
{
|
||||
// Check if this specific menu has a localizer
|
||||
if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer))
|
||||
if (category.MenuLocalizers.TryGetValue(context.MenuId, out IStringLocalizer? menuLocalizer))
|
||||
{
|
||||
using (new WithTemporaryCulture(admin.GetLanguage()))
|
||||
{
|
||||
|
|
@ -416,7 +383,9 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
string? permission = null)
|
||||
{
|
||||
if (menu is not MenuBuilder menuBuilder)
|
||||
{
|
||||
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
|
||||
}
|
||||
|
||||
menuBuilder.AddOption(name, action, disabled, permission);
|
||||
}
|
||||
|
|
@ -425,13 +394,18 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
bool disabled = false, string? permission = null)
|
||||
{
|
||||
if (menu is not MenuBuilder menuBuilder)
|
||||
{
|
||||
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
|
||||
}
|
||||
|
||||
menuBuilder.AddSubMenu(name, player =>
|
||||
{
|
||||
var subMenu = subMenuFactory(player);
|
||||
object subMenu = subMenuFactory(player);
|
||||
if (subMenu is not MenuBuilder builder)
|
||||
{
|
||||
throw new InvalidOperationException("SubMenu factory must return MenuBuilder");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}, disabled, permission);
|
||||
}
|
||||
|
|
@ -439,8 +413,10 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
|
|||
public void OpenMenu(object menu, CCSPlayerController player)
|
||||
{
|
||||
if (menu is not MenuBuilder menuBuilder)
|
||||
{
|
||||
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
|
||||
}
|
||||
|
||||
menuBuilder.OpenMenu(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,22 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
{
|
||||
internal static CS2_SimpleAdmin Instance { get; private set; } = new();
|
||||
|
||||
public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
|
||||
public override string ModuleName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Helper.IsDebugBuild)
|
||||
{
|
||||
return "CS2-SimpleAdmin (DEBUG)";
|
||||
}
|
||||
|
||||
return "CS2-SimpleAdmin (RELEASE)";
|
||||
}
|
||||
}
|
||||
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
|
||||
public override string ModuleAuthor => "daffyy";
|
||||
public override string ModuleVersion => "1.9.1";
|
||||
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
Instance = this;
|
||||
|
|
@ -31,30 +42,38 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
{
|
||||
ServerLoaded = false;
|
||||
_serverLoading = false;
|
||||
|
||||
|
||||
CacheManager?.Dispose();
|
||||
CacheManager = new CacheManager();
|
||||
|
||||
|
||||
// OnGameServerSteamAPIActivated();
|
||||
OnMapStart(string.Empty);
|
||||
|
||||
AddTimer(6.0f, () =>
|
||||
{
|
||||
if (DatabaseProvider == null) return;
|
||||
|
||||
if (DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayersInfo.Clear();
|
||||
CachedPlayers.Clear();
|
||||
BotPlayers.Clear();
|
||||
|
||||
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && p is { Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToArray())
|
||||
|
||||
foreach (CCSPlayerController? player in Utilities.GetPlayers().Where(p => p.IsValid && p is { Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToArray())
|
||||
{
|
||||
if (!player.IsBot)
|
||||
{
|
||||
PlayerManager.LoadPlayerData(player, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
BotPlayers.Add(player);
|
||||
};
|
||||
}
|
||||
}
|
||||
;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
_cBasePlayerControllerSetPawnFunc = new MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>(GameData.GetSignature("CBasePlayerController_SetPawn"));
|
||||
|
||||
|
|
@ -64,22 +83,14 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
PlayersTimer?.Kill();
|
||||
PlayersTimer = null;
|
||||
PlayerManager.CheckPlayersTimer();
|
||||
|
||||
|
||||
Menus.MenuManager.Instance.InitializeDefaultCategories();
|
||||
BasicMenu.Initialize();
|
||||
}
|
||||
|
||||
public override void OnAllPluginsLoaded(bool hotReload)
|
||||
{
|
||||
try
|
||||
{
|
||||
MenuApi = MenuCapability.Get();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Unable to load required plugins ... \n{exception}", ex.Message);
|
||||
Unload(false);
|
||||
}
|
||||
UiKit.Preload(Logger);
|
||||
|
||||
RegisterEvents();
|
||||
AddTimer(0.5f, RegisterCommands.InitializeCommands);
|
||||
|
|
@ -87,7 +98,10 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
// Loading the plugin mid-map (css_plugins load) fires no map start, so kick server loading off ourselves.
|
||||
AddTimer(5.0f, () =>
|
||||
{
|
||||
if (!ServerLoaded) OnGameServerSteamAPIActivated();
|
||||
if (!ServerLoaded)
|
||||
{
|
||||
OnGameServerSteamAPIActivated();
|
||||
}
|
||||
});
|
||||
|
||||
if (!CoreConfig.UnlockConCommands)
|
||||
|
|
@ -107,16 +121,17 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
|
||||
_logger = Logger;
|
||||
Config = config;
|
||||
UiKit.Configure(config.MenuConfigs);
|
||||
|
||||
bool missing = false;
|
||||
var cssPath = Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp");
|
||||
var pluginsPath = Path.Combine(cssPath, "plugins");
|
||||
var sharedPath = Path.Combine(cssPath, "shared");
|
||||
string cssPath = Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp");
|
||||
string pluginsPath = Path.Combine(cssPath, "plugins");
|
||||
string sharedPath = Path.Combine(cssPath, "shared");
|
||||
|
||||
foreach (var plugin in _requiredPlugins)
|
||||
foreach (string plugin in _requiredPlugins)
|
||||
{
|
||||
var pluginDirPath = Path.Combine(pluginsPath, plugin);
|
||||
var pluginDllPath = Path.Combine(pluginDirPath, $"{plugin}.dll");
|
||||
string pluginDirPath = Path.Combine(pluginsPath, plugin);
|
||||
string pluginDllPath = Path.Combine(pluginDirPath, $"{plugin}.dll");
|
||||
|
||||
if (!Directory.Exists(pluginDirPath))
|
||||
{
|
||||
|
|
@ -135,10 +150,10 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
}
|
||||
}
|
||||
|
||||
foreach (var shared in _requiredShared)
|
||||
foreach (string shared in _requiredShared)
|
||||
{
|
||||
var sharedDirPath = Path.Combine(sharedPath, shared);
|
||||
var sharedDllPath = Path.Combine(sharedDirPath, $"{shared}.dll");
|
||||
string sharedDirPath = Path.Combine(sharedPath, shared);
|
||||
string sharedDllPath = Path.Combine(sharedDirPath, $"{shared}.dll");
|
||||
|
||||
if (!Directory.Exists(sharedDirPath))
|
||||
{
|
||||
|
|
@ -156,7 +171,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
missing = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (missing)
|
||||
{
|
||||
Server.ExecuteCommand($"css_plugins unload {ModuleDirectory}");
|
||||
|
|
@ -164,54 +179,59 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
}
|
||||
|
||||
Instance = this;
|
||||
|
||||
if (Config.DatabaseConfig.DatabaseType.Contains("mysql", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseHost) ||
|
||||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseName) ||
|
||||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseUser))
|
||||
|
||||
if (Config.DatabaseConfig.DatabaseType.Contains("mysql", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
throw new Exception("[CS2-SimpleAdmin] You need to setup MySQL credentials in config!");
|
||||
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseHost) ||
|
||||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseName) ||
|
||||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseUser))
|
||||
{
|
||||
throw new Exception("[CS2-SimpleAdmin] You need to setup MySQL credentials in config!");
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(config.DatabaseConfig.DatabaseSSlMode, true, out MySqlSslMode sslMode))
|
||||
{
|
||||
sslMode = MySqlSslMode.Preferred;
|
||||
}
|
||||
|
||||
MySqlConnectionStringBuilder builder = new MySqlConnectionStringBuilder()
|
||||
{
|
||||
Server = config.DatabaseConfig.DatabaseHost,
|
||||
Database = config.DatabaseConfig.DatabaseName,
|
||||
UserID = config.DatabaseConfig.DatabaseUser,
|
||||
Password = config.DatabaseConfig.DatabasePassword,
|
||||
Port = (uint)config.DatabaseConfig.DatabasePort,
|
||||
SslMode = sslMode,
|
||||
Pooling = true,
|
||||
};
|
||||
|
||||
DbConnectionString = builder.ConnectionString;
|
||||
DatabaseProvider = new MySqlDatabaseProvider(DbConnectionString);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.SqliteFilePath))
|
||||
{
|
||||
throw new Exception("[CS2-SimpleAdmin] You need to specify SQLite file path in config!");
|
||||
}
|
||||
|
||||
DatabaseProvider = new SqliteDatabaseProvider(ModuleDirectory + "/" + config.DatabaseConfig.SqliteFilePath);
|
||||
}
|
||||
|
||||
var builder = new MySqlConnectionStringBuilder()
|
||||
(bool success, string? exception) = Task.Run(() => DatabaseProvider.CheckConnectionAsync()).GetAwaiter().GetResult();
|
||||
if (!success)
|
||||
{
|
||||
Server = config.DatabaseConfig.DatabaseHost,
|
||||
Database = config.DatabaseConfig.DatabaseName,
|
||||
UserID = config.DatabaseConfig.DatabaseUser,
|
||||
Password = config.DatabaseConfig.DatabasePassword,
|
||||
Port = (uint)config.DatabaseConfig.DatabasePort,
|
||||
SslMode = Enum.TryParse(config.DatabaseConfig.DatabaseSSlMode, true, out MySqlSslMode sslMode)
|
||||
? sslMode
|
||||
: MySqlSslMode.Preferred,
|
||||
Pooling = true,
|
||||
};
|
||||
if (exception != null)
|
||||
{
|
||||
Logger.LogError("Problem with database connection! \n{exception}", exception);
|
||||
}
|
||||
|
||||
DbConnectionString = builder.ConnectionString;
|
||||
DatabaseProvider = new MySqlDatabaseProvider(DbConnectionString);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.SqliteFilePath))
|
||||
{
|
||||
throw new Exception("[CS2-SimpleAdmin] You need to specify SQLite file path in config!");
|
||||
Unload(false);
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseProvider = new SqliteDatabaseProvider(ModuleDirectory + "/" + config.DatabaseConfig.SqliteFilePath);
|
||||
}
|
||||
|
||||
var (success, exception) = Task.Run(() => DatabaseProvider.CheckConnectionAsync()).GetAwaiter().GetResult();
|
||||
if (!success)
|
||||
{
|
||||
if (exception != null)
|
||||
Logger.LogError("Problem with database connection! \n{exception}", exception);
|
||||
|
||||
Unload(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(() => DatabaseProvider.DatabaseMigrationAsync());
|
||||
|
||||
|
||||
if (!Directory.Exists(ModuleDirectory + "/data"))
|
||||
{
|
||||
Directory.CreateDirectory(ModuleDirectory + "/data");
|
||||
|
|
@ -220,11 +240,15 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
_localizer = Localizer;
|
||||
|
||||
if (!string.IsNullOrEmpty(Config.Discord.DiscordLogWebhook))
|
||||
{
|
||||
DiscordWebhookClientLog = new DiscordManager(Config.Discord.DiscordLogWebhook);
|
||||
}
|
||||
|
||||
if (Config.EnableUpdateCheck)
|
||||
{
|
||||
Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, Logger));
|
||||
|
||||
}
|
||||
|
||||
PermissionManager = new PermissionManager(DatabaseProvider);
|
||||
BanManager = new BanManager(DatabaseProvider);
|
||||
MuteManager = new MuteManager(DatabaseProvider);
|
||||
|
|
@ -233,7 +257,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
|
||||
internal static TargetResult? GetTarget(CommandInfo command, int argument = 1)
|
||||
{
|
||||
var matches = command.GetArgTargetResult(argument);
|
||||
TargetResult matches = command.GetArgTargetResult(argument);
|
||||
|
||||
if (!matches.Any())
|
||||
{
|
||||
|
|
@ -242,10 +266,14 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
}
|
||||
|
||||
if (command.GetArg(argument).StartsWith('@'))
|
||||
{
|
||||
return matches;
|
||||
}
|
||||
|
||||
if (matches.Count() == 1)
|
||||
{
|
||||
return matches;
|
||||
}
|
||||
|
||||
command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(argument)}\".");
|
||||
return null;
|
||||
|
|
@ -257,12 +285,16 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
CacheManager = null;
|
||||
PlayersTimer?.Kill();
|
||||
PlayersTimer = null;
|
||||
|
||||
|
||||
UnregisterEvents();
|
||||
|
||||
|
||||
if (hotReload)
|
||||
{
|
||||
PlayersInfo.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
Server.ExecuteCommand($"css_plugins unload {ModuleDirectory}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369">
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.376">
|
||||
<PrivateAssets>none</PrivateAssets>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
|
@ -47,8 +47,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="MenuManagerApi">
|
||||
<HintPath>3rd_party\MenuManagerApi.dll</HintPath>
|
||||
<Reference Include="PanoramaUiKit.Shared">
|
||||
<HintPath>3rd_party\PanoramaUiKit.Shared.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ public static class RegisterCommands
|
|||
{
|
||||
internal static readonly Dictionary<string, IList<CommandDefinition>> _commandDefinitions =
|
||||
new(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
|
||||
private delegate void CommandCallback(CCSPlayerController? caller, CommandInfo.CommandCallback callback);
|
||||
|
||||
|
||||
private static readonly string CommandsPath = Path.Combine(CS2_SimpleAdmin.ConfigDirectory, "Commands.json");
|
||||
private static readonly List<CommandMapping> CommandMappings =
|
||||
[
|
||||
|
|
@ -101,7 +101,7 @@ public static class RegisterCommands
|
|||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
private static void CreateConfig()
|
||||
{
|
||||
var commands = new CommandsConfig
|
||||
CommandsConfig commands = new CommandsConfig
|
||||
{
|
||||
Commands = new Dictionary<string, Command>
|
||||
{
|
||||
|
|
@ -161,14 +161,14 @@ public static class RegisterCommands
|
|||
{ "css_adminvoice", new Command { Aliases = ["css_adminvoice", "css_listenall"] } }
|
||||
}
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
|
||||
JsonSerializerOptions options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(commands, options);
|
||||
string json = JsonSerializer.Serialize(commands, options);
|
||||
File.WriteAllText(CommandsPath, json);
|
||||
}
|
||||
|
||||
|
|
@ -179,46 +179,52 @@ public static class RegisterCommands
|
|||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
private static void Register()
|
||||
{
|
||||
var json = File.ReadAllText(CommandsPath);
|
||||
var commandsConfig = JsonSerializer.Deserialize<CommandsConfig>(json,
|
||||
string json = File.ReadAllText(CommandsPath);
|
||||
CommandsConfig? commandsConfig = JsonSerializer.Deserialize<CommandsConfig>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (commandsConfig?.Commands != null)
|
||||
{
|
||||
foreach (var command in commandsConfig.Commands)
|
||||
foreach (KeyValuePair<string, Command> command in commandsConfig.Commands)
|
||||
{
|
||||
if (command.Value.Aliases == null) continue;
|
||||
|
||||
if (command.Value.Aliases == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CS2_SimpleAdmin._logger?.LogInformation(
|
||||
$"Registering command: `{command.Key}` with aliases: `{string.Join(", ", command.Value.Aliases)}`");
|
||||
|
||||
var mapping = CommandMappings.FirstOrDefault(m => m.CommandKey == command.Key);
|
||||
if (mapping == null || command.Value.Aliases.Length == 0) continue;
|
||||
|
||||
foreach (var alias in command.Value.Aliases)
|
||||
|
||||
CommandMapping? mapping = CommandMappings.FirstOrDefault(m => m.CommandKey == command.Key);
|
||||
if (mapping == null || command.Value.Aliases.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (string alias in command.Value.Aliases)
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Commands added after Commands.json was generated are registered under their default name
|
||||
foreach (var mapping in CommandMappings.Where(m => !commandsConfig.Commands.ContainsKey(m.CommandKey)))
|
||||
foreach (CommandMapping? mapping in CommandMappings.Where(m => !commandsConfig.Commands.ContainsKey(m.CommandKey)))
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogInformation($"Registering command: `{mapping.CommandKey}` (not in Commands.json, using default alias)");
|
||||
CS2_SimpleAdmin.Instance.AddCommand(mapping.CommandKey, "", mapping.Callback);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (name, definitions) in _commandDefinitions)
|
||||
|
||||
foreach ((string? name, IList<CommandDefinition>? definitions) in _commandDefinitions)
|
||||
{
|
||||
foreach (var definition in definitions)
|
||||
foreach (CommandDefinition definition in definitions)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogInformation($"Registering custom command: `{name}`");
|
||||
CS2_SimpleAdmin.Instance.AddCommand(name, definition.Description, definition.Callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents the JSON configuration structure for commands.
|
||||
/// </summary>
|
||||
|
|
@ -226,7 +232,7 @@ public static class RegisterCommands
|
|||
{
|
||||
public Dictionary<string, Command>? Commands { get; init; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command definition containing a list of aliases.
|
||||
/// </summary>
|
||||
|
|
@ -234,7 +240,7 @@ public static class RegisterCommands
|
|||
{
|
||||
public string[]? Aliases { get; init; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Maps a command key to its respective command callback handler.
|
||||
/// </summary>
|
||||
|
|
@ -243,4 +249,4 @@ public static class RegisterCommands
|
|||
public string CommandKey { get; } = commandKey;
|
||||
public CommandInfo.CommandCallback Callback { get; } = callback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using CounterStrikeSharp.API;
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Commands.Targeting;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
||||
using CS2_SimpleAdmin.Managers;
|
||||
|
|
@ -21,34 +22,38 @@ public partial class CS2_SimpleAdmin
|
|||
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
if (command.ArgCount < 2)
|
||||
{
|
||||
return;
|
||||
|
||||
var targets = GetTarget(command);
|
||||
if (targets == null) return;
|
||||
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToList();
|
||||
}
|
||||
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToList();
|
||||
|
||||
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reason = command.ArgCount >= 3
|
||||
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
|
||||
: _localizer?["sa_unknown"] ?? "Unknown";
|
||||
|
||||
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
|
||||
var time = Helper.ParsePenaltyTime(command.GetArg(2));
|
||||
string reason = Helper.GetArgsReason(command, 3);
|
||||
int time = Helper.ParsePenaltyTime(command.GetArg(2));
|
||||
|
||||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
if (!caller.CanTarget(player)) return;
|
||||
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
|
||||
{
|
||||
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_ban"] ?? "Ban"}: {player.PlayerName}", player,
|
||||
ManagePlayersMenu.BanMenu);
|
||||
BasicMenu.CreatePenaltyMenu(caller, player, PenaltyType.Ban).OpenMenu(caller);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -69,18 +74,27 @@ public partial class CS2_SimpleAdmin
|
|||
/// <param name="silent">If true, suppresses command logging.</param>
|
||||
internal void Ban(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, BanManager? banManager = null, CommandInfo? command = null, bool silent = false)
|
||||
{
|
||||
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
|
||||
if (!caller.CanTarget(player)) return;
|
||||
if (!CheckValidBan(caller, time)) return;
|
||||
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CheckValidBan(caller, time))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default caller name if not provided
|
||||
callerName = !string.IsNullOrEmpty(caller?.PlayerName)
|
||||
? caller.PlayerName
|
||||
: (_localizer?["sa_console"] ?? "Console");
|
||||
|
||||
callerName = Helper.GetCallerName(caller);
|
||||
|
||||
// Get player and admin information
|
||||
var playerInfo = PlayersInfo[player.SteamID];
|
||||
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
|
||||
PlayerInfo playerInfo = PlayersInfo[player.SteamID];
|
||||
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
|
||||
|
||||
// Asynchronously handle banning logic
|
||||
Task.Run(async () =>
|
||||
|
|
@ -93,13 +107,25 @@ public partial class CS2_SimpleAdmin
|
|||
});
|
||||
|
||||
// Determine message keys and arguments based on ban time
|
||||
var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = time == 0
|
||||
? ("sa_player_ban_message_perm", "sa_admin_ban_message_perm",
|
||||
[reason, "CALLER"],
|
||||
["CALLER", player.PlayerName, reason])
|
||||
: ("sa_player_ban_message_time", "sa_admin_ban_message_time",
|
||||
new object[] { reason, time, "CALLER" },
|
||||
new object[] { "CALLER", player.PlayerName, reason, time });
|
||||
string? messageKey;
|
||||
string? activityMessageKey;
|
||||
object[]? centerArgs;
|
||||
object[]? adminActivityArgs;
|
||||
|
||||
if (time == 0)
|
||||
{
|
||||
messageKey = "sa_player_ban_message_perm";
|
||||
activityMessageKey = "sa_admin_ban_message_perm";
|
||||
centerArgs = [reason, "CALLER"];
|
||||
adminActivityArgs = ["CALLER", player.PlayerName, reason];
|
||||
}
|
||||
else
|
||||
{
|
||||
messageKey = "sa_player_ban_message_time";
|
||||
activityMessageKey = "sa_admin_ban_message_time";
|
||||
centerArgs = new object[] { reason, time, "CALLER" };
|
||||
adminActivityArgs = new object[] { "CALLER", player.PlayerName, reason, time };
|
||||
}
|
||||
|
||||
// Display center message to the player
|
||||
Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs);
|
||||
|
|
@ -125,11 +151,15 @@ public partial class CS2_SimpleAdmin
|
|||
if (!silent)
|
||||
{
|
||||
if (command == null)
|
||||
Helper.LogCommand(caller, $"css_ban {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
|
||||
{
|
||||
Helper.LogCommand(caller, $"css_ban {Helper.GetLogName(player)} {time} {reason}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Helper.LogCommand(caller, command);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Ban, _localizer);
|
||||
}
|
||||
|
||||
|
|
@ -144,23 +174,26 @@ public partial class CS2_SimpleAdmin
|
|||
internal void AddBan(CCSPlayerController? caller, SteamID steamid, int time, string reason, BanManager? banManager = null)
|
||||
{
|
||||
// Set default caller name if not provided
|
||||
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
|
||||
? caller.PlayerName
|
||||
: (_localizer?["sa_console"] ?? "Console");
|
||||
|
||||
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
|
||||
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
|
||||
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
|
||||
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
|
||||
if (player != null && player.IsValid)
|
||||
{
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Ban(caller, player, time, reason, callerName, silent: true);
|
||||
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!caller.CanTarget(steamid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Asynchronous ban operation if player is not online or not found
|
||||
Task.Run(async () =>
|
||||
{
|
||||
|
|
@ -171,7 +204,7 @@ public partial class CS2_SimpleAdmin
|
|||
penaltyId);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Ban, _localizer);
|
||||
}
|
||||
}
|
||||
|
|
@ -185,34 +218,41 @@ public partial class CS2_SimpleAdmin
|
|||
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
if (DatabaseProvider == null) return;
|
||||
var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
|
||||
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
|
||||
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
|
||||
if (DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
|
||||
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Helper.ValidateSteamId(command.GetArg(1), out SteamID? steamId) || steamId == null)
|
||||
{
|
||||
command.ReplyToCommand("Invalid SteamID64.");
|
||||
return;
|
||||
}
|
||||
|
||||
var steamid = steamId.SteamId64;
|
||||
var reason = command.ArgCount >= 3
|
||||
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
|
||||
: _localizer?["sa_unknown"] ?? "Unknown";
|
||||
|
||||
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
|
||||
ulong steamid = steamId.SteamId64;
|
||||
string reason = Helper.GetArgsReason(command, 3);
|
||||
|
||||
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
|
||||
if (!CheckValidBan(caller, time)) return;
|
||||
int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
|
||||
if (!CheckValidBan(caller, time))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var adminInfo = caller != null && caller.UserId.HasValue
|
||||
? PlayersInfo[caller.SteamID]
|
||||
: null;
|
||||
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
|
||||
|
||||
var player = Helper.GetPlayerFromSteamid64(steamid);
|
||||
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid);
|
||||
if (player != null && player.IsValid)
|
||||
{
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Ban(caller, player, time, reason, callerName, silent: true);
|
||||
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
|
||||
|
|
@ -220,8 +260,10 @@ public partial class CS2_SimpleAdmin
|
|||
else
|
||||
{
|
||||
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Asynchronous ban operation if player is not online or not found
|
||||
Task.Run(async () =>
|
||||
{
|
||||
|
|
@ -232,7 +274,7 @@ public partial class CS2_SimpleAdmin
|
|||
penaltyId);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Ban, _localizer);
|
||||
|
||||
command.ReplyToCommand($"Player with steamid {steamid} is not online. Ban has been added offline.");
|
||||
|
|
@ -241,7 +283,9 @@ public partial class CS2_SimpleAdmin
|
|||
Helper.LogCommand(caller, command);
|
||||
|
||||
if (UnlockedCommands)
|
||||
{
|
||||
Server.ExecuteCommand($"banid 1 {steamId.SteamId3}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -253,10 +297,18 @@ public partial class CS2_SimpleAdmin
|
|||
[CommandHelper(minArgs: 1, usage: "<ip> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnBanIpCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
if (DatabaseProvider == null) return;
|
||||
var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
|
||||
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
|
||||
var ipAddress = command.GetArg(1);
|
||||
if (DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
|
||||
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string ipAddress = command.GetArg(1);
|
||||
|
||||
if (!Helper.IsValidIp(ipAddress))
|
||||
{
|
||||
|
|
@ -264,28 +316,31 @@ public partial class CS2_SimpleAdmin
|
|||
return;
|
||||
}
|
||||
|
||||
var reason = command.ArgCount >= 3
|
||||
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
|
||||
: _localizer?["sa_unknown"] ?? "Unknown";
|
||||
|
||||
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
|
||||
string reason = Helper.GetArgsReason(command, 3);
|
||||
|
||||
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
|
||||
int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
|
||||
|
||||
if (!CheckValidBan(caller, time)) return;
|
||||
if (!CheckValidBan(caller, time))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var adminInfo = caller != null && caller.UserId.HasValue
|
||||
? PlayersInfo[caller.SteamID]
|
||||
: null;
|
||||
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
|
||||
|
||||
var players = Helper.GetPlayerFromIp(ipAddress);
|
||||
List<CCSPlayerController> players = Helper.GetPlayerFromIp(ipAddress);
|
||||
if (players.Count >= 1)
|
||||
{
|
||||
foreach (var player in players)
|
||||
foreach (CCSPlayerController player in players)
|
||||
{
|
||||
if (player == null || !player.IsValid) continue;
|
||||
if (player == null || !player.IsValid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Ban(caller, player, time, reason, callerName, silent: true);
|
||||
}
|
||||
|
|
@ -312,9 +367,12 @@ public partial class CS2_SimpleAdmin
|
|||
/// <returns>True if ban duration is valid; otherwise, false.</returns>
|
||||
private bool CheckValidBan(CCSPlayerController? caller, int duration)
|
||||
{
|
||||
if (caller == null) return true;
|
||||
if (caller == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban");
|
||||
bool canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban");
|
||||
|
||||
if (duration <= 0 && !canPermBan)
|
||||
{
|
||||
|
|
@ -322,7 +380,10 @@ public partial class CS2_SimpleAdmin
|
|||
return false;
|
||||
}
|
||||
|
||||
if (duration <= Config.OtherSettings.MaxBanDuration || canPermBan) return true;
|
||||
if (duration <= Config.OtherSettings.MaxBanDuration || canPermBan)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_max_duration_exceeded", Config.OtherSettings.MaxBanDuration]}");
|
||||
return false;
|
||||
|
|
@ -337,20 +398,20 @@ public partial class CS2_SimpleAdmin
|
|||
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnUnbanCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
if (DatabaseProvider == null) return;
|
||||
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
|
||||
if (DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
|
||||
if (command.GetArg(1).Length <= 1)
|
||||
{
|
||||
command.ReplyToCommand($"Too short pattern to search.");
|
||||
return;
|
||||
}
|
||||
|
||||
var pattern = command.GetArg(1);
|
||||
var reason = command.ArgCount >= 2
|
||||
? string.Join(" ", Enumerable.Range(2, command.ArgCount - 2).Select(command.GetArg)).Trim()
|
||||
: _localizer?["sa_unknown"] ?? "Unknown";
|
||||
|
||||
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
|
||||
string pattern = command.GetArg(1);
|
||||
string reason = Helper.GetArgsReason(command, 2);
|
||||
Task.Run(async () => await BanManager.UnbanPlayer(pattern, callerSteamId, reason));
|
||||
Helper.LogCommand(caller, command);
|
||||
command.ReplyToCommand($"Unbanned player with pattern {pattern}.");
|
||||
|
|
@ -366,26 +427,31 @@ public partial class CS2_SimpleAdmin
|
|||
public void OnWarnCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
if (DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
}
|
||||
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
if (command.ArgCount < 2)
|
||||
{
|
||||
return;
|
||||
|
||||
var targets = GetTarget(command);
|
||||
if (targets == null) return;
|
||||
var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.Connected && !player.IsHLTV).ToList();
|
||||
}
|
||||
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.Connected && !player.IsHLTV).ToList();
|
||||
|
||||
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
|
||||
var reason = command.ArgCount >= 3
|
||||
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
|
||||
: _localizer?["sa_unknown"] ?? "Unknown";
|
||||
|
||||
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
|
||||
int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
|
||||
string reason = Helper.GetArgsReason(command, 3);
|
||||
|
||||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
|
|
@ -407,14 +473,23 @@ public partial class CS2_SimpleAdmin
|
|||
/// <param name="command">Optional command info for logging.</param>
|
||||
internal void Warn(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null)
|
||||
{
|
||||
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
|
||||
if (!caller.CanTarget(player)) return;
|
||||
if (!CheckValidBan(caller, time)) return;
|
||||
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CheckValidBan(caller, time))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default caller name if not provided
|
||||
callerName = !string.IsNullOrEmpty(caller?.PlayerName)
|
||||
? caller.PlayerName
|
||||
: (_localizer?["sa_console"] ?? "Console");
|
||||
callerName = Helper.GetCallerName(caller);
|
||||
|
||||
// Freeze player pawn if alive
|
||||
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
|
||||
|
|
@ -424,8 +499,8 @@ public partial class CS2_SimpleAdmin
|
|||
}
|
||||
|
||||
// Get player and admin information
|
||||
var playerInfo = PlayersInfo[player.SteamID];
|
||||
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
|
||||
PlayerInfo playerInfo = PlayersInfo[player.SteamID];
|
||||
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
|
||||
|
||||
// Asynchronously handle warning logic
|
||||
Task.Run(async () =>
|
||||
|
|
@ -438,16 +513,20 @@ public partial class CS2_SimpleAdmin
|
|||
});
|
||||
|
||||
// Check for warn thresholds and execute punish command if applicable
|
||||
var totalWarns = await WarnManager.GetPlayerWarnsCount(player.SteamID);
|
||||
int totalWarns = await WarnManager.GetPlayerWarnsCount(player.SteamID);
|
||||
if (Config.WarnThreshold.Count > 0)
|
||||
{
|
||||
string? punishCommand = null;
|
||||
var lastKey = Config.WarnThreshold.Keys.Max();
|
||||
int lastKey = Config.WarnThreshold.Keys.Max();
|
||||
|
||||
if (totalWarns >= lastKey)
|
||||
{
|
||||
punishCommand = Config.WarnThreshold[lastKey];
|
||||
else if (Config.WarnThreshold.TryGetValue(totalWarns, out var value))
|
||||
}
|
||||
else if (Config.WarnThreshold.TryGetValue(totalWarns, out string? value))
|
||||
{
|
||||
punishCommand = value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(punishCommand))
|
||||
{
|
||||
|
|
@ -460,13 +539,25 @@ public partial class CS2_SimpleAdmin
|
|||
});
|
||||
|
||||
// Determine message keys and arguments based on warning time
|
||||
var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = time == 0
|
||||
? ("sa_player_warn_message_perm", "sa_admin_warn_message_perm",
|
||||
new object[] { reason, "CALLER" },
|
||||
new object[] { "CALLER", player.PlayerName, reason })
|
||||
: ("sa_player_warn_message_time", "sa_admin_warn_message_time",
|
||||
[reason, time, "CALLER"],
|
||||
["CALLER", player.PlayerName, reason, time]);
|
||||
string? messageKey;
|
||||
string? activityMessageKey;
|
||||
object[]? centerArgs;
|
||||
object[]? adminActivityArgs;
|
||||
|
||||
if (time == 0)
|
||||
{
|
||||
messageKey = "sa_player_warn_message_perm";
|
||||
activityMessageKey = "sa_admin_warn_message_perm";
|
||||
centerArgs = new object[] { reason, "CALLER" };
|
||||
adminActivityArgs = new object[] { "CALLER", player.PlayerName, reason };
|
||||
}
|
||||
else
|
||||
{
|
||||
messageKey = "sa_player_warn_message_time";
|
||||
activityMessageKey = "sa_admin_warn_message_time";
|
||||
centerArgs = [reason, time, "CALLER"];
|
||||
adminActivityArgs = ["CALLER", player.PlayerName, reason, time];
|
||||
}
|
||||
|
||||
// Display center message to the playser
|
||||
Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs);
|
||||
|
|
@ -479,14 +570,18 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
// Log the warning command
|
||||
if (command == null)
|
||||
Helper.LogCommand(caller, $"css_warn {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
|
||||
{
|
||||
Helper.LogCommand(caller, $"css_warn {Helper.GetLogName(player)} {time} {reason}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Helper.LogCommand(caller, command);
|
||||
}
|
||||
|
||||
// Send Discord notification for the warning
|
||||
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Warn, _localizer);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a warning to a player by their SteamID, including support for offline players.
|
||||
/// </summary>
|
||||
|
|
@ -498,18 +593,18 @@ public partial class CS2_SimpleAdmin
|
|||
internal void AddWarn(CCSPlayerController? caller, SteamID steamid, int time, string reason, WarnManager? warnManager = null)
|
||||
{
|
||||
// Set default caller name if not provided
|
||||
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
|
||||
? caller.PlayerName
|
||||
: (_localizer?["sa_console"] ?? "Console");
|
||||
|
||||
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
|
||||
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
|
||||
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
|
||||
|
||||
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
|
||||
|
||||
if (player != null && player.IsValid)
|
||||
{
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Warn(caller, player, time, reason, callerName);
|
||||
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
|
||||
|
|
@ -517,8 +612,10 @@ public partial class CS2_SimpleAdmin
|
|||
else
|
||||
{
|
||||
if (!caller.CanTarget(steamid))
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Asynchronous ban operation if player is not online or not found
|
||||
Task.Run(async () =>
|
||||
{
|
||||
|
|
@ -530,16 +627,20 @@ public partial class CS2_SimpleAdmin
|
|||
});
|
||||
|
||||
// Check for warn thresholds and execute punish command if applicable
|
||||
var totalWarns = await WarnManager.GetPlayerWarnsCount(steamid.SteamId64);
|
||||
int totalWarns = await WarnManager.GetPlayerWarnsCount(steamid.SteamId64);
|
||||
if (Config.WarnThreshold.Count > 0)
|
||||
{
|
||||
string? punishCommand = null;
|
||||
var lastKey = Config.WarnThreshold.Keys.Max();
|
||||
int lastKey = Config.WarnThreshold.Keys.Max();
|
||||
|
||||
if (totalWarns >= lastKey)
|
||||
{
|
||||
punishCommand = Config.WarnThreshold[lastKey];
|
||||
else if (Config.WarnThreshold.TryGetValue(totalWarns, out var value))
|
||||
}
|
||||
else if (Config.WarnThreshold.TryGetValue(totalWarns, out string? value))
|
||||
{
|
||||
punishCommand = value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(punishCommand))
|
||||
{
|
||||
|
|
@ -550,11 +651,11 @@ public partial class CS2_SimpleAdmin
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Warn, _localizer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles removing a warning (unwarn) by a pattern string.
|
||||
/// </summary>
|
||||
|
|
@ -564,7 +665,10 @@ public partial class CS2_SimpleAdmin
|
|||
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnUnwarnCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
if (DatabaseProvider == null) return;
|
||||
if (DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.GetArg(1).Length <= 1)
|
||||
{
|
||||
|
|
@ -572,9 +676,9 @@ public partial class CS2_SimpleAdmin
|
|||
return;
|
||||
}
|
||||
|
||||
var pattern = command.GetArg(1);
|
||||
string pattern = command.GetArg(1);
|
||||
Task.Run(async () => await WarnManager.UnwarnPlayer(pattern));
|
||||
Helper.LogCommand(caller, command);
|
||||
command.ReplyToCommand($"Unwarned player with pattern {pattern}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Translations;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Memory;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using System.Text;
|
||||
using CounterStrikeSharp.API.Modules.Commands.Targeting;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CS2_SimpleAdmin.Menus;
|
||||
using PanoramaUiKit.Shared.Api;
|
||||
using PanoramaUiKit.Shared.Common;
|
||||
|
||||
namespace CS2_SimpleAdmin;
|
||||
|
||||
public partial class CS2_SimpleAdmin
|
||||
{
|
||||
private const float AnnouncementSeconds = 7f;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a chat message only to admins that have chat permission.
|
||||
/// The message is encoded properly to handle UTF-8 characters.
|
||||
|
|
@ -23,16 +28,18 @@ public partial class CS2_SimpleAdmin
|
|||
{
|
||||
Helper.LogCommand(caller, command);
|
||||
|
||||
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
|
||||
foreach (var player in Helper.GetValidPlayers()
|
||||
foreach (CCSPlayerController? player in Helper.GetValidPlayers()
|
||||
.Where(p => AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
{
|
||||
if (_localizer != null)
|
||||
{
|
||||
player.PrintToChat(_localizer["sa_adminchat_template_admin",
|
||||
caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName,
|
||||
Helper.GetCallerName(caller),
|
||||
utf8String]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,14 +52,17 @@ public partial class CS2_SimpleAdmin
|
|||
[RequiresPermissions("@css/chat")]
|
||||
public void OnAdminCustomSayCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0) return;
|
||||
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
|
||||
Helper.LogCommand(caller, command);
|
||||
|
||||
foreach (var player in Helper.GetValidPlayers())
|
||||
foreach (CCSPlayerController player in Helper.GetValidPlayers())
|
||||
{
|
||||
player.PrintToChat(utf8String.ReplaceColorTags());
|
||||
}
|
||||
|
|
@ -67,14 +77,17 @@ public partial class CS2_SimpleAdmin
|
|||
[RequiresPermissions("@css/chat")]
|
||||
public void OnAdminSayCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0) return;
|
||||
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
var callerName = caller?.PlayerName;
|
||||
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
string? callerName = caller?.PlayerName;
|
||||
|
||||
Helper.LogCommand(caller, command);
|
||||
foreach (var player in Helper.GetValidPlayers())
|
||||
foreach (CCSPlayerController player in Helper.GetValidPlayers())
|
||||
{
|
||||
// The message always goes out (it is a broadcast, not an activity notice),
|
||||
// but the admin's name follows ShowActivityType like every other activity message.
|
||||
|
|
@ -93,19 +106,23 @@ public partial class CS2_SimpleAdmin
|
|||
[RequiresPermissions("@css/chat")]
|
||||
public void OnAdminPrivateSayCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
|
||||
var targets = GetTarget(command);
|
||||
if (targets == null) return;
|
||||
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
|
||||
|
||||
//Helper.LogCommand(caller, command);
|
||||
|
||||
var range = command.GetArg(0).Length + command.GetArg(1).Length + 2;
|
||||
var message = command.GetCommandString[range..];
|
||||
int range = command.GetArg(0).Length + command.GetArg(1).Length + 2;
|
||||
string message = command.GetCommandString[range..];
|
||||
|
||||
var utf8BytesString = Encoding.UTF8.GetBytes(message);
|
||||
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
byte[] utf8BytesString = Encoding.UTF8.GetBytes(message);
|
||||
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
|
||||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
|
|
@ -116,7 +133,7 @@ public partial class CS2_SimpleAdmin
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts a center-screen message to all players.
|
||||
/// Broadcasts a message to all players in a Panorama UI Kit banner.
|
||||
/// </summary>
|
||||
/// <param name="caller">The admin or console sending the message.</param>
|
||||
/// <param name="command">The command input containing the message.</param>
|
||||
|
|
@ -124,15 +141,15 @@ public partial class CS2_SimpleAdmin
|
|||
[RequiresPermissions("@css/chat")]
|
||||
public void OnAdminCenterSayCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
|
||||
Helper.LogCommand(caller, command);
|
||||
Helper.PrintToCenterAll(utf8String.ReplaceColorTags());
|
||||
UiKit.Instance?.Banner(UiKit.BannerLayout, StripColorTags(utf8String), UiStyle.Info, AnnouncementSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a HUD alert message to all players.
|
||||
/// Sends a message to all players in a Panorama UI Kit toast, titled with the admin's name.
|
||||
/// </summary>
|
||||
/// <param name="caller">The admin or console sending the message.</param>
|
||||
/// <param name="command">The command input containing the message.</param>
|
||||
|
|
@ -140,14 +157,31 @@ public partial class CS2_SimpleAdmin
|
|||
[RequiresPermissions("@css/chat")]
|
||||
public void OnAdminHudSayCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
|
||||
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
|
||||
Helper.LogCommand(caller, command);
|
||||
|
||||
VirtualFunctions.ClientPrintAll(
|
||||
HudDestination.Alert,
|
||||
utf8String.ReplaceColorTags(),
|
||||
0, 0, 0, 0, 0);
|
||||
IPanoramaUiKit? kit = UiKit.Instance;
|
||||
if (kit == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string message = StripColorTags(utf8String);
|
||||
string? callerName = caller?.PlayerName;
|
||||
|
||||
foreach (CCSPlayerController player in Helper.GetValidPlayers())
|
||||
{
|
||||
kit.Toast(player, UiKit.ToastLayout, Helper.ResolveActivityName(player, callerName), message, UiStyle.Info, AnnouncementSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Panorama does not render chat color tags such as {red}: drop them.
|
||||
/// </summary>
|
||||
private static string StripColorTags(string message)
|
||||
{
|
||||
return Regex.Replace(message, @"\{[A-Za-z]+\}", string.Empty).Trim();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Translations;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Menu;
|
||||
using CounterStrikeSharp.API.Modules.Timers;
|
||||
using CS2_SimpleAdmin.Menus;
|
||||
using PanoramaUiKit.Shared.Api;
|
||||
using PanoramaUiKit.Shared.Votes;
|
||||
|
||||
namespace CS2_SimpleAdmin;
|
||||
|
||||
public partial class CS2_SimpleAdmin
|
||||
{
|
||||
private const float VoteDurationSeconds = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the vote command, creates voting menu for players, and collects answers.
|
||||
/// Displays results after timeout and resets voting state.
|
||||
/// Handles the vote command: shows the vote screen to every player, collects one answer each
|
||||
/// and prints the results when the countdown ends.
|
||||
/// </summary>
|
||||
/// <param name="caller">The player/admin who initiated the vote, or null for console.</param>
|
||||
/// <param name="command">Command object containing question and options.</param>
|
||||
|
|
@ -19,80 +24,123 @@ public partial class CS2_SimpleAdmin
|
|||
public void OnVoteCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
if (command.ArgCount < 2 || _localizer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Helper.LogCommand(caller, command);
|
||||
|
||||
VoteAnswers.Clear();
|
||||
|
||||
var question = command.GetArg(1);
|
||||
var answersCount = command.ArgCount;
|
||||
|
||||
if (caller == null || !SilentPlayers.Contains(caller.Slot))
|
||||
if (caller != null && SilentPlayers.Contains(caller.Slot))
|
||||
{
|
||||
for (var i = 2; i <= answersCount - 1; i++)
|
||||
{
|
||||
VoteAnswers.Add(command.GetArg(i), 0);
|
||||
}
|
||||
|
||||
foreach (var player in Helper.GetValidPlayers())
|
||||
{
|
||||
using (new WithTemporaryCulture(player.GetLanguage()))
|
||||
{
|
||||
IMenu? voteMenu = Helper.CreateMenu(_localizer["sa_admin_vote_menu_title", question]);
|
||||
if (voteMenu == null)
|
||||
return;
|
||||
//ChatMenu voteMenu = new(_localizer!["sa_admin_vote_menu_title", question]);
|
||||
|
||||
for (var i = 2; i <= answersCount - 1; i++)
|
||||
{
|
||||
voteMenu.AddMenuOption(command.GetArg(i), Helper.HandleVotes);
|
||||
}
|
||||
|
||||
voteMenu.PostSelectAction = PostSelectAction.Close;
|
||||
|
||||
Helper.PrintToCenterAll(_localizer["sa_admin_vote_message", caller == null ? _localizer["sa_console"] : caller.PlayerName, question]);
|
||||
|
||||
player.SendLocalizedMessage(_localizer,
|
||||
"sa_admin_vote_message",
|
||||
caller == null ? _localizer["sa_console"] : caller.PlayerName,
|
||||
question);
|
||||
|
||||
voteMenu.Open(player);
|
||||
|
||||
//MenuManager.OpenChatMenu(player, voteMenu);
|
||||
}
|
||||
}
|
||||
|
||||
VoteInProgress = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (VoteInProgress)
|
||||
IPanoramaUiKit? kit = UiKit.Instance;
|
||||
if (kit == null)
|
||||
{
|
||||
AddTimer(30, () =>
|
||||
return;
|
||||
}
|
||||
|
||||
string question = command.GetArg(1);
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
List<string> answers = [];
|
||||
for (int i = 2; i < command.ArgCount; i++)
|
||||
{
|
||||
answers.Add(command.GetArg(i));
|
||||
}
|
||||
|
||||
int[] counts = new int[answers.Count];
|
||||
Dictionary<ulong, int> ballots = [];
|
||||
|
||||
VoteInProgress = true;
|
||||
|
||||
foreach (CCSPlayerController player in Helper.GetValidPlayers())
|
||||
{
|
||||
kit.Votes.Show(player, BuildVote(player, question, answers, counts, null, OnVote));
|
||||
player.SendLocalizedMessage(_localizer, "sa_admin_vote_message", callerName, question);
|
||||
}
|
||||
|
||||
Helper.PrintToCenterAll(_localizer["sa_admin_vote_message", callerName, question]);
|
||||
|
||||
AddTimer(VoteDurationSeconds, () =>
|
||||
{
|
||||
UiKit.Instance?.Votes.HideAll(UiKit.VoteLayout);
|
||||
VoteInProgress = false;
|
||||
|
||||
if (_localizer == null)
|
||||
{
|
||||
foreach (var player in Helper.GetValidPlayers())
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (CCSPlayerController player in Helper.GetValidPlayers())
|
||||
{
|
||||
player.SendLocalizedMessage(_localizer, "sa_admin_vote_message_results", question);
|
||||
|
||||
for (int i = 0; i < answers.Count; i++)
|
||||
{
|
||||
if (_localizer != null)
|
||||
player.SendLocalizedMessage(_localizer,
|
||||
"sa_admin_vote_message_results",
|
||||
question);
|
||||
player.SendLocalizedMessage(_localizer, "sa_admin_vote_message_results_answer", answers[i], counts[i]);
|
||||
}
|
||||
}
|
||||
}, TimerFlags.STOP_ON_MAPCHANGE);
|
||||
|
||||
return;
|
||||
|
||||
void OnVote(VoteEvent vote)
|
||||
{
|
||||
if (!VoteInProgress || ballots.ContainsKey(vote.Player.SteamID))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ballots[vote.Player.SteamID] = vote.Index;
|
||||
counts[vote.Index]++;
|
||||
|
||||
IPanoramaUiKit? currentKit = UiKit.Instance;
|
||||
if (currentKit == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (CCSPlayerController player in Helper.GetValidPlayers())
|
||||
{
|
||||
int? selected = null;
|
||||
if (ballots.TryGetValue(player.SteamID, out int index))
|
||||
{
|
||||
selected = index;
|
||||
}
|
||||
|
||||
foreach (var (key, value) in VoteAnswers)
|
||||
{
|
||||
foreach (var player in Helper.GetValidPlayers())
|
||||
{
|
||||
if (_localizer != null)
|
||||
player.SendLocalizedMessage(_localizer,
|
||||
"sa_admin_vote_message_results_answer",
|
||||
key,
|
||||
value);
|
||||
}
|
||||
}
|
||||
VoteAnswers.Clear();
|
||||
VoteInProgress = false;
|
||||
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
|
||||
currentKit.Votes.Update(player, BuildVote(player, question, answers, counts, selected, OnVote));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The vote screen for one player, locked once that player has voted.
|
||||
/// </summary>
|
||||
private static VoteOptions BuildVote(CCSPlayerController player, string question, List<string> answers, int[] counts,
|
||||
int? selected, Action<VoteEvent> onVote)
|
||||
{
|
||||
string title;
|
||||
using (new WithTemporaryCulture(player.GetLanguage()))
|
||||
{
|
||||
title = _localizer?["sa_admin_vote_menu_title", question] ?? question;
|
||||
}
|
||||
|
||||
List<VoteChoice> choices = [];
|
||||
for (int i = 0; i < answers.Count; i++)
|
||||
{
|
||||
choices.Add(new VoteChoice(answers[i], counts[i]));
|
||||
}
|
||||
|
||||
return new VoteOptions
|
||||
{
|
||||
Layout = UiKit.VoteLayout,
|
||||
Title = title,
|
||||
Choices = choices,
|
||||
Selected = selected,
|
||||
Locked = selected.HasValue,
|
||||
Seconds = VoteDurationSeconds,
|
||||
OnVote = onVote
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
// if (!caller.CanTarget(player)) return;
|
||||
//
|
||||
// // Set default caller name if not provided
|
||||
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
|
||||
// callerName ??= Helper.GetCallerName(caller);
|
||||
//
|
||||
// // Toggle no-clip mode for the player
|
||||
// player.Pawn.Value?.ToggleNoclip();
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
//
|
||||
// // Log the command
|
||||
// if (command == null)
|
||||
// Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
|
||||
// Helper.LogCommand(caller, $"css_noclip {Helper.GetLogName(player)}");
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
// [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
// public void OnGodCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
// {
|
||||
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
// var callerName = Helper.GetCallerName(caller);
|
||||
// var targets = GetTarget(command);
|
||||
// if (targets == null) return;
|
||||
//
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
// if (!caller.CanTarget(player)) return;
|
||||
//
|
||||
// // Set default caller name if not provided
|
||||
// var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
|
||||
// var callerName = Helper.GetCallerName(caller);
|
||||
//
|
||||
// // Toggle god mode for the player
|
||||
// if (!GodPlayers.Add(player.Slot))
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
//
|
||||
// // Log the command
|
||||
// if (command == null)
|
||||
// Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
|
||||
// Helper.LogCommand(caller, $"css_god {Helper.GetLogName(player)}");
|
||||
//
|
||||
// // Determine message key and arguments for the god mode notification
|
||||
// var (activityMessageKey, adminActivityArgs) =
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
// [RequiresPermissions("@css/slay")]
|
||||
// public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
// {
|
||||
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
// var callerName = Helper.GetCallerName(caller);
|
||||
// int.TryParse(command.GetArg(2), out var time);
|
||||
//
|
||||
// var targets = GetTarget(command);
|
||||
|
|
@ -171,7 +171,7 @@
|
|||
// [RequiresPermissions("@css/slay")]
|
||||
// public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
// {
|
||||
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
// var callerName = Helper.GetCallerName(caller);
|
||||
// float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size);
|
||||
//
|
||||
// var targets = GetTarget(command);
|
||||
|
|
@ -221,7 +221,7 @@
|
|||
// if (!caller.CanTarget(player)) return;
|
||||
//
|
||||
// // Set default caller name if not provided
|
||||
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
|
||||
// callerName ??= Helper.GetCallerName(caller);
|
||||
//
|
||||
// // Freeze player pawn
|
||||
// player.Pawn.Value?.Freeze();
|
||||
|
|
@ -245,7 +245,7 @@
|
|||
//
|
||||
// // Log the command and send Discord notification
|
||||
// if (command == null)
|
||||
// Helper.LogCommand(caller, $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}");
|
||||
// Helper.LogCommand(caller, $"css_freeze {Helper.GetLogName(player)} {time}");
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
|
|
@ -257,7 +257,7 @@
|
|||
// [RequiresPermissions("@css/slay")]
|
||||
// public void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
// {
|
||||
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
// var callerName = Helper.GetCallerName(caller);
|
||||
//
|
||||
// var targets = GetTarget(command);
|
||||
// if (targets == null) return;
|
||||
|
|
@ -284,7 +284,7 @@
|
|||
// if (!caller.CanTarget(player)) return;
|
||||
//
|
||||
// // Set default caller name if not provided
|
||||
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
|
||||
// callerName ??= Helper.GetCallerName(caller);
|
||||
//
|
||||
// // Unfreeze player pawn
|
||||
// player.Pawn.Value?.Unfreeze();
|
||||
|
|
@ -302,6 +302,6 @@
|
|||
//
|
||||
// // Log the command and send Discord notification
|
||||
// if (command == null)
|
||||
// Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
|
||||
// Helper.LogCommand(caller, $"css_unfreeze {Helper.GetLogName(player)}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
using System.Globalization;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Commands.Targeting;
|
||||
using CounterStrikeSharp.API.Modules.Entities.Constants;
|
||||
using CounterStrikeSharp.API.Modules.Memory;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
|
||||
namespace CS2_SimpleAdmin;
|
||||
|
|
@ -21,17 +20,20 @@ public partial class CS2_SimpleAdmin
|
|||
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnSlayCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
var targets = GetTarget(command);
|
||||
if (targets == null) return;
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playersToTarget = targets.Players.Where(player => player.IsValid && player is {IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
|
||||
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
|
||||
|
||||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
Slay(caller, player, callerName, command);
|
||||
});
|
||||
|
||||
|
||||
Helper.LogCommand(caller, command);
|
||||
}
|
||||
|
||||
|
|
@ -44,18 +46,25 @@ public partial class CS2_SimpleAdmin
|
|||
/// <param name="command">Optional command info for logging.</param>
|
||||
internal static void Slay(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
|
||||
{
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.Connected) return;
|
||||
if (!caller.CanTarget(player)) return;
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default caller name if not provided
|
||||
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
|
||||
callerName ??= Helper.GetCallerName(caller);
|
||||
|
||||
// Make the player commit suicide
|
||||
player.CommitSuicide(false, true);
|
||||
player.EmitSound("Player.Death");
|
||||
|
||||
// Determine message keys and arguments for the slay notification
|
||||
var (activityMessageKey, adminActivityArgs) =
|
||||
(string? activityMessageKey, object[]? adminActivityArgs) =
|
||||
("sa_admin_slay_message",
|
||||
new object[] { "CALLER", player.PlayerName });
|
||||
|
||||
|
|
@ -67,7 +76,9 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
// Log the command and send Discord notification
|
||||
if (command == null)
|
||||
Helper.LogCommand(caller, $"css_slay {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
|
||||
{
|
||||
Helper.LogCommand(caller, $"css_slay {Helper.GetLogName(player)}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -79,12 +90,15 @@ public partial class CS2_SimpleAdmin
|
|||
[CommandHelper(minArgs: 1, usage: "<#userid or name> [damage]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnSlapCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var damage = 0;
|
||||
int damage = 0;
|
||||
|
||||
var targets = GetTarget(command);
|
||||
if (targets == null) return;
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
|
||||
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
|
||||
|
||||
if (command.ArgCount >= 2)
|
||||
{
|
||||
|
|
@ -94,14 +108,16 @@ public partial class CS2_SimpleAdmin
|
|||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
if (player.Connected != PlayerConnectedState.Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (caller!.CanTarget(player))
|
||||
{
|
||||
Slap(caller, player, damage, command);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Helper.LogCommand(caller, command);
|
||||
}
|
||||
|
||||
|
|
@ -114,26 +130,34 @@ public partial class CS2_SimpleAdmin
|
|||
/// <param name="command">Optional command info for logging.</param>
|
||||
internal static void Slap(CCSPlayerController? caller, CCSPlayerController player, int damage, CommandInfo? command = null)
|
||||
{
|
||||
if (!caller.CanTarget(player)) return;
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default caller name if not provided
|
||||
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
|
||||
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
|
||||
// Apply slap damage to the player
|
||||
player.Pawn.Value?.Slap(damage);
|
||||
player.EmitSound("Player.DamageFall");
|
||||
|
||||
// Log the command
|
||||
if (command == null)
|
||||
Helper.LogCommand(caller, $"css_slap {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {damage}");
|
||||
|
||||
{
|
||||
Helper.LogCommand(caller, $"css_slap {Helper.GetLogName(player)} {damage}");
|
||||
}
|
||||
|
||||
// Determine message key and arguments for the slap notification
|
||||
var (activityMessageKey, adminActivityArgs) =
|
||||
(string? activityMessageKey, object[]? adminActivityArgs) =
|
||||
("sa_admin_slap_message",
|
||||
new object[] { "CALLER", player.PlayerName });
|
||||
|
||||
// Display admin activity message to other players
|
||||
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
|
||||
if (caller != null && SilentPlayers.Contains(caller.Slot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_localizer != null)
|
||||
{
|
||||
|
|
@ -150,15 +174,18 @@ public partial class CS2_SimpleAdmin
|
|||
[CommandHelper(minArgs: 2, usage: "<#userid or name> [<ct/tt/spec>] [-k]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||
public void OnTeamCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
var teamName = command.GetArg(2).ToLower();
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
string teamName = command.GetArg(2).ToLower();
|
||||
string _teamName;
|
||||
var teamNum = CsTeam.Spectator;
|
||||
CsTeam teamNum = CsTeam.Spectator;
|
||||
|
||||
var targets = GetTarget(command);
|
||||
if (targets == null) return;
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
|
||||
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
|
||||
|
||||
switch (teamName)
|
||||
{
|
||||
|
|
@ -185,13 +212,13 @@ public partial class CS2_SimpleAdmin
|
|||
break;
|
||||
}
|
||||
|
||||
var kill = command.GetArg(3).ToLower().Equals("-k");
|
||||
bool kill = command.GetArg(3).ToLower().Equals("-k");
|
||||
|
||||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
ChangeTeam(caller, player, _teamName, teamNum, kill, command);
|
||||
});
|
||||
|
||||
|
||||
Helper.LogCommand(caller, command);
|
||||
}
|
||||
|
||||
|
|
@ -208,45 +235,79 @@ public partial class CS2_SimpleAdmin
|
|||
{
|
||||
// Check if the player is valid and connected
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the caller can target the player
|
||||
if (!caller.CanTarget(player)) return;
|
||||
if (!caller.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set default caller name if not provided
|
||||
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
|
||||
// Change team based on the provided teamName and conditions
|
||||
if (!teamName.Equals("swap", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && teamNum != CsTeam.Spectator && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
|
||||
{
|
||||
player.SwitchTeam(teamNum);
|
||||
}
|
||||
else
|
||||
{
|
||||
player.ChangeTeam(teamNum);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (player.TeamNum != (byte)CsTeam.Spectator)
|
||||
{
|
||||
var _teamNum = (CsTeam)player.TeamNum == CsTeam.Terrorist ? CsTeam.CounterTerrorist : CsTeam.Terrorist;
|
||||
teamName = _teamNum == CsTeam.Terrorist ? "TT" : "CT";
|
||||
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
|
||||
player.SwitchTeam(_teamNum);
|
||||
CsTeam _teamNum;
|
||||
|
||||
if ((CsTeam)player.TeamNum == CsTeam.Terrorist)
|
||||
{
|
||||
_teamNum = CsTeam.CounterTerrorist;
|
||||
}
|
||||
else
|
||||
{
|
||||
_teamNum = CsTeam.Terrorist;
|
||||
}
|
||||
if (_teamNum == CsTeam.Terrorist)
|
||||
{
|
||||
teamName = "TT";
|
||||
}
|
||||
else
|
||||
{
|
||||
teamName = "CT";
|
||||
}
|
||||
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
|
||||
{
|
||||
player.SwitchTeam(_teamNum);
|
||||
}
|
||||
else
|
||||
{
|
||||
player.ChangeTeam(_teamNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log the command
|
||||
if (command == null)
|
||||
{
|
||||
Helper.LogCommand(caller, $"css_team {player.PlayerName} {teamName}");
|
||||
}
|
||||
|
||||
// Determine message key and arguments for the team change notification
|
||||
var activityMessageKey = "sa_admin_team_message";
|
||||
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, teamName };
|
||||
string activityMessageKey = "sa_admin_team_message";
|
||||
object[] adminActivityArgs = new object[] { "CALLER", player.PlayerName, teamName };
|
||||
|
||||
// Display admin activity message to other players
|
||||
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
|
||||
if (caller != null && SilentPlayers.Contains(caller.Slot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
|
||||
}
|
||||
|
|
@ -261,21 +322,26 @@ public partial class CS2_SimpleAdmin
|
|||
public void OnRenameCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
// Set default caller name if not provided
|
||||
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
|
||||
// Get the new name from the command arguments
|
||||
var newName = command.GetArg(2);
|
||||
string newName = command.GetArg(2);
|
||||
|
||||
// Check if the new name is valid
|
||||
if (string.IsNullOrEmpty(newName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the targets based on the command
|
||||
var targets = GetTarget(command);
|
||||
if (targets == null) return;
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out valid players from the targets
|
||||
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
|
||||
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
|
||||
|
||||
// Log the command
|
||||
Helper.LogCommand(caller, command);
|
||||
|
|
@ -285,15 +351,19 @@ public partial class CS2_SimpleAdmin
|
|||
{
|
||||
// Check if the player is connected and can be targeted
|
||||
if (player.Connected != PlayerConnectedState.Connected || !caller!.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Determine message key and arguments for the rename notification
|
||||
var activityMessageKey = "sa_admin_rename_message";
|
||||
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
|
||||
string activityMessageKey = "sa_admin_rename_message";
|
||||
object[] adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
|
||||
|
||||
// Display admin activity message to other players
|
||||
if (caller != null && !SilentPlayers.Contains(caller.Slot))
|
||||
{
|
||||
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
|
||||
}
|
||||
|
||||
// Rename the player
|
||||
player.Rename(newName);
|
||||
|
|
@ -310,17 +380,20 @@ public partial class CS2_SimpleAdmin
|
|||
public void OnPrenameCommand(CCSPlayerController? caller, CommandInfo command)
|
||||
{
|
||||
// Set default caller name if not provided
|
||||
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
|
||||
string callerName = Helper.GetCallerName(caller);
|
||||
|
||||
// Get the new name from the command arguments
|
||||
var newName = command.GetArg(2);
|
||||
string newName = command.GetArg(2);
|
||||
|
||||
// Retrieve the targets based on the command
|
||||
var targets = GetTarget(command);
|
||||
if (targets == null) return;
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out valid players from the targets
|
||||
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
|
||||
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
|
||||
|
||||
// Log the command
|
||||
Helper.LogCommand(caller, command);
|
||||
|
|
@ -330,20 +403,22 @@ public partial class CS2_SimpleAdmin
|
|||
{
|
||||
// Check if the player is connected and can be targeted
|
||||
if (player.Connected != PlayerConnectedState.Connected || !caller!.CanTarget(player))
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Determine message key and arguments for the rename notification
|
||||
var activityMessageKey = "sa_admin_rename_message";
|
||||
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
|
||||
string activityMessageKey = "sa_admin_rename_message";
|
||||
object[] adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
|
||||
|
||||
// Display admin activity message to other players
|
||||
if (caller != null && !SilentPlayers.Contains(caller.Slot))
|
||||
{
|
||||
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
|
||||
}
|
||||
|
||||
|
||||
// Determine if the new name is valid and update the renamed players list
|
||||
var steamId = player.SteamID;
|
||||
ulong steamId = player.SteamID;
|
||||
if (!string.IsNullOrEmpty(newName))
|
||||
{
|
||||
RenamedPlayers[steamId] = newName;
|
||||
|
|
@ -370,51 +445,65 @@ public partial class CS2_SimpleAdmin
|
|||
IEnumerable<CCSPlayerController> playersToTeleport;
|
||||
CCSPlayerController? destinationPlayer;
|
||||
|
||||
var targets = GetTarget(command);
|
||||
TargetResult? targets = GetTarget(command);
|
||||
|
||||
if (command.ArgCount < 3)
|
||||
{
|
||||
if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (targets == null || targets.Count() != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destinationPlayer = targets.Players.FirstOrDefault(p =>
|
||||
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
|
||||
|
||||
if (destinationPlayer == null || !caller.CanTarget(destinationPlayer) || caller.PlayerPawn.Value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playersToTeleport = [caller];
|
||||
}
|
||||
else
|
||||
{
|
||||
var destination = GetTarget(command, 2);
|
||||
TargetResult? destination = GetTarget(command, 2);
|
||||
if (targets == null || destination == null || destination.Count() != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destinationPlayer = destination.Players.FirstOrDefault(p =>
|
||||
p is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
|
||||
|
||||
if (destinationPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playersToTeleport = targets.Players
|
||||
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p))
|
||||
.ToList();
|
||||
|
||||
if (!playersToTeleport.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Log command
|
||||
Helper.LogCommand(caller, command);
|
||||
|
||||
foreach (var player in playersToTeleport)
|
||||
foreach (CCSPlayerController player in playersToTeleport)
|
||||
{
|
||||
if (player.PlayerPawn?.Value == null || destinationPlayer?.PlayerPawn?.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
player.TeleportPlayer(destinationPlayer);
|
||||
|
||||
|
|
@ -453,7 +542,7 @@ public partial class CS2_SimpleAdmin
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Brings targeted player(s) to the caller or specified destination player's location.
|
||||
/// </summary>
|
||||
|
|
@ -469,11 +558,15 @@ public partial class CS2_SimpleAdmin
|
|||
if (command.ArgCount < 3)
|
||||
{
|
||||
if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targets = GetTarget(command);
|
||||
TargetResult? targets = GetTarget(command);
|
||||
if (targets == null || !targets.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destinationPlayer = caller;
|
||||
|
||||
|
|
@ -483,20 +576,26 @@ public partial class CS2_SimpleAdmin
|
|||
}
|
||||
else
|
||||
{
|
||||
var destination = GetTarget(command);
|
||||
TargetResult? destination = GetTarget(command);
|
||||
if (destination == null || destination.Count() != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destinationPlayer = destination.Players.FirstOrDefault(p =>
|
||||
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
|
||||
|
||||
if (destinationPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Rest args = targets to teleport
|
||||
var targets = GetTarget(command, 2);
|
||||
TargetResult? targets = GetTarget(command, 2);
|
||||
if (targets == null || !targets.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playersToTeleport = targets.Players
|
||||
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p))
|
||||
|
|
@ -504,15 +603,19 @@ public partial class CS2_SimpleAdmin
|
|||
}
|
||||
|
||||
if (destinationPlayer == null || !playersToTeleport.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Log command
|
||||
Helper.LogCommand(caller, command);
|
||||
|
||||
foreach (var player in playersToTeleport)
|
||||
foreach (CCSPlayerController player in playersToTeleport)
|
||||
{
|
||||
if (player.PlayerPawn?.Value == null || destinationPlayer.PlayerPawn?.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Teleport
|
||||
player.TeleportPlayer(destinationPlayer);
|
||||
|
|
@ -624,4 +727,4 @@ public partial class CS2_SimpleAdmin
|
|||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
|
||||
namespace CS2_SimpleAdmin;
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ public class Discord
|
|||
new() { Name = "Footer", Value = "" },
|
||||
new() { Name = "Time", Value = "{relative}" },
|
||||
];
|
||||
|
||||
|
||||
[JsonPropertyName("DiscordAssociatedAccountsSettings")]
|
||||
public DiscordPenaltySetting[] DiscordAssociatedAccountsSettings { get; set; } =
|
||||
[
|
||||
|
|
@ -119,7 +119,13 @@ public class CustomServerCommandData
|
|||
|
||||
public class MenuConfig
|
||||
{
|
||||
[JsonPropertyName("MenuType")] public string MenuType { get; set; } = "selectable";
|
||||
[JsonPropertyName("MenuLayout")] public string MenuLayout { get; set; } = "uikit_menu";
|
||||
|
||||
[JsonPropertyName("VoteLayout")] public string VoteLayout { get; set; } = "uikit_vote";
|
||||
|
||||
[JsonPropertyName("BannerLayout")] public string BannerLayout { get; set; } = "uikit_banner";
|
||||
|
||||
[JsonPropertyName("ToastLayout")] public string ToastLayout { get; set; } = "uikit_toast";
|
||||
|
||||
[JsonPropertyName("Durations")]
|
||||
public DurationItem[] Durations { get; set; } =
|
||||
|
|
@ -214,7 +220,7 @@ public class OtherSettings
|
|||
|
||||
[JsonPropertyName("MaxBanDuration")]
|
||||
public int MaxBanDuration { get; set; } = 60 * 24 * 7;
|
||||
|
||||
|
||||
[JsonPropertyName("MaxMuteDuration")]
|
||||
public int MaxMuteDuration { get; set; } = 60 * 24 * 7;
|
||||
|
||||
|
|
@ -226,16 +232,16 @@ public class OtherSettings
|
|||
|
||||
[JsonPropertyName("DisconnectedPlayersHistoryCount")]
|
||||
public int DisconnectedPlayersHistoryCount { get; set; } = 10;
|
||||
|
||||
|
||||
[JsonPropertyName("NotifyPenaltiesToAdminOnConnect")]
|
||||
public bool NotifyPenaltiesToAdminOnConnect { get; set; } = true;
|
||||
|
||||
|
||||
[JsonPropertyName("ShowBanMenuIfNoTime")]
|
||||
public bool ShowBanMenuIfNoTime { get; set; } = true;
|
||||
|
||||
|
||||
[JsonPropertyName("UserMessageGagChatType")]
|
||||
public bool UserMessageGagChatType { get; set; } = false;
|
||||
|
||||
|
||||
[JsonPropertyName("CheckMultiAccountsByIp")]
|
||||
public bool CheckMultiAccountsByIp { get; set; } = true;
|
||||
|
||||
|
|
@ -247,11 +253,11 @@ public class OtherSettings
|
|||
|
||||
public class CS2_SimpleAdminConfig : BasePluginConfig
|
||||
{
|
||||
[JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 25;
|
||||
[JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 26;
|
||||
|
||||
[JsonPropertyName("DatabaseConfig")]
|
||||
[JsonPropertyName("DatabaseConfig")]
|
||||
public DatabaseConfig DatabaseConfig { get; set; } = new();
|
||||
|
||||
|
||||
[JsonPropertyName("OtherSettings")]
|
||||
public OtherSettings OtherSettings { get; set; } = new();
|
||||
|
||||
|
|
@ -295,7 +301,7 @@ public class DatabaseConfig
|
|||
{
|
||||
[JsonPropertyName("DatabaseType")]
|
||||
public string DatabaseType { get; set; } = "SQLite";
|
||||
|
||||
|
||||
[JsonPropertyName("SqliteFilePath")]
|
||||
public string SqliteFilePath { get; set; } = "cs2-simpleadmin.sqlite";
|
||||
|
||||
|
|
@ -313,7 +319,7 @@ public class DatabaseConfig
|
|||
|
||||
[JsonPropertyName("DatabaseName")]
|
||||
public string DatabaseName { get; set; } = "";
|
||||
|
||||
|
||||
[JsonPropertyName("DatabaseSSlMode")]
|
||||
public string DatabaseSSlMode { get; set; } = "preferred";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ public class Database(string dbConnectionString)
|
|||
{
|
||||
try
|
||||
{
|
||||
var connection = new MySqlConnection(dbConnectionString);
|
||||
MySqlConnection connection = new MySqlConnection(dbConnectionString);
|
||||
connection.Open();
|
||||
|
||||
|
||||
// using var cmd = connection.CreateCommand();
|
||||
// cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
|
||||
// cmd.ExecuteNonQueryAsync();
|
||||
|
||||
|
||||
return connection;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -29,13 +29,13 @@ public class Database(string dbConnectionString)
|
|||
{
|
||||
try
|
||||
{
|
||||
var connection = new MySqlConnection(dbConnectionString);
|
||||
MySqlConnection connection = new MySqlConnection(dbConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
// await using var cmd = connection.CreateCommand();
|
||||
// cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
|
||||
// await cmd.ExecuteNonQueryAsync();
|
||||
|
||||
|
||||
return connection;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -53,9 +53,9 @@ public class Database(string dbConnectionString)
|
|||
|
||||
public bool CheckDatabaseConnection(out string? exception)
|
||||
{
|
||||
using var connection = GetConnection();
|
||||
using MySqlConnection connection = GetConnection();
|
||||
exception = null;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
return connection.Ping();
|
||||
|
|
@ -66,4 +66,4 @@ public class Database(string dbConnectionString)
|
|||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ public interface IDatabaseProvider
|
|||
Task<DbConnection> CreateConnectionAsync();
|
||||
Task<(bool Success, string? Exception)> CheckConnectionAsync();
|
||||
Task DatabaseMigrationAsync();
|
||||
|
||||
|
||||
// CacheManager
|
||||
string GetBanSelectQuery(bool multiServer);
|
||||
string GetIpHistoryQuery();
|
||||
|
|
@ -15,7 +15,7 @@ public interface IDatabaseProvider
|
|||
|
||||
// PlayerManager
|
||||
string GetUpsertPlayerIpQuery();
|
||||
|
||||
|
||||
// PermissionManager
|
||||
string GetAdminsQuery();
|
||||
string GetDeleteAdminQuery(bool globalDelete);
|
||||
|
|
@ -29,7 +29,7 @@ public interface IDatabaseProvider
|
|||
string GetAddGroupServerQuery();
|
||||
string GetDeleteGroupQuery();
|
||||
string GetDeleteOldAdminsQuery();
|
||||
|
||||
|
||||
// BanManager
|
||||
string GetAddBanQuery();
|
||||
string GetAddBanBySteamIdQuery();
|
||||
|
|
@ -58,7 +58,7 @@ public interface IDatabaseProvider
|
|||
string GetInsertUnmuteQuery(bool includeReason);
|
||||
string GetUpdateMuteStatusQuery();
|
||||
string GetExpireMutesQuery(bool multiServer, int timeMode);
|
||||
|
||||
|
||||
// WarnManager
|
||||
string GetAddWarnQuery(bool includePlayerName);
|
||||
string GetPlayerWarnsQuery(bool multiServer, bool active);
|
||||
|
|
@ -69,4 +69,4 @@ public interface IDatabaseProvider
|
|||
|
||||
// Penalty history (css_history)
|
||||
string GetPenaltyHistoryQuery(bool multiServer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,19 @@ public class Migration(string migrationsPath)
|
|||
/// </summary>
|
||||
public async Task ExecuteMigrationsAsync()
|
||||
{
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
var files = Directory.GetFiles(migrationsPath, "*.sql").OrderBy(f => f).ToList();
|
||||
if (files.Count == 0) return;
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await using (var cmd = connection.CreateCommand())
|
||||
List<string> files = Directory.GetFiles(migrationsPath, "*.sql").OrderBy(f => f).ToList();
|
||||
if (files.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await using (DbCommand cmd = connection.CreateCommand())
|
||||
{
|
||||
if (migrationsPath.Contains("sqlite", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
|
|
@ -42,19 +49,21 @@ public class Migration(string migrationsPath)
|
|||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
var lastAppliedVersion = await GetLastAppliedVersionAsync(connection);
|
||||
string lastAppliedVersion = await GetLastAppliedVersionAsync(connection);
|
||||
|
||||
foreach (var file in files)
|
||||
foreach (string file in files)
|
||||
{
|
||||
var version = Path.GetFileNameWithoutExtension(file);
|
||||
string version = Path.GetFileNameWithoutExtension(file);
|
||||
if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sqlScript = await File.ReadAllTextAsync(file);
|
||||
string sqlScript = await File.ReadAllTextAsync(file);
|
||||
|
||||
await using (var cmdMigration = connection.CreateCommand())
|
||||
await using (DbCommand cmdMigration = connection.CreateCommand())
|
||||
{
|
||||
cmdMigration.CommandText = sqlScript;
|
||||
await cmdMigration.ExecuteNonQueryAsync();
|
||||
|
|
@ -79,9 +88,9 @@ public class Migration(string migrationsPath)
|
|||
/// <returns>The version string of the last applied migration, or empty string if none.</returns>
|
||||
private static async Task<string> GetLastAppliedVersionAsync(DbConnection connection)
|
||||
{
|
||||
await using var cmd = connection.CreateCommand();
|
||||
await using DbCommand cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT version FROM sa_migrations ORDER BY id DESC LIMIT 1;";
|
||||
var result = await cmd.ExecuteScalarAsync();
|
||||
object? result = await cmd.ExecuteScalarAsync();
|
||||
return result?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
|
|
@ -92,10 +101,10 @@ public class Migration(string migrationsPath)
|
|||
/// <param name="version">The version string of the migration applied.</param>
|
||||
private static async Task UpdateLastAppliedVersionAsync(DbConnection connection, string version)
|
||||
{
|
||||
await using var cmd = connection.CreateCommand();
|
||||
await using DbCommand cmd = connection.CreateCommand();
|
||||
cmd.CommandText = "INSERT INTO sa_migrations (version) VALUES (@Version);";
|
||||
|
||||
var param = cmd.CreateParameter();
|
||||
DbParameter param = cmd.CreateParameter();
|
||||
param.ParameterName = "@Version";
|
||||
param.Value = version;
|
||||
cmd.Parameters.Add(param);
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
{
|
||||
public async Task<DbConnection> CreateConnectionAsync()
|
||||
{
|
||||
var connection = new MySqlConnection(connectionString);
|
||||
MySqlConnection connection = new MySqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
await using var cmd = connection.CreateCommand();
|
||||
|
||||
|
||||
await using MySqlCommand cmd = connection.CreateCommand();
|
||||
|
||||
cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
|
||||
|
||||
// cmd.CommandText = "SET time_zone = '+00:00';";
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
{
|
||||
try
|
||||
{
|
||||
await using var conn = await CreateConnectionAsync();
|
||||
await using DbConnection conn = await CreateConnectionAsync();
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -33,52 +33,62 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Task DatabaseMigrationAsync()
|
||||
{
|
||||
var migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Mysql");
|
||||
Migration migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Mysql");
|
||||
return migration.ExecuteMigrationsAsync();
|
||||
}
|
||||
|
||||
public string GetBanSelectQuery(bool multiServer)
|
||||
{
|
||||
return multiServer ? """
|
||||
SELECT
|
||||
id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
""" : """
|
||||
SELECT
|
||||
id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
WHERE server_id = @serverId
|
||||
""";
|
||||
}
|
||||
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
SELECT
|
||||
id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
""";
|
||||
}
|
||||
|
||||
return """
|
||||
SELECT
|
||||
id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
WHERE server_id = @serverId
|
||||
""";
|
||||
}
|
||||
|
||||
public static string GetBanUpdatedSelectQuery(bool multiServer)
|
||||
{
|
||||
return multiServer ? """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
|
||||
""" : """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC
|
||||
""";
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
|
||||
""";
|
||||
}
|
||||
|
||||
return """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetIpHistoryQuery()
|
||||
|
|
@ -99,25 +109,30 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
|
||||
public string GetBanUpdateQuery(bool multiServer)
|
||||
{
|
||||
return multiServer ? """
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
player_name = COALESCE(player_name, @PlayerName)
|
||||
WHERE
|
||||
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
""" : """
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
player_name = COALESCE(player_name, @PlayerName)
|
||||
WHERE
|
||||
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId
|
||||
""";
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
player_name = COALESCE(player_name, @PlayerName)
|
||||
WHERE
|
||||
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
""";
|
||||
}
|
||||
|
||||
return """
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
player_name = COALESCE(player_name, @PlayerName)
|
||||
WHERE
|
||||
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetAdminsQuery()
|
||||
|
|
@ -131,12 +146,17 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
ORDER BY sa_admins.player_steamid
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetDeleteAdminQuery(bool globalDelete) =>
|
||||
globalDelete
|
||||
? "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID"
|
||||
: "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
|
||||
|
||||
|
||||
public string GetDeleteAdminQuery(bool globalDelete)
|
||||
{
|
||||
if (globalDelete)
|
||||
{
|
||||
return "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID";
|
||||
}
|
||||
|
||||
return "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
|
||||
}
|
||||
|
||||
public string GetAddAdminQuery() =>
|
||||
"INSERT INTO sa_admins (player_steamid, player_name, immunity, ends, created, server_id) " +
|
||||
"VALUES (@playerSteamId, @playerName, @immunity, @ends, @created, @serverid); SELECT LAST_INSERT_ID();";
|
||||
|
|
@ -160,7 +180,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
|
||||
public string GetAddGroupQuery() =>
|
||||
"INSERT INTO sa_groups (name, immunity) VALUES (@groupName, @immunity); SELECT LAST_INSERT_ID();";
|
||||
|
||||
|
||||
public string GetGroupIdByNameQuery() =>
|
||||
"""
|
||||
SELECT sgs.group_id
|
||||
|
|
@ -181,7 +201,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
|
||||
public string GetDeleteOldAdminsQuery() =>
|
||||
"DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime;";
|
||||
|
||||
|
||||
public string GetAddBanQuery()
|
||||
{
|
||||
return """
|
||||
|
|
@ -192,7 +212,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
}
|
||||
|
||||
|
||||
public string GetAddBanBySteamIdQuery()
|
||||
{
|
||||
return """
|
||||
|
|
@ -213,12 +233,15 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
(@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
|
||||
""";
|
||||
}
|
||||
|
||||
|
||||
public string GetUnbanRetrieveBansQuery(bool multiServer)
|
||||
{
|
||||
return multiServer
|
||||
? "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
|
||||
: "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
if (multiServer)
|
||||
{
|
||||
return "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
return "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetUnbanAdminIdQuery()
|
||||
|
|
@ -228,9 +251,12 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
|
||||
public string GetInsertUnbanQuery(bool includeReason)
|
||||
{
|
||||
return includeReason
|
||||
? "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT LAST_INSERT_ID();"
|
||||
: "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT LAST_INSERT_ID();";
|
||||
if (includeReason)
|
||||
{
|
||||
return "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT LAST_INSERT_ID();";
|
||||
}
|
||||
|
||||
return "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT LAST_INSERT_ID();";
|
||||
}
|
||||
|
||||
public string GetUpdateBanStatusQuery()
|
||||
|
|
@ -249,145 +275,245 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
|
||||
public string GetExpireBansQuery(bool multiServer)
|
||||
{
|
||||
return multiServer
|
||||
? "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime"
|
||||
: "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime";
|
||||
}
|
||||
|
||||
return "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetExpireIpBansQuery(bool multiServer)
|
||||
{
|
||||
return multiServer
|
||||
? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime"
|
||||
: "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime";
|
||||
}
|
||||
|
||||
return "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetExpireOldPlayerIpsQuery()
|
||||
{
|
||||
return "DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
|
||||
}
|
||||
|
||||
public string GetAddMuteQuery(bool includePlayerName) =>
|
||||
includePlayerName
|
||||
? """
|
||||
|
||||
public string GetAddMuteQuery(bool includePlayerName)
|
||||
{
|
||||
if (includePlayerName)
|
||||
{
|
||||
return """
|
||||
INSERT INTO `sa_mutes`
|
||||
(`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`)
|
||||
VALUES (@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
"""
|
||||
: """
|
||||
INSERT INTO `sa_mutes`
|
||||
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`)
|
||||
VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetIsMutedQuery(bool multiServer, int timeMode) =>
|
||||
multiServer
|
||||
? (timeMode == 1
|
||||
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)"
|
||||
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))")
|
||||
: (timeMode == 1
|
||||
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid"
|
||||
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid");
|
||||
return """
|
||||
INSERT INTO `sa_mutes`
|
||||
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`)
|
||||
VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetMuteStatsQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? """
|
||||
public string GetIsMutedQuery(bool multiServer, int timeMode)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
if (timeMode == 1)
|
||||
{
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))";
|
||||
}
|
||||
}
|
||||
|
||||
if (timeMode == 1)
|
||||
{
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid";
|
||||
}
|
||||
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetMuteStatsQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
SELECT
|
||||
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
|
||||
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
|
||||
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
|
||||
FROM sa_mutes
|
||||
WHERE player_steamid = @PlayerSteamID;
|
||||
"""
|
||||
: """
|
||||
SELECT
|
||||
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
|
||||
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
|
||||
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
|
||||
FROM sa_mutes
|
||||
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetUpdateMutePassedQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'"
|
||||
: "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
return """
|
||||
SELECT
|
||||
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
|
||||
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
|
||||
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
|
||||
FROM sa_mutes
|
||||
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetCheckExpiredMutesQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'"
|
||||
: "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
public string GetUpdateMutePassedQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
public string GetRetrieveMutesQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'"
|
||||
: "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
return "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetCheckExpiredMutesQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
return "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetRetrieveMutesQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
return "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetUnmuteAdminIdQuery() =>
|
||||
"SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
|
||||
|
||||
public string GetInsertUnmuteQuery(bool includeReason) =>
|
||||
includeReason
|
||||
? "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT LAST_INSERT_ID();"
|
||||
: "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT LAST_INSERT_ID();";
|
||||
public string GetInsertUnmuteQuery(bool includeReason)
|
||||
{
|
||||
if (includeReason)
|
||||
{
|
||||
return "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT LAST_INSERT_ID();";
|
||||
}
|
||||
|
||||
return "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT LAST_INSERT_ID();";
|
||||
}
|
||||
|
||||
public string GetUpdateMuteStatusQuery() =>
|
||||
"UPDATE sa_mutes SET status = 'UNMUTED', unmute_id = @unmuteId WHERE id = @muteId";
|
||||
|
||||
public string GetExpireMutesQuery(bool multiServer, int timeMode) =>
|
||||
multiServer
|
||||
? (timeMode == 1
|
||||
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
|
||||
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration`")
|
||||
: (timeMode == 1
|
||||
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid"
|
||||
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration` AND server_id = @serverid");
|
||||
|
||||
|
||||
public string GetAddWarnQuery(bool includePlayerName) =>
|
||||
includePlayerName
|
||||
? """
|
||||
public string GetExpireMutesQuery(bool multiServer, int timeMode)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
if (timeMode == 1)
|
||||
{
|
||||
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration`";
|
||||
}
|
||||
}
|
||||
|
||||
if (timeMode == 1)
|
||||
{
|
||||
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||
}
|
||||
|
||||
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration` AND server_id = @serverid";
|
||||
}
|
||||
|
||||
|
||||
public string GetAddWarnQuery(bool includePlayerName)
|
||||
{
|
||||
if (includePlayerName)
|
||||
{
|
||||
return """
|
||||
INSERT INTO `sa_warns`
|
||||
(`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @playerName, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
"""
|
||||
: """
|
||||
INSERT INTO `sa_warns`
|
||||
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetPlayerWarnsQuery(bool multiServer, bool active) =>
|
||||
multiServer
|
||||
? active
|
||||
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC"
|
||||
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC"
|
||||
: active
|
||||
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC"
|
||||
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
|
||||
return """
|
||||
INSERT INTO `sa_warns`
|
||||
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetPlayerWarnsCountQuery(bool multiServer, bool active) =>
|
||||
multiServer
|
||||
? active
|
||||
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'"
|
||||
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID"
|
||||
: active
|
||||
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'"
|
||||
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid";
|
||||
public string GetPlayerWarnsQuery(bool multiServer, bool active)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC";
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUnwarnByIdQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId"
|
||||
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
|
||||
if (active)
|
||||
{
|
||||
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC";
|
||||
}
|
||||
|
||||
public string GetUnwarnLastQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? """
|
||||
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
|
||||
}
|
||||
|
||||
public string GetPlayerWarnsCountQuery(bool multiServer, bool active)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID";
|
||||
}
|
||||
}
|
||||
|
||||
if (active)
|
||||
{
|
||||
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetUnwarnByIdQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId";
|
||||
}
|
||||
|
||||
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetUnwarnLastQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
UPDATE sa_warns
|
||||
JOIN (
|
||||
SELECT MAX(id) AS max_id
|
||||
|
|
@ -396,45 +522,66 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
) AS subquery ON sa_warns.id = subquery.max_id
|
||||
SET sa_warns.status = 'EXPIRED'
|
||||
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid;
|
||||
"""
|
||||
: """
|
||||
UPDATE sa_warns
|
||||
JOIN (
|
||||
SELECT MAX(id) AS max_id
|
||||
FROM sa_warns
|
||||
WHERE player_steamid = @steamid AND status = 'ACTIVE' AND server_id = @serverid
|
||||
) AS subquery ON sa_warns.id = subquery.max_id
|
||||
SET sa_warns.status = 'EXPIRED'
|
||||
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid AND sa_warns.server_id = @serverid;
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetExpireWarnsQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
|
||||
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||
return """
|
||||
UPDATE sa_warns
|
||||
JOIN (
|
||||
SELECT MAX(id) AS max_id
|
||||
FROM sa_warns
|
||||
WHERE player_steamid = @steamid AND status = 'ACTIVE' AND server_id = @serverid
|
||||
) AS subquery ON sa_warns.id = subquery.max_id
|
||||
SET sa_warns.status = 'EXPIRED'
|
||||
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid AND sa_warns.server_id = @serverid;
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetPenaltyHistoryQuery(bool multiServer) =>
|
||||
$"""
|
||||
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
|
||||
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
|
||||
FROM sa_bans b
|
||||
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
|
||||
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
|
||||
WHERE b.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND b.server_id = @serverid")}
|
||||
UNION ALL
|
||||
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
|
||||
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
|
||||
FROM sa_mutes m
|
||||
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
|
||||
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
|
||||
WHERE m.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND m.server_id = @serverid")}
|
||||
UNION ALL
|
||||
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
|
||||
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
|
||||
FROM sa_warns w
|
||||
WHERE w.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND w.server_id = @serverid")}
|
||||
ORDER BY created DESC
|
||||
""";
|
||||
public string GetExpireWarnsQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime";
|
||||
}
|
||||
|
||||
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetPenaltyHistoryQuery(bool multiServer)
|
||||
{
|
||||
string banServerFilter = string.Empty;
|
||||
string muteServerFilter = string.Empty;
|
||||
string warnServerFilter = string.Empty;
|
||||
|
||||
if (!multiServer)
|
||||
{
|
||||
banServerFilter = "AND b.server_id = @serverid";
|
||||
muteServerFilter = "AND m.server_id = @serverid";
|
||||
warnServerFilter = "AND w.server_id = @serverid";
|
||||
}
|
||||
|
||||
return $"""
|
||||
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
|
||||
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
|
||||
FROM sa_bans b
|
||||
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
|
||||
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
|
||||
WHERE b.player_steamid = @PlayerSteamID {banServerFilter}
|
||||
UNION ALL
|
||||
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
|
||||
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
|
||||
FROM sa_mutes m
|
||||
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
|
||||
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
|
||||
WHERE m.player_steamid = @PlayerSteamID {muteServerFilter}
|
||||
UNION ALL
|
||||
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
|
||||
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
|
||||
FROM sa_warns w
|
||||
WHERE w.player_steamid = @PlayerSteamID {warnServerFilter}
|
||||
ORDER BY created DESC
|
||||
""";
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
|
||||
public async Task<DbConnection> CreateConnectionAsync()
|
||||
{
|
||||
var conn = new SQLiteConnection(_connectionString);
|
||||
SQLiteConnection conn = new SQLiteConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
return conn;
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
{
|
||||
try
|
||||
{
|
||||
await using var conn = await CreateConnectionAsync();
|
||||
await using var cmd = conn.CreateCommand();
|
||||
await using DbConnection conn = await CreateConnectionAsync();
|
||||
await using DbCommand cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT 1";
|
||||
await cmd.ExecuteScalarAsync();
|
||||
return (true, null);
|
||||
|
|
@ -32,33 +32,40 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
|
||||
public Task DatabaseMigrationAsync()
|
||||
{
|
||||
var migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Sqlite");
|
||||
Migration migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Sqlite");
|
||||
return migration.ExecuteMigrationsAsync();
|
||||
}
|
||||
|
||||
public string GetBanSelectQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? """
|
||||
public string GetBanSelectQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
"""
|
||||
: """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
WHERE server_id = @serverId
|
||||
""";
|
||||
}
|
||||
|
||||
public static string GetBanUpdatedSelectQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? """
|
||||
return """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
WHERE server_id = @serverId
|
||||
""";
|
||||
}
|
||||
|
||||
public static string GetBanUpdatedSelectQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
|
|
@ -67,18 +74,21 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
FROM sa_bans
|
||||
WHERE updated_at > @lastUpdate OR created > @lastUpdate
|
||||
ORDER BY updated_at DESC
|
||||
"""
|
||||
: """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
WHERE (updated_at > @lastUpdate OR created > @lastUpdate)
|
||||
AND server_id = @serverId
|
||||
ORDER BY updated_at DESC
|
||||
""";
|
||||
}
|
||||
|
||||
return """
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM sa_bans
|
||||
WHERE (updated_at > @lastUpdate OR created > @lastUpdate)
|
||||
AND server_id = @serverId
|
||||
ORDER BY updated_at DESC
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetIpHistoryQuery() =>
|
||||
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC";
|
||||
|
|
@ -94,25 +104,30 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
""";
|
||||
}
|
||||
|
||||
public string GetBanUpdateQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? """
|
||||
public string GetBanUpdateQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
UPDATE sa_bans
|
||||
SET player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
player_name = COALESCE(player_name, @PlayerName)
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
"""
|
||||
: """
|
||||
UPDATE sa_bans
|
||||
SET player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
player_name = COALESCE(player_name, @PlayerName)
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
AND server_id = @ServerId
|
||||
""";
|
||||
}
|
||||
|
||||
return """
|
||||
UPDATE sa_bans
|
||||
SET player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
player_name = COALESCE(player_name, @PlayerName)
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
AND server_id = @ServerId
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetAddBanQuery() =>
|
||||
"""
|
||||
|
|
@ -141,32 +156,52 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
SELECT last_insert_rowid();
|
||||
""";
|
||||
|
||||
public string GetUnbanRetrieveBansQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
|
||||
: "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
public string GetUnbanRetrieveBansQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
return "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetUnbanAdminIdQuery() =>
|
||||
"SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
|
||||
|
||||
public string GetInsertUnbanQuery(bool includeReason) =>
|
||||
includeReason
|
||||
? "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT last_insert_rowid();"
|
||||
: "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT last_insert_rowid();";
|
||||
public string GetInsertUnbanQuery(bool includeReason)
|
||||
{
|
||||
if (includeReason)
|
||||
{
|
||||
return "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT last_insert_rowid();";
|
||||
}
|
||||
|
||||
return "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT last_insert_rowid();";
|
||||
}
|
||||
|
||||
// SQLite has no ON UPDATE CURRENT_TIMESTAMP, so updated_at must be set explicitly or the cache refresh never sees the change
|
||||
public string GetUpdateBanStatusQuery() =>
|
||||
"UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId, updated_at = CURRENT_TIMESTAMP WHERE id = @banId";
|
||||
|
||||
public string GetExpireBansQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime"
|
||||
: "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
|
||||
public string GetExpireBansQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime";
|
||||
}
|
||||
|
||||
public string GetExpireIpBansQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime"
|
||||
: "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
|
||||
return "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetExpireIpBansQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime";
|
||||
}
|
||||
|
||||
return "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetExpireOldPlayerIpsQuery() =>
|
||||
"DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
|
||||
|
|
@ -190,10 +225,15 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
ORDER BY sa_admins.player_steamid
|
||||
""";
|
||||
|
||||
public string GetDeleteAdminQuery(bool globalDelete) =>
|
||||
globalDelete
|
||||
? "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID"
|
||||
: "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
|
||||
public string GetDeleteAdminQuery(bool globalDelete)
|
||||
{
|
||||
if (globalDelete)
|
||||
{
|
||||
return "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID";
|
||||
}
|
||||
|
||||
return "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
|
||||
}
|
||||
|
||||
public string GetAddAdminQuery() =>
|
||||
"""
|
||||
|
|
@ -244,170 +284,285 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
|
||||
public string GetDeleteOldAdminsQuery() =>
|
||||
"DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime;";
|
||||
|
||||
public string GetAddMuteQuery(bool includePlayerName) =>
|
||||
includePlayerName
|
||||
? """
|
||||
|
||||
public string GetAddMuteQuery(bool includePlayerName)
|
||||
{
|
||||
if (includePlayerName)
|
||||
{
|
||||
return """
|
||||
INSERT INTO sa_mutes
|
||||
(player_steamid, player_name, admin_steamid, admin_name, reason, duration, ends, created, type, server_id)
|
||||
VALUES (@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
|
||||
SELECT last_insert_rowid();
|
||||
"""
|
||||
: """
|
||||
INSERT INTO sa_mutes
|
||||
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, type, server_id)
|
||||
VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
|
||||
SELECT last_insert_rowid();
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetIsMutedQuery(bool multiServer, int timeMode) =>
|
||||
multiServer
|
||||
? (timeMode == 1
|
||||
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)"
|
||||
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))")
|
||||
: (timeMode == 1
|
||||
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid"
|
||||
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid");
|
||||
return """
|
||||
INSERT INTO sa_mutes
|
||||
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, type, server_id)
|
||||
VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
|
||||
SELECT last_insert_rowid();
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetMuteStatsQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? """
|
||||
public string GetIsMutedQuery(bool multiServer, int timeMode)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
if (timeMode == 1)
|
||||
{
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))";
|
||||
}
|
||||
}
|
||||
|
||||
if (timeMode == 1)
|
||||
{
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid";
|
||||
}
|
||||
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetMuteStatsQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
SELECT
|
||||
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
|
||||
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
|
||||
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
|
||||
FROM sa_mutes
|
||||
WHERE player_steamid = @PlayerSteamID;
|
||||
"""
|
||||
: """
|
||||
SELECT
|
||||
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
|
||||
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
|
||||
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
|
||||
FROM sa_mutes
|
||||
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetUpdateMutePassedQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'"
|
||||
: "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
return """
|
||||
SELECT
|
||||
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
|
||||
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
|
||||
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
|
||||
FROM sa_mutes
|
||||
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetCheckExpiredMutesQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'"
|
||||
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
public string GetUpdateMutePassedQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
public string GetRetrieveMutesQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'"
|
||||
: "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
return "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetCheckExpiredMutesQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetRetrieveMutesQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
return "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetUnmuteAdminIdQuery() =>
|
||||
"SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
|
||||
|
||||
public string GetInsertUnmuteQuery(bool includeReason) =>
|
||||
includeReason
|
||||
? "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT last_insert_rowid();"
|
||||
: "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT last_insert_rowid();";
|
||||
public string GetInsertUnmuteQuery(bool includeReason)
|
||||
{
|
||||
if (includeReason)
|
||||
{
|
||||
return "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT last_insert_rowid();";
|
||||
}
|
||||
|
||||
return "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT last_insert_rowid();";
|
||||
}
|
||||
|
||||
public string GetUpdateMuteStatusQuery() =>
|
||||
"UPDATE sa_mutes SET status = 'UNMUTED', unmute_id = @unmuteId WHERE id = @muteId";
|
||||
|
||||
public string GetExpireMutesQuery(bool multiServer, int timeMode) =>
|
||||
multiServer
|
||||
? (timeMode == 1
|
||||
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime"
|
||||
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration")
|
||||
: (timeMode == 1
|
||||
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid"
|
||||
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration AND server_id = @serverid");
|
||||
|
||||
public string GetAddWarnQuery(bool includePlayerName) =>
|
||||
includePlayerName
|
||||
? """
|
||||
public string GetExpireMutesQuery(bool multiServer, int timeMode)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
if (timeMode == 1)
|
||||
{
|
||||
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration";
|
||||
}
|
||||
}
|
||||
|
||||
if (timeMode == 1)
|
||||
{
|
||||
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||
}
|
||||
|
||||
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetAddWarnQuery(bool includePlayerName)
|
||||
{
|
||||
if (includePlayerName)
|
||||
{
|
||||
return """
|
||||
INSERT INTO sa_warns
|
||||
(player_steamid, player_name, admin_steamid, admin_name, reason, duration, ends, created, server_id)
|
||||
VALUES
|
||||
(@playerSteamid, @playerName, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
|
||||
SELECT last_insert_rowid();
|
||||
"""
|
||||
: """
|
||||
INSERT INTO sa_warns
|
||||
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, server_id)
|
||||
VALUES
|
||||
(@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
|
||||
SELECT last_insert_rowid();
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetPlayerWarnsQuery(bool multiServer, bool active) =>
|
||||
multiServer
|
||||
? active
|
||||
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC"
|
||||
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC"
|
||||
: active
|
||||
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC"
|
||||
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
|
||||
return """
|
||||
INSERT INTO sa_warns
|
||||
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, server_id)
|
||||
VALUES
|
||||
(@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
|
||||
SELECT last_insert_rowid();
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetPlayerWarnsCountQuery(bool multiServer, bool active) =>
|
||||
multiServer
|
||||
? active
|
||||
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'"
|
||||
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID"
|
||||
: active
|
||||
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'"
|
||||
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid";
|
||||
public string GetPlayerWarnsQuery(bool multiServer, bool active)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC";
|
||||
}
|
||||
}
|
||||
|
||||
public string GetUnwarnByIdQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId"
|
||||
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
|
||||
if (active)
|
||||
{
|
||||
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC";
|
||||
}
|
||||
|
||||
public string GetUnwarnLastQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? """
|
||||
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
|
||||
}
|
||||
|
||||
public string GetPlayerWarnsCountQuery(bool multiServer, bool active)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID";
|
||||
}
|
||||
}
|
||||
|
||||
if (active)
|
||||
{
|
||||
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'";
|
||||
}
|
||||
|
||||
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetUnwarnByIdQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId";
|
||||
}
|
||||
|
||||
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetUnwarnLastQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return """
|
||||
UPDATE sa_warns
|
||||
SET status = 'EXPIRED'
|
||||
WHERE status = 'ACTIVE'
|
||||
AND player_steamid = @steamid
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
: """
|
||||
UPDATE sa_warns
|
||||
SET status = 'EXPIRED'
|
||||
WHERE status = 'ACTIVE'
|
||||
AND player_steamid = @steamid
|
||||
AND server_id = @serverid
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetExpireWarnsQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime"
|
||||
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||
return """
|
||||
UPDATE sa_warns
|
||||
SET status = 'EXPIRED'
|
||||
WHERE status = 'ACTIVE'
|
||||
AND player_steamid = @steamid
|
||||
AND server_id = @serverid
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
}
|
||||
|
||||
public string GetPenaltyHistoryQuery(bool multiServer) =>
|
||||
$"""
|
||||
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
|
||||
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
|
||||
FROM sa_bans b
|
||||
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
|
||||
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
|
||||
WHERE b.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND b.server_id = @serverid")}
|
||||
UNION ALL
|
||||
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
|
||||
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
|
||||
FROM sa_mutes m
|
||||
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
|
||||
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
|
||||
WHERE m.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND m.server_id = @serverid")}
|
||||
UNION ALL
|
||||
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
|
||||
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
|
||||
FROM sa_warns w
|
||||
WHERE w.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND w.server_id = @serverid")}
|
||||
ORDER BY created DESC
|
||||
""";
|
||||
public string GetExpireWarnsQuery(bool multiServer)
|
||||
{
|
||||
if (multiServer)
|
||||
{
|
||||
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime";
|
||||
}
|
||||
|
||||
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||
}
|
||||
|
||||
public string GetPenaltyHistoryQuery(bool multiServer)
|
||||
{
|
||||
string banServerFilter = string.Empty;
|
||||
string muteServerFilter = string.Empty;
|
||||
string warnServerFilter = string.Empty;
|
||||
|
||||
if (!multiServer)
|
||||
{
|
||||
banServerFilter = "AND b.server_id = @serverid";
|
||||
muteServerFilter = "AND m.server_id = @serverid";
|
||||
warnServerFilter = "AND w.server_id = @serverid";
|
||||
}
|
||||
|
||||
return $"""
|
||||
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
|
||||
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
|
||||
FROM sa_bans b
|
||||
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
|
||||
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
|
||||
WHERE b.player_steamid = @PlayerSteamID {banServerFilter}
|
||||
UNION ALL
|
||||
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
|
||||
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
|
||||
FROM sa_mutes m
|
||||
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
|
||||
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
|
||||
WHERE m.player_steamid = @PlayerSteamID {muteServerFilter}
|
||||
UNION ALL
|
||||
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
|
||||
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
|
||||
FROM sa_warns w
|
||||
WHERE w.player_steamid = @PlayerSteamID {warnServerFilter}
|
||||
ORDER BY created DESC
|
||||
""";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
using System.Numerics;
|
||||
using System.Text;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Core.Translations;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Commands;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CounterStrikeSharp.API.Modules.UserMessages;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using CS2_SimpleAdmin.Managers;
|
||||
using CS2_SimpleAdmin.Models;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
using CounterStrikeSharp.API.Core.Translations;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.UserMessages;
|
||||
|
||||
namespace CS2_SimpleAdmin;
|
||||
|
||||
public partial class CS2_SimpleAdmin
|
||||
{
|
||||
private bool _serverLoading;
|
||||
|
||||
|
||||
private void RegisterEvents()
|
||||
{
|
||||
RegisterListener<Listeners.OnMapStart>(OnMapStart);
|
||||
|
|
@ -27,14 +28,16 @@ public partial class CS2_SimpleAdmin
|
|||
RegisterListener<Listeners.OnClientConnected>(OnClientConnected);
|
||||
RegisterListener<Listeners.OnGameServerSteamAPIActivated>(OnGameServerSteamAPIActivated);
|
||||
if (Config.OtherSettings.UserMessageGagChatType)
|
||||
{
|
||||
HookUserMessage(118, HookUmChat);
|
||||
|
||||
}
|
||||
|
||||
AddCommandListener(null, ComamndListenerHandler);
|
||||
// AddCommandListener("callvote", OnCommandCallVote);
|
||||
// AddCommandListener("say", OnCommandSay);
|
||||
// AddCommandListener("say_team", OnCommandTeamSay);
|
||||
}
|
||||
|
||||
|
||||
private void UnregisterEvents()
|
||||
{
|
||||
RemoveListener<Listeners.OnMapStart>(OnMapStart);
|
||||
|
|
@ -42,15 +45,17 @@ public partial class CS2_SimpleAdmin
|
|||
RemoveListener<Listeners.OnClientConnected>(OnClientConnected);
|
||||
RemoveListener<Listeners.OnGameServerSteamAPIActivated>(OnGameServerSteamAPIActivated);
|
||||
if (Config.OtherSettings.UserMessageGagChatType)
|
||||
{
|
||||
UnhookUserMessage(118, HookUmChat);
|
||||
|
||||
}
|
||||
|
||||
RemoveCommandListener(null!, ComamndListenerHandler, HookMode.Pre);
|
||||
// AddCommandListener("callvote", OnCommandCallVote);
|
||||
// AddCommandListener("say", OnCommandSay);
|
||||
// AddCommandListener("say_team", OnCommandTeamSay);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// private HookResult OnCommandCallVote(CCSPlayerController? caller, CommandInfo info)
|
||||
// {
|
||||
// var voteType = info.GetArg(1).ToLower();
|
||||
|
|
@ -71,8 +76,10 @@ public partial class CS2_SimpleAdmin
|
|||
private void OnGameServerSteamAPIActivated()
|
||||
{
|
||||
if (ServerLoaded || _serverLoading)
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
_serverLoading = true;
|
||||
new ServerManager().LoadServerData();
|
||||
}
|
||||
|
|
@ -81,17 +88,21 @@ public partial class CS2_SimpleAdmin
|
|||
public HookResult OnClientDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
|
||||
{
|
||||
if (@event.Reason is 149 or 6)
|
||||
{
|
||||
info.DontBroadcast = true;
|
||||
}
|
||||
|
||||
var player = @event.Userid;
|
||||
CCSPlayerController? player = @event.Userid;
|
||||
|
||||
#if DEBUG
|
||||
Logger.LogCritical("[OnClientDisconnect] Before");
|
||||
#endif
|
||||
|
||||
if (player == null || !player.IsValid || player.IsHLTV)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
|
||||
}
|
||||
|
||||
CachedPlayers.Remove(player);
|
||||
BotPlayers.Remove(player);
|
||||
SilentPlayers.Remove(player.Slot);
|
||||
|
|
@ -107,10 +118,12 @@ public partial class CS2_SimpleAdmin
|
|||
try
|
||||
{
|
||||
if (DisconnectedPlayers.Count >= Config.OtherSettings.DisconnectedPlayersHistoryCount)
|
||||
{
|
||||
DisconnectedPlayers.RemoveAt(0);
|
||||
}
|
||||
|
||||
var steamId = new SteamID(player.SteamID);
|
||||
var disconnectedPlayer = DisconnectedPlayers.FirstOrDefault(p => p.SteamId == steamId);
|
||||
SteamID steamId = new SteamID(player.SteamID);
|
||||
DisconnectedPlayer? disconnectedPlayer = DisconnectedPlayers.FirstOrDefault(p => p.SteamId == steamId);
|
||||
|
||||
if (disconnectedPlayer != null)
|
||||
{
|
||||
|
|
@ -123,13 +136,15 @@ public partial class CS2_SimpleAdmin
|
|||
DisconnectedPlayers.Add(new DisconnectedPlayer(steamId, player.PlayerName,
|
||||
player.IpAddress?.Split(":")[0], Time.ActualDateTime()));
|
||||
}
|
||||
|
||||
PlayerPenaltyManager.RemoveAllPenalties(player.Slot);
|
||||
|
||||
if (player.UserId.HasValue)
|
||||
PlayersInfo.TryRemove(player.SteamID, out _);
|
||||
|
||||
if (!PermissionManager.AdminCache.TryGetValue(steamId, out var data)
|
||||
PlayerPenaltyManager.RemoveAllPenalties(player.Slot);
|
||||
|
||||
if (player.UserId.HasValue)
|
||||
{
|
||||
PlayersInfo.TryRemove(player.SteamID, out _);
|
||||
}
|
||||
|
||||
if (!PermissionManager.AdminCache.TryGetValue(steamId, out (DateTime? ExpirationTime, List<string> Flags) data)
|
||||
|| !(data.ExpirationTime <= Time.ActualDateTime()))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
|
|
@ -137,10 +152,12 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
AdminManager.RemovePlayerPermissions(steamId, PermissionManager.AdminCache[steamId].Flags.ToArray());
|
||||
AdminManager.RemovePlayerFromGroup(steamId, true, PermissionManager.AdminCache[steamId].Flags.ToArray());
|
||||
var adminData = AdminManager.GetPlayerAdminData(steamId);
|
||||
AdminData? adminData = AdminManager.GetPlayerAdminData(steamId);
|
||||
|
||||
if (adminData == null || data.Flags.ToList().Count != 0 && adminData.Groups.ToList().Count != 0)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
AdminManager.ClearPlayerPermissions(steamId);
|
||||
AdminManager.RemovePlayerAdminData(steamId);
|
||||
|
|
@ -160,64 +177,70 @@ public partial class CS2_SimpleAdmin
|
|||
Logger.LogCritical("[OnClientConnect]");
|
||||
#endif
|
||||
|
||||
var player = Utilities.GetPlayerFromSlot(playerslot);
|
||||
CCSPlayerController? player = Utilities.GetPlayerFromSlot(playerslot);
|
||||
if (player == null || !player.IsValid || player.IsBot)
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
PlayerManager.LoadPlayerData(player);
|
||||
}
|
||||
|
||||
|
||||
private void OnClientConnected(int playerslot)
|
||||
{
|
||||
#if DEBUG
|
||||
Logger.LogCritical("[OnClientConnected]");
|
||||
#endif
|
||||
|
||||
var player = Utilities.GetPlayerFromSlot(playerslot);
|
||||
CCSPlayerController? player = Utilities.GetPlayerFromSlot(playerslot);
|
||||
if (player == null || !player.IsValid || player.IsBot)
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (!CachedPlayers.Contains(player))
|
||||
{
|
||||
CachedPlayers.Add(player);
|
||||
|
||||
}
|
||||
|
||||
PlayerManager.LoadPlayerData(player);
|
||||
}
|
||||
|
||||
// private void OnClientConnect(int playerslot, string name, string ipaddress)
|
||||
// {
|
||||
// #if DEBUG
|
||||
// Logger.LogCritical("[OnClientConnect]");
|
||||
// #endif
|
||||
// if (Config.OtherSettings.BanType == 0)
|
||||
// return;
|
||||
//
|
||||
// if (Instance.CacheManager != null && !Instance.CacheManager.IsPlayerBanned(null, ipaddress.Split(":")[0]))
|
||||
// return;
|
||||
//
|
||||
// var testPlayer = Utilities.GetPlayerFromSlot(playerslot);
|
||||
// if (testPlayer == null)
|
||||
// return;
|
||||
// Logger.LogInformation($"Gracz {testPlayer.PlayerName} ({testPlayer.SteamID.ToString()}) Czas: {DateTime.Now}");
|
||||
//
|
||||
// Server.NextFrame((() =>
|
||||
// {
|
||||
// var player = Utilities.GetPlayerFromSlot(playerslot);
|
||||
// if (player == null || !player.IsValid || player.IsBot)
|
||||
// return;
|
||||
//
|
||||
// Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
|
||||
// }));
|
||||
//
|
||||
// // Server.NextFrame(() =>
|
||||
// // {
|
||||
// // var player = Utilities.GetPlayerFromSlot(playerslot);
|
||||
// //
|
||||
// // if (player == null || !player.IsValid || player.IsBot)
|
||||
// // return;
|
||||
// //
|
||||
// // new PlayerManager().LoadPlayerData(player);
|
||||
// // });
|
||||
// }
|
||||
// private void OnClientConnect(int playerslot, string name, string ipaddress)
|
||||
// {
|
||||
// #if DEBUG
|
||||
// Logger.LogCritical("[OnClientConnect]");
|
||||
// #endif
|
||||
// if (Config.OtherSettings.BanType == 0)
|
||||
// return;
|
||||
//
|
||||
// if (Instance.CacheManager != null && !Instance.CacheManager.IsPlayerBanned(null, ipaddress.Split(":")[0]))
|
||||
// return;
|
||||
//
|
||||
// var testPlayer = Utilities.GetPlayerFromSlot(playerslot);
|
||||
// if (testPlayer == null)
|
||||
// return;
|
||||
// Logger.LogInformation($"Gracz {testPlayer.PlayerName} ({testPlayer.SteamID.ToString()}) Czas: {DateTime.Now}");
|
||||
//
|
||||
// Server.NextFrame((() =>
|
||||
// {
|
||||
// var player = Utilities.GetPlayerFromSlot(playerslot);
|
||||
// if (player == null || !player.IsValid || player.IsBot)
|
||||
// return;
|
||||
//
|
||||
// Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
|
||||
// }));
|
||||
//
|
||||
// // Server.NextFrame(() =>
|
||||
// // {
|
||||
// // var player = Utilities.GetPlayerFromSlot(playerslot);
|
||||
// //
|
||||
// // if (player == null || !player.IsValid || player.IsBot)
|
||||
// // return;
|
||||
// //
|
||||
// // new PlayerManager().LoadPlayerData(player);
|
||||
// // });
|
||||
// }
|
||||
|
||||
[GameEventHandler]
|
||||
public HookResult OnPlayerFullConnect(EventPlayerConnectFull @event, GameEventInfo info)
|
||||
|
|
@ -226,10 +249,12 @@ public partial class CS2_SimpleAdmin
|
|||
Logger.LogCritical("[OnPlayerFullConnect]");
|
||||
#endif
|
||||
|
||||
var player = @event.Userid;
|
||||
CCSPlayerController? player = @event.Userid;
|
||||
|
||||
if (player == null || !player.IsValid)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
if (player is { IsBot: true, IsHLTV: false })
|
||||
{
|
||||
|
|
@ -248,22 +273,26 @@ public partial class CS2_SimpleAdmin
|
|||
Logger.LogCritical("[OnRoundStart]");
|
||||
#endif
|
||||
|
||||
foreach (var player in PlayersInfo.Values)
|
||||
foreach (PlayerInfo player in PlayersInfo.Values)
|
||||
{
|
||||
player.DiePosition = null;
|
||||
}
|
||||
|
||||
AddTimer(0.5f, () =>
|
||||
{
|
||||
foreach (var list in RenamedPlayers)
|
||||
foreach (KeyValuePair<ulong, string> list in RenamedPlayers)
|
||||
{
|
||||
var player = Utilities.GetPlayerFromSteamId(list.Key);
|
||||
CCSPlayerController? player = Utilities.GetPlayerFromSteamId(list.Key);
|
||||
|
||||
if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.Connected)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (player.PlayerName.Equals(list.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
player.Rename(list.Value);
|
||||
}
|
||||
|
|
@ -271,32 +300,41 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
|
||||
private HookResult HookUmChat(UserMessage um)
|
||||
{
|
||||
var author = Utilities.GetPlayerFromIndex(um.ReadInt("entityindex"));
|
||||
CCSPlayerController? author = Utilities.GetPlayerFromIndex(um.ReadInt("entityindex"));
|
||||
if (author == null || !author.IsValid || author.IsBot)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
if (!PlayerPenaltyManager.IsPenalized(author.Slot, PenaltyType.Gag, out DateTime? endDateTime) &&
|
||||
!PlayerPenaltyManager.IsPenalized(author.Slot, PenaltyType.Silence, out endDateTime))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
|
||||
if (_localizer == null || endDateTime == null)
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
var message = um.ReadString("param2");
|
||||
var triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger);
|
||||
if (!triggers.Any(trigger => message.StartsWith(trigger))) return HookResult.Stop;
|
||||
|
||||
for (var i = um.Recipients.Count - 1; i >= 0; i--)
|
||||
if (_localizer == null || endDateTime == null)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
string message = um.ReadString("param2");
|
||||
IEnumerable<string> triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger);
|
||||
if (!triggers.Any(trigger => message.StartsWith(trigger)))
|
||||
{
|
||||
return HookResult.Stop;
|
||||
}
|
||||
|
||||
for (int i = um.Recipients.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (um.Recipients[i] != author)
|
||||
{
|
||||
um.Recipients.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return HookResult.Continue;
|
||||
|
||||
// author.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", author.GetLanguage()));
|
||||
|
|
@ -305,12 +343,16 @@ public partial class CS2_SimpleAdmin
|
|||
private HookResult ComamndListenerHandler(CCSPlayerController? player, CommandInfo info)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.IsBot)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
var command = info.GetArg(0).ToLower();
|
||||
string command = info.GetArg(0).ToLower();
|
||||
|
||||
if (Config.OtherSettings.AdditionalCommandsToLog.Contains(command))
|
||||
{
|
||||
Helper.LogCommand(player, info);
|
||||
}
|
||||
|
||||
switch (command)
|
||||
{
|
||||
|
|
@ -318,59 +360,85 @@ public partial class CS2_SimpleAdmin
|
|||
AddTimer(1.0f, () => ReloadAdmins(null));
|
||||
return HookResult.Continue;
|
||||
case "callvote":
|
||||
{
|
||||
var voteType = info.GetArg(1).ToLower();
|
||||
|
||||
if (voteType != "kick")
|
||||
return HookResult.Continue;
|
||||
{
|
||||
string voteType = info.GetArg(1).ToLower();
|
||||
|
||||
if (voteType != "kick")
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
CCSPlayerController? target;
|
||||
|
||||
if (int.TryParse(info.GetArg(2), out int userId))
|
||||
{
|
||||
target = Utilities.GetPlayerFromUserid(userId);
|
||||
}
|
||||
else
|
||||
{
|
||||
target = null;
|
||||
}
|
||||
|
||||
if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.Connected)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
if (!player.CanTarget(target))
|
||||
{
|
||||
return HookResult.Stop;
|
||||
}
|
||||
|
||||
var target = int.TryParse(info.GetArg(2), out var userId)
|
||||
? Utilities.GetPlayerFromUserid(userId)
|
||||
: null;
|
||||
|
||||
if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.Connected)
|
||||
return HookResult.Continue;
|
||||
|
||||
return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!command.Contains("say"))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
|
||||
}
|
||||
|
||||
if (info.GetArg(1).Length == 0)
|
||||
{
|
||||
return HookResult.Stop;
|
||||
|
||||
var triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger);
|
||||
}
|
||||
|
||||
IEnumerable<string> triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger);
|
||||
if (triggers.Any(trigger => info.GetArg(1).StartsWith(trigger)))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
|
||||
// if (!Config.OtherSettings.UserMessageGagChatType)
|
||||
// {
|
||||
if (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out DateTime? endDateTime) ||
|
||||
PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out endDateTime))
|
||||
if (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out DateTime? endDateTime) ||
|
||||
PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out endDateTime))
|
||||
{
|
||||
if (_localizer != null && endDateTime is not null)
|
||||
{
|
||||
if (_localizer != null && endDateTime is not null)
|
||||
player.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", player.GetLanguage()));
|
||||
return HookResult.Stop;
|
||||
player.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", player.GetLanguage()));
|
||||
}
|
||||
|
||||
return HookResult.Stop;
|
||||
}
|
||||
// }
|
||||
|
||||
|
||||
if (AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat") && command == "say" && info.GetArg(1).StartsWith($"@"))
|
||||
{
|
||||
player.ExecuteClientCommandFromServer($"css_say {info.GetArg(1).Remove(0, 1)}");
|
||||
return HookResult.Stop;
|
||||
}
|
||||
|
||||
if (command != "say_team" || !info.GetArg(1).StartsWith($"@")) return HookResult.Continue;
|
||||
|
||||
if (command != "say_team" || !info.GetArg(1).StartsWith($"@"))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
StringBuilder sb = new();
|
||||
if (AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat"))
|
||||
{
|
||||
sb.Append(_localizer!["sa_adminchat_template_admin", player.PlayerName, info.GetArg(1).Remove(0, 1)]);
|
||||
foreach (var p in Utilities.GetPlayers().Where(p => p.IsValid && p is { IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
foreach (CCSPlayerController? p in Utilities.GetPlayers().Where(p => p.IsValid && p is { IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
{
|
||||
p.PrintToChat(sb.ToString());
|
||||
}
|
||||
|
|
@ -379,7 +447,7 @@ public partial class CS2_SimpleAdmin
|
|||
{
|
||||
sb.Append(_localizer!["sa_adminchat_template_player", player.PlayerName, info.GetArg(1).Remove(0, 1)]);
|
||||
player.PrintToChat(sb.ToString());
|
||||
foreach (var p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
foreach (CCSPlayerController? p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
{
|
||||
p.PrintToChat(sb.ToString());
|
||||
}
|
||||
|
|
@ -409,26 +477,37 @@ public partial class CS2_SimpleAdmin
|
|||
public HookResult OnCommandTeamSay(CCSPlayerController? player, CommandInfo info)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.IsBot)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
if (info.GetArg(1).StartsWith($"/")
|
||||
|| info.GetArg(1).StartsWith($"!"))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
if (info.GetArg(1).Length == 0)
|
||||
{
|
||||
return HookResult.Handled;
|
||||
}
|
||||
|
||||
if (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _) || PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _))
|
||||
{
|
||||
return HookResult.Stop;
|
||||
}
|
||||
|
||||
if (!info.GetArg(1).StartsWith($"@")) return HookResult.Continue;
|
||||
if (!info.GetArg(1).StartsWith($"@"))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
StringBuilder sb = new();
|
||||
|
||||
if (AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat"))
|
||||
{
|
||||
sb.Append(_localizer!["sa_adminchat_template_admin", player.PlayerName, info.GetArg(1).Remove(0, 1)]);
|
||||
foreach (var p in Utilities.GetPlayers().Where(p => p.IsValid && p is { IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
foreach (CCSPlayerController? p in Utilities.GetPlayers().Where(p => p.IsValid && p is { IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
{
|
||||
p.PrintToChat(sb.ToString());
|
||||
}
|
||||
|
|
@ -437,7 +516,7 @@ public partial class CS2_SimpleAdmin
|
|||
{
|
||||
sb.Append(_localizer!["sa_adminchat_template_player", player.PlayerName, info.GetArg(1).Remove(0, 1)]);
|
||||
player.PrintToChat(sb.ToString());
|
||||
foreach (var p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
foreach (CCSPlayerController? p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
|
||||
{
|
||||
p.PrintToChat(sb.ToString());
|
||||
}
|
||||
|
|
@ -449,12 +528,16 @@ public partial class CS2_SimpleAdmin
|
|||
private void OnMapStart(string mapName)
|
||||
{
|
||||
if (Config.OtherSettings.ReloadAdminsEveryMapChange && ServerLoaded && ServerId != null)
|
||||
{
|
||||
ReloadAdmins(null);
|
||||
}
|
||||
|
||||
AddTimer(1.0f, ServerManager.CheckHibernationStatus);
|
||||
|
||||
|
||||
if (!ServerLoaded || ServerId == null)
|
||||
{
|
||||
AddTimer(1.5f, OnGameServerSteamAPIActivated);
|
||||
}
|
||||
|
||||
// AddTimer(34, () =>
|
||||
// {
|
||||
|
|
@ -470,15 +553,17 @@ public partial class CS2_SimpleAdmin
|
|||
[GameEventHandler]
|
||||
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
|
||||
{
|
||||
var player = @event.Userid;
|
||||
|
||||
CCSPlayerController? player = @event.Userid;
|
||||
|
||||
if (player?.UserId == null || !player.IsValid || player.IsHLTV ||
|
||||
player.Connected != PlayerConnectedState.Connected || !PlayersInfo.ContainsKey(player.SteamID) ||
|
||||
@event.Attacker == null)
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
var playerPosition = player.PlayerPawn.Value?.AbsOrigin;
|
||||
var playerRotation = player.PlayerPawn.Value?.AbsRotation;
|
||||
CounterStrikeSharp.API.Modules.Utils.Vector? playerPosition = player.PlayerPawn.Value?.AbsOrigin;
|
||||
QAngle? playerRotation = player.PlayerPawn.Value?.AbsRotation;
|
||||
PlayersInfo[player.SteamID].DiePosition = new DiePosition(
|
||||
new Vector3(
|
||||
playerPosition?.X ?? 0,
|
||||
|
|
@ -498,12 +583,17 @@ public partial class CS2_SimpleAdmin
|
|||
[GameEventHandler(HookMode.Pre)]
|
||||
public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info)
|
||||
{
|
||||
var player = @event.Userid;
|
||||
CCSPlayerController? player = @event.Userid;
|
||||
if (player == null || !player.IsValid || player.IsBot || !SilentPlayers.Contains(player.Slot))
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
if (@event is not { Oldteam: <= 1, Team: >= 1 })
|
||||
{
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
if (@event is not { Oldteam: <= 1, Team: >= 1 }) return HookResult.Continue;
|
||||
|
||||
SilentPlayers.Remove(player.Slot);
|
||||
SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false);
|
||||
|
||||
|
|
@ -531,13 +621,22 @@ public partial class CS2_SimpleAdmin
|
|||
/// </summary>
|
||||
private void EnforceRename(CCSPlayerController? player)
|
||||
{
|
||||
if (player is null || !player.IsValid || player.IsBot) return;
|
||||
if (!RenamedPlayers.TryGetValue(player.SteamID, out var name)) return;
|
||||
if (player is null || !player.IsValid || player.IsBot)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RenamedPlayers.TryGetValue(player.SteamID, out string? name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddTimer(0.5f, () =>
|
||||
{
|
||||
if (player.IsValid && !player.PlayerName.Equals(name))
|
||||
{
|
||||
player.Rename(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ public static class EnumerableExtensions
|
|||
.GroupBy(x => x.Index / chunkSize)
|
||||
.Select(x => x.Select(v => v.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Translations;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CounterStrikeSharp.API.Modules.Memory;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System.Text;
|
||||
using CounterStrikeSharp.API.Modules.UserMessages;
|
||||
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace CS2_SimpleAdmin;
|
||||
|
||||
|
|
@ -45,12 +44,19 @@ public static class PlayerExtensions
|
|||
/// <returns>True if targeting is allowed, false otherwise.</returns>
|
||||
public static bool CanTarget(this CCSPlayerController? controller, CCSPlayerController? target)
|
||||
{
|
||||
if (controller is null || target is null) return true;
|
||||
if (target.IsBot) return true;
|
||||
if (controller is null || target is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target.IsBot)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return AdminManager.CanPlayerTarget(controller, target) ||
|
||||
AdminManager.CanPlayerTarget(new SteamID(controller.SteamID),
|
||||
new SteamID(target.SteamID)) ||
|
||||
new SteamID(target.SteamID)) ||
|
||||
AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(target);
|
||||
}
|
||||
|
||||
|
|
@ -62,9 +68,12 @@ public static class PlayerExtensions
|
|||
/// <returns>True if targeting is permitted, false otherwise.</returns>
|
||||
public static bool CanTarget(this CCSPlayerController? controller, SteamID steamId)
|
||||
{
|
||||
if (controller is null) return true;
|
||||
if (controller is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return AdminManager.CanPlayerTarget(new SteamID(controller.SteamID), steamId) ||
|
||||
return AdminManager.CanPlayerTarget(new SteamID(controller.SteamID), steamId) ||
|
||||
AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(steamId);
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +84,11 @@ public static class PlayerExtensions
|
|||
/// <param name="speed">The speed modifier value.</param>
|
||||
public static void SetSpeed(this CCSPlayerController? controller, float speed)
|
||||
{
|
||||
var playerPawnValue = controller?.PlayerPawn.Value;
|
||||
if (playerPawnValue == null) return;
|
||||
CCSPlayerPawn? playerPawnValue = controller?.PlayerPawn.Value;
|
||||
if (playerPawnValue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playerPawnValue.VelocityModifier = speed;
|
||||
}
|
||||
|
|
@ -88,8 +100,11 @@ public static class PlayerExtensions
|
|||
/// <param name="gravity">The gravity scale.</param>
|
||||
public static void SetGravity(this CCSPlayerController? controller, float gravity)
|
||||
{
|
||||
var playerPawnValue = controller?.PlayerPawn.Value;
|
||||
if (playerPawnValue == null) return;
|
||||
CCSPlayerPawn? playerPawnValue = controller?.PlayerPawn.Value;
|
||||
if (playerPawnValue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playerPawnValue.ActualGravityScale = gravity;
|
||||
}
|
||||
|
|
@ -101,12 +116,18 @@ public static class PlayerExtensions
|
|||
/// <param name="money">The amount of money to set.</param>
|
||||
public static void SetMoney(this CCSPlayerController? controller, int money)
|
||||
{
|
||||
var moneyServices = controller?.InGameMoneyServices;
|
||||
if (moneyServices == null) return;
|
||||
CCSPlayerController_InGameMoneyServices? moneyServices = controller?.InGameMoneyServices;
|
||||
if (moneyServices == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
moneyServices.Account = money;
|
||||
|
||||
if (controller != null) Utilities.SetStateChanged(controller, "CCSPlayerController", "m_pInGameMoneyServices");
|
||||
if (controller != null)
|
||||
{
|
||||
Utilities.SetStateChanged(controller, "CCSPlayerController", "m_pInGameMoneyServices");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -116,8 +137,15 @@ public static class PlayerExtensions
|
|||
/// <param name="health">The health value, default is 100.</param>
|
||||
public static void SetHp(this CCSPlayerController? controller, int health = 100)
|
||||
{
|
||||
if (controller == null) return;
|
||||
if (health <= 0 || controller.PlayerPawn.Value == null || controller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) return;
|
||||
if (controller == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (health <= 0 || controller.PlayerPawn.Value == null || controller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
controller.PlayerPawn.Value.Health = health;
|
||||
|
||||
|
|
@ -136,10 +164,10 @@ public static class PlayerExtensions
|
|||
/// <param name="depth">The depth offset (default 10 units).</param>
|
||||
public static void Bury(this CBasePlayerPawn pawn, float depth = 10f)
|
||||
{
|
||||
var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
|
||||
Vector3 newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
|
||||
pawn.AbsOrigin!.Z - depth);
|
||||
var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
|
||||
var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
|
||||
Vector3 newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
|
||||
Vector3 newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
|
||||
|
||||
pawn.Teleport(newPos, newRotation, newVelocity);
|
||||
}
|
||||
|
|
@ -151,10 +179,10 @@ public static class PlayerExtensions
|
|||
/// <param name="depth">The depth offset (default 15 units).</param>
|
||||
public static void Unbury(this CBasePlayerPawn pawn, float depth = 15f)
|
||||
{
|
||||
var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
|
||||
Vector3 newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
|
||||
pawn.AbsOrigin!.Z + depth);
|
||||
var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
|
||||
var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
|
||||
Vector3 newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
|
||||
Vector3 newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
|
||||
|
||||
pawn.Teleport(newPos, newRotation, newVelocity);
|
||||
}
|
||||
|
|
@ -169,7 +197,7 @@ public static class PlayerExtensions
|
|||
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 11); // invalid
|
||||
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Unfreezes the player pawn, enabling movement.
|
||||
/// </summary>
|
||||
|
|
@ -180,7 +208,7 @@ public static class PlayerExtensions
|
|||
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 2); // walk
|
||||
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Changes the player's color tint to specified RGBA values.
|
||||
/// </summary>
|
||||
|
|
@ -226,7 +254,7 @@ public static class PlayerExtensions
|
|||
|
||||
if (controller != null)
|
||||
{
|
||||
var playerName = new SchemaString<CBasePlayerController>(controller, "m_iszPlayerName");
|
||||
SchemaString<CBasePlayerController> playerName = new SchemaString<CBasePlayerController>(controller, "m_iszPlayerName");
|
||||
playerName.Set(newName + " ");
|
||||
|
||||
CS2_SimpleAdmin.Instance.AddTimer(0.25f, () =>
|
||||
|
|
@ -243,7 +271,10 @@ public static class PlayerExtensions
|
|||
|
||||
CS2_SimpleAdmin.Instance.AddTimer(0.4f, () =>
|
||||
{
|
||||
if (controller != null) Utilities.SetStateChanged(controller, "CBasePlayerController", "m_iszPlayerName");
|
||||
if (controller != null)
|
||||
{
|
||||
Utilities.SetStateChanged(controller, "CBasePlayerController", "m_iszPlayerName");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +286,9 @@ public static class PlayerExtensions
|
|||
public static void TeleportPlayer(this CCSPlayerController? controller, CCSPlayerController? target)
|
||||
{
|
||||
if (controller?.PlayerPawn.Value == null && target?.PlayerPawn.Value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
controller?.PlayerPawn.Value is { AbsOrigin: not null, AbsRotation: not null } &&
|
||||
|
|
@ -278,17 +311,19 @@ public static class PlayerExtensions
|
|||
private static void PerformSlap(CBasePlayerPawn pawn, int damage = 0)
|
||||
{
|
||||
if (pawn.LifeState != (int)LifeState_t.LIFE_ALIVE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var controller = pawn.Controller.Value?.As<CCSPlayerController>();
|
||||
CCSPlayerController? controller = pawn.Controller.Value?.As<CCSPlayerController>();
|
||||
|
||||
/* Teleport in a random direction - thank you, Mani!*/
|
||||
/* Thank you AM & al!*/
|
||||
var random = new Random();
|
||||
var vel = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
|
||||
Random random = new Random();
|
||||
Vector3 vel = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
|
||||
|
||||
vel.X += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1);
|
||||
vel.Y += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1);
|
||||
vel.X += (random.Next(180) + 50) * RandomSign(random);
|
||||
vel.Y += (random.Next(180) + 50) * RandomSign(random);
|
||||
vel.Z += random.Next(200) + 100;
|
||||
|
||||
pawn.AbsVelocity.X = vel.X;
|
||||
|
|
@ -297,7 +332,7 @@ public static class PlayerExtensions
|
|||
|
||||
if (controller != null && controller.IsValid)
|
||||
{
|
||||
var shakeMessage = UserMessage.FromPartialName("Shake");
|
||||
UserMessage shakeMessage = UserMessage.FromPartialName("Shake");
|
||||
shakeMessage.SetFloat("duration", 1);
|
||||
shakeMessage.SetFloat("amplitude", 10);
|
||||
shakeMessage.SetFloat("frequency", 1f);
|
||||
|
|
@ -307,13 +342,17 @@ public static class PlayerExtensions
|
|||
}
|
||||
|
||||
if (damage <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pawn.Health -= damage;
|
||||
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
|
||||
|
||||
if (pawn.Health <= 0)
|
||||
{
|
||||
pawn.CommitSuicide(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -329,16 +368,19 @@ public static class PlayerExtensions
|
|||
public static void SendLocalizedMessage(this CCSPlayerController? controller, IStringLocalizer? localizer,
|
||||
string messageKey, params object[] messageArgs)
|
||||
{
|
||||
if (controller == null || localizer == null) return;
|
||||
if (controller == null || localizer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (new WithTemporaryCulture(controller.GetLanguage()))
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append(localizer[messageKey, messageArgs]);
|
||||
|
||||
foreach (var part in Helper.SeparateLines(sb.ToString()))
|
||||
|
||||
foreach (string part in Helper.SeparateLines(sb.ToString()))
|
||||
{
|
||||
var lineWithPrefix = (CS2_SimpleAdmin._localizer?["sa_prefix"] ?? "") + part.Trim();
|
||||
string lineWithPrefix = (CS2_SimpleAdmin._localizer?["sa_prefix"] ?? "") + part.Trim();
|
||||
controller.PrintToChat(lineWithPrefix);
|
||||
}
|
||||
}
|
||||
|
|
@ -357,20 +399,33 @@ public static class PlayerExtensions
|
|||
public static void SendLocalizedMessageCenter(this CCSPlayerController? controller, IStringLocalizer? localizer,
|
||||
string messageKey, params object[] messageArgs)
|
||||
{
|
||||
if (controller == null || localizer == null) return;
|
||||
if (controller == null || localizer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (new WithTemporaryCulture(controller.GetLanguage()))
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append(localizer[messageKey, messageArgs]);
|
||||
|
||||
foreach (var part in Helper.SeparateLines(sb.ToString()))
|
||||
foreach (string part in Helper.SeparateLines(sb.ToString()))
|
||||
{
|
||||
string _part;
|
||||
_part = Helper.CenterMessage(part);
|
||||
var lineWithPrefix = localizer["sa_prefix"] + _part;
|
||||
string lineWithPrefix = localizer["sa_prefix"] + _part;
|
||||
controller.PrintToChat(lineWithPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int RandomSign(Random random)
|
||||
{
|
||||
if (random.Next(2) == 1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
||||
using System.Data.Common;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MySqlConnector;
|
||||
using System.Text;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
|
|
@ -22,19 +20,29 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>The newly created ban ID if successful, otherwise null.</returns>
|
||||
public async Task<int?> BanPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (databaseProvider == null) return null;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
try
|
||||
{
|
||||
var sql = databaseProvider.GetAddBanQuery();
|
||||
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
string? playerIp = null;
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1)
|
||||
{
|
||||
playerIp = player.IpAddress;
|
||||
}
|
||||
|
||||
string sql = databaseProvider.GetAddBanQuery();
|
||||
int? banId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = player.SteamId.SteamId64,
|
||||
playerName = player.Name,
|
||||
playerIp = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1 ? player.IpAddress : null,
|
||||
playerIp,
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
banReason = reason,
|
||||
|
|
@ -46,7 +54,7 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
return banId;
|
||||
}
|
||||
catch(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
|
||||
return null;
|
||||
|
|
@ -63,16 +71,19 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>The ID of the newly created ban if successful, otherwise null.</returns>
|
||||
public async Task<int?> AddBanBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (databaseProvider == null) return null;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddBanBySteamIdQuery();
|
||||
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetAddBanBySteamIdQuery();
|
||||
int? banId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = playerSteamId,
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
|
|
@ -83,10 +94,10 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
|
|||
created = now,
|
||||
serverid = CS2_SimpleAdmin.ServerId
|
||||
});
|
||||
|
||||
|
||||
return banId;
|
||||
}
|
||||
catch(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
|
||||
return null;
|
||||
|
|
@ -102,18 +113,24 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
|
|||
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
|
||||
public async Task AddBanByIp(string playerIp, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(playerIp)) return;
|
||||
if (string.IsNullOrEmpty(playerIp))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetAddBanByIpQuery();
|
||||
string sql = databaseProvider.GetAddBanByIpQuery();
|
||||
await connection.ExecuteAsync(sql, new
|
||||
{
|
||||
playerIp,
|
||||
|
|
@ -129,179 +146,179 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
|
|||
catch { }
|
||||
}
|
||||
|
||||
// public async Task<bool> IsPlayerBanned(PlayerInfo player)
|
||||
// {
|
||||
// if (database == null) return false;
|
||||
//
|
||||
// if (player.IpAddress == null)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// #if DEBUG
|
||||
// if (CS2_SimpleAdmin._logger != null)
|
||||
// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
|
||||
// #endif
|
||||
//
|
||||
// int banCount;
|
||||
//
|
||||
// DateTime currentTime = Time.ActualDateTime();
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// string sql;
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
|
||||
// {
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
// SELECT COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// ), 0)
|
||||
// +
|
||||
// COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
|
||||
// WHERE sa_bans.status = 'ACTIVE'
|
||||
// AND sa_players_ips.address = @PlayerIP
|
||||
// AND NOT EXISTS (
|
||||
// SELECT 1
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// )
|
||||
// ), 0) AS TotalBanCount;
|
||||
// """ : """
|
||||
// SELECT COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// AND server_id = @ServerId
|
||||
// ), 0)
|
||||
// +
|
||||
// COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
|
||||
// WHERE sa_bans.status = 'ACTIVE'
|
||||
// AND sa_players_ips.address = @PlayerIP
|
||||
// AND NOT EXISTS (
|
||||
// SELECT 1
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// AND server_id = @ServerId
|
||||
// )
|
||||
// ), 0) AS TotalBanCount;
|
||||
// """;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
// UPDATE sa_bans
|
||||
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
|
||||
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime);
|
||||
//
|
||||
// SELECT COUNT(*) FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime);
|
||||
// """ : """
|
||||
// UPDATE sa_bans
|
||||
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
|
||||
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
|
||||
//
|
||||
// SELECT COUNT(*) FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
|
||||
// """;
|
||||
// }
|
||||
//
|
||||
// await using var connection = await database.GetConnectionAsync();
|
||||
//
|
||||
// var parameters = new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
|
||||
// string.IsNullOrEmpty(player.IpAddress) ||
|
||||
// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
|
||||
// ? null
|
||||
// : player.IpAddress,
|
||||
// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
|
||||
// CurrentTime = currentTime,
|
||||
// CS2_SimpleAdmin.ServerId
|
||||
// };
|
||||
//
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
|
||||
// player.Name, ex.Message);
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return banCount > 0;
|
||||
// }
|
||||
//
|
||||
// public async Task<int> GetPlayerBans(PlayerInfo player)
|
||||
// {
|
||||
// if (database == null) return 0;
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// string sql;
|
||||
//
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
|
||||
// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
|
||||
//
|
||||
// int banCount;
|
||||
//
|
||||
// await using var connection = await database.GetConnectionAsync();
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
|
||||
// {
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql,
|
||||
// new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = player.IpAddress,
|
||||
// serverid = CS2_SimpleAdmin.ServerId
|
||||
// });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql,
|
||||
// new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = DBNull.Value,
|
||||
// serverid = CS2_SimpleAdmin.ServerId
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return banCount;
|
||||
// }
|
||||
// catch { }
|
||||
//
|
||||
// return 0;
|
||||
// }
|
||||
// public async Task<bool> IsPlayerBanned(PlayerInfo player)
|
||||
// {
|
||||
// if (database == null) return false;
|
||||
//
|
||||
// if (player.IpAddress == null)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// #if DEBUG
|
||||
// if (CS2_SimpleAdmin._logger != null)
|
||||
// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
|
||||
// #endif
|
||||
//
|
||||
// int banCount;
|
||||
//
|
||||
// DateTime currentTime = Time.ActualDateTime();
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// string sql;
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
|
||||
// {
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
// SELECT COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// ), 0)
|
||||
// +
|
||||
// COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
|
||||
// WHERE sa_bans.status = 'ACTIVE'
|
||||
// AND sa_players_ips.address = @PlayerIP
|
||||
// AND NOT EXISTS (
|
||||
// SELECT 1
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// )
|
||||
// ), 0) AS TotalBanCount;
|
||||
// """ : """
|
||||
// SELECT COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// AND server_id = @ServerId
|
||||
// ), 0)
|
||||
// +
|
||||
// COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
|
||||
// WHERE sa_bans.status = 'ACTIVE'
|
||||
// AND sa_players_ips.address = @PlayerIP
|
||||
// AND NOT EXISTS (
|
||||
// SELECT 1
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// AND server_id = @ServerId
|
||||
// )
|
||||
// ), 0) AS TotalBanCount;
|
||||
// """;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
// UPDATE sa_bans
|
||||
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
|
||||
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime);
|
||||
//
|
||||
// SELECT COUNT(*) FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime);
|
||||
// """ : """
|
||||
// UPDATE sa_bans
|
||||
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
|
||||
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
|
||||
//
|
||||
// SELECT COUNT(*) FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
|
||||
// """;
|
||||
// }
|
||||
//
|
||||
// await using var connection = await database.GetConnectionAsync();
|
||||
//
|
||||
// var parameters = new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
|
||||
// string.IsNullOrEmpty(player.IpAddress) ||
|
||||
// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
|
||||
// ? null
|
||||
// : player.IpAddress,
|
||||
// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
|
||||
// CurrentTime = currentTime,
|
||||
// CS2_SimpleAdmin.ServerId
|
||||
// };
|
||||
//
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
|
||||
// player.Name, ex.Message);
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return banCount > 0;
|
||||
// }
|
||||
//
|
||||
// public async Task<int> GetPlayerBans(PlayerInfo player)
|
||||
// {
|
||||
// if (database == null) return 0;
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// string sql;
|
||||
//
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
|
||||
// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
|
||||
//
|
||||
// int banCount;
|
||||
//
|
||||
// await using var connection = await database.GetConnectionAsync();
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
|
||||
// {
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql,
|
||||
// new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = player.IpAddress,
|
||||
// serverid = CS2_SimpleAdmin.ServerId
|
||||
// });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql,
|
||||
// new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = DBNull.Value,
|
||||
// serverid = CS2_SimpleAdmin.ServerId
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return banCount;
|
||||
// }
|
||||
// catch { }
|
||||
//
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Unbans a player based on a pattern match of SteamID or IP address.
|
||||
|
|
@ -309,46 +326,51 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
|
|||
/// <param name="playerPattern">Pattern to match against player identifiers (e.g., partial SteamID).</param>
|
||||
/// <param name="adminSteamId">SteamID64 of the admin performing the unban.</param>
|
||||
/// <param name="reason">Optional reason for the unban. If null or empty, the unban reason is not stored.</param>
|
||||
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
if (playerPattern is not { Length: > 1 })
|
||||
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
|
||||
var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
|
||||
var bansList = bans as dynamic[] ?? bans.ToArray();
|
||||
if (bansList.Length == 0)
|
||||
return;
|
||||
|
||||
var sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
|
||||
var adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
|
||||
|
||||
foreach (var ban in bansList)
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
int banId = ban.id;
|
||||
|
||||
var sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
|
||||
var unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
|
||||
|
||||
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
|
||||
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply immediately; the periodic cache refresh would otherwise keep rejecting the player for up to a minute
|
||||
CS2_SimpleAdmin.Instance.CacheManager?.SetBanStatus(bansList.Select(b => (int)b.id), Models.BanStatus.UNBANNED);
|
||||
|
||||
// css_ban also issues a native banid when UnlockedCommands is on; clear it so the engine stops
|
||||
// rejecting the player. Done here, after the rows are updated, and for whichever pattern matched them.
|
||||
if (CS2_SimpleAdmin.UnlockedCommands)
|
||||
if (playerPattern is not { Length: > 1 })
|
||||
{
|
||||
var steamIds = bansList
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
|
||||
IEnumerable<dynamic> bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
|
||||
dynamic[] bansList = bans as dynamic[] ?? bans.ToArray();
|
||||
if (bansList.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
|
||||
int adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
|
||||
|
||||
foreach (dynamic ban in bansList)
|
||||
{
|
||||
int banId = ban.id;
|
||||
|
||||
string sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
|
||||
int unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
|
||||
|
||||
string sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
|
||||
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
|
||||
}
|
||||
|
||||
// Apply immediately; the periodic cache refresh would otherwise keep rejecting the player for up to a minute
|
||||
CS2_SimpleAdmin.Instance.CacheManager?.SetBanStatus(bansList.Select(b => (int)b.id), Models.BanStatus.UNBANNED);
|
||||
|
||||
// css_ban also issues a native banid when UnlockedCommands is on; clear it so the engine stops
|
||||
// rejecting the player. Done here, after the rows are updated, and for whichever pattern matched them.
|
||||
if (CS2_SimpleAdmin.UnlockedCommands)
|
||||
{
|
||||
List<ulong> steamIds = bansList
|
||||
.Select(b => (object?)b.player_steamid)
|
||||
.Where(id => id != null)
|
||||
.Select(id => Convert.ToUInt64(id))
|
||||
|
|
@ -356,19 +378,23 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
|
|||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (steamIds.Count > 0)
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
if (steamIds.Count > 0)
|
||||
{
|
||||
foreach (var steamId in steamIds)
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
foreach (ulong steamId in steamIds)
|
||||
{
|
||||
Server.ExecuteCommand($"removeid {new SteamID(steamId).SteamId3}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Unable to unban player: {exception}", ex.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Unable to unban player: {exception}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
|
||||
// {
|
||||
|
|
@ -445,17 +471,21 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
|
|||
/// </summary>
|
||||
public async Task ExpireOldBans()
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
var currentTime = Time.ActualDateTime();
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime currentTime = Time.ActualDateTime();
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { currentTime, serverid = CS2_SimpleAdmin.ServerId });
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans > 0)
|
||||
{
|
||||
var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
|
||||
DateTime ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
|
||||
sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId });
|
||||
|
||||
|
|
@ -468,4 +498,4 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
|
|||
CS2_SimpleAdmin._logger?.LogCritical("Unable to remove expired bans");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
using System.Collections.Concurrent;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
using System.Data.Common;
|
||||
using CS2_SimpleAdmin.Models;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZLinq;
|
||||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
internal class CacheManager: IDisposable
|
||||
internal class CacheManager : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
|
||||
// Rebuilt into fresh instances and swapped, so unlocked readers on the game thread never see a half-built index
|
||||
|
|
@ -17,22 +16,33 @@ internal class CacheManager: IDisposable
|
|||
|
||||
private readonly ConcurrentDictionary<ulong, HashSet<IpRecord>> _playerIpsCache = [];
|
||||
private HashSet<uint> _cachedIgnoredIps = [];
|
||||
|
||||
|
||||
private DateTime _lastUpdateTime = DateTime.MinValue;
|
||||
private DateTime? _lastDatabaseTime = null; // Track actual time from database
|
||||
private bool _isInitialized;
|
||||
private bool _disposed;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes and builds the ban and IP cache from the database. Loads bans, player IP history, and config settings.
|
||||
/// </summary>
|
||||
/// <returns>Asynchronous task representing the initialization process.</returns>
|
||||
public async Task InitializeCacheAsync()
|
||||
{
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
if (!CS2_SimpleAdmin.ServerLoaded) return;
|
||||
if (_isInitialized) return;
|
||||
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CS2_SimpleAdmin.ServerLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Clear();
|
||||
|
|
@ -41,9 +51,9 @@ internal class CacheManager: IDisposable
|
|||
.Select(IpHelper.IpToUint)
|
||||
.ToHashSet();
|
||||
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
List<BanRecord> bans;
|
||||
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
{
|
||||
bans = (await connection.QueryAsync<BanRecord>(
|
||||
|
|
@ -69,21 +79,21 @@ internal class CacheManager: IDisposable
|
|||
status AS Status
|
||||
FROM sa_bans
|
||||
WHERE server_id = @serverId
|
||||
""", new {serverId = CS2_SimpleAdmin.ServerId})).ToList();
|
||||
""", new { serverId = CS2_SimpleAdmin.ServerId })).ToList();
|
||||
}
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
|
||||
{
|
||||
// Optimization: Load IP history and build cache in single pass
|
||||
var ipHistory = await connection.QueryAsync<IpHistoryRow>(
|
||||
IEnumerable<IpHistoryRow> ipHistory = await connection.QueryAsync<IpHistoryRow>(
|
||||
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY steamid, address, used_at DESC");
|
||||
|
||||
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
var currentSteamId = 0UL;
|
||||
var currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
|
||||
var latestIpTimestamps = new Dictionary<uint, DateTime>();
|
||||
string unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
ulong currentSteamId = 0UL;
|
||||
HashSet<IpRecord> currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
|
||||
Dictionary<uint, DateTime> latestIpTimestamps = new Dictionary<uint, DateTime>();
|
||||
|
||||
foreach (var record in ipHistory)
|
||||
foreach (IpHistoryRow record in ipHistory)
|
||||
{
|
||||
// When we encounter a new steamid, save the previous one
|
||||
if ((ulong)record.Steamid != currentSteamId && currentSteamId != 0)
|
||||
|
|
@ -96,14 +106,14 @@ internal class CacheManager: IDisposable
|
|||
currentSteamId = (ulong)record.Steamid;
|
||||
|
||||
// Only keep the latest timestamp for each IP
|
||||
if (!latestIpTimestamps.TryGetValue(record.Address, out var existingTimestamp) ||
|
||||
if (!latestIpTimestamps.TryGetValue(record.Address, out DateTime existingTimestamp) ||
|
||||
record.Used_at > existingTimestamp)
|
||||
{
|
||||
latestIpTimestamps[record.Address] = record.Used_at;
|
||||
currentIpSet.Add(new IpRecord(
|
||||
record.Address,
|
||||
record.Used_at,
|
||||
string.IsNullOrEmpty(record.Name) ? unknownName : record.Name
|
||||
NameOrDefault(record.Name, unknownName)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -115,11 +125,13 @@ internal class CacheManager: IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
foreach (var ban in bans.AsValueEnumerable())
|
||||
foreach (BanRecord? ban in bans.AsValueEnumerable())
|
||||
{
|
||||
_banCache.TryAdd(ban.Id, ban);
|
||||
|
||||
}
|
||||
|
||||
RebuildIndexes();
|
||||
|
||||
|
||||
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
|
@ -128,7 +140,7 @@ internal class CacheManager: IDisposable
|
|||
Console.WriteLine(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached data and reinitializes the cache from the database.
|
||||
/// </summary>
|
||||
|
|
@ -136,15 +148,15 @@ internal class CacheManager: IDisposable
|
|||
public async Task ForceReInitializeCacheAsync()
|
||||
{
|
||||
_isInitialized = false;
|
||||
|
||||
|
||||
_banCache.Clear();
|
||||
_playerIpsCache.Clear();
|
||||
_cachedIgnoredIps = [];
|
||||
_lastUpdateTime = DateTime.MinValue;
|
||||
|
||||
|
||||
await InitializeCacheAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the in-memory cache with updated or new data from the database since the last update time.
|
||||
/// Also updates multi-account IP history if enabled.
|
||||
|
|
@ -152,16 +164,23 @@ internal class CacheManager: IDisposable
|
|||
/// <returns>Asynchronous task representing the refresh operation.</returns>
|
||||
public async Task RefreshCacheAsync()
|
||||
{
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
if (!_isInitialized) return;
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
IEnumerable<BanRecord> updatedBans;
|
||||
|
||||
// Get current time from database in local timezone (CURRENT_TIMESTAMP uses session timezone, not UTC)
|
||||
var currentDatabaseTime = await connection.QueryFirstAsync<DateTime>("SELECT CURRENT_TIMESTAMP");
|
||||
DateTime currentDatabaseTime = await connection.QueryFirstAsync<DateTime>("SELECT CURRENT_TIMESTAMP");
|
||||
|
||||
// Optimization: Only get IDs for comparison if we need to check for deletions
|
||||
// Most of the time bans are just added/updated, not deleted
|
||||
|
|
@ -170,10 +189,10 @@ internal class CacheManager: IDisposable
|
|||
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
{
|
||||
// Use previous database time or start from far past if first run
|
||||
var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
|
||||
DateTime lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
|
||||
|
||||
// Get recently updated bans by timestamp (using database time to avoid timezone issues)
|
||||
var updatedBans_Query = (await connection.QueryAsync<BanRecord>(
|
||||
List<BanRecord> updatedBans_Query = (await connection.QueryAsync<BanRecord>(
|
||||
"""
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
|
|
@ -186,10 +205,10 @@ internal class CacheManager: IDisposable
|
|||
)).ToList();
|
||||
|
||||
// Detect changes: new bans or status changes
|
||||
var updatedList = new List<BanRecord>();
|
||||
foreach (var ban in updatedBans_Query)
|
||||
List<BanRecord> updatedList = new List<BanRecord>();
|
||||
foreach (BanRecord ban in updatedBans_Query)
|
||||
{
|
||||
if (!_banCache.TryGetValue(ban.Id, out var cachedBan))
|
||||
if (!_banCache.TryGetValue(ban.Id, out BanRecord? cachedBan))
|
||||
{
|
||||
// New ban
|
||||
updatedList.Add(ban);
|
||||
|
|
@ -213,10 +232,10 @@ internal class CacheManager: IDisposable
|
|||
else
|
||||
{
|
||||
// Use previous database time or start from far past if first run
|
||||
var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
|
||||
DateTime lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
|
||||
|
||||
// Get recently updated bans for this server by timestamp (using database time to avoid timezone issues)
|
||||
var updatedBans_Query = (await connection.QueryAsync<BanRecord>(
|
||||
List<BanRecord> updatedBans_Query = (await connection.QueryAsync<BanRecord>(
|
||||
"""
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
|
|
@ -229,10 +248,10 @@ internal class CacheManager: IDisposable
|
|||
)).ToList();
|
||||
|
||||
// Detect changes: new bans or status changes
|
||||
var updatedList = new List<BanRecord>();
|
||||
foreach (var ban in updatedBans_Query)
|
||||
List<BanRecord> updatedList = new List<BanRecord>();
|
||||
foreach (BanRecord ban in updatedBans_Query)
|
||||
{
|
||||
if (!_banCache.TryGetValue(ban.Id, out var cachedBan))
|
||||
if (!_banCache.TryGetValue(ban.Id, out BanRecord? cachedBan))
|
||||
{
|
||||
// New ban
|
||||
updatedList.Add(ban);
|
||||
|
|
@ -260,49 +279,56 @@ internal class CacheManager: IDisposable
|
|||
// Optimization: Only process deletions if we have the full ID list
|
||||
if (allIds != null)
|
||||
{
|
||||
foreach (var id in _banCache.Keys)
|
||||
foreach (int id in _banCache.Keys)
|
||||
{
|
||||
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
|
||||
if (allIds.Contains(id) || !_banCache.TryRemove(id, out BanRecord? ban))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ban.PlayerSteamId != null &&
|
||||
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans))
|
||||
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out List<BanRecord>? steamBans))
|
||||
{
|
||||
steamBans.RemoveAll(b => b.Id == id);
|
||||
if (steamBans.Count == 0)
|
||||
{
|
||||
_steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
|
||||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
|
||||
!_ipIndex.TryGetValue(ipUInt, out var ipBans))
|
||||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out uint ipUInt) ||
|
||||
!_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipBans))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ipBans.RemoveAll(b => b.Id == id);
|
||||
if (ipBans.Count == 0)
|
||||
{
|
||||
_ipIndex.TryRemove(ipUInt, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
|
||||
{
|
||||
var ipHistory = (await connection.QueryAsync<IpHistoryRow>(
|
||||
IEnumerable<IpHistoryRow> ipHistory = (await connection.QueryAsync<IpHistoryRow>(
|
||||
"SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300",
|
||||
new { lastUpdate = _lastUpdateTime }));
|
||||
|
||||
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid))
|
||||
foreach (IGrouping<long, IpHistoryRow>? group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid))
|
||||
{
|
||||
var ipSet = new HashSet<IpRecord>(
|
||||
HashSet<IpRecord> ipSet = new HashSet<IpRecord>(
|
||||
group
|
||||
.GroupBy(x => x.Address)
|
||||
.Select(g =>
|
||||
{
|
||||
var latest = g.MaxBy(x => x.Used_at)!; // group is never empty
|
||||
IpHistoryRow latest = g.MaxBy(x => x.Used_at)!; // group is never empty
|
||||
return new IpRecord(
|
||||
g.Key,
|
||||
latest.Used_at,
|
||||
!string.IsNullOrEmpty(latest.Name)
|
||||
? latest.Name
|
||||
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
|
||||
NameOrDefault(latest.Name, CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown")
|
||||
);
|
||||
}),
|
||||
new IpRecordComparer()
|
||||
|
|
@ -313,7 +339,7 @@ internal class CacheManager: IDisposable
|
|||
_ => ipSet,
|
||||
(_, existingSet) =>
|
||||
{
|
||||
foreach (var newEntry in ipSet)
|
||||
foreach (IpRecord newEntry in ipSet)
|
||||
{
|
||||
existingSet.Remove(newEntry);
|
||||
existingSet.Add(newEntry);
|
||||
|
|
@ -325,10 +351,10 @@ internal class CacheManager: IDisposable
|
|||
}
|
||||
|
||||
// Update cache with new/modified bans
|
||||
var needsRebuild = false;
|
||||
foreach (var ban in updatedBans)
|
||||
bool needsRebuild = false;
|
||||
foreach (BanRecord ban in updatedBans)
|
||||
{
|
||||
if (_banCache.TryGetValue(ban.Id, out var oldBan) && oldBan.Status != ban.Status)
|
||||
if (_banCache.TryGetValue(ban.Id, out BanRecord? oldBan) && oldBan.Status != ban.Status)
|
||||
{
|
||||
// Ban status changed (e.g., ACTIVE -> EXPIRED/UNBANNED), need to rebuild indexes
|
||||
needsRebuild = true;
|
||||
|
|
@ -370,36 +396,43 @@ internal class CacheManager: IDisposable
|
|||
/// <param name="status">New status.</param>
|
||||
public void SetBanStatus(IEnumerable<int> banIds, BanStatus status)
|
||||
{
|
||||
var changed = false;
|
||||
foreach (var id in banIds)
|
||||
bool changed = false;
|
||||
foreach (int id in banIds)
|
||||
{
|
||||
if (!_banCache.TryGetValue(id, out var ban) || ban.StatusEnum == status) continue;
|
||||
if (!_banCache.TryGetValue(id, out BanRecord? ban) || ban.StatusEnum == status)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_banCache[id] = ban with { Status = status.ToString() };
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) RebuildIndexes();
|
||||
if (changed)
|
||||
{
|
||||
RebuildIndexes();
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildIndexesCore()
|
||||
{
|
||||
var steamIdIndex = new ConcurrentDictionary<ulong, List<BanRecord>>();
|
||||
var ipIndex = new ConcurrentDictionary<uint, List<BanRecord>>();
|
||||
ConcurrentDictionary<ulong, List<BanRecord>> steamIdIndex = new ConcurrentDictionary<ulong, List<BanRecord>>();
|
||||
ConcurrentDictionary<uint, List<BanRecord>> ipIndex = new ConcurrentDictionary<uint, List<BanRecord>>();
|
||||
|
||||
// Optimization: Cache config value to avoid repeated property access
|
||||
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
|
||||
var checkIpBans = banType != 0;
|
||||
int banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
|
||||
bool checkIpBans = banType != 0;
|
||||
|
||||
// Optimization: Pre-filter only ACTIVE bans to avoid checking status in loop
|
||||
var activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
IEnumerable<BanRecord> activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
|
||||
foreach (var ban in activeBans)
|
||||
foreach (BanRecord? ban in activeBans)
|
||||
{
|
||||
// Index by Steam ID
|
||||
if (ban.PlayerSteamId.HasValue)
|
||||
{
|
||||
var steamId = ban.PlayerSteamId.Value;
|
||||
if (!steamIdIndex.TryGetValue(steamId, out var steamList))
|
||||
ulong steamId = ban.PlayerSteamId.Value;
|
||||
if (!steamIdIndex.TryGetValue(steamId, out List<BanRecord>? steamList))
|
||||
{
|
||||
steamList = new List<BanRecord>();
|
||||
steamIdIndex[steamId] = steamList;
|
||||
|
|
@ -409,9 +442,9 @@ internal class CacheManager: IDisposable
|
|||
|
||||
// Index by IP (only if IP bans are enabled)
|
||||
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
|
||||
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
|
||||
IpHelper.TryConvertIpToUint(ban.PlayerIp, out uint ipUInt))
|
||||
{
|
||||
if (!ipIndex.TryGetValue(ipUInt, out var ipList))
|
||||
if (!ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipList))
|
||||
{
|
||||
ipList = new List<BanRecord>();
|
||||
ipIndex[ipUInt] = ipList;
|
||||
|
|
@ -424,19 +457,19 @@ internal class CacheManager: IDisposable
|
|||
Volatile.Write(ref _steamIdIndex, steamIdIndex);
|
||||
Volatile.Write(ref _ipIndex, ipIndex);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all ban records currently stored in the cache.
|
||||
/// </summary>
|
||||
/// <returns>List of all <see cref="BanRecord"/> objects.</returns>
|
||||
public List<BanRecord> GetAllBans() => _banCache.Values.ToList();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves only active ban records from the cache.
|
||||
/// </summary>
|
||||
/// <returns>List of active <see cref="BanRecord"/> objects.</returns>
|
||||
public List<BanRecord> GetActiveBans() => _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE).ToList();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all ban records for a specific player by their Steam ID.
|
||||
/// </summary>
|
||||
|
|
@ -446,7 +479,7 @@ internal class CacheManager: IDisposable
|
|||
// ponytail: O(n) over all cached bans per call; add an all-status index if this gets hot.
|
||||
public List<BanRecord> GetPlayerBansBySteamId(ulong steamId) =>
|
||||
_banCache.Values.Where(b => b.PlayerSteamId == steamId).ToList();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets all known Steam accounts that have used the specified IP address.
|
||||
/// </summary>
|
||||
|
|
@ -456,16 +489,16 @@ internal class CacheManager: IDisposable
|
|||
/// </returns>
|
||||
public List<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
|
||||
{
|
||||
var ipAsUint = IpHelper.IpToUint(ipAddress);
|
||||
var results = new List<(ulong, DateTime, string)>();
|
||||
uint ipAsUint = IpHelper.IpToUint(ipAddress);
|
||||
List<(ulong, DateTime, string)> results = new List<(ulong, DateTime, string)>();
|
||||
|
||||
// Optimization: Direct lookup using HashSet.Contains instead of TryGetValue
|
||||
var searchRecord = new IpRecord(ipAsUint, default, null!);
|
||||
IpRecord searchRecord = new IpRecord(ipAsUint, default, null!);
|
||||
|
||||
foreach (var (steamId, ipSet) in _playerIpsCache)
|
||||
foreach ((ulong steamId, HashSet<IpRecord>? ipSet) in _playerIpsCache)
|
||||
{
|
||||
// Optimization: Single pass through the set
|
||||
foreach (var entry in ipSet)
|
||||
foreach (IpRecord entry in ipSet)
|
||||
{
|
||||
if (entry.Ip == ipAsUint)
|
||||
{
|
||||
|
|
@ -488,11 +521,15 @@ internal class CacheManager: IDisposable
|
|||
|
||||
private bool IsIpBanned(string ipAddress)
|
||||
{
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
|
||||
var ipUInt = IpHelper.IpToUint(ipAddress);
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint ipUInt = IpHelper.IpToUint(ipAddress);
|
||||
return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt);
|
||||
}
|
||||
|
||||
|
||||
// public bool IsPlayerBanned(ulong? steamId, string? ipAddress)
|
||||
// {
|
||||
// if (steamId != null && _steamIdIndex.ContainsKey(steamId.Value))
|
||||
|
|
@ -518,13 +555,13 @@ internal class CacheManager: IDisposable
|
|||
public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress)
|
||||
{
|
||||
BanRecord? record;
|
||||
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
|
||||
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out List<BanRecord>? steamRecords))
|
||||
{
|
||||
record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
|
||||
if (record != null)
|
||||
{
|
||||
// Double-check the ban is still active in cache (handle race conditions)
|
||||
if (_banCache.TryGetValue(record.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
|
||||
if (_banCache.TryGetValue(record.Id, out BanRecord? cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
|
||||
{
|
||||
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
|
||||
(!record.PlayerSteamId.HasValue))
|
||||
|
|
@ -536,21 +573,31 @@ internal class CacheManager: IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ipAddress) ||
|
||||
!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) ||
|
||||
!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt) ||
|
||||
_cachedIgnoredIps.Contains(ipUInt) ||
|
||||
!_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false;
|
||||
!_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipRecords))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
|
||||
if (record == null) return false;
|
||||
if (record == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Double-check the ban is still active in cache (handle race conditions)
|
||||
if (!_banCache.TryGetValue(record.Id, out var cachedBanIp) || cachedBanIp.StatusEnum != BanStatus.ACTIVE)
|
||||
if (!_banCache.TryGetValue(record.Id, out BanRecord? cachedBanIp) || cachedBanIp.StatusEnum != BanStatus.ACTIVE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
|
||||
(!record.PlayerSteamId.HasValue && steamId.HasValue))
|
||||
|
|
@ -560,7 +607,7 @@ internal class CacheManager: IDisposable
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress)
|
||||
// {
|
||||
// if (_steamIdIndex.ContainsKey(steamId))
|
||||
|
|
@ -638,16 +685,18 @@ internal class CacheManager: IDisposable
|
|||
/// <returns>True if the player or their known IPs are banned, otherwise false.</returns>
|
||||
public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress)
|
||||
{
|
||||
if (_steamIdIndex.TryGetValue(steamId, out var steamBans))
|
||||
if (_steamIdIndex.TryGetValue(steamId, out List<BanRecord>? steamBans))
|
||||
{
|
||||
var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
BanRecord? activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
if (activeBan != null)
|
||||
{
|
||||
// Double-check the ban is still active in cache (handle race conditions)
|
||||
if (_banCache.TryGetValue(activeBan.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
|
||||
if (_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
|
||||
{
|
||||
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
|
||||
{
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -655,25 +704,34 @@ internal class CacheManager: IDisposable
|
|||
}
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt))
|
||||
if (!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_cachedIgnoredIps.Contains(ipUInt))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Direct ip ban (ban record has player_ip set)
|
||||
if (_ipIndex.TryGetValue(ipUInt, out var ipBanRecords))
|
||||
if (_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipBanRecords))
|
||||
{
|
||||
var ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
|
||||
if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out var cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE)
|
||||
BanRecord? ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
|
||||
if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out BanRecord? cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE)
|
||||
{
|
||||
var expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans;
|
||||
int expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans;
|
||||
if (expireOldIpBans <= 0 || ipBan.Created >= Time.ActualDateTime().AddDays(-expireOldIpBans))
|
||||
{
|
||||
if (string.IsNullOrEmpty(ipBan.PlayerName))
|
||||
{
|
||||
ipBan.PlayerName = playerName;
|
||||
}
|
||||
|
||||
ipBan.PlayerSteamId ??= steamId;
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
return true;
|
||||
|
|
@ -684,19 +742,32 @@ internal class CacheManager: IDisposable
|
|||
// Multiaccount ban - check if other accounts using current ip are banned
|
||||
if (!_playerIpsCache.IsEmpty)
|
||||
{
|
||||
foreach (var (otherSteamId, ipSet) in _playerIpsCache)
|
||||
foreach ((ulong otherSteamId, HashSet<IpRecord>? ipSet) in _playerIpsCache)
|
||||
{
|
||||
// Skip current player
|
||||
if (otherSteamId == steamId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this ip is in the other accounts ip history
|
||||
if (ipSet.All(record => record.Ip != ipUInt)) continue;
|
||||
if (ipSet.All(record => record.Ip != ipUInt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Found another account using this ip - check if its banned
|
||||
if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans)) continue;
|
||||
var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
|
||||
cachedBan.StatusEnum != BanStatus.ACTIVE) continue;
|
||||
if (!_steamIdIndex.TryGetValue(otherSteamId, out List<BanRecord>? otherSteamBans))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BanRecord? activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) ||
|
||||
cachedBan.StatusEnum != BanStatus.ACTIVE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
return true;
|
||||
}
|
||||
|
|
@ -705,32 +776,44 @@ internal class CacheManager: IDisposable
|
|||
// Multiaccount ban - check if this player used any ip where other banned accounts are connected
|
||||
// Search sa_players_ips for all accounts sharing the same ips as current player
|
||||
if (!CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_playerIpsCache.TryGetValue(steamId, out var playerIps))
|
||||
if (!_playerIpsCache.TryGetValue(steamId, out HashSet<IpRecord>? playerIps))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// For each ip the player used (current or historical)
|
||||
foreach (var playerIpRecord in playerIps)
|
||||
foreach (IpRecord playerIpRecord in playerIps)
|
||||
{
|
||||
// Search sa_players_ips for other accounts using this same ip (as uint)
|
||||
foreach (var (otherSteamId, otherIpSet) in _playerIpsCache)
|
||||
foreach ((ulong otherSteamId, HashSet<IpRecord>? otherIpSet) in _playerIpsCache)
|
||||
{
|
||||
if (otherSteamId == steamId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this other account used the player ip
|
||||
if (otherIpSet.All(record => record.Ip != playerIpRecord.Ip))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this other account is banned
|
||||
if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans))
|
||||
if (!_steamIdIndex.TryGetValue(otherSteamId, out List<BanRecord>? otherSteamBans))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
|
||||
BanRecord? activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) ||
|
||||
cachedBan.StatusEnum != BanStatus.ACTIVE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
return true;
|
||||
|
|
@ -749,15 +832,19 @@ internal class CacheManager: IDisposable
|
|||
public bool HasIpForPlayer(ulong steamId, string ipAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
{
|
||||
return false;
|
||||
|
||||
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUint))
|
||||
return false;
|
||||
}
|
||||
|
||||
return _playerIpsCache.TryGetValue(steamId, out var ipData) &&
|
||||
if (!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _playerIpsCache.TryGetValue(steamId, out HashSet<IpRecord>? ipData) &&
|
||||
ipData.Contains(new IpRecord(ipUint, default, null!));
|
||||
}
|
||||
|
||||
|
||||
// public bool HasIpForPlayer(ulong steamId, string ipAddress)
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
|
|
@ -778,9 +865,11 @@ internal class CacheManager: IDisposable
|
|||
private async Task UpdatePlayerData(string? playerName, ulong? steamId, string? ipAddress)
|
||||
{
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var baseSql = """
|
||||
string baseSql = """
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
|
|
@ -796,44 +885,56 @@ internal class CacheManager: IDisposable
|
|||
baseSql += " AND server_id = @ServerId;";
|
||||
}
|
||||
|
||||
string? playerIp = ipAddress;
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|
||||
|| string.IsNullOrEmpty(ipAddress)
|
||||
|| CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress))
|
||||
{
|
||||
playerIp = null;
|
||||
}
|
||||
|
||||
var parameters = new
|
||||
{
|
||||
PlayerSteamID = steamId,
|
||||
PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|
||||
|| string.IsNullOrEmpty(ipAddress)
|
||||
|| CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress)
|
||||
? null
|
||||
: ipAddress,
|
||||
PlayerName = string.IsNullOrEmpty(playerName) ? string.Empty : playerName,
|
||||
PlayerIP = playerIp,
|
||||
PlayerName = playerName ?? string.Empty,
|
||||
CurrentTime = Time.ActualDateTime(),
|
||||
CS2_SimpleAdmin.ServerId
|
||||
};
|
||||
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await connection.ExecuteAsync(baseSql, parameters);
|
||||
|
||||
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
|
||||
|
||||
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out List<BanRecord>? steamRecords))
|
||||
{
|
||||
foreach (var rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
|
||||
foreach (BanRecord? rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
|
||||
{
|
||||
if (string.IsNullOrEmpty(rec.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
|
||||
{
|
||||
rec.PlayerIp = ipAddress;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
|
||||
{
|
||||
rec.PlayerName = playerName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)
|
||||
&& _ipIndex.TryGetValue(ipUInt, out var ipRecords))
|
||||
if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt)
|
||||
&& _ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipRecords))
|
||||
{
|
||||
foreach (var rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
|
||||
foreach (BanRecord? rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
|
||||
{
|
||||
if (!rec.PlayerSteamId.HasValue && steamId.HasValue)
|
||||
{
|
||||
rec.PlayerSteamId = steamId;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
|
||||
{
|
||||
rec.PlayerName = playerName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -847,16 +948,30 @@ internal class CacheManager: IDisposable
|
|||
_playerIpsCache.Clear();
|
||||
_cachedIgnoredIps.Clear();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Clears and disposes of all cached data and marks the object as disposed.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private static string NameOrDefault(string? name, string defaultName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return defaultName;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public class IpRecordComparer : IEqualityComparer<IpRecord>
|
||||
|
|
@ -866,4 +981,4 @@ public class IpRecordComparer : IEqualityComparer<IpRecord>
|
|||
|
||||
public int GetHashCode(IpRecord obj)
|
||||
=> obj.Ip.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace CS2_SimpleAdmin.Managers;
|
|||
|
||||
public class DiscordManager(string webhookUrl)
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sends a plain text message asynchronously to the configured Discord webhook URL.
|
||||
/// </summary>
|
||||
|
|
@ -16,23 +16,23 @@ public class DiscordManager(string webhookUrl)
|
|||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
public async Task SendMessageAsync(string message)
|
||||
{
|
||||
var client = CS2_SimpleAdmin.HttpClient;
|
||||
var payload = new
|
||||
HttpClient client = CS2_SimpleAdmin.HttpClient;
|
||||
Dictionary<string, object> payload = new Dictionary<string, object>
|
||||
{
|
||||
content = message
|
||||
["content"] = message
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
JsonSerializerOptions options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, options);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
string json = JsonSerializer.Serialize(payload, options);
|
||||
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.PostAsync(webhookUrl, content);
|
||||
HttpResponseMessage response = await client.PostAsync(webhookUrl, content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
|
|
@ -54,48 +54,75 @@ public class DiscordManager(string webhookUrl)
|
|||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
public async Task SendEmbedAsync(Embed embed)
|
||||
{
|
||||
var httpClient = CS2_SimpleAdmin.HttpClient;
|
||||
HttpClient httpClient = CS2_SimpleAdmin.HttpClient;
|
||||
|
||||
var payload = new
|
||||
// Same JSON as before: every key is written, null when the part is empty
|
||||
Dictionary<string, object?> embedPayload = new Dictionary<string, object?>
|
||||
{
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
color = embed.Color,
|
||||
title = !string.IsNullOrEmpty(embed.Title) ? embed.Title : null,
|
||||
description = !string.IsNullOrEmpty(embed.Description) ? embed.Description : null,
|
||||
thumbnail = !string.IsNullOrEmpty(embed.ThumbnailUrl) ? new { url = embed.ThumbnailUrl } : null,
|
||||
image = !string.IsNullOrEmpty(embed.ImageUrl) ? new { url = embed.ImageUrl } : null,
|
||||
footer = !string.IsNullOrEmpty(embed.Footer?.Text) ? new { text = embed.Footer.Text, icon_url = embed.Footer.IconUrl } : null,
|
||||
timestamp = embed.Timestamp,
|
||||
fields = embed.Fields.Count > 0 ? embed.Fields.Select(field => new
|
||||
{
|
||||
name = field.Name,
|
||||
value = field.Value,
|
||||
inline = field.Inline
|
||||
}).ToArray() : null
|
||||
}
|
||||
}
|
||||
["color"] = embed.Color,
|
||||
["title"] = null,
|
||||
["description"] = null,
|
||||
["thumbnail"] = null,
|
||||
["image"] = null,
|
||||
["footer"] = null,
|
||||
["timestamp"] = embed.Timestamp,
|
||||
["fields"] = null
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
if (!string.IsNullOrEmpty(embed.Title))
|
||||
{
|
||||
embedPayload["title"] = embed.Title;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(embed.Description))
|
||||
{
|
||||
embedPayload["description"] = embed.Description;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(embed.ThumbnailUrl))
|
||||
{
|
||||
embedPayload["thumbnail"] = new Dictionary<string, object?> { ["url"] = embed.ThumbnailUrl };
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(embed.ImageUrl))
|
||||
{
|
||||
embedPayload["image"] = new Dictionary<string, object?> { ["url"] = embed.ImageUrl };
|
||||
}
|
||||
|
||||
if (embed.Footer != null && !string.IsNullOrEmpty(embed.Footer.Text))
|
||||
{
|
||||
embedPayload["footer"] = new Dictionary<string, object?> { ["text"] = embed.Footer.Text, ["icon_url"] = embed.Footer.IconUrl };
|
||||
}
|
||||
|
||||
if (embed.Fields.Count > 0)
|
||||
{
|
||||
embedPayload["fields"] = embed.Fields
|
||||
.Select(field => new Dictionary<string, object?> { ["name"] = field.Name, ["value"] = field.Value, ["inline"] = field.Inline })
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
Dictionary<string, object> payload = new Dictionary<string, object>
|
||||
{
|
||||
["embeds"] = new[] { embedPayload }
|
||||
};
|
||||
|
||||
JsonSerializerOptions options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(payload, options);
|
||||
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
string jsonPayload = JsonSerializer.Serialize(payload, options);
|
||||
StringContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await httpClient.PostAsync(webhookUrl, content);
|
||||
HttpResponseMessage response = await httpClient.PostAsync(webhookUrl, content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorMessage = await response.Content.ReadAsStringAsync();
|
||||
string errorMessage = await response.Content.ReadAsStringAsync();
|
||||
CS2_SimpleAdmin._logger?.LogError($"Failed to send embed: {response.StatusCode} - {errorMessage}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts a hexadecimal color string (e.g. "#FF0000") to its integer representation.
|
||||
/// </summary>
|
||||
|
|
@ -135,13 +162,13 @@ public class Embed
|
|||
/// <param name="inline">Whether the field should be displayed inline with other fields.</param>
|
||||
public void AddField(string name, string value, bool inline)
|
||||
{
|
||||
var field = new EmbedField
|
||||
EmbedField field = new EmbedField
|
||||
{
|
||||
Name = name,
|
||||
Value = value,
|
||||
Inline = inline
|
||||
};
|
||||
|
||||
|
||||
Fields.Add(field);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using CS2_SimpleAdmin.Database;
|
||||
using System.Data.Common;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -18,12 +19,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>Mute ID if successfully added, otherwise null.</returns>
|
||||
public async Task<int?> MutePlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
|
||||
{
|
||||
if (databaseProvider == null) return null;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
var futureTime = now.AddMinutes(time);
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
var muteType = type switch
|
||||
string muteType = type switch
|
||||
{
|
||||
1 => "MUTE",
|
||||
2 => "SILENCE",
|
||||
|
|
@ -32,10 +36,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddMuteQuery(true);
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetAddMuteQuery(true);
|
||||
|
||||
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
int? muteId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = player.SteamId.SteamId64,
|
||||
playerName = player.Name,
|
||||
|
|
@ -69,12 +73,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>Mute ID if successful, otherwise null.</returns>
|
||||
public async Task<int?> AddMuteBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
|
||||
{
|
||||
if (databaseProvider == null) return null;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
var futureTime = now.AddMinutes(time);
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
var muteType = type switch
|
||||
string muteType = type switch
|
||||
{
|
||||
1 => "MUTE",
|
||||
2 => "SILENCE",
|
||||
|
|
@ -83,10 +90,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddMuteQuery(false);
|
||||
|
||||
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetAddMuteQuery(false);
|
||||
|
||||
int? muteId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = playerSteamId,
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
|
|
@ -114,7 +121,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>List of active mute records; empty list if none or on error.</returns>
|
||||
public async Task<List<dynamic>> IsPlayerMuted(string steamId)
|
||||
{
|
||||
if (databaseProvider == null) return [];
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(steamId))
|
||||
{
|
||||
|
|
@ -123,18 +133,20 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
#if DEBUG
|
||||
if (CS2_SimpleAdmin._logger != null)
|
||||
{
|
||||
CS2_SimpleAdmin._logger.LogCritical($"IsPlayerMuted for {steamId}");
|
||||
}
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var currentTime = Time.ActualDateTime();
|
||||
|
||||
var sql = databaseProvider.GetIsMutedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
|
||||
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
DateTime currentTime = Time.ActualDateTime();
|
||||
|
||||
string sql = databaseProvider.GetIsMutedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
|
||||
|
||||
var parameters = new { PlayerSteamID = steamId, CurrentTime = currentTime, serverid = CS2_SimpleAdmin.ServerId };
|
||||
var activeMutes = (await connection.QueryAsync(sql, parameters)).ToList();
|
||||
List<dynamic> activeMutes = (await connection.QueryAsync(sql, parameters)).ToList();
|
||||
return activeMutes;
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
@ -153,14 +165,17 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
/// </returns>
|
||||
public async Task<(int TotalMutes, int TotalGags, int TotalSilences)> GetPlayerMutes(PlayerInfo playerInfo)
|
||||
{
|
||||
if (databaseProvider == null) return (0,0,0);
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return (0, 0, 0);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetMuteStatsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
var result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new
|
||||
string sql = databaseProvider.GetMuteStatsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
(int TotalMutes, int TotalGags, int TotalSilences) result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new
|
||||
{
|
||||
PlayerSteamID = playerInfo.SteamId.SteamId64,
|
||||
CS2_SimpleAdmin.ServerId
|
||||
|
|
@ -173,7 +188,7 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
return (0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Processes a batch of online players to update their mute status and remove expired penalties.
|
||||
/// </summary>
|
||||
|
|
@ -181,21 +196,24 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>Task representing the asynchronous operation.</returns>
|
||||
public async Task CheckOnlineModeMutes(List<(ulong SteamID, int? UserId, int Slot)> players)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const int batchSize = 20;
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
string sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
|
||||
for (var i = 0; i < players.Count; i += batchSize)
|
||||
for (int i = 0; i < players.Count; i += batchSize)
|
||||
{
|
||||
var batch = players.Skip(i).Take(batchSize);
|
||||
var parametersList = new List<object>();
|
||||
IEnumerable<(ulong SteamID, int? UserId, int Slot)> batch = players.Skip(i).Take(batchSize);
|
||||
List<object> parametersList = new List<object>();
|
||||
|
||||
foreach (var (steamId, _, _) in batch)
|
||||
foreach ((ulong steamId, int? _, int _) in batch)
|
||||
{
|
||||
parametersList.Add(new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
|
|
@ -205,11 +223,11 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
sql = databaseProvider.GetCheckExpiredMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
|
||||
foreach (var (steamId, _, slot) in players)
|
||||
foreach ((ulong steamId, int? _, int slot) in players)
|
||||
{
|
||||
var muteRecords = await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
IEnumerable<dynamic> muteRecords = await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
|
||||
foreach (var muteRecord in muteRecords)
|
||||
foreach (dynamic muteRecord in muteRecords)
|
||||
{
|
||||
DateTime endDateTime = muteRecord.ends;
|
||||
PlayerPenaltyManager.RemovePenaltiesByDateTime(slot, endDateTime);
|
||||
|
|
@ -230,7 +248,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>Task representing the asynchronous operation.</returns>
|
||||
public async Task UnmutePlayer(string playerPattern, string adminSteamId, string reason, int type = 0)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerPattern.Length <= 1)
|
||||
{
|
||||
|
|
@ -239,36 +260,38 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var muteType = type switch
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string muteType = type switch
|
||||
{
|
||||
1 => "MUTE",
|
||||
2 => "SILENCE",
|
||||
_ => "GAG"
|
||||
};
|
||||
|
||||
var sqlRetrieveMutes =
|
||||
string sqlRetrieveMutes =
|
||||
databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
var mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId });
|
||||
IEnumerable<dynamic> mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId });
|
||||
|
||||
var mutesList = mutes as dynamic[] ?? mutes.ToArray();
|
||||
dynamic[] mutesList = mutes as dynamic[] ?? mutes.ToArray();
|
||||
if (mutesList.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery();
|
||||
var sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason));
|
||||
string sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery();
|
||||
string sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason));
|
||||
|
||||
var sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
|
||||
var adminId = sqlAdminId ?? 0;
|
||||
int? sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
|
||||
int adminId = sqlAdminId ?? 0;
|
||||
|
||||
foreach (var mute in mutesList)
|
||||
foreach (dynamic mute in mutesList)
|
||||
{
|
||||
int muteId = mute.id;
|
||||
|
||||
int? unmuteId =
|
||||
await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId, reason });
|
||||
|
||||
var sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery();
|
||||
string sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery();
|
||||
await connection.ExecuteAsync(sqlUpdateMute, new { unmuteId, muteId });
|
||||
}
|
||||
}
|
||||
|
|
@ -284,12 +307,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>Task representing the asynchronous expiration operation.</returns>
|
||||
public async Task ExpireOldMutes()
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetExpireMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetExpireMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
|
||||
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
@ -297,4 +323,4 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
CS2_SimpleAdmin._logger?.LogCritical("Unable to remove expired mutes");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
|
|
@ -18,58 +19,60 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// Retrieves all players' flags and associated data asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>A list of tuples containing player SteamID, name, flags, immunity, and expiration time.</returns>
|
||||
private async Task<List<(ulong, string ,List<string>, int, DateTime?)>> GetAllPlayersFlags()
|
||||
private async Task<List<(ulong, string, List<string>, int, DateTime?)>> GetAllPlayersFlags()
|
||||
{
|
||||
if (databaseProvider == null)
|
||||
return new List<(ulong, string, List<string>, int, DateTime?)>();
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return new List<(ulong, string, List<string>, int, DateTime?)>();
|
||||
}
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
DateTime now = Time.ActualDateTime();
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAdminsQuery();
|
||||
var admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList();
|
||||
try
|
||||
{
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetAdminsQuery();
|
||||
List<dynamic> admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList();
|
||||
|
||||
var groupedPlayers = admins
|
||||
.GroupBy(r => new { playerSteamId = r.player_steamid, playerName = r.player_name, r.immunity, r.ends })
|
||||
.Select(g =>
|
||||
{
|
||||
ulong steamId = g.Key.playerSteamId switch
|
||||
{
|
||||
long l => (ulong)l,
|
||||
int i => (ulong)i,
|
||||
string s when ulong.TryParse(s, out var parsed) => parsed,
|
||||
_ => 0UL
|
||||
};
|
||||
List<(ulong steamId, string playerName, List<string> flags, int immunity, DateTime? ends)> groupedPlayers = admins
|
||||
.GroupBy(r => new { playerSteamId = r.player_steamid, playerName = r.player_name, r.immunity, r.ends })
|
||||
.Select(g =>
|
||||
{
|
||||
ulong steamId = g.Key.playerSteamId switch
|
||||
{
|
||||
long l => (ulong)l,
|
||||
int i => (ulong)i,
|
||||
string s when ulong.TryParse(s, out ulong parsed) => parsed,
|
||||
_ => 0UL
|
||||
};
|
||||
|
||||
int immunity = g.Key.immunity switch
|
||||
{
|
||||
int i => i,
|
||||
string s when int.TryParse(s, out var parsed) => parsed,
|
||||
_ => 0
|
||||
};
|
||||
int immunity = g.Key.immunity switch
|
||||
{
|
||||
int i => i,
|
||||
string s when int.TryParse(s, out int parsed) => parsed,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
DateTime? ends = g.Key.ends as DateTime?;
|
||||
DateTime? ends = g.Key.ends as DateTime?;
|
||||
|
||||
string playerName = g.Key.playerName as string ?? string.Empty;
|
||||
string playerName = g.Key.playerName as string ?? string.Empty;
|
||||
|
||||
// Dapper returns string here, not dynamic
|
||||
var flags = g.Select(r => r.flag as string ?? string.Empty)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
// Dapper returns string here, not dynamic
|
||||
List<string> flags = g.Select(r => r.flag as string ?? string.Empty)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return (steamId, playerName, flags, immunity, ends);
|
||||
})
|
||||
.ToList();
|
||||
return (steamId, playerName, flags, immunity, ends);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return groupedPlayers;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
|
||||
throw; // an empty list would strip every admin; abort the reload and keep the current permissions
|
||||
}
|
||||
return groupedPlayers;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
|
||||
throw; // an empty list would strip every admin; abort the reload and keep the current permissions
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -78,24 +81,27 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>A dictionary with group names as keys and tuples of flags and immunity as values.</returns>
|
||||
private async Task<Dictionary<string, (List<string>, int)>> GetAllGroupsData()
|
||||
{
|
||||
if (databaseProvider == null) return [];
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetGroupsQuery();
|
||||
var groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetGroupsQuery();
|
||||
List<dynamic> groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
|
||||
if (groupData.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var groupInfoDictionary = new Dictionary<string, (List<string>, int)>();
|
||||
foreach (var row in groupData)
|
||||
Dictionary<string, (List<string>, int)> groupInfoDictionary = new Dictionary<string, (List<string>, int)>();
|
||||
foreach (dynamic row in groupData)
|
||||
{
|
||||
var groupName = (string)row.group_name;
|
||||
var flag = (string)row.flag;
|
||||
var immunity = (int)row.immunity;
|
||||
string groupName = (string)row.group_name;
|
||||
string flag = (string)row.flag;
|
||||
int immunity = (int)row.immunity;
|
||||
|
||||
if (!groupInfoDictionary.TryGetValue(groupName, out (List<string>, int) value))
|
||||
{
|
||||
|
|
@ -121,12 +127,12 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
public async Task CreateGroupsJsonFile()
|
||||
{
|
||||
var groupsData = await GetAllGroupsData();
|
||||
var jsonData = new Dictionary<string, object>();
|
||||
Dictionary<string, (List<string>, int)> groupsData = await GetAllGroupsData();
|
||||
Dictionary<string, object> jsonData = new Dictionary<string, object>();
|
||||
|
||||
foreach (var kvp in groupsData)
|
||||
foreach (KeyValuePair<string, (List<string>, int)> kvp in groupsData)
|
||||
{
|
||||
var groupData = new Dictionary<string, object>
|
||||
Dictionary<string, object> groupData = new Dictionary<string, object>
|
||||
{
|
||||
["flags"] = kvp.Value.Item1,
|
||||
["immunity"] = kvp.Value.Item2
|
||||
|
|
@ -135,14 +141,14 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
jsonData[kvp.Key] = groupData;
|
||||
}
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
JsonSerializerOptions options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(jsonData, options);
|
||||
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "groups.json");
|
||||
string json = JsonSerializer.Serialize(jsonData, options);
|
||||
string filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "groups.json");
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
}
|
||||
|
||||
|
|
@ -153,55 +159,55 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
public async Task<List<(SteamID steamId, DateTime? ends, List<string> flags)>> CreateAdminsJsonFile()
|
||||
{
|
||||
List<(ulong identity, string name, List<string> flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags();
|
||||
var validPlayers = allPlayers
|
||||
List<(ulong identity, string name, List<string> flags, int immunity, DateTime? ends)> validPlayers = allPlayers
|
||||
.Where(player => SteamID.TryParse(player.identity.ToString(), out _))
|
||||
.ToList();
|
||||
|
||||
var jsonData = validPlayers
|
||||
.GroupBy(player => player.name)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
object (group) =>
|
||||
{
|
||||
var consolidatedData = group.Aggregate(
|
||||
new
|
||||
{
|
||||
identity = string.Empty,
|
||||
immunity = 0,
|
||||
flags = new List<string>(),
|
||||
groups = new List<string>()
|
||||
},
|
||||
(acc, player) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
|
||||
{
|
||||
acc = acc with { identity = player.identity.ToString() };
|
||||
}
|
||||
Dictionary<string, object> jsonData = validPlayers
|
||||
.GroupBy(player => player.name)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
object (group) =>
|
||||
{
|
||||
var consolidatedData = group.Aggregate(
|
||||
new
|
||||
{
|
||||
identity = string.Empty,
|
||||
immunity = 0,
|
||||
flags = new List<string>(),
|
||||
groups = new List<string>()
|
||||
},
|
||||
(acc, player) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
|
||||
{
|
||||
acc = acc with { identity = player.identity.ToString() };
|
||||
}
|
||||
|
||||
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
|
||||
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
|
||||
|
||||
acc = acc with
|
||||
{
|
||||
flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(),
|
||||
groups = acc.groups.Concat(player.flags.Where(flag => flag.StartsWith($"#"))).Distinct().ToList()
|
||||
};
|
||||
acc = acc with
|
||||
{
|
||||
flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(),
|
||||
groups = acc.groups.Concat(player.flags.Where(flag => flag.StartsWith($"#"))).Distinct().ToList()
|
||||
};
|
||||
|
||||
return acc;
|
||||
});
|
||||
return acc;
|
||||
});
|
||||
|
||||
return consolidatedData;
|
||||
});
|
||||
return consolidatedData;
|
||||
});
|
||||
|
||||
var newCache = validPlayers.Select(player => (new SteamID(player.identity), player.ends, player.flags)).ToList();
|
||||
List<(SteamID, DateTime? ends, List<string> flags)> newCache = validPlayers.Select(player => (new SteamID(player.identity), player.ends, player.flags)).ToList();
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
JsonSerializerOptions options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(jsonData, options);
|
||||
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
|
||||
string json = JsonSerializer.Serialize(jsonData, options);
|
||||
string filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
|
||||
return newCache;
|
||||
|
|
@ -214,25 +220,34 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <param name="admins">Admins loaded from the database.</param>
|
||||
public static void ApplyAdminCache(List<(SteamID steamId, DateTime? ends, List<string> flags)> admins)
|
||||
{
|
||||
foreach (var steamId in AdminCache.Keys.ToList())
|
||||
foreach (SteamID? steamId in AdminCache.Keys.ToList())
|
||||
{
|
||||
if (!AdminCache.TryRemove(steamId, out var cached)) continue;
|
||||
if (!AdminCache.TryRemove(steamId, out (DateTime? ExpirationTime, List<string> Flags) cached))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = AdminManager.GetPlayerAdminData(steamId);
|
||||
if (data == null) continue;
|
||||
AdminData? data = AdminManager.GetPlayerAdminData(steamId);
|
||||
if (data == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var flagsArray = cached.Flags.ToArray();
|
||||
string[] flagsArray = cached.Flags.ToArray();
|
||||
AdminManager.RemovePlayerPermissions(steamId, flagsArray);
|
||||
AdminManager.RemovePlayerFromGroup(steamId, true, flagsArray);
|
||||
|
||||
data = AdminManager.GetPlayerAdminData(steamId);
|
||||
if (data == null || (data.Flags.Count != 0 && data.Groups.Count != 0)) continue;
|
||||
if (data == null || (data.Flags.Count != 0 && data.Groups.Count != 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AdminManager.ClearPlayerPermissions(steamId);
|
||||
AdminManager.RemovePlayerAdminData(steamId);
|
||||
}
|
||||
|
||||
foreach (var (steamId, ends, flags) in admins)
|
||||
foreach ((SteamID? steamId, DateTime? ends, List<string>? flags) in admins)
|
||||
{
|
||||
AdminCache.TryAdd(steamId, (ends, flags));
|
||||
}
|
||||
|
|
@ -245,13 +260,20 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <param name="globalDelete">Whether to delete the admin globally or only for the current server.</param>
|
||||
public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (string.IsNullOrEmpty(playerSteamId)) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(playerSteamId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetDeleteAdminQuery(globalDelete);
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetDeleteAdminQuery(globalDelete);
|
||||
await connection.ExecuteAsync(sql, new { PlayerSteamID = playerSteamId, CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -271,31 +293,52 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <param name="globalAdmin">Whether the admin is global or server-specific.</param>
|
||||
public async Task AddAdminBySteamId(string playerSteamId, string playerName, List<string> flagsList, int immunity = 0, int time = 0, bool globalAdmin = false)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return;
|
||||
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
DateTime? futureTime = time != 0 ? now.AddMinutes(time) : null;
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime? futureTime;
|
||||
|
||||
if (time != 0)
|
||||
{
|
||||
futureTime = now.AddMinutes(time);
|
||||
}
|
||||
else
|
||||
{
|
||||
futureTime = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var insertAdminSql = databaseProvider.GetAddAdminQuery();
|
||||
var adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
|
||||
int? serverId = CS2_SimpleAdmin.ServerId;
|
||||
if (globalAdmin)
|
||||
{
|
||||
serverId = null;
|
||||
}
|
||||
|
||||
string insertAdminSql = databaseProvider.GetAddAdminQuery();
|
||||
int adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
|
||||
{
|
||||
playerSteamId,
|
||||
playerName,
|
||||
immunity,
|
||||
ends = futureTime,
|
||||
created = now,
|
||||
serverid = globalAdmin ? null : CS2_SimpleAdmin.ServerId
|
||||
serverid = serverId
|
||||
});
|
||||
|
||||
foreach (var flag in flagsList)
|
||||
foreach (string flag in flagsList)
|
||||
{
|
||||
var insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery();
|
||||
string insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery();
|
||||
await connection.ExecuteAsync(insertFlagsSql, new
|
||||
{
|
||||
adminId,
|
||||
|
|
@ -323,24 +366,30 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <param name="globalGroup">Whether the group is global or server-specific.</param>
|
||||
public async Task AddGroup(string groupName, List<string> flagsList, int immunity = 0, bool globalGroup = false)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return;
|
||||
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var insertGroup = databaseProvider.GetAddGroupQuery();
|
||||
var groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
|
||||
string insertGroup = databaseProvider.GetAddGroupQuery();
|
||||
int groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
|
||||
{
|
||||
groupName,
|
||||
immunity
|
||||
});
|
||||
|
||||
foreach (var flag in flagsList)
|
||||
foreach (string flag in flagsList)
|
||||
{
|
||||
var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
|
||||
string insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
|
||||
await connection.ExecuteAsync(insertFlagsSql, new
|
||||
{
|
||||
groupId,
|
||||
|
|
@ -348,8 +397,14 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
});
|
||||
}
|
||||
|
||||
var insertGroupServer = databaseProvider.GetAddGroupServerQuery();
|
||||
await connection.ExecuteAsync(insertGroupServer, new { groupId, server_id = globalGroup ? null : CS2_SimpleAdmin.ServerId });
|
||||
string insertGroupServer = databaseProvider.GetAddGroupServerQuery();
|
||||
int? groupServerId = CS2_SimpleAdmin.ServerId;
|
||||
if (globalGroup)
|
||||
{
|
||||
groupServerId = null;
|
||||
}
|
||||
|
||||
await connection.ExecuteAsync(insertGroupServer, new { groupId, server_id = groupServerId });
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.ReloadAdmins(null);
|
||||
|
|
@ -367,14 +422,20 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <param name="groupName">Name of the group to delete.</param>
|
||||
public async Task DeleteGroup(string groupName)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(groupName)) return;
|
||||
if (string.IsNullOrEmpty(groupName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetDeleteGroupQuery();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetDeleteGroupQuery();
|
||||
await connection.ExecuteAsync(sql, new { groupName });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -388,13 +449,16 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// </summary>
|
||||
public async Task DeleteOldAdmins()
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetDeleteOldAdminsQuery();
|
||||
string sql = databaseProvider.GetDeleteOldAdminsQuery();
|
||||
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime() });
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
|
|
@ -37,20 +38,30 @@ internal class PlayerManager
|
|||
return;
|
||||
}
|
||||
|
||||
var userId = player.UserId.Value;
|
||||
var slot = player.Slot;
|
||||
var steamId = player.SteamID;
|
||||
var playerName = !string.IsNullOrEmpty(player.PlayerName)
|
||||
? player.PlayerName
|
||||
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
var ipAddress = player.IpAddress?.Split(":")[0];
|
||||
int userId = player.UserId.Value;
|
||||
int slot = player.Slot;
|
||||
ulong steamId = player.SteamID;
|
||||
string playerName;
|
||||
|
||||
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(steamId, out var renamedTo))
|
||||
if (!string.IsNullOrEmpty(player.PlayerName))
|
||||
{
|
||||
playerName = player.PlayerName;
|
||||
}
|
||||
else
|
||||
{
|
||||
playerName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
}
|
||||
string? ipAddress = player.IpAddress?.Split(":")[0];
|
||||
|
||||
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(steamId, out string? renamedTo))
|
||||
{
|
||||
player.Rename(renamedTo);
|
||||
}
|
||||
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
|
|
@ -62,14 +73,8 @@ internal class PlayerManager
|
|||
await SavePlayerIpAddress(steamId, playerName, ipAddress);
|
||||
|
||||
// Always check bans first, regardless of PlayersInfo state
|
||||
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
|
||||
{
|
||||
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null),
|
||||
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
|
||||
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId,
|
||||
ipAddress)
|
||||
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress)
|
||||
};
|
||||
bool isBanned = IsBanned(CS2_SimpleAdmin.Instance.CacheManager, CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType,
|
||||
CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp, playerName, steamId, ipAddress);
|
||||
|
||||
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Player {playerName} ({steamId}) IP: {ipAddress} - BanType: {CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType} - CheckMultiAccounts: {CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp} - isBanned: {isBanned}");
|
||||
|
||||
|
|
@ -87,7 +92,7 @@ internal class PlayerManager
|
|||
|
||||
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
|
||||
{
|
||||
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
|
||||
PlayerInfo playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
|
||||
|
||||
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null)
|
||||
|
|
@ -109,8 +114,8 @@ internal class PlayerManager
|
|||
|
||||
if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(steamId, out PlayerInfo? value)) // Temp skip
|
||||
{
|
||||
var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false);
|
||||
var (totalMutes, totalGags, totalSilences) =
|
||||
List<dynamic> warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false);
|
||||
(int totalMutes, int totalGags, int totalSilences) =
|
||||
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(value);
|
||||
value.TotalBans = CS2_SimpleAdmin.Instance.CacheManager
|
||||
?.GetPlayerBansBySteamId(value.SteamId.SteamId64)
|
||||
|
|
@ -120,13 +125,13 @@ internal class PlayerManager
|
|||
value.TotalSilences = totalSilences;
|
||||
value.TotalWarns = warns.Count;
|
||||
|
||||
var activeMutes =
|
||||
List<dynamic> activeMutes =
|
||||
await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(value.SteamId.SteamId64
|
||||
.ToString());
|
||||
|
||||
if (activeMutes.Count > 0)
|
||||
{
|
||||
foreach (var mute in activeMutes)
|
||||
foreach (dynamic mute in activeMutes)
|
||||
{
|
||||
string muteType = mute.type;
|
||||
DateTime ends = mute.ends;
|
||||
|
|
@ -171,7 +176,7 @@ internal class PlayerManager
|
|||
{
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
foreach (var admin in Helper.GetValidPlayers()
|
||||
foreach (CCSPlayerController? admin in Helper.GetValidPlayers()
|
||||
.Where(p => (AdminManager.PlayerHasPermissions(
|
||||
new SteamID(p.SteamID),
|
||||
"@css/kick") ||
|
||||
|
|
@ -182,7 +187,11 @@ internal class PlayerManager
|
|||
!CS2_SimpleAdmin.AdminDisabledJoinComms
|
||||
.Contains(p.SteamID)))
|
||||
{
|
||||
if (CS2_SimpleAdmin._localizer == null || admin == player) continue;
|
||||
if (CS2_SimpleAdmin._localizer == null || admin == player)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
|
||||
"sa_admin_penalty_info",
|
||||
player.PlayerName,
|
||||
|
|
@ -195,10 +204,10 @@ internal class PlayerManager
|
|||
|
||||
if (CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.Count >= 2)
|
||||
{
|
||||
var associatedAcccountsChunks =
|
||||
List<IEnumerable<(ulong SteamId, string PlayerName)>> associatedAcccountsChunks =
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.ChunkBy(5)
|
||||
.ToList();
|
||||
foreach (var chunk in associatedAcccountsChunks)
|
||||
foreach (IEnumerable<(ulong SteamId, string PlayerName)> chunk in associatedAcccountsChunks)
|
||||
{
|
||||
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
|
||||
"sa_admin_associated_accounts",
|
||||
|
|
@ -235,17 +244,22 @@ internal class PlayerManager
|
|||
/// <param name="type">Optional filter: bans, gags, mutes, silences or warns.</param>
|
||||
public async Task<List<dynamic>> GetPenaltyHistory(ulong steamId, string? type = null)
|
||||
{
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return [];
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
var sql = CS2_SimpleAdmin.DatabaseProvider.GetPenaltyHistoryQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
var rows = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId })).ToList();
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
string sql = CS2_SimpleAdmin.DatabaseProvider.GetPenaltyHistoryQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
List<dynamic> rows = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId })).ToList();
|
||||
|
||||
// Filter word is the plural (bans, gags, mutes, silences, warns); rows carry BAN/GAG/MUTE/SILENCE/WARN
|
||||
if (type != null)
|
||||
{
|
||||
rows.RemoveAll(r => !type.Equals((string)r.type + "s", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
|
@ -263,7 +277,7 @@ internal class PlayerManager
|
|||
value switch
|
||||
{
|
||||
DateTime d => d,
|
||||
string s when DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) => parsed,
|
||||
string s when DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsed) => parsed,
|
||||
_ => null
|
||||
};
|
||||
|
||||
|
|
@ -272,17 +286,22 @@ internal class PlayerManager
|
|||
/// </summary>
|
||||
public async Task LoadRenamedPlayers()
|
||||
{
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
var rows = await connection.QueryAsync<(long steamId, string name)>(CS2_SimpleAdmin.DatabaseProvider.GetRenamesQuery());
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
IEnumerable<(long steamId, string name)> rows = await connection.QueryAsync<(long steamId, string name)>(CS2_SimpleAdmin.DatabaseProvider.GetRenamesQuery());
|
||||
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
foreach (var (steamId, name) in rows)
|
||||
foreach ((long steamId, string? name) in rows)
|
||||
{
|
||||
CS2_SimpleAdmin.RenamedPlayers[(ulong)steamId] = name;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -298,15 +317,22 @@ internal class PlayerManager
|
|||
/// <param name="name">Forced name, or null/empty to remove the rename.</param>
|
||||
public async Task SaveRenamedPlayer(ulong steamId, string? name)
|
||||
{
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetDeleteRenameQuery(), new { steamId });
|
||||
}
|
||||
else
|
||||
{
|
||||
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetUpsertRenameQuery(), new { steamId, name });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -321,16 +347,18 @@ internal class PlayerManager
|
|||
private async Task SavePlayerIpAddress(ulong steamId, string playerName, string? ipAddress)
|
||||
{
|
||||
if (!_config.OtherSettings.CheckMultiAccountsByIp || ipAddress == null || CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
|
||||
var steamId64 = steamId;
|
||||
var ipUint = IpHelper.IpToUint(ipAddress);
|
||||
ulong steamId64 = steamId;
|
||||
uint ipUint = IpHelper.IpToUint(ipAddress);
|
||||
|
||||
var upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
|
||||
string upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
|
||||
|
||||
await connection.ExecuteAsync(upsertQuery, new
|
||||
{
|
||||
|
|
@ -365,40 +393,46 @@ internal class PlayerManager
|
|||
{
|
||||
CS2_SimpleAdmin.Instance.AddTimer(5f, () =>
|
||||
{
|
||||
foreach (var (steamid, name) in CS2_SimpleAdmin.RenamedPlayers)
|
||||
foreach ((ulong steamid, string? name) in CS2_SimpleAdmin.RenamedPlayers)
|
||||
{
|
||||
var player = Helper.GetPlayerFromSteamid64(steamid);
|
||||
if (player == null || !player.IsValid || player.PlayerName == name) continue;
|
||||
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid);
|
||||
if (player == null || !player.IsValid || player.PlayerName == name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
player.Rename(name);
|
||||
}
|
||||
}, TimerFlags.REPEAT);
|
||||
|
||||
|
||||
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
|
||||
{
|
||||
#if DEBUG
|
||||
CS2_SimpleAdmin._logger?.LogCritical("[OnMapStart] Expired check");
|
||||
#endif
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Optimization: Get players once and avoid allocating anonymous types
|
||||
var validPlayers = Helper.GetValidPlayers();
|
||||
List<CCSPlayerController> validPlayers = Helper.GetValidPlayers();
|
||||
// Use ValueTuple instead of anonymous type - better performance and less allocations
|
||||
var tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
|
||||
foreach (var p in validPlayers)
|
||||
List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)> tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
|
||||
foreach (CCSPlayerController p in validPlayers)
|
||||
{
|
||||
tempPlayers.Add((p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot));
|
||||
}
|
||||
|
||||
var pluginInstance = CS2_SimpleAdmin.Instance;
|
||||
var config = _config.OtherSettings; // Cache config access
|
||||
CS2_SimpleAdmin pluginInstance = CS2_SimpleAdmin.Instance;
|
||||
OtherSettings config = _config.OtherSettings; // Cache config access
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Run all expire tasks in parallel
|
||||
var expireTasks = new[]
|
||||
Task[] expireTasks = new[]
|
||||
{
|
||||
pluginInstance.BanManager.ExpireOldBans(),
|
||||
pluginInstance.MuteManager.ExpireOldMutes(),
|
||||
|
|
@ -415,7 +449,7 @@ internal class PlayerManager
|
|||
|
||||
if (ex is AggregateException aggregate)
|
||||
{
|
||||
foreach (var inner in aggregate.InnerExceptions)
|
||||
foreach (Exception inner in aggregate.InnerExceptions)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError($"Inner exception: {inner.Message}");
|
||||
}
|
||||
|
|
@ -423,28 +457,24 @@ internal class PlayerManager
|
|||
}
|
||||
|
||||
if (pluginInstance.CacheManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Optimization: Cache ban type and multi-account check to avoid repeated config access
|
||||
var banType = config.BanType;
|
||||
var checkMultiAccounts = config.CheckMultiAccountsByIp;
|
||||
int banType = config.BanType;
|
||||
bool checkMultiAccounts = config.CheckMultiAccountsByIp;
|
||||
|
||||
var bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>();
|
||||
List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)> bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>();
|
||||
|
||||
// Manual loop instead of LINQ - better performance
|
||||
foreach (var player in tempPlayers)
|
||||
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in tempPlayers)
|
||||
{
|
||||
var playerName = player.PlayerName;
|
||||
var steamId = player.SteamID;
|
||||
var ip = player.IpAddress?.Split(':')[0];
|
||||
string playerName = player.PlayerName;
|
||||
ulong steamId = player.SteamID;
|
||||
string? ip = player.IpAddress?.Split(':')[0];
|
||||
|
||||
bool isBanned = banType switch
|
||||
{
|
||||
0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null),
|
||||
_ => checkMultiAccounts
|
||||
? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip)
|
||||
: pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip)
|
||||
};
|
||||
bool isBanned = IsBanned(pluginInstance.CacheManager, banType, checkMultiAccounts, playerName, steamId, ip);
|
||||
|
||||
if (isBanned)
|
||||
{
|
||||
|
|
@ -454,23 +484,29 @@ internal class PlayerManager
|
|||
|
||||
if (bannedPlayers.Count > 0)
|
||||
{
|
||||
foreach (var player in bannedPlayers)
|
||||
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in bannedPlayers)
|
||||
{
|
||||
if (!player.UserId.HasValue) continue;
|
||||
if (!player.UserId.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
if (Helper.GetPlayerFromSteamid64(player.SteamID) != null)
|
||||
{
|
||||
Helper.KickPlayer((int)player.UserId,
|
||||
NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (config.TimeMode == 0)
|
||||
{
|
||||
// Optimization: Manual projection instead of LINQ
|
||||
var onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count);
|
||||
foreach (var player in tempPlayers)
|
||||
List<(ulong, int?, int)> onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count);
|
||||
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in tempPlayers)
|
||||
{
|
||||
onlinePlayers.Add((player.SteamID, player.UserId, player.Slot));
|
||||
}
|
||||
|
|
@ -485,14 +521,16 @@ internal class PlayerManager
|
|||
try
|
||||
{
|
||||
// Optimization: Process penalties without LINQ allocations
|
||||
var players = Helper.GetValidPlayers();
|
||||
foreach (var player in players)
|
||||
List<CCSPlayerController> players = Helper.GetValidPlayers();
|
||||
foreach (CCSPlayerController player in players)
|
||||
{
|
||||
if (!PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
|
||||
var isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
|
||||
bool isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
|
||||
bool isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
|
||||
|
||||
// Only reset voice flags if not muted or silenced
|
||||
if (!isMuted && !isSilenced)
|
||||
|
|
@ -509,4 +547,22 @@ internal class PlayerManager
|
|||
}
|
||||
}, TimerFlags.REPEAT);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BanType 0 checks the SteamID only; otherwise the IP too, and every IP the player used when multi-account checks are on.
|
||||
/// </summary>
|
||||
private static bool IsBanned(CacheManager cacheManager, int banType, bool checkMultiAccounts, string playerName, ulong steamId, string? ipAddress)
|
||||
{
|
||||
if (banType == 0)
|
||||
{
|
||||
return cacheManager.IsPlayerBanned(playerName, steamId, null);
|
||||
}
|
||||
|
||||
if (checkMultiAccounts)
|
||||
{
|
||||
return cacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ipAddress);
|
||||
}
|
||||
|
||||
return cacheManager.IsPlayerBanned(playerName, steamId, ipAddress);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using CS2_SimpleAdminApi;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using CS2_SimpleAdminApi;
|
||||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ public static class PlayerPenaltyManager
|
|||
Penalties.AddOrUpdate(slot,
|
||||
(_) =>
|
||||
{
|
||||
var dict = new Dictionary<PenaltyType, List<(DateTime, int, bool)>>
|
||||
Dictionary<PenaltyType, List<(DateTime, int, bool)>> dict = new Dictionary<PenaltyType, List<(DateTime, int, bool)>>
|
||||
{
|
||||
[penaltyType] = [(endDateTime, durationInMinutes, false)]
|
||||
};
|
||||
|
|
@ -28,7 +28,7 @@ public static class PlayerPenaltyManager
|
|||
},
|
||||
(_, existingDict) =>
|
||||
{
|
||||
if (!existingDict.TryGetValue(penaltyType, out var value))
|
||||
if (!existingDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? value))
|
||||
{
|
||||
value = new List<(DateTime, int, bool)>();
|
||||
existingDict[penaltyType] = value;
|
||||
|
|
@ -50,21 +50,27 @@ public static class PlayerPenaltyManager
|
|||
{
|
||||
endDateTime = null;
|
||||
|
||||
if (!Penalties.TryGetValue(slot, out var penaltyDict) ||
|
||||
!penaltyDict.TryGetValue(penaltyType, out var penaltiesList)) return false;
|
||||
if (!Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) ||
|
||||
!penaltyDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
|
||||
{
|
||||
if (penaltiesList.Count == 0) return false;
|
||||
|
||||
if (penaltiesList.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
endDateTime = penaltiesList.First().EndDateTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
DateTime now = Time.ActualDateTime();
|
||||
|
||||
// Check if any active penalties exist
|
||||
foreach (var penalty in penaltiesList.ToList())
|
||||
foreach ((DateTime EndDateTime, int Duration, bool Passed) penalty in penaltiesList.ToList())
|
||||
{
|
||||
// Check if the penalty is still active
|
||||
if (penalty.Duration > 0 && now >= penalty.EndDateTime)
|
||||
|
|
@ -95,14 +101,14 @@ public static class PlayerPenaltyManager
|
|||
/// <returns>A list of penalties if found, otherwise an empty list.</returns>
|
||||
public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, PenaltyType penaltyType)
|
||||
{
|
||||
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
|
||||
penaltyDict.TryGetValue(penaltyType, out var penaltiesList))
|
||||
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) &&
|
||||
penaltyDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
|
||||
{
|
||||
return penaltiesList;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all penalties for a player across multiple penalty types.
|
||||
/// </summary>
|
||||
|
|
@ -113,11 +119,11 @@ public static class PlayerPenaltyManager
|
|||
{
|
||||
List<(DateTime EndDateTime, int Duration, bool Passed)> result = [];
|
||||
|
||||
if (Penalties.TryGetValue(slot, out var penaltyDict))
|
||||
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
|
||||
{
|
||||
foreach (var type in penaltyType)
|
||||
foreach (PenaltyType type in penaltyType)
|
||||
{
|
||||
if (penaltyDict.TryGetValue(type, out var penaltiesList))
|
||||
if (penaltyDict.TryGetValue(type, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
|
||||
{
|
||||
result.AddRange(penaltiesList);
|
||||
}
|
||||
|
|
@ -126,7 +132,7 @@ public static class PlayerPenaltyManager
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all penalties for a player across all penalty types.
|
||||
/// </summary>
|
||||
|
|
@ -134,12 +140,12 @@ public static class PlayerPenaltyManager
|
|||
/// <returns>A dictionary with penalty types as keys and lists of penalties as values.</returns>
|
||||
public static Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetAllPlayerPenalties(int slot)
|
||||
{
|
||||
// Check if the player has any penalties in the dictionary
|
||||
return Penalties.TryGetValue(slot, out var penaltyDict) ?
|
||||
// Return all penalty types and their respective penalties for the player
|
||||
penaltyDict :
|
||||
// If the player has no penalties, return an empty dictionary
|
||||
new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
|
||||
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
|
||||
{
|
||||
return penaltyDict;
|
||||
}
|
||||
|
||||
return new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -179,7 +185,7 @@ public static class PlayerPenaltyManager
|
|||
/// <param name="penaltyType">The penalty type to remove.</param>
|
||||
public static void RemovePenaltiesByType(int slot, PenaltyType penaltyType)
|
||||
{
|
||||
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
|
||||
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) &&
|
||||
penaltyDict.ContainsKey(penaltyType))
|
||||
{
|
||||
penaltyDict.Remove(penaltyType);
|
||||
|
|
@ -193,15 +199,21 @@ public static class PlayerPenaltyManager
|
|||
/// <param name="dateTime">The end datetime of penalties to mark as passed.</param>
|
||||
public static void RemovePenaltiesByDateTime(int slot, DateTime dateTime)
|
||||
{
|
||||
if (!Penalties.TryGetValue(slot, out var penaltyDict)) return;
|
||||
|
||||
foreach (var penaltiesList in penaltyDict.Values)
|
||||
if (!Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
|
||||
{
|
||||
for (var i = 0; i < penaltiesList.Count; i++)
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)> penaltiesList in penaltyDict.Values)
|
||||
{
|
||||
for (int i = 0; i < penaltiesList.Count; i++)
|
||||
{
|
||||
if (penaltiesList[i].EndDateTime != dateTime) continue;
|
||||
if (penaltiesList[i].EndDateTime != dateTime)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Create a copy of the penalty
|
||||
var penalty = penaltiesList[i];
|
||||
(DateTime EndDateTime, int Duration, bool Passed) penalty = penaltiesList[i];
|
||||
|
||||
// Update the end datetime of the copied penalty to the current datetime
|
||||
penalty.Passed = true;
|
||||
|
|
@ -223,10 +235,10 @@ public static class PlayerPenaltyManager
|
|||
{
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
|
||||
{
|
||||
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
|
||||
foreach ((int playerSlot, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
|
||||
{
|
||||
// Remove expired penalties for the player
|
||||
foreach (var penaltiesList in penaltyDict.Values)
|
||||
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList in penaltyDict.Values)
|
||||
{
|
||||
penaltiesList.RemoveAll(p => p is { Duration: > 0, Passed: true });
|
||||
}
|
||||
|
|
@ -241,10 +253,10 @@ public static class PlayerPenaltyManager
|
|||
return;
|
||||
}
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
|
||||
DateTime now = Time.ActualDateTime();
|
||||
foreach ((int playerSlot, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
|
||||
{
|
||||
foreach (var penaltiesList in penaltyDict.Values)
|
||||
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList in penaltyDict.Values)
|
||||
{
|
||||
penaltiesList.RemoveAll(p => p.Duration > 0 && now >= p.EndDateTime);
|
||||
}
|
||||
|
|
@ -255,4 +267,4 @@ public static class PlayerPenaltyManager
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using System.Data.Common;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -15,13 +15,15 @@ public class ServerManager
|
|||
/// </summary>
|
||||
public static void CheckHibernationStatus()
|
||||
{
|
||||
var convar = ConVar.Find("sv_hibernate_when_empty");
|
||||
ConVar? convar = ConVar.Find("sv_hibernate_when_empty");
|
||||
if (convar == null || !convar.GetPrimitiveValue<bool>())
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
CS2_SimpleAdmin._logger?.LogError("Detected setting \"sv_hibernate_when_empty true\", set false to make plugin work properly");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initiates the asynchronous process to load server data such as IP address, port, hostname, and RCON password.
|
||||
/// Handles retry attempts if IP address is not immediately available.
|
||||
|
|
@ -33,11 +35,14 @@ public class ServerManager
|
|||
{
|
||||
CS2_SimpleAdmin.Instance.AddTimer(1.0f, () =>
|
||||
{
|
||||
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Optimization: Get server IP once and reuse
|
||||
var serverIp = Helper.GetServerIp();
|
||||
var isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0");
|
||||
string serverIp = Helper.GetServerIp();
|
||||
bool isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0");
|
||||
|
||||
// Check if we've exceeded retry limit with invalid IP
|
||||
if (_getIpTryCount > 32 && isInvalidIp)
|
||||
|
|
@ -49,8 +54,8 @@ public class ServerManager
|
|||
}
|
||||
|
||||
// Optimization: Cache ConVar lookups
|
||||
var ipConVar = ConVar.Find("ip");
|
||||
var ipAddress = ipConVar?.StringValue;
|
||||
ConVar? ipConVar = ConVar.Find("ip");
|
||||
string? ipAddress = ipConVar?.StringValue;
|
||||
|
||||
// Use Helper IP if ConVar IP is invalid
|
||||
if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))
|
||||
|
|
@ -67,20 +72,20 @@ public class ServerManager
|
|||
}
|
||||
|
||||
// Optimization: Cache remaining ConVar lookups
|
||||
var hostportConVar = ConVar.Find("hostport");
|
||||
var hostnameConVar = ConVar.Find("hostname");
|
||||
var rconPasswordConVar = ConVar.Find("rcon_password");
|
||||
ConVar? hostportConVar = ConVar.Find("hostport");
|
||||
ConVar? hostnameConVar = ConVar.Find("hostname");
|
||||
ConVar? rconPasswordConVar = ConVar.Find("rcon_password");
|
||||
|
||||
var address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue<int>()}";
|
||||
var hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
var rconPassword = rconPasswordConVar?.StringValue ?? "";
|
||||
string address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue<int>()}";
|
||||
string hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
string rconPassword = rconPasswordConVar?.StringValue ?? "";
|
||||
CS2_SimpleAdmin.IpAddress = address;
|
||||
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
int? serverId = await connection.ExecuteScalarAsync<int?>(
|
||||
"SELECT id FROM sa_servers WHERE address = @address",
|
||||
new { address });
|
||||
|
|
@ -126,8 +131,8 @@ public class ServerManager
|
|||
|
||||
if (CS2_SimpleAdmin.Instance.Config.EnableMetrics)
|
||||
{
|
||||
var queryString = $"?address={address}&hostname={hostname}";
|
||||
var client = CS2_SimpleAdmin.HttpClient;
|
||||
string queryString = $"?address={address}&hostname={hostname}";
|
||||
HttpClient client = CS2_SimpleAdmin.HttpClient;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -139,8 +144,8 @@ public class ServerManager
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
CS2_SimpleAdmin.SimpleAdminApi?.OnSimpleAdminReadyEvent();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using CS2_SimpleAdmin.Database;
|
||||
using System.Data.Common;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -17,17 +18,20 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
|
||||
public async Task<int?> WarnPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (databaseProvider == null) return null;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
var futureTime = now.AddMinutes(time);
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddWarnQuery(true);
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetAddWarnQuery(true);
|
||||
|
||||
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
int? warnId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = player.SteamId.SteamId64,
|
||||
playerName = player.Name,
|
||||
|
|
@ -42,7 +46,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
return warnId;
|
||||
}
|
||||
catch(Exception)
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
@ -58,17 +62,20 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
|
||||
public async Task<int?> AddWarnBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (databaseProvider == null) return null;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
var futureTime = now.AddMinutes(time);
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddWarnQuery(false);
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
string sql = databaseProvider.GetAddWarnQuery(false);
|
||||
|
||||
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
int? warnId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = playerSteamId,
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
|
|
@ -96,15 +103,18 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>A list of dynamic objects representing warnings, or an empty list if none found or on failure.</returns>
|
||||
public async Task<List<dynamic>> GetPlayerWarns(PlayerInfo player, bool active = true)
|
||||
{
|
||||
if (databaseProvider == null) return [];
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
|
||||
string sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
|
||||
var parameters = new { PlayerSteamID = player.SteamId.SteamId64, serverid = CS2_SimpleAdmin.ServerId };
|
||||
var warns = await connection.QueryAsync<dynamic>(sql, parameters);
|
||||
IEnumerable<dynamic> warns = await connection.QueryAsync<dynamic>(sql, parameters);
|
||||
|
||||
return warns.ToList();
|
||||
}
|
||||
|
|
@ -122,14 +132,17 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>The count of warnings as an integer, or 0 if none found or on failure.</returns>
|
||||
public async Task<int> GetPlayerWarnsCount(ulong steamId, bool active = true)
|
||||
{
|
||||
if (databaseProvider == null) return 0;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
|
||||
var warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
string sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
|
||||
int warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
return warnsCount;
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
@ -146,13 +159,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task UnwarnPlayer(PlayerInfo player, int warnId)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
string sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64, warnId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -160,7 +176,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
CS2_SimpleAdmin._logger?.LogCritical($"Unable to remove warn + {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes the most recent warning matching a player pattern (usually SteamID string).
|
||||
/// </summary>
|
||||
|
|
@ -168,13 +184,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task UnwarnPlayer(string playerPattern)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
string sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { steamid = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -189,13 +208,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task ExpireOldWarns()
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (databaseProvider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
string sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -203,4 +225,4 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
CS2_SimpleAdmin._logger?.LogCritical($"Unable to remove expired warns + {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CounterStrikeSharp.API.Modules.Menu;
|
||||
|
||||
namespace CS2_SimpleAdmin.Menus;
|
||||
|
||||
public static class AdminMenu
|
||||
{
|
||||
public static void OpenMenu(CCSPlayerController admin)
|
||||
{
|
||||
MenuManager.Instance.OpenMainMenu(admin);
|
||||
}
|
||||
|
||||
public static IMenu? CreateMenu(string title, Action<CCSPlayerController>? backAction = null)
|
||||
{
|
||||
return Helper.CreateMenu(title, backAction);
|
||||
// return CS2_SimpleAdmin.Instance.Config.UseChatMenu ? new ChatMenu(title) : new CenterHtmlMenu(title, CS2_SimpleAdmin.Instance);
|
||||
}
|
||||
|
||||
public static void OpenMenu(CCSPlayerController player, IMenu menu)
|
||||
{
|
||||
menu.Open(player);
|
||||
// switch (menu)
|
||||
// {
|
||||
// case CenterHtmlMenu centerHtmlMenu:
|
||||
// MenuManager.OpenCenterHtmlMenu(CS2_SimpleAdmin.Instance, player, centerHtmlMenu);
|
||||
// break;
|
||||
// case ChatMenu chatMenu:
|
||||
// MenuManager.OpenChatMenu(player, chatMenu);
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
|
||||
// public static void OpenMenu(CCSPlayerController admin)
|
||||
// {
|
||||
// if (admin.IsValid == false)
|
||||
// return;
|
||||
//
|
||||
// var localizer = CS2_SimpleAdmin._localizer;
|
||||
// if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false)
|
||||
// {
|
||||
// admin.PrintToChat(localizer?["sa_prefix"] ??
|
||||
// "[SimpleAdmin] " +
|
||||
// (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// var menu = CreateMenu(localizer?["sa_title"] ?? "SimpleAdmin");
|
||||
// List<ChatMenuOptionData> options =
|
||||
// [
|
||||
// new(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)),
|
||||
// new(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)),
|
||||
// new(localizer?["sa_menu_fun_commands"] ?? "Fun Commands", () => FunActionsMenu.OpenMenu(admin)),
|
||||
// ];
|
||||
//
|
||||
// var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
|
||||
// if (customCommands.Count > 0)
|
||||
// {
|
||||
// options.Add(new ChatMenuOptionData(localizer?["sa_menu_custom_commands"] ?? "Custom Commands", () => CustomCommandsMenu.OpenMenu(admin)));
|
||||
// }
|
||||
//
|
||||
// if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"))
|
||||
// options.Add(new ChatMenuOptionData(localizer?["sa_menu_admins_manage"] ?? "Admins Manage", () => ManageAdminsMenu.OpenMenu(admin)));
|
||||
//
|
||||
// foreach (var menuOptionData in options)
|
||||
// {
|
||||
// var menuName = menuOptionData.Name;
|
||||
// menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
|
||||
// }
|
||||
//
|
||||
// if (menu != null) OpenMenu(admin, menu);
|
||||
// }
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace CS2_SimpleAdmin.Menus;
|
||||
|
||||
public class ChatMenuOptionData(string name, Action action, bool disabled = false)
|
||||
{
|
||||
public readonly string Name = name;
|
||||
public readonly Action Action = action;
|
||||
public readonly bool Disabled = disabled;
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
|
||||
namespace CS2_SimpleAdmin.Menus;
|
||||
|
||||
public static class CustomCommandsMenu
|
||||
{
|
||||
public static void OpenMenu(CCSPlayerController admin)
|
||||
{
|
||||
if (!admin.IsValid)
|
||||
return;
|
||||
|
||||
var localizer = CS2_SimpleAdmin._localizer;
|
||||
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
|
||||
{
|
||||
admin.PrintToChat(localizer?["sa_prefix"] ??
|
||||
"[SimpleAdmin] " +
|
||||
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_custom_commands"] ?? "Custom Commands");
|
||||
List<ChatMenuOptionData> options = [];
|
||||
|
||||
var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
|
||||
options.AddRange(from customCommand in customCommands
|
||||
where !string.IsNullOrEmpty(customCommand.DisplayName) && !string.IsNullOrEmpty(customCommand.Command)
|
||||
let hasRights = AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), customCommand.Flag)
|
||||
where hasRights
|
||||
select new ChatMenuOptionData(customCommand.DisplayName, () =>
|
||||
{
|
||||
Helper.TryLogCommandOnDiscord(admin, customCommand.Command);
|
||||
|
||||
if (customCommand.ExecuteOnClient)
|
||||
admin.ExecuteClientCommandFromServer(customCommand.Command);
|
||||
else
|
||||
Server.ExecuteCommand(customCommand.Command);
|
||||
}));
|
||||
|
||||
foreach (var menuOptionData in options)
|
||||
{
|
||||
var menuName = menuOptionData.Name;
|
||||
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action(); }, menuOptionData.Disabled);
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using CS2_SimpleAdmin.Models;
|
||||
|
||||
namespace CS2_SimpleAdmin.Menus;
|
||||
|
||||
public static class DurationMenu
|
||||
{
|
||||
public static void OpenMenu(CCSPlayerController admin, string menuName, CCSPlayerController player, Action<CCSPlayerController, CCSPlayerController, int> onSelectAction)
|
||||
{
|
||||
var menu = AdminMenu.CreateMenu(menuName);
|
||||
if (menu == null)
|
||||
return;
|
||||
|
||||
var durations = CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations;
|
||||
|
||||
// Capture admin and player to avoid closure issues
|
||||
var capturedAdmin = admin;
|
||||
var capturedPlayer = player;
|
||||
var capturedAction = onSelectAction;
|
||||
|
||||
foreach (var durationItem in durations)
|
||||
{
|
||||
var duration = durationItem.Duration; // Capture in local variable
|
||||
var name = durationItem.Name;
|
||||
|
||||
menu.AddMenuOption(name, (controller, option) =>
|
||||
{
|
||||
capturedAction(capturedAdmin, capturedPlayer, duration);
|
||||
});
|
||||
}
|
||||
|
||||
AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
public static void OpenMenu(CCSPlayerController admin, string menuName, DisconnectedPlayer player, Action<CCSPlayerController, DisconnectedPlayer, int> onSelectAction)
|
||||
{
|
||||
var menu = AdminMenu.CreateMenu(menuName);
|
||||
foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations)
|
||||
{
|
||||
menu?.AddMenuOption(durationItem.Name, (_, _) => { onSelectAction(admin, player, durationItem.Duration); });
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -264,4 +264,4 @@
|
|||
// {
|
||||
// CS2_SimpleAdmin.SetMoney(admin, player, money);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
|
||||
namespace CS2_SimpleAdmin.Menus;
|
||||
|
||||
public static class ManageAdminsMenu
|
||||
{
|
||||
public static void OpenMenu(CCSPlayerController admin)
|
||||
{
|
||||
if (!admin.IsValid)
|
||||
return;
|
||||
|
||||
var localizer = CS2_SimpleAdmin._localizer;
|
||||
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"))
|
||||
{
|
||||
admin.PrintToChat(localizer?["sa_prefix"] ??
|
||||
"[SimpleAdmin] " +
|
||||
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_admins_manage"] ?? "Admins Manage");
|
||||
List<ChatMenuOptionData> options =
|
||||
[
|
||||
new(localizer?["sa_admin_add"] ?? "Add Admin",
|
||||
() => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_admin_add"] ?? "Add Admin", AddAdminMenu)),
|
||||
new(localizer?["sa_admin_remove"] ?? "Remove Admin",
|
||||
() => PlayersMenu.OpenAdminPlayersMenu(admin, localizer?["sa_admin_remove"] ?? "Remove Admin", RemoveAdmin,
|
||||
player => player != admin && admin.CanTarget(player))),
|
||||
new(localizer?["sa_admin_reload"] ?? "Reload Admins", () => ReloadAdmins(admin))
|
||||
];
|
||||
|
||||
foreach (var menuOptionData in options)
|
||||
{
|
||||
var menuName = menuOptionData.Name;
|
||||
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void AddAdminMenu(CCSPlayerController admin, CCSPlayerController player)
|
||||
{
|
||||
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_admin_add"] ?? "Add Admin"}: {player.PlayerName}");
|
||||
|
||||
foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags)
|
||||
{
|
||||
var disabled = AdminManager.PlayerHasPermissions(player, adminFlag.Flag);
|
||||
menu?.AddMenuOption(adminFlag.Name, (_, _) => { AddAdmin(admin, player, adminFlag.Flag); }, disabled);
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void AddAdmin(CCSPlayerController admin, CCSPlayerController player, string flag)
|
||||
{
|
||||
// TODO: Change default immunity?
|
||||
CS2_SimpleAdmin.AddAdmin(admin, player.SteamID.ToString(), player.PlayerName, flag, 10);
|
||||
}
|
||||
|
||||
private static void RemoveAdmin(CCSPlayerController admin, CCSPlayerController player)
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.RemoveAdmin(admin, player.SteamID.ToString());
|
||||
}
|
||||
|
||||
private static void ReloadAdmins(CCSPlayerController admin)
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.ReloadAdmins(admin);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using CS2_SimpleAdminApi;
|
||||
|
||||
namespace CS2_SimpleAdmin.Menus;
|
||||
|
||||
public static class ManagePlayersMenu
|
||||
{
|
||||
public static void OpenMenu(CCSPlayerController admin)
|
||||
{
|
||||
if (!admin.IsValid)
|
||||
return;
|
||||
|
||||
var localizer = CS2_SimpleAdmin._localizer;
|
||||
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
|
||||
{
|
||||
admin.PrintToChat(localizer?["sa_prefix"] ??
|
||||
"[SimpleAdmin] " +
|
||||
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_players_manage"] ?? "Manage Players");
|
||||
List<ChatMenuOptionData> options = [];
|
||||
|
||||
// permissions
|
||||
var hasSlay = AdminManager.CommandIsOverriden("css_slay") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_slay")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay");
|
||||
var hasKick = AdminManager.CommandIsOverriden("css_kick") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_kick")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick");
|
||||
var hasBan = AdminManager.CommandIsOverriden("css_ban") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_ban")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/ban");
|
||||
var hasChat = AdminManager.CommandIsOverriden("css_gag") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gag")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat");
|
||||
|
||||
// TODO: Localize options
|
||||
// options added in order
|
||||
|
||||
if (hasSlay)
|
||||
{
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_slap"] ?? "Slap", () => PlayersMenu.OpenMenu(admin, localizer?["sa_slap"] ?? "Slap", SlapMenu)));
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_slay"] ?? "Slay", () => PlayersMenu.OpenMenu(admin, localizer?["sa_slay"] ?? "Slay", Slay)));
|
||||
}
|
||||
|
||||
if (hasKick)
|
||||
{
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_kick"] ?? "Kick", () => PlayersMenu.OpenMenu(admin, localizer?["sa_kick"] ?? "Kick", KickMenu)));
|
||||
}
|
||||
|
||||
if (AdminManager.CommandIsOverriden("css_warn")
|
||||
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_warn"))
|
||||
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick"))
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_warn"] ?? "Warn", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_warn"] ?? "Warn", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_warn"] ?? "Warn"}: {p.PlayerName}", p, WarnMenu))));
|
||||
|
||||
if (hasBan)
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_ban"] ?? "Ban", () =>
|
||||
PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_ban"] ?? "Ban", (a, p) =>
|
||||
DurationMenu.OpenMenu(a, $"{localizer?["sa_ban"] ?? "Ban"}: {p.PlayerName}", p, BanMenu))));
|
||||
|
||||
if (hasChat)
|
||||
{
|
||||
if (AdminManager.CommandIsOverriden("css_gag")
|
||||
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gag"))
|
||||
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_gag"] ?? "Gag", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_gag"] ?? "Gag", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_gag"] ?? "Gag"}: {p.PlayerName}", p, GagMenu))));
|
||||
if (AdminManager.CommandIsOverriden("css_mute")
|
||||
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_mute"))
|
||||
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_mute"] ?? "Mute", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_mute"] ?? "Mute", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_mute"] ?? "Mute"}: {p.PlayerName}", p, MuteMenu))));
|
||||
if (AdminManager.CommandIsOverriden("css_silence")
|
||||
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_silence"))
|
||||
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_silence"] ?? "Silence", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_silence"] ?? "Silence", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_silence"] ?? "Silence"}: {p.PlayerName}", p, SilenceMenu))));
|
||||
}
|
||||
|
||||
if (AdminManager.CommandIsOverriden("css_team")
|
||||
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_team"))
|
||||
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick"))
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_team_force"] ?? "Force Team", () => PlayersMenu.OpenMenu(admin, localizer?["sa_team_force"] ?? "Force Team", ForceTeamMenu)));
|
||||
|
||||
foreach (var menuOptionData in options)
|
||||
{
|
||||
var menuName = menuOptionData.Name;
|
||||
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void SlapMenu(CCSPlayerController admin, CCSPlayerController player)
|
||||
{
|
||||
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_slap"] ?? "Slap"}: {player.PlayerName}");
|
||||
List<ChatMenuOptionData> options =
|
||||
[
|
||||
// options added in order
|
||||
new("0 hp", () => ApplySlapAndKeepMenu(admin, player, 0)),
|
||||
new("1 hp", () => ApplySlapAndKeepMenu(admin, player, 1)),
|
||||
new("5 hp", () => ApplySlapAndKeepMenu(admin, player, 5)),
|
||||
new("10 hp", () => ApplySlapAndKeepMenu(admin, player, 10)),
|
||||
new("50 hp", () => ApplySlapAndKeepMenu(admin, player, 50)),
|
||||
new("100 hp", () => ApplySlapAndKeepMenu(admin, player, 100)),
|
||||
];
|
||||
|
||||
foreach (var menuOptionData in options)
|
||||
{
|
||||
var menuName = menuOptionData.Name;
|
||||
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void ApplySlapAndKeepMenu(CCSPlayerController admin, CCSPlayerController player, int damage)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.Slap(admin, player, damage);
|
||||
SlapMenu(admin, player);
|
||||
}
|
||||
|
||||
private static void Slay(CCSPlayerController admin, CCSPlayerController player)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.Slay(admin, player);
|
||||
}
|
||||
|
||||
private static void KickMenu(CCSPlayerController admin, CCSPlayerController player)
|
||||
{
|
||||
ReasonMenu.OpenMenu(admin, PenaltyType.Kick,
|
||||
$"{CS2_SimpleAdmin._localizer?["sa_kick"] ?? "Kick"}: {player.PlayerName}", player, (_, _, reason) =>
|
||||
{
|
||||
if (player is { IsValid: true })
|
||||
Kick(admin, player, reason);
|
||||
});
|
||||
|
||||
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_kick"] ?? "Kick"}: {player?.PlayerName}");
|
||||
//
|
||||
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.KickReasons)
|
||||
// {
|
||||
// menu?.AddMenuOption(option, (_, _) =>
|
||||
// {
|
||||
// if (player is { IsValid: true })
|
||||
// Kick(admin, player, option);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void Kick(CCSPlayerController admin, CCSPlayerController player, string? reason)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.Instance.Kick(admin, player, reason, admin.PlayerName);
|
||||
}
|
||||
|
||||
internal static void BanMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
|
||||
{
|
||||
ReasonMenu.OpenMenu(admin, PenaltyType.Ban,
|
||||
$"{CS2_SimpleAdmin._localizer?["sa_ban"] ?? "Ban"}: {player.PlayerName}", player, (_, _, reason) =>
|
||||
{
|
||||
if (player is { IsValid: true })
|
||||
Ban(admin, player, duration, reason);
|
||||
|
||||
CS2_SimpleAdmin.MenuApi?.CloseMenu(admin);
|
||||
});
|
||||
}
|
||||
|
||||
private static void Ban(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason, admin.PlayerName);
|
||||
}
|
||||
|
||||
private static void WarnMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
|
||||
{
|
||||
ReasonMenu.OpenMenu(admin, PenaltyType.Warn,
|
||||
$"{CS2_SimpleAdmin._localizer?["sa_warn"] ?? "Warn"}: {player.PlayerName}", player, (_, _, reason) =>
|
||||
{
|
||||
if (player is { IsValid: true })
|
||||
Warn(admin, player, duration, reason);
|
||||
});
|
||||
|
||||
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_warn"] ?? "Warn"}: {player?.PlayerName}");
|
||||
//
|
||||
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.WarnReasons)
|
||||
// {
|
||||
// menu?.AddMenuOption(option, (_, _) =>
|
||||
// {
|
||||
// if (player is { IsValid: true })
|
||||
// Warn(admin, player, duration, option);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void Warn(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason, admin.PlayerName);
|
||||
}
|
||||
|
||||
internal static void GagMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
|
||||
{
|
||||
ReasonMenu.OpenMenu(admin, PenaltyType.Gag,
|
||||
$"{CS2_SimpleAdmin._localizer?["sa_gag"] ?? "Gag"}: {player.PlayerName}", player, (_, _, reason) =>
|
||||
{
|
||||
if (player is { IsValid: true })
|
||||
Gag(admin, player, duration, reason);
|
||||
});
|
||||
|
||||
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_gag"] ?? "Gag"}: {player?.PlayerName}");
|
||||
//
|
||||
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons)
|
||||
// {
|
||||
// menu?.AddMenuOption(option, (_, _) =>
|
||||
// {
|
||||
// if (player is { IsValid: true })
|
||||
// Gag(admin, player, duration, option);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void Gag(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
|
||||
}
|
||||
|
||||
internal static void MuteMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
|
||||
{
|
||||
ReasonMenu.OpenMenu(admin, PenaltyType.Mute,
|
||||
$"{CS2_SimpleAdmin._localizer?["sa_mute"] ?? "mute"}: {player.PlayerName}", player, (_, _, reason) =>
|
||||
{
|
||||
if (player is { IsValid: true })
|
||||
Mute(admin, player, duration, reason);
|
||||
});
|
||||
|
||||
// // TODO: Localize and make options in config?
|
||||
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_mute"] ?? "Mute"}: {player?.PlayerName}");
|
||||
//
|
||||
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons)
|
||||
// {
|
||||
// menu?.AddMenuOption(option, (_, _) =>
|
||||
// {
|
||||
// if (player is { IsValid: true })
|
||||
// Mute(admin, player, duration, option);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void Mute(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
|
||||
}
|
||||
|
||||
internal static void SilenceMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
|
||||
{
|
||||
ReasonMenu.OpenMenu(admin, PenaltyType.Silence,
|
||||
$"{CS2_SimpleAdmin._localizer?["sa_silence"] ?? "Silence"}: {player.PlayerName}", player, (_, _, reason) =>
|
||||
{
|
||||
if (player is { IsValid: true })
|
||||
Silence(admin, player, duration, reason);
|
||||
});
|
||||
|
||||
// // TODO: Localize and make options in config?
|
||||
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_silence"] ?? "Silence"}: {player?.PlayerName}");
|
||||
//
|
||||
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons)
|
||||
// {
|
||||
// menu?.AddMenuOption(option, (_, _) =>
|
||||
// {
|
||||
// if (player is { IsValid: true })
|
||||
// Silence(admin, player, duration, option);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void Silence(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
|
||||
}
|
||||
|
||||
private static void ForceTeamMenu(CCSPlayerController admin, CCSPlayerController player)
|
||||
{
|
||||
// TODO: Localize
|
||||
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_team_force"] ?? "Force Team"} {player.PlayerName}");
|
||||
List<ChatMenuOptionData> options =
|
||||
[
|
||||
new(CS2_SimpleAdmin._localizer?["sa_team_ct"] ?? "CT", () => ForceTeam(admin, player, "ct", CsTeam.CounterTerrorist)),
|
||||
new(CS2_SimpleAdmin._localizer?["sa_team_t"] ?? "T", () => ForceTeam(admin, player, "t", CsTeam.Terrorist)),
|
||||
new(CS2_SimpleAdmin._localizer?["sa_team_swap"] ?? "Swap", () => ForceTeam(admin, player, "swap", CsTeam.Spectator)),
|
||||
new(CS2_SimpleAdmin._localizer?["sa_team_spec"] ?? "Spec", () => ForceTeam(admin, player, "spec", CsTeam.Spectator)),
|
||||
];
|
||||
|
||||
foreach (var menuOptionData in options)
|
||||
{
|
||||
var menuName = menuOptionData.Name;
|
||||
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void ForceTeam(CCSPlayerController admin, CCSPlayerController player, string teamName, CsTeam teamNum)
|
||||
{
|
||||
if (player is not { IsValid: true }) return;
|
||||
|
||||
CS2_SimpleAdmin.ChangeTeam(admin, player, teamName, teamNum, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Admin;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
|
||||
namespace CS2_SimpleAdmin.Menus;
|
||||
|
||||
public static class ManageServerMenu
|
||||
{
|
||||
public static void OpenMenu(CCSPlayerController admin)
|
||||
{
|
||||
if (!admin.IsValid)
|
||||
return;
|
||||
|
||||
var localizer = CS2_SimpleAdmin._localizer;
|
||||
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
|
||||
{
|
||||
admin.PrintToChat(localizer?["sa_prefix"] ??
|
||||
"[SimpleAdmin] " +
|
||||
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_server_manage"] ?? "Server Manage");
|
||||
List<ChatMenuOptionData> options = [];
|
||||
|
||||
// permissions
|
||||
var hasMap = AdminManager.CommandIsOverriden("css_map") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_map")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/changemap");
|
||||
var hasPlugins = AdminManager.CommandIsOverriden("css_pluginsmanager") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_pluginsmanager")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root");
|
||||
|
||||
//bool hasMap = AdminManager.PlayerHasPermissions(admin, "@css/changemap");
|
||||
|
||||
// options added in order
|
||||
|
||||
if (hasPlugins)
|
||||
{
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_menu_pluginsmanager_title"] ?? "Manage Plugins", () => admin.ExecuteClientCommandFromServer("css_pluginsmanager")));
|
||||
}
|
||||
|
||||
if (hasMap)
|
||||
{
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_changemap"] ?? "Change Map", () => ChangeMapMenu(admin)));
|
||||
}
|
||||
|
||||
options.Add(new ChatMenuOptionData(localizer?["sa_restart_game"] ?? "Restart Game", () => CS2_SimpleAdmin.RestartGame(admin)));
|
||||
|
||||
foreach (var menuOptionData in options)
|
||||
{
|
||||
var menuName = menuOptionData.Name;
|
||||
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void ChangeMapMenu(CCSPlayerController admin)
|
||||
{
|
||||
var menu = AdminMenu.CreateMenu(CS2_SimpleAdmin._localizer?["sa_changemap"] ?? "Change Map");
|
||||
List<ChatMenuOptionData> options = [];
|
||||
|
||||
var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps;
|
||||
options.AddRange(maps.Select(map => new ChatMenuOptionData(map, () => ExecuteChangeMap(admin, map, false))));
|
||||
|
||||
var wsMaps = CS2_SimpleAdmin.Instance.Config.WorkshopMaps;
|
||||
options.AddRange(wsMaps.Select(map => new ChatMenuOptionData($"{map.Key} (WS)", () => ExecuteChangeMap(admin, map.Value?.ToString() ?? map.Key, true))));
|
||||
|
||||
foreach (var menuOptionData in options)
|
||||
{
|
||||
var menuName = menuOptionData.Name;
|
||||
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
|
||||
}
|
||||
|
||||
if (menu != null) AdminMenu.OpenMenu(admin, menu);
|
||||
}
|
||||
|
||||
private static void ExecuteChangeMap(CCSPlayerController admin, string mapName, bool workshop)
|
||||
{
|
||||
if (workshop)
|
||||
CS2_SimpleAdmin.Instance.ChangeWorkshopMap(admin, mapName);
|
||||
else
|
||||
CS2_SimpleAdmin.Instance.ChangeMap(admin, mapName);
|
||||
}
|
||||
}
|
||||