Compare commits

..

40 Commits

Author SHA1 Message Date
Dawid Bepierszcz
78318102fe Refactor fun commands to external module
Commented out fun command implementations (noclip, godmode, freeze, unfreeze, resize) in funcommands.cs and removed their registration from RegisterCommands.cs. These commands are now intended to be provided by the new CS2-SimpleAdmin_FunCommands external module, improving modularity and maintainability.
2025-10-19 03:12:58 +02:00
Dawid Bepierszcz
2edacc2b3f Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2025-10-03 12:10:52 +02:00
Dawid Bepierszcz
e1e66441f2 Remove custom ClearBuildFiles target and cleanup csproj
Commented out the GenerateDependencyFile property and removed the ClearBuildFiles target from the project file. Also reformatted and cleaned up the ItemGroup for migration scripts, improving maintainability.
2025-10-03 12:10:50 +02:00
Dawid Bepierszcz
cc54b9e879 Update README.md 2025-10-03 02:35:45 +02:00
Dawid Bepierszcz
640e618f3b Update README.md 2025-10-03 02:34:03 +02:00
Dawid Bepierszcz
23d174c4a5 Update build.yml 2025-10-03 02:26:07 +02:00
Dawid Bepierszcz
b7371adf26 Update build.yml 2025-10-03 02:23:07 +02:00
Dawid Bepierszcz
9154748ce6 Update build.yml 2025-10-03 02:19:05 +02:00
Dawid Bepierszcz
da9830ee05 Bump version to 1.7.7-alpha-10 and update release workflow
Updated the version to 1.7.7-alpha-10 in both the code and VERSION file. Fixed artifact naming and release name in the GitHub Actions workflow. Also, modified Helper.cs to return DateTime.UtcNow in ActualDateTime().
2025-10-03 02:16:49 +02:00
Dawid Bepierszcz
b41ac992c0 Update README.md 2025-10-03 02:13:51 +02:00
Dawid Bepierszcz
af0bda8f3a Update README.md 2025-10-03 02:04:10 +02:00
Dawid Bepierszcz
e2529cd646 Update build.yml 2025-10-03 01:44:23 +02:00
Dawid Bepierszcz
9d2cd34845 Update build.yml 2025-10-03 01:41:48 +02:00
Dawid Bepierszcz
5701455de0 Refactor database layer and add module/plugin improvements
Reworked the database layer to support both MySQL and SQLite via new provider classes and migration scripts for each backend. Updated the build workflow to support building and packaging additional modules, including StealthModule and BanSoundModule, and improved artifact handling. Refactored command registration to allow dynamic registration/unregistration and improved API event handling. Updated dependencies, project structure, and configuration checks for better reliability and extensibility. Added new language files, updated versioning, and removed obsolete files.

**⚠️ Warning: SQLite support is currently experimental.
Using this version requires reconfiguration of your database settings!
Plugin now uses UTC time. Please adjust your configurations accordingly!
**
2025-10-03 01:37:03 +02:00
Dawid Bepierszcz
b97426313b 1.7.7-alpha
- Fixed steamid only bans
- Added missing multiservermode
- Better caching
2025-05-25 01:00:14 +02:00
Dawid Bepierszcz
3ab63c05db 1.7.7-alpha-small-optimizations
- Clear bans cache on plugin reload
- Changed ip history to int
2025-05-23 03:28:10 +02:00
Dawid Bepierszcz
f654d6b085 1.7.7-alpha-connection-fix
- Fixed lag when player connecting
- Added command to force refresh ban cache (css_reloadbans)
2025-05-21 13:11:01 +02:00
Dawid Bepierszcz
676a18d9b4 1.7.7-alpha-associated-accounts (missing migration) 2025-05-21 03:13:05 +02:00
Dawid Bepierszcz
d75a092047 1.7.7-alpha-associated-accounts
- Added possibility to view player associated account (by ip)
2025-05-21 03:08:02 +02:00
Dawid Bepierszcz
d34ca64970 1.7.7-alpha-fix
- Added missing migration
- Added missing ignoredips
2025-05-21 00:38:54 +02:00
Dawid Bepierszcz
f69f1277f8 1.7.7-alpha
- Fixed (@all,@team,@lifestate) spam with discord webhook
- Currently `small` actions for ban optimizations
2025-05-20 23:51:59 +02:00
Dawid Bepierszcz
b6c876d709 1.7.6a
- Changed PawnIsAlive to LifeState
- Changed AdminCache - now it only removes flags added in the database - not all assigned to player
- Added config variable `IgnoredIps` to ignore ip check on connect, useful when proxying
2025-03-25 11:39:00 +01:00
Dawid Bepierszcz
888d6b0152 1.7.5a
- Changed api domain
- Changed link to wiki
- Added command `css_rresize` to resize player model
- Fixed saving IP addresses, from now they will not be saved if the check option in config is disabled
2025-03-09 01:01:45 +01:00
Dawid Bepierszcz
5d62c743dd AntiDLL Module Fix
Fixed banning when detecting a forbidden event
2025-02-20 01:28:28 +01:00
Dawid Bepierszcz
62b1987fde AntiDLL module
(?) Fix for AntiDLL module
2025-02-19 14:51:36 +01:00
Dawid Bepierszcz
708ae6cb90 1.7.4b
【UPDATE 1.7.4b】

**🆕  What's new and what's changed:**
- Added support for MySQL SSL (DatabaseSSlMode in config)
2025-02-19 12:27:24 +01:00
Dawid Bepierszcz
2d77e86d59 Hotfix for AntiDLL module 2025-02-18 20:18:16 +01:00
Dawid Bepierszcz
babcbc2119 1.7.4a
【UPDATE 1.7.4a】

**🆕  What's new and what's changed:**
- Exposed event `OnAdminShowActivity` - fire when the function of informing players of admin action is performed
- Exposed `ShowAdminActivity`
- Added module to integration with AntiDLL
- Added module to integration with Redis (plugin send info about penalties between all servers)
2025-02-18 20:11:05 +01:00
Dawid Bepierszcz
64e5f1156e 1.7.3a
【UPDATE 1.7.3a】

🆕  What's new and what's changed:
- Fixed problem when you can't add admin because of the same group names on multiple servers from now on it takes in order -> server id and if there is no -> global
- Added ability to add penalties via SteamID
- Added ability to display penalty messages only for admins (ShowActivityType = 3)
- Reverted OnClientConnect to EventPlayerFullConnect
2025-02-09 15:37:01 +01:00
Dawid Bepierszcz
8cc0398f6b 1.7.2c
- Fixed no permissions in menu and perm penalty (due to authorization issue on steam)
- Fixed non-async action in async method
2025-01-26 02:10:33 +01:00
Dawid Bepierszcz
ab14956ae5 1.7.2b
- Revert temp fix for kick
2025-01-16 03:28:17 +01:00
Dawid Bepierszcz
9163caeb0d Update Helper.cs 2025-01-15 13:09:27 +01:00
Dawid Bepierszcz
f2e4b84b29 1.7.2a
- Temp fix for player.Disconnect
- Fix for no reasons
- Fix for no time used
- Added check for hibernation
- Some changes in unwarn action
2025-01-15 12:56:03 +01:00
Dawid Bepierszcz
3f1b6b3bf7 1.7.1a
【UPDATE 1.7.1a】

🆕  What's new and what's changed:
- Fixed 2x player lock text
- Added immunity check when using css_addX
- MenuManagerCS2 has been updated
- Added return of lock ID by api (check example_module)
- Fixed reasons for locks, no longer need to use `“”` if there is a space in them
- Improved handling of lock times, from now on you can use:
-- 1mo - 1 month
-- 1w - 1 week
-- 1d - 1 day
-- 1h - 1 hour
-- 1m - 1 minute
-- 1 - 1 minute
You can also combine it, e.g:
10d5h - 10 days and 5 hours
2w3d - 2 weeks and 3 days
1w1d1h1 - 1 week, 1 day and 1 minute
So for example css_ban player 10d5h Super reason for my ban
- Added a `rcon_password` column in the `sa_servers` table that populates with the rcon password
- Fixed `banid` and banning
- Fixed too fast kick (it was caused by checking players online)

⚠️  **Remember to update all files + api**
2025-01-08 19:24:47 +01:00
Dawid Bepierszcz
8af805632a 1.7.0a
- Fixed css_warn (unfreeze player after 5s)
- Fixed server loading from database (reduced delay)
- Added checking the player in an earlier phase of the connection
- Fixed admin name when using action from menu
2024-12-16 01:55:38 +01:00
Dawid Bepierszcz
8c94a867d3 Merge branch 'main' of https://github.com/daffyyyy/CS2-SimpleAdmin 2024-11-26 23:37:52 +01:00
Dawid Bepierszcz
023e1a031b 1.6.9c
- Added config variable to enable/disable checking for banned multiaccounts by ip `CheckMultiAccountsByIp`
- Fixed css_speed after player hurt
- Finally fixed vote kick (callvote)
- Small fix for admins loading from database
2024-11-26 23:37:49 +01:00
Dawid Bepierszcz
1f1c214357 Merge pull request #183 from 1370533448/patch-1
Update zh-Hans.json
2024-11-26 13:09:00 +01:00
Dawid Bepierszcz
2defb2fe14 Hotfix - server loading
- Fix for server loading
2024-11-22 23:01:31 +01:00
Ktm
5668c0ad7b Update zh-Hans.json
Incorrect translation of time and reason leads to incorrect display
2024-11-17 03:41:30 +08:00
171 changed files with 14725 additions and 3035 deletions

View File

@@ -1,6 +1,7 @@
name: Build
name: Build and Publish
on:
workflow_dispatch:
push:
branches: [ "main" ]
paths-ignore:
@@ -11,85 +12,111 @@ on:
- '**/README.md'
env:
BUILD_NUMBER: ${{ github.run_number }}
PROJECT_PATH_CS2_SIMPLEADMIN: "CS2-SimpleAdmin/CS2-SimpleAdmin.csproj"
PROJECT_PATH_CS2_SIMPLEADMINAPI: "CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj"
PROJECT_NAME_CS2_SIMPLEADMIN: "CS2-SimpleAdmin"
PROJECT_PATH_CS2_SIMPLEADMINAPI: "CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj"
PROJECT_NAME_CS2_SIMPLEADMINAPI: "CS2-SimpleAdminApi"
PROJECT_PATH_STEALTHMODULE: "Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.csproj"
PROJECT_NAME_STEALTHMODULE: "CS2-SimpleAdmin_StealthModule"
OUTPUT_PATH: "./counterstrikesharp"
TMP_PATH: "./tmp"
jobs:
build:
permissions: write-all
runs-on: ubuntu-latest
permissions: write-all
outputs:
build_version: ${{ steps.get_version.outputs.VERSION }}
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore CS2-SimpleAdmin
run: dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }}
- name: Build CS2-SimpleAdmin
run: dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}
- name: Restore CS2-SimpleAdminApi
run: dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }}
- name: Build CS2-SimpleAdminApi
run: dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Get Version
id: get_version
run: echo "VERSION=$(cat CS2-SimpleAdmin/VERSION)" >> $GITHUB_OUTPUT
- name: Restore & Build All Projects
run: |
dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }}
dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}
dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }}
dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}
dotnet restore ${{ env.PROJECT_PATH_STEALTHMODULE }}
dotnet build ${{ env.PROJECT_PATH_STEALTHMODULE }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_STEALTHMODULE }}
- name: Combine projects
run: |
mkdir -p ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin
mkdir -p ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin_StealthModule
mkdir -p ${{ env.OUTPUT_PATH }}/shared/CS2-SimpleAdminApi
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/* ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin/
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_STEALTHMODULE }}/* ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin_StealthModule
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/* ${{ env.OUTPUT_PATH }}/shared/CS2-SimpleAdminApi/
- name: Zip Main Build Output
run: zip -r CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip ${{ env.OUTPUT_PATH }}
- name: Extract & Zip StatusBlocker Linux
run: |
mkdir -p statusblocker-linux &&
tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-linux.tar.gz -C statusblocker-linux &&
cd statusblocker-linux &&
zip -r ../StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip ./*
- name: Extract & Zip StatusBlocker Windows
run: |
mkdir -p statusblocker-windows &&
tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-windows.tar.gz -C statusblocker-windows &&
cd statusblocker-windows &&
zip -r ../StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip ./*
- name: Upload all artifacts
uses: actions/upload-artifact@v4
with:
name: CS2-SimpleAdmin-Build-Artifacts
path: |
CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip
StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip
StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip
publish:
if: github.event_name == 'push'
permissions: write-all
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: write-all
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore CS2-SimpleAdmin
run: dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }}
- name: Build CS2-SimpleAdmin
run: dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}
- name: Restore CS2-SimpleAdminApi
run: dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }}
- name: Build CS2-SimpleAdminApi
run: dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}
- name: Clean files
run: |
rm -f \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/CounterStrikeSharp.API.dll \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/McMaster.NETCore.Plugins.dll \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/Microsoft.DotNet.PlatformAbstractions.dll \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/Microsoft.Extensions.DependencyModel.dll \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/CS2-SimpleAdminApi.* \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/Microsoft.* \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/CounterStrikeSharp.API.dll \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/McMaster.NETCore.Plugins.dll \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/Microsoft.DotNet.PlatformAbstractions.dll \
${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/Microsoft.Extensions.DependencyModel.dll
- name: Combine projects
run: |
mkdir -p ${{ env.OUTPUT_PATH }}/plugins
mkdir -p ${{ env.OUTPUT_PATH }}/shared
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }} ${{ env.OUTPUT_PATH }}/plugins/
cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }} ${{ env.OUTPUT_PATH }}/shared/
- name: Zip combined
uses: thedoctor0/zip-release@0.7.6
with:
type: 'zip'
filename: '${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}-${{ env.BUILD_NUMBER }}.zip'
path: ${{ env.OUTPUT_PATH }}
- name: Publish combined release
uses: ncipollo/release-action@v1.14.0
with:
artifacts: "${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}-${{ env.BUILD_NUMBER }}.zip"
name: "CS2-SimpleAdmin - Build ${{ env.BUILD_NUMBER }}"
tag: "build-${{ env.BUILD_NUMBER }}"
body: |
Place files in addons/counterstrikesharp
After first launch, configure the plugins in the respective configs:
- CS2-SimpleAdmin: addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json
- uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: CS2-SimpleAdmin-Build-Artifacts
path: .
- name: Unzip main build artifact
run: unzip CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}.zip -d ./counterstrikesharp
- name: Publish combined release
uses: ncipollo/release-action@v1.14.0
with:
artifacts: |
CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}.zip
StatusBlocker-linux-${{ needs.build.outputs.build_version }}.zip
StatusBlocker-windows-${{ needs.build.outputs.build_version }}.zip
name: "CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}"
tag: "build-${{ needs.build.outputs.build_version }}"
body: |
Place the files in your server as follows:
- CS2-SimpleAdmin:
Place the files inside the addons/counterstrikesharp directory.
After the first launch, configure the plugin using the JSON config file at:
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json
- StatusBlocker:
Place the plugin files directly into the addons directory.
This plugin is a Metamod module for the StealthModule and does not require a subfolder.
Remember to restart or reload your game server after installing and configuring the plugins.

8
.gitignore vendored
View File

@@ -3,4 +3,10 @@ obj/
.vs/
.git
.vscode/
.idea/
.idea/
Modules/CS2-SimpleAdmin_PlayTimeModule
CS2-SimpleAdmin.sln.DotSettings.user
Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.sln.DotSettings.user
CS2-SimpleAdmin_BanSoundModule — kopia
*.user
CLAUDE.md

Binary file not shown.

View File

@@ -1,40 +1,57 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Localization;
namespace CS2_SimpleAdmin.Api;
public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
{
public event Action? OnSimpleAdminReady;
public void OnSimpleAdminReadyEvent() => OnSimpleAdminReady?.Invoke();
public PlayerInfo GetPlayerInfo(CCSPlayerController player)
{
if (!player.UserId.HasValue)
throw new KeyNotFoundException("Player with specific UserId not found");
return CS2_SimpleAdmin.PlayersInfo[player.UserId.Value];
return !player.UserId.HasValue
? throw new KeyNotFoundException("Player with specific UserId not found")
: CS2_SimpleAdmin.PlayersInfo[player.SteamID];
}
public string GetConnectionString() => CS2_SimpleAdmin.Instance.DbConnectionString;
public string GetServerAddress() => CS2_SimpleAdmin.IpAddress;
public int? GetServerId() => CS2_SimpleAdmin.ServerId;
public Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetPlayerMuteStatus(CCSPlayerController player)
public string GetConnectionString() => CS2_SimpleAdmin.Instance.DbConnectionString;
public string GetServerAddress() => CS2_SimpleAdmin.IpAddress;
public int? GetServerId() => CS2_SimpleAdmin.ServerId;
public Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetPlayerMuteStatus(
CCSPlayerController player)
{
return PlayerPenaltyManager.GetAllPlayerPenalties(player.Slot);
}
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?>? OnPlayerPenaltied;
public event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?>? OnPlayerPenaltiedAdded;
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltied;
public event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltiedAdded;
public event Action<string, string?, bool, object>? OnAdminShowActivity;
public event Action<int, bool>? OnAdminToggleSilent;
public void OnPlayerPenaltiedEvent(PlayerInfo player, PlayerInfo? admin, PenaltyType penaltyType, string reason,
int duration = -1) => OnPlayerPenaltied?.Invoke(player, admin, penaltyType, reason, duration, CS2_SimpleAdmin.ServerId);
public void OnPlayerPenaltiedAddedEvent(SteamID player, PlayerInfo? admin, PenaltyType penaltyType, string reason,
int duration) => OnPlayerPenaltiedAdded?.Invoke(player, admin, penaltyType, reason, duration, CS2_SimpleAdmin.ServerId);
int duration, int? penaltyId) => OnPlayerPenaltied?.Invoke(player, admin, penaltyType, reason, duration,
penaltyId, CS2_SimpleAdmin.ServerId);
public void IssuePenalty(CCSPlayerController player, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1)
public void OnPlayerPenaltiedAddedEvent(SteamID player, PlayerInfo? admin, PenaltyType penaltyType, string reason,
int duration, int? penaltyId) => OnPlayerPenaltiedAdded?.Invoke(player, admin, penaltyType, reason, duration,
penaltyId, CS2_SimpleAdmin.ServerId);
public void OnAdminShowActivityEvent(string messageKey, string? callerName = null, bool dontPublish = false,
params object[] messageArgs) => OnAdminShowActivity?.Invoke(messageKey, callerName, dontPublish, messageArgs);
public void OnAdminToggleSilentEvent(int slot, bool status) => OnAdminToggleSilent?.Invoke(slot, status);
public void IssuePenalty(CCSPlayerController player, CCSPlayerController? admin, PenaltyType penaltyType,
string reason, int duration = -1)
{
switch (penaltyType)
{
@@ -73,6 +90,41 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
}
}
public void IssuePenalty(SteamID steamid, CCSPlayerController? admin, PenaltyType penaltyType, string reason,
int duration = -1)
{
switch (penaltyType)
{
case PenaltyType.Ban:
{
CS2_SimpleAdmin.Instance.AddBan(admin, steamid, duration, reason);
break;
}
case PenaltyType.Gag:
{
CS2_SimpleAdmin.Instance.AddGag(admin, steamid, duration, reason);
break;
}
case PenaltyType.Mute:
{
CS2_SimpleAdmin.Instance.AddMute(admin, steamid, duration, reason);
break;
}
case PenaltyType.Silence:
{
CS2_SimpleAdmin.Instance.AddSilence(admin, steamid, duration, reason);
break;
}
case PenaltyType.Warn:
{
CS2_SimpleAdmin.Instance.AddWarn(admin, steamid, duration, reason);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
}
}
public void LogCommand(CCSPlayerController? caller, string command)
{
Helper.LogCommand(caller, command);
@@ -82,9 +134,184 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
{
Helper.LogCommand(caller, command);
}
public bool IsAdminSilent(CCSPlayerController player)
{
return CS2_SimpleAdmin.SilentPlayers.Contains(player.Slot);
}
public HashSet<int> ListSilentAdminsSlots()
{
return CS2_SimpleAdmin.SilentPlayers;
}
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))
{
list = new List<CommandDefinition>();
RegisterCommands._commandDefinitions[name] = list;
}
list.Add(definition);
}
public void UnRegisterCommand(string commandName)
{
var definitions = RegisterCommands._commandDefinitions[commandName];
if (definitions.Count == 0)
return;
foreach (var definition in definitions)
{
CS2_SimpleAdmin.Instance.RemoveCommand(commandName, definition.Callback);
}
}
public TargetResult? GetTarget(CommandInfo command)
{
return CS2_SimpleAdmin.GetTarget(command);
}
public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false,
params object[] messageArgs)
{
Helper.ShowAdminActivity(messageKey, callerName, dontPublish, messageArgs);
}
public void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null,
bool dontPublish = false)
{
Helper.ShowAdminActivityTranslated(translatedMessage, callerName, dontPublish);
}
public void ShowAdminActivityLocalized(object moduleLocalizer, string messageKey, string? callerName = null,
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);
}
public void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic")
{
Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryName, permission);
}
public void RegisterMenu(string categoryId, string menuId, string menuName,
Func<CCSPlayerController, object> menuFactory, string? permission = null)
{
Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuName, BuilderFactory, permission);
return;
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);
}
});
return menuBuilder;
}
}
public void UnregisterMenu(string categoryId, string menuId)
{
Menus.MenuManager.Instance.UnregisterMenu(categoryId, menuId);
}
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;
}
public List<CCSPlayerController> GetValidPlayers()
{
return Helper.GetValidPlayers();
}
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);
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
menu.AddOption(playerName, _ =>
{
if (player.IsValid)
{
onSelect(admin, player);
}
});
}
return menu;
}
public void AddMenuOption(object menu, string name, Action<CCSPlayerController> action, bool disabled = false,
string? permission = null)
{
if (menu is not MenuBuilder menuBuilder)
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
menuBuilder.AddOption(name, action, disabled, permission);
}
public void AddSubMenu(object menu, string name, Func<CCSPlayerController, object> subMenuFactory,
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);
if (subMenu is not MenuBuilder builder)
throw new InvalidOperationException("SubMenu factory must return MenuBuilder");
return builder;
}, disabled, permission);
}
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);
}
}

View File

@@ -1,17 +1,20 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Logging;
using MySqlConnector;
namespace CS2_SimpleAdmin;
[MinimumApiVersion(286)]
[MinimumApiVersion(300)]
public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdminConfig>
{
internal static CS2_SimpleAdmin Instance { get; private set; } = new();
@@ -19,44 +22,58 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
public override string ModuleAuthor => "daffyy & Dliix66";
public override string ModuleVersion => "1.6.9a";
public override string ModuleVersion => "1.7.8-beta-1";
public override void Load(bool hotReload)
{
Instance = this;
RegisterEvents();
if (hotReload)
{
ServerLoaded = false;
OnGameServerSteamAPIActivated();
_serverLoading = false;
CacheManager?.Dispose();
CacheManager = new CacheManager();
// OnGameServerSteamAPIActivated();
OnMapStart(string.Empty);
AddTimer(2.0f, () =>
AddTimer(6.0f, () =>
{
if (Database == null) return;
if (DatabaseProvider == null) return;
var playerManager = new PlayerManager();
Helper.GetValidPlayers().ForEach(player =>
PlayersInfo.Clear();
CachedPlayers.Clear();
BotPlayers.Clear();
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsHLTV).ToArray())
{
playerManager.LoadPlayerData(player);
});
if (!player.IsBot)
PlayerManager.LoadPlayerData(player, true);
else
BotPlayers.Add(player);
};
});
PlayersTimer?.Kill();
PlayersTimer = null;
}
_cBasePlayerControllerSetPawnFunc = new MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>(GameData.GetSignature("CBasePlayerController_SetPawn"));
SimpleAdminApi = new Api.CS2_SimpleAdminApi();
Capabilities.RegisterPluginCapability(ICS2_SimpleAdminApi.PluginCapability, () => SimpleAdminApi);
new PlayerManager().CheckPlayersTimer();
PlayersTimer?.Kill();
PlayersTimer = null;
PlayerManager.CheckPlayersTimer();
Menus.MenuManager.Instance.InitializeDefaultCategories();
BasicMenu.Initialize();
}
public override void OnAllPluginsLoaded(bool hotReload)
{
AddTimer(3.0f, () => ReloadAdmins(null));
try
{
MenuApi = MenuCapability.Get();
@@ -64,50 +81,141 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
catch (Exception ex)
{
Logger.LogError("Unable to load required plugins ... \n{exception}", ex.Message);
Unload(false);
}
AddTimer(6.0f, () => ReloadAdmins(null));
RegisterEvents();
AddTimer(0.5f, RegisterCommands.InitializeCommands);
if (!CoreConfig.UnlockConCommands)
{
_logger?.LogError(
$"⚠️ Warning: 'UnlockConCommands' is disabled in core.json. " +
$"Players will not be automatically banned when kicked and will be able " +
$"to rejoin the server for 60 seconds. " +
$"To enable instant banning, set 'UnlockConCommands': true"
);
_logger?.LogError(
$"⚠️ Warning: 'UnlockConCommands' is disabled in core.json. " +
$"Players will not be automatically banned when kicked and will be able " +
$"to rejoin the server for 60 seconds. " +
$"To enable instant banning, set 'UnlockConCommands': true"
);
}
RegisterCommands.InitializeCommands();
}
public void OnConfigParsed(CS2_SimpleAdminConfig config)
{
Instance = this;
_logger = Logger;
if (System.Diagnostics.Debugger.IsAttached)
Environment.FailFast(":(!");
if (config.DatabaseHost.Length < 1 || config.DatabaseName.Length < 1 || config.DatabaseUser.Length < 1)
Helper.UpdateConfig(config);
_logger = Logger;
Config = config;
bool missing = false;
var cssPath = Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp");
var pluginsPath = Path.Combine(cssPath, "plugins");
var sharedPath = Path.Combine(cssPath, "shared");
foreach (var plugin in _requiredPlugins)
{
throw new Exception("[CS2-SimpleAdmin] You need to setup Database credentials in config!");
var pluginDirPath = Path.Combine(pluginsPath, plugin);
var pluginDllPath = Path.Combine(pluginDirPath, $"{plugin}.dll");
if (!Directory.Exists(pluginDirPath))
{
_logger?.LogError(
$"❌ Plugin directory '{plugin}' missing at: {pluginDirPath}"
);
missing = true;
}
if (!File.Exists(pluginDllPath))
{
_logger?.LogError(
$"❌ Plugin DLL '{plugin}.dll' missing at: {pluginDllPath}"
);
missing = true;
}
}
foreach (var shared in _requiredShared)
{
var sharedDirPath = Path.Combine(sharedPath, shared);
var sharedDllPath = Path.Combine(sharedDirPath, $"{shared}.dll");
if (!Directory.Exists(sharedDirPath))
{
_logger?.LogError(
$"❌ Shared library directory '{shared}' missing at: {sharedDirPath}"
);
missing = true;
}
if (!File.Exists(sharedDllPath))
{
_logger?.LogError(
$"❌ Shared library DLL '{shared}.dll' missing at: {sharedDllPath}"
);
missing = true;
}
}
MySqlConnectionStringBuilder builder = new()
if (missing)
Server.ExecuteCommand($"css_plugins unload {ModuleName}");
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))
{
Server = config.DatabaseHost,
Database = config.DatabaseName,
UserID = config.DatabaseUser,
Password = config.DatabasePassword,
Port = (uint)config.DatabasePort,
throw new Exception("[CS2-SimpleAdmin] You need to setup MySQL credentials in config!");
}
var 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 = Enum.TryParse(config.DatabaseConfig.DatabaseSSlMode, true, out MySqlSslMode sslMode)
? sslMode
: MySqlSslMode.Preferred,
Pooling = true,
MinimumPoolSize = 0,
MaximumPoolSize = 640,
};
DbConnectionString = builder.ConnectionString;
Database = new Database.Database(DbConnectionString);
if (!Database.CheckDatabaseConnection(out var exception))
DatabaseProvider = new MySqlDatabaseProvider(DbConnectionString);
}
else
{
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.SqliteFilePath))
{
if (exception != null)
Logger.LogError("Problem with database connection! \n{exception}", exception);
Unload(false);
return;
throw new Exception("[CS2-SimpleAdmin] You need to specify SQLite file path in config!");
}
Task.Run(() => Database.DatabaseMigration());
Config = config;
Helper.UpdateConfig(config);
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");
@@ -118,33 +226,46 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
if (!string.IsNullOrEmpty(Config.Discord.DiscordLogWebhook))
DiscordWebhookClientLog = new DiscordManager(Config.Discord.DiscordLogWebhook);
PluginInfo.ShowAd(ModuleVersion);
if (Config.EnableUpdateCheck)
Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, _logger));
Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, Logger));
PermissionManager = new PermissionManager(Database);
BanManager = new BanManager(Database);
MuteManager = new MuteManager(Database);
WarnManager = new WarnManager(Database);
PermissionManager = new PermissionManager(DatabaseProvider);
BanManager = new BanManager(DatabaseProvider);
MuteManager = new MuteManager(DatabaseProvider);
WarnManager = new WarnManager(DatabaseProvider);
}
private static TargetResult? GetTarget(CommandInfo command)
internal static TargetResult? GetTarget(CommandInfo command, int argument = 1)
{
var matches = command.GetArgTargetResult(1);
var matches = command.GetArgTargetResult(argument);
if (!matches.Any())
{
command.ReplyToCommand($"Target {command.GetArg(1)} not found.");
command.ReplyToCommand($"Target {command.GetArg(argument)} not found.");
return null;
}
if (command.GetArg(1).StartsWith('@'))
if (command.GetArg(argument).StartsWith('@'))
return matches;
if (matches.Count() == 1)
return matches;
command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(1)}\".");
command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(argument)}\".");
return null;
}
public override void Unload(bool hotReload)
{
CacheManager?.Dispose();
CacheManager = null;
PlayersTimer?.Kill();
PlayersTimer = null;
UnregisterEvents();
if (hotReload)
PlayersInfo.Clear();
else
Server.ExecuteCommand($"css_plugins unload {ModuleDirectory}");
}
}

View File

@@ -7,25 +7,132 @@
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.287" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<PublishTrimmed>true</PublishTrimmed>
<DebuggerSupport>false</DebuggerSupport>
<!-- <GenerateDependencyFile>false</GenerateDependencyFile>-->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.340">
<PrivateAssets>none</PrivateAssets>
<ExcludeAssets>runtime</ExcludeAssets>
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="MySqlConnector" Version="2.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="*" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Linq.Async" Version="6.0.3" />
<PackageReference Include="ZLinq" Version="1.5.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CS2-SimpleAdminApi\CS2-SimpleAdminApi.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
<None Update="Database\Migrations\010_CreateWarnsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
<None Update="Database\Migrations\Mysql\001_CreateTables.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\002_CreateFlagsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\003_ChangeColumnsPosition.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\004_MoveOldFlagsToFlagsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\005_CreateUnbansTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\006_ServerGroupsFeature.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\007_ServerGroupsGlobal.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\008_OnlineTimeInPenalties.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\009_BanAllUsedIpAddress.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\010_CreateWarnsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\011_AddRconColumnToServersTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\012_AddUpdatedAtColumnToSaBansTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\013_AddNameColumnToSaPlayerIpsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\014_AddIndexesToMutesTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\015_steamidToBigInt.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Mysql\016_OptimizeTablesAndIndexes.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\001_CreateTables.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\002_CreateFlagsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\003_ChangeColumnsPosition.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\004_MoveOldFlagsToFlagsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\005_CreateUnbansTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\006_ServerGroupsFeature.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\007_ServerGroupsGlobal.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\008_OnlineTimeInPenalties.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\009_BanAllUsedIpAddress.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\010_CreateWarnsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\011_AddRconColumnToServersTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\012_AddUpdatedAtColumnToSaBansTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\013_AddNameColumnToSaPlayerIpsTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\014_AddIndexesToMutesTable.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\015_steamidToBigInt.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Database\Migrations\Sqlite\016_OptimizeTablesAndIndexes.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Update="Database\Migrations\*.sql" CopyToOutputDirectory="PreserveNewest" />
@@ -38,7 +145,8 @@
<ItemGroup>
<Reference Include="MenuManagerApi">
<HintPath>3rd_party\MenuManagerApi.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View File

@@ -1,86 +1,86 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CS2_SimpleAdmin;
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 =
[
new CommandMapping("css_ban", CS2_SimpleAdmin.Instance.OnBanCommand),
new CommandMapping("css_addban", CS2_SimpleAdmin.Instance.OnAddBanCommand),
new CommandMapping("css_banip", CS2_SimpleAdmin.Instance.OnBanIpCommand),
new CommandMapping("css_unban", CS2_SimpleAdmin.Instance.OnUnbanCommand),
new CommandMapping("css_warn", CS2_SimpleAdmin.Instance.OnWarnCommand),
new CommandMapping("css_unwarn", CS2_SimpleAdmin.Instance.OnUnwarnCommand),
new("css_ban", CS2_SimpleAdmin.Instance.OnBanCommand),
new("css_addban", CS2_SimpleAdmin.Instance.OnAddBanCommand),
new("css_banip", CS2_SimpleAdmin.Instance.OnBanIpCommand),
new("css_unban", CS2_SimpleAdmin.Instance.OnUnbanCommand),
new("css_warn", CS2_SimpleAdmin.Instance.OnWarnCommand),
new("css_unwarn", CS2_SimpleAdmin.Instance.OnUnwarnCommand),
new CommandMapping("css_asay", CS2_SimpleAdmin.Instance.OnAdminToAdminSayCommand),
new CommandMapping("css_cssay", CS2_SimpleAdmin.Instance.OnAdminCustomSayCommand),
new CommandMapping("css_say", CS2_SimpleAdmin.Instance.OnAdminSayCommand),
new CommandMapping("css_psay", CS2_SimpleAdmin.Instance.OnAdminPrivateSayCommand),
new CommandMapping("css_csay", CS2_SimpleAdmin.Instance.OnAdminCenterSayCommand),
new CommandMapping("css_hsay", CS2_SimpleAdmin.Instance.OnAdminHudSayCommand),
new("css_asay", CS2_SimpleAdmin.Instance.OnAdminToAdminSayCommand),
new("css_cssay", CS2_SimpleAdmin.Instance.OnAdminCustomSayCommand),
new("css_say", CS2_SimpleAdmin.Instance.OnAdminSayCommand),
new("css_psay", CS2_SimpleAdmin.Instance.OnAdminPrivateSayCommand),
new("css_csay", CS2_SimpleAdmin.Instance.OnAdminCenterSayCommand),
new("css_hsay", CS2_SimpleAdmin.Instance.OnAdminHudSayCommand),
new CommandMapping("css_penalties", CS2_SimpleAdmin.Instance.OnPenaltiesCommand),
new CommandMapping("css_admin", CS2_SimpleAdmin.Instance.OnAdminCommand),
new CommandMapping("css_adminhelp", CS2_SimpleAdmin.Instance.OnAdminHelpCommand),
new CommandMapping("css_addadmin", CS2_SimpleAdmin.Instance.OnAddAdminCommand),
new CommandMapping("css_deladmin", CS2_SimpleAdmin.Instance.OnDelAdminCommand),
new CommandMapping("css_addgroup", CS2_SimpleAdmin.Instance.OnAddGroup),
new CommandMapping("css_delgroup", CS2_SimpleAdmin.Instance.OnDelGroupCommand),
new CommandMapping("css_reloadadmins", CS2_SimpleAdmin.Instance.OnRelAdminCommand),
new CommandMapping("css_hide", CS2_SimpleAdmin.Instance.OnHideCommand),
new CommandMapping("css_hidecomms", CS2_SimpleAdmin.Instance.OnHideCommsCommand),
new CommandMapping("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand),
new CommandMapping("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand),
new CommandMapping("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand),
new CommandMapping("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand),
new CommandMapping("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand),
new CommandMapping("css_map", CS2_SimpleAdmin.Instance.OnMapCommand),
new CommandMapping("css_wsmap", CS2_SimpleAdmin.Instance.OnWorkshopMapCommand),
new CommandMapping("css_cvar", CS2_SimpleAdmin.Instance.OnCvarCommand),
new CommandMapping("css_rcon", CS2_SimpleAdmin.Instance.OnRconCommand),
new CommandMapping("css_rr", CS2_SimpleAdmin.Instance.OnRestartCommand),
new("css_penalties", CS2_SimpleAdmin.Instance.OnPenaltiesCommand),
new("css_admin", CS2_SimpleAdmin.Instance.OnAdminCommand),
new("css_adminhelp", CS2_SimpleAdmin.Instance.OnAdminHelpCommand),
new("css_addadmin", CS2_SimpleAdmin.Instance.OnAddAdminCommand),
new("css_deladmin", CS2_SimpleAdmin.Instance.OnDelAdminCommand),
new("css_addgroup", CS2_SimpleAdmin.Instance.OnAddGroup),
new("css_delgroup", CS2_SimpleAdmin.Instance.OnDelGroupCommand),
new("css_reloadadmins", CS2_SimpleAdmin.Instance.OnRelAdminCommand),
new("css_reloadbans", CS2_SimpleAdmin.Instance.OnRelBans),
new("css_hide", CS2_SimpleAdmin.Instance.OnHideCommand),
new("css_hidecomms", CS2_SimpleAdmin.Instance.OnHideCommsCommand),
new("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand),
new("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand),
new("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand),
new("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand),
new("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand),
new("css_map", CS2_SimpleAdmin.Instance.OnMapCommand),
new("css_wsmap", CS2_SimpleAdmin.Instance.OnWorkshopMapCommand),
new("css_cvar", CS2_SimpleAdmin.Instance.OnCvarCommand),
new("css_rcon", CS2_SimpleAdmin.Instance.OnRconCommand),
new("css_rr", CS2_SimpleAdmin.Instance.OnRestartCommand),
new CommandMapping("css_gag", CS2_SimpleAdmin.Instance.OnGagCommand),
new CommandMapping("css_addgag", CS2_SimpleAdmin.Instance.OnAddGagCommand),
new CommandMapping("css_ungag", CS2_SimpleAdmin.Instance.OnUngagCommand),
new CommandMapping("css_mute", CS2_SimpleAdmin.Instance.OnMuteCommand),
new CommandMapping("css_addmute", CS2_SimpleAdmin.Instance.OnAddMuteCommand),
new CommandMapping("css_unmute", CS2_SimpleAdmin.Instance.OnUnmuteCommand),
new CommandMapping("css_silence", CS2_SimpleAdmin.Instance.OnSilenceCommand),
new CommandMapping("css_addsilence", CS2_SimpleAdmin.Instance.OnAddSilenceCommand),
new CommandMapping("css_unsilence", CS2_SimpleAdmin.Instance.OnUnsilenceCommand),
new("css_gag", CS2_SimpleAdmin.Instance.OnGagCommand),
new("css_addgag", CS2_SimpleAdmin.Instance.OnAddGagCommand),
new("css_ungag", CS2_SimpleAdmin.Instance.OnUngagCommand),
new("css_mute", CS2_SimpleAdmin.Instance.OnMuteCommand),
new("css_addmute", CS2_SimpleAdmin.Instance.OnAddMuteCommand),
new("css_unmute", CS2_SimpleAdmin.Instance.OnUnmuteCommand),
new("css_silence", CS2_SimpleAdmin.Instance.OnSilenceCommand),
new("css_addsilence", CS2_SimpleAdmin.Instance.OnAddSilenceCommand),
new("css_unsilence", CS2_SimpleAdmin.Instance.OnUnsilenceCommand),
new CommandMapping("css_vote", CS2_SimpleAdmin.Instance.OnVoteCommand),
new("css_vote", CS2_SimpleAdmin.Instance.OnVoteCommand),
new CommandMapping("css_noclip", CS2_SimpleAdmin.Instance.OnNoclipCommand),
new CommandMapping("css_freeze", CS2_SimpleAdmin.Instance.OnFreezeCommand),
new CommandMapping("css_unfreeze", CS2_SimpleAdmin.Instance.OnUnfreezeCommand),
new CommandMapping("css_godmode", CS2_SimpleAdmin.Instance.OnGodCommand),
new CommandMapping("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand),
new CommandMapping("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand),
new CommandMapping("css_give", CS2_SimpleAdmin.Instance.OnGiveCommand),
new CommandMapping("css_strip", CS2_SimpleAdmin.Instance.OnStripCommand),
new CommandMapping("css_hp", CS2_SimpleAdmin.Instance.OnHpCommand),
new CommandMapping("css_speed", CS2_SimpleAdmin.Instance.OnSpeedCommand),
new CommandMapping("css_gravity", CS2_SimpleAdmin.Instance.OnGravityCommand),
new CommandMapping("css_money", CS2_SimpleAdmin.Instance.OnMoneyCommand),
new CommandMapping("css_team", CS2_SimpleAdmin.Instance.OnTeamCommand),
new CommandMapping("css_rename", CS2_SimpleAdmin.Instance.OnRenameCommand),
new CommandMapping("css_prename", CS2_SimpleAdmin.Instance.OnPrenameCommand),
new CommandMapping("css_respawn", CS2_SimpleAdmin.Instance.OnRespawnCommand),
new CommandMapping("css_tp", CS2_SimpleAdmin.Instance.OnGotoCommand),
new CommandMapping("css_bring", CS2_SimpleAdmin.Instance.OnBringCommand),
new CommandMapping("css_pluginsmanager", CS2_SimpleAdmin.Instance.OnPluginManagerCommand)
new("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand),
new("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand),
new("css_team", CS2_SimpleAdmin.Instance.OnTeamCommand),
new("css_rename", CS2_SimpleAdmin.Instance.OnRenameCommand),
new("css_prename", CS2_SimpleAdmin.Instance.OnPrenameCommand),
new("css_tp", CS2_SimpleAdmin.Instance.OnGotoCommand),
new("css_bring", CS2_SimpleAdmin.Instance.OnBringCommand),
new("css_pluginsmanager", CS2_SimpleAdmin.Instance.OnPluginManagerCommand),
new("css_adminvoice", CS2_SimpleAdmin.Instance.OnAdminVoiceCommand)
];
/// <summary>
/// Initializes command registration.
/// If the commands config file does not exist, creates it and then recurses to register commands.
/// Otherwise, directly registers commands from the configuration.
/// </summary>
public static void InitializeCommands()
{
if (!File.Exists(CommandsPath))
@@ -94,6 +94,10 @@ public static class RegisterCommands
}
}
/// <summary>
/// Creates the default commands configuration JSON file with built-in commands and aliases.
/// </summary>
[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
@@ -120,6 +124,7 @@ public static class RegisterCommands
{ "css_addgroup", new Command { Aliases = ["css_addgroup"] } },
{ "css_delgroup", new Command { Aliases = ["css_delgroup"] } },
{ "css_reloadadmins", new Command { Aliases = ["css_reloadadmins"] } },
{ "css_reloadbans", new Command { Aliases = ["css_reloadbans"] } },
{ "css_hide", new Command { Aliases = ["css_hide", "css_stealth"] } },
{ "css_hidecomms", new Command { Aliases = ["css_hidecomms"] } },
{ "css_who", new Command { Aliases = ["css_who"] } },
@@ -142,66 +147,88 @@ public static class RegisterCommands
{ "css_addsilence", new Command { Aliases = ["css_addsilence"] } },
{ "css_unsilence", new Command { Aliases = ["css_unsilence"] } },
{ "css_vote", new Command { Aliases = ["css_vote"] } },
{ "css_noclip", new Command { Aliases = ["css_noclip"] } },
{ "css_freeze", new Command { Aliases = ["css_freeze"] } },
{ "css_unfreeze", new Command { Aliases = ["css_unfreeze"] } },
{ "css_godmode", new Command { Aliases = ["css_godmode"] } },
{ "css_slay", new Command { Aliases = ["css_slay"] } },
{ "css_slap", new Command { Aliases = ["css_slap"] } },
{ "css_give", new Command { Aliases = ["css_give"] } },
{ "css_strip", new Command { Aliases = ["css_strip"] } },
{ "css_hp", new Command { Aliases = ["css_hp"] } },
{ "css_speed", new Command { Aliases = ["css_speed"] } },
{ "css_gravity", new Command { Aliases = ["css_gravity"] } },
{ "css_money", new Command { Aliases = ["css_money"] } },
{ "css_team", new Command { Aliases = ["css_team"] } },
{ "css_rename", new Command { Aliases = ["css_rename"] } },
{ "css_prename", new Command { Aliases = ["css_prename"] } },
{ "css_respawn", new Command { Aliases = ["css_respawn"] } },
{ "css_resize", new Command { Aliases = ["css_resize", "css_size"] } },
{ "css_tp", new Command { Aliases = ["css_tp", "css_tpto", "css_goto"] } },
{ "css_bring", new Command { Aliases = ["css_bring", "css_tphere"] } },
{ "css_pluginsmanager", new Command { Aliases = ["css_pluginsmanager", "css_pluginmanager"] } }
{ "css_pluginsmanager", new Command { Aliases = ["css_pluginsmanager", "css_pluginmanager"] } },
{ "css_adminvoice", new Command { Aliases = ["css_adminvoice", "css_listenall"] } }
}
};
var json = JsonConvert.SerializeObject(commands, Formatting.Indented);
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(commands, options);
File.WriteAllText(CommandsPath, json);
}
/// <summary>
/// Reads the command configuration JSON file and registers all commands and their aliases with their callbacks.
/// Also registers any custom commands previously stored.
/// </summary>
[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 = JsonConvert.DeserializeObject<CommandsConfig>(json);
var commandsConfig = JsonSerializer.Deserialize<CommandsConfig>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (commandsConfig?.Commands == null) return;
foreach (var command in commandsConfig.Commands)
if (commandsConfig?.Commands != null)
{
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)
foreach (var command in commandsConfig.Commands)
{
CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback);
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)
{
CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback);
}
}
}
}
foreach (var (name, definitions) in _commandDefinitions)
{
foreach (var 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>
private class CommandsConfig
{
public Dictionary<string, Command>? Commands { get; init; }
}
/// <summary>
/// Represents a command definition containing a list of aliases.
/// </summary>
private class Command
{
public string[]? Aliases { get; init; }
}
/// <summary>
/// Maps a command key to its respective command callback handler.
/// </summary>
private class CommandMapping(string commandKey, CommandInfo.CommandCallback callback)
{
public string CommandKey { get; } = commandKey;

View File

@@ -12,6 +12,11 @@ namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Handles the 'ban' command, allowing admins to ban one or more valid connected players.
/// </summary>
/// <param name="caller">The player issuing the ban command, or null for console.</param>
/// <param name="command">The command information including arguments.</param>
[RequiresPermissions("@css/ban")]
[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)
@@ -19,9 +24,7 @@ public partial class CS2_SimpleAdmin
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
if (command.ArgCount < 2)
return;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, IsHLTV: false }).ToList();
@@ -31,14 +34,18 @@ public partial class CS2_SimpleAdmin
return;
}
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0)
reason = command.GetArg(3);
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));
playersToTarget.ForEach(player =>
{
if (!caller.CanTarget(player)) return;
if (!int.TryParse(command.GetArg(2), out var time) && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_ban"] ?? "Ban"}: {player.PlayerName}", player,
ManagePlayersMenu.BanMenu);
@@ -49,37 +56,42 @@ public partial class CS2_SimpleAdmin
});
}
/// <summary>
/// Core logic to ban a specific player, scheduling database updates, notifications, and kicks.
/// </summary>
/// <param name="caller">The player issuing the ban, or null for console.</param>
/// <param name="player">The player to be banned.</param>
/// <param name="time">Ban duration in minutes; 0 means permanent.</param>
/// <param name="reason">Reason for the ban.</param>
/// <param name="callerName">Optional caller name string. If null, defaults to player name or console.</param>
/// <param name="banManager">Optional BanManager to handle ban persistence.</param>
/// <param name="command">Optional command info object for logging.</param>
/// <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 (Database == null || !player.IsValid || !player.UserId.HasValue) 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 ??= _localizer?["sa_console"] ?? "Console";
// Freeze player pawn if alive
if (player.PawnIsAlive)
{
player.Pawn.Value?.Freeze();
}
callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
// Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null;
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle banning logic
Task.Run(async () =>
{
await BanManager.BanPlayer(playerInfo, adminInfo, reason, time);
int? penaltyId = await BanManager.BanPlayer(playerInfo, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Ban, reason, time, penaltyId);
});
});
// Update banned players list
if (playerInfo.IpAddress != null && !BannedPlayers.Contains(playerInfo.IpAddress))
BannedPlayers.Add(playerInfo.IpAddress);
if (!BannedPlayers.Contains(player.SteamID.ToString()))
BannedPlayers.Add(player.SteamID.ToString());
// 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",
@@ -95,19 +107,13 @@ public partial class CS2_SimpleAdmin
// Display admin activity message if necessary
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Schedule a kick timer
if (player.UserId.HasValue)
{
AddTimer(Config.OtherSettings.KickTime, () =>
{
if (player is { IsValid: true, UserId: not null })
{
Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED);
}
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED, Config.OtherSettings.KickTime);
}
// Execute ban command if necessary
@@ -125,14 +131,61 @@ public partial class CS2_SimpleAdmin
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Ban, _localizer);
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Ban, reason, time);
}
/// <summary>
/// Adds a ban for a player by their SteamID, including offline bans.
/// </summary>
/// <param name="caller">The player issuing the ban command.</param>
/// <param name="steamid">SteamID of the player to ban.</param>
/// <param name="time">Ban duration in minutes (0 means permanent).</param>
/// <param name="reason">Reason for banning.</param>
/// <param name="banManager">Optional ban manager for database operations.</param>
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);
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 () =>
{
int? penaltyId = await BanManager.AddBanBySteamid(steamid.SteamId64, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Ban, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Ban, _localizer);
}
}
/// <summary>
/// Handles banning a player by specifying their SteamID via command.
/// </summary>
/// <param name="caller">The player issuing the command, or null if console.</param>
/// <param name="command">Command information including arguments (SteamID, time, reason).</param>
[RequiresPermissions("@css/ban")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
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)
@@ -141,21 +194,21 @@ public partial class CS2_SimpleAdmin
return;
}
var steamid = steamId.SteamId64.ToString();
var reason = command.ArgCount >= 3 && !string.IsNullOrEmpty(command.GetArg(3))
? command.GetArg(3)
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;
int.TryParse(command.GetArg(2), out var time);
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidBan(caller, time)) return;
var adminInfo = caller != null && caller.UserId.HasValue
? PlayersInfo[caller.UserId.Value]
? PlayersInfo[caller.SteamID]
: null;
var matches = Helper.GetPlayerFromSteamid64(steamid);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
@@ -166,13 +219,21 @@ 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 () =>
{
await BanManager.AddBanBySteamid(steamid, adminInfo, reason, time);
int? penaltyId = await BanManager.AddBanBySteamid(steamid, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Ban, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid, reason, time, PenaltyType.Ban, _localizer);
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Ban, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Ban has been added offline.");
}
@@ -181,15 +242,18 @@ public partial class CS2_SimpleAdmin
if (UnlockedCommands)
Server.ExecuteCommand($"banid 1 {steamId.SteamId3}");
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Ban, reason, time);
}
/// <summary>
/// Handles banning a player by their IP address, supporting offline banning if player is not online.
/// </summary>
/// <param name="caller">The player issuing the ban command.</param>
/// <param name="command">The command containing the IP, time, and reason arguments.</param>
[RequiresPermissions("@css/ban")]
[CommandHelper(minArgs: 1, usage: "<ip> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnBanIpCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
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);
@@ -200,26 +264,31 @@ public partial class CS2_SimpleAdmin
return;
}
var reason = command.ArgCount >= 3 && !string.IsNullOrEmpty(command.GetArg(3))
? command.GetArg(3)
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 = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
int.TryParse(command.GetArg(2), out var time);
if (!CheckValidBan(caller, time)) return;
var adminInfo = caller != null && caller.UserId.HasValue
? PlayersInfo[caller.UserId.Value]
? PlayersInfo[caller.SteamID]
: null;
var matches = Helper.GetPlayerFromIp(ipAddress);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
if (player != null && player.IsValid)
var players = Helper.GetPlayerFromIp(ipAddress);
if (players.Count >= 1)
{
if (!caller.CanTarget(player))
return;
foreach (var player in players)
{
if (player == null || !player.IsValid) continue;
if (!caller.CanTarget(player))
return;
Ban(caller, player, time, reason, callerName, silent: true);
Ban(caller, player, time, reason, callerName, silent: true);
}
}
else
{
@@ -235,13 +304,19 @@ public partial class CS2_SimpleAdmin
Helper.LogCommand(caller, command);
}
/// <summary>
/// Checks whether the ban duration is valid based on the caller's permissions and configured limits.
/// </summary>
/// <param name="caller">The player issuing the ban command.</param>
/// <param name="duration">Requested ban duration in minutes.</param>
/// <returns>True if ban duration is valid; otherwise, false.</returns>
private bool CheckValidBan(CCSPlayerController? caller, int duration)
{
if (caller == null) return true;
var canPermBan = AdminManager.PlayerHasPermissions(caller, "@css/permban");
var canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban");
if (duration <= 0 && canPermBan == false)
if (duration <= 0 && !canPermBan)
{
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}");
return false;
@@ -253,14 +328,17 @@ public partial class CS2_SimpleAdmin
return false;
}
/// <summary>
/// Handles unbanning players by pattern (steamid, name, or IP).
/// </summary>
/// <param name="caller">The player issuing the unban command.</param>
/// <param name="command">Command containing target pattern and optional reason.</param>
[RequiresPermissions("@css/unban")]
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnbanCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
if (command.GetArg(1).Length <= 1)
{
command.ReplyToCommand($"Too short pattern to search.");
@@ -268,27 +346,31 @@ public partial class CS2_SimpleAdmin
}
var pattern = command.GetArg(1);
var reason = command.GetArg(2);
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;
Task.Run(async () => await BanManager.UnbanPlayer(pattern, callerSteamId, reason));
Helper.LogCommand(caller, command);
command.ReplyToCommand($"Unbanned player with pattern {pattern}.");
}
/// <summary>
/// Handles warning players, supporting multiple targets and warning durations.
/// </summary>
/// <param name="caller">The player issuing the warn command.</param>
/// <param name="command">The command containing target, time, and reason parameters.</param>
[RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnWarnCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null)
if (DatabaseProvider == null)
return;
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
if (command.ArgCount < 2)
return;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected && !player.IsHLTV).ToList();
@@ -298,49 +380,65 @@ public partial class CS2_SimpleAdmin
return;
}
WarnManager warnManager = new(Database);
int.TryParse(command.GetArg(2), out var time);
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0)
reason = command.GetArg(3);
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;
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
Warn(caller, player, time, reason, callerName, warnManager, command);
Warn(caller, player, time, reason, callerName, command);
}
});
}
internal void Warn(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, WarnManager? warnManager = null, CommandInfo? command = null)
/// <summary>
/// Issues a warning penalty to a specific player with optional duration and reason.
/// </summary>
/// <param name="caller">The player issuing the warning.</param>
/// <param name="player">The player to warn.</param>
/// <param name="time">Duration of the warning in minutes.</param>
/// <param name="reason">Reason for the warning.</param>
/// <param name="callerName">Optional display name of the caller.</param>
/// <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 (Database == null || !player.IsValid || !player.UserId.HasValue) 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 ??= _localizer?["sa_console"] ?? "Console";
callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
// Freeze player pawn if alive
if (player.PawnIsAlive)
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
player.Pawn.Value?.Freeze();
player.PlayerPawn?.Value?.Freeze();
AddTimer(5.0f, () => player.PlayerPawn?.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
// Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null;
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle warning logic
Task.Run(async () =>
{
warnManager ??= new WarnManager(Database);
await warnManager.WarnPlayer(playerInfo, adminInfo, reason, time);
int? penaltyId = await WarnManager.WarnPlayer(playerInfo, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Warn, reason, time,
penaltyId);
});
// Check for warn thresholds and execute punish command if applicable
var totalWarns = await warnManager.GetPlayerWarnsCount(player.SteamID.ToString());
var totalWarns = await WarnManager.GetPlayerWarnsCount(player.SteamID);
if (Config.WarnThreshold.Count > 0)
{
string? punishCommand = null;
@@ -353,7 +451,7 @@ public partial class CS2_SimpleAdmin
if (!string.IsNullOrEmpty(punishCommand))
{
await Server.NextFrameAsync(() =>
await Server.NextWorldUpdateAsync(() =>
{
Server.ExecuteCommand(punishCommand.Replace("USERID", playerInfo.UserId.ToString()).Replace("STEAMID64", playerInfo.SteamId?.ToString()));
});
@@ -376,7 +474,7 @@ public partial class CS2_SimpleAdmin
// Display admin activity message if necessary
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Log the warning command
@@ -387,14 +485,86 @@ public partial class CS2_SimpleAdmin
// Send Discord notification for the warning
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Warn, _localizer);
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Warn, reason, time);
}
/// <summary>
/// Adds a warning to a player by their SteamID, including support for offline players.
/// </summary>
/// <param name="caller">The player issuing the warn command.</param>
/// <param name="steamid">SteamID of the player to warn.</param>
/// <param name="time">Warning duration in minutes.</param>
/// <param name="reason">Reason for the warning.</param>
/// <param name="warnManager">Optional warn manager instance.</param>
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;
var 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}.");
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await WarnManager.AddWarnBySteamid(steamid.SteamId64, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Warn, reason, time,
penaltyId);
});
// Check for warn thresholds and execute punish command if applicable
var totalWarns = await WarnManager.GetPlayerWarnsCount(steamid.SteamId64);
if (Config.WarnThreshold.Count > 0)
{
string? punishCommand = null;
var lastKey = Config.WarnThreshold.Keys.Max();
if (totalWarns >= lastKey)
punishCommand = Config.WarnThreshold[lastKey];
else if (Config.WarnThreshold.TryGetValue(totalWarns, out var value))
punishCommand = value;
if (!string.IsNullOrEmpty(punishCommand))
{
await Server.NextWorldUpdateAsync(() =>
{
Server.ExecuteCommand(punishCommand.Replace("STEAMID64", steamid.SteamId64.ToString()));
});
}
}
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Warn, _localizer);
}
}
/// <summary>
/// Handles removing a warning (unwarn) by a pattern string.
/// </summary>
/// <param name="caller">The player issuing the unwarn command.</param>
/// <param name="command">The command containing target pattern.</param>
[RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnwarnCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
if (command.GetArg(1).Length <= 1)
{
@@ -403,9 +573,7 @@ public partial class CS2_SimpleAdmin
}
var pattern = command.GetArg(1);
Task.Run(async () => await WarnManager.UnwarnPlayer(pattern));
Helper.LogCommand(caller, command);
command.ReplyToCommand($"Unwarned player with pattern {pattern}.");
}

View File

@@ -5,11 +5,18 @@ using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using System.Text;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Sends a chat message only to admins that have chat permission.
/// The message is encoded properly to handle UTF-8 characters.
/// </summary>
/// <param name="caller">The admin player sending the message, or null for console.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminToAdminSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -20,7 +27,7 @@ public partial class CS2_SimpleAdmin
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
foreach (var player in Helper.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(p, "@css/chat")))
.Where(p => AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
{
if (_localizer != null)
player.PrintToChat(_localizer["sa_adminchat_template_admin",
@@ -29,6 +36,11 @@ public partial class CS2_SimpleAdmin
}
}
/// <summary>
/// Sends a custom chat message to all players with color tags processed.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminCustomSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -46,6 +58,11 @@ public partial class CS2_SimpleAdmin
}
}
/// <summary>
/// Sends a chat message to all players with localization prefix and color tags handled.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -56,7 +73,6 @@ public partial class CS2_SimpleAdmin
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
foreach (var player in Helper.GetValidPlayers())
{
player.SendLocalizedMessage(_localizer,
@@ -65,6 +81,11 @@ public partial class CS2_SimpleAdmin
}
}
/// <summary>
/// Sends a private chat message from the caller to the specified target player(s).
/// </summary>
/// <param name="caller">The admin or console sending the private message.</param>
/// <param name="command">The command input containing target and message.</param>
[CommandHelper(2, "<#userid or name> <message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminPrivateSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -91,6 +112,11 @@ public partial class CS2_SimpleAdmin
command.ReplyToCommand($" Private message sent!");
}
/// <summary>
/// Broadcasts a center-screen message to all players.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminCenterSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -99,10 +125,14 @@ public partial class CS2_SimpleAdmin
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
Helper.PrintToCenterAll(utf8String.ReplaceColorTags());
}
/// <summary>
/// Sends a HUD alert message to all players.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
[CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")]
public void OnAdminHudSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -115,6 +145,6 @@ public partial class CS2_SimpleAdmin
VirtualFunctions.ClientPrintAll(
HudDestination.Alert,
utf8String.ReplaceColorTags(),
0, 0, 0, 0);
0, 0, 0, 0, 0);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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.Entities;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi;
@@ -10,15 +11,18 @@ namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Processes the 'gag' command, applying a muted penalty to target players with optional time and reason.
/// </summary>
/// <param name="caller">The player issuing the gag command or null for console.</param>
/// <param name="command">The command input containing targets, time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnGagCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
@@ -28,13 +32,18 @@ public partial class CS2_SimpleAdmin
return;
}
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0)
reason = command.GetArg(3);
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));
playersToTarget.ForEach(player =>
{
if (!caller!.CanTarget(player)) return;
if (!int.TryParse(command.GetArg(2), out var time) && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_gag"] ?? "Gag"}: {player.PlayerName}", player,
ManagePlayersMenu.GagMenu);
@@ -45,9 +54,19 @@ public partial class CS2_SimpleAdmin
});
}
/// <summary>
/// Applies the gag penalty logic to an individual player, performing permission checks, notification, and logging.
/// </summary>
/// <param name="caller">The player issuing the gag.</param>
/// <param name="player">The player to gag.</param>
/// <param name="time">Duration of the gag in minutes, 0 is permanent.</param>
/// <param name="reason">Reason for the gag.</param>
/// <param name="callerName">Optional caller name for notifications.</param>
/// <param name="command">Optional command info for logging.</param>
/// <param name="silent">If true, suppresses logging notifications.</param>
internal void Gag(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false)
{
if (Database == null || !player.IsValid || !player.UserId.HasValue) return;
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return;
@@ -55,13 +74,18 @@ public partial class CS2_SimpleAdmin
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null;
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle gag logic
Task.Run(async () =>
{
await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time);
int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Gag, reason, time,
penaltyId);
});
});
// Add penalty to the player's penalty manager
@@ -82,11 +106,11 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Increment the player's total gags count
PlayersInfo[player.UserId.Value].TotalGags++;
PlayersInfo[player.SteamID].TotalGags++;
// Log the gag command and send Discord notification
if (!silent)
@@ -98,14 +122,63 @@ public partial class CS2_SimpleAdmin
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Gag, _localizer);
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Gag, reason, time);
}
/// <summary>
/// Adds a gag penalty to a player identified by SteamID, supporting offline players.
/// </summary>
/// <param name="caller">The player issuing the command or null for console.</param>
/// <param name="steamid">SteamID of the target player.</param>
/// <param name="time">Duration in minutes (0 for permanent).</param>
/// <param name="reason">Reason for the gag.</param>
internal void AddGag(CCSPlayerController? caller, SteamID steamid, int time, string reason)
{
// 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);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Gag(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64, adminInfo, reason, time, 3);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Gag, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Gag, _localizer);
}
}
/// <summary>
/// Handles the 'addgag' command, which adds a gag penalty to a player specified by SteamID.
/// </summary>
/// <param name="caller">The player issuing the command or null for console.</param>
/// <param name="command">Command input that includes SteamID, optional time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddGagCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
// Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
@@ -120,20 +193,21 @@ public partial class CS2_SimpleAdmin
return;
}
var steamid = steamId.SteamId64.ToString();
var reason = command.ArgCount >= 3 && command.GetArg(3).Length > 0
? command.GetArg(3)
: (_localizer?["sa_unknown"] ?? "Unknown");
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;
int.TryParse(command.GetArg(2), out var time);
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidMute(caller, time)) return;
// Get player and admin info
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null;
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Attempt to match player based on SteamID
var matches = Helper.GetPlayerFromSteamid64(steamid);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
@@ -145,32 +219,48 @@ public partial class CS2_SimpleAdmin
}
else
{
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous gag operation for offline players
Task.Run(async () =>
{
await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time);
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Gag, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid, reason, time, PenaltyType.Gag, _localizer);
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Gag, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Gag has been added offline.");
}
// Log the gag command and respond to the command
Helper.LogCommand(caller, command);
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Gag, reason, time);
}
/// <summary>
/// Handles removing a gag penalty ('ungag') of a player, either by SteamID or pattern match.
/// </summary>
/// <param name="caller">The player issuing the ungag command or null for console.</param>
/// <param name="command">Command input containing SteamID or player name and optional reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUngagCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1);
var reason = command.GetArg(2);
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;
if (pattern.Length <= 1)
{
command.ReplyToCommand($"Too short pattern to search.");
@@ -182,8 +272,7 @@ public partial class CS2_SimpleAdmin
// Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{
var matches = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString());
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
if (player != null && player.IsValid)
{
@@ -207,8 +296,8 @@ public partial class CS2_SimpleAdmin
{
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Gag);
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalGags > 0)
PlayersInfo[namePlayer.UserId.Value].TotalGags--;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalGags > 0)
PlayersInfo[namePlayer.SteamID].TotalGags--;
Task.Run(async () =>
{
@@ -228,15 +317,18 @@ public partial class CS2_SimpleAdmin
}
}
/// <summary>
/// Processes the 'mute' command, applying a voice mute penalty to target players with optional time and reason.
/// </summary>
/// <param name="caller">The player issuing the mute command or null for console.</param>
/// <param name="command">The command input containing targets, time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnMuteCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
@@ -246,13 +338,18 @@ public partial class CS2_SimpleAdmin
return;
}
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0)
reason = command.GetArg(3);
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));
playersToTarget.ForEach(player =>
{
if (!caller!.CanTarget(player)) return;
if (!int.TryParse(command.GetArg(2), out var time) && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_mute"] ?? "Mute"}: {player.PlayerName}", player,
ManagePlayersMenu.MuteMenu);
@@ -263,9 +360,19 @@ public partial class CS2_SimpleAdmin
});
}
/// <summary>
/// Applies the mute penalty logic to an individual player, handling permissions, notifications, logging, and countdown timers.
/// </summary>
/// <param name="caller">The player issuing the mute.</param>
/// <param name="player">The player to mute.</param>
/// <param name="time">Duration in minutes, 0 indicates permanent mute.</param>
/// <param name="reason">Reason for the mute.</param>
/// <param name="callerName">Optional caller name for notification messages.</param>
/// <param name="command">Optional command info for logging.</param>
/// <param name="silent">If true, suppresses some logging.</param>
internal void Mute(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false)
{
if (Database == null || !player.IsValid || !player.UserId.HasValue) return;
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return;
@@ -273,8 +380,8 @@ public partial class CS2_SimpleAdmin
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null;
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Set player's voice flags to muted
player.VoiceFlags = VoiceFlags.Muted;
@@ -282,7 +389,12 @@ public partial class CS2_SimpleAdmin
// Asynchronously handle mute logic
Task.Run(async () =>
{
await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time, 1);
int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time, 1);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Mute, reason, time,
penaltyId);
});
});
// Add penalty to the player's penalty manager
@@ -303,11 +415,11 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Increment the player's total mutes count
PlayersInfo[player.UserId.Value].TotalMutes++;
PlayersInfo[player.SteamID].TotalMutes++;
// Log the mute command and send Discord notification
if (!silent)
@@ -319,14 +431,18 @@ public partial class CS2_SimpleAdmin
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Mute, _localizer);
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Mute, reason, time);
}
/// <summary>
/// Handles the 'addmute' command that adds a mute penalty to a player specified by SteamID.
/// </summary>
/// <param name="caller">The player issuing the command or null for console.</param>
/// <param name="command">Command input includes SteamID, optional time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddMuteCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
// Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
@@ -341,20 +457,21 @@ public partial class CS2_SimpleAdmin
return;
}
var steamid = steamId.SteamId64.ToString();
var reason = command.ArgCount >= 3 && command.GetArg(3).Length > 0
? command.GetArg(3)
: (_localizer?["sa_unknown"] ?? "Unknown");
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;
int.TryParse(command.GetArg(2), out var time);
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidMute(caller, time)) return;
// Get player and admin info
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null;
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Attempt to match player based on SteamID
var matches = Helper.GetPlayerFromSteamid64(steamid);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
@@ -366,32 +483,95 @@ public partial class CS2_SimpleAdmin
}
else
{
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous mute operation for offline players
Task.Run(async () =>
{
await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time, 1);
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time, 1);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Mute, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid, reason, time, PenaltyType.Mute, _localizer);
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Mute, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Mute has been added offline.");
}
// Log the mute command and respond to the command
Helper.LogCommand(caller, command);
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Mute, reason, time);
}
/// <summary>
/// Asynchronously adds a mute penalty to a player by Steam ID. Handles both online and offline players.
/// </summary>
/// <param name="caller">The admin/player issuing the mute.</param>
/// <param name="steamid">The Steam ID of the player to mute.</param>
/// <param name="time">Duration of the mute in minutes.</param>
/// <param name="reason">Reason for the mute.</param>
/// <param name="muteManager">Optional mute manager instance for handling database ops.</param>
internal void AddMute(CCSPlayerController? caller, SteamID steamid, int time, string reason, MuteManager? muteManager = 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);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Mute(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64, adminInfo, reason, time, 1);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Mute, reason, time,
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Mute, _localizer);
}
}
/// <summary>
/// Handles the unmute command - removes mute penalty from player identified by SteamID or name.
/// Can target both online and offline players.
/// </summary>
/// <param name="caller">The admin/player issuing the unmute.</param>
/// <param name="command">The command arguments including target identifier and optional reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnmuteCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1);
var reason = command.GetArg(2);
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;
if (pattern.Length <= 1)
{
command.ReplyToCommand("Too short pattern to search.");
@@ -403,8 +583,7 @@ public partial class CS2_SimpleAdmin
// Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{
var matches = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString());
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
if (player != null && player.IsValid)
{
@@ -430,8 +609,8 @@ public partial class CS2_SimpleAdmin
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Mute);
namePlayer.VoiceFlags = VoiceFlags.Normal;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalMutes > 0)
PlayersInfo[namePlayer.UserId.Value].TotalMutes--;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalMutes > 0)
PlayersInfo[namePlayer.SteamID].TotalMutes--;
Task.Run(async () =>
{
@@ -451,15 +630,19 @@ public partial class CS2_SimpleAdmin
}
}
/// <summary>
/// Issue a 'silence' penalty to a player - disables voice communication.
/// Handles online and offline players, with duration and reason specified.
/// </summary>
/// <param name="caller">The admin/player issuing the silence.</param>
/// <param name="command">Command containing target, duration, and optional reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSilenceCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
@@ -469,13 +652,18 @@ public partial class CS2_SimpleAdmin
return;
}
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0)
reason = command.GetArg(3);
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));
playersToTarget.ForEach(player =>
{
if (!caller!.CanTarget(player)) return;
if (!int.TryParse(command.GetArg(2), out var time) && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_silence"] ?? "Silence"}: {player.PlayerName}", player,
ManagePlayersMenu.SilenceMenu);
@@ -486,9 +674,19 @@ public partial class CS2_SimpleAdmin
});
}
/// <summary>
/// Applies silence logical processing for a player - updates database and notifies.
/// </summary>
/// <param name="caller">Admin/player applying the silence.</param>
/// <param name="player">Target player.</param>
/// <param name="time">Duration of silence.</param>
/// <param name="reason">Reason for silence.</param>
/// <param name="callerName">Optional name of silent admin or console.</param>
/// <param name="command">Optional command details for logging.</param>
/// <param name="silent">If true, suppresses logging notifications.</param>
internal void Silence(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false)
{
if (Database == null || !player.IsValid || !player.UserId.HasValue) return;
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return;
@@ -496,13 +694,18 @@ public partial class CS2_SimpleAdmin
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null;
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle silence logic
Task.Run(async () =>
{
await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time, 2); // Assuming 2 is the type for silence
int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time, 2);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Silence, reason, time,
penaltyId);
});
});
// Add penalty to the player's penalty manager
@@ -524,11 +727,11 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Increment the player's total silences count
PlayersInfo[player.UserId.Value].TotalSilences++;
PlayersInfo[player.SteamID].TotalSilences++;
// Log the silence command and send Discord notification
if (!silent)
@@ -540,14 +743,19 @@ public partial class CS2_SimpleAdmin
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Silence, _localizer);
SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Silence, reason, time);
}
/// <summary>
/// Handles the 'AddSilence' command, applying a silence penalty to a player specified by SteamID,
/// with support for offline player penalties.
/// </summary>
/// <param name="caller">The player/admin issuing the command.</param>
/// <param name="command">The command input containing SteamID, optional time, and reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddSilenceCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
// Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
@@ -562,20 +770,21 @@ public partial class CS2_SimpleAdmin
return;
}
var steamid = steamId.SteamId64.ToString();
var reason = command.ArgCount >= 3 && command.GetArg(3).Length > 0
? command.GetArg(3)
: (_localizer?["sa_unknown"] ?? "Unknown");
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;
int.TryParse(command.GetArg(2), out var time);
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidMute(caller, time)) return;
// Get player and admin info
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null;
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Attempt to match player based on SteamID
var matches = Helper.GetPlayerFromSteamid64(steamid);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
@@ -587,45 +796,107 @@ public partial class CS2_SimpleAdmin
}
else
{
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous silence operation for offline players
Task.Run(async () =>
{
await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time, 2);
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time, 2);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Silence, reason,
time, penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid, reason, time, PenaltyType.Silence, _localizer);
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Silence, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Silence has been added offline.");
}
// Log the silence command and respond to the command
Helper.LogCommand(caller, command);
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Silence, reason, time);
}
/// <summary>
/// Adds a silence penalty to a player by Steam ID. Manages both online and offline player cases.
/// </summary>
/// <param name="caller">Admin/player initiating the silence.</param>
/// <param name="steamid">Steam ID of player.</param>
/// <param name="time">Duration of silence.</param>
/// <param name="reason">Reason for the penalty.</param>
/// <param name="muteManager">Optional mute manager for DB operations.</param>
internal void AddSilence(CCSPlayerController? caller, SteamID steamid, int time, string reason, MuteManager? muteManager = 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);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
return;
Silence(caller, player, time, reason, callerName, silent: true);
}
else
{
if (!caller.CanTarget(steamid))
return;
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64, adminInfo, reason, time, 2);
await Server.NextWorldUpdateAsync(() =>
{
SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Silence, reason,
time, penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Silence, _localizer);
}
}
/// <summary>
/// Removes the silence penalty from a player, either by SteamID, name, or offline pattern.
/// Resets voice settings and updates notices accordingly.
/// </summary>
/// <param name="caller">Admin/player issuing the unsilence.</param>
/// <param name="command">Command arguments with target pattern and optional reason.</param>
[RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnsilenceCommand(CCSPlayerController? caller, CommandInfo command)
{
if (Database == null) return;
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1);
var reason = command.GetArg(2);
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;
if (pattern.Length <= 1)
{
command.ReplyToCommand("Too short pattern to search.");
return;
}
Helper.LogCommand(caller, command);
// Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{
var matches = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString());
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
if (player != null && player.IsValid)
{
@@ -655,8 +926,8 @@ public partial class CS2_SimpleAdmin
// Reset voice flags to normal
namePlayer.VoiceFlags = VoiceFlags.Normal;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalSilences > 0)
PlayersInfo[namePlayer.UserId.Value].TotalSilences--;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalSilences > 0)
PlayersInfo[namePlayer.SteamID].TotalSilences--;
Task.Run(async () =>
{
@@ -676,13 +947,19 @@ public partial class CS2_SimpleAdmin
}
}
/// <summary>
/// Validates mute penalty duration based on admin privileges and configured max duration.
/// </summary>
/// <param name="caller">Admin/player issuing the mute.</param>
/// <param name="duration">Requested duration in minutes.</param>
/// <returns>True if mute penalty duration is allowed; false otherwise.</returns>
private bool CheckValidMute(CCSPlayerController? caller, int duration)
{
if (caller == null) return true;
var canPermMute = AdminManager.PlayerHasPermissions(caller, "@css/permmute");
var canPermMute = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permmute");
if (duration <= 0 && canPermMute == false)
if (duration <= 0 && !canPermMute)
{
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}");
return false;

View File

@@ -8,6 +8,12 @@ namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
/// <summary>
/// Handles the vote command, creates voting menu for players, and collects answers.
/// Displays results after timeout and resets voting state.
/// </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>
[RequiresPermissions("@css/generic")]
[CommandHelper(minArgs: 2, usage: "<question> [... options ...]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnVoteCommand(CCSPlayerController? caller, CommandInfo command)

View File

@@ -1,215 +1,307 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/cheats")]
public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player =>
player.IsValid &&
player is { PawnIsAlive: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
NoClip(caller, player, callerName);
}
});
}
internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Toggle no-clip mode for the player
player.Pawn.Value?.ToggleNoclip();
// Determine message keys and arguments for the no-clip notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_noclip_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
// Log the command
if (command == null)
{
Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
}
else
{
Helper.LogCommand(caller, command);
}
}
[RequiresPermissions("@css/cheats")]
[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 targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
God(caller, player, command);
}
});
}
internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Toggle god mode for the player
if (!GodPlayers.Add(player.Slot))
{
GodPlayers.Remove(player.Slot);
}
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
else
Helper.LogCommand(caller, command);
// Determine message key and arguments for the god mode notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_god_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
}
[CommandHelper(1, "<#userid or name> [duration]")]
[RequiresPermissions("@css/slay")]
public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
int.TryParse(command.GetArg(2), out var time);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, PawnIsAlive: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
Freeze(caller, player, time, callerName, command);
}
});
}
internal static void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time, string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Freeze player pawn
player.Pawn.Value?.Freeze();
// Determine message keys and arguments for the freeze notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_freeze_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
// Schedule unfreeze for the player if time is specified
if (time > 0)
{
Instance.AddTimer(time, () => player.Pawn.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
// 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}");
else
Helper.LogCommand(caller, command);
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/slay")]
public void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, PawnIsAlive: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
Unfreeze(caller, player, callerName, command);
});
}
internal static void Unfreeze(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Unfreeze player pawn
player.Pawn.Value?.Unfreeze();
// Determine message keys and arguments for the unfreeze notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_unfreeze_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
// Log the command and send Discord notification
if (command == null)
Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
else
Helper.LogCommand(caller, command);
}
}
// using System.Globalization;
// using CounterStrikeSharp.API;
// using CounterStrikeSharp.API.Core;
// using CounterStrikeSharp.API.Modules.Admin;
// using CounterStrikeSharp.API.Modules.Commands;
//
// namespace CS2_SimpleAdmin;
//
// public partial class CS2_SimpleAdmin
// {
// /// <summary>
// /// Enables or disables no-clip mode for specified player(s).
// /// </summary>
// /// <param name="caller">The player issuing the command.</param>
// /// <param name="command">The command input containing targets.</param>
// [CommandHelper(1, "<#userid or name>")]
// [RequiresPermissions("@css/cheats")]
// public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
//
// var targets = GetTarget(command);
// if (targets == null) return;
// var playersToTarget = targets.Players.Where(player =>
// player.IsValid &&
// player is { IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// if (caller!.CanTarget(player))
// {
// NoClip(caller, player, callerName);
// }
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Toggles no-clip mode for a player and shows admin activity messages.
// /// </summary>
// /// <param name="caller">The player/admin toggling no-clip.</param>
// /// <param name="player">The target player whose no-clip state changes.</param>
// /// <param name="callerName">Optional caller name for messages.</param>
// /// <param name="command">Optional command info for logging.</param>
// internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
// {
// if (!player.IsValid) return;
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
// // Toggle no-clip mode for the player
// player.Pawn.Value?.ToggleNoclip();
//
// // Determine message keys and arguments for the no-clip notification
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_noclip_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
//
// // Log the command
// if (command == null)
// Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// }
//
// /// <summary>
// /// Enables or disables god mode for specified player(s).
// /// </summary>
// /// <param name="caller">The player issuing the command.</param>
// /// <param name="command">The command input containing targets.</param>
//
// [RequiresPermissions("@css/cheats")]
// [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 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();
//
// playersToTarget.ForEach(player =>
// {
// if (player.Connected != PlayerConnectedState.PlayerConnected)
// return;
//
// if (caller!.CanTarget(player))
// {
// God(caller, player, command);
// }
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Toggles god mode for a player and notifies admins.
// /// </summary>
// /// <param name="caller">The player/admin toggling god mode.</param>
// /// <param name="player">The target player whose god mode changes.</param>
// /// <param name="command">Optional command info for logging.</param>
// internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null)
// {
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
// // Toggle god mode for the player
// if (!GodPlayers.Add(player.Slot))
// {
// GodPlayers.Remove(player.Slot);
// }
//
// // Log the command
// if (command == null)
// Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
//
// // Determine message key and arguments for the god mode notification
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_god_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
// }
//
// /// <summary>
// /// Freezes target player(s) for an optional specified duration.
// /// </summary>
// /// <param name="caller">The player issuing the freeze command.</param>
// /// <param name="command">The command input containing targets and duration.</param>
// [CommandHelper(1, "<#userid or name> [duration]")]
// [RequiresPermissions("@css/slay")]
// public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// int.TryParse(command.GetArg(2), out var time);
//
// var targets = GetTarget(command);
// if (targets == null) return;
// var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// if (caller!.CanTarget(player))
// {
// Freeze(caller, player, time, callerName, command);
// }
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Resizes the target player(s) models to a specified scale.
// /// </summary>
// /// <param name="caller">The player issuing the resize command.</param>
// /// <param name="command">The command input containing targets and scale factor.</param>
// [CommandHelper(1, "<#userid or name> [size]")]
// [RequiresPermissions("@css/slay")]
// public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size);
//
// var targets = GetTarget(command);
// if (targets == null) return;
// var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// if (!caller!.CanTarget(player)) return;
//
// var sceneNode = player.PlayerPawn.Value!.CBodyComponent?.SceneNode;
// if (sceneNode == null) return;
//
// sceneNode.GetSkeletonInstance().Scale = size;
// player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture));
//
// Server.NextWorldUpdate(() =>
// {
// Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent");
// });
//
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_resize_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Freezes a single player and optionally schedules automatic unfreeze after a duration.
// /// </summary>
// /// <param name="caller">The player/admin freezing the player.</param>
// /// <param name="player">The player to freeze.</param>
// /// <param name="time">Duration of freeze in seconds.</param>
// /// <param name="callerName">Optional name for notifications.</param>
// /// <param name="command">Optional command info for logging.</param>
// internal static void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time, string? callerName = null, CommandInfo? command = null)
// {
// if (!player.IsValid) return;
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
// // Freeze player pawn
// player.Pawn.Value?.Freeze();
//
// // Determine message keys and arguments for the freeze notification
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_freeze_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
//
// // Schedule unfreeze for the player if time is specified
// if (time > 0)
// {
// Instance.AddTimer(time, () => player.Pawn.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
// }
//
// // 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}");
// }
//
// /// <summary>
// /// Unfreezes target player(s).
// /// </summary>
// /// <param name="caller">The player issuing the unfreeze command.</param>
// /// <param name="command">The command input with targets.</param>
// [CommandHelper(1, "<#userid or name>")]
// [RequiresPermissions("@css/slay")]
// public void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
//
// var targets = GetTarget(command);
// if (targets == null) return;
// var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// playersToTarget.ForEach(player =>
// {
// Unfreeze(caller, player, callerName, command);
// });
//
// Helper.LogCommand(caller, command);
// }
//
// /// <summary>
// /// Unfreezes a single player and notifies admins.
// /// </summary>
// /// <param name="caller">The player/admin unfreezing the player.</param>
// /// <param name="player">The player to unfreeze.</param>
// /// <param name="callerName">Optional name for notifications.</param>
// /// <param name="command">Optional command info for logging.</param>
// internal static void Unfreeze(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
// {
// if (!player.IsValid) return;
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
// // Unfreeze player pawn
// player.Pawn.Value?.Unfreeze();
//
// // Determine message keys and arguments for the unfreeze notification
// var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_unfreeze_message",
// new object[] { "CALLER", player.PlayerName });
//
// // Display admin activity message to other players
// if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
//
// // Log the command and send Discord notification
// if (command == null)
// Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// }
// }

View File

@@ -1,4 +1,5 @@
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
@@ -10,9 +11,12 @@ namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
internal static readonly Dictionary<int, float> SpeedPlayers = [];
internal static readonly Dictionary<CCSPlayerController, float> GravityPlayers = [];
/// <summary>
/// Executes the 'slay' command, forcing the targeted players to commit suicide.
/// Checks player validity and permissions.
/// </summary>
/// <param name="caller">Player or console issuing the command.</param>
/// <param name="command">Command details, including targets.</param>
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSlayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -21,14 +25,23 @@ public partial class CS2_SimpleAdmin
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
var 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);
}
/// <summary>
/// Performs the actual slay action on a player, with notification and logging.
/// </summary>
/// <param name="caller">Admin or console issuing the slay.</param>
/// <param name="player">Target player to slay.</param>
/// <param name="callerName">Optional name to display as the slayer.</param>
/// <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.PlayerConnected) return;
@@ -48,386 +61,19 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Log the command and send Discord notification
if (command == null)
Helper.LogCommand(caller, $"css_slay {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
else
Helper.LogCommand(caller, command);
}
[RequiresPermissions("@css/cheats")]
[CommandHelper(minArgs: 2, usage: "<#userid or name> <weapon>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnGiveCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
var weaponName = command.GetArg(2);
// check if item is typed
// if (weaponName.Length < 2)
// {
// command.ReplyToCommand($"No weapon typed.");
// return;
// }
// check if weapon is knife
if (weaponName.Contains("_knife") || weaponName.Contains("bayonet"))
{
if (CoreConfig.FollowCS2ServerGuidelines)
{
command.ReplyToCommand($"Cannot Give {weaponName} because it's illegal to be given.");
return;
}
}
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
GiveWeapon(caller, player, weaponName, callerName, command);
});
}
private static void GiveWeapon(CCSPlayerController? caller, CCSPlayerController player, string weaponName, string? callerName = null, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
var weapons = WeaponHelper.GetWeaponsByPartialName(weaponName);
switch (weapons.Count)
{
case 0:
return;
case > 1:
{
var weaponList = string.Join(", ", weapons.Select(w => w.EnumMemberValue));
command?.ReplyToCommand($"Found weapons with a similar name: {weaponList}");
return;
}
}
// Give weapon to the player
player.GiveNamedItem(weapons.First().EnumValue);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_giveweapon {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {weaponName}");
else
Helper.LogCommand(caller, command);
// Determine message keys and arguments for the weapon give notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_give_message",
new object[] { "CALLER", player.PlayerName, weaponName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
}
internal static void GiveWeapon(CCSPlayerController? caller, CCSPlayerController player, CsItem weapon, string? callerName = null, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Give weapon to the player
player.GiveNamedItem(weapon);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_giveweapon {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {weapon.ToString()}");
else
Helper.LogCommand(caller, command);
// Determine message keys and arguments for the weapon give notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_give_message",
new object[] { "CALLER", player.PlayerName, weapon.ToString() });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
}
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnStripCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
StripWeapons(caller, player, callerName, command);
}
});
}
internal static void StripWeapons(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Check if player is valid, alive, and connected
if (!player.IsValid || !player.PawnIsAlive || player.Connected != PlayerConnectedState.PlayerConnected)
return;
// Strip weapons from the player
player.RemoveWeapons();
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_strip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
else
Helper.LogCommand(caller, command);
// Determine message keys and arguments for the weapon strip notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_strip_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
}
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> <health>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnHpCommand(CCSPlayerController? caller, CommandInfo command)
{
int.TryParse(command.GetArg(2), out var health);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (caller!.CanTarget(player))
{
SetHp(caller, player, health, command);
}
});
}
internal static void SetHp(CCSPlayerController? caller, CCSPlayerController player, int health, CommandInfo? command = null)
{
if (!player.IsValid || player.IsHLTV) return;
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Set player's health
player.SetHp(health);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_hp {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {health}");
else
Helper.LogCommand(caller, command);
// Determine message keys and arguments for the HP set notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_hp_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
}
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> <speed>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSpeedCommand(CCSPlayerController? caller, CommandInfo command)
{
float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var speed);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
SetSpeed(caller, player, speed, command);
}
});
}
internal static void SetSpeed(CCSPlayerController? caller, CCSPlayerController player, float speed, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Set player's speed
player.SetSpeed(speed);
if (speed == 1f)
SpeedPlayers.Remove(player.Slot);
else
SpeedPlayers[player.Slot] = speed;
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_speed {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {speed}");
else
Helper.LogCommand(caller, command);
// Determine message keys and arguments for the speed set notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_speed_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
}
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> <gravity>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnGravityCommand(CCSPlayerController? caller, CommandInfo command)
{
float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var gravity);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
SetGravity(caller, player, gravity, command);
}
});
}
internal static void SetGravity(CCSPlayerController? caller, CCSPlayerController player, float gravity, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Set player's gravity
player.SetGravity(gravity);
if (gravity == 1f)
GravityPlayers.Remove(player);
else
GravityPlayers[player] = gravity;
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_gravity {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {gravity}");
else
Helper.LogCommand(caller, command);
// Determine message keys and arguments for the gravity set notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_gravity_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
}
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> <money>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnMoneyCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
int.TryParse(command.GetArg(2), out var money);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
SetMoney(caller, player, money, command);
}
});
}
internal static void SetMoney(CCSPlayerController? caller, CCSPlayerController player, int money, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// Set player's money
player.SetMoney(money);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_money {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {money}");
else
Helper.LogCommand(caller, command);
// Determine message keys and arguments for the money set notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_money_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
}
/// <summary>
/// Applies damage as a slap effect to the targeted players.
/// </summary>
/// <param name="caller">The player/admin executing the slap command.</param>
/// <param name="command">The command including targets and optional damage value.</param>
[RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [damage]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSlapCommand(CCSPlayerController? caller, CommandInfo command)
@@ -437,7 +83,7 @@ public partial class CS2_SimpleAdmin
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList();
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
if (command.ArgCount >= 2)
{
@@ -454,8 +100,17 @@ public partial class CS2_SimpleAdmin
Slap(caller, player, damage, command);
}
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Applies slap damage to a specific player with notifications and logging.
/// </summary>
/// <param name="caller">The player/admin applying the slap effect.</param>
/// <param name="player">The target player to slap.</param>
/// <param name="damage">The damage amount to apply.</param>
/// <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;
@@ -469,9 +124,7 @@ public partial class CS2_SimpleAdmin
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_slap {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {damage}");
else
Helper.LogCommand(caller, command);
// Determine message key and arguments for the slap notification
var (activityMessageKey, adminActivityArgs) =
("sa_admin_slap_message",
@@ -482,10 +135,15 @@ public partial class CS2_SimpleAdmin
if (_localizer != null)
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
}
/// <summary>
/// Changes the team of targeted players with optional kill on switch.
/// </summary>
/// <param name="caller">The player/admin issuing the command.</param>
/// <param name="command">The command containing targets, team info, and optional kill flag.</param>
[RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 2, usage: "<#userid or name> [<ct/tt/spec>] [-k]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnTeamCommand(CCSPlayerController? caller, CommandInfo command)
@@ -531,8 +189,19 @@ public partial class CS2_SimpleAdmin
{
ChangeTeam(caller, player, _teamName, teamNum, kill, command);
});
Helper.LogCommand(caller, command);
}
/// <summary>
/// Changes the team of a player with various conditions and logs the operation.
/// </summary>
/// <param name="caller">The player/admin issuing the change.</param>
/// <param name="player">The target player.</param>
/// <param name="teamName">Team name string.</param>
/// <param name="teamNum">Team enumeration value.</param>
/// <param name="kill">If true, kills player on team change.</param>
/// <param name="command">Optional command info for logging.</param>
internal static void ChangeTeam(CCSPlayerController? caller, CCSPlayerController player, string teamName, CsTeam teamNum, bool kill, CommandInfo? command = null)
{
// Check if the player is valid and connected
@@ -548,7 +217,7 @@ public partial class CS2_SimpleAdmin
// Change team based on the provided teamName and conditions
if (!teamName.Equals("swap", StringComparison.OrdinalIgnoreCase))
{
if (player.PawnIsAlive && teamNum != CsTeam.Spectator && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
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);
@@ -559,7 +228,7 @@ public partial class CS2_SimpleAdmin
{
var _teamNum = (CsTeam)player.TeamNum == CsTeam.Terrorist ? CsTeam.CounterTerrorist : CsTeam.Terrorist;
teamName = _teamNum == CsTeam.Terrorist ? "TT" : "CT";
if (player.PawnIsAlive && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
player.SwitchTeam(_teamNum);
else
player.ChangeTeam(_teamNum);
@@ -569,8 +238,6 @@ public partial class CS2_SimpleAdmin
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_team {player.PlayerName} {teamName}");
else
Helper.LogCommand(caller, command);
// Determine message key and arguments for the team change notification
var activityMessageKey = "sa_admin_team_message";
@@ -579,9 +246,14 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
/// <summary>
/// Renames targeted players to a new name.
/// </summary>
/// <param name="caller">The admin issuing the rename command.</param>
/// <param name="command">The command including targets and new name.</param>
[CommandHelper(1, "<#userid or name> <new name>")]
[RequiresPermissions("@css/kick")]
public void OnRenameCommand(CCSPlayerController? caller, CommandInfo command)
@@ -620,13 +292,18 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// Rename the player
player.Rename(newName);
});
}
/// <summary>
/// Renames permamently targeted players to a new name.
/// </summary>
/// <param name="caller">The admin issuing the pre-rename command.</param>
/// <param name="command">The command containing targets and new alias.</param>
[CommandHelper(1, "<#userid or name> <new name>")]
[RequiresPermissions("@css/ban")]
public void OnPrenameCommand(CCSPlayerController? caller, CommandInfo command)
@@ -661,7 +338,7 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players
if (caller != null && !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Determine if the new name is valid and update the renamed players list
@@ -677,146 +354,270 @@ public partial class CS2_SimpleAdmin
});
}
[CommandHelper(1, "<#userid or name>")]
[RequiresPermissions("@css/cheats")]
public void OnRespawnCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.PlayerConnected)
return;
if (caller!.CanTarget(player))
{
Respawn(caller, player, callerName, command);
}
});
}
internal static void Respawn(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
// Check if the caller can target the player
if (!caller.CanTarget(player)) return;
// Set default caller name if not provided
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Ensure the player's pawn is valid before attempting to respawn
if (_cBasePlayerControllerSetPawnFunc == null || player.PlayerPawn.Value == null || !player.PlayerPawn.IsValid) return;
// Perform the respawn operation
var playerPawn = player.PlayerPawn.Value;
_cBasePlayerControllerSetPawnFunc.Invoke(player, playerPawn, true, false);
VirtualFunction.CreateVoid<CCSPlayerController>(player.Handle, GameData.GetOffset("CCSPlayerController_Respawn"))(player);
if (player.UserId.HasValue && PlayersInfo.TryGetValue(player.UserId.Value, out var value) && value.DiePosition != null)
playerPawn.Teleport(value.DiePosition?.Position, value.DiePosition?.Angle);
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_respawn {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
else
Helper.LogCommand(caller, command);
// Determine message key and arguments for the respawn notification
var activityMessageKey = "sa_admin_respawn_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName };
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs);
}
[CommandHelper(1, "<#userid or name>")]
/// <summary>
/// Teleports targeted player(s) to another player's location.
/// </summary>
/// <param name="caller">Admin issuing teleport command.</param>
/// <param name="command">Command containing teleport targets and destination.</param>
[CommandHelper(1, "<#userid or name> [#userid or name]")]
[RequiresPermissions("@css/kick")]
public void OnGotoCommand(CCSPlayerController? caller, CommandInfo command)
{
// Check if the caller is valid and has a live pawn
if (caller == null || !caller.PawnIsAlive) return;
IEnumerable<CCSPlayerController> playersToTeleport;
CCSPlayerController? destinationPlayer;
// Get the target players
var targets = GetTarget(command);
if (targets == null || targets.Count() > 1) return;
var playersToTarget = targets.Players
.Where(player => player is { IsValid: true, IsHLTV: false })
.ToList();
if (command.ArgCount < 3)
{
if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
return;
// Log the command
if (targets == null || targets.Count() != 1)
return;
destinationPlayer = targets.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, 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);
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.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p))
.ToList();
if (!playersToTeleport.Any())
return;
}
// Log command
Helper.LogCommand(caller, command);
// Process each player to teleport
foreach (var player in playersToTarget.Where(player => player is { Connected: PlayerConnectedState.PlayerConnected, PawnIsAlive: true }).Where(caller.CanTarget))
foreach (var player in playersToTeleport)
{
if (caller.PlayerPawn.Value == null)
if (player.PlayerPawn?.Value == null || destinationPlayer?.PlayerPawn?.Value == null)
continue;
// Teleport the caller to the player and toggle noclip
caller.TeleportPlayer(player);
caller.PlayerPawn.Value.ToggleNoclip();
player.TeleportPlayer(destinationPlayer);
// Set a timer to toggle noclip back after 3 seconds
AddTimer(3, () => caller.PlayerPawn.Value.ToggleNoclip());
player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
// Prepare message key and arguments for the teleport notification
var activityMessageKey = "sa_admin_tp_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName };
destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
// Show admin activity
if (!SilentPlayers.Contains(caller.Slot) && _localizer != null)
AddTimer(4, () =>
{
Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, adminActivityArgs);
if (player is { IsValid: true, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
}
if (destinationPlayer.IsValid && destinationPlayer.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
}
});
if (caller != null && !SilentPlayers.Contains(caller.Slot) && _localizer != null)
{
Helper.ShowAdminActivity("sa_admin_tp_message", player.PlayerName, false, "CALLER", destinationPlayer.PlayerName);
}
}
}
[CommandHelper(1, "<#userid or name>")]
/// <summary>
/// Brings targeted player(s) to the caller or specified destination player's location.
/// </summary>
/// <param name="caller">Player issuing the bring command.</param>
/// <param name="command">Command containing the destination and targets.</param>
[CommandHelper(1, "<#destination or name> [#userid or name...]")]
[RequiresPermissions("@css/kick")]
public void OnBringCommand(CCSPlayerController? caller, CommandInfo command)
{
// Check if the caller is valid and has a live pawn
if (caller == null || !caller.PawnIsAlive) return;
IEnumerable<CCSPlayerController> playersToTeleport;
CCSPlayerController? destinationPlayer;
// Get the target players
var targets = GetTarget(command);
if (targets == null || targets.Count() > 1) return;
if (command.ArgCount < 3)
{
if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
return;
var playersToTarget = targets.Players
.Where(player => player is { IsValid: true, IsHLTV: false })
.ToList();
var targets = GetTarget(command);
if (targets == null || !targets.Any())
return;
// Log the command
destinationPlayer = caller;
playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p))
.ToList();
}
else
{
var destination = GetTarget(command);
if (destination == null || destination.Count() != 1)
return;
destinationPlayer = destination.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null)
return;
// Rest args = targets to teleport
var targets = GetTarget(command, 2);
if (targets == null || !targets.Any())
return;
playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p))
.ToList();
}
if (destinationPlayer == null || !playersToTeleport.Any())
return;
// Log command
Helper.LogCommand(caller, command);
// Process each player to teleport
foreach (var player in playersToTarget.Where(player => player is { Connected: PlayerConnectedState.PlayerConnected, PawnIsAlive: true }).Where(caller.CanTarget))
foreach (var player in playersToTeleport)
{
if (caller.PlayerPawn.Value == null)
if (player.PlayerPawn?.Value == null || destinationPlayer.PlayerPawn?.Value == null)
continue;
// Teleport the player to the caller and toggle noclip
player.TeleportPlayer(caller);
caller.PlayerPawn.Value.ToggleNoclip();
// Teleport
player.TeleportPlayer(destinationPlayer);
// Set a timer to toggle noclip back after 3 seconds
AddTimer(3, () => caller.PlayerPawn.Value.ToggleNoclip());
player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
// Prepare message key and arguments for the bring notification
var activityMessageKey = "sa_admin_bring_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName };
destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
// Show admin activity
if (!SilentPlayers.Contains(caller.Slot) && _localizer != null)
AddTimer(4, () =>
{
Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, adminActivityArgs);
if (player is { IsValid: true, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
}
if (destinationPlayer.IsValid && destinationPlayer.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
}
});
if (caller != null && !SilentPlayers.Contains(caller.Slot) && _localizer != null)
{
Helper.ShowAdminActivity("sa_admin_bring_message", player.PlayerName, false, "CALLER", destinationPlayer.PlayerName);
}
}
}
// [CommandHelper(1, "<#userid or name> [#userid or name]")]
// [RequiresPermissions("@css/kick")]
// public void OnBringCommand(CCSPlayerController? caller, CommandInfo command)
// {
// // Check if the caller is valid and has a live pawn
// if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
// return;
//
// // Get the target players
// var targets = GetTarget(command);
// if (targets == null || targets.Count() > 1) return;
//
// var playersToTarget = targets.Players
// .Where(player => player is { IsValid: true, IsHLTV: false })
// .ToList();
//
// // Log the command
// Helper.LogCommand(caller, command);
//
// // Process each player to teleport
// foreach (var player in playersToTarget.Where(player => player is { Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).Where(caller.CanTarget))
// {
// if (caller.PlayerPawn.Value == null || player.PlayerPawn.Value == null)
// continue;
//
// // Teleport the player to the caller and toggle noclip
// player.TeleportPlayer(caller);
// // caller.PlayerPawn.Value.ToggleNoclip();
//
// caller.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
// caller.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
//
// Utilities.SetStateChanged(caller, "CCollisionProperty", "m_CollisionGroup");
// Utilities.SetStateChanged(caller, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
//
// player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
// player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
//
// Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
// Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
//
// // Set a timer to toggle collision back after 4 seconds
// AddTimer(4, () =>
// {
// if (!player.IsValid || player.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
// return;
//
// caller.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
// caller.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
//
// Utilities.SetStateChanged(caller, "CCollisionProperty", "m_CollisionGroup");
// Utilities.SetStateChanged(caller, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
//
// player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
// player.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_PLAYER;
//
// Utilities.SetStateChanged(player, "CCollisionProperty", "m_CollisionGroup");
// Utilities.SetStateChanged(player, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
// });
//
// // Prepare message key and arguments for the bring notification
// var activityMessageKey = "sa_admin_bring_message";
// var adminActivityArgs = new object[] { "CALLER", player.PlayerName };
//
// // Show admin activity
// if (!SilentPlayers.Contains(caller.Slot) && _localizer != null)
// {
// Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, false, adminActivityArgs);
// }
// }
// }
}

View File

@@ -38,56 +38,67 @@ public class Discord
[JsonPropertyName("DiscordPenaltyBanSettings")]
public DiscordPenaltySetting[] DiscordPenaltyBanSettings { get; set; } =
[
new DiscordPenaltySetting { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" },
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordPenaltyMuteSettings")]
public DiscordPenaltySetting[] DiscordPenaltyMuteSettings { get; set; } =
[
new DiscordPenaltySetting { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" },
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordPenaltyGagSettings")]
public DiscordPenaltySetting[] DiscordPenaltyGagSettings { get; set; } =
[
new DiscordPenaltySetting { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" },
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordPenaltySilenceSettings")]
public DiscordPenaltySetting[] DiscordPenaltySilenceSettings { get; set; } =
[
new DiscordPenaltySetting { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" },
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordPenaltyWarnSettings")]
public DiscordPenaltySetting[] DiscordPenaltyWarnSettings { get; set; } =
[
new DiscordPenaltySetting { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" },
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordAssociatedAccountsSettings")]
public DiscordPenaltySetting[] DiscordAssociatedAccountsSettings { get; set; } =
[
new() { Name = "Color", Value = "" },
new() { Name = "Webhook", Value = "" },
new() { Name = "ThumbnailUrl", Value = "" },
new() { Name = "ImageUrl", Value = "" },
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
}
@@ -113,15 +124,15 @@ public class MenuConfig
[JsonPropertyName("Durations")]
public DurationItem[] Durations { get; set; } =
[
new DurationItem { Name = "1 minute", Duration = 1 },
new DurationItem { Name = "5 minutes", Duration = 5 },
new DurationItem { Name = "15 minutes", Duration = 15 },
new DurationItem { Name = "1 hour", Duration = 60 },
new DurationItem { Name = "1 day", Duration = 60 * 24 },
new DurationItem { Name = "7 days", Duration = 60 * 24 * 7 },
new DurationItem { Name = "14 days", Duration = 60 * 24 * 14 },
new DurationItem { Name = "30 days", Duration = 60 * 24 * 30 },
new DurationItem { Name = "Permanent", Duration = 0 }
new() { Name = "1 minute", Duration = 1 },
new() { Name = "5 minutes", Duration = 5 },
new() { Name = "15 minutes", Duration = 15 },
new() { Name = "1 hour", Duration = 60 },
new() { Name = "1 day", Duration = 60 * 24 },
new() { Name = "7 days", Duration = 60 * 24 * 7 },
new() { Name = "14 days", Duration = 60 * 24 * 14 },
new() { Name = "30 days", Duration = 60 * 24 * 30 },
new() { Name = "Permanent", Duration = 0 }
];
[JsonPropertyName("BanReasons")]
@@ -166,18 +177,18 @@ public class MenuConfig
[JsonPropertyName("AdminFlags")]
public AdminFlag[] AdminFlags { get; set; } =
[
new AdminFlag { Name = "Generic", Flag = "@css/generic" },
new AdminFlag { Name = "Chat", Flag = "@css/chat" },
new AdminFlag { Name = "Change Map", Flag = "@css/changemap" },
new AdminFlag { Name = "Slay", Flag = "@css/slay" },
new AdminFlag { Name = "Kick", Flag = "@css/kick" },
new AdminFlag { Name = "Ban", Flag = "@css/ban" },
new AdminFlag { Name = "Perm Ban", Flag = "@css/permban" },
new AdminFlag { Name = "Unban", Flag = "@css/unban" },
new AdminFlag { Name = "Show IP", Flag = "@css/showip" },
new AdminFlag { Name = "Cvar", Flag = "@css/cvar" },
new AdminFlag { Name = "Rcon", Flag = "@css/rcon" },
new AdminFlag { Name = "Root (all flags)", Flag = "@css/root" }
new() { Name = "Generic", Flag = "@css/generic" },
new() { Name = "Chat", Flag = "@css/chat" },
new() { Name = "Change Map", Flag = "@css/changemap" },
new() { Name = "Slay", Flag = "@css/slay" },
new() { Name = "Kick", Flag = "@css/kick" },
new() { Name = "Ban", Flag = "@css/ban" },
new() { Name = "Perm Ban", Flag = "@css/permban" },
new() { Name = "Unban", Flag = "@css/unban" },
new() { Name = "Show IP", Flag = "@css/showip" },
new() { Name = "Cvar", Flag = "@css/cvar" },
new() { Name = "Rcon", Flag = "@css/rcon" },
new() { Name = "Root (all flags)", Flag = "@css/root" }
];
}
@@ -224,30 +235,23 @@ public class OtherSettings
[JsonPropertyName("UserMessageGagChatType")]
public bool UserMessageGagChatType { get; set; } = false;
[JsonPropertyName("CheckMultiAccountsByIp")]
public bool CheckMultiAccountsByIp { get; set; } = true;
[JsonPropertyName("AdditionalCommandsToLog")]
public List<string> AdditionalCommandsToLog = new();
public List<string> AdditionalCommandsToLog { get; set; } = new();
[JsonPropertyName("IgnoredIps")]
public List<string> IgnoredIps { get; set; } = new();
}
public class CS2_SimpleAdminConfig : BasePluginConfig
{
[JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 23;
[JsonPropertyName("DatabaseHost")]
public string DatabaseHost { get; set; } = "";
[JsonPropertyName("DatabasePort")]
public int DatabasePort { get; set; } = 3306;
[JsonPropertyName("DatabaseUser")]
public string DatabaseUser { get; set; } = "";
[JsonPropertyName("DatabasePassword")]
public string DatabasePassword { get; set; } = "";
[JsonPropertyName("DatabaseName")]
public string DatabaseName { get; set; } = "";
[JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 25;
[JsonPropertyName("DatabaseConfig")]
public DatabaseConfig DatabaseConfig { get; set; } = new();
[JsonPropertyName("OtherSettings")]
public OtherSettings OtherSettings { get; set; } = new();
@@ -284,4 +288,38 @@ public class CS2_SimpleAdminConfig : BasePluginConfig
[JsonPropertyName("MenuConfig")]
public MenuConfig MenuConfigs { get; set; } = new();
}
}
public class DatabaseConfig
{
[JsonPropertyName("DatabaseType")]
public string DatabaseType { get; set; } = "SQLite";
[JsonPropertyName("SqliteFilePath")]
public string SqliteFilePath { get; set; } = "cs2-simpleadmin.sqlite";
[JsonPropertyName("DatabaseHost")]
public string DatabaseHost { get; set; } = "";
[JsonPropertyName("DatabasePort")]
public int DatabasePort { get; set; } = 3306;
[JsonPropertyName("DatabaseUser")]
public string DatabaseUser { get; set; } = "";
[JsonPropertyName("DatabasePassword")]
public string DatabasePassword { get; set; } = "";
[JsonPropertyName("DatabaseName")]
public string DatabaseName { get; set; } = "";
[JsonPropertyName("DatabaseSSlMode")]
public string DatabaseSSlMode { get; set; } = "preferred";
}
public enum DatabaseType
{
MySQL,
SQLite
}

View File

@@ -11,6 +11,11 @@ public class Database(string dbConnectionString)
{
var 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)
@@ -26,6 +31,11 @@ public class Database(string dbConnectionString)
{
var 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)
@@ -35,11 +45,11 @@ public class Database(string dbConnectionString)
}
}
public void DatabaseMigration()
{
Migration migrator = new(this);
migrator.ExecuteMigrations();
}
// public async Task DatabaseMigration()
// {
// Migration migrator = new(this);
// await migrator.ExecuteMigrationsAsync();
// }
public bool CheckDatabaseConnection(out string? exception)
{

View File

@@ -0,0 +1,60 @@
using System.Data.Common;
namespace CS2_SimpleAdmin.Database;
public interface IDatabaseProvider
{
Task<DbConnection> CreateConnectionAsync();
Task<(bool Success, string? Exception)> CheckConnectionAsync();
Task DatabaseMigrationAsync();
// CacheManager
string GetBanSelectQuery(bool multiServer);
string GetIpHistoryQuery();
string GetBanUpdateQuery(bool multiServer);
// PermissionManager
string GetAdminsQuery();
string GetDeleteAdminQuery(bool globalDelete);
string GetAddAdminQuery();
string GetAddAdminFlagsQuery();
string GetUpdateAdminGroupQuery();
string GetGroupsQuery();
string GetGroupIdByNameQuery();
string GetAddGroupQuery();
string GetAddGroupFlagsQuery();
string GetAddGroupServerQuery();
string GetDeleteGroupQuery();
string GetDeleteOldAdminsQuery();
// BanManager
string GetAddBanQuery();
string GetAddBanBySteamIdQuery();
string GetAddBanByIpQuery();
string GetUnbanRetrieveBansQuery(bool multiServer);
string GetUnbanAdminIdQuery();
string GetInsertUnbanQuery(bool includeReason);
string GetUpdateBanStatusQuery();
string GetExpireBansQuery(bool multiServer);
string GetExpireIpBansQuery(bool multiServer);
// MuteManager
string GetAddMuteQuery(bool includePlayerName);
string GetIsMutedQuery(bool multiServer, int timeMode);
string GetMuteStatsQuery(bool multiServer);
string GetUpdateMutePassedQuery(bool multiServer);
string GetCheckExpiredMutesQuery(bool multiServer);
string GetRetrieveMutesQuery(bool multiServer);
string GetUnmuteAdminIdQuery();
string GetInsertUnmuteQuery(bool includeReason);
string GetUpdateMuteStatusQuery();
string GetExpireMutesQuery(bool multiServer, int timeMode);
// WarnManager
string GetAddWarnQuery(bool includePlayerName);
string GetPlayerWarnsQuery(bool multiServer, bool active);
string GetPlayerWarnsCountQuery(bool multiServer, bool active);
string GetUnwarnByIdQuery(bool multiServer);
string GetUnwarnLastQuery(bool multiServer);
string GetExpireWarnsQuery(bool multiServer);
}

View File

@@ -1,61 +1,105 @@
using Microsoft.Extensions.Logging;
using MySqlConnector;
using System.Data.Common;
using Microsoft.Extensions.Logging;
namespace CS2_SimpleAdmin.Database;
public class Migration(Database database)
public class Migration(string migrationsPath)
{
public void ExecuteMigrations()
/// <summary>
/// Executes all migration scripts found in the configured migrations path that have not been applied yet.
/// Creates a migration tracking table if it does not exist.
/// Applies migration scripts in filename order and logs successes or failures.
/// </summary>
public async Task ExecuteMigrationsAsync()
{
var migrationsDirectory = CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations";
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
var files = Directory.GetFiles(migrationsPath, "*.sql").OrderBy(f => f).ToList();
if (files.Count == 0) return;
var files = Directory.GetFiles(migrationsDirectory, "*.sql")
.OrderBy(f => f);
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using (var cmd = connection.CreateCommand())
{
if (migrationsPath.Contains("sqlite", StringComparison.CurrentCultureIgnoreCase))
{
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS sa_migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL
);
""";
}
else
{
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS sa_migrations (
id INT PRIMARY KEY AUTO_INCREMENT,
version VARCHAR(128) NOT NULL
);
""";
}
using var connection = database.GetConnection();
await cmd.ExecuteNonQueryAsync();
}
// Create sa_migrations table if not exists
using var cmd = new MySqlCommand("""
CREATE TABLE IF NOT EXISTS `sa_migrations` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`version` VARCHAR(255) NOT NULL
);
""", connection);
cmd.ExecuteNonQuery();
// Get the last applied migration version
var lastAppliedVersion = GetLastAppliedVersion(connection);
var lastAppliedVersion = await GetLastAppliedVersionAsync(connection);
foreach (var file in files)
{
var version = Path.GetFileNameWithoutExtension(file);
if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0)
continue;
// Check if the migration has already been applied
if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0) continue;
var sqlScript = File.ReadAllText(file);
try
{
var sqlScript = await File.ReadAllTextAsync(file);
using var cmdMigration = new MySqlCommand(sqlScript, connection);
cmdMigration.ExecuteNonQuery();
await using (var cmdMigration = connection.CreateCommand())
{
cmdMigration.CommandText = sqlScript;
await cmdMigration.ExecuteNonQueryAsync();
}
// Update the last applied migration version
UpdateLastAppliedVersion(connection, version);
await UpdateLastAppliedVersionAsync(connection, version);
CS2_SimpleAdmin._logger?.LogInformation($"Migration \"{version}\" successfully applied.");
CS2_SimpleAdmin._logger?.LogInformation($"Migration \"{version}\" successfully applied.");
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, $"Error applying migration \"{version}\".");
break;
}
}
}
private static string GetLastAppliedVersion(MySqlConnection connection)
/// <summary>
/// Retrieves the version string of the last applied migration from the database.
/// </summary>
/// <param name="connection">The open database connection.</param>
/// <returns>The version string of the last applied migration, or empty string if none.</returns>
private static async Task<string> GetLastAppliedVersionAsync(DbConnection connection)
{
using var cmd = new MySqlCommand("SELECT `version` FROM `sa_migrations` ORDER BY `id` DESC LIMIT 1;", connection);
var result = cmd.ExecuteScalar();
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT version FROM sa_migrations ORDER BY id DESC LIMIT 1;";
var result = await cmd.ExecuteScalarAsync();
return result?.ToString() ?? string.Empty;
}
private static void UpdateLastAppliedVersion(MySqlConnection connection, string version)
/// <summary>
/// Inserts a record tracking the successful application of a migration version.
/// </summary>
/// <param name="connection">The open database connection.</param>
/// <param name="version">The version string of the migration applied.</param>
private static async Task UpdateLastAppliedVersionAsync(DbConnection connection, string version)
{
using var cmd = new MySqlCommand("INSERT INTO `sa_migrations` (`version`) VALUES (@Version);", connection);
cmd.Parameters.AddWithValue("@Version", version);
cmd.ExecuteNonQuery();
await using var cmd = connection.CreateCommand();
cmd.CommandText = "INSERT INTO sa_migrations (version) VALUES (@Version);";
var param = cmd.CreateParameter();
param.ParameterName = "@Version";
param.Value = version;
cmd.Parameters.Add(param);
await cmd.ExecuteNonQueryAsync();
}
}

View File

@@ -16,8 +16,8 @@ CREATE TABLE IF NOT EXISTS `sa_unmutes` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`)
VALUES (-1, 'Console', 'Console', '', '0', NULL, NULL, NOW());
INSERT IGNORE INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`)
VALUES (0, 'Console', 'Console', '', '0', NULL, NULL, NOW());
UPDATE `sa_admins` SET `id` = 0 WHERE `id` = -1;

View File

@@ -20,7 +20,6 @@ CREATE TABLE IF NOT EXISTS `sa_groups_servers` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `sa_admins` ADD `group_id` INT NULL AFTER `created`;
ALTER TABLE `sa_groups_flags` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE CASCADE;
ALTER TABLE `sa_groups_servers` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE CASCADE;
ALTER TABLE `sa_admins` ADD FOREIGN KEY (`group_id`) REFERENCES `sa_groups`(`id`) ON DELETE SET NULL;

View File

@@ -1,8 +1,6 @@
CREATE TABLE IF NOT EXISTS `sa_players_ips` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`steamid` bigint(20) NOT NULL,
`address` varchar(64) NOT NULL,
`used_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `steamid` (`steamid`,`address`)
PRIMARY KEY (`steamid`, `address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@@ -0,0 +1 @@
ALTER TABLE `sa_servers` ADD `rcon_password` varchar(128) NULL AFTER `hostname`;

View File

@@ -0,0 +1 @@
ALTER TABLE `sa_bans` ADD COLUMN `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER `status`;

View File

@@ -0,0 +1,5 @@
DELETE FROM sa_players_ips WHERE INET_ATON(address) IS NULL AND address IS NOT NULL;
UPDATE `sa_players_ips` SET `address` = INET_ATON(address);
ALTER TABLE `sa_players_ips` CHANGE `address` `address` INT UNSIGNED NOT NULL;
ALTER TABLE `sa_players_ips` ADD INDEX (used_at DESC);
ALTER TABLE `sa_players_ips` ADD `name` VARCHAR(64) NULL DEFAULT NULL AFTER `steamid`;

View File

@@ -0,0 +1,3 @@
ALTER TABLE sa_mutes ADD INDEX (player_steamid, status, ends);
ALTER TABLE sa_mutes ADD INDEX(player_steamid, status, server_id, duration);
ALTER TABLE sa_mutes ADD INDEX(player_steamid, type);

View File

@@ -0,0 +1,23 @@
ALTER TABLE `sa_bans` CHANGE `player_steamid` `player_steamid` BIGINT NULL DEFAULT NULL;
UPDATE `sa_bans`
SET admin_steamid = '0'
WHERE admin_steamid NOT REGEXP '^[0-9]+$';
ALTER TABLE `sa_bans` CHANGE `admin_steamid` `admin_steamid` BIGINT NOT NULL;
ALTER TABLE `sa_mutes` CHANGE `player_steamid` `player_steamid` BIGINT NULL DEFAULT NULL;
UPDATE `sa_mutes`
SET admin_steamid = '0'
WHERE admin_steamid NOT REGEXP '^[0-9]+$';
ALTER TABLE `sa_mutes` CHANGE `admin_steamid` `admin_steamid` BIGINT NOT NULL;
ALTER TABLE `sa_warns` CHANGE `player_steamid` `player_steamid` BIGINT NULL DEFAULT NULL;
UPDATE `sa_warns`
SET admin_steamid = '0'
WHERE admin_steamid NOT REGEXP '^[0-9]+$';
ALTER TABLE `sa_warns` CHANGE `admin_steamid` `admin_steamid` BIGINT NOT NULL;
UPDATE `sa_admins`
SET player_steamid = '0'
WHERE player_steamid NOT REGEXP '^[0-9]+$';
ALTER TABLE `sa_admins` CHANGE `player_steamid` `player_steamid` BIGINT NULL DEFAULT NULL;

View File

@@ -0,0 +1,33 @@
-- Migration 016: Optimize tables and indexes
-- Add proper indexes for all tables to improve query performance
-- Optimize sa_players_ips table indexes
-- Add index on used_at for efficient date-based queries
ALTER TABLE `sa_players_ips` ADD INDEX IF NOT EXISTS `idx_used_at` (`used_at` DESC);
-- Optimize sa_bans table indexes
-- Add composite indexes for common query patterns
CREATE INDEX IF NOT EXISTS `idx_bans_steamid_status` ON `sa_bans` (`player_steamid`, `status`);
CREATE INDEX IF NOT EXISTS `idx_bans_ip_status` ON `sa_bans` (`player_ip`, `status`);
CREATE INDEX IF NOT EXISTS `idx_bans_status_ends` ON `sa_bans` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_server_status` ON `sa_bans` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_created` ON `sa_bans` (`created` DESC);
-- Optimize sa_admins table indexes
CREATE INDEX IF NOT EXISTS `idx_admins_steamid` ON `sa_admins` (`player_steamid`);
CREATE INDEX IF NOT EXISTS `idx_admins_server_ends` ON `sa_admins` (`server_id`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_admins_ends` ON `sa_admins` (`ends`);
-- Optimize sa_mutes table indexes (in addition to migration 014)
-- Add index for expire queries
CREATE INDEX IF NOT EXISTS `idx_mutes_status_ends` ON `sa_mutes` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_server_status` ON `sa_mutes` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_created` ON `sa_mutes` (`created` DESC);
-- Optimize sa_warns table indexes (if exists)
CREATE INDEX IF NOT EXISTS `idx_warns_steamid_status` ON `sa_warns` (`player_steamid`, `status`);
CREATE INDEX IF NOT EXISTS `idx_warns_status_ends` ON `sa_warns` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_warns_server_status` ON `sa_warns` (`server_id`, `status`, `ends`);
-- Add index on sa_servers for faster lookups
CREATE INDEX IF NOT EXISTS `idx_servers_hostname` ON `sa_servers` (`hostname`);

View File

@@ -0,0 +1,47 @@
CREATE TABLE IF NOT EXISTS `sa_bans` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`player_name` VARCHAR(128),
`player_steamid` VARCHAR(64),
`player_ip` VARCHAR(128),
`admin_steamid` VARCHAR(64) NOT NULL,
`admin_name` VARCHAR(128) NOT NULL,
`reason` VARCHAR(255) NOT NULL,
`duration` INTEGER NOT NULL,
`ends` TIMESTAMP NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`server_id` INTEGER NULL,
`status` TEXT NOT NULL DEFAULT 'ACTIVE'
);
CREATE TABLE IF NOT EXISTS `sa_mutes` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`player_name` VARCHAR(128) NULL,
`player_steamid` VARCHAR(64) NOT NULL,
`admin_steamid` VARCHAR(64) NOT NULL,
`admin_name` VARCHAR(128) NOT NULL,
`reason` VARCHAR(255) NOT NULL,
`duration` INTEGER NOT NULL,
`ends` TIMESTAMP NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`type` TEXT NOT NULL DEFAULT 'GAG',
`server_id` INTEGER NULL,
`status` TEXT NOT NULL DEFAULT 'ACTIVE'
);
CREATE TABLE IF NOT EXISTS `sa_admins` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`player_name` VARCHAR(128) NOT NULL,
`player_steamid` VARCHAR(64) NOT NULL,
`flags` TEXT NULL,
`immunity` INTEGER NOT NULL DEFAULT 0,
`server_id` INTEGER NULL,
`ends` TIMESTAMP NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `sa_servers` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`hostname` VARCHAR(128) NOT NULL,
`address` VARCHAR(64) NOT NULL,
UNIQUE (`address`)
);

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `sa_admins_flags` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`admin_id` INTEGER NOT NULL,
`flag` VARCHAR(64) NOT NULL,
FOREIGN KEY (`admin_id`) REFERENCES `sa_admins` (`id`) ON DELETE CASCADE
);

View File

@@ -0,0 +1,46 @@
INSERT INTO sa_admins_flags (admin_id, flag)
WITH RECURSIVE
min_admins AS (
SELECT MIN(id) AS admin_id, player_steamid, server_id
FROM sa_admins
WHERE player_steamid != 'Console'
GROUP BY player_steamid, server_id
),
split_flags AS (
SELECT
ma.admin_id,
sa.flags,
1 AS pos,
CASE
WHEN INSTR(sa.flags || ',', ',') = 0 THEN sa.flags
ELSE SUBSTR(sa.flags, 1, INSTR(sa.flags || ',', ',') - 1)
END AS flag,
CASE
WHEN INSTR(sa.flags || ',', ',') = 0 THEN ''
ELSE SUBSTR(sa.flags, INSTR(sa.flags || ',', ',') + 1)
END AS remaining
FROM min_admins ma
JOIN sa_admins sa ON ma.player_steamid = sa.player_steamid
AND (ma.server_id = sa.server_id OR (ma.server_id IS NULL AND sa.server_id IS NULL))
WHERE sa.flags IS NOT NULL AND sa.flags != ''
UNION ALL
SELECT
admin_id,
flags,
pos + 1,
CASE
WHEN INSTR(remaining || ',', ',') = 0 THEN remaining
ELSE SUBSTR(remaining, 1, INSTR(remaining || ',', ',') - 1)
END AS flag,
CASE
WHEN INSTR(remaining || ',', ',') = 0 THEN ''
ELSE SUBSTR(remaining, INSTR(remaining || ',', ',') + 1)
END AS remaining
FROM split_flags
WHERE remaining != ''
)
SELECT admin_id, TRIM(flag)
FROM split_flags
WHERE flag IS NOT NULL AND TRIM(flag) != '';

View File

@@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS `sa_unbans` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`ban_id` INTEGER NOT NULL,
`admin_id` INTEGER NOT NULL DEFAULT 0,
`reason` VARCHAR(255) NOT NULL DEFAULT 'Unknown',
`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `sa_unmutes` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`mute_id` INTEGER NOT NULL,
`admin_id` INTEGER NOT NULL DEFAULT 0,
`reason` VARCHAR(255) NOT NULL DEFAULT 'Unknown',
`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`)
VALUES (0, 'Console', 'Console', '', '0', NULL, NULL, CURRENT_TIMESTAMP);
UPDATE `sa_admins` SET `id` = 0 WHERE `id` = -1;
ALTER TABLE `sa_bans` ADD `unban_id` INTEGER NULL;
ALTER TABLE `sa_mutes` ADD `unmute_id` INTEGER NULL;

View File

@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS `sa_groups` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR(255) NOT NULL,
`immunity` INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS `sa_groups_flags` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`group_id` INTEGER NOT NULL,
`flag` VARCHAR(64) NOT NULL,
FOREIGN KEY (`group_id`) REFERENCES `sa_groups` (`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `sa_groups_servers` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`group_id` INTEGER NOT NULL,
`server_id` INTEGER NULL,
FOREIGN KEY (`group_id`) REFERENCES `sa_groups` (`id`) ON DELETE CASCADE
);
ALTER TABLE `sa_admins` ADD `group_id` INTEGER NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE `sa_mutes` ADD `passed` INTEGER NULL;

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `sa_players_ips` (
`steamid` INTEGER NOT NULL,
`address` VARCHAR(64) NOT NULL,
`used_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`steamid`, `address`)
);

View File

@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS `sa_warns` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`player_name` VARCHAR(128) DEFAULT NULL,
`player_steamid` VARCHAR(64) NOT NULL,
`admin_steamid` VARCHAR(64) NOT NULL,
`admin_name` VARCHAR(128) NOT NULL,
`reason` VARCHAR(255) NOT NULL,
`duration` INTEGER NOT NULL,
`ends` TIMESTAMP NOT NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`server_id` INTEGER DEFAULT NULL,
`status` TEXT NOT NULL DEFAULT 'ACTIVE'
);

View File

@@ -0,0 +1 @@
ALTER TABLE `sa_servers` ADD `rcon_password` VARCHAR(128) NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE `sa_bans` ADD COLUMN `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -0,0 +1,9 @@
DELETE FROM `sa_players_ips`
WHERE `id` NOT IN (
SELECT MIN(`id`)
FROM `sa_players_ips`
GROUP BY `steamid`
);
ALTER TABLE `sa_players_ips` ADD `name` VARCHAR(64) NULL DEFAULT NULL;
CREATE INDEX IF NOT EXISTS `idx_sa_players_ips_used_at` ON `sa_players_ips` (`used_at` DESC);

View File

@@ -0,0 +1,3 @@
CREATE INDEX IF NOT EXISTS `idx_sa_mutes_steamid_status_ends` ON `sa_mutes` (`player_steamid`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_sa_mutes_steamid_status_server_duration` ON `sa_mutes` (`player_steamid`, `status`, `server_id`, `duration`);
CREATE INDEX IF NOT EXISTS `idx_sa_mutes_steamid_type` ON `sa_mutes` (`player_steamid`, `type`);

View File

@@ -0,0 +1,15 @@
UPDATE `sa_bans`
SET admin_steamid = '0'
WHERE admin_steamid NOT GLOB '[0-9]*';
UPDATE `sa_mutes`
SET admin_steamid = '0'
WHERE admin_steamid NOT GLOB '[0-9]*';
UPDATE `sa_warns`
SET admin_steamid = '0'
WHERE admin_steamid NOT GLOB '[0-9]*';
UPDATE `sa_admins`
SET player_steamid = '0'
WHERE player_steamid NOT GLOB '[0-9]*';

View File

@@ -0,0 +1,33 @@
-- Migration 016: Optimize tables and indexes
-- Add proper indexes for all tables to improve query performance
-- Optimize sa_players_ips table indexes
-- Add index on used_at for efficient date-based queries
CREATE INDEX IF NOT EXISTS `idx_used_at` ON `sa_players_ips` (`used_at` DESC);
-- Optimize sa_bans table indexes
-- Add composite indexes for common query patterns
CREATE INDEX IF NOT EXISTS `idx_bans_steamid_status` ON `sa_bans` (`player_steamid`, `status`);
CREATE INDEX IF NOT EXISTS `idx_bans_ip_status` ON `sa_bans` (`player_ip`, `status`);
CREATE INDEX IF NOT EXISTS `idx_bans_status_ends` ON `sa_bans` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_server_status` ON `sa_bans` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_bans_created` ON `sa_bans` (`created` DESC);
-- Optimize sa_admins table indexes
CREATE INDEX IF NOT EXISTS `idx_admins_steamid` ON `sa_admins` (`player_steamid`);
CREATE INDEX IF NOT EXISTS `idx_admins_server_ends` ON `sa_admins` (`server_id`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_admins_ends` ON `sa_admins` (`ends`);
-- Optimize sa_mutes table indexes (in addition to migration 014)
-- Add index for expire queries
CREATE INDEX IF NOT EXISTS `idx_mutes_status_ends` ON `sa_mutes` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_server_status` ON `sa_mutes` (`server_id`, `status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_mutes_created` ON `sa_mutes` (`created` DESC);
-- Optimize sa_warns table indexes (if exists)
CREATE INDEX IF NOT EXISTS `idx_warns_steamid_status` ON `sa_warns` (`player_steamid`, `status`);
CREATE INDEX IF NOT EXISTS `idx_warns_status_ends` ON `sa_warns` (`status`, `ends`);
CREATE INDEX IF NOT EXISTS `idx_warns_server_status` ON `sa_warns` (`server_id`, `status`, `ends`);
-- Add index on sa_servers for faster lookups
CREATE INDEX IF NOT EXISTS `idx_servers_hostname` ON `sa_servers` (`hostname`);

View File

@@ -0,0 +1,393 @@
using System.Data.Common;
using MySqlConnector;
namespace CS2_SimpleAdmin.Database;
public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
{
public async Task<DbConnection> CreateConnectionAsync()
{
var connection = new MySqlConnection(connectionString);
await connection.OpenAsync();
await using var 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();
return connection;
}
public async Task<(bool Success, string? Exception)> CheckConnectionAsync()
{
try
{
await using var conn = await CreateConnectionAsync();
return (true, null);
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
public Task DatabaseMigrationAsync()
{
var 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
""";
}
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
""";
}
public string GetIpHistoryQuery()
{
return "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC";
}
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
""";
}
public string GetAdminsQuery()
{
return """
SELECT sa_admins.player_steamid, sa_admins.player_name, sa_admins_flags.flag, sa_admins.immunity, sa_admins.ends
FROM sa_admins_flags
JOIN sa_admins ON sa_admins_flags.admin_id = sa_admins.id
WHERE (sa_admins.ends IS NULL OR sa_admins.ends > @CurrentTime)
AND (sa_admins.server_id IS NULL OR sa_admins.server_id = @serverid)
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 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();";
public string GetGroupsQuery()
{
return """
SELECT g.group_id, sg.name AS group_name, sg.immunity, f.flag
FROM sa_groups_flags f
JOIN sa_groups_servers g ON f.group_id = g.group_id
JOIN sa_groups sg ON sg.id = g.group_id
WHERE (g.server_id = @serverid OR server_id IS NULL)
""";
}
public string GetAddAdminFlagsQuery() =>
"INSERT INTO sa_admins_flags (admin_id, flag) VALUES (@adminId, @flag);";
public string GetUpdateAdminGroupQuery() =>
"UPDATE sa_admins SET group_id = @groupId WHERE id = @adminId;";
public string GetAddGroupQuery() =>
"INSERT INTO sa_groups (name, immunity) VALUES (@groupName, @immunity); SELECT LAST_INSERT_ID();";
public string GetGroupIdByNameQuery() =>
"""
SELECT sgs.group_id
FROM sa_groups_servers sgs
JOIN sa_groups sg ON sgs.group_id = sg.id
WHERE sg.name = @groupName
ORDER BY (sgs.server_id = @serverId) DESC, sgs.server_id ASC
LIMIT 1;
""";
public string GetAddGroupFlagsQuery() =>
"INSERT INTO sa_groups_flags (group_id, flag) VALUES (@groupId, @flag);";
public string GetAddGroupServerQuery() =>
"INSERT INTO sa_groups_servers (group_id, server_id) VALUES (@groupId, @server_id);";
public string GetDeleteGroupQuery() =>
"DELETE FROM sa_groups WHERE name = @groupName;";
public string GetDeleteOldAdminsQuery() =>
"DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime;";
public string GetAddBanQuery()
{
return """
INSERT INTO `sa_bans`
(`player_steamid`, `player_name`, `player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @playerName, @playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
""";
}
public string GetAddBanBySteamIdQuery()
{
return """
INSERT INTO `sa_bans`
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
""";
}
public string GetAddBanByIpQuery()
{
return """
INSERT INTO `sa_bans`
(`player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
""";
}
public string GetUnbanRetrieveBansQuery(bool multiServer)
{
return multiServer
? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
: "SELECT id 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()
{
return "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
}
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();";
}
public string GetUpdateBanStatusQuery()
{
return "UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId";
}
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";
}
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";
}
public string GetAddMuteQuery(bool includePlayerName) =>
includePlayerName
? """
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");
public string GetMuteStatsQuery(bool multiServer) =>
multiServer
? """
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";
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 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";
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 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
? """
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";
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 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";
public string GetUnwarnLastQuery(bool multiServer) =>
multiServer
? """
UPDATE sa_warns
JOIN (
SELECT MAX(id) AS max_id
FROM sa_warns
WHERE player_steamid = @steamid AND status = 'ACTIVE'
) 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";
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

View File

@@ -0,0 +1,366 @@
using System.Data.Common;
using System.Data.SQLite;
namespace CS2_SimpleAdmin.Database;
public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
{
private readonly string _connectionString = $"Data Source={filePath}";
public async Task<DbConnection> CreateConnectionAsync()
{
var conn = new SQLiteConnection(_connectionString);
await conn.OpenAsync();
return conn;
}
public async Task<(bool Success, string? Exception)> CheckConnectionAsync()
{
try
{
await using var conn = await CreateConnectionAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT 1";
await cmd.ExecuteScalarAsync();
return (true, null);
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
public Task DatabaseMigrationAsync()
{
var migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Sqlite");
return migration.ExecuteMigrationsAsync();
}
public string GetBanSelectQuery(bool multiServer) =>
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
""";
public static string GetBanUpdatedSelectQuery(bool multiServer) =>
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
""";
public string GetIpHistoryQuery() =>
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC";
public string GetBanUpdateQuery(bool multiServer) =>
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
""";
public string GetAddBanQuery() =>
"""
INSERT INTO sa_bans
(player_steamid, player_name, player_ip, admin_steamid, admin_name, reason, duration, ends, created, server_id)
VALUES
(@playerSteamid, @playerName, @playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
SELECT last_insert_rowid();
""";
public string GetAddBanBySteamIdQuery() =>
"""
INSERT INTO sa_bans
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, server_id)
VALUES
(@playerSteamid, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
SELECT last_insert_rowid();
""";
public string GetAddBanByIpQuery() =>
"""
INSERT INTO sa_bans
(player_ip, admin_steamid, admin_name, reason, duration, ends, created, server_id)
VALUES
(@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
SELECT last_insert_rowid();
""";
public string GetUnbanRetrieveBansQuery(bool multiServer) =>
multiServer
? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
: "SELECT id 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 GetUpdateBanStatusQuery() =>
"UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId";
public string GetExpireBansQuery(bool multiServer) =>
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";
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";
public string GetAdminsQuery() =>
"""
SELECT sa_admins.player_steamid, sa_admins.player_name, sa_admins_flags.flag, sa_admins.immunity, sa_admins.ends
FROM sa_admins_flags
JOIN sa_admins ON sa_admins_flags.admin_id = sa_admins.id
WHERE (sa_admins.ends IS NULL OR sa_admins.ends > @CurrentTime)
AND (sa_admins.server_id IS NULL OR sa_admins.server_id = @serverid)
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 GetAddAdminQuery() =>
"""
INSERT INTO sa_admins (player_steamid, player_name, immunity, ends, created, server_id)
VALUES (@playerSteamId, @playerName, @immunity, @ends, @created, @serverid);
SELECT last_insert_rowid();
""";
public string GetGroupsQuery() =>
"""
SELECT g.group_id, sg.name AS group_name, sg.immunity, f.flag
FROM sa_groups_flags f
JOIN sa_groups_servers g ON f.group_id = g.group_id
JOIN sa_groups sg ON sg.id = g.group_id
WHERE (g.server_id = @serverid OR server_id IS NULL)
""";
public string GetAddAdminFlagsQuery() =>
"INSERT INTO sa_admins_flags (admin_id, flag) VALUES (@adminId, @flag);";
public string GetUpdateAdminGroupQuery() =>
"UPDATE sa_admins SET group_id = @groupId WHERE id = @adminId;";
public string GetAddGroupQuery() =>
"""
INSERT INTO sa_groups (name, immunity) VALUES (@groupName, @immunity);
SELECT last_insert_rowid();
""";
public string GetGroupIdByNameQuery() =>
"""
SELECT sgs.group_id
FROM sa_groups_servers sgs
JOIN sa_groups sg ON sgs.group_id = sg.id
WHERE sg.name = @groupName
ORDER BY (sgs.server_id = @serverId) DESC, sgs.server_id ASC
LIMIT 1;
""";
public string GetAddGroupFlagsQuery() =>
"INSERT INTO sa_groups_flags (group_id, flag) VALUES (@groupId, @flag);";
public string GetAddGroupServerQuery() =>
"INSERT INTO sa_groups_servers (group_id, server_id) VALUES (@groupId, @server_id);";
public string GetDeleteGroupQuery() =>
"DELETE FROM sa_groups WHERE name = @groupName;";
public string GetDeleteOldAdminsQuery() =>
"DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime;";
public string GetAddMuteQuery(bool includePlayerName) =>
includePlayerName
? """
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");
public string GetMuteStatsQuery(bool multiServer) =>
multiServer
? """
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";
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 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";
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 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
? """
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";
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 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";
public string GetUnwarnLastQuery(bool multiServer) =>
multiServer
? """
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";
}

View File

@@ -1,9 +1,9 @@
using CounterStrikeSharp.API;
using System.Numerics;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Utils;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Models;
using CS2_SimpleAdminApi;
@@ -17,9 +17,14 @@ namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
private bool _serverLoading;
private void RegisterEvents()
{
RegisterListener<Listeners.OnMapStart>(OnMapStart);
// RegisterListener<Listeners.OnClientConnect>(OnClientConnect);
RegisterListener<Listeners.OnClientConnect>(OnClientConnect);
RegisterListener<Listeners.OnClientConnected>(OnClientConnected);
RegisterListener<Listeners.OnGameServerSteamAPIActivated>(OnGameServerSteamAPIActivated);
if (Config.OtherSettings.UserMessageGagChatType)
HookUserMessage(118, HookUmChat);
@@ -30,6 +35,22 @@ public partial class CS2_SimpleAdmin
// AddCommandListener("say_team", OnCommandTeamSay);
}
private void UnregisterEvents()
{
RemoveListener<Listeners.OnMapStart>(OnMapStart);
RemoveListener<Listeners.OnClientConnect>(OnClientConnect);
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();
@@ -49,22 +70,35 @@ public partial class CS2_SimpleAdmin
private void OnGameServerSteamAPIActivated()
{
if (ServerLoaded || _serverLoading)
return;
_serverLoading = true;
new ServerManager().LoadServerData();
}
[GameEventHandler]
[GameEventHandler(HookMode.Pre)]
public HookResult OnClientDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
{
if (@event.Reason is 149 or 6)
info.DontBroadcast = true;
var player = @event.Userid;
#if DEBUG
Logger.LogCritical("[OnClientDisconnect] Before");
#endif
if (player == null || !player.IsValid || player.IsBot)
{
if (player == null || !player.IsValid || player.IsHLTV)
return HookResult.Continue;
BotPlayers.Remove(player);
CachedPlayers.Remove(player);
SilentPlayers.Remove(player.Slot);
if (player.IsBot)
return HookResult.Continue;
}
#if DEBUG
Logger.LogCritical("[OnClientDisconnect] After Check");
@@ -85,26 +119,30 @@ public partial class CS2_SimpleAdmin
}
else
{
DisconnectedPlayers.Add(new DisconnectedPlayer(steamId, player.PlayerName, player.IpAddress?.Split(":")[0], Time.ActualDateTime()));
DisconnectedPlayers.Add(new DisconnectedPlayer(steamId, player.PlayerName,
player.IpAddress?.Split(":")[0], Time.ActualDateTime()));
}
PlayerPenaltyManager.RemoveAllPenalties(player.Slot);
SilentPlayers.Remove(player.Slot);
GodPlayers.Remove(player.Slot);
SpeedPlayers.Remove(player.Slot);
GravityPlayers.Remove(player);
if (player.UserId.HasValue)
PlayersInfo.TryRemove(player.UserId.Value, out _);
PlayersInfo.TryRemove(player.SteamID, out _);
var authorizedSteamId = player.AuthorizedSteamID;
if (authorizedSteamId == null || !PermissionManager.AdminCache.TryGetValue(authorizedSteamId,
out var expirationTime)
|| !(expirationTime <= Time.ActualDateTime())) return HookResult.Continue;
if (!PermissionManager.AdminCache.TryGetValue(steamId, out var data)
|| !(data.ExpirationTime <= Time.ActualDateTime()))
{
return HookResult.Continue;
}
AdminManager.ClearPlayerPermissions(authorizedSteamId);
AdminManager.RemovePlayerAdminData(authorizedSteamId);
AdminManager.RemovePlayerPermissions(steamId, PermissionManager.AdminCache[steamId].Flags.ToArray());
AdminManager.RemovePlayerFromGroup(steamId, true, PermissionManager.AdminCache[steamId].Flags.ToArray());
var 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);
return HookResult.Continue;
}
@@ -115,16 +153,87 @@ public partial class CS2_SimpleAdmin
}
}
private void OnClientConnect(int playerslot, string name, string ipAddress)
{
#if DEBUG
Logger.LogCritical("[OnClientConnect]");
#endif
var 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);
if (player == null || !player.IsValid || player.IsBot)
return;
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)
{
#if DEBUG
Logger.LogCritical("[OnPlayerFullConnect]");
#endif
var player = @event.Userid;
if (player == null || !player.IsValid || player.IsBot)
if (player == null || !player.IsValid)
return HookResult.Continue;
new PlayerManager().LoadPlayerData(player);
if (player is { IsBot: true, IsHLTV: false })
{
BotPlayers.Add(player);
return HookResult.Continue;
}
PlayerManager.LoadPlayerData(player, true);
return HookResult.Continue;
}
@@ -135,10 +244,6 @@ public partial class CS2_SimpleAdmin
Logger.LogCritical("[OnRoundStart]");
#endif
GodPlayers.Clear();
SpeedPlayers.Clear();
GravityPlayers.Clear();
foreach (var player in PlayersInfo.Values)
{
player.DiePosition = null;
@@ -172,12 +277,25 @@ public partial class CS2_SimpleAdmin
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 is not null)
author.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", author.GetLanguage()));
return HookResult.Stop;
if (_localizer == null || endDateTime == null)
return HookResult.Continue;
// um.Recipients.Clear();
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 (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()));
}
private HookResult ComamndListenerHandler(CCSPlayerController? player, CommandInfo info)
@@ -209,8 +327,6 @@ public partial class CS2_SimpleAdmin
if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.PlayerConnected)
return HookResult.Continue;
Logger.LogInformation($"{player.PlayerName} {AdminManager.GetPlayerImmunity(player).ToString()} probuje wywalic {target.PlayerName} {AdminManager.GetPlayerImmunity(target).ToString()}");
return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue;
}
}
@@ -218,8 +334,17 @@ public partial class CS2_SimpleAdmin
if (!command.Contains("say"))
return HookResult.Continue;
if (!Config.OtherSettings.UserMessageGagChatType)
if (info.GetArg(1).Length == 0)
return HookResult.Stop;
var 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))
{
@@ -227,33 +352,21 @@ public partial class CS2_SimpleAdmin
player.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", player.GetLanguage()));
return HookResult.Stop;
}
}
if (info.GetArg(1).StartsWith($"/")
|| info.GetArg(1).StartsWith($"!"))
return HookResult.Continue;
if (info.GetArg(1).Length == 0)
return HookResult.Stop;
// }
if (command == "say" && info.GetArg(1).StartsWith($"@") &&
AdminManager.PlayerHasPermissions(player, "@vip/chat"))
if (AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat") && command == "say" && info.GetArg(1).StartsWith($"@"))
{
sb.Append(_localizer!["sa_vipchat_template", 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(p, "@vip/chat")))
{
p.PrintToChat(sb.ToString());
}
player.ExecuteClientCommandFromServer($"css_say {info.GetArg(1).Remove(0, 1)}");
return HookResult.Stop;
}
if (command != "say_team" || !info.GetArg(1).StartsWith($"@")) return HookResult.Continue;
StringBuilder sb = new();
if (AdminManager.PlayerHasPermissions(player, "@css/chat"))
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(p, "@css/chat")))
foreach (var 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());
}
@@ -262,7 +375,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(p, "@css/chat")))
foreach (var 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());
}
@@ -308,10 +421,10 @@ public partial class CS2_SimpleAdmin
StringBuilder sb = new();
if (AdminManager.PlayerHasPermissions(player, "@css/chat"))
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(p, "@css/chat")))
foreach (var 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());
}
@@ -320,7 +433,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(p, "@css/chat")))
foreach (var 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());
}
@@ -331,19 +444,22 @@ public partial class CS2_SimpleAdmin
private void OnMapStart(string mapName)
{
if (Config.OtherSettings.ReloadAdminsEveryMapChange && ServerLoaded && ServerId != null)
AddTimer(3.0f, () => ReloadAdmins(null));
if (!ServerLoaded || ServerId == null)
AddTimer(2.0f, OnGameServerSteamAPIActivated);
AddTimer(34, () =>
{
if (!ServerLoaded)
OnGameServerSteamAPIActivated();
});
if (Config.OtherSettings.ReloadAdminsEveryMapChange && ServerLoaded && ServerId != null)
AddTimer(5.0f, () => ReloadAdmins(null));
AddTimer(1.0f, ServerManager.CheckHibernationStatus);
// AddTimer(34, () =>
// {
// if (!ServerLoaded)
// OnGameServerSteamAPIActivated();
// });
GodPlayers.Clear();
SilentPlayers.Clear();
SpeedPlayers.Clear();
GravityPlayers.Clear();
PlayerPenaltyManager.RemoveAllPenalties();
}
@@ -353,11 +469,8 @@ public partial class CS2_SimpleAdmin
{
var player = @event.Userid;
if (player is null || @event.Attacker is null || !player.PawnIsAlive || player.PlayerPawn.Value == null)
if (player is null || @event.Attacker is null || player.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE || player.PlayerPawn.Value == null)
return HookResult.Continue;
if (SpeedPlayers.TryGetValue(player.Slot, out var speedPlayer))
player.SetSpeed(speedPlayer);
if (!GodPlayers.Contains(player.Slot)) return HookResult.Continue;
@@ -371,26 +484,24 @@ public partial class CS2_SimpleAdmin
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
if (player?.UserId == null || !player.IsValid || player.IsHLTV || player.Connected != PlayerConnectedState.PlayerConnected)
return HookResult.Continue;
SpeedPlayers.Remove(player.Slot);
GravityPlayers.Remove(player);
if (!PlayersInfo.ContainsKey(player.UserId.Value))
return HookResult.Continue;
PlayersInfo[player.UserId.Value].DiePosition = new DiePosition(
new Vector(
player.PlayerPawn.Value?.AbsOrigin?.X ?? 0,
player.PlayerPawn.Value?.AbsOrigin?.Y ?? 0,
player.PlayerPawn.Value?.AbsOrigin?.Z ?? 0
if (player?.UserId == null || !player.IsValid || player.IsHLTV ||
player.Connected != PlayerConnectedState.PlayerConnected || !PlayersInfo.ContainsKey(player.SteamID) ||
@event.Attacker == null)
return HookResult.Continue;
var playerPosition = player.PlayerPawn.Value?.AbsOrigin;
var playerRotation = player.PlayerPawn.Value?.AbsRotation;
PlayersInfo[player.SteamID].DiePosition = new DiePosition(
new Vector3(
playerPosition?.X ?? 0,
playerPosition?.Y ?? 0,
playerPosition?.Z ?? 0
),
new QAngle(
player.PlayerPawn.Value?.AbsRotation?.X ?? 0,
player.PlayerPawn.Value?.AbsRotation?.Y ?? 0,
player.PlayerPawn.Value?.AbsRotation?.Z ?? 0
new Vector3(
playerRotation?.X ?? 0,
playerRotation?.Y ?? 0,
playerRotation?.Z ?? 0
)
);
@@ -401,17 +512,17 @@ public partial class CS2_SimpleAdmin
public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info)
{
var player = @event.Userid;
if (player == null || !player.IsValid || player.IsBot)
return HookResult.Continue;
if (!SilentPlayers.Contains(player.Slot))
return HookResult.Continue;
info.DontBroadcast = true;
if (@event.Team > 1)
if (@event is { Oldteam: <= 1, Team: >= 1 })
{
SilentPlayers.Remove(player.Slot);
SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false);
}
return HookResult.Continue;
}

View File

@@ -0,0 +1,12 @@
namespace CS2_SimpleAdmin;
public static class EnumerableExtensions
{
public static IEnumerable<IEnumerable<T>> ChunkBy<T>(this IEnumerable<T> source, int chunkSize)
{
return source
.Select((x, i) => new { Index = i, Value = x })
.GroupBy(x => x.Index / chunkSize)
.Select(x => x.Select(v => v.Value));
}
}

View File

@@ -1,4 +1,6 @@
using CounterStrikeSharp.API;
using System.Drawing;
using System.Numerics;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
@@ -13,11 +15,21 @@ namespace CS2_SimpleAdmin;
public static class PlayerExtensions
{
/// <summary>
/// Slaps the player pawn by applying optional damage and adding a random velocity knockback.
/// </summary>
/// <param name="pawn">The player pawn to slap.</param>
/// <param name="damage">The amount of damage to apply (default is 0).</param>
public static void Slap(this CBasePlayerPawn pawn, int damage = 0)
{
PerformSlap(pawn, damage);
}
/// <summary>
/// Prints a localized chat message to the player with a prefix.
/// </summary>
/// <param name="controller">The player controller to send the message to.</param>
/// <param name="message">The message string.</param>
public static void Print(this CCSPlayerController controller, string message = "")
{
StringBuilder _message = new(CS2_SimpleAdmin._localizer!["sa_prefix"]);
@@ -25,6 +37,12 @@ public static class PlayerExtensions
controller.PrintToChat(_message.ToString());
}
/// <summary>
/// Determines if the player controller can target another player controller, respecting admin permissions and immunity.
/// </summary>
/// <param name="controller">The player controller who wants to target.</param>
/// <param name="target">The player controller being targeted.</param>
/// <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;
@@ -32,9 +50,29 @@ public static class PlayerExtensions
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);
}
/// <summary>
/// Checks if the controller can target a player by SteamID, considering targeting permissions and immunities.
/// </summary>
/// <param name="controller">The attacker player controller.</param>
/// <param name="steamId">The SteamID of the target player.</param>
/// <returns>True if targeting is permitted, false otherwise.</returns>
public static bool CanTarget(this CCSPlayerController? controller, SteamID steamId)
{
if (controller is null) return true;
return AdminManager.CanPlayerTarget(new SteamID(controller.SteamID), steamId) ||
AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(steamId);
}
/// <summary>
/// Sets the movement speed modifier of the player controller.
/// </summary>
/// <param name="controller">The player controller.</param>
/// <param name="speed">The speed modifier value.</param>
public static void SetSpeed(this CCSPlayerController? controller, float speed)
{
var playerPawnValue = controller?.PlayerPawn.Value;
@@ -43,14 +81,24 @@ public static class PlayerExtensions
playerPawnValue.VelocityModifier = speed;
}
/// <summary>
/// Sets the gravity scale for the player controller.
/// </summary>
/// <param name="controller">The player controller.</param>
/// <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;
playerPawnValue.GravityScale = gravity;
playerPawnValue.ActualGravityScale = gravity;
}
/// <summary>
/// Sets the player's in-game money amount.
/// </summary>
/// <param name="controller">The player controller.</param>
/// <param name="money">The amount of money to set.</param>
public static void SetMoney(this CCSPlayerController? controller, int money)
{
var moneyServices = controller?.InGameMoneyServices;
@@ -61,10 +109,15 @@ public static class PlayerExtensions
if (controller != null) Utilities.SetStateChanged(controller, "CCSPlayerController", "m_pInGameMoneyServices");
}
/// <summary>
/// Sets the player's health points.
/// </summary>
/// <param name="controller">The player controller.</param>
/// <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.PawnIsAlive || controller.PlayerPawn.Value == null)) return;
if (health <= 0 || controller.PlayerPawn.Value == null || controller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) return;
controller.PlayerPawn.Value.Health = health;
@@ -76,36 +129,76 @@ public static class PlayerExtensions
Utilities.SetStateChanged(controller.PlayerPawn.Value, "CBaseEntity", "m_iHealth");
}
/// <summary>
/// Buries the player pawn by moving it down by a depth offset.
/// </summary>
/// <param name="pawn">The player pawn to bury.</param>
/// <param name="depth">The depth offset (default 10 units).</param>
public static void Bury(this CBasePlayerPawn pawn, float depth = 10f)
{
var newPos = new Vector(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
var 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);
pawn.Teleport(newPos, pawn.AbsRotation!, pawn.AbsVelocity);
pawn.Teleport(newPos, newRotation, newVelocity);
}
/// <summary>
/// Unburies the player pawn by moving it up by a depth offset.
/// </summary>
/// <param name="pawn">The player pawn to unbury.</param>
/// <param name="depth">The depth offset (default 15 units).</param>
public static void Unbury(this CBasePlayerPawn pawn, float depth = 15f)
{
var newPos = new Vector(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
var 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);
pawn.Teleport(newPos, pawn.AbsRotation!, pawn.AbsVelocity);
pawn.Teleport(newPos, newRotation, newVelocity);
}
/// <summary>
/// Freezes the player pawn, disabling movement.
/// </summary>
/// <param name="pawn">The player pawn to freeze.</param>
public static void Freeze(this CBasePlayerPawn pawn)
{
pawn.MoveType = MoveType_t.MOVETYPE_INVALID;
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 11); // invalid
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
}
/// <summary>
/// Unfreezes the player pawn, enabling movement.
/// </summary>
/// <param name="pawn">The player pawn to unfreeze.</param>
public static void Unfreeze(this CBasePlayerPawn pawn)
{
pawn.MoveType = MoveType_t.MOVETYPE_WALK;
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>
/// <param name="pawn">The pawn to colorize.</param>
/// <param name="r">Red component (0-255).</param>
/// <param name="g">Green component (0-255).</param>
/// <param name="b">Blue component (0-255).</param>
/// <param name="a">Alpha (transparency) component (0-255).</param>
public static void Colorize(this CBasePlayerPawn pawn, int r = 255, int g = 255, int b = 255, int a = 255)
{
pawn.Render = Color.FromArgb(a, r, g, b);
Utilities.SetStateChanged(pawn, "CBaseModelEntity", "m_clrRender");
}
/// <summary>
/// Toggles noclip mode for the player pawn.
/// </summary>
/// <param name="pawn">The player pawn.</param>
public static void ToggleNoclip(this CBasePlayerPawn pawn)
{
if (pawn.MoveType == MoveType_t.MOVETYPE_NOCLIP)
@@ -122,6 +215,11 @@ public static class PlayerExtensions
}
}
/// <summary>
/// Renames the player controller to a new name, with fallback to a localized "Unknown".
/// </summary>
/// <param name="controller">The player controller to rename.</param>
/// <param name="newName">The new name to assign.</param>
public static void Rename(this CCSPlayerController? controller, string newName = "Unknown")
{
newName ??= CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
@@ -149,6 +247,11 @@ public static class PlayerExtensions
});
}
/// <summary>
/// Teleports a player controller to the position, rotation, and velocity of another player controller.
/// </summary>
/// <param name="controller">The controller to teleport.</param>
/// <param name="target">The target controller whose position to copy.</param>
public static void TeleportPlayer(this CCSPlayerController? controller, CCSPlayerController? target)
{
if (controller?.PlayerPawn.Value == null && target?.PlayerPawn.Value == null)
@@ -167,6 +270,11 @@ public static class PlayerExtensions
}
}
/// <summary>
/// Applies a slap effect to the given player pawn, optionally inflicting damage and adding velocity knockback.
/// </summary>
/// <param name="pawn">The player pawn to slap.</param>
/// <param name="damage">The amount of damage to deal (default is 0).</param>
private static void PerformSlap(CBasePlayerPawn pawn, int damage = 0)
{
if (pawn.LifeState != (int)LifeState_t.LIFE_ALIVE)
@@ -177,7 +285,7 @@ public static class PlayerExtensions
/* Teleport in a random direction - thank you, Mani!*/
/* Thank you AM & al!*/
var random = new Random();
var vel = new Vector(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
var 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);
@@ -208,6 +316,16 @@ public static class PlayerExtensions
pawn.CommitSuicide(true, true);
}
/// <summary>
/// Sends a localized chat message to the player controller.
/// The message is retrieved from the specified localizer using the given message key and optional formatting arguments.
/// Each line of the message is prefixed with a localized prefix string.
/// The message respects the player's configured language for proper localization.
/// </summary>
/// <param name="controller">The target player controller to receive the message.</param>
/// <param name="localizer">The string localizer used for localization.</param>
/// <param name="messageKey">The key identifying the localized message.</param>
/// <param name="messageArgs">Optional arguments to format the localized message.</param>
public static void SendLocalizedMessage(this CCSPlayerController? controller, IStringLocalizer? localizer,
string messageKey, params object[] messageArgs)
{
@@ -217,15 +335,25 @@ public static class PlayerExtensions
{
StringBuilder sb = new();
sb.Append(localizer[messageKey, messageArgs]);
foreach (var part in Helper.SeparateLines(sb.ToString()))
{
var lineWithPrefix = localizer["sa_prefix"] + part.Trim();
var lineWithPrefix = (CS2_SimpleAdmin._localizer?["sa_prefix"] ?? "") + part.Trim();
controller.PrintToChat(lineWithPrefix);
}
}
}
/// <summary>
/// Sends a localized chat message to the player controller, centered horizontally on the player's screen.
/// The message is retrieved from the specified localizer using the given message key and optional formatting arguments.
/// Each line of the message is centered and prefixed with a localized prefix string.
/// The message respects the player's configured language for localization.
/// </summary>
/// <param name="controller">The target player controller to receive the message.</param>
/// <param name="localizer">The string localizer used for localization.</param>
/// <param name="messageKey">The key identifying the localized message.</param>
/// <param name="messageArgs">Optional arguments to format the localized message.</param>
public static void SendLocalizedMessageCenter(this CCSPlayerController? controller, IStringLocalizer? localizer,
string messageKey, params object[] messageArgs)
{

View File

@@ -22,6 +22,7 @@ using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CS2_SimpleAdmin.Managers;
using MenuManager;
using ZLinq;
namespace CS2_SimpleAdmin;
@@ -48,42 +49,34 @@ internal static class Helper
public static List<CCSPlayerController> GetPlayerFromName(string name)
{
return Utilities.GetPlayers().FindAll(x => x.PlayerName.Equals(name, StringComparison.OrdinalIgnoreCase));
return GetValidPlayers().FindAll(x => x.PlayerName.Equals(name, StringComparison.OrdinalIgnoreCase));
}
public static List<CCSPlayerController> GetPlayerFromSteamid64(string steamid)
public static CCSPlayerController? GetPlayerFromSteamid64(ulong steamid)
{
return GetValidPlayers().FindAll(x =>
x.SteamID.ToString().Equals(steamid, StringComparison.OrdinalIgnoreCase)
);
return GetValidPlayers().FirstOrDefault(x => x.SteamID == steamid);
}
public static List<CCSPlayerController> GetPlayerFromIp(string ipAddress)
{
return GetValidPlayers().FindAll(x =>
x.IpAddress != null &&
x.IpAddress.Split(":")[0].Equals(ipAddress)
);
return CS2_SimpleAdmin.CachedPlayers.FindAll(x => x.IpAddress != null && x.IpAddress.Split(":")[0].Equals(ipAddress));
}
public static List<CCSPlayerController> GetValidPlayers()
{
return Utilities.GetPlayers().FindAll(p => p is
{ IsValid: true, IsBot: false, Connected: PlayerConnectedState.PlayerConnected });
return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().ToList();
}
public static List<CCSPlayerController> GetValidPlayersWithBots()
{
return CS2_SimpleAdmin.CachedPlayers.Concat(CS2_SimpleAdmin.BotPlayers).AsValueEnumerable().ToList();
}
public static IEnumerable<CCSPlayerController?> GetValidPlayersWithBots()
{
return Utilities.GetPlayers().FindAll(p =>
p is { IsValid: true, IsBot: false, IsHLTV: false } or { IsValid: true, IsBot: true, IsHLTV: false }
);
}
public static bool IsValidSteamId64(string input)
{
const string pattern = @"^\d{17}$";
return Regex.IsMatch(input, pattern);
}
// public static bool IsValidSteamId64(string input)
// {
// const string pattern = @"^\d{17}$";
// return Regex.IsMatch(input, pattern);
// }
public static bool ValidateSteamId(string input, out SteamID? steamId)
{
@@ -93,7 +86,7 @@ internal static class Helper
{
return false;
}
if (!SteamID.TryParse(input, out var parsedSteamId)) return false;
steamId = parsedSteamId;
@@ -137,14 +130,67 @@ internal static class Helper
}
}
public static void KickPlayer(int userId, NetworkDisconnectionReason reason = NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED)
public static void KickPlayer(int userId, NetworkDisconnectionReason reason = NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED, int delay = 0)
{
var player = Utilities.GetPlayerFromUserid(userId);
if (player == null || !player.IsValid || player.IsHLTV)
return;
if (player.UserId.HasValue && CS2_SimpleAdmin.PlayersInfo.TryGetValue(player.SteamID, out var value))
value.WaitingForKick = true;
player.Disconnect(reason);
// player.CommitSuicide(true, true);
player.VoiceFlags = VoiceFlags.Muted;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn != null && playerPawn.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
playerPawn.Freeze();
playerPawn.Colorize(255, 0, 0);
var weaponServices = playerPawn.WeaponServices;
if (weaponServices == null)
return;
foreach (var _weap in weaponServices.MyWeapons)
{
var weapon = _weap.Value;;
if (weapon == null || !weapon.IsValid)
continue;
if (weapon.DesignerName.Contains("c4") || weapon.DesignerName.Contains("healthshot"))
continue;
weapon.NextPrimaryAttackTick = Server.TickCount + 999;
weapon.NextSecondaryAttackTick = Server.TickCount + 999;
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick");
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick");
}
}
if (delay > 0)
{
CS2_SimpleAdmin.Instance.AddTimer(delay, () =>
{
if (!player.IsValid || player.IsHLTV)
return;
// Server.ExecuteCommand($"kickid {player.UserId}");
playerPawn?.Colorize();
player.Disconnect(reason);
});
}
else
{
// Server.ExecuteCommand($"kickid {player.UserId}");
playerPawn?.Colorize();
player.Disconnect(reason);
}
if (CS2_SimpleAdmin.UnlockedCommands && reason == NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED)
Server.ExecuteCommand($"banid 1 {new SteamID(player.SteamID).SteamId3}");
// if (!string.IsNullOrEmpty(reason))
// {
@@ -158,6 +204,141 @@ internal static class Helper
//
// Server.ExecuteCommand($"kickid {userId} {reason}");
}
public static void KickPlayer(CCSPlayerController player, NetworkDisconnectionReason reason = NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED, int delay = 0)
{
if (!player.IsValid || player.IsHLTV)
return;
if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(player.SteamID, out var value))
{
if (value.WaitingForKick)
return;
value.WaitingForKick = true;
}
player.VoiceFlags = VoiceFlags.Muted;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn != null && playerPawn.LifeState == (int)LifeState_t.LIFE_ALIVE)
{
playerPawn.Freeze();
playerPawn.Colorize(255, 0, 0);
var weaponServices = playerPawn.WeaponServices;
if (weaponServices == null)
return;
foreach (var _weap in weaponServices.MyWeapons)
{
var weapon = _weap.Value;
;
if (weapon == null || !weapon.IsValid)
continue;
if (weapon.DesignerName.Contains("c4") || weapon.DesignerName.Contains("healthshot"))
continue;
weapon.NextPrimaryAttackTick = Server.TickCount + 999;
weapon.NextSecondaryAttackTick = Server.TickCount + 999;
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick");
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick");
}
}
if (delay > 0)
{
CS2_SimpleAdmin.Instance.AddTimer(delay, () =>
{
if (!player.IsValid || player.IsHLTV)
return;
// if (!string.IsNullOrEmpty(reason))
// {
// var escapeChars = reason.IndexOfAny([';', '|']);
//
// if (escapeChars != -1)
// {
// reason = reason[..escapeChars];
// }
// }
//
// Server.ExecuteCommand($"kickid {player.UserId}");
player.Disconnect(reason);
});
}
else
{
// Server.ExecuteCommand($"kickid {player.UserId}");
player.Disconnect(reason);
}
if (CS2_SimpleAdmin.UnlockedCommands && reason == NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED)
Server.ExecuteCommand($"banid 1 {new SteamID(player.SteamID).SteamId3}");
// if (!string.IsNullOrEmpty(reason))
// {
// var escapeChars = reason.IndexOfAny([';', '|']);
//
// if (escapeChars != -1)
// {
// reason = reason[..escapeChars];
// }
// }
//
// Server.ExecuteCommand($"kickid {userId} {reason}");
}
public static int ParsePenaltyTime(string time)
{
time = time.ToLower();
if (string.IsNullOrWhiteSpace(time) || !time.Any(char.IsDigit))
{
// CS2_SimpleAdmin._logger?.LogError("Time string cannot be null or empty.");
return -1;
}
if (time.Equals($"0"))
return 0;
var timeUnits = new Dictionary<string, int>
{
{ "m", 1 }, // Minute
{ "h", 60 }, // Hour
{ "d", 1440 }, // Day (24 * 60)
{ "w", 10080 }, // Week (7 * 24 * 60)
{ "mo", 43200 }, // Month (30 * 24 * 60)
{ "y", 525600 } // Year (365 * 24 * 60)
};
// Check if the input is purely numeric (e.g., "10" for 10 minutes)
if (int.TryParse(time, out var numericMinutes))
{
return numericMinutes;
}
int totalMinutes = 0;
var regex = new Regex(@"(\d+)([a-z]+)");
var matches = regex.Matches(time);
foreach (Match match in matches)
{
var value = int.Parse(match.Groups[1].Value); // Numeric part
var unit = match.Groups[2].Value; // Unit part
if (timeUnits.TryGetValue(unit, out var minutesPerUnit))
{
totalMinutes += value * minutesPerUnit;
}
else
{
throw new ArgumentException($"Invalid time unit '{unit}' in time string.", nameof(time));
}
}
return totalMinutes > 0 ? totalMinutes : -1;
}
public static void PrintToCenterAll(string message)
{
@@ -182,7 +363,6 @@ internal static class Helper
return;
var playerName = caller?.PlayerName ?? CS2_SimpleAdmin._localizer["sa_console"];
var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer["sa_unknown"];
CS2_SimpleAdmin.Instance.Logger.LogInformation($"{CS2_SimpleAdmin._localizer[
@@ -258,15 +438,23 @@ internal static class Helper
_ = CS2_SimpleAdmin.DiscordWebhookClientLog.SendMessageAsync(GenerateMessageDiscord(localizer["sa_discord_log_command", $"[{callerName}]({communityUrl})", command]));
}
public static void ShowAdminActivity(string messageKey, string? callerName = null, params object[] messageArgs)
public static void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs)
{
string[] publishActions = ["ban", "gag", "silence", "mute"];
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return;
if (CS2_SimpleAdmin._localizer == null) return;
// Determine the localized message key
var localizedMessageKey = $"{messageKey}";
if (string.IsNullOrWhiteSpace(callerName))
callerName = CS2_SimpleAdmin._localizer["sa_console"];
var formattedMessageArgs = messageArgs.Select(arg => arg.ToString() ?? string.Empty).ToArray();
if (!dontPublish && publishActions.Any(messageKey.Contains))
{
CS2_SimpleAdmin.SimpleAdminApi?.OnAdminShowActivityEvent(messageKey, callerName, dontPublish, messageArgs);
}
// // Replace placeholder based on showActivityType
// for (var i = 0; i < formattedMessageArgs.Length; i++)
// {
@@ -279,8 +467,19 @@ internal static class Helper
// _ => arg
// };
// }
var validPlayers = GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false });
foreach (var controller in GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false }))
if (!validPlayers.Any())
return;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 3)
{
validPlayers = validPlayers.Where(c =>
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") ||
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban"));
}
foreach (var controller in validPlayers.ToList())
{
var currentMessageArgs = (string[])formattedMessageArgs.Clone();
@@ -290,14 +489,96 @@ internal static class Helper
var arg = currentMessageArgs[i];
currentMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{
1 => arg.Replace("CALLER", AdminManager.PlayerHasPermissions(controller, "@css/kick") || AdminManager.PlayerHasPermissions(controller, "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]),
2 => arg.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]),
_ => arg
1 => arg.Replace("CALLER", AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]),
_ => arg.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]),
};
}
// Send the localized message to each player
controller.SendLocalizedMessage(CS2_SimpleAdmin._localizer, localizedMessageKey, currentMessageArgs.Cast<object>().ToArray());
controller.SendLocalizedMessage(CS2_SimpleAdmin._localizer, messageKey, currentMessageArgs.Cast<object>().ToArray());
}
}
/// <summary>
/// Shows admin activity with a custom translated message (for modules with their own localizer).
/// </summary>
public static void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null, bool dontPublish = false)
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return;
if (CS2_SimpleAdmin._localizer == null) return;
if (string.IsNullOrWhiteSpace(callerName))
callerName = CS2_SimpleAdmin._localizer["sa_console"];
var validPlayers = GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false });
if (!validPlayers.Any())
return;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 3)
{
validPlayers = validPlayers.Where(c =>
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") ||
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban"));
}
foreach (var controller in validPlayers.ToList())
{
// Replace "CALLER" placeholder based on showActivityType
var message = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{
1 => translatedMessage.Replace("CALLER", AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]),
_ => translatedMessage.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]),
};
// Send the pre-translated message to the player
controller.PrintToChat(message);
}
}
/// <summary>
/// Shows admin activity using module's localizer for per-player language support.
/// Each player receives the message in their configured language using SendLocalizedMessage.
/// </summary>
public static void ShowAdminActivityLocalized(IStringLocalizer moduleLocalizer, string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs)
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 0) return;
if (CS2_SimpleAdmin._localizer == null) return;
if (string.IsNullOrWhiteSpace(callerName))
callerName = CS2_SimpleAdmin._localizer["sa_console"];
var formattedMessageArgs = messageArgs.Select(arg => arg.ToString() ?? string.Empty).ToArray();
var validPlayers = GetValidPlayers().Where(c => c is { IsValid: true, IsBot: false });
if (!validPlayers.Any())
return;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType == 3)
{
validPlayers = validPlayers.Where(c =>
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/kick") ||
AdminManager.PlayerHasPermissions(new SteamID(c.SteamID), "@css/ban"));
}
foreach (var controller in validPlayers.ToList())
{
var currentMessageArgs = (string[])formattedMessageArgs.Clone();
// Replace "CALLER" placeholder based on showActivityType
for (var i = 0; i < currentMessageArgs.Length; i++)
{
var arg = currentMessageArgs[i];
currentMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{
1 => arg.Replace("CALLER", AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/kick") || AdminManager.PlayerHasPermissions(new SteamID(controller.SteamID), "@css/ban") ? callerName : CS2_SimpleAdmin._localizer["sa_admin"]),
_ => arg.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]),
};
}
// Send the localized message to each player using their language
controller.SendLocalizedMessage(moduleLocalizer, messageKey, currentMessageArgs.Cast<object>().ToArray());
}
}
@@ -309,25 +590,20 @@ internal static class Helper
{
if (CS2_SimpleAdmin._localizer == null) return;
// Determine the localized message key
var localizedMessageKey = $"{messageKey}";
var formattedMessageArgs = messageArgs.Select(arg => arg?.ToString() ?? string.Empty).ToArray();
// Replace placeholder based on showActivityType
for (var i = 0; i < formattedMessageArgs.Length; i++)
{
var arg = formattedMessageArgs[i]; // Convert argument to string if not null
// Replace "CALLER" placeholder in the argument string
var arg = formattedMessageArgs[i];
formattedMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch
{
1 => arg.Replace("CALLER", CS2_SimpleAdmin._localizer["sa_admin"]),
2 => arg.Replace("CALLER", callerName ?? CS2_SimpleAdmin._localizer["sa_console"]),
_ => arg
};
}
// Print the localized message to the center of the screen for the player
using (new WithTemporaryCulture(player.GetLanguage()))
{
player.PrintToCenter(CS2_SimpleAdmin._localizer[localizedMessageKey, formattedMessageArgs.Cast<object>().ToArray()]);
@@ -344,7 +620,7 @@ internal static class Helper
public static void SendDiscordPenaltyMessage(CCSPlayerController? caller, CCSPlayerController? target, string reason, int duration, PenaltyType penalty, IStringLocalizer? localizer)
{
if (localizer == null) return;
var penaltySetting = penalty switch
{
PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyBanSettings,
@@ -358,7 +634,7 @@ internal static class Helper
var webhookUrl = penaltySetting.FirstOrDefault(s => s.Name.Equals("Webhook"))?.Value;
if (string.IsNullOrEmpty(webhookUrl)) return;
const string defaultCommunityUrl = "<https://steamcommunity.com/profiles/0>";
var callerCommunityUrl = caller != null ? $"<{new SteamID(caller.SteamID).ToCommunityUrl()}>" : defaultCommunityUrl;
var targetCommunityUrl = target != null ? $"<{new SteamID(target.SteamID).ToCommunityUrl()}>" : defaultCommunityUrl;
@@ -391,10 +667,8 @@ internal static class Helper
bool[] inlineFlags = [true, true, true, false, false];
var hostname = ConVar.Find("hostname")?.StringValue ?? localizer["sa_unknown"];
var colorHex = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value ?? "#FFFFFF";
if (string.IsNullOrEmpty(colorHex))
colorHex = "#FFFFFF";
var colorValue = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value;
var colorHex = string.IsNullOrWhiteSpace(colorValue) ? "#FFFFFF" : colorValue.Trim();
var embed = new Embed
{
@@ -438,10 +712,10 @@ internal static class Helper
});
}
public static void SendDiscordPenaltyMessage(CCSPlayerController? caller, string steamId, string reason, int duration, PenaltyType penalty, IStringLocalizer? localizer)
public static void SendDiscordPenaltyMessage(CCSPlayerController? caller, string steamId, string reason, int duration, PenaltyType penalty, IStringLocalizer? localizer)
{
if (localizer == null) return;
var penaltySetting = penalty switch
{
PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.Discord.DiscordPenaltyBanSettings,
@@ -487,7 +761,8 @@ internal static class Helper
bool[] inlineFlags = [true, true, true, false, false];
var hostname = ConVar.Find("hostname")?.StringValue ?? localizer["sa_unknown"];
var colorHex = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value ?? "#FFFFFF";
var colorValue = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value;
var colorHex = string.IsNullOrWhiteSpace(colorValue) ? "#FFFFFF" : colorValue.Trim();
var embed = new Embed
{
@@ -525,12 +800,11 @@ internal static class Helper
}
catch (Exception ex)
{
// Log or handle the exception
Console.WriteLine(ex);
}
});
}
private static string GenerateMessageDiscord(string message)
{
var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
@@ -603,7 +877,7 @@ internal static class Helper
if (CS2_SimpleAdmin.DiscordWebhookClientLog == null || CS2_SimpleAdmin._localizer == null)
return;
if (caller != null && caller.IsValid == false)
if (caller != null && !caller.IsValid)
caller = null;
var callerName = caller == null ? CS2_SimpleAdmin._localizer["sa_console"] : caller.PlayerName;
@@ -615,32 +889,38 @@ internal static class Helper
commandString]));
}
public static IMenu? CreateMenu(string title)
#pragma warning disable CS8604
public static IMenu? CreateMenu(string title, Action<CCSPlayerController>? backAction = null, Action<CCSPlayerController>? resetAction = null)
{
if (CS2_SimpleAdmin.MenuApi == null)
{
return null;
}
var menuType = CS2_SimpleAdmin.Instance.Config.MenuConfigs.MenuType.ToLower();
var menu = menuType switch
{
_ when menuType.Equals("selectable", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.NewMenu(title),
CS2_SimpleAdmin.MenuApi.GetMenu(title, backAction, resetAction),
_ when menuType.Equals("dynamic", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.NewMenuForcetype(title, MenuType.ButtonMenu),
CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ButtonMenu, backAction, resetAction),
_ when menuType.Equals("center", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.NewMenuForcetype(title, MenuType.CenterMenu),
CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.CenterMenu, backAction, resetAction),
_ when menuType.Equals("chat", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.NewMenuForcetype(title, MenuType.ChatMenu),
CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ChatMenu, backAction, resetAction),
_ when menuType.Equals("console", StringComparison.CurrentCultureIgnoreCase) =>
CS2_SimpleAdmin.MenuApi?.NewMenuForcetype(title, MenuType.ConsoleMenu),
CS2_SimpleAdmin.MenuApi.GetMenuForcetype(title, MenuType.ConsoleMenu, backAction, resetAction),
_ => CS2_SimpleAdmin.MenuApi?.NewMenu(title)
_ => CS2_SimpleAdmin.MenuApi.GetMenu(title, backAction, resetAction)
};
return menu;
}
#pragma warning restore CS8604
internal static IPluginManager? GetPluginManager()
{
@@ -653,15 +933,18 @@ internal static class Helper
return pluginManager;
}
}
public static class PluginInfo
{
internal static async Task CheckVersion(string localVersion, ILogger logger)
{
ShowAd(localVersion);
const string versionUrl = "https://raw.githubusercontent.com/daffyyyy/CS2-SimpleAdmin/main/CS2-SimpleAdmin/VERSION";
var client = CS2_SimpleAdmin.HttpClient;
client.Timeout = TimeSpan.FromSeconds(3);
try
{
var response = await client.GetAsync(versionUrl);
@@ -700,8 +983,8 @@ public static class PluginInfo
logger.LogError(ex, "An error occurred while checking version.");
}
}
internal static void ShowAd(string moduleVersion)
private static void ShowAd(string moduleVersion)
{
Console.WriteLine(" ");
Console.WriteLine(" _______ ___ __ __ _______ ___ _______ _______ ______ __ __ ___ __ _ ");
@@ -743,6 +1026,7 @@ public static class Time
{
public static DateTime ActualDateTime()
{
return DateTime.UtcNow;
string timezoneId = CS2_SimpleAdmin.Instance.Config.Timezone;
DateTime utcNow = DateTime.UtcNow;
@@ -826,3 +1110,48 @@ public static class WeaponHelper
return filteredWeapons; // Return all relevant matches for the partial input
}
}
public static class IpHelper
{
public static uint IpToUint(string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP address cannot be null or empty.", nameof(ipAddress));
if (!System.Net.IPAddress.TryParse(ipAddress, out var ip))
throw new FormatException($"Invalid IP address format: {ipAddress}");
// Ensure it's IPv4 (IPv6 will throw)
if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
throw new FormatException("Only IPv4 addresses are supported.");
byte[] bytes = ip.GetAddressBytes();
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes); // Ensure big-endian (network order)
return BitConverter.ToUInt32(bytes, 0);
}
public static bool TryConvertIpToUint(string ipString, out uint ipUint)
{
ipUint = 0;
if (string.IsNullOrWhiteSpace(ipString))
return false;
if (!System.Net.IPAddress.TryParse(ipString, out var ipAddress))
return false;
var bytes = ipAddress.GetAddressBytes();
if (bytes.Length != 4)
return false;
ipUint = IpToUint(ipString);
return true;
}
public static string UintToIp(uint ipAddress)
{
var bytes = BitConverter.GetBytes(ipAddress).Reverse().ToArray();
return new System.Net.IPAddress(bytes).ToString();
}
}

View File

@@ -5,31 +5,36 @@ using Dapper;
using Microsoft.Extensions.Logging;
using MySqlConnector;
using System.Text;
using CS2_SimpleAdmin.Database;
namespace CS2_SimpleAdmin.Managers;
internal class BanManager(Database.Database? database)
internal class BanManager(IDatabaseProvider? databaseProvider)
{
public async Task BanPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
/// <summary>
/// Bans an online player and inserts the ban record into the database.
/// </summary>
/// <param name="player">The player to be banned (must be currently online).</param>
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
/// <param name="reason">The reason for the ban.</param>
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
/// <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 (database == null) return;
if (databaseProvider == null) return null;
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
await using MySqlConnection connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
try
{
const string sql =
"INSERT INTO `sa_bans` (`player_steamid`, `player_name`, `player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) " +
"VALUES (@playerSteamid, @playerName, @playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid)";
await connection.ExecuteAsync(sql, new
var sql = databaseProvider.GetAddBanQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64.ToString(),
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
playerIp = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1 ? player.IpAddress : null,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
banReason = reason,
duration = time,
@@ -37,30 +42,39 @@ internal class BanManager(Database.Database? database)
created = now,
serverid = CS2_SimpleAdmin.ServerId
});
return banId;
}
catch(Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
return null;
}
catch { }
}
public async Task AddBanBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
/// <summary>
/// Adds a ban for an offline player identified by their SteamID.
/// </summary>
/// <param name="playerSteamId">The SteamID64 of the player to ban.</param>
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
/// <param name="reason">The reason for the ban.</param>
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
/// <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 (database == null) return;
if (string.IsNullOrEmpty(playerSteamId)) return;
if (databaseProvider == null) return null;
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using MySqlConnection connection = await database.GetConnectionAsync();
var sql = "INSERT INTO `sa_bans` (`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) " +
"VALUES (@playerSteamid, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid)";
await connection.ExecuteAsync(sql, new
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddBanBySteamIdQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
banReason = reason,
duration = time,
@@ -68,13 +82,26 @@ internal class BanManager(Database.Database? database)
created = now,
serverid = CS2_SimpleAdmin.ServerId
});
return banId;
}
catch(Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
return null;
}
catch { }
}
/// <summary>
/// Adds a ban for an offline player identified by their IP address.
/// </summary>
/// <param name="playerIp">The IP address of the player to ban.</param>
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
/// <param name="reason">The reason for the ban.</param>
/// <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 (database == null) return;
if (databaseProvider == null) return;
if (string.IsNullOrEmpty(playerIp)) return;
@@ -83,15 +110,13 @@ internal class BanManager(Database.Database? database)
try
{
await using MySqlConnection connection = await database.GetConnectionAsync();
var sql = "INSERT INTO `sa_bans` (`player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) " +
"VALUES (@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid)";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddBanByIpQuery();
await connection.ExecuteAsync(sql, new
{
playerIp,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
banReason = reason,
duration = time,
@@ -103,336 +128,308 @@ internal class BanManager(Database.Database? database)
catch { }
}
public async Task<bool> IsPlayerBanned(PlayerInfo player)
// 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.
/// </summary>
/// <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 })
{
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
{
var 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;
""";
await using var connection = await database.GetConnectionAsync();
var parameters = new
{
PlayerSteamID = player.SteamId.SteamId64.ToString(),
PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
string.IsNullOrEmpty(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;
return;
}
public async Task<int> GetPlayerBans(PlayerInfo player)
try
{
if (database == null) return 0;
await using var connection = await databaseProvider.CreateConnectionAsync();
var sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
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.ToString(),
PlayerIP = player.IpAddress,
serverid = CS2_SimpleAdmin.ServerId
});
}
else
{
banCount = await connection.ExecuteScalarAsync<int>(sql,
new
{
PlayerSteamID = player.SteamId.SteamId64.ToString(),
PlayerIP = DBNull.Value,
serverid = CS2_SimpleAdmin.ServerId
});
}
return banCount;
}
catch { }
return 0;
}
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
{
if (database == null) return;
if (playerPattern is not { Length: > 1 })
{
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;
}
try
var sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
var adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
foreach (var ban in bansList)
{
await using var connection = await database.GetConnectionAsync();
int banId = ban.id;
var sqlRetrieveBans = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
: "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
var sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
var unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
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;
const string sqlAdmin = "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
var sqlInsertUnban = "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT LAST_INSERT_ID();";
var sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
var adminId = sqlAdminId ?? 0;
foreach (var ban in bansList)
{
int banId = ban.id;
int? unbanId;
if (reason != null)
{
unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
}
else
{
sqlInsertUnban = "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT LAST_INSERT_ID();";
unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId });
}
const string sqlUpdateBan = "UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId";
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
}
}
catch { }
}
public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
{
if (database == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
bool checkIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0;
var filteredPlayers = players.Where(p => p.UserId.HasValue).ToList();
var steamIds = filteredPlayers.Select(p => p.SteamID).Distinct().ToList();
var ipAddresses = filteredPlayers
.Where(p => !string.IsNullOrEmpty(p.IpAddress))
.Select(p => p.IpAddress)
.Distinct()
.ToList();
var sql = new StringBuilder();
sql.Append("SELECT `player_steamid`, `player_ip` FROM `sa_bans` WHERE `status` = 'ACTIVE' ");
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
sql.Append("AND (player_steamid IN @SteamIDs ");
if (checkIpBans && ipAddresses.Count != 0)
{
sql.Append("OR player_ip IN @IpAddresses");
}
sql.Append(')');
}
else
{
sql.Append("AND server_id = @ServerId AND (player_steamid IN @SteamIDs ");
if (checkIpBans && ipAddresses.Count != 0)
{
sql.Append("OR player_ip IN @IpAddresses");
}
sql.Append(')');
}
var bannedPlayers = await connection.QueryAsync<(ulong PlayerSteamID, string PlayerIP)>(
sql.ToString(),
new
{
SteamIDs = steamIds,
IpAddresses = checkIpBans ? ipAddresses : [],
ServerId = CS2_SimpleAdmin.ServerId
});
var valueTuples = bannedPlayers.ToList();
var bannedSteamIds = valueTuples.Select(b => b.PlayerSteamID).ToHashSet();
var bannedIps = valueTuples.Select(b => b.PlayerIP).ToHashSet();
foreach (var player in filteredPlayers.Where(player => bannedSteamIds.Contains(player.SteamID) ||
(checkIpBans && bannedIps.Contains(player.IpAddress ?? ""))))
{
if (!player.UserId.HasValue) continue;
await Server.NextFrameAsync(() =>
{
Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED);
});
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}");
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
}
}
catch { }
}
// public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
// {
// if (database == null) return;
//
// try
// {
// await using var connection = await database.GetConnectionAsync();
// bool checkIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0;
//
// var filteredPlayers = players.Where(p => p.UserId.HasValue).ToList();
//
// var steamIds = filteredPlayers.Select(p => p.SteamID).Distinct().ToList();
// var ipAddresses = filteredPlayers
// .Where(p => !string.IsNullOrEmpty(p.IpAddress))
// .Select(p => p.IpAddress)
// .Distinct()
// .ToList();
//
// var sql = new StringBuilder();
// sql.Append("SELECT `player_steamid`, `player_ip` FROM `sa_bans` WHERE `status` = 'ACTIVE' ");
//
// if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
// {
// sql.Append("AND (player_steamid IN @SteamIDs ");
// if (checkIpBans && ipAddresses.Count != 0)
// {
// sql.Append("OR player_ip IN @IpAddresses");
// }
// sql.Append(')');
// }
// else
// {
// sql.Append("AND server_id = @ServerId AND (player_steamid IN @SteamIDs ");
// if (checkIpBans && ipAddresses.Count != 0)
// {
// sql.Append("OR player_ip IN @IpAddresses");
// }
// sql.Append(')');
// }
//
// var bannedPlayers = await connection.QueryAsync<(ulong PlayerSteamID, string PlayerIP)>(
// sql.ToString(),
// new
// {
// SteamIDs = steamIds,
// IpAddresses = checkIpBans ? ipAddresses : [],
// CS2_SimpleAdmin.ServerId
// });
//
// var valueTuples = bannedPlayers.ToList();
// var bannedSteamIds = valueTuples.Select(b => b.PlayerSteamID).ToHashSet();
// var bannedIps = valueTuples.Select(b => b.PlayerIP).ToHashSet();
//
// foreach (var player in filteredPlayers.Where(player => bannedSteamIds.Contains(player.SteamID) ||
// (checkIpBans && bannedIps.Contains(player.IpAddress ?? ""))))
// {
// if (!player.UserId.HasValue || CS2_SimpleAdmin.PlayersInfo[player.SteamID].WaitingForKick) continue;
//
// await Server.NextWorldUpdateAsync(() =>
// {
// Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED);
// });
// }
// }
// catch (Exception ex)
// {
// CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}");
// }
// }
/// <summary>
/// Expires all bans that have passed their end time, including optional cleanup of old IP bans.
/// </summary>
public async Task ExpireOldBans()
{
if (database == null) return;
if (databaseProvider == null) return;
var currentTime = Time.ActualDateTime();
try
{
await using var connection = await database.GetConnectionAsync();
/*
string sql = "";
await using MySqlConnection connection = await _database.GetConnectionAsync();
sql = "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime";
await connection.ExecuteAsync(sql, new { CurrentTime = DateTime.UtcNow });
*/
string sql;
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
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
""";
await using var connection = await databaseProvider.CreateConnectionAsync();
var 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);
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
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
""";
sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId });
}
}

View File

@@ -0,0 +1,725 @@
using System.Collections.Concurrent;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Models;
using Dapper;
using ZLinq;
namespace CS2_SimpleAdmin.Managers;
internal class CacheManager: IDisposable
{
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
private readonly ConcurrentDictionary<ulong, List<BanRecord>> _steamIdIndex = [];
private readonly ConcurrentDictionary<uint, List<BanRecord>> _ipIndex = [];
private readonly ConcurrentDictionary<ulong, HashSet<IpRecord>> _playerIpsCache = [];
private HashSet<uint> _cachedIgnoredIps = [];
private DateTime _lastUpdateTime = DateTime.MinValue;
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;
try
{
Clear();
_cachedIgnoredIps = CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps
.AsValueEnumerable()
.Select(IpHelper.IpToUint)
.ToHashSet();
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
List<BanRecord> bans;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
bans = (await connection.QueryAsync<BanRecord>(
"""
SELECT
id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
""")).ToList();
}
else
{
bans = (await connection.QueryAsync<BanRecord>(
"""
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
""", 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<(ulong steamid, string? name, uint address, DateTime used_at)>(
"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>();
foreach (var record in ipHistory)
{
// When we encounter a new steamid, save the previous one
if (record.steamid != currentSteamId && currentSteamId != 0)
{
_playerIpsCache[currentSteamId] = currentIpSet;
currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
latestIpTimestamps.Clear();
}
currentSteamId = record.steamid;
// Only keep the latest timestamp for each IP
if (!latestIpTimestamps.TryGetValue(record.address, out var 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
));
}
}
// Don't forget the last steamid
if (currentSteamId != 0)
{
_playerIpsCache[currentSteamId] = currentIpSet;
}
}
foreach (var ban in bans.AsValueEnumerable())
_banCache.TryAdd(ban.Id, ban);
RebuildIndexes();
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
_isInitialized = true;
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
/// <summary>
/// Clears all cached data and reinitializes the cache from the database.
/// </summary>
/// <returns>Asynchronous task representing the reinitialization process.</returns>
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.
/// </summary>
/// <returns>Asynchronous task representing the refresh operation.</returns>
public async Task RefreshCacheAsync()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (!_isInitialized) return;
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<BanRecord> updatedBans;
// Optimization: Only get IDs for comparison if we need to check for deletions
// Most of the time bans are just added/updated, not deleted
HashSet<int>? allIds = null;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
updatedBans = (await connection.QueryAsync<BanRecord>(
"""
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
""",
new { lastUpdate = _lastUpdateTime }
));
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0)
{
allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
}
updatedBans = updatedList;
}
else
{
updatedBans = (await connection.QueryAsync<BanRecord>(
"""
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
""",
new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId }
));
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0)
{
allIds = (await connection.QueryAsync<int>(
"SELECT id FROM sa_bans WHERE server_id = @serverId",
new { serverId = CS2_SimpleAdmin.ServerId }
)).ToHashSet();
}
updatedBans = updatedList;
}
// Optimization: Only process deletions if we have the full ID list
if (allIds != null)
{
foreach (var id in _banCache.Keys)
{
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
if (ban.PlayerSteamId != null &&
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var 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)) 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<(ulong steamid, string? name, uint address, DateTime used_at)>(
"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))
{
var ipSet = new HashSet<IpRecord>(
group
.GroupBy(x => x.address)
.Select(g =>
{
var latest = g.MaxBy(x => x.used_at);
return new IpRecord(
g.Key,
latest.used_at,
!string.IsNullOrEmpty(latest.name)
? latest.name
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
);
}),
new IpRecordComparer()
);
_playerIpsCache.AddOrUpdate(
group.Key,
_ => ipSet,
(_, existingSet) =>
{
foreach (var newEntry in ipSet)
{
existingSet.Remove(newEntry);
existingSet.Add(newEntry);
}
return existingSet;
});
}
}
// Update cache with new/modified bans
var hasUpdates = false;
foreach (var ban in updatedBans)
{
_banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban);
hasUpdates = true;
}
// Always rebuild indexes if there were any updates
// This ensures status changes (ACTIVE -> UNBANNED) are reflected
if (hasUpdates)
{
RebuildIndexes();
}
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
}
catch (Exception)
{
}
}
/// <summary>
/// Rebuilds the internal indexes for fast lookup of active bans by Steam ID and IP address.
/// Clears and repopulates both indexes based on the current in-memory ban cache.
/// </summary>
private void RebuildIndexes()
{
_steamIdIndex.Clear();
_ipIndex.Clear();
// Optimization: Cache config value to avoid repeated property access
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
var 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);
foreach (var ban in activeBans)
{
// Index by Steam ID
if (ban.PlayerSteamId.HasValue)
{
var steamId = ban.PlayerSteamId.Value;
if (!_steamIdIndex.TryGetValue(steamId, out var steamList))
{
steamList = new List<BanRecord>();
_steamIdIndex[steamId] = steamList;
}
steamList.Add(ban);
}
// Index by IP (only if IP bans are enabled)
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
{
if (!_ipIndex.TryGetValue(ipUInt, out var ipList))
{
ipList = new List<BanRecord>();
_ipIndex[ipUInt] = ipList;
}
ipList.Add(ban);
}
}
}
/// <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>
/// <param name="steamId">64-bit Steam ID of the player.</param>
/// <returns>List of <see cref="BanRecord"/> objects associated with the Steam ID.</returns>
public List<BanRecord> GetPlayerBansBySteamId(ulong steamId) => _steamIdIndex.TryGetValue(steamId, out var bans) ? bans : [];
/// <summary>
/// Gets all known Steam accounts that have used the specified IP address.
/// </summary>
/// <param name="ipAddress">The IP address to search for, in string format.</param>
/// <returns>
/// List of tuples containing the Steam ID, last used time, and player name for each matching entry.
/// </returns>
public List<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
{
var ipAsUint = IpHelper.IpToUint(ipAddress);
var results = new List<(ulong, DateTime, string)>();
// Optimization: Direct lookup using HashSet.Contains instead of TryGetValue
var searchRecord = new IpRecord(ipAsUint, default, null!);
foreach (var (steamId, ipSet) in _playerIpsCache)
{
// Optimization: Single pass through the set
foreach (var entry in ipSet)
{
if (entry.Ip == ipAsUint)
{
results.Add((steamId, entry.UsedAt, entry.PlayerName));
}
}
}
return results;
}
// public IEnumerable<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
// {
// var ipAsUint = IpHelper.IpToUint(ipAddress);
//
// return _playerIpsCache.SelectMany(kvp => kvp.Value
// .Where(entry => entry.Ip == ipAsUint)
// .Select(entry => (kvp.Key, entry.UsedAt, entry.PlayerName)));
// }
private bool IsIpBanned(string ipAddress)
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
var 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))
// return true;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
// return false;
//
// if (string.IsNullOrEmpty(ipAddress) || !IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt))
// return false;
//
// return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt);
// }
/// <summary>
/// Checks if a player is currently banned by Steam ID or IP address.
/// If a partial ban record is found, updates it with the latest player information.
/// </summary>
/// <param name="playerName">Name of the player attempting to connect.</param>
/// <param name="steamId">Optional 64-bit Steam ID of the player.</param>
/// <param name="ipAddress">Optional IP address of the player.</param>
/// <returns>True if the player is banned, otherwise false.</returns>
public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress)
{
BanRecord? record;
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
{
record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record != null)
{
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue))
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
}
return true;
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
return false;
if (string.IsNullOrEmpty(ipAddress) ||
!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) ||
_cachedIgnoredIps.Contains(ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false;
record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record == null) return false;
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue && steamId.HasValue))
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
}
return true;
}
// public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress)
// {
// if (_steamIdIndex.ContainsKey(steamId))
// return true;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
// return false;
//
// if (!_playerIpsCache.TryGetValue(steamId, out var ipData))
// return false;
//
// // var now = Time.ActualDateTime();
// var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
// var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
//
// if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint))
// {
// if (!_cachedIgnoredIps.Contains(ipAsUint))
// {
// ipData.Add(new IpRecord(
// ipAsUint,
// Time.ActualDateTime().AddSeconds(-2),
// unknownName
// ));
// }
// }
//
// // foreach (var ipRecord in ipData)
// // {
// // // Skip if too old or in ignored list
// // if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
// // continue;
// //
// // // Check if IP is banned
// // if (_ipIndex.ContainsKey(ipRecord.Ip))
// // return true;
// // }
//
// foreach (var ipRecord in ipData)
// {
// if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
// continue;
//
// if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords)) continue;
//
// var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
// if (activeBan == null) continue;
//
// if (!string.IsNullOrEmpty(activeBan.PlayerName) && activeBan.PlayerSteamId.HasValue) return true;
//
// _ = Task.Run(() => UpdatePlayerData(
// activeBan.PlayerName,
// steamId,
// ipAddress
// ));
//
// if (string.IsNullOrEmpty(activeBan.PlayerName) && !string.IsNullOrEmpty(unknownName))
// activeBan.PlayerName = unknownName;
//
// activeBan.PlayerSteamId ??= steamId;
//
// return true;
// }
//
// return false;
// }
/// <summary>
/// Checks if the player or any IP previously associated with them is currently banned.
/// Also updates ban records with missing player info if found.
/// </summary>
/// <param name="playerName">Current player name.</param>
/// <param name="steamId">64-bit Steam ID of the player.</param>
/// <param name="ipAddress">Current IP address of the player (optional).</param>
/// <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))
{
var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan != null)
{
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp))
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
return false;
if (!_playerIpsCache.TryGetValue(steamId, out var ipData))
return false;
var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint))
{
if (!_cachedIgnoredIps.Contains(ipAsUint))
{
ipData.Add(new IpRecord(ipAsUint, Time.ActualDateTime().AddSeconds(-2), unknownName));
}
}
foreach (var ipRecord in ipData)
{
if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
continue;
if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords))
continue;
var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null)
continue;
if (string.IsNullOrEmpty(activeBan.PlayerName))
activeBan.PlayerName = unknownName;
activeBan.PlayerSteamId ??= steamId;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
return false;
}
/// <summary>
/// Checks if the given IP address is known (previously recorded) for the specified Steam ID.
/// </summary>
/// <param name="steamId">64-bit Steam ID of the player.</param>
/// <param name="ipAddress">IP address to check.</param>
/// <returns>True if the IP is recorded for the player, otherwise false.</returns>
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) &&
ipData.Contains(new IpRecord(ipUint, default, null!));
}
// public bool HasIpForPlayer(ulong steamId, string ipAddress)
// {
// if (string.IsNullOrWhiteSpace(ipAddress))
// return false;
//
// return _playerIpsCache.TryGetValue(steamId, out var ipData)
// && ipData.Any(x => x.Ip == IpHelper.IpToUint(ipAddress));
// }
/// <summary>
/// Updates existing active ban records in the database with the latest known player name and IP address.
/// Also updates in-memory cache to reflect these changes.
/// </summary>
/// <param name="playerName">Current player name.</param>
/// <param name="steamId">Optional Steam ID of the player.</param>
/// <param name="ipAddress">Optional IP address of the player.</param>
/// <returns>Asynchronous task representing the update operation.</returns>
private async Task UpdatePlayerData(string? playerName, ulong? steamId, string? ipAddress)
{
if (CS2_SimpleAdmin.DatabaseProvider == null)
return;
var baseSql = """
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)
""";
if (!CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
baseSql += " AND server_id = @ServerId;";
}
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,
CurrentTime = Time.ActualDateTime(),
CS2_SimpleAdmin.ServerId
};
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await connection.ExecuteAsync(baseSql, parameters);
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
{
foreach (var 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))
{
foreach (var 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;
}
}
}
private void Clear()
{
_steamIdIndex.Clear();
_ipIndex.Clear();
_banCache.Clear();
_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;
Clear();
_disposed = true;
}
}
public class IpRecordComparer : IEqualityComparer<IpRecord>
{
public bool Equals(IpRecord x, IpRecord y)
=> x.Ip == y.Ip;
public int GetHashCode(IpRecord obj)
=> obj.Ip.GetHashCode();
}

View File

@@ -1,21 +1,33 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CS2_SimpleAdmin.Managers;
public class DiscordManager(string webhookUrl)
{
/// <summary>
/// Sends a plain text message asynchronously to the configured Discord webhook URL.
/// </summary>
/// <param name="message">The text message to send to Discord.</param>
/// <returns>A task representing the asynchronous operation.</returns>
[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
{
content = message
};
var json = JsonConvert.SerializeObject(payload);
var options = new JsonSerializerOptions
{
WriteIndented = false
};
var json = JsonSerializer.Serialize(payload, options);
var content = new StringContent(json, Encoding.UTF8, "application/json");
try
@@ -34,6 +46,12 @@ public class DiscordManager(string webhookUrl)
}
}
/// <summary>
/// Sends an embed message asynchronously to the configured Discord webhook URL.
/// </summary>
/// <param name="embed">The embed object containing rich content to send.</param>
/// <returns>A task representing the asynchronous operation.</returns>
[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;
@@ -61,7 +79,12 @@ public class DiscordManager(string webhookUrl)
}
};
var jsonPayload = JsonConvert.SerializeObject(payload);
var options = new JsonSerializerOptions
{
WriteIndented = false
};
var jsonPayload = JsonSerializer.Serialize(payload, options);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(webhookUrl, content);
@@ -73,6 +96,11 @@ public class DiscordManager(string webhookUrl)
}
}
/// <summary>
/// Converts a hexadecimal color string (e.g. "#FF0000") to its integer representation.
/// </summary>
/// <param name="hex">The hexadecimal color string, optionally starting with '#'.</param>
/// <returns>An integer representing the color.</returns>
public static int ColorFromHex(string hex)
{
if (hex.StartsWith($"#"))
@@ -84,6 +112,9 @@ public class DiscordManager(string webhookUrl)
}
}
/// <summary>
/// Represents a Discord embed message containing rich content such as title, description, fields, and images.
/// </summary>
public class Embed
{
public int Color { get; init; }
@@ -96,6 +127,12 @@ public class Embed
public List<EmbedField> Fields { get; } = [];
/// <summary>
/// Adds a field to the embed message.
/// </summary>
/// <param name="name">The name of the field.</param>
/// <param name="value">The value or content of the field.</param>
/// <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
@@ -109,12 +146,18 @@ public class Embed
}
}
/// <summary>
/// Represents the footer section of a Discord embed message, including optional text and icon URL.
/// </summary>
public class Footer
{
public string? Text { get; init; }
public string? IconUrl { get; set; }
}
/// <summary>
/// Represents a field inside a Discord embed message.
/// </summary>
public class EmbedField
{
public string? Name { get; init; }

View File

@@ -1,14 +1,24 @@
using CS2_SimpleAdminApi;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
namespace CS2_SimpleAdmin.Managers;
internal class MuteManager(Database.Database? database)
internal class MuteManager(IDatabaseProvider? databaseProvider)
{
public async Task MutePlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
/// <summary>
/// Adds a mute entry for a specified player with detailed information.
/// </summary>
/// <param name="player">Player to be muted.</param>
/// <param name="issuer">Admin issuing the mute; null if issued from console.</param>
/// <param name="reason">Reason for muting the player.</param>
/// <param name="time">Duration of the mute in minutes. Zero means permanent mute.</param>
/// <param name="type">Mute type: 0 = GAG, 1 = MUTE, 2 = SILENCE.</param>
/// <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 (database == null) return;
if (databaseProvider == null) return null;
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
@@ -22,16 +32,14 @@ internal class MuteManager(Database.Database? database)
try
{
await using var connection = await database.GetConnectionAsync();
const string sql =
"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)";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddMuteQuery(true);
await connection.ExecuteAsync(sql, new
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64.ToString(),
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason,
duration = time,
@@ -40,19 +48,28 @@ internal class MuteManager(Database.Database? database)
type = muteType,
serverid = CS2_SimpleAdmin.ServerId
});
return muteId;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex.Message);
};
return null;
}
}
public async Task AddMuteBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
/// <summary>
/// Adds a mute entry for a offline player identified by their SteamID.
/// </summary>
/// <param name="playerSteamId">SteamID64 of the player to mute.</param>
/// <param name="issuer">Admin issuing the mute; can be null if from console.</param>
/// <param name="reason">Reason for the mute.</param>
/// <param name="time">Mute duration in minutes; 0 for permanent.</param>
/// <param name="type">Mute type: 0 = GAG, 1 = MUTE, 2 = SILENCE.</param>
/// <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 (database == null) return;
if (string.IsNullOrEmpty(playerSteamId)) return;
if (databaseProvider == null) return null;
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
@@ -66,14 +83,13 @@ internal class MuteManager(Database.Database? database)
try
{
await using var connection = await database.GetConnectionAsync();
const string sql = "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)";
await connection.ExecuteAsync(sql, new
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddMuteQuery(false);
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason,
duration = time,
@@ -82,13 +98,23 @@ internal class MuteManager(Database.Database? database)
type = muteType,
serverid = CS2_SimpleAdmin.ServerId
});
return muteId;
}
catch
{
return null;
}
catch { };
}
/// <summary>
/// Checks if a player with the given SteamID currently has any active mutes.
/// </summary>
/// <param name="steamId">SteamID64 of the player to check.</param>
/// <returns>List of active mute records; empty list if none or on error.</returns>
public async Task<List<dynamic>> IsPlayerMuted(string steamId)
{
if (database == null) return [];
if (databaseProvider == null) return [];
if (string.IsNullOrEmpty(steamId))
{
@@ -102,24 +128,11 @@ internal class MuteManager(Database.Database? database)
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
var currentTime = Time.ActualDateTime();
var sql = "";
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.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))";
}
else
{
sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.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";
}
var 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();
return activeMutes;
@@ -130,35 +143,26 @@ internal class MuteManager(Database.Database? database)
}
}
/// <summary>
/// Retrieves counts of total mutes, gags, and silences for a given player.
/// </summary>
/// <param name="playerInfo">Information about the player.</param>
/// <returns>
/// Tuple containing total mutes, total gags, and total silences respectively.
/// Returns zeros if no data or on error.
/// </returns>
public async Task<(int TotalMutes, int TotalGags, int TotalSilences)> GetPlayerMutes(PlayerInfo playerInfo)
{
if (database == null) return (0,0,0);
if (databaseProvider == null) return (0,0,0);
try
{
await using var connection = await database.GetConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? """
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;
""";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new
{
PlayerSteamID = playerInfo.SteamId.SteamId64.ToString(),
PlayerSteamID = playerInfo.SteamId.SteamId64,
CS2_SimpleAdmin.ServerId
});
@@ -170,25 +174,28 @@ internal class MuteManager(Database.Database? database)
}
}
public async Task CheckOnlineModeMutes(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
/// <summary>
/// Processes a batch of online players to update their mute status and remove expired penalties.
/// </summary>
/// <param name="players">List of tuples containing player SteamID, optional UserID, and slot index.</param>
/// <returns>Task representing the asynchronous operation.</returns>
public async Task CheckOnlineModeMutes(List<(ulong SteamID, int? UserId, int Slot)> players)
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
var batchSize = 10;
await using var connection = await database.GetConnectionAsync();
const int batchSize = 20;
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "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";
var sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
for (var i = 0; i < players.Count; i += batchSize)
{
var batch = players.Skip(i).Take(batchSize);
var parametersList = new List<object>();
foreach (var (_, steamId, _, _) in batch)
foreach (var (steamId, _, _) in batch)
{
parametersList.Add(new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
}
@@ -196,12 +203,9 @@ internal class MuteManager(Database.Database? database)
await connection.ExecuteAsync(sql, parametersList);
}
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "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";
sql = databaseProvider.GetCheckExpiredMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
foreach (var (_, steamId, _, slot) in players)
foreach (var (steamId, _, slot) in players)
{
var muteRecords = await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
@@ -216,9 +220,17 @@ internal class MuteManager(Database.Database? database)
catch { }
}
/// <summary>
/// Removes active mutes for players matching the specified pattern.
/// </summary>
/// <param name="playerPattern">Pattern to match player names or identifiers.</param>
/// <param name="adminSteamId">SteamID64 of the admin performing the unmute.</param>
/// <param name="reason">Reason for unmuting the player(s).</param>
/// <param name="type">Mute type to remove: 0 = GAG, 1 = MUTE, 2 = SILENCE.</param>
/// <returns>Task representing the asynchronous operation.</returns>
public async Task UnmutePlayer(string playerPattern, string adminSteamId, string reason, int type = 0)
{
if (database == null) return;
if (databaseProvider == null) return;
if (playerPattern.Length <= 1)
{
@@ -227,8 +239,7 @@ internal class MuteManager(Database.Database? database)
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
var muteType = type switch
{
1 => "MUTE",
@@ -236,27 +247,16 @@ internal class MuteManager(Database.Database? database)
_ => "GAG"
};
string sqlRetrieveMutes;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
sqlRetrieveMutes = "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND " +
"type = @muteType AND status = 'ACTIVE'";
}
else
{
sqlRetrieveMutes = "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND " +
"type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
}
var sqlRetrieveMutes =
databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId });
var mutesList = mutes as dynamic[] ?? mutes.ToArray();
if (mutesList.Length == 0)
return;
const string sqlAdmin = "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
var sqlInsertUnmute = "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT LAST_INSERT_ID();";
var sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery();
var sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason));
var sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
var adminId = sqlAdminId ?? 0;
@@ -264,21 +264,11 @@ internal class MuteManager(Database.Database? database)
foreach (var mute in mutesList)
{
int muteId = mute.id;
int? unmuteId;
// Insert into sa_unmutes
if (reason != null)
{
unmuteId = await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId, reason });
}
else
{
sqlInsertUnmute = "INSERT INTO sa_unmutes (muteId, admin_id) VALUES (@muteId, @adminId); SELECT LAST_INSERT_ID();";
unmuteId = await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId });
}
int? unmuteId =
await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId, reason });
// Update sa_mutes to set unmute_id
const string sqlUpdateMute = "UPDATE sa_mutes SET status = 'UNMUTED', unmute_id = @unmuteId WHERE id = @muteId";
var sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery();
await connection.ExecuteAsync(sqlUpdateMute, new { unmuteId, muteId });
}
}
@@ -288,28 +278,18 @@ internal class MuteManager(Database.Database? database)
}
}
/// <summary>
/// Expires all old mutes that have passed their duration according to current time.
/// </summary>
/// <returns>Task representing the asynchronous expiration operation.</returns>
public async Task ExpireOldMutes()
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
string sql;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.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`";
}
else
{
sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.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";
}
await using var connection = await databaseProvider.CreateConnectionAsync();
var 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)

View File

@@ -2,17 +2,20 @@
using CounterStrikeSharp.API.Modules.Entities;
using Dapper;
using Microsoft.Extensions.Logging;
using MySqlConnector;
using Newtonsoft.Json;
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;
public class PermissionManager(Database.Database? database)
public class PermissionManager(IDatabaseProvider? databaseProvider)
{
// Unused for now
//public static readonly ConcurrentDictionary<string, ConcurrentBag<string>> _adminCache = new ConcurrentDictionary<string, ConcurrentBag<string>>();
public static readonly ConcurrentDictionary<SteamID, DateTime?> AdminCache = new();
// public static readonly ConcurrentDictionary<SteamID, DateTime?> AdminCache = new();
public static readonly ConcurrentDictionary<SteamID, (DateTime? ExpirationTime, List<string> Flags)> AdminCache = new();
/*
public async Task<List<(List<string>, int)>> GetAdminFlags(string steamId)
@@ -57,60 +60,65 @@ public class PermissionManager(Database.Database? database)
}
*/
private async Task<List<(string, string, List<string>, int, DateTime?)>> GetAllPlayersFlags()
/// <summary>
/// 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()
{
if (database == null) return [];
if (databaseProvider == null)
return new List<(ulong, string, List<string>, int, DateTime?)>();
var now = Time.ActualDateTime();
var now = Time.ActualDateTime();
try
{
await using var connection = await database.GetConnectionAsync();
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();
const string sql = """
SELECT sa_admins.player_steamid, sa_admins.player_name, sa_admins_flags.flag, sa_admins.immunity, sa_admins.ends
FROM sa_admins_flags
JOIN sa_admins ON sa_admins_flags.admin_id = sa_admins.id
WHERE (sa_admins.ends IS NULL OR sa_admins.ends > @CurrentTime)
AND (sa_admins.server_id IS NULL OR sa_admins.server_id = @serverid)
ORDER BY sa_admins.player_steamid
""";
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
};
var admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList();
int immunity = g.Key.immunity switch
{
int i => i,
string s when int.TryParse(s, out var parsed) => parsed,
_ => 0
};
// Group by player_steamid and aggregate the flags
var groupedPlayers = admins
.GroupBy(r => new { r.player_steamid, r.player_name, r.immunity, r.ends })
.Select(g => (
PlayerSteamId: (string)g.Key.player_steamid,
PlayerName: (string)g.Key.player_name,
Flags: g.Select(r => (string)r.flag).Distinct().ToList(),
Immunity: g.Key.immunity is int i ? i : int.TryParse((string)g.Key.immunity, out var immunity) ? immunity : 0,
Ends: g.Key.ends is DateTime dateTime ? dateTime : (DateTime?)null
))
.ToList();
DateTime? ends = g.Key.ends as DateTime?;
/*
foreach (var player in groupedPlayers)
{
Console.WriteLine($"Player SteamID: {player.PlayerSteamId}, Name: {player.PlayerName}, Flags: {string.Join(", ", player.Flags)}, Immunity: {player.Immunity}, Ends: {player.Ends}");
}
*/
string playerName = g.Key.playerName as string ?? string.Empty;
List<(string, string, List<string>, int, DateTime?)> filteredFlagsWithImmunity = [];
// tutaj zakładamy, że Dapper zwraca już string (nie dynamic)
var flags = g.Select(r => r.flag as string ?? string.Empty)
.Distinct()
.ToList();
// Add the grouped players to the list
filteredFlagsWithImmunity.AddRange(groupedPlayers);
return (steamId, playerName, flags, immunity, ends);
})
.ToList();
return filteredFlagsWithImmunity;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
return [];
}
return groupedPlayers;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
return [];
}
}
/*
public async Task<Dictionary<int, Tuple<List<string>, List<Tuple<string, DateTime?>>, int>>> GetAllGroupsFlags()
{
@@ -176,33 +184,29 @@ public class PermissionManager(Database.Database? database)
}
*/
/// <summary>
/// Retrieves all groups' data including flags and immunity asynchronously.
/// </summary>
/// <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 (database == null) return [];
if (databaseProvider == null) return [];
await using MySqlConnection connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
;
try
{
var sql = "SELECT group_id FROM sa_groups_servers WHERE (server_id = @serverid OR server_id IS NULL)";
var groupDataSql = connection.Query<int>(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
sql = """
SELECT g.group_id, sg.name AS group_name, sg.immunity, f.flag
FROM sa_groups_flags f
JOIN sa_groups_servers g ON f.group_id = g.group_id
JOIN sa_groups sg ON sg.id = g.group_id
WHERE (g.server_id = @serverid OR server_id IS NULL)
""";
// var sql = "SELECT group_id FROM sa_groups_servers WHERE (server_id = @serverid OR server_id IS NULL)";
// var groupDataSql = connection.Query<int>(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
var sql = databaseProvider.GetGroupsQuery();
var groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
if (groupDataSql.Count == 0 || groupData.Count == 0)
if (groupData.Count == 0)
{
return [];
}
var groupInfoDictionary = new Dictionary<string, (List<string>, int)>();
foreach (var row in groupData)
{
var groupName = (string)row.group_name;
@@ -230,10 +234,13 @@ public class PermissionManager(Database.Database? database)
return [];
}
/// <summary>
/// Creates a JSON file containing groups data asynchronously.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task CrateGroupsJsonFile()
{
var groupsData = await GetAllGroupsData();
var jsonData = new Dictionary<string, object>();
foreach (var kvp in groupsData)
@@ -247,7 +254,13 @@ public class PermissionManager(Database.Database? database)
jsonData[kvp.Key] = groupData;
}
var json = JsonConvert.SerializeObject(jsonData, Formatting.Indented);
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(jsonData, options);
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "groups.json");
await File.WriteAllTextAsync(filePath, json);
}
@@ -312,88 +325,187 @@ public class PermissionManager(Database.Database? database)
}
*/
/// <summary>
/// Creates a JSON file containing admins data asynchronously.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task CreateAdminsJsonFile()
{
List<(string identity, string name, List<string> flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags();
List<(ulong identity, string name, List<string> flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags();
var validPlayers = allPlayers
.Where(player => SteamID.TryParse(player.identity, out _)) // Filter invalid SteamID
.Where(player => SteamID.TryParse(player.identity.ToString(), out _))
.ToList();
/*
foreach (var player in allPlayers)
// foreach (var player in allPlayers)
// {
// var (steamId, name, flags, immunity, ends) = player;
//
// Console.WriteLine($"Player SteamID: {steamId}");
// Console.WriteLine($"Player Name: {name}");
// Console.WriteLine($"Flags: {string.Join(", ", flags)}");
// Console.WriteLine($"Immunity: {immunity}");
// Console.WriteLine($"Ends: {(ends.HasValue ? ends.Value.ToString("yyyy-MM-dd HH:mm:ss") : "Never")}");
// Console.WriteLine();
// }
var jsonData = validPlayers
.GroupBy(player => player.name) // Group by player name
.ToDictionary(
group => group.Key, // Use the player name as key
object (group) =>
{
// Consolidate data for players with same name
var consolidatedData = group.Aggregate(
new
{
identity = string.Empty,
immunity = 0,
flags = new List<string>(),
groups = new List<string>()
},
(acc, player) =>
{
// Merge identities
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
{
acc = acc with { identity = player.identity.ToString() };
}
// Combine immunities by maximum value
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
// Combine flags and groups
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;
});
Server.NextWorldUpdate(() =>
{
var keysToRemove = new List<SteamID>();
foreach (var steamId in AdminCache.Keys.ToList())
{
var data = AdminManager.GetPlayerAdminData(steamId);
if (data != null)
{
var flagsArray = AdminCache[steamId].Flags.ToArray();
AdminManager.RemovePlayerPermissions(steamId, flagsArray);
AdminManager.RemovePlayerFromGroup(steamId, true, flagsArray);
}
keysToRemove.Add(steamId);
}
foreach (var steamId in keysToRemove)
{
if (!AdminCache.TryRemove(steamId, out _)) continue;
var data = AdminManager.GetPlayerAdminData(steamId);
if (data == null) continue;
if (data.Flags.Count != 0 && data.Groups.Count != 0) continue;
AdminManager.ClearPlayerPermissions(steamId);
AdminManager.RemovePlayerAdminData(steamId);
}
foreach (var player in group)
{
if (SteamID.TryParse(player.identity.ToString(), out var steamId) && steamId != null)
{
AdminCache.TryAdd(steamId, (player.ends, player.flags));
}
}
});
// Server.NextFrameAsync(() =>
// {
// for (var index = 0; index < AdminCache.Keys.ToList().Count; index++)
// {
// var steamId = AdminCache.Keys.ToList()[index];
//
// var data = AdminManager.GetPlayerAdminData(steamId);
// if (data != null)
// {
// AdminManager.RemovePlayerPermissions(steamId, AdminCache[steamId].Flags.ToArray());
// AdminManager.RemovePlayerFromGroup(steamId, true, AdminCache[steamId].Flags.ToArray());
// }
//
// if (!AdminCache.TryRemove(steamId, out _)) continue;
//
// if (data == null) continue;
// if (data.Flags.ToList().Count != 0 && data.Groups.ToList().Count != 0)
// continue;
//
// AdminManager.ClearPlayerPermissions(steamId);
// AdminManager.RemovePlayerAdminData(steamId);
// }
//
// foreach (var player in group)
// {
// SteamID.TryParse(player.identity, out var steamId);
// if (steamId == null) continue;
// AdminCache.TryAdd(steamId, (player.ends, player.flags));
// }
// });
return consolidatedData;
});
var options = new JsonSerializerOptions
{
var (steamId, name, flags, immunity, ends) = player;
// Print or process each item
Console.WriteLine($"Player SteamID: {steamId}");
Console.WriteLine($"Player Name: {name}");
Console.WriteLine($"Flags: {string.Join(", ", flags)}");
Console.WriteLine($"Immunity: {immunity}");
Console.WriteLine($"Ends: {(ends.HasValue ? ends.Value.ToString("yyyy-MM-dd HH:mm:ss") : "Never")}");
Console.WriteLine(); // New line for better readability
}
*/
var jsonData = validPlayers
.Select(player =>
{
SteamID.TryParse(player.identity, out var steamId);
// Update cache if SteamID is valid and not already cached
if (steamId != null && !AdminCache.ContainsKey(steamId))
{
AdminCache.TryAdd(steamId, player.ends);
}
// Create an anonymous object with player data
return new
{
playerName = player.name,
playerData = new
{
player.identity,
player.immunity,
flags = player.flags.Where(flag => flag.StartsWith("@")).ToList(),
groups = player.flags.Where(flag => flag.StartsWith("#")).ToList()
}
};
})
.ToDictionary(item => item.playerName, item => (object)item.playerData);
var json = JsonConvert.SerializeObject(jsonData, Formatting.Indented);
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(jsonData, options);
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
await File.WriteAllTextAsync(filePath, json);
//await File.WriteAllTextAsync(CS2_SimpleAdmin.Instance.ModuleDirectory + "/data/admins.json", json);
}
/// <summary>
/// Deletes an admin by their SteamID from the database asynchronously.
/// </summary>
/// <param name="playerSteamId">The SteamID of the admin to delete.</param>
/// <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 (database == null) return;
if (databaseProvider == null) return;
if (string.IsNullOrEmpty(playerSteamId)) return;
//_adminCache.TryRemove(playerSteamId, out _);
try
{
await using var connection = await database.GetConnectionAsync();
var sql = globalDelete
? "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID"
: "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetDeleteAdminQuery(globalDelete);
await connection.ExecuteAsync(sql, new { PlayerSteamID = playerSteamId, CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex.ToString());
CS2_SimpleAdmin._logger?.LogError(ex.Message);
}
}
/// <summary>
/// Adds a new admin with specified details asynchronously.
/// </summary>
/// <param name="playerSteamId">SteamID of the admin.</param>
/// <param name="playerName">Name of the admin.</param>
/// <param name="flagsList">List of flags assigned to the admin.</param>
/// <param name="immunity">Immunity level.</param>
/// <param name="time">Duration in minutes for admin expiration; 0 means permanent.</param>
/// <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 (database == null) return;
if (databaseProvider == null) return;
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return;
@@ -407,12 +519,10 @@ public class PermissionManager(Database.Database? database)
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
// Insert admin into sa_admins table
const string insertAdminSql = "INSERT INTO `sa_admins` (`player_steamid`, `player_name`, `immunity`, `ends`, `created`, `server_id`) " +
"VALUES (@playerSteamid, @playerName, @immunity, @ends, @created, @serverid); SELECT LAST_INSERT_ID();";
var insertAdminSql = databaseProvider.GetAddAdminQuery();
var adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
{
playerSteamId,
@@ -426,25 +536,26 @@ public class PermissionManager(Database.Database? database)
// Insert flags into sa_admins_flags table
foreach (var flag in flagsList)
{
if (flag.StartsWith($"#"))
{
const string sql = "SELECT id FROM `sa_groups` WHERE name = @groupName";
var groupId = await connection.QuerySingleOrDefaultAsync<int?>(sql, new { groupName = flag });
if (groupId != null)
{
const string updateAdminGroup = "UPDATE `sa_admins` SET group_id = @groupId WHERE id = @adminId";
await connection.ExecuteAsync(updateAdminGroup, new
{
groupId,
adminId
});
}
}
const string insertFlagsSql = "INSERT INTO `sa_admins_flags` (`admin_id`, `flag`) " +
"VALUES (@adminId, @flag)";
// if (flag.StartsWith($"#"))
// {
// // const string sql = "SELECT id FROM `sa_groups` WHERE name = @groupName";
// // var groupId = await connection.QuerySingleOrDefaultAsync<int?>(sql, new { groupName = flag });
//
// var sql = databaseProvider.GetGroupIdByNameQuery();
// var groupId = await connection.QuerySingleOrDefaultAsync<int?>(sql, new { groupName = flag, CS2_SimpleAdmin.ServerId });
//
// if (groupId != null)
// {
// var updateAdminGroup = "UPDATE `sa_admins` SET group_id = @groupId WHERE id = @adminId";
// await connection.ExecuteAsync(updateAdminGroup, new
// {
// groupId,
// adminId
// });
// }
// }
var insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery();
await connection.ExecuteAsync(insertFlagsSql, new
{
adminId,
@@ -463,18 +574,24 @@ public class PermissionManager(Database.Database? database)
}
}
/// <summary>
/// Adds a new group with flags and immunity asynchronously.
/// </summary>
/// <param name="groupName">Name of the group.</param>
/// <param name="flagsList">List of flags assigned to the group.</param>
/// <param name="immunity">Immunity level of the group.</param>
/// <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 (database == null) return;
if (databaseProvider == null) return;
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return;
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
try
{
// Insert group into sa_groups table
const string insertGroup = "INSERT INTO `sa_groups` (`name`, `immunity`) " +
"VALUES (@groupName, @immunity); SELECT LAST_INSERT_ID();";
var insertGroup = databaseProvider.GetAddGroupQuery();
var groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
{
groupName,
@@ -484,8 +601,7 @@ public class PermissionManager(Database.Database? database)
// Insert flags into sa_groups_flags table
foreach (var flag in flagsList)
{
const string insertFlagsSql = "INSERT INTO `sa_groups_flags` (`group_id`, `flag`) " +
"VALUES (@groupId, @flag)";
var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
await connection.ExecuteAsync(insertFlagsSql, new
{
@@ -494,11 +610,8 @@ public class PermissionManager(Database.Database? database)
});
}
const string insertGroupServer = "INSERT INTO `sa_groups_servers` (`group_id`, `server_id`) " +
"VALUES (@groupId, @server_id)";
var insertGroupServer = databaseProvider.GetAddGroupServerQuery();
await connection.ExecuteAsync(insertGroupServer, new { groupId, server_id = globalGroup ? null : CS2_SimpleAdmin.ServerId });
await Server.NextWorldUpdateAsync(() =>
{
CS2_SimpleAdmin.Instance.ReloadAdmins(null);
@@ -511,16 +624,20 @@ public class PermissionManager(Database.Database? database)
}
}
/// <summary>
/// Deletes a group by name asynchronously.
/// </summary>
/// <param name="groupName">Name of the group to delete.</param>
public async Task DeleteGroup(string groupName)
{
if (database == null) return;
if (databaseProvider == null) return;
if (string.IsNullOrEmpty(groupName)) return;
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
try
{
const string sql = "DELETE FROM `sa_groups` WHERE name = @groupName";
var sql = databaseProvider.GetDeleteGroupQuery();
await connection.ExecuteAsync(sql, new { groupName });
}
catch (Exception ex)
@@ -529,15 +646,18 @@ public class PermissionManager(Database.Database? database)
}
}
/// <summary>
/// Deletes admins whose permissions have expired asynchronously.
/// </summary>
public async Task DeleteOldAdmins()
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
const string sql = "DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime";
var sql = databaseProvider.GetDeleteOldAdminsQuery();
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime() });
}
catch (Exception)

View File

@@ -7,332 +7,414 @@ using CounterStrikeSharp.API.ValveConstants.Protobuf;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
using ZLinq;
namespace CS2_SimpleAdmin.Managers;
public class PlayerManager
internal class PlayerManager
{
private readonly SemaphoreSlim _loadPlayerSemaphore = new(5);
private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config;
public void LoadPlayerData(CCSPlayerController player)
/// <summary>
/// Loads and initializes player data when a client connects.
/// </summary>
/// <param name="player">The <see cref="CCSPlayerController"/> instance representing the connecting player.</param>
/// <param name="fullConnect">
/// Determines whether to perform a full synchronization of player data.
/// If true, full checks (bans, IP history, penalties, warns, mutes) will be loaded and applied.
/// </param>
/// <remarks>
/// This method validates the player's identity, checks for bans, updates the IP history table,
/// loads penalties (mutes/gags/warns), and optionally notifies admin players about the connecting player's penalties.
/// </remarks>
public void LoadPlayerData(CCSPlayerController player, bool fullConnect = false)
{
if (player.IsBot || string.IsNullOrEmpty(player.IpAddress) || player.IpAddress.Contains("127.0.0.1")
|| !player.UserId.HasValue)
return;
var ipAddress = player.IpAddress?.Split(":")[0];
CS2_SimpleAdmin.PlayersInfo[player.UserId.Value] =
new PlayerInfo(player.UserId.Value, player.Slot, new SteamID(player.SteamID), player.PlayerName, ipAddress);
var userId = player.UserId.Value;
// Check if the player's IP or SteamID is in the bannedPlayers list
if (_config.OtherSettings.BanType > 0 && CS2_SimpleAdmin.BannedPlayers.Contains(ipAddress) ||
CS2_SimpleAdmin.BannedPlayers.Contains(player.SteamID.ToString()))
if (!player.UserId.HasValue)
{
// Kick the player if banned
if (player.UserId.HasValue)
Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
return;
}
if (CS2_SimpleAdmin.Database == null) 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];
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
// Perform asynchronous database operations within a single method
Task.Run(async () =>
{
try
{
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
const string selectQuery = "SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;";
var recordExists = await connection.ExecuteScalarAsync<int>(selectQuery, new
await _loadPlayerSemaphore.WaitAsync();
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
{
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
IPAddress = ipAddress
});
if (recordExists > 0)
{
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP
WHERE steamid = @SteamID AND address = @IPAddress;
""";
await connection.ExecuteAsync(updateQuery, new
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
{
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
IPAddress = ipAddress
});
}
else
{
const string insertQuery = """
INSERT INTO `sa_players_ips` (steamid, address, used_at)
VALUES (@SteamID, @IPAddress, CURRENT_TIMESTAMP);
""";
await connection.ExecuteAsync(insertQuery, new
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)
};
// CS2_SimpleAdmin._logger?.LogInformation($"Player {playerName} ({steamId} - {ipAddress}) is banned? {isBanned.ToString()}");
if (isBanned)
{
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
IPAddress = ipAddress
});
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(
$"Unable to save ip address for {CS2_SimpleAdmin.PlayersInfo[userId].Name} ({ipAddress}) {ex.Message}");
}
try
{
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(userId))
{
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
}
// Check if the player is banned
var isBanned = await CS2_SimpleAdmin.Instance.BanManager.IsPlayerBanned(CS2_SimpleAdmin.PlayersInfo[userId]);
if (isBanned)
{
// Add player's IP and SteamID to bannedPlayers list if not already present
if (_config.OtherSettings.BanType > 0 && ipAddress != null &&
!CS2_SimpleAdmin.BannedPlayers.Contains(ipAddress))
{
CS2_SimpleAdmin.BannedPlayers.Add(ipAddress);
}
if (!CS2_SimpleAdmin.BannedPlayers.Contains(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString()))
{
CS2_SimpleAdmin.BannedPlayers.Add(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString());
}
// Kick the player if banned
await Server.NextFrameAsync(() =>
{
var victim = Utilities.GetPlayerFromUserid(userId);
if (victim?.UserId == null) return;
if (CS2_SimpleAdmin.UnlockedCommands)
Server.ExecuteCommand($"banid 1 {userId}");
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
});
return;
}
var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(CS2_SimpleAdmin.PlayersInfo[userId], false);
var (totalMutes, totalGags, totalSilences) =
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(CS2_SimpleAdmin.PlayersInfo[userId]);
CS2_SimpleAdmin.PlayersInfo[userId].TotalBans =
await CS2_SimpleAdmin.Instance.BanManager.GetPlayerBans(CS2_SimpleAdmin.PlayersInfo[userId]);
CS2_SimpleAdmin.PlayersInfo[userId].TotalMutes = totalMutes;
CS2_SimpleAdmin.PlayersInfo[userId].TotalGags = totalGags;
CS2_SimpleAdmin.PlayersInfo[userId].TotalSilences = totalSilences;
CS2_SimpleAdmin.PlayersInfo[userId].TotalWarns = warns.Count;
// Check if the player is muted
var activeMutes = await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString());
if (activeMutes.Count > 0)
{
foreach (var mute in activeMutes)
{
string muteType = mute.type;
DateTime ends = mute.ends;
int duration = mute.duration;
switch (muteType)
await Server.NextWorldUpdateAsync(() =>
{
// Apply mute penalty based on mute type
case "GAG":
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Gag, ends, duration);
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
case "MUTE":
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Mute, ends, duration);
await Server.NextFrameAsync(() =>
// CS2_SimpleAdmin._logger?.LogInformation($"Kicking {playerName}");
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
});
return;
}
}
if (fullConnect)
{
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
await Server.NextWorldUpdateAsync(() =>
{
if (!CS2_SimpleAdmin.CachedPlayers.Contains(player))
CS2_SimpleAdmin.CachedPlayers.Add(player);
});
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null &&
CS2_SimpleAdmin.PlayersInfo[steamId] != null)
{
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
// Eliminates the need for SELECT COUNT and duplicate UPDATE queries
var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64;
var ipUint = IpHelper.IpToUint(ipAddress);
// MySQL: INSERT ... ON DUPLICATE KEY UPDATE pattern
const string upsertQuery = """
INSERT INTO `sa_players_ips` (steamid, name, address, used_at)
VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
used_at = CURRENT_TIMESTAMP,
name = @playerName;
""";
await connection.ExecuteAsync(upsertQuery, new
{
SteamID = steamId64,
playerName,
IPAddress = ipUint
});
// // Cache will be updated on next refresh cycle
// if (!CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(steamId, ipAddress))
// {
// // IP association will be reflected after cache refresh
// }
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(
$"Unable to save ip address for {playerInfo.Name} ({ipAddress}): {ex.Message}");
}
playerInfo.AccountsAssociated =
CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable()
.Select(x => (x.SteamId, x.PlayerName)).ToList() ?? [];
}
try
{
// var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(
// CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), null)
// : CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(CS2_SimpleAdmin
// .PlayersInfo[userId].SteamId.SteamId64)
// : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), ipAddress);
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) =
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(value);
value.TotalBans = CS2_SimpleAdmin.Instance.CacheManager
?.GetPlayerBansBySteamId(value.SteamId.SteamId64)
.Count ?? 0;
value.TotalMutes = totalMutes;
value.TotalGags = totalGags;
value.TotalSilences = totalSilences;
value.TotalWarns = warns.Count;
var activeMutes =
await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(value.SteamId.SteamId64
.ToString());
if (activeMutes.Count > 0)
{
foreach (var mute in activeMutes)
{
player.VoiceFlags = VoiceFlags.Muted;
});
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Mute].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.InvariantCulture)]);
break;
default:
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Silence, ends, duration);
await Server.NextFrameAsync(() =>
string muteType = mute.type;
DateTime ends = mute.ends;
int duration = mute.duration;
switch (muteType)
{
// Apply mute penalty based on mute type
case "GAG":
PlayerPenaltyManager.AddPenalty(
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
PenaltyType.Gag, ends, duration);
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
case "MUTE":
PlayerPenaltyManager.AddPenalty(
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
PenaltyType.Mute, ends, duration);
await Server.NextWorldUpdateAsync(() =>
{
player.VoiceFlags = VoiceFlags.Muted;
});
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Mute].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.InvariantCulture)]);
break;
default:
PlayerPenaltyManager.AddPenalty(
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
PenaltyType.Silence, ends, duration);
await Server.NextWorldUpdateAsync(() =>
{
player.VoiceFlags = VoiceFlags.Muted;
});
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Silence].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.NotifyPenaltiesToAdminOnConnect)
{
await Server.NextWorldUpdateAsync(() =>
{
player.VoiceFlags = VoiceFlags.Muted;
foreach (var admin in Helper.GetValidPlayers()
.Where(p => (AdminManager.PlayerHasPermissions(
new SteamID(p.SteamID),
"@css/kick") ||
AdminManager.PlayerHasPermissions(
new SteamID(p.SteamID),
"@css/ban")) &&
p.Connected == PlayerConnectedState.PlayerConnected &&
!CS2_SimpleAdmin.AdminDisabledJoinComms
.Contains(p.SteamID)))
{
if (CS2_SimpleAdmin._localizer == null || admin == player) continue;
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
"sa_admin_penalty_info",
player.PlayerName,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalBans,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalGags,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalMutes,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalSilences,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalWarns
);
if (CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.Count >= 2)
{
var associatedAcccountsChunks =
CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.ChunkBy(5)
.ToList();
foreach (var chunk in associatedAcccountsChunks)
{
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
"sa_admin_associated_accounts",
player.PlayerName,
string.Join(", ",
chunk.Select(a => $"{a.PlayerName} ({a.SteamId})"))
);
}
}
}
});
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Silence].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
}
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.NotifyPenaltiesToAdminOnConnect)
{
await Server.NextFrameAsync(() =>
catch (Exception ex)
{
foreach (var admin in Helper.GetValidPlayers()
.Where(p => (AdminManager.PlayerHasPermissions(p, "@css/kick") ||
AdminManager.PlayerHasPermissions(p, "@css/ban")) &&
p.Connected == PlayerConnectedState.PlayerConnected && !CS2_SimpleAdmin.AdminDisabledJoinComms.Contains(p.SteamID)))
{
if (CS2_SimpleAdmin._localizer != null && admin != player)
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, "sa_admin_penalty_info",
player.PlayerName,
CS2_SimpleAdmin.PlayersInfo[userId].TotalBans,
CS2_SimpleAdmin.PlayersInfo[userId].TotalGags,
CS2_SimpleAdmin.PlayersInfo[userId].TotalMutes,
CS2_SimpleAdmin.PlayersInfo[userId].TotalSilences,
CS2_SimpleAdmin.PlayersInfo[userId].TotalWarns
);
}
});
CS2_SimpleAdmin._logger?.LogError("Error processing player connection: {exception}",
ex.Message);
}
}
}
catch (Exception ex)
finally
{
CS2_SimpleAdmin._logger?.LogError("Error processing player connection: {exception}", ex.Message);
_loadPlayerSemaphore.Release();
}
});
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name))
{
player.Rename(name);
}
}
/// <summary>
/// Periodically checks the status of online players and applies timers for speed, gravity,
/// and penalty expiration validation.
/// </summary>
/// <remarks>
/// This method registers two repeating timers:
/// <list type="bullet">
/// <item><description>One short-interval timer to update speed/gravity modifications applied to players.</description></item>
/// <item><description>
/// One long-interval timer (default 61 seconds) to expire bans, mutes, warns, refresh caches,
/// and remove outdated penalties from connected players.
/// </description></item>
/// </list>
/// Additionally, banned players still online are kicked, and admins may be updated about mute statuses based on the configured time mode.
/// </remarks>
public void CheckPlayersTimer()
{
CS2_SimpleAdmin.Instance.AddTimer(0.1f, () =>
{
if (CS2_SimpleAdmin.GravityPlayers.Count <= 0) return;
foreach (var value in CS2_SimpleAdmin.GravityPlayers)
{
if (value.Key is not
{ IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PawnIsAlive: true })
continue;
value.Key.SetGravity(value.Value);
}
}, TimerFlags.REPEAT);
CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
{
#if DEBUG
CS2_SimpleAdmin._logger?.LogCritical("[OnMapStart] Expired check");
#endif
if (CS2_SimpleAdmin.Database == null)
if (CS2_SimpleAdmin.DatabaseProvider == null)
return;
var players = Helper.GetValidPlayers();
var onlinePlayers = new List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)>();
// var onlinePlayers = players
// .Where(player => player.IpAddress != null)
// .Select(player => (player.IpAddress, player.SteamID, player.UserId, player.Slot))
// .ToList();
// Optimization: Get players once and avoid allocating anonymous types
var validPlayers = Helper.GetValidPlayers();
if (validPlayers.Count == 0)
return;
foreach (var player in players)
// 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)
{
if (player.IpAddress != null)
onlinePlayers.Add((player.IpAddress, player.SteamID, player.UserId, player.Slot));
tempPlayers.Add((p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot));
}
var pluginInstance = CS2_SimpleAdmin.Instance;
var config = _config.OtherSettings; // Cache config access
_ = Task.Run(async () =>
{
try
{
// Run all expire tasks in parallel
var expireTasks = new[]
{
pluginInstance.BanManager.ExpireOldBans(),
pluginInstance.MuteManager.ExpireOldMutes(),
pluginInstance.WarnManager.ExpireOldWarns(),
pluginInstance.CacheManager?.RefreshCacheAsync() ?? Task.CompletedTask,
pluginInstance.PermissionManager.DeleteOldAdmins()
};
await Task.WhenAll(expireTasks);
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError($"Error processing players timer tasks: {ex.Message}");
if (ex is AggregateException aggregate)
{
foreach (var inner in aggregate.InnerExceptions)
{
CS2_SimpleAdmin._logger?.LogError($"Inner exception: {inner.Message}");
}
}
}
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;
var 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)
{
var playerName = player.PlayerName;
var steamId = player.SteamID;
var 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)
};
if (isBanned)
{
bannedPlayers.Add(player);
}
}
if (bannedPlayers.Count > 0)
{
foreach (var player in bannedPlayers)
{
if (!player.UserId.HasValue) continue;
await Server.NextWorldUpdateAsync(() => 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)
{
onlinePlayers.Add((player.SteamID, player.UserId, player.Slot));
}
if (onlinePlayers.Count > 0)
{
await pluginInstance.MuteManager.CheckOnlineModeMutes(onlinePlayers);
}
}
});
try
{
var expireTasks = new[]
// Optimization: Process penalties without LINQ allocations
var players = Helper.GetValidPlayers();
foreach (var player in players)
{
CS2_SimpleAdmin.Instance.BanManager.ExpireOldBans(),
CS2_SimpleAdmin.Instance.MuteManager.ExpireOldMutes(),
CS2_SimpleAdmin.Instance.WarnManager.ExpireOldWarns(),
CS2_SimpleAdmin.Instance.PermissionManager.DeleteOldAdmins()
};
if (!PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
continue;
Task.WhenAll(expireTasks).ContinueWith(t =>
{
if (t is not { IsFaulted: true, Exception: not null }) return;
foreach (var ex in t.Exception.InnerExceptions)
var isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
var isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
// Only reset voice flags if not muted or silenced
if (!isMuted && !isSilenced)
{
CS2_SimpleAdmin._logger?.LogError($"Error expiring penalties: {ex.Message}");
player.VoiceFlags = VoiceFlags.Normal;
}
});
}
PlayerPenaltyManager.RemoveExpiredPenalties();
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unexpected error: {exception}", ex.Message);
CS2_SimpleAdmin._logger?.LogError($"Unable to remove old penalties: {ex.Message}");
}
CS2_SimpleAdmin.BannedPlayers.Clear();
if (onlinePlayers.Count > 0)
{
try
{
Task.Run(async () =>
{
await CS2_SimpleAdmin.Instance.BanManager.CheckOnlinePlayers(onlinePlayers);
if (_config.OtherSettings.TimeMode == 0)
{
await CS2_SimpleAdmin.Instance.MuteManager.CheckOnlineModeMutes(onlinePlayers);
}
}).ContinueWith(t =>
{
if (t is not { IsFaulted: true, Exception: not null }) return;
foreach (var ex in t.Exception.InnerExceptions)
{
CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}");
}
});
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError($"Unexpected error: {ex.Message}");
}
}
if (onlinePlayers.Count <= 0) return;
{
try
{
var penalizedSlots = players
.Where(player => PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
.Select(player => new
{
Player = player,
IsMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _),
IsSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _),
IsGagged = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _)
});
foreach (var entry in penalizedSlots)
{
// If the player is not muted or silenced, set voice flags to normal
if (!entry.IsMuted && !entry.IsSilenced)
{
entry.Player.VoiceFlags = VoiceFlags.Normal;
}
}
PlayerPenaltyManager.RemoveExpiredPenalties();
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError($"Unable to remove old penalties: {ex.Message}");
}
}
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.REPEAT);
}, TimerFlags.REPEAT);
}
}

View File

@@ -8,7 +8,13 @@ public static class PlayerPenaltyManager
private static readonly ConcurrentDictionary<int, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>> Penalties =
new();
// Add a penalty for a player
/// <summary>
/// Adds a penalty for a specific player slot and penalty type.
/// </summary>
/// <param name="slot">The player slot where the penalty should be applied.</param>
/// <param name="penaltyType">The type of penalty to apply (e.g. gag, mute, silence).</param>
/// <param name="endDateTime">The validity expiration date/time of the penalty.</param>
/// <param name="durationInMinutes">The duration of the penalty in minutes (0 for permanent).</param>
public static void AddPenalty(int slot, PenaltyType penaltyType, DateTime endDateTime, int durationInMinutes)
{
Penalties.AddOrUpdate(slot,
@@ -33,6 +39,13 @@ public static class PlayerPenaltyManager
});
}
/// <summary>
/// Determines whether a player is currently penalized with the given penalty type.
/// </summary>
/// <param name="slot">The player slot to check.</param>
/// <param name="penaltyType">The penalty type to check.</param>
/// <param name="endDateTime">The out-parameter returning the end datetime of the penalty if active.</param>
/// <returns>True if the player has an active penalty, false otherwise.</returns>
public static bool IsPenalized(int slot, PenaltyType penaltyType, out DateTime? endDateTime)
{
endDateTime = null;
@@ -74,7 +87,12 @@ public static class PlayerPenaltyManager
return false;
}
// Get the end datetime and duration of penalties for a player and penalty type
/// <summary>
/// Retrieves all penalties for a player of a specific penalty type.
/// </summary>
/// <param name="slot">The player slot.</param>
/// <param name="penaltyType">The penalty type to retrieve.</param>
/// <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) &&
@@ -85,6 +103,35 @@ public static class PlayerPenaltyManager
return [];
}
/// <summary>
/// Retrieves all penalties for a player across multiple penalty types.
/// </summary>
/// <param name="slot">The player slot.</param>
/// <param name="penaltyType">A list of penalty types to retrieve.</param>
/// <returns>A combined list of penalties of all requested types.</returns>
public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, List<PenaltyType> penaltyType)
{
List<(DateTime EndDateTime, int Duration, bool Passed)> result = [];
if (Penalties.TryGetValue(slot, out var penaltyDict))
{
foreach (var type in penaltyType)
{
if (penaltyDict.TryGetValue(type, out var penaltiesList))
{
result.AddRange(penaltiesList);
}
}
}
return result;
}
/// <summary>
/// Retrieves all penalties for a player across all penalty types.
/// </summary>
/// <param name="slot">The player slot.</param>
/// <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
@@ -95,12 +142,20 @@ public static class PlayerPenaltyManager
new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
}
/// <summary>
/// Checks if a given slot has any penalties assigned.
/// </summary>
/// <param name="slot">The player slot.</param>
/// <returns>True if the player has any penalties, false otherwise.</returns>
public static bool IsSlotInPenalties(int slot)
{
return Penalties.ContainsKey(slot);
}
// Remove all penalties for a player slot
/// <summary>
/// Removes all penalties assigned to a specific player slot.
/// </summary>
/// <param name="slot">The player slot.</param>
public static void RemoveAllPenalties(int slot)
{
if (Penalties.ContainsKey(slot))
@@ -109,13 +164,19 @@ public static class PlayerPenaltyManager
}
}
// Remove all penalties
/// <summary>
/// Removes all penalties for all players.
/// </summary>
public static void RemoveAllPenalties()
{
Penalties.Clear();
}
// Remove all penalties of a selected type from a specific player
/// <summary>
/// Removes all penalties of a specific type from a player.
/// </summary>
/// <param name="slot">The player slot.</param>
/// <param name="penaltyType">The penalty type to remove.</param>
public static void RemovePenaltiesByType(int slot, PenaltyType penaltyType)
{
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
@@ -125,6 +186,11 @@ public static class PlayerPenaltyManager
}
}
/// <summary>
/// Marks penalties with a specific end datetime as "passed" for a player.
/// </summary>
/// <param name="slot">The player slot.</param>
/// <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;
@@ -146,7 +212,13 @@ public static class PlayerPenaltyManager
}
}
// Remove all expired penalties for all players and penalty types
/// <summary>
/// Removes or expires penalties automatically across all players based on their duration or "passed" flag.
/// </summary>
/// <remarks>
/// If <c>TimeMode == 0</c>, penalties are considered passed manually and are removed if flagged as such.
/// Otherwise, expired penalties are removed based on the current datetime compared with their end time.
/// </remarks>
public static void RemoveExpiredPenalties()
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
@@ -172,13 +244,11 @@ public static class PlayerPenaltyManager
var now = Time.ActualDateTime();
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
{
// Remove expired penalties for the player
foreach (var penaltiesList in penaltyDict.Values)
{
penaltiesList.RemoveAll(p => p.Duration > 0 && now >= p.EndDateTime);
}
// Remove player slot if no penalties left
if (penaltyDict.Count == 0)
{
Penalties.TryRemove(playerSlot, out _);

View File

@@ -9,64 +9,99 @@ public class ServerManager
{
private int _getIpTryCount;
/// <summary>
/// Checks whether the server setting <c>sv_hibernate_when_empty</c> is enabled.
/// Logs an error if this setting is true, since it prevents the plugin from working properly.
/// </summary>
public static void CheckHibernationStatus()
{
var 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.
/// Updates or inserts the server record in the database accordingly.
/// After loading, triggers admin reload and cache initialization.
/// Also optionally sends plugin usage metrics if enabled in configuration.
/// </summary>
public void LoadServerData()
{
CS2_SimpleAdmin.Instance.AddTimer(2.0f, () =>
{
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.ServerId != null || CS2_SimpleAdmin.Database == null) return;
if (_getIpTryCount > 16)
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");
// Check if we've exceeded retry limit with invalid IP
if (_getIpTryCount > 32 && isInvalidIp)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load server data - can't fetch ip address!");
return;
}
var ipAddress = ConVar.Find("ip")?.StringValue;
// Optimization: Cache ConVar lookups
var ipConVar = ConVar.Find("ip");
var ipAddress = ipConVar?.StringValue;
// Use Helper IP if ConVar IP is invalid
if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))
{
ipAddress = Helper.GetServerIp();
ipAddress = serverIp;
if (_getIpTryCount <= 16)
// Retry if still invalid and under retry limit
if (_getIpTryCount <= 32 && isInvalidIp)
{
_getIpTryCount++;
LoadServerData();
return;
}
}
var address = $"{ipAddress}:{ConVar.Find("hostport")?.GetPrimitiveValue<int>()}";
var hostname = ConVar.Find("hostname")!.StringValue;
CS2_SimpleAdmin.IpAddress = address;
// Optimization: Cache remaining ConVar lookups
var hostportConVar = ConVar.Find("hostport");
var hostnameConVar = ConVar.Find("hostname");
var 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 ?? "";
CS2_SimpleAdmin.IpAddress = address;
Task.Run(async () =>
{
try
{
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
var addressExists = await connection.ExecuteScalarAsync<bool>(
"SELECT COUNT(*) FROM sa_servers WHERE address = @address",
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
int? serverId = await connection.ExecuteScalarAsync<int?>(
"SELECT id FROM sa_servers WHERE address = @address",
new { address });
if (!addressExists)
if (serverId == null)
{
await connection.ExecuteAsync(
"INSERT INTO sa_servers (address, hostname) VALUES (@address, @hostname)",
new { address, hostname });
"INSERT INTO sa_servers (address, hostname, rcon_password) VALUES (@address, @hostname, @rconPassword)",
new { address, hostname, rconPassword });
serverId = await connection.ExecuteScalarAsync<int>(
"SELECT id FROM sa_servers WHERE address = @address",
new { address });
}
else
{
await connection.ExecuteAsync(
"UPDATE `sa_servers` SET `hostname` = @hostname WHERE `address` = @address",
new { address, hostname });
"UPDATE sa_servers SET hostname = @hostname, rcon_password = @rconPassword WHERE address = @address",
new { address, hostname, rconPassword });
}
int? serverId = await connection.ExecuteScalarAsync<int>(
"SELECT `id` FROM `sa_servers` WHERE `address` = @address",
new { address });
CS2_SimpleAdmin.ServerId = serverId;
CS2_SimpleAdmin._logger?.LogInformation("Loaded server with ip {ip}", ipAddress);
if (CS2_SimpleAdmin.ServerId != null)
{
@@ -74,6 +109,8 @@ public class ServerManager
}
CS2_SimpleAdmin.ServerLoaded = true;
if (CS2_SimpleAdmin.Instance.CacheManager != null)
await CS2_SimpleAdmin.Instance.CacheManager.InitializeCacheAsync();
}
catch (Exception ex)
{
@@ -87,7 +124,7 @@ public class ServerManager
try
{
await client.GetAsync($"https://api.daffyy.love/index.php{queryString}");
await client.GetAsync($"https://api.daffyy.dev/index.php{queryString}");
}
catch (HttpRequestException ex)
{
@@ -95,6 +132,8 @@ public class ServerManager
}
}
});
CS2_SimpleAdmin.SimpleAdminApi?.OnSimpleAdminReadyEvent();
});
}
}

View File

@@ -1,30 +1,37 @@
using CS2_SimpleAdminApi;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
namespace CS2_SimpleAdmin.Managers;
internal class WarnManager(Database.Database? database)
internal class WarnManager(IDatabaseProvider? databaseProvider)
{
public async Task WarnPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
/// <summary>
/// Adds a warning to a player with an optional issuer and reason.
/// </summary>
/// <param name="player">The player who is being warned.</param>
/// <param name="issuer">The player issuing the warning; null indicates console or system.</param>
/// <param name="reason">The reason for the warning.</param>
/// <param name="time">Optional duration of the warning in minutes (0 means permanent).</param>
/// <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 (database == null) return;
if (databaseProvider == null) return null;
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
try
{
await using var connection = await database.GetConnectionAsync();
const string sql =
"INSERT INTO `sa_warns` (`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) " +
"VALUES (@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @serverid)";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddWarnQuery(true);
await connection.ExecuteAsync(sql, new
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64.ToString(),
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason,
duration = time,
@@ -32,28 +39,39 @@ internal class WarnManager(Database.Database? database)
created = now,
serverid = CS2_SimpleAdmin.ServerId
});
return warnId;
}
catch
{
return null;
}
catch { };
}
public async Task AddWarnBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
/// <summary>
/// Adds a warning to a player identified by SteamID with optional issuer and reason.
/// </summary>
/// <param name="playerSteamId">The SteamID64 of the player being warned.</param>
/// <param name="issuer">The player issuing the warning; null indicates console or system.</param>
/// <param name="reason">The reason for the warning.</param>
/// <param name="time">Optional duration of the warning in minutes (0 means permanent).</param>
/// <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 (database == null) return;
if (string.IsNullOrEmpty(playerSteamId)) return;
if (databaseProvider == null) return null;
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
try
{
await using var connection = await database.GetConnectionAsync();
const string sql = "INSERT INTO `sa_warns` (`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) " +
"VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @serverid)";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddWarnQuery(false);
await connection.ExecuteAsync(sql, new
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason,
duration = time,
@@ -61,34 +79,31 @@ internal class WarnManager(Database.Database? database)
created = now,
serverid = CS2_SimpleAdmin.ServerId
});
return warnId;
}
catch
{
return null;
}
catch { };
}
/// <summary>
/// Retrieves a list of warnings for a specific player.
/// </summary>
/// <param name="player">The player whose warnings to retrieve.</param>
/// <param name="active">If true, returns only active (non-expired) warnings; otherwise returns all warnings.</param>
/// <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 (database == null) return [];
if (databaseProvider == null) return [];
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
string sql;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
sql = 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";
}
else
{
sql = 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";
}
var parameters = new { PlayerSteamID = player.SteamId.SteamId64.ToString(), serverid = CS2_SimpleAdmin.ServerId };
var 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);
return warns.ToList();
@@ -99,24 +114,23 @@ internal class WarnManager(Database.Database? database)
}
}
public async Task<int> GetPlayerWarnsCount(string steamId, bool active = true)
/// <summary>
/// Retrieves the count of warnings for a player specified by SteamID.
/// </summary>
/// <param name="steamId">The SteamID64 of the player.</param>
/// <param name="active">If true, counts only active (non-expired) warnings; otherwise counts all warnings.</param>
/// <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 (database == null) return 0;
if (databaseProvider == null) return 0;
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? 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'";
var muteCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
return muteCount;
var sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
var warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
return warnsCount;
}
catch (Exception)
{
@@ -124,19 +138,22 @@ internal class WarnManager(Database.Database? database)
}
}
/// <summary>
/// Removes a specific warning by its identifier from a player's record.
/// </summary>
/// <param name="player">The player whose warning will be removed.</param>
/// <param name="warnId">The identifier of the warning to remove.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnwarnPlayer(PlayerInfo player, int warnId)
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "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";
await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64.ToString(), warnId, serverid = CS2_SimpleAdmin.ServerId });
var 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)
{
@@ -144,38 +161,41 @@ internal class WarnManager(Database.Database? database)
}
}
/// <summary>
/// Removes the most recent warning matching a player pattern (usually SteamID string).
/// </summary>
/// <param name="playerPattern">The pattern identifying the player whose last warning should be removed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnwarnPlayer(string playerPattern)
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = (SELECT MAX(id) FROM sa_warns WHERE player_steamid = @steamid AND status = 'ACTIVE')"
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = (SELECT MAX(id) FROM sa_warns WHERE player_steamid = @steamid AND status = 'ACTIVE' AND server_id = @serverid)";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { steamid = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogCritical($"Unable to remove last warn + {ex}");
CS2_SimpleAdmin._logger?.LogCritical("Unable to remove last warn {exception}", ex.Message);
}
}
/// <summary>
/// Expires old warnings based on the current time, removing or marking them as inactive.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task ExpireOldWarns()
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "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";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)

View File

@@ -1,14 +1,20 @@
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 IMenu? CreateMenu(string title)
public static void OpenMenu(CCSPlayerController admin)
{
return Helper.CreateMenu(title);
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);
}
@@ -26,44 +32,44 @@ public static class AdminMenu
// }
}
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(admin, "@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 ChatMenuOptionData(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)),
new ChatMenuOptionData(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)),
new ChatMenuOptionData(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(admin, "@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);
}
// 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);
// }
}

View File

@@ -0,0 +1,602 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using CS2_SimpleAdminApi;
namespace CS2_SimpleAdmin.Menus;
public abstract class BasicMenu
{
/// <summary>
/// Initializes all menus in the system by registering them with the MenuManager.
/// </summary>
public static void Initialize()
{
var manager = MenuManager.Instance;
// Players category menus
manager.RegisterMenu("players", "slap", "Slap Player", CreateSlapMenu, "@css/slay");
manager.RegisterMenu("players", "slay", "Slay Player", CreateSlayMenu, "@css/slay");
manager.RegisterMenu("players", "kick", "Kick Player", CreateKickMenu, "@css/kick");
manager.RegisterMenu("players", "warn", "Warn Player", CreateWarnMenu, "@css/kick");
manager.RegisterMenu("players", "ban", "Ban Player", CreateBanMenu, "@css/ban");
manager.RegisterMenu("players", "gag", "Gag Player", CreateGagMenu, "@css/chat");
manager.RegisterMenu("players", "mute", "Mute Player", CreateMuteMenu, "@css/chat");
manager.RegisterMenu("players", "silence", "Silence Player", CreateSilenceMenu, "@css/chat");
manager.RegisterMenu("players", "team", "Force Team", CreateForceTeamMenu, "@css/kick");
// Server category menus
manager.RegisterMenu("server", "plugins", "Manage Plugins", CreatePluginsMenu, "@css/root");
manager.RegisterMenu("server", "changemap", "Change Map", CreateChangeMapMenu, "@css/changemap");
manager.RegisterMenu("server", "restart", "Restart Game", CreateRestartGameMenu, "@css/generic");
manager.RegisterMenu("server", "custom", "Custom Commands", CreateCustomCommandsMenu, "@css/generic");
// Admin category menus
manager.RegisterMenu("admin", "add", "Add Admin", CreateAddAdminMenu, "@css/root");
manager.RegisterMenu("admin", "remove", "Remove Admin", CreateRemoveAdminMenu, "@css/root");
manager.RegisterMenu("admin", "reload", "Reload Admins", CreateReloadAdminsMenu, "@css/root");
}
/// <summary>
/// Creates menu for slapping players with selectable damage amounts.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the slap menu.</returns>
private static MenuBuilder CreateSlapMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var slapMenu = new MenuBuilder(localizer?["sa_slap"] ?? "Slap Player");
var players = Helper.GetValidPlayers().Where(admin.CanTarget);
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
slapMenu.AddSubMenu(playerName, () => CreateSlapDamageMenu(admin, player));
}
return slapMenu.WithBackButton();
}
/// <summary>
/// Creates damage selection submenu for slapping a specific player.
/// </summary>
/// <param name="admin">The admin player executing the slap.</param>
/// <param name="target">The target player to be slapped.</param>
/// <returns>A MenuBuilder instance for the slap damage menu.</returns>
private static MenuBuilder CreateSlapDamageMenu(CCSPlayerController admin, CCSPlayerController target)
{
var slapDamageMenu = new MenuBuilder($"Slap: {target.PlayerName}");
var damages = new[] { 0, 1, 5, 10, 50, 100 };
foreach (var damage in damages)
{
slapDamageMenu.AddOption($"{damage} HP", _ =>
{
if (target.IsValid)
{
CS2_SimpleAdmin.Slap(admin, target, damage);
// Keep menu open for consecutive slaps
CreateSlapDamageMenu(admin, target).OpenMenu(admin);
}
});
}
return slapDamageMenu.WithBackButton();
}
/// <summary>
/// Creates menu for slaying (killing) players.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the slay menu.</returns>
private static MenuBuilder CreateSlayMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var slayMenu = new MenuBuilder(localizer?["sa_slay"] ?? "Slay Player");
var players = Helper.GetValidPlayers().Where(admin.CanTarget);
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
slayMenu.AddOption(playerName, _ =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Slay(admin, player);
}
});
}
return slayMenu.WithBackButton();
}
/// <summary>
/// Creates menu for kicking players with reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the kick menu.</returns>
private static MenuBuilder CreateKickMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var kickMenu = new MenuBuilder(localizer?["sa_kick"] ?? "Kick Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
kickMenu.AddSubMenu(playerName, () => CreateReasonMenu(admin, player, "Kick", PenaltyType.Kick,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Kick(admin, player, reason, admin.PlayerName);
}
}));
}
return kickMenu.WithBackButton();
}
/// <summary>
/// Creates menu for warning players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the warn menu.</returns>
private static MenuBuilder CreateWarnMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var warnMenu = new MenuBuilder(localizer?["sa_warn"] ?? "Warn Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
warnMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Warn",
(_, _, duration) => CreateReasonMenu(admin, player, "Warn", PenaltyType.Warn,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason, admin.PlayerName);
}
})));
}
return warnMenu.WithBackButton();
}
/// <summary>
/// Creates menu for banning players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the ban menu.</returns>
private static MenuBuilder CreateBanMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var banMenu = new MenuBuilder(localizer?["sa_ban"] ?? "Ban Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
banMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Ban",
(_, _, duration) => CreateReasonMenu(admin, player, "Ban", PenaltyType.Ban,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason, admin.PlayerName);
}
})));
}
return banMenu.WithBackButton();
}
/// <summary>
/// Creates menu for gagging (text chat muting) players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the gag menu.</returns>
private static MenuBuilder CreateGagMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var gagMenu = new MenuBuilder(localizer?["sa_gag"] ?? "Gag Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
gagMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Gag",
(_, _, duration) => CreateReasonMenu(admin, player, "Gag", PenaltyType.Gag,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
}
})));
}
return gagMenu.WithBackButton();
}
/// <summary>
/// Creates menu for muting (voice chat muting) players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the mute menu.</returns>
private static MenuBuilder CreateMuteMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var muteMenu = new MenuBuilder(localizer?["sa_mute"] ?? "Mute Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
muteMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Mute",
(_, _, duration) => CreateReasonMenu(admin, player, "Mute", PenaltyType.Mute,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
}
})));
}
return muteMenu.WithBackButton();
}
/// <summary>
/// Creates menu for silencing (both text and voice chat muting) players with duration and reason selection.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the silence menu.</returns>
private static MenuBuilder CreateSilenceMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var silenceMenu = new MenuBuilder(localizer?["sa_silence"] ?? "Silence Player");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
silenceMenu.AddSubMenu(playerName, () => CreateDurationMenu(admin, player, "Silence",
(_, _, duration) => CreateReasonMenu(admin, player, "Silence", PenaltyType.Silence,
(_, _, reason) =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
}
})));
}
return silenceMenu.WithBackButton();
}
/// <summary>
/// Creates menu for forcing players to switch teams.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the force team menu.</returns>
private static MenuBuilder CreateForceTeamMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var teamMenu = new MenuBuilder(localizer?["sa_team_force"] ?? "Force Team");
var players = Helper.GetValidPlayers().Where(p => admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
teamMenu.AddSubMenu(playerName, () => CreateTeamSelectionMenu(admin, player));
}
return teamMenu.WithBackButton();
}
/// <summary>
/// Creates team selection submenu for forcing a specific player to a team.
/// </summary>
/// <param name="admin">The admin player executing the team change.</param>
/// <param name="target">The target player to be moved.</param>
/// <returns>A MenuBuilder instance for the team selection menu.</returns>
private static MenuBuilder CreateTeamSelectionMenu(CCSPlayerController admin, CCSPlayerController target)
{
var localizer = CS2_SimpleAdmin._localizer;
var teamSelectionMenu = new MenuBuilder($"Force Team: {target.PlayerName}");
var teams = new[]
{
(localizer?["sa_team_ct"] ?? "CT", "ct", CsTeam.CounterTerrorist),
(localizer?["sa_team_t"] ?? "T", "t", CsTeam.Terrorist),
(localizer?["sa_team_swap"] ?? "Swap", "swap", CsTeam.Spectator),
(localizer?["sa_team_spec"] ?? "Spec", "spec", CsTeam.Spectator)
};
foreach (var (name, teamName, teamNum) in teams)
{
teamSelectionMenu.AddOption(name, _ =>
{
if (target.IsValid)
{
CS2_SimpleAdmin.ChangeTeam(admin, target, teamName, teamNum, true);
}
});
}
return teamSelectionMenu.WithBackButton();
}
/// <summary>
/// Creates menu for managing server plugins.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the plugins menu.</returns>
private static MenuBuilder CreatePluginsMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var pluginsMenu = new MenuBuilder(localizer?["sa_menu_pluginsmanager_title"] ?? "Manage Plugins");
pluginsMenu.AddOption("Open Plugins Manager", _ =>
{
admin.ExecuteClientCommandFromServer("css_pluginsmanager");
});
return pluginsMenu.WithBackButton();
}
/// <summary>
/// Creates menu for changing the current map (includes default and workshop maps).
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the change map menu.</returns>
private static MenuBuilder CreateChangeMapMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var mapMenu = new MenuBuilder(localizer?["sa_changemap"] ?? "Change Map");
// Add default maps
var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps;
foreach (var map in maps)
{
mapMenu.AddOption(map, _ =>
{
CS2_SimpleAdmin.Instance.ChangeMap(admin, map);
});
}
// Add workshop maps
var wsMaps = CS2_SimpleAdmin.Instance.Config.WorkshopMaps;
foreach (var wsMap in wsMaps)
{
mapMenu.AddOption($"{wsMap.Key} (WS)", _ =>
{
CS2_SimpleAdmin.Instance.ChangeWorkshopMap(admin, wsMap.Value?.ToString() ?? wsMap.Key);
});
}
return mapMenu.WithBackButton();
}
/// <summary>
/// Creates menu for restarting the current game/round.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the restart game menu.</returns>
private static MenuBuilder CreateRestartGameMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var restartMenu = new MenuBuilder(localizer?["sa_restart_game"] ?? "Restart Game");
restartMenu.AddOption("Restart Round", _ =>
{
CS2_SimpleAdmin.RestartGame(admin);
});
return restartMenu.WithBackButton();
}
/// <summary>
/// Creates menu for executing custom server commands defined in configuration.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the custom commands menu.</returns>
private static MenuBuilder CreateCustomCommandsMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var customMenu = new MenuBuilder(localizer?["sa_menu_custom_commands"] ?? "Custom Commands");
var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
foreach (var customCommand in customCommands)
{
if (string.IsNullOrEmpty(customCommand.DisplayName) || string.IsNullOrEmpty(customCommand.Command))
continue;
var steamId = new SteamID(admin.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, customCommand.Flag))
continue;
customMenu.AddOption(customCommand.DisplayName, _ =>
{
Helper.TryLogCommandOnDiscord(admin, customCommand.Command);
if (customCommand.ExecuteOnClient)
admin.ExecuteClientCommandFromServer(customCommand.Command);
else
Server.ExecuteCommand(customCommand.Command);
});
}
return customMenu.WithBackButton();
}
/// <summary>
/// Creates menu for adding admin privileges to players.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the add admin menu.</returns>
private static MenuBuilder CreateAddAdminMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var addAdminMenu = new MenuBuilder(localizer?["sa_admin_add"] ?? "Add Admin");
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
foreach (var player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
addAdminMenu.AddSubMenu(playerName, () => CreateAdminFlagsMenu(admin, player));
}
return addAdminMenu.WithBackButton();
}
/// <summary>
/// Creates admin flags selection submenu for granting specific permissions to a player.
/// </summary>
/// <param name="admin">The admin player granting permissions.</param>
/// <param name="target">The target player to receive admin privileges.</param>
/// <returns>A MenuBuilder instance for the admin flags menu.</returns>
private static MenuBuilder CreateAdminFlagsMenu(CCSPlayerController admin, CCSPlayerController target)
{
var flagsMenu = new MenuBuilder($"Add Admin: {target.PlayerName}");
foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags)
{
var hasFlag = AdminManager.PlayerHasPermissions(target, adminFlag.Flag);
flagsMenu.AddOption(adminFlag.Name, _ =>
{
if (target.IsValid)
{
CS2_SimpleAdmin.AddAdmin(admin, target.SteamID.ToString(), target.PlayerName, adminFlag.Flag, 10);
}
}, hasFlag); // Disabled if player already has this flag
}
return flagsMenu.WithBackButton();
}
/// <summary>
/// Creates menu for removing admin privileges from players.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the remove admin menu.</returns>
private static MenuBuilder CreateRemoveAdminMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var removeAdminMenu = new MenuBuilder(localizer?["sa_admin_remove"] ?? "Remove Admin");
var adminPlayers = Helper.GetValidPlayers().Where(p =>
AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0 &&
p != admin &&
admin.CanTarget(p));
foreach (var player in adminPlayers)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
removeAdminMenu.AddOption(playerName, _ =>
{
if (player.IsValid)
{
CS2_SimpleAdmin.Instance.RemoveAdmin(admin, player.SteamID.ToString());
}
});
}
return removeAdminMenu.WithBackButton();
}
/// <summary>
/// Creates menu for reloading admin list from database.
/// </summary>
/// <param name="admin">The admin player opening the menu.</param>
/// <returns>A MenuBuilder instance for the reload admins menu.</returns>
private static MenuBuilder CreateReloadAdminsMenu(CCSPlayerController admin)
{
var localizer = CS2_SimpleAdmin._localizer;
var reloadMenu = new MenuBuilder(localizer?["sa_admin_reload"] ?? "Reload Admins");
reloadMenu.AddOption("Reload Admins", _ =>
{
CS2_SimpleAdmin.Instance.ReloadAdmins(admin);
});
return reloadMenu.WithBackButton();
}
/// <summary>
/// Creates duration selection submenu for time-based penalties (ban, mute, gag, etc.).
/// </summary>
/// <param name="admin">The admin player selecting duration.</param>
/// <param name="player">The target player for the penalty.</param>
/// <param name="actionName">The name of the penalty action.</param>
/// <param name="onSelectAction">Callback action executed when duration is selected.</param>
/// <returns>A MenuBuilder instance for the duration menu.</returns>
private static MenuBuilder CreateDurationMenu(CCSPlayerController admin, CCSPlayerController player, string actionName,
Action<CCSPlayerController, CCSPlayerController, int> onSelectAction)
{
var durationMenu = new MenuBuilder($"{actionName} Duration: {player.PlayerName}");
foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations)
{
durationMenu.AddOption(durationItem.Name, _ =>
{
onSelectAction(admin, player, durationItem.Duration);
});
}
return durationMenu.WithBackButton();
}
/// <summary>
/// Creates reason selection submenu for penalties with predefined reasons from configuration.
/// </summary>
/// <param name="admin">The admin player selecting reason.</param>
/// <param name="player">The target player for the penalty.</param>
/// <param name="actionName">The name of the penalty action.</param>
/// <param name="penaltyType">The type of penalty to determine which reason list to use.</param>
/// <param name="onSelectAction">Callback action executed when reason is selected.</param>
/// <returns>A MenuBuilder instance for the reason menu.</returns>
private static MenuBuilder CreateReasonMenu(CCSPlayerController admin, CCSPlayerController player, string actionName,
PenaltyType penaltyType, Action<CCSPlayerController, CCSPlayerController, string> onSelectAction)
{
var reasonMenu = new MenuBuilder($"{actionName} Reason: {player.PlayerName}");
var reasons = penaltyType switch
{
PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons,
PenaltyType.Kick => CS2_SimpleAdmin.Instance.Config.MenuConfigs.KickReasons,
PenaltyType.Mute => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons,
PenaltyType.Warn => CS2_SimpleAdmin.Instance.Config.MenuConfigs.WarnReasons,
PenaltyType.Gag or PenaltyType.Silence => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons,
_ => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons
};
foreach (var reason in reasons)
{
reasonMenu.AddOption(reason, _ =>
{
onSelectAction(admin, player, reason);
});
}
return reasonMenu.WithBackButton();
}
}

View File

@@ -1,6 +1,7 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
@@ -8,11 +9,11 @@ public static class CustomCommandsMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(admin, "@css/generic") == false)
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
@@ -27,7 +28,7 @@ public static class CustomCommandsMenu
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(admin, customCommand.Flag)
let hasRights = AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), customCommand.Flag)
where hasRights
select new ChatMenuOptionData(customCommand.DisplayName, () =>
{

View File

@@ -8,7 +8,6 @@ public static class DurationMenu
public static void OpenMenu(CCSPlayerController admin, string menuName, CCSPlayerController player, Action<CCSPlayerController, CCSPlayerController, 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); });
@@ -20,7 +19,6 @@ public static class DurationMenu
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); });

View File

@@ -1,266 +1,267 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities.Constants;
namespace CS2_SimpleAdmin.Menus;
public static class FunActionsMenu
{
private static Dictionary<int, CsItem>? _weaponsCache;
private static Dictionary<int, CsItem> GetWeaponsCache
{
get
{
if (_weaponsCache != null) return _weaponsCache;
var weaponsArray = Enum.GetValues(typeof(CsItem));
// avoid duplicates in the menu
_weaponsCache = new Dictionary<int, CsItem>();
foreach (CsItem item in weaponsArray)
{
if (item == CsItem.Tablet)
continue;
_weaponsCache[(int)item] = item;
}
return _weaponsCache;
}
}
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(admin, "@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 = AdminMenu.CreateMenu(localizer?["sa_menu_fun_commands"] ?? "Fun Commands");
List<ChatMenuOptionData> options = [];
//var hasCheats = AdminManager.PlayerHasPermissions(admin, "@css/cheats");
//var hasSlay = AdminManager.PlayerHasPermissions(admin, "@css/slay");
// options added in order
if (AdminManager.CommandIsOverriden("css_god")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_god"))
: AdminManager.PlayerHasPermissions(admin, "@css/cheats"))
options.Add(new ChatMenuOptionData(localizer?["sa_godmode"] ?? "God Mode", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_godmode"] ?? "God Mode", GodMode)));
if (AdminManager.CommandIsOverriden("css_noclip")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_noclip"))
: AdminManager.PlayerHasPermissions(admin, "@css/cheats"))
options.Add(new ChatMenuOptionData(localizer?["sa_noclip"] ?? "No Clip", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_noclip"] ?? "No Clip", NoClip)));
if (AdminManager.CommandIsOverriden("css_respawn")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_respawn"))
: AdminManager.PlayerHasPermissions(admin, "@css/cheats"))
options.Add(new ChatMenuOptionData(localizer?["sa_respawn"] ?? "Respawn", () => PlayersMenu.OpenDeadMenu(admin, localizer?["sa_respawn"] ?? "Respawn", Respawn)));
if (AdminManager.CommandIsOverriden("css_give")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_give"))
: AdminManager.PlayerHasPermissions(admin, "@css/cheats"))
options.Add(new ChatMenuOptionData(localizer?["sa_give_weapon"] ?? "Give Weapon", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_give_weapon"] ?? "Give Weapon", GiveWeaponMenu)));
if (AdminManager.CommandIsOverriden("css_strip")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_strip"))
: AdminManager.PlayerHasPermissions(admin, "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_strip_weapons"] ?? "Strip Weapons", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_strip_weapons"] ?? "Strip Weapons", StripWeapons)));
if (AdminManager.CommandIsOverriden("css_freeze")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_freeze"))
: AdminManager.PlayerHasPermissions(admin, "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_freeze"] ?? "Freeze", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_freeze"] ?? "Freeze", Freeze)));
if (AdminManager.CommandIsOverriden("css_hp")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_hp"))
: AdminManager.PlayerHasPermissions(admin, "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_set_hp"] ?? "Set Hp", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_hp"] ?? "Set Hp", SetHpMenu)));
if (AdminManager.CommandIsOverriden("css_speed")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_speed"))
: AdminManager.PlayerHasPermissions(admin, "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_set_speed"] ?? "Set Speed", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_speed"] ?? "Set Speed", SetSpeedMenu)));
if (AdminManager.CommandIsOverriden("css_gravity")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_gravity"))
: AdminManager.PlayerHasPermissions(admin, "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_set_gravity"] ?? "Set Gravity", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_gravity"] ?? "Set Gravity", SetGravityMenu)));
if (AdminManager.CommandIsOverriden("css_money")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_money"))
: AdminManager.PlayerHasPermissions(admin, "@css/slay"))
options.Add(new ChatMenuOptionData(localizer?["sa_set_money"] ?? "Set Money", () => PlayersMenu.OpenMenu(admin, localizer?["sa_set_money"] ?? "Set Money", SetMoneyMenu)));
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void GodMode(CCSPlayerController admin, CCSPlayerController player)
{
CS2_SimpleAdmin.God(admin, player);
}
private static void NoClip(CCSPlayerController admin, CCSPlayerController player)
{
CS2_SimpleAdmin.NoClip(admin, player);
}
private static void Respawn(CCSPlayerController? admin, CCSPlayerController player)
{
CS2_SimpleAdmin.Respawn(admin, player);
}
private static void GiveWeaponMenu(CCSPlayerController admin, CCSPlayerController player)
{
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_give_weapon"] ?? "Give Weapon"}: {player.PlayerName}");
foreach (var weapon in GetWeaponsCache)
{
menu?.AddMenuOption(weapon.Value.ToString(), (_, _) => { GiveWeapon(admin, player, weapon.Value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void GiveWeapon(CCSPlayerController admin, CCSPlayerController player, CsItem weaponValue)
{
CS2_SimpleAdmin.GiveWeapon(admin, player, weaponValue);
}
private static void StripWeapons(CCSPlayerController admin, CCSPlayerController player)
{
CS2_SimpleAdmin.StripWeapons(admin, player);
}
private static void Freeze(CCSPlayerController admin, CCSPlayerController player)
{
if (!(player.PlayerPawn.Value?.IsValid ?? false))
return;
if (player.PlayerPawn.Value.MoveType != MoveType_t.MOVETYPE_INVALID)
CS2_SimpleAdmin.Freeze(admin, player, -1);
else
CS2_SimpleAdmin.Unfreeze(admin, player);
}
private static void SetHpMenu(CCSPlayerController admin, CCSPlayerController player)
{
var hpArray = new[]
{
new Tuple<string, int>("1", 1),
new Tuple<string, int>("10", 10),
new Tuple<string, int>("25", 25),
new Tuple<string, int>("50", 50),
new Tuple<string, int>("100", 100),
new Tuple<string, int>("200", 200),
new Tuple<string, int>("500", 500),
new Tuple<string, int>("999", 999)
};
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_hp"] ?? "Set Hp"}: {player.PlayerName}");
foreach (var (optionName, value) in hpArray)
{
menu?.AddMenuOption(optionName, (_, _) => { SetHp(admin, player, value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SetHp(CCSPlayerController admin, CCSPlayerController player, int hp)
{
CS2_SimpleAdmin.SetHp(admin, player, hp);
}
private static void SetSpeedMenu(CCSPlayerController admin, CCSPlayerController player)
{
var speedArray = new[]
{
new Tuple<string, float>("0.1", .1f),
new Tuple<string, float>("0.25", .25f),
new Tuple<string, float>("0.5", .5f),
new Tuple<string, float>("0.75", .75f),
new Tuple<string, float>("1", 1),
new Tuple<string, float>("2", 2),
new Tuple<string, float>("3", 3),
new Tuple<string, float>("4", 4)
};
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_speed"] ?? "Set Speed"}: {player.PlayerName}");
foreach (var (optionName, value) in speedArray)
{
menu?.AddMenuOption(optionName, (_, _) => { SetSpeed(admin, player, value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SetSpeed(CCSPlayerController admin, CCSPlayerController player, float speed)
{
CS2_SimpleAdmin.SetSpeed(admin, player, speed);
}
private static void SetGravityMenu(CCSPlayerController admin, CCSPlayerController player)
{
var gravityArray = new[]
{
new Tuple<string, float>("0.1", .1f),
new Tuple<string, float>("0.25", .25f),
new Tuple<string, float>("0.5", .5f),
new Tuple<string, float>("0.75", .75f),
new Tuple<string, float>("1", 1),
new Tuple<string, float>("2", 2)
};
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_gravity"] ?? "Set Gravity"}: {player.PlayerName}");
foreach (var (optionName, value) in gravityArray)
{
menu?.AddMenuOption(optionName, (_, _) => { SetGravity(admin, player, value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SetGravity(CCSPlayerController admin, CCSPlayerController player, float gravity)
{
CS2_SimpleAdmin.SetGravity(admin, player, gravity);
}
private static void SetMoneyMenu(CCSPlayerController admin, CCSPlayerController player)
{
var moneyArray = new[]
{
new Tuple<string, int>("$0", 0),
new Tuple<string, int>("$1000", 1000),
new Tuple<string, int>("$2500", 2500),
new Tuple<string, int>("$5000", 5000),
new Tuple<string, int>("$10000", 10000),
new Tuple<string, int>("$16000", 16000)
};
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_money"] ?? "Set Money"}: {player.PlayerName}");
foreach (var (optionName, value) in moneyArray)
{
menu?.AddMenuOption(optionName, (_, _) => { SetMoney(admin, player, value); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SetMoney(CCSPlayerController admin, CCSPlayerController player, int money)
{
CS2_SimpleAdmin.SetMoney(admin, player, money);
}
}
// using CounterStrikeSharp.API.Core;
// using CounterStrikeSharp.API.Modules.Admin;
// using CounterStrikeSharp.API.Modules.Entities;
// using CounterStrikeSharp.API.Modules.Entities.Constants;
//
// namespace CS2_SimpleAdmin.Menus;
//
// public static class FunActionsMenu
// {
// private static Dictionary<int, CsItem>? _weaponsCache;
//
// private static Dictionary<int, CsItem> GetWeaponsCache
// {
// get
// {
// if (_weaponsCache != null) return _weaponsCache;
//
// var weaponsArray = Enum.GetValues(typeof(CsItem));
//
// // avoid duplicates in the menu
// _weaponsCache = new Dictionary<int, CsItem>();
// foreach (CsItem item in weaponsArray)
// {
// if (item == CsItem.Tablet)
// continue;
//
// _weaponsCache[(int)item] = item;
// }
//
// return _weaponsCache;
// }
// }
//
// 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_fun_commands"] ?? "Fun Commands");
// List<ChatMenuOptionData> options = [];
//
// //var hasCheats = AdminManager.PlayerHasPermissions(admin, "@css/cheats");
// //var hasSlay = AdminManager.PlayerHasPermissions(admin, "@css/slay");
//
// // options added in order
//
// if (AdminManager.CommandIsOverriden("css_god")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_god"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
// options.Add(new ChatMenuOptionData(localizer?["sa_godmode"] ?? "God Mode", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_godmode"] ?? "God Mode", GodMode)));
// if (AdminManager.CommandIsOverriden("css_noclip")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_noclip"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
// options.Add(new ChatMenuOptionData(localizer?["sa_noclip"] ?? "No Clip", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_noclip"] ?? "No Clip", NoClip)));
// if (AdminManager.CommandIsOverriden("css_respawn")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_respawn"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
// options.Add(new ChatMenuOptionData(localizer?["sa_respawn"] ?? "Respawn", () => PlayersMenu.OpenDeadMenu(admin, localizer?["sa_respawn"] ?? "Respawn", Respawn)));
// if (AdminManager.CommandIsOverriden("css_give")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_give"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/cheats"))
// options.Add(new ChatMenuOptionData(localizer?["sa_give_weapon"] ?? "Give Weapon", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_give_weapon"] ?? "Give Weapon", GiveWeaponMenu)));
//
// if (AdminManager.CommandIsOverriden("css_strip")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_strip"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_strip_weapons"] ?? "Strip Weapons", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_strip_weapons"] ?? "Strip Weapons", StripWeapons)));
// if (AdminManager.CommandIsOverriden("css_freeze")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_freeze"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_freeze"] ?? "Freeze", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_freeze"] ?? "Freeze", Freeze)));
// if (AdminManager.CommandIsOverriden("css_hp")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_hp"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_set_hp"] ?? "Set Hp", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_hp"] ?? "Set Hp", SetHpMenu)));
// if (AdminManager.CommandIsOverriden("css_speed")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_speed"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_set_speed"] ?? "Set Speed", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_speed"] ?? "Set Speed", SetSpeedMenu)));
// if (AdminManager.CommandIsOverriden("css_gravity")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gravity"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_set_gravity"] ?? "Set Gravity", () => PlayersMenu.OpenAliveMenu(admin, localizer?["sa_set_gravity"] ?? "Set Gravity", SetGravityMenu)));
// if (AdminManager.CommandIsOverriden("css_money")
// ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_money"))
// : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay"))
// options.Add(new ChatMenuOptionData(localizer?["sa_set_money"] ?? "Set Money", () => PlayersMenu.OpenMenu(admin, localizer?["sa_set_money"] ?? "Set Money", SetMoneyMenu)));
//
// foreach (var menuOptionData in options)
// {
// var menuName = menuOptionData.Name;
// menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action(); }, menuOptionData.Disabled);
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void GodMode(CCSPlayerController admin, CCSPlayerController player)
// {
// CS2_SimpleAdmin.God(admin, player);
// }
//
// private static void NoClip(CCSPlayerController admin, CCSPlayerController player)
// {
// CS2_SimpleAdmin.NoClip(admin, player);
// }
//
// private static void Respawn(CCSPlayerController? admin, CCSPlayerController player)
// {
// CS2_SimpleAdmin.Respawn(admin, player);
// }
//
// private static void GiveWeaponMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_give_weapon"] ?? "Give Weapon"}: {player.PlayerName}");
//
// foreach (var weapon in GetWeaponsCache)
// {
// menu?.AddMenuOption(weapon.Value.ToString(), (_, _) => { GiveWeapon(admin, player, weapon.Value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void GiveWeapon(CCSPlayerController admin, CCSPlayerController player, CsItem weaponValue)
// {
// CS2_SimpleAdmin.GiveWeapon(admin, player, weaponValue);
// }
//
// private static void StripWeapons(CCSPlayerController admin, CCSPlayerController player)
// {
// CS2_SimpleAdmin.StripWeapons(admin, player);
// }
//
// private static void Freeze(CCSPlayerController admin, CCSPlayerController player)
// {
// if (!(player.PlayerPawn.Value?.IsValid ?? false))
// return;
//
// if (player.PlayerPawn.Value.MoveType != MoveType_t.MOVETYPE_INVALID)
// CS2_SimpleAdmin.Freeze(admin, player, -1);
// else
// CS2_SimpleAdmin.Unfreeze(admin, player);
// }
//
// private static void SetHpMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var hpArray = new[]
// {
// new Tuple<string, int>("1", 1),
// new Tuple<string, int>("10", 10),
// new Tuple<string, int>("25", 25),
// new Tuple<string, int>("50", 50),
// new Tuple<string, int>("100", 100),
// new Tuple<string, int>("200", 200),
// new Tuple<string, int>("500", 500),
// new Tuple<string, int>("999", 999)
// };
//
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_hp"] ?? "Set Hp"}: {player.PlayerName}");
//
// foreach (var (optionName, value) in hpArray)
// {
// menu?.AddMenuOption(optionName, (_, _) => { SetHp(admin, player, value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void SetHp(CCSPlayerController admin, CCSPlayerController player, int hp)
// {
// CS2_SimpleAdmin.SetHp(admin, player, hp);
// }
//
// private static void SetSpeedMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var speedArray = new[]
// {
// new Tuple<string, float>("0.1", .1f),
// new Tuple<string, float>("0.25", .25f),
// new Tuple<string, float>("0.5", .5f),
// new Tuple<string, float>("0.75", .75f),
// new Tuple<string, float>("1", 1),
// new Tuple<string, float>("2", 2),
// new Tuple<string, float>("3", 3),
// new Tuple<string, float>("4", 4)
// };
//
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_speed"] ?? "Set Speed"}: {player.PlayerName}");
//
// foreach (var (optionName, value) in speedArray)
// {
// menu?.AddMenuOption(optionName, (_, _) => { SetSpeed(admin, player, value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void SetSpeed(CCSPlayerController admin, CCSPlayerController player, float speed)
// {
// CS2_SimpleAdmin.SetSpeed(admin, player, speed);
// }
//
// private static void SetGravityMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var gravityArray = new[]
// {
// new Tuple<string, float>("0.1", .1f),
// new Tuple<string, float>("0.25", .25f),
// new Tuple<string, float>("0.5", .5f),
// new Tuple<string, float>("0.75", .75f),
// new Tuple<string, float>("1", 1),
// new Tuple<string, float>("2", 2)
// };
//
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_gravity"] ?? "Set Gravity"}: {player.PlayerName}");
//
// foreach (var (optionName, value) in gravityArray)
// {
// menu?.AddMenuOption(optionName, (_, _) => { SetGravity(admin, player, value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void SetGravity(CCSPlayerController admin, CCSPlayerController player, float gravity)
// {
// CS2_SimpleAdmin.SetGravity(admin, player, gravity);
// }
//
// private static void SetMoneyMenu(CCSPlayerController admin, CCSPlayerController player)
// {
// var moneyArray = new[]
// {
// new Tuple<string, int>("$0", 0),
// new Tuple<string, int>("$1000", 1000),
// new Tuple<string, int>("$2500", 2500),
// new Tuple<string, int>("$5000", 5000),
// new Tuple<string, int>("$10000", 10000),
// new Tuple<string, int>("$16000", 16000)
// };
//
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_set_money"] ?? "Set Money"}: {player.PlayerName}");
//
// foreach (var (optionName, value) in moneyArray)
// {
// menu?.AddMenuOption(optionName, (_, _) => { SetMoney(admin, player, value); });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
// }
//
// private static void SetMoney(CCSPlayerController admin, CCSPlayerController player, int money)
// {
// CS2_SimpleAdmin.SetMoney(admin, player, money);
// }
// }

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
@@ -7,11 +8,11 @@ public static class ManageAdminsMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(admin, "@css/root") == false)
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
@@ -23,12 +24,12 @@ public static class ManageAdminsMenu
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_admins_manage"] ?? "Admins Manage");
List<ChatMenuOptionData> options =
[
new ChatMenuOptionData(localizer?["sa_admin_add"] ?? "Add Admin",
new(localizer?["sa_admin_add"] ?? "Add Admin",
() => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_admin_add"] ?? "Add Admin", AddAdminMenu)),
new ChatMenuOptionData(localizer?["sa_admin_remove"] ?? "Remove Admin",
new(localizer?["sa_admin_remove"] ?? "Remove Admin",
() => PlayersMenu.OpenAdminPlayersMenu(admin, localizer?["sa_admin_remove"] ?? "Remove Admin", RemoveAdmin,
player => player != admin && admin.CanTarget(player))),
new ChatMenuOptionData(localizer?["sa_admin_reload"] ?? "Reload Admins", () => ReloadAdmins(admin))
new(localizer?["sa_admin_reload"] ?? "Reload Admins", () => ReloadAdmins(admin))
];
foreach (var menuOptionData in options)

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Utils;
using CS2_SimpleAdminApi;
@@ -9,11 +10,11 @@ public static class ManagePlayersMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(admin, "@css/generic") == false)
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
@@ -26,10 +27,10 @@ public static class ManagePlayersMenu
List<ChatMenuOptionData> options = [];
// permissions
var hasSlay = AdminManager.CommandIsOverriden("css_slay") ? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_slay")) : AdminManager.PlayerHasPermissions(admin, "@css/slay");
var hasKick = AdminManager.CommandIsOverriden("css_kick") ? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_kick")) : AdminManager.PlayerHasPermissions(admin, "@css/kick");
var hasBan = AdminManager.CommandIsOverriden("css_ban") ? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_ban")) : AdminManager.PlayerHasPermissions(admin, "@css/ban");
var hasChat = AdminManager.CommandIsOverriden("css_gag") ? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_gag")) : AdminManager.PlayerHasPermissions(admin, "@css/chat");
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
@@ -46,8 +47,8 @@ public static class ManagePlayersMenu
}
if (AdminManager.CommandIsOverriden("css_warn")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_warn"))
: AdminManager.PlayerHasPermissions(admin, "@css/kick"))
? 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", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_warn"] ?? "Warn"}: {player.PlayerName}", player, WarnMenu))));
if (hasBan)
@@ -56,22 +57,22 @@ public static class ManagePlayersMenu
if (hasChat)
{
if (AdminManager.CommandIsOverriden("css_gag")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_gag"))
: AdminManager.PlayerHasPermissions(admin, "@css/chat"))
? 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", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_gag"] ?? "Gag"}: {player.PlayerName}", player, GagMenu))));
if (AdminManager.CommandIsOverriden("css_mute")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_mute"))
: AdminManager.PlayerHasPermissions(admin, "@css/chat"))
? 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", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_mute"] ?? "Mute"}: {player.PlayerName}", player, MuteMenu))));
if (AdminManager.CommandIsOverriden("css_silence")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_silence"))
: AdminManager.PlayerHasPermissions(admin, "@css/chat"))
? 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", (admin, player) => DurationMenu.OpenMenu(admin, $"{localizer?["sa_silence"] ?? "Silence"}: {player.PlayerName}", player, SilenceMenu))));
}
if (AdminManager.CommandIsOverriden("css_team")
? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_team"))
: AdminManager.PlayerHasPermissions(admin, "@css/kick"))
? 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)
@@ -89,12 +90,12 @@ public static class ManagePlayersMenu
List<ChatMenuOptionData> options =
[
// options added in order
new ChatMenuOptionData("0 hp", () => ApplySlapAndKeepMenu(admin, player, 0)),
new ChatMenuOptionData("1 hp", () => ApplySlapAndKeepMenu(admin, player, 1)),
new ChatMenuOptionData("5 hp", () => ApplySlapAndKeepMenu(admin, player, 5)),
new ChatMenuOptionData("10 hp", () => ApplySlapAndKeepMenu(admin, player, 10)),
new ChatMenuOptionData("50 hp", () => ApplySlapAndKeepMenu(admin, player, 50)),
new ChatMenuOptionData("100 hp", () => ApplySlapAndKeepMenu(admin, player, 100)),
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)
@@ -148,7 +149,7 @@ public static class ManagePlayersMenu
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Kick(admin, player, reason);
CS2_SimpleAdmin.Instance.Kick(admin, player, reason, admin.PlayerName);
}
internal static void BanMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
@@ -180,7 +181,7 @@ public static class ManagePlayersMenu
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason);
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason, admin.PlayerName);
}
private static void WarnMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
@@ -210,7 +211,7 @@ public static class ManagePlayersMenu
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason);
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason, admin.PlayerName);
}
internal static void GagMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
@@ -311,10 +312,10 @@ public static class ManagePlayersMenu
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_team_force"] ?? "Force Team"} {player.PlayerName}");
List<ChatMenuOptionData> options =
[
new ChatMenuOptionData(CS2_SimpleAdmin._localizer?["sa_team_ct"] ?? "CT", () => ForceTeam(admin, player, "ct", CsTeam.CounterTerrorist)),
new ChatMenuOptionData(CS2_SimpleAdmin._localizer?["sa_team_t"] ?? "T", () => ForceTeam(admin, player, "t", CsTeam.Terrorist)),
new ChatMenuOptionData(CS2_SimpleAdmin._localizer?["sa_team_swap"] ?? "Swap", () => ForceTeam(admin, player, "swap", CsTeam.Spectator)),
new ChatMenuOptionData(CS2_SimpleAdmin._localizer?["sa_team_spec"] ?? "Spec", () => ForceTeam(admin, player, "spec", CsTeam.Spectator)),
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)

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
@@ -7,11 +8,11 @@ public static class ManageServerMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (admin.IsValid == false)
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (AdminManager.PlayerHasPermissions(admin, "@css/generic") == false)
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
@@ -23,10 +24,9 @@ public static class ManageServerMenu
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_server_manage"] ?? "Server Manage");
List<ChatMenuOptionData> options = [];
// permissions
var hasMap = AdminManager.CommandIsOverriden("css_map") ? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_map")) : AdminManager.PlayerHasPermissions(admin, "@css/changemap");
var hasPlugins = AdminManager.CommandIsOverriden("css_pluginsmanager") ? AdminManager.PlayerHasPermissions(admin, AdminManager.GetPermissionOverrides("css_pluginsmanager")) : AdminManager.PlayerHasPermissions(admin, "@css/root");
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");

View File

@@ -0,0 +1,170 @@
using CounterStrikeSharp.API.Core;
namespace CS2_SimpleAdmin.Menus;
public class MenuBuilder(string title)
{
private readonly List<MenuOption> _options = [];
private MenuBuilder? _parentMenu;
private Action<CCSPlayerController>? _backAction;
private Action<CCSPlayerController>? _resetAction;
/// <summary>
/// Adds a menu option with an action.
/// </summary>
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null)
{
_options.Add(new MenuOption
{
Name = name,
Action = action,
Disabled = disabled,
Permission = permission
});
return this;
}
/// <summary>
/// Adds a menu option that opens a submenu.
/// </summary>
public MenuBuilder AddSubMenu(string name, Func<MenuBuilder> subMenuFactory, bool disabled = false, string? permission = null)
{
_options.Add(new MenuOption
{
Name = name,
Action = player =>
{
var subMenu = subMenuFactory();
subMenu.SetParent(this);
// Automatically add back button to submenu
subMenu.WithBackButton();
subMenu.OpenMenu(player);
},
Disabled = disabled,
Permission = permission
});
return this;
}
/// <summary>
/// Adds a menu option that opens a submenu (with player parameter in factory).
/// </summary>
public MenuBuilder AddSubMenu(string name, Func<CCSPlayerController, MenuBuilder> subMenuFactory, bool disabled = false, string? permission = null)
{
_options.Add(new MenuOption
{
Name = name,
Action = player =>
{
var subMenu = subMenuFactory(player);
subMenu.SetParent(this);
// Automatically add back button to submenu
subMenu.WithBackButton();
subMenu.OpenMenu(player);
},
Disabled = disabled,
Permission = permission
});
return this;
}
/// <summary>
/// Adds a back button to return to the previous menu.
/// </summary>
public MenuBuilder WithBackButton()
{
if (_parentMenu != null)
{
_backAction = player => _parentMenu.OpenMenu(player);
// Add back option at the end of menu
// AddOption(backButtonText, _backAction);
}
return this;
}
/// <summary>
/// Sets the parent menu (for navigation).
/// </summary>
private void SetParent(MenuBuilder parent)
{
_parentMenu = parent;
_backAction = player => _parentMenu.OpenMenu(player);
}
/// <summary>
/// Opens the menu for a player.
/// </summary>
/// <param name="player">The player to open the menu for.</param>
public void OpenMenu(CCSPlayerController player)
{
if (!player.IsValid) return;
// Use MenuManager dependency
var menu = Helper.CreateMenu(title, _backAction);
if (menu == null) return;
foreach (var option in _options)
{
// Check permissions if required
if (!string.IsNullOrEmpty(option.Permission))
{
var steamId = new CounterStrikeSharp.API.Modules.Entities.SteamID(player.SteamID);
if (!CounterStrikeSharp.API.Modules.Admin.AdminManager.PlayerHasPermissions(steamId, option.Permission))
{
continue; // Skip option if player doesn't have permission
}
}
menu.AddMenuOption(option.Name, (menuPlayer, menuOption) =>
{
option.Action?.Invoke(menuPlayer);
}, option.Disabled);
}
menu.Open(player);
}
/// <summary>
/// Clears all menu options.
/// </summary>
/// <returns>This MenuBuilder instance for chaining.</returns>
public MenuBuilder Clear()
{
_options.Clear();
return this;
}
/// <summary>
/// Sets a reset action for the menu.
/// </summary>
/// <param name="resetAction">The action to execute on reset.</param>
/// <returns>This MenuBuilder instance for chaining.</returns>
public MenuBuilder WithResetAction(Action<CCSPlayerController> resetAction)
{
_resetAction = resetAction;
return this;
}
/// <summary>
/// Sets a custom back action for the menu.
/// </summary>
/// <param name="backAction">The action to execute when going back (nullable).</param>
/// <returns>This MenuBuilder instance for chaining.</returns>
public MenuBuilder WithBackAction(Action<CCSPlayerController>? backAction)
{
_backAction = backAction;
return this;
}
}
/// <summary>
/// Represents an option within a menu.
/// </summary>
public class MenuOption
{
public string Name { get; set; } = string.Empty;
public Action<CCSPlayerController>? Action { get; set; }
public bool Disabled { get; set; }
public string? Permission { get; set; }
}

View File

@@ -0,0 +1,190 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
public class MenuManager
{
private static MenuManager? _instance;
public static MenuManager Instance => _instance ??= new MenuManager();
private readonly Dictionary<string, Func<CCSPlayerController, MenuBuilder>> _menuFactories = [];
private readonly Dictionary<string, MenuCategory> _menuCategories = [];
/// <summary>
/// Provides public access to menu categories (for API usage).
/// </summary>
/// <returns>Dictionary of menu categories keyed by category ID.</returns>
public Dictionary<string, MenuCategory> GetMenuCategories()
{
return _menuCategories;
}
/// <summary>
/// Registers a new menu category with specified permissions.
/// </summary>
/// <param name="categoryId">Unique identifier for the category.</param>
/// <param name="categoryName">Display name of the category.</param>
/// <param name="permission">Required permission to access this category (default: @css/generic).</param>
public void RegisterCategory(string categoryId, string categoryName, string permission = "@css/generic")
{
_menuCategories[categoryId] = new MenuCategory
{
Id = categoryId,
Name = categoryName,
Permission = permission,
MenuFactories = new Dictionary<string, Func<CCSPlayerController, MenuBuilder>>()
};
}
/// <summary>
/// Registers a menu within a category (API for other plugins).
/// </summary>
/// <param name="categoryId">The category to add this menu to.</param>
/// <param name="menuId">Unique identifier for the menu.</param>
/// <param name="menuName">Display name of the menu.</param>
/// <param name="menuFactory">Factory function that creates the menu for a player.</param>
/// <param name="permission">Required permission to access this menu (optional).</param>
public void RegisterMenu(string categoryId, string menuId, string menuName, Func<CCSPlayerController, MenuBuilder> menuFactory, string? permission = null)
{
if (!_menuCategories.ContainsKey(categoryId))
{
RegisterCategory(categoryId, categoryId); // Auto-create category if it doesn't exist
}
_menuCategories[categoryId].MenuFactories[menuId] = menuFactory;
_menuCategories[categoryId].MenuNames[menuId] = menuName;
if (permission != null)
{
_menuCategories[categoryId].MenuPermissions[menuId] = permission;
}
}
/// <summary>
/// Unregisters a menu from a category.
/// </summary>
/// <param name="categoryId">The category containing the menu.</param>
/// <param name="menuId">The menu to unregister.</param>
public void UnregisterMenu(string categoryId, string menuId)
{
if (!_menuCategories.TryGetValue(categoryId, out var category)) return;
category.MenuFactories.Remove(menuId);
_menuCategories[categoryId].MenuNames.Remove(menuId);
_menuCategories[categoryId].MenuPermissions.Remove(menuId);
}
/// <summary>
/// Creates the main admin menu for a player with accessible categories.
/// </summary>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the main menu.</returns>
public MenuBuilder CreateMainMenu(CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
var mainMenu = new MenuBuilder(localizer?["sa_title"] ?? "SimpleAdmin");
foreach (var category in _menuCategories.Values)
{
if (category.MenuFactories.Count <= 0) continue;
// Check category permissions
var steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, category.Permission))
continue;
// Pass player to CreateCategoryMenu
mainMenu.AddSubMenu(category.Name, () => CreateCategoryMenu(category, player),
permission: category.Permission);
}
return mainMenu;
}
/// <summary>
/// Creates a category submenu containing all registered menus in that category.
/// </summary>
/// <param name="category">The menu category to create.</param>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the category menu.</returns>
private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player)
{
var categoryMenu = new MenuBuilder(category.Name);
foreach (var kvp in category.MenuFactories)
{
var menuId = kvp.Key;
var menuFactory = kvp.Value;
var menuName = category.MenuNames.TryGetValue(menuId, out var name) ? name : menuId;
var permission = category.MenuPermissions.TryGetValue(menuId, out var perm) ? perm : null;
// Check permissions
if (!string.IsNullOrEmpty(permission))
{
var steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, permission))
continue;
}
// Call the actual factory with player parameter
categoryMenu.AddSubMenu(menuName, () => menuFactory(player), permission: permission);
}
return categoryMenu.WithBackButton();
}
/// <summary>
/// Opens the main admin menu for a player.
/// </summary>
/// <param name="player">The player to open the menu for.</param>
public void OpenMainMenu(CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
var steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, "@css/generic"))
{
player.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command"));
return;
}
CreateMainMenu(player).OpenMenu(player);
}
/// <summary>
/// Initializes default menu categories (Players, Server, Admin).
/// </summary>
public void InitializeDefaultCategories()
{
var localizer = CS2_SimpleAdmin._localizer;
RegisterCategory("players", localizer?["sa_menu_players_manage"] ?? "Manage Players", "@css/generic");
RegisterCategory("server", localizer?["sa_menu_server_manage"] ?? "Server Management", "@css/generic");
// RegisterCategory("fun", localizer?["sa_menu_fun_commands"] ?? "Fun Commands", "@css/generic");
RegisterCategory("admin", localizer?["sa_menu_admins_manage"] ?? "Admin Management", "@css/root");
}
/// <summary>
/// Public method for creating category menus (for API usage).
/// </summary>
/// <param name="category">The menu category to create.</param>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the category menu.</returns>
public MenuBuilder CreateCategoryMenuPublic(MenuCategory category, CCSPlayerController player)
{
return CreateCategoryMenu(category, player);
}
}
/// <summary>
/// Represents a menu category containing multiple menus.
/// </summary>
public class MenuCategory
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Permission { get; set; } = "@css/generic";
public Dictionary<string, Func<CCSPlayerController, MenuBuilder>> MenuFactories { get; set; } = [];
public Dictionary<string, string> MenuNames { get; set; } = [];
public Dictionary<string, string> MenuPermissions { get; set; } = [];
}

View File

@@ -8,7 +8,7 @@ public static class PlayersMenu
{
public static void OpenRealPlayersMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
{
OpenMenu(admin, menuName, onSelectAction, p => p.IsBot == false);
OpenMenu(admin, menuName, onSelectAction, p => !p.IsBot);
}
public static void OpenAdminPlayersMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController?, bool>? enableFilter = null)
@@ -18,12 +18,12 @@ public static class PlayersMenu
public static void OpenAliveMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
{
OpenMenu(admin, menuName, onSelectAction, p => p.PawnIsAlive);
OpenMenu(admin, menuName, onSelectAction, p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE);
}
public static void OpenDeadMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController?, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
{
OpenMenu(admin, menuName, onSelectAction, p => p.PawnIsAlive == false);
OpenMenu(admin, menuName, onSelectAction, p => p.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE);
}
public static void OpenMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
@@ -37,7 +37,7 @@ public static class PlayersMenu
var playerName = player != null && player.PlayerName.Length > 26 ? player.PlayerName[..26] : player?.PlayerName;
var optionName = HttpUtility.HtmlEncode(playerName);
if (player != null && enableFilter != null && enableFilter(player) == false)
if (player != null && enableFilter != null && !enableFilter(player))
continue;
var enabled = admin.CanTarget(player);
@@ -47,7 +47,7 @@ public static class PlayersMenu
{
if (player != null) onSelectAction.Invoke(admin, player);
},
enabled == false);
!enabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);

View File

@@ -46,7 +46,7 @@ public static class ReasonMenu
{
menu?.AddMenuOption(reason, (_, _) => onSelectAction(admin, player, reason));
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
}

View File

@@ -0,0 +1,39 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
namespace CS2_SimpleAdmin.Models;
public enum BanStatus
{
[Description("ACTIVE")] ACTIVE,
[Description("UNBANNED")] UNBANNED,
[Description("EXPIRED")] EXPIRED,
[Description("")] UNKNOWN
}
public record BanRecord
{
[Column("id")]
public int Id { get; init; }
[Column("player_name")]
public string? PlayerName { get; set; }
[Column("player_steamid")]
public ulong? PlayerSteamId { get; set; }
[Column("player_ip")]
public string? PlayerIp { get; set; }
[Column("status")]
public required string Status { get; init; }
[NotMapped]
public BanStatus StatusEnum => Status.ToUpper() switch
{
"ACTIVE" => BanStatus.ACTIVE,
"UNBANNED" => BanStatus.UNBANNED,
"EXPIRED" => BanStatus.EXPIRED,
_ => BanStatus.UNKNOWN
};
}

View File

@@ -0,0 +1,3 @@
namespace CS2_SimpleAdmin.Models;
public readonly record struct IpRecord(uint Ip, DateTime UsedAt, string PlayerName);

View File

@@ -0,0 +1,12 @@
namespace CS2_SimpleAdmin.Models;
public record PlayerStats(int Score, int Kills, int Deaths, int MVPs);
public record PlayerDto(
int UserId,
string Name,
string SteamId,
string IpAddress,
uint Ping,
bool IsAdmin,
PlayerStats Stats
);

View File

@@ -1 +1 @@
1.6.9a
1.7.8-beta-1

View File

@@ -7,7 +7,9 @@ using MenuManager;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Managers;
using Timer = CounterStrikeSharp.API.Modules.Timers.Timer;
namespace CS2_SimpleAdmin;
@@ -15,12 +17,13 @@ public partial class CS2_SimpleAdmin
{
// Config
public CS2_SimpleAdminConfig Config { get; set; } = new();
// HttpClient
internal static readonly HttpClient HttpClient = new();
// Paths
internal static readonly string ConfigDirectory = Path.Combine(Application.RootDirectory, "configs/plugins/CS2-SimpleAdmin");
internal static readonly string ConfigDirectory =
Path.Combine(Application.RootDirectory, "configs/plugins/CS2-SimpleAdmin");
// Localization
public static IStringLocalizer? _localizer;
@@ -32,16 +35,17 @@ public partial class CS2_SimpleAdmin
// Command and Server Settings
public static readonly bool UnlockedCommands = CoreConfig.UnlockConCommands;
internal static string IpAddress = string.Empty;
public static bool ServerLoaded;
public static int? ServerId = null;
internal static bool ServerLoaded;
internal static int? ServerId = null;
internal static readonly HashSet<ulong> AdminDisabledJoinComms = [];
// Player Management
private static readonly HashSet<int> GodPlayers = [];
internal static readonly HashSet<int> SilentPlayers = [];
internal static readonly ConcurrentBag<string?> BannedPlayers = [];
internal static readonly Dictionary<ulong, string> RenamedPlayers = [];
internal static readonly ConcurrentDictionary<int, PlayerInfo> PlayersInfo = [];
internal static readonly ConcurrentDictionary<ulong, PlayerInfo> PlayersInfo = [];
internal static readonly List<CCSPlayerController> CachedPlayers = [];
internal static readonly List<CCSPlayerController> BotPlayers = [];
private static readonly List<DisconnectedPlayer> DisconnectedPlayers = [];
// Discord Integration
@@ -49,24 +53,35 @@ public partial class CS2_SimpleAdmin
// Database Settings
internal string DbConnectionString = string.Empty;
internal static Database.Database? Database;
// internal static Database.Database? Database;
internal static IDatabaseProvider? DatabaseProvider;
// Logger
internal static ILogger? _logger;
// Memory Function (Game-related)
private static MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>? _cBasePlayerControllerSetPawnFunc;
private static MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>?
_cBasePlayerControllerSetPawnFunc;
// Menu API and Capabilities
internal static IMenuApi? MenuApi;
private static readonly PluginCapability<IMenuApi> MenuCapability = new("menu:nfcore");
// Shared API
private Api.CS2_SimpleAdminApi? SimpleAdminApi { get; set; }
internal static Api.CS2_SimpleAdminApi? SimpleAdminApi { get; private set; }
// Managers
internal PermissionManager PermissionManager = new(Database);
internal BanManager BanManager = new(Database);
internal MuteManager MuteManager = new(Database);
internal WarnManager WarnManager = new(Database);
internal PermissionManager PermissionManager = new(DatabaseProvider);
internal BanManager BanManager = new(DatabaseProvider);
internal MuteManager MuteManager = new(DatabaseProvider);
internal WarnManager WarnManager = new(DatabaseProvider);
internal CacheManager? CacheManager = new();
private static readonly PlayerManager PlayerManager = new();
// Timers
internal Timer? PlayersTimer = null;
// Funny list
private readonly List<string> _requiredPlugins = ["MenuManagerCore", "PlayerSettings"];
private readonly List<string> _requiredShared = ["MenuManagerApi", "PlayerSettingsApi", "AnyBaseLib", "CS2-SimpleAdminApi"];
}

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nعقوبات اللاعبين لـ {lightred}{0}{default},\nعدد الحظر: {lightred}{1}{default}, عدد الصمت: {lightred}{2}{default}, عدد الكتم: {lightred}{3}{default}, عدد السكوت: {lightred}{4}{default}, عدد التحذيرات: {lightred}{5}{default}\nالعقوبات النشطة:\n{6}\nالتحذيرات النشطة:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}عقوبات اللاعبين لـ {lightred}{0}{grey}, حظر: {lightred}{1}{grey}, صمت: {lightred}{2}{grey}, كتم: {lightred}{3}{grey}, سكوت: {lightred}{4}{grey}, تحذيرات: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}الحسابات المرتبطة باللاعب {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "تم حظرك لمدة {lightred}{0}{default} لمدة {lightred}{1}{default} دقيقة من قبل {lightred}{2}{default}!",
"sa_player_ban_message_perm": "تم حظرك بشكل دائم لمدة {lightred}{0}{default} من قبل {lightred}{1}{default}!",
"sa_player_kick_message": "تم طردك لمدة {lightred}{0}{default} من قبل {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} غيّر الجاذبية لـ {lightred}{1}{default}!",
"sa_admin_money_message": "{lightred}{0}{default} غيّر المال لـ {lightred}{1}{default}!",
"sa_admin_god_message": "{lightred}{0}{default} غيّر وضع الله لـ {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} قام بتغيير الحجم لـ {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} قتل {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} صفع {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} غيّر الخريطة إلى {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} بدأ التصويت لـ {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}نتائج التصويت لـ {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}لقد قمت بكتم {default}جميع اللاعبين أثناء حديثك",
"sa_admin_voice_unmute_all": "{Lime}لقد ألغيت كتم {default}جميع اللاعبين",
"sa_admin_voice_listen_all": "{Default}أنت الآن تسمع {lime}جميع {default}اللاعبين",
"sa_admin_voice_unlisten_all": "{Default}أنت الآن لا تسمع {lime}جميع {default}اللاعبين",
"sa_adminsay_prefix": "{RED}الإداري: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(إداري) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(لاعب) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** أصدر الأمر `{1}` على الخادم `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nSpielerstrafe für {lightred}{0}{default},\nAnzahl der Sperren: {lightred}{1}{default}, Anzahl der Mundtot: {lightred}{2}{default}, Anzahl der Stummschaltungen: {lightred}{3}{default}, Anzahl der Stille: {lightred}{4}{default}, Anzahl der Warnungen: {lightred}{5}{default}\nAktive Strafen:\n{6}\nAktive Warnungen:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Spielerstrafe für {lightred}{0}{grey}, Sperren: {lightred}{1}{grey}, Mundtot: {lightred}{2}{grey}, Stummschaltungen: {lightred}{3}{grey}, Stille: {lightred}{4}{grey}, Warnungen: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Verknüpfte Konten des Spielers {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "Du wurdest wegen {lightred}{0}{default} für {lightred}{1}{default} Minuten von {lightred}{2}{default} gebannt!",
"sa_player_ban_message_perm": "Du wurdest wegen {lightred}{0}{default} von {lightred}{1}{default} permanent gebannt!",
"sa_player_kick_message": "Du wurdest wegen {lightred}{0}{default} von {lightred}{1}{default} gekickt!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} hat die Schwerkraft von {lightred}{1}{default} geändert!",
"sa_admin_money_message": "{lightred}{0}{default} hat das Geld von {lightred}{1}{default} geändert!",
"sa_admin_god_message": "{lightred}{0}{default} hat den Gottmodus von {lightred}{1}{default} geändert!",
"sa_admin_resize_message": "{lightred}{0}{default} hat die Größe für {lightred}{1}{default} geändert!",
"sa_admin_slay_message": "{lightred}{0}{default} hat {lightred}{1}{default} getötet!",
"sa_admin_slap_message": "{lightred}{0}{default} hat {lightred}{1}{default} geschlagen!",
"sa_admin_changemap_message": "{lightred}{0}{default} hat die Karte zu {lightred}{1}{default} geändert!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} hat eine Abstimmung für {lightred}{1}{default} gestartet",
"sa_admin_vote_message_results": "{lime}ABSTIMMUNGSERGEBNISSE FÜR {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}Du hast {default}alle Spieler während deiner Ansprache stummgeschaltet",
"sa_admin_voice_unmute_all": "{Lime}Du hast {default}alle Spieler wieder aktiviert",
"sa_admin_voice_listen_all": "{Default}Du hörst jetzt {lime}alle {default}Spieler",
"sa_admin_voice_unlisten_all": "{Default}Du hörst jetzt {lime}nicht mehr {default}alle Spieler",
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(SPIELER) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** hat den Befehl `{1}` auf dem Server `HOSTNAME` ausgeführt",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nPlayer penalties for {lightred}{0}{default},\nNumber of bans: {lightred}{1}{default}, Number of gags: {lightred}{2}{default}, Number of mutes: {lightred}{3}{default}, Number of silences: {lightred}{4}{default}, Number of warnings: {lightred}{5}{default}\nActive penalties:\n{6}\nActive warnings:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Player penalties for {lightred}{0}{grey}, Bans: {lightred}{1}{grey}, Gags: {lightred}{2}{grey}, Mutes: {lightred}{3}{grey}, Silences: {lightred}{4}{grey}, Warns: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Associated accounts of player {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "You have been banned for {lightred}{0}{default} for {lightred}{1}{default} minutes by {lightred}{2}{default}!",
"sa_player_ban_message_perm": "You have been banned permanently for {lightred}{0}{default} by {lightred}{1}{default}!",
"sa_player_kick_message": "You have been kicked for {lightred}{0}{default} by {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} changed gravity for {lightred}{1}{default}!",
"sa_admin_money_message": "{lightred}{0}{default} changed money for {lightred}{1}{default}!",
"sa_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} changed size for {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} slayed {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} slapped {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} changed map to {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} started voting for {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}VOTING RESULTS FOR {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}You muted {default}all players during your speech",
"sa_admin_voice_unmute_all": "{Lime}You unmuted {default}all players",
"sa_admin_voice_listen_all": "{Default}You can now hear {lime}all {default}players",
"sa_admin_voice_unlisten_all": "{Default}You no longer hear {lime}all {default}players",
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(PLAYER) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** issued command `{1}` on server `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nPenalizaciones del jugador para {lightred}{0}{default},\nNúmero de prohibiciones: {lightred}{1}{default}, Número de boqueos: {lightred}{2}{default}, Número de silenciamientos: {lightred}{3}{default}, Número de silencios: {lightred}{4}{default}, Número de advertencias: {lightred}{5}{default}\nPenalizaciones activas:\n{6}\nAdvertencias activas:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Penalizaciones del jugador para {lightred}{0}{grey}, Prohibiciones: {lightred}{1}{grey}, Boqueos: {lightred}{2}{grey}, Silenciamientos: {lightred}{3}{grey}, Silencios: {lightred}{4}{grey}, Advertencias: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Cuentas asociadas del jugador {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "Has sido baneado por {lightred}{0}{default} durante {lightred}{1}{default} minutos por {lightred}{2}{default}!",
"sa_player_ban_message_perm": "Has sido baneado permanentemente por {lightred}{0}{default} por {lightred}{1}{default}!",
"sa_player_kick_message": "Has sido expulsado por {lightred}{0}{default} durante {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} cambió la gravedad de {lightred}{1}{default}!",
"sa_admin_money_message": "{lightred}{0}{default} cambió el dinero de {lightred}{1}{default}!",
"sa_admin_god_message": "{lightred}{0}{default} cambió el modo dios de {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} cambió el tamaño de {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} mató a {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} abofeteó a {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} cambió el mapa a {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} inició una votación para {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}RESULTADOS DE LA VOTACIÓN PARA {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}Has silenciado a {default}todos los jugadores durante tu discurso",
"sa_admin_voice_unmute_all": "{Lime}Has reactivado el sonido de {default}todos los jugadores",
"sa_admin_voice_listen_all": "{Default}Ahora puedes escuchar a {lime}todos {default}los jugadores",
"sa_admin_voice_unlisten_all": "{Default}Ya no escuchas a {lime}todos {default}los jugadores",
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(JUGADOR) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** ejecutó el comando `{1}` en el servidor `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nتنبیهات بازیکن برای {lightred}{0}{default},\nتعداد مسدودیت‌ها: {lightred}{1}{default}, تعداد سکوت‌ها: {lightred}{2}{default}, تعداد بی‌صدا کردن‌ها: {lightred}{3}{default}, تعداد سکوت‌ها: {lightred}{4}{default}, تعداد هشدارها: {lightred}{5}{default}\nتنبیهات فعال:\n{6}\nهشدارهای فعال:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}تنبیهات بازیکن برای {lightred}{0}{grey}, مسدودیت‌ها: {lightred}{1}{grey}, سکوت‌ها: {lightred}{2}{grey}, بی‌صدا کردن‌ها: {lightred}{3}{grey}, سکوت‌ها: {lightred}{4}{grey}, هشدارها: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}حساب‌های مرتبط با بازیکن {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "شما توسط {lightred}{2}{default} برای {lightred}{1}{default} دقیقه به دلیل {lightred}{0}{default} مسدود شده‌اید!",
"sa_player_ban_message_perm": "شما توسط {lightred}{1}{default} به دلیل {lightred}{0}{default} برای همیشه مسدود شده‌اید!",
"sa_player_kick_message": "شما توسط {lightred}{1}{default} به دلیل {lightred}{0}{default} اخراج شده‌اید!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} جاذبه {lightred}{1}{default} را تغییر داد!",
"sa_admin_money_message": "{lightred}{0}{default} پول {lightred}{1}{default} را تغییر داد!",
"sa_admin_god_message": "{lightred}{0}{default} حالت خدا را برای {lightred}{1}{default} تغییر داد!",
"sa_admin_resize_message": "{lightred}{0}{default} اندازه {lightred}{1}{default} را تغییر داد!",
"sa_admin_slay_message": "{lightred}{0}{default} {lightred}{1}{default} را کشت!",
"sa_admin_slap_message": "{lightred}{0}{default} به {lightred}{1}{default} سیلی زد!",
"sa_admin_changemap_message": "{lightred}{0}{default} نقشه را به {lightred}{1}{default} تغییر داد!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} رأی‌گیری برای {lightred}{1}{default} را شروع کرد",
"sa_admin_vote_message_results": "{lime}نتایج رأی‌گیری برای {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}شما {default}همه بازیکنان را در طول سخنرانی بی‌صدا کردید",
"sa_admin_voice_unmute_all": "{Lime}شما {default}همه بازیکنان را از حالت بی‌صدا خارج کردید",
"sa_admin_voice_listen_all": "{Default}اکنون می‌توانید {lime}همه {default}بازیکنان را بشنوید",
"sa_admin_voice_unlisten_all": "{Default}شما دیگر {lime}همه {default}بازیکنان را نمی‌شنوید",
"sa_adminsay_prefix": "{RED}ادمین: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ادمین) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(بازیکن) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** فرمان `{1}` را در سرور `HOSTNAME` اجرا کرد",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nPénalités du joueur pour {lightred}{0}{default},\nNombre de bannissements: {lightred}{1}{default}, Nombre de gag: {lightred}{2}{default}, Nombre de mutes: {lightred}{3}{default}, Nombre de silences: {lightred}{4}{default}, Nombre davertissements: {lightred}{5}{default}\nPénalités actives:\n{6}\nAvertissements actifs:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Pénalités du joueur pour {lightred}{0}{grey}, Bannissements: {lightred}{1}{grey}, Gags: {lightred}{2}{grey}, Mutes: {lightred}{3}{grey}, Silences: {lightred}{4}{grey}, Avertissements: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Comptes associés du joueur {lightred}{0}{grey} : {1}",
"sa_player_ban_message_time": "Vous avez été banni pour {lightred}{0}{default} pendant {lightred}{1}{default} minutes par {lightred}{2}{default}!",
"sa_player_ban_message_perm": "Vous avez été banni définitivement pour {lightred}{0}{default} par {lightred}{1}{default}!",
"sa_player_kick_message": "Vous avez été expulsé pour {lightred}{0}{default} par {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} a modifié la gravité de {lightred}{1}{default}!",
"sa_admin_money_message": "{lightred}{0}{default} a modifié l'argent de {lightred}{1}{default}!",
"sa_admin_god_message": "{lightred}{0}{default} a modifié le mode dieu de {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} a changé la taille de {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} a tué {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} a giflé {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} a changé la carte pour {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} a lancé un vote pour {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}RÉSULTATS DU VOTE POUR {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}Vous avez mis en sourdine {default}tous les joueurs pendant votre discours",
"sa_admin_voice_unmute_all": "{Lime}Vous avez réactivé le son de {default}tous les joueurs",
"sa_admin_voice_listen_all": "{Default}Vous entendez maintenant {lime}tous {default}les joueurs",
"sa_admin_voice_unlisten_all": "{Default}Vous n'entendez plus {lime}tous {default}les joueurs",
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(JOUEUR) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** a exécuté la commande `{1}` sur le serveur `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nSpēlētāja sods priekš {lightred}{0}{default},\nAizliegumu skaits: {lightred}{1}{default}, Klusumu skaits: {lightred}{2}{default}, Izslēgšanas skaits: {lightred}{3}{default}, Klusēšanas skaits: {lightred}{4}{default}, Brīdinājumu skaits: {lightred}{5}{default}\nAktīvie sodi:\n{6}\nAktīvie brīdinājumi:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Spēlētāja sods priekš {lightred}{0}{grey}, Aizliegumi: {lightred}{1}{grey}, Klusumi: {lightred}{2}{grey}, Izslēgšana: {lightred}{3}{grey}, Klusēšana: {lightred}{4}{grey}, Brīdinājumi: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Spēlētāja {lightred}{0}{grey} saistītie konti: {1}",
"sa_player_ban_message_time": "Tu esi nobanots uz {lightred}{0}{default} uz {lightred}{1}{default} minūtēm, iemesls: {lightred}{2}{default}!",
"sa_player_ban_message_perm": "Tevis bans ir uz mūžu, iemesls: {lightred}{0}{default}, Admins: {lightred}{1}{default}!",
"sa_player_kick_message": "Tu esi izmests, iemesls: {lightred}{0}{default}, Admins: {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} gravitāciju!",
"sa_admin_money_message": "{lightred}{0}{default} mainīja {lightred}{1}{default} naudu!",
"sa_admin_god_message": "{lightred}{0}{default} mainīja dieva režīmu priekš {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} mainīja izmēru {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} nogalināja {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} iedeva {lightred}{1}{default} pa seju!",
"sa_admin_changemap_message": "{lightred}{0}{default} mainīja karti uz {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} uzsāka balsošanu par {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}BALSOŠANAS REZULTĀTI PAR {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}Tu esi apklusinājis {default}visus spēlētājus runas laikā",
"sa_admin_voice_unmute_all": "{Lime}Tu atvienoji {default}visus spēlētājus",
"sa_admin_voice_listen_all": "{Default}Tu tagad dzirdi {lime}visus {default}spēlētājus",
"sa_admin_voice_unlisten_all": "{Default}Tu vairs nedzirdi {lime}visus {default}spēlētājus",
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(SPĒLĒTĀJS) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** izpildīja komandu `{1}` serverī `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nBlokady gracza {lightred}{0}{default},\nIlość banów: {lightred}{1}{default}, Ilość zakneblowań: {lightred}{2}{default}, Ilość wyciszeń: {lightred}{3}{default}, Ilość uciszeń: {lightred}{4}{default}Ilość ostrzeżeń: {lightred}{5}{default}\nAktywne blokady:\n{6}\nAktywne ostrzeżenia:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Blokady gracza {lightred}{0}{grey} - bany: {lightred}{1}{grey}, zakneblowania: {lightred}{2}{grey}, wyciszenia: {lightred}{3}{grey}, uciszenia: {lightred}{4}{grey}, ostrzeżenia: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Powiązane konta gracza {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "Zostałeś zbanowany za {lightred}{0}{default} na {lightred}{1}{default} minut przez {lightred}{2}{default}!",
"sa_player_ban_message_perm": "Zostałeś zbanowany na zawsze za {lightred}{0}{default} przez {lightred}{1}{default}!",
"sa_player_kick_message": "Zostałeś wyrzucony za {lightred}{0}{default} przez {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} zmienił grawitacje dla {lightred}{1}{default}!",
"sa_admin_money_message": "{lightred}{0}{default} zmienił pieniądze dla {lightred}{1}{default}!",
"sa_admin_god_message": "{lightred}{0}{default} zmienił tryb Boga dla {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} zmienił rozmiar dla {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} zgładził {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} uderzył {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} zmienił mapę na {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} rozpoczął głosowanie na {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}WYNIKI GŁOSOWANIA {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}[{1}]",
"sa_admin_voice_mute_all": "{Lightred}Wyciszyłeś {default}wszystkich graczy na czas przemówienia",
"sa_admin_voice_unmute_all": "{Lime}Odciszyłeś {default}wszystkich graczy",
"sa_admin_voice_listen_all": "{Default}Słyszysz teraz {lime}wszystkich {default}graczy",
"sa_admin_voice_unlisten_all": "{Default}Nie słyszysz teraz {lime}wszystkich {default}graczy",
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(GRACZ) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** użył komendy `{1}` na serwerze `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nPenalidades do jogador para {lightred}{0}{default},\nNúmero de banimentos: {lightred}{1}{default}, Número de gags: {lightred}{2}{default}, Número de mutes: {lightred}{3}{default}, Número de silêncios: {lightred}{4}{default}, Número de avisos: {lightred}{5}{default}\nPenalidades ativas:\n{6}\nAvisos ativos:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Penalidades do jogador para {lightred}{0}{grey}, Banimentos: {lightred}{1}{grey}, Gags: {lightred}{2}{grey}, Mutes: {lightred}{3}{grey}, Silêncios: {lightred}{4}{grey}, Avisos: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Contas associadas do jogador {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "Você foi banido por {lightred}{0}{default} por {lightred}{1}{default} minutos por {lightred}{2}{default}!",
"sa_player_ban_message_perm": "Você foi banido permanentemente por {lightred}{0}{default} por {lightred}{1}{default}!",
"sa_player_kick_message": "Você foi expulso por {lightred}{0}{default} por {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} mudou a gravidade de {lightred}{1}{default}!",
"sa_admin_money_message": "{lightred}{0}{default} mudou a quantidade de dinheiro de {lightred}{1}{default}!",
"sa_admin_god_message": "{lightred}{0}{default} mudou o modo Deus de {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} alterou o tamanho de {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} matou {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} deu um tapa em {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} mudou o mapa para {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} iniciou uma votação para {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}RESULTADOS DA VOTAÇÃO PARA {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}Você silenciou {default}todos os jogadores durante o seu discurso",
"sa_admin_voice_unmute_all": "{Lime}Você reativou o som de {default}todos os jogadores",
"sa_admin_voice_listen_all": "{Default}Agora você pode ouvir {lime}todos {default}os jogadores",
"sa_admin_voice_unlisten_all": "{Default}Você não ouve mais {lime}todos {default}os jogadores",
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(JOGADOR) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** executou o comando `{1}` no servidor `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nPenalidades do jogador para {lightred}{0}{default},\nNúmero de banimentos: {lightred}{1}{default}, Número de gags: {lightred}{2}{default}, Número de mutes: {lightred}{3}{default}, Número de silêncios: {lightred}{4}{default}, Número de avisos: {lightred}{5}{default}\nPenalidades ativas:\n{6}\nAvisos ativos:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Penalidades do jogador para {lightred}{0}{grey}, Banimentos: {lightred}{1}{grey}, Gags: {lightred}{2}{grey}, Mutes: {lightred}{3}{grey}, Silêncios: {lightred}{4}{grey}, Avisos: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Contas associadas do jogador {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "Foste banido pelo administrador {lightred}{0}{default} durante {lightred}{1}{default} minutos. Motivo: {lightred}{2}{default}!",
"sa_player_ban_message_perm": "Foste banido permanentemente pelo administrador {lightred}{0}{default}. Motivo: {lightred}{1}{default}!",
"sa_player_kick_message": "Foste expulso pelo administrador {lightred}{0}{default}. Motivo: {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} mudou a gravidade de {lightred}{1}{default}!",
"sa_admin_money_message": "{lightred}{0}{default} mudou a quantidade de dinheiro de {lightred}{1}{default}!",
"sa_admin_god_message": "{lightred}{0}{default} mudou o modo Deus de {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} alterou o tamanho de {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} matou {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} deu um tapa em {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} mudou o mapa para {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} iniciou uma votação para {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}RESULTADOS DA VOTAÇÃO PARA {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}Silenciaste {default}todos os jogadores durante o teu discurso",
"sa_admin_voice_unmute_all": "{Lime}Ativaste novamente o som de {default}todos os jogadores",
"sa_admin_voice_listen_all": "{Default}Agora consegues ouvir {lime}todos {default}os jogadores",
"sa_admin_voice_unlisten_all": "{Default}Já não ouves {lime}todos {default}os jogadores",
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(JOGADOR) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** executou o comando `{1}` no servidor `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nШтрафы игрока для {lightred}{0}{default},\nКоличество банов: {lightred}{1}{default}, Количество гэгов: {lightred}{2}{default}, Количество мутов: {lightred}{3}{default}, Количество тишин: {lightred}{4}{default}, Количество предупреждений: {lightred}{5}{default}\nАктивные штрафы:\n{6}\nАктивные предупреждения:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Штрафы игрока для {lightred}{0}{grey}, Баны: {lightred}{1}{grey}, Гэги: {lightred}{2}{grey}, Муты: {lightred}{3}{grey}, Тишины: {lightred}{4}{grey}, Предупреждения: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}Связанные аккаунты игрока {lightred}{0}{grey}: {1}",
"sa_player_ban_message_time": "Вы были забанены по причине {lightred}{0}{default} на {lightred}{1}{default} минут(ы) администратором {lightred}{2}{default}!",
"sa_player_ban_message_perm": "Вас забанили навсегда по причине {lightred}{0}{default} администратором {lightred}{1}{default}!",
"sa_player_kick_message": "Вы были выгнаны {lightred}{0}{default} администратором {lightred}{1}{default}!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} изменил гравитацию для {lightred}{1}{default}!",
"sa_admin_money_message": "{lightred}{0}{default} изменил количество денег у {lightred}{1}{default}!",
"sa_admin_god_message": "{lightred}{0}{default} изменил режим бога для {lightred}{1}{default}!",
"sa_admin_resize_message": "{lightred}{0}{default} изменил размер {lightred}{1}{default}!",
"sa_admin_slay_message": "{lightred}{0}{default} убил {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} дал пощечину {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} изменил карту на {lightred}{1}{default}!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} начал голосование за {lightred}{1}{default}",
"sa_admin_vote_message_results": "{lime}РЕЗУЛЬТАТЫ ГОЛОСОВАНИЯ ЗА {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}Вы заглушили {default}всех игроков во время своей речи",
"sa_admin_voice_unmute_all": "{Lime}Вы включили звук {default}всем игрокам",
"sa_admin_voice_listen_all": "{Default}Теперь вы слышите {lime}всех {default}игроков",
"sa_admin_voice_unlisten_all": "{Default}Вы больше не слышите {lime}всех {default}игроков",
"sa_adminsay_prefix": "{RED}АДМИН: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(ВИП ЧАТ) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(АДМИН) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(ИГРОК) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** выполнил команду `{1}` на сервере `HOSTNAME`",

View File

@@ -78,6 +78,7 @@
"sa_player_penalty_info": "===========================\nOyuncunun cezaları {lightred}{0}{default} için,\nBan sayısı: {lightred}{1}{default}, Gag sayısı: {lightred}{2}{default}, Mute sayısı: {lightred}{3}{default}, Sessizlik sayısı: {lightred}{4}{default}, Uyarı sayısı: {lightred}{5}{default}\nAktif cezalar:\n{6}\nAktif uyarılar:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}Oyuncunun cezaları {lightred}{0}{grey}, Banlar: {lightred}{1}{grey}, Gaglar: {lightred}{2}{grey}, Mute'lar: {lightred}{3}{grey}, Sessizlikler: {lightred}{4}{grey}, Uyarılar: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}{lightred}{0}{grey} oyuncusunun bağlı hesapları: {1}",
"sa_player_ban_message_time": "Senaryo nedeniyle {lightred}{0}{default} dakika boyunca {lightred}{1}{default} tarafından yasaklandınız!",
"sa_player_ban_message_perm": "Senaryo nedeniyle kalıcı olarak {lightred}{0}{default} tarafından yasaklandınız!",
"sa_player_kick_message": "Senaryo nedeniyle {lightred}{0}{default} tarafından atıldınız!",
@@ -107,6 +108,7 @@
"sa_admin_gravity_message": "{lightred}{0}{default} {lightred}{1}{default}'in yer çekimini değiştirdi!",
"sa_admin_money_message": "{lightred}{0}{default} {lightred}{1}{default}'in parasını değiştirdi!",
"sa_admin_god_message": "{lightred}{0}{default} {lightred}{1}{default}'in tanrı modunu değiştirdi!",
"sa_admin_resize_message": "{lightred}{0}{default} boyutu {lightred}{1}{default} için değiştirdi!",
"sa_admin_slay_message": "{lightred}{0}{default} {lightred}{1}{default}'i öldürdü!",
"sa_admin_slap_message": "{lightred}{0}{default} {lightred}{1}{default}'e tokat attı!",
"sa_admin_changemap_message": "{lightred}{0}{default} haritayı {lightred}{1}{default} olarak değiştirdi!",
@@ -124,8 +126,11 @@
"sa_admin_vote_message": "{lightred}{0}{default} {lightred}{1}{default} için oy kullanmaya başladı",
"sa_admin_vote_message_results": "{lime}OY SONUÇLARI {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}Konuşman sırasında {default}tüm oyuncuları susturdun",
"sa_admin_voice_unmute_all": "{Lime}Tüm oyuncuların sesini {default}açtın",
"sa_admin_voice_listen_all": "{Default}Artık {lime}tüm {default}oyuncuları duyabiliyorsun",
"sa_admin_voice_unlisten_all": "{Default}Artık {lime}tüm {default}oyuncuları duymuyorsun",
"sa_adminsay_prefix": "{RED}Yönetici: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(Yönetici) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(Oyuncu) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** `{1}` komutunu `HOSTNAME` sunucusunda gerçekleştirdi",

View File

@@ -1,11 +1,11 @@
{
"sa_title": "SimpleAdmin",
"sa_prefix": "{lightred}[SA] {default}",
"sa_unknown": "未知",
"sa_no_permission": "您没有权限使用此命令。",
"sa_ban_max_duration_exceeded": "禁令持续时间不能超过{lightred}{0}{default}分钟。",
"sa_ban_perm_restricted": "您没有永久封禁的权限。",
"sa_ban_max_duration_exceeded": "禁止时长不能超过 {lightred}{0}{default} 分钟。",
"sa_ban_perm_restricted": "您没有永久禁止权限。",
"sa_admin_add": "添加管理员",
"sa_admin_remove": "移除管理员",
@@ -15,7 +15,7 @@
"sa_noclip": "穿墙模式",
"sa_respawn": "重生",
"sa_give_weapon": "给予武器",
"sa_strip_weapons": "剥夺武器",
"sa_strip_weapons": "移除武器",
"sa_freeze": "冻结",
"sa_set_hp": "设置生命值",
"sa_set_speed": "设置速度",
@@ -23,22 +23,22 @@
"sa_set_money": "设置金钱",
"sa_changemap": "更换地图",
"sa_restart_game": "重游戏",
"sa_restart_game": "重新开始游戏",
"sa_team_ct": "CT",
"sa_team_t": "T",
"sa_team_swap": "交换",
"sa_team_spec": "观",
"sa_team_spec": "观察者",
"sa_slap": "掌掴",
"sa_slay": "杀",
"sa_slay": "杀",
"sa_kick": "踢出",
"sa_ban": "禁",
"sa_ban": "禁",
"sa_gag": "禁言",
"sa_mute": "静音",
"sa_silence": "沉默",
"sa_warn": "警告",
"sa_team_force": "强制队",
"sa_team_force": "强制队",
"sa_menu_custom_commands": "自定义命令",
"sa_menu_server_manage": "服务器管理",
@@ -47,88 +47,91 @@
"sa_menu_players_manage": "玩家管理",
"sa_menu_disconnected_title": "最近的玩家",
"sa_menu_disconnected_action_title": "选择操作",
"sa_menu_pluginsmanager_title": "管理插件",
"sa_menu_pluginsmanager_title": "插件管理",
"sa_player": "玩家",
"sa_console": "控制台",
"sa_steamid": "SteamID",
"sa_duration": "持续时间",
"sa_duration": "时长",
"sa_reason": "原因",
"sa_admin": "管理员",
"sa_permanent": "永久",
"sa_discord_penalty_ban": "封禁已记录",
"sa_discord_penalty_mute": "禁言已记录",
"sa_discord_penalty_gag": "禁言记录",
"sa_discord_penalty_silence": "禁声已记录",
"sa_discord_penalty_warn": "警告已注册",
"sa_discord_penalty_unknown": "未知记录",
"sa_discord_penalty_ban": "禁止记录",
"sa_discord_penalty_mute": "静音记录",
"sa_discord_penalty_gag": "禁言记录",
"sa_discord_penalty_silence": "沉默记录",
"sa_discord_penalty_warn": "警告记录",
"sa_discord_penalty_unknown": "未知记录",
"sa_player_penalty_chat_active": "{lightred}您的聊天已被封锁到: {grey}{0}",
"sa_player_penalty_info_active_mute": "➔ 静音 [{lightred}❌{default}] - 到期 [{lightred}{0}{default}]",
"sa_player_penalty_info_active_gag": "➔ 禁言 [{lightred}❌{default}] - 到期 [{lightred}{0}{default}]",
"sa_player_penalty_info_active_silence": "➔ 沉默 [{lightred}❌{default}] - 到期 [{lightred}{0}{default}]",
"sa_player_penalty_info_active_warn": "➔ 警告 [{lightred}❌{default}] - 到期 [{lightred}{0}{default}] - 原因 [{lightred}{1}{default}]",
"sa_player_penalty_info_active_mute": "➔ 静音 [{lightred}❌{default}] - 将于 [{lightred}{0}{default}] 过期",
"sa_player_penalty_info_active_gag": "➔ 禁言 [{lightred}❌{default}] - 将于 [{lightred}{0}{default}] 过期",
"sa_player_penalty_info_active_silence": "➔ 沉默 [{lightred}❌{default}] - 将于 [{lightred}{0}{default}] 过期",
"sa_player_penalty_info_active_warn": "➔ 警告 [{lightred}❌{default}] - 将于 [{lightred}{0}{default}] 过期 - 原因 [{lightred}{1}{default}]",
"sa_player_penalty_info_no_active_mute": "➔ 静音 [{lime}✔{default}]",
"sa_player_penalty_info_no_active_gag": "➔ 禁言 [{lime}✔{default}]",
"sa_player_penalty_info_no_active_silence": "➔ 沉默 [{lime}✔{default}]",
"sa_player_penalty_info_no_active_warn": "➔ 警告 [{lime}✔{default}]",
"sa_player_penalty_info": "===========================\n玩家处罚信息 {lightred}{0}{default}\n禁次数: {lightred}{1}{default}, 禁言次数: {lightred}{2}{default}, 静音次数: {lightred}{3}{default}, 沉默次数: {lightred}{4}{default}, 警告次数: {lightred}{5}{default}\n当前处罚:\n{6}\n当前警告:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}玩家处罚信息 {lightred}{0}{grey}, 禁: {lightred}{1}{grey}, 禁言: {lightred}{2}{grey}, 静音: {lightred}{3}{grey}, 沉默: {lightred}{4}{grey}, 警告: {lightred}{5}",
"sa_player_ban_message_time": "你因为{lightred}{0}{default}的原因被{lightred}{1}{default}禁止{lightred}{2}{default}分钟!",
"sa_player_ban_message_perm": "你因为{lightred}{0}{default}的原因被{lightred}{1}{default}永久禁止!",
"sa_player_kick_message": "你因为{lightred}{0}{default}的原因被{lightred}{1}{default}踢出!",
"sa_player_gag_message_time": "你因为{lightred}{0}{default}的原因被{lightred}{2}{default}禁言{lightred}{1}{default}分钟!",
"sa_player_gag_message_perm": "你因为{lightred}{0}{default}的原因被{lightred}{1}{default}永久禁言!",
"sa_player_mute_message_time": "你因为{lightred}{0}{default}的原因被{lightred}{2}{default}禁声{lightred}{1}{default}分钟!",
"sa_player_mute_message_perm": "你因为{lightred}{0}{default}的原因被{lightred}{1}{default}永久禁声!",
"sa_player_silence_message_time": "你因为{lightred}{0}{default}的原因被{lightred}{2}{default}禁止发言{lightred}{1}{default}分钟!",
"sa_player_silence_message_perm": "你因为{lightred}{0}{default}的原因被{lightred}{1}{default}永久禁止发言!",
"sa_player_warn_message_time": "您 {lightred}{0}{default} {lightred}{1}{default} 分钟内由 {lightred}{2}{default} 警告!",
"sa_player_warn_message_perm": "您 {lightred}{0}{default} {lightred}{1}{default} 永久警告!",
"sa_admin_ban_message_time": "{lightred}{0}{default} 因 {lightred}{1}{default} 被 {lightred}{2}{default} 禁言 {lightred}{3}{default} 分钟!",
"sa_admin_ban_message_perm": "{lightred}{0}{default} 因为 {lightred}{1}{default} 被永久禁言 {lightred}{2}{default}!",
"sa_admin_kick_message": "{lightred}{0}{default} 因为 {lightred}{1}{default} 被踢出!",
"sa_admin_gag_message_time": "{lightred}{0}{default} 因为 {lightred}{1}{default} {lightred}{2}{default} 禁言 {lightred}{3}{default} 分钟!",
"sa_admin_gag_message_perm": "{lightred}{0}{default} 因为 {lightred}{1}{default} 被永久禁言 {lightred}{2}{default}!",
"sa_admin_mute_message_time": "{lightred}{0}{default} 因为 {lightred}{1}{default} 的声音被 {lightred}{2}{default} 禁言 {lightred}{3}{default} 分钟!",
"sa_admin_mute_message_perm": "{lightred}{0}{default} 因为 {lightred}{1}{default} 的声音被永久禁言 {lightred}{2}{default}!",
"sa_admin_silence_message_time": "{lightred}{0}{default} 因为 {lightred}{1}{default} {lightred}{2}{default} 禁言 {lightred}{3}{default} 分钟!",
"sa_admin_silence_message_perm": "{lightred}{0}{default} 因为 {lightred}{1}{default} 被永久禁言 {lightred}{2}{default}!",
"sa_admin_warn_message_time": "{lightred}{0}{default} 因为 {lightred}{1}{default} 被警告 {lightred}{2}{default} {lightred}{3}{default} 分钟!",
"sa_admin_warn_message_perm": "{lightred}{0}{default} 因为 {lightred}{1}{default} 被永久警告 {lightred}{2}{default}!",
"sa_player_penalty_info": "===========================\n玩家 {lightred}{0}{default} 的处罚信息,\n禁次数: {lightred}{1}{default}, 禁言次数: {lightred}{2}{default}, 静音次数: {lightred}{3}{default}, 沉默次数: {lightred}{4}{default}, 警告次数: {lightred}{5}{default}\n活跃的处罚:\n{6}\n活跃的警告:\n{7}\n===========================",
"sa_admin_penalty_info": "{grey}玩家 {lightred}{0}{grey} 的处罚信息, 禁: {lightred}{1}{grey}, 禁言: {lightred}{2}{grey}, 静音: {lightred}{3}{grey}, 沉默: {lightred}{4}{grey}, 警告: {lightred}{5}",
"sa_admin_associated_accounts": "{grey}玩家 {lightred}{0}{grey} 的关联账户:{1}",
"sa_player_ban_message_time": "您已被 {lightred}{0}{default}{lightred}{2}{default} 禁止 {lightred}{1}{default} 分钟!",
"sa_player_ban_message_perm": "您已被 {lightred}{0}{default}{lightred}{1}{default} 永久禁止!",
"sa_player_kick_message": "您已被 {lightred}{0}{default}{lightred}{1}{default} 踢出!",
"sa_player_gag_message_time": "您已被 {lightred}{0}{default}{lightred}{2}{default} 禁言 {lightred}{1}{default} 分钟!",
"sa_player_gag_message_perm": "您已被 {lightred}{0}{default}{lightred}{1}{default} 永久禁言!",
"sa_player_mute_message_time": "您已被 {lightred}{0}{default} 因 {lightred}{2}{default} 静音 {lightred}{1}{default} 分钟!",
"sa_player_mute_message_perm": "您已被 {lightred}{0}{default}{lightred}{1}{default} 永久静音!",
"sa_player_silence_message_time": "您已被 {lightred}{0}{default} 因 {lightred}{2}{default} 沉默 {lightred}{1}{default} 分钟!",
"sa_player_silence_message_perm": "您已被 {lightred}{0}{default} {lightred}{1}{default} 永久沉默!",
"sa_player_warn_message_time": "您已被 {lightred}{0}{default} {lightred}{2}{default} 警告 {lightred}{1}{default} 分钟!",
"sa_player_warn_message_perm": "您已被 {lightred}{0}{default} 因 {lightred}{1}{default} 永久警告!",
"sa_admin_ban_message_time": "{lightred}{0}{default} 禁止了 {lightred}{1}{default} {lightred}{2}{default} 共 {lightred}{3}{default} 分钟!",
"sa_admin_ban_message_perm": "{lightred}{0}{default} 永久禁止了 {lightred}{1}{default} 因 {lightred}{2}{default}!",
"sa_admin_kick_message": "{lightred}{0}{default} 踢出了 {lightred}{1}{default} {lightred}{2}{default}!",
"sa_admin_gag_message_time": "{lightred}{0}{default} 禁言了 {lightred}{1}{default} {lightred}{2}{default} 共 {lightred}{3}{default} 分钟!",
"sa_admin_gag_message_perm": "{lightred}{0}{default} 永久禁言了 {lightred}{1}{default} {lightred}{2}{default}!",
"sa_admin_mute_message_time": "{lightred}{0}{default} 静音了 {lightred}{1}{default} {lightred}{2}{default} 共 {lightred}{3}{default} 分钟!",
"sa_admin_mute_message_perm": "{lightred}{0}{default} 永久静音了 {lightred}{1}{default} {lightred}{2}{default}!",
"sa_admin_silence_message_time": "{lightred}{0}{default} 沉默了 {lightred}{1}{default} {lightred}{2}{default} 共 {lightred}{3}{default} 分钟!",
"sa_admin_silence_message_perm": "{lightred}{0}{default} 永久沉默了 {lightred}{1}{default} {lightred}{2}{default}!",
"sa_admin_warn_message_time": "{lightred}{0}{default} 警告了 {lightred}{1}{default} {lightred}{2}{default} 共 {lightred}{3}{default} 分钟!",
"sa_admin_warn_message_perm": "{lightred}{0}{default} 永久警告了 {lightred}{1}{default} 因 {lightred}{2}{default}!",
"sa_admin_give_message": "{lightred}{0}{default} 给了 {lightred}{1}{default} {lightred}{2}{default}!",
"sa_admin_strip_message": "{lightred}{0}{default} 拿走了 {lightred}{1}{default} 的所有武器!",
"sa_admin_hp_message": "{lightred}{0}{default} 改了 {lightred}{1}{default} 的生命值!",
"sa_admin_speed_message": "{lightred}{0}{default} 改了 {lightred}{1}{default} 的速度!",
"sa_admin_gravity_message": "{lightred}{0}{default} 改了 {lightred}{1}{default} 的重力!",
"sa_admin_money_message": "{lightred}{0}{default} 改了 {lightred}{1}{default} 的金钱!",
"sa_admin_god_message": "{lightred}{0}{default} 改变了 {lightred}{1}{default} 的上帝模式!",
"sa_admin_slay_message": "{lightred}{0}{default} 杀死了 {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} 了 {lightred}{1}{default} 一巴掌!",
"sa_admin_changemap_message": "{lightred}{0}{default} 更改了地图为 {lightred}{1}{default}!",
"sa_admin_noclip_message": "{lightred}{0}{default} 为 {lightred}{1}{default} 切换了 noclip!",
"sa_admin_strip_message": "{lightred}{0}{default} 移除了 {lightred}{1}{default} 的所有武器!",
"sa_admin_hp_message": "{lightred}{0}{default} 改了 {lightred}{1}{default} 的生命值!",
"sa_admin_speed_message": "{lightred}{0}{default} 改了 {lightred}{1}{default} 的速度!",
"sa_admin_gravity_message": "{lightred}{0}{default} 改了 {lightred}{1}{default} 的重力!",
"sa_admin_money_message": "{lightred}{0}{default} 改了 {lightred}{1}{default} 的金钱!",
"sa_admin_god_message": "{lightred}{0}{default} 切换了 {lightred}{1}{default} 的上帝模式!",
"sa_admin_resize_message": "{lightred}{0}{default} 更改了 {lightred}{1}{default} 的大小!",
"sa_admin_slay_message": "{lightred}{0}{default} 击杀了 {lightred}{1}{default}!",
"sa_admin_slap_message": "{lightred}{0}{default} 掌掴了 {lightred}{1}{default}!",
"sa_admin_changemap_message": "{lightred}{0}{default} 将地图更换为 {lightred}{1}{default}!",
"sa_admin_noclip_message": "{lightred}{0}{default} 切换了 {lightred}{1}{default} 的穿墙模式!",
"sa_admin_freeze_message": "{lightred}{0}{default} 冻结了 {lightred}{1}{default}!",
"sa_admin_unfreeze_message": "{lightred}{0}{default} 解冻了 {lightred}{1}{default}!",
"sa_admin_rename_message": "{lightred}{0}{default} 更改了 {lightred}{1}{default} 的昵称为 {lightred}{2}{default}!",
"sa_admin_respawn_message": "{lightred}{0}{default} 重新生成了 {lightred}{1}{default}!",
"sa_admin_tp_message": "{lightred}{0}{default} 传送到 {lightred}{1}{default}!",
"sa_admin_bring_message": "{lightred}{0}{default} 将 {lightred}{1}{default} 传送到自己身边!",
"sa_admin_team_message": "{lightred}{0}{default} 将 {lightred}{1}{default} 转移到 {lightred}{2}{default} 队伍!",
"sa_admin_rename_message": "{lightred}{0}{default} {lightred}{1}{default} 的昵称为 {lightred}{2}{default}!",
"sa_admin_respawn_message": "{lightred}{0}{default} 复活了 {lightred}{1}{default}!",
"sa_admin_tp_message": "{lightred}{0}{default} 传送到 {lightred}{1}{default}!",
"sa_admin_bring_message": "{lightred}{0}{default} 将 {lightred}{1}{default} 传送到自己!",
"sa_admin_team_message": "{lightred}{0}{default} 将 {lightred}{1}{default} 转移到 {lightred}{2}{default} 队伍!",
"sa_admin_warns_menu_title": "{gold}{0} {lime}警告",
"sa_admin_warns_unwarn": "{lime}成功{default} 取消了对 {gold}{0}{default} 的警告 {gold}{1}{default}!",
"sa_admin_warns_unwarn": "{lime}成功移除警告{default} {gold}{0} 对于 {gold}{1}{default}!",
"sa_admin_vote_menu_title": "{lime}投票 {gold}{0}",
"sa_admin_vote_message": "{lightred}{0}{default} 开始为 {lightred}{1}{default} 进行投票",
"sa_admin_vote_message": "{lightred}{0}{default} 发起了 [{lightred}{1}{default}] 的投票",
"sa_admin_vote_message_results": "{lime}投票结果 {gold}{0}",
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
"sa_admin_voice_mute_all": "{Lightred}你已在讲话期间静音了{default}所有玩家",
"sa_admin_voice_unmute_all": "{Lime}你已取消了{default}所有玩家的静音",
"sa_admin_voice_listen_all": "{Default}你现在可以听到{lime}所有{default}玩家了",
"sa_admin_voice_unlisten_all": "{Default}你现在不再听到{lime}所有{default}玩家了",
"sa_adminsay_prefix": "{RED}管理员: {lightred}{0}{default}",
"sa_vipchat_template": "{LIME}(VIP CHAT) {0}{default}: {1}",
"sa_adminchat_template_admin": "{LIME}(管理员) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_adminchat_template_player": "{SILVER}(玩家) {lightred}{0}{default}: {lightred}{1}{default}",
"sa_discord_log_command": "**{0}** 在服务器 `HOSTNAME` 上发出了 `{1}` 命令",
"sa_menu_pluginsmanager_loaded": "{lime}已启用 {default}插件 {lime}{0}",
"sa_menu_pluginsmanager_unloaded": "{lightred}已禁用 {default}插件 {lightred}{0}"
}
"sa_discord_log_command": "**{0}** 在服务器 `HOSTNAME` 上执行了命令 `{1}`",
"sa_menu_pluginsmanager_loaded": "{lime}激活了 {default}插件 {lime}{0}",
"sa_menu_pluginsmanager_unloaded": "{lightred}停用了 {default}插件 {lightred}{0}"
}

View File

@@ -5,10 +5,11 @@
<RootNamespace>CS2_SimpleAdminApi</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.287" />
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.340" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,7 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdminApi;
@@ -8,21 +9,169 @@ namespace CS2_SimpleAdminApi;
public interface ICS2_SimpleAdminApi
{
public static readonly PluginCapability<ICS2_SimpleAdminApi?> PluginCapability = new("simpleadmin:api");
public event Action? OnSimpleAdminReady;
/// <summary>
/// Gets player information associated with the specified player controller.
/// </summary>
/// <param name="player">The player controller.</param>
/// <returns>PlayerInfo object representing player data.</returns>
public PlayerInfo GetPlayerInfo(CCSPlayerController player);
/// <summary>
/// Returns the database connection string used by the plugin.
/// </summary>
public string GetConnectionString();
/// <summary>
/// Returns the configured server IP address with port.
/// </summary>
public string GetServerAddress();
/// <summary>
/// Returns the internal server ID assigned in the plugin's database.
/// </summary>
public int? GetServerId();
/// <summary>
/// Returns mute-related penalties for the specified player.
/// </summary>
/// <param name="player">The player controller.</param>
/// <returns>A dictionary mapping penalty types to lists of penalties with end date, duration, and pass state.</returns>
public Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetPlayerMuteStatus(CCSPlayerController player);
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?>? OnPlayerPenaltied;
public event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?>? OnPlayerPenaltiedAdded;
/// <summary>
/// Event fired when a player receives a penalty.
/// </summary>
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltied;
/// <summary>
/// Event fired when a penalty is added to a player by SteamID.
/// </summary>
public event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltiedAdded;
/// <summary>
/// Event to show admin activity messages.
/// </summary>
public event Action<string, string?, bool, object>? OnAdminShowActivity;
/// <summary>
/// Event fired when an admin toggles silent mode.
/// </summary>
public event Action<int, bool>? OnAdminToggleSilent;
/// <summary>
/// Issues a penalty to a player controller with specified type, reason, and optional duration.
/// </summary>
public void IssuePenalty(CCSPlayerController player, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1);
/// <summary>
/// Issues a penalty to a player identified by SteamID with specified type, reason, and optional duration.
/// </summary>
public void IssuePenalty(SteamID steamid, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1);
/// <summary>
/// Logs a command invoked by a caller with the command string.
/// </summary>
public void LogCommand(CCSPlayerController? caller, string command);
/// <summary>
/// Logs a command invoked by a caller with the command info object.
/// </summary>
public void LogCommand(CCSPlayerController? caller, CommandInfo command);
/// <summary>
/// Shows an admin activity message, optionally suppressing broadcasting.
/// </summary>
public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs);
/// <summary>
/// Shows an admin activity message with a custom translated message (for modules with their own localizer).
/// </summary>
/// <param name="translatedMessage">Already translated message to display to players.</param>
/// <param name="callerName">Name of the admin executing the action (optional).</param>
/// <param name="dontPublish">If true, won't trigger publish events.</param>
public void ShowAdminActivityTranslated(string translatedMessage, string? callerName = null, bool dontPublish = false);
/// <summary>
/// Shows an admin activity message using module's localizer for per-player language support.
/// This method sends messages in each player's configured language.
/// </summary>
/// <param name="moduleLocalizer">The module's IStringLocalizer instance.</param>
/// <param name="messageKey">The translation key from the module's lang files.</param>
/// <param name="callerName">Name of the admin executing the action (optional).</param>
/// <param name="dontPublish">If true, won't trigger publish events.</param>
/// <param name="messageArgs">Arguments to format the localized message.</param>
public void ShowAdminActivityLocalized(object moduleLocalizer, string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs);
/// <summary>
/// Returns true if the specified admin player is in silent mode (not broadcasting activity).
/// </summary>
public bool IsAdminSilent(CCSPlayerController player);
/// <summary>
/// Returns a set of player slots representing admins currently in silent mode.
/// </summary>
public HashSet<int> ListSilentAdminsSlots();
/// <summary>
/// Registers a new command with the specified name, description, and callback.
/// </summary>
public void RegisterCommand(string name, string? description, CommandInfo.CommandCallback callback);
/// <summary>
/// Unregisters an existing command by its name.
/// </summary>
public void UnRegisterCommand(string name);
/// <summary>
/// Gets target players from command
/// </summary>
TargetResult? GetTarget(CommandInfo command);
/// <summary>
/// Returns the list of current valid players, available to call from other plugins.
/// </summary>
List<CCSPlayerController> GetValidPlayers();
/// <summary>
/// Registers a menu category.
/// </summary>
void RegisterMenuCategory(string categoryId, string categoryName, string permission = "@css/generic");
/// <summary>
/// Registers a menu in a category.
/// </summary>
void RegisterMenu(string categoryId, string menuId, string menuName, Func<CCSPlayerController, object> menuFactory, string? permission = null);
/// <summary>
/// Unregisters a menu from a category.
/// </summary>
void UnregisterMenu(string categoryId, string menuId);
/// <summary>
/// Creates a menu with an automatic back button.
/// </summary>
object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player);
/// <summary>
/// Creates a menu with a list of players with filter and action.
/// </summary>
object CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin, Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect);
/// <summary>
/// Adds an option to the menu (extension method helper).
/// </summary>
void AddMenuOption(object menu, string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null);
/// <summary>
/// Adds a submenu to the menu (extension method helper).
/// </summary>
void AddSubMenu(object menu, string name, Func<CCSPlayerController, object> subMenuFactory, bool disabled = false, string? permission = null);
/// <summary>
/// Opens a menu for a player.
/// </summary>
void OpenMenu(object menu, CCSPlayerController player);
}

View File

@@ -1,5 +1,5 @@
using System.Numerics;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Utils;
namespace CS2_SimpleAdminApi;
@@ -25,11 +25,16 @@ public class PlayerInfo(
public int TotalGags { get; set; } = totalGags;
public int TotalSilences { get; set; } = totalSilences;
public int TotalWarns { get; set; } = totalWarns;
public bool WaitingForKick { get; set; } = false;
public List<(ulong SteamId, string PlayerName)> AccountsAssociated { get; set; } = [];
public DiePosition? DiePosition { get; set; }
public bool IsLoaded { get; set; }
}
public struct DiePosition(Vector? position = null, QAngle? angle = null)
public class DiePosition(Vector3 position, Vector3 angle)
{
public Vector? Position { get; set; } = position;
public QAngle? Angle { get; set; } = angle;
public Vector3 Position { get; } = position;
public Vector3 Angle { get; } = angle;
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>AntiDLL_CS2_SimpleAdmin</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.305" />
</ItemGroup>
<ItemGroup>
<Reference Include="AntiDLL.API">
<HintPath>AntiDLL.API.dll</HintPath>
</Reference>
<Reference Include="CS2-SimpleAdminApi">
<HintPath>CS2-SimpleAdminApi.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AntiDLL-CS2-SimpleAdmin", "AntiDLL-CS2-SimpleAdmin.csproj", "{21D8E512-1FA9-41DD-B955-709704CEC377}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{21D8E512-1FA9-41DD-B955-709704CEC377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21D8E512-1FA9-41DD-B955-709704CEC377}.Debug|Any CPU.Build.0 = Debug|Any CPU
{21D8E512-1FA9-41DD-B955-709704CEC377}.Release|Any CPU.ActiveCfg = Release|Any CPU
{21D8E512-1FA9-41DD-B955-709704CEC377}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

Some files were not shown because too many files have changed in this diff Show More