Compare commits

..

37 Commits

Author SHA1 Message Date
Dawid Bepierszcz
099e91b43b Update FunCommands module project path in build workflow
Corrected the PROJECT_PATH_FUNCOMMANDSMODULE environment variable to point to the new location of the CS2-SimpleAdmin_FunCommands.csproj file within the build workflow configuration.
2025-10-19 16:08:45 +02:00
Dawid Bepierszcz
206c18db66 Add permission override support for menus
Menus can now specify a command name for permission override checking, allowing server admins to control menu visibility via CounterStrikeSharp's admin system. Updated API, MenuManager, and FunCommands module to support this feature. Also updated slap command to emit a sound, fixed SQL migration for IP address type, and bumped version to 1.7.8-beta-2.
2025-10-19 16:06:03 +02:00
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
171 changed files with 14682 additions and 2997 deletions

View File

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

8
.gitignore vendored
View File

@@ -3,4 +3,10 @@ obj/
.vs/ .vs/
.git .git
.vscode/ .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;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Managers; using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi; using CS2_SimpleAdminApi;
using Microsoft.Extensions.Localization;
namespace CS2_SimpleAdmin.Api; namespace CS2_SimpleAdmin.Api;
public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
{ {
public event Action? OnSimpleAdminReady;
public void OnSimpleAdminReadyEvent() => OnSimpleAdminReady?.Invoke();
public PlayerInfo GetPlayerInfo(CCSPlayerController player) public PlayerInfo GetPlayerInfo(CCSPlayerController player)
{ {
if (!player.UserId.HasValue) return !player.UserId.HasValue
throw new KeyNotFoundException("Player with specific UserId not found"); ? throw new KeyNotFoundException("Player with specific UserId not found")
: CS2_SimpleAdmin.PlayersInfo[player.SteamID];
return CS2_SimpleAdmin.PlayersInfo[player.UserId.Value];
} }
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); return PlayerPenaltyManager.GetAllPlayerPenalties(player.Slot);
} }
public event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?>? OnPlayerPenaltied; public event Action<PlayerInfo, PlayerInfo?, PenaltyType, string, int, int?, int?>? OnPlayerPenaltied;
public event Action<SteamID, PlayerInfo?, PenaltyType, string, int, int?>? OnPlayerPenaltiedAdded; 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, public void OnPlayerPenaltiedEvent(PlayerInfo player, PlayerInfo? admin, PenaltyType penaltyType, string reason,
int duration = -1) => OnPlayerPenaltied?.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 OnPlayerPenaltiedAddedEvent(SteamID player, PlayerInfo? admin, PenaltyType penaltyType, string reason,
int duration) => OnPlayerPenaltiedAdded?.Invoke(player, admin, penaltyType, reason, duration, 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) 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) public void LogCommand(CCSPlayerController? caller, string command)
{ {
Helper.LogCommand(caller, command); Helper.LogCommand(caller, command);
@@ -82,9 +134,184 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
{ {
Helper.LogCommand(caller, command); Helper.LogCommand(caller, command);
} }
public bool IsAdminSilent(CCSPlayerController player) public bool IsAdminSilent(CCSPlayerController player)
{ {
return CS2_SimpleAdmin.SilentPlayers.Contains(player.Slot); 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, string? commandName = null)
{
Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuName, BuilderFactory, permission, commandName);
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.Attributes;
using CounterStrikeSharp.API.Core.Capabilities; using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Managers; using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi; using CS2_SimpleAdminApi;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MySqlConnector; using MySqlConnector;
namespace CS2_SimpleAdmin; namespace CS2_SimpleAdmin;
[MinimumApiVersion(286)] [MinimumApiVersion(300)]
public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdminConfig> public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdminConfig>
{ {
internal static CS2_SimpleAdmin Instance { get; private set; } = new(); internal static CS2_SimpleAdmin Instance { get; private set; } = new();
@@ -19,45 +22,58 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
public override string ModuleAuthor => "daffyy & Dliix66"; public override string ModuleAuthor => "daffyy & Dliix66";
public override string ModuleVersion => "1.6.9c"; public override string ModuleVersion => "1.7.8-beta-2";
public override void Load(bool hotReload) public override void Load(bool hotReload)
{ {
Instance = this; Instance = this;
RegisterEvents();
if (hotReload) if (hotReload)
{ {
ServerLoaded = false; ServerLoaded = false;
_serverLoading = false; _serverLoading = false;
OnGameServerSteamAPIActivated();
CacheManager?.Dispose();
CacheManager = new CacheManager();
// OnGameServerSteamAPIActivated();
OnMapStart(string.Empty); OnMapStart(string.Empty);
AddTimer(2.0f, () => AddTimer(6.0f, () =>
{ {
if (Database == null) return; if (DatabaseProvider == null) return;
var playerManager = new PlayerManager(); PlayersInfo.Clear();
CachedPlayers.Clear();
Helper.GetValidPlayers().ForEach(player => 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")); _cBasePlayerControllerSetPawnFunc = new MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>(GameData.GetSignature("CBasePlayerController_SetPawn"));
SimpleAdminApi = new Api.CS2_SimpleAdminApi(); SimpleAdminApi = new Api.CS2_SimpleAdminApi();
Capabilities.RegisterPluginCapability(ICS2_SimpleAdminApi.PluginCapability, () => 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) public override void OnAllPluginsLoaded(bool hotReload)
{ {
AddTimer(3.0f, () => ReloadAdmins(null));
try try
{ {
MenuApi = MenuCapability.Get(); MenuApi = MenuCapability.Get();
@@ -65,50 +81,141 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError("Unable to load required plugins ... \n{exception}", ex.Message); 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) public void OnConfigParsed(CS2_SimpleAdminConfig config)
{ {
Instance = this; if (System.Diagnostics.Debugger.IsAttached)
_logger = Logger; 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, throw new Exception("[CS2-SimpleAdmin] You need to setup MySQL credentials in config!");
Database = config.DatabaseName, }
UserID = config.DatabaseUser,
Password = config.DatabasePassword, var builder = new MySqlConnectionStringBuilder()
Port = (uint)config.DatabasePort, {
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, Pooling = true,
MinimumPoolSize = 0,
MaximumPoolSize = 640,
}; };
DbConnectionString = builder.ConnectionString; DbConnectionString = builder.ConnectionString;
Database = new Database.Database(DbConnectionString); DatabaseProvider = new MySqlDatabaseProvider(DbConnectionString);
}
if (!Database.CheckDatabaseConnection(out var exception)) else
{
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.SqliteFilePath))
{ {
if (exception != null) throw new Exception("[CS2-SimpleAdmin] You need to specify SQLite file path in config!");
Logger.LogError("Problem with database connection! \n{exception}", exception);
Unload(false);
return;
} }
Task.Run(() => Database.DatabaseMigration());
Config = config; DatabaseProvider = new SqliteDatabaseProvider(ModuleDirectory + "/" + config.DatabaseConfig.SqliteFilePath);
Helper.UpdateConfig(config); }
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")) if (!Directory.Exists(ModuleDirectory + "/data"))
{ {
Directory.CreateDirectory(ModuleDirectory + "/data"); Directory.CreateDirectory(ModuleDirectory + "/data");
@@ -119,33 +226,46 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
if (!string.IsNullOrEmpty(Config.Discord.DiscordLogWebhook)) if (!string.IsNullOrEmpty(Config.Discord.DiscordLogWebhook))
DiscordWebhookClientLog = new DiscordManager(Config.Discord.DiscordLogWebhook); DiscordWebhookClientLog = new DiscordManager(Config.Discord.DiscordLogWebhook);
PluginInfo.ShowAd(ModuleVersion);
if (Config.EnableUpdateCheck) if (Config.EnableUpdateCheck)
Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, _logger)); Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, Logger));
PermissionManager = new PermissionManager(Database); PermissionManager = new PermissionManager(DatabaseProvider);
BanManager = new BanManager(Database); BanManager = new BanManager(DatabaseProvider);
MuteManager = new MuteManager(Database); MuteManager = new MuteManager(DatabaseProvider);
WarnManager = new WarnManager(Database); 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()) if (!matches.Any())
{ {
command.ReplyToCommand($"Target {command.GetArg(1)} not found."); command.ReplyToCommand($"Target {command.GetArg(argument)} not found.");
return null; return null;
} }
if (command.GetArg(1).StartsWith('@')) if (command.GetArg(argument).StartsWith('@'))
return matches; return matches;
if (matches.Count() == 1) if (matches.Count() == 1)
return matches; return matches;
command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(1)}\"."); command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(argument)}\".");
return null; 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> <Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <PropertyGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.287" /> <DebugType>none</DebugType>
<PackageReference Include="Dapper" Version="2.1.35" /> <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="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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CS2-SimpleAdminApi\CS2-SimpleAdminApi.csproj" /> <ProjectReference Include="..\CS2-SimpleAdminApi\CS2-SimpleAdminApi.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ItemGroup> <None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" /> <None Update="Database\Migrations\Mysql\001_CreateTables.sql">
<None Update="Database\Migrations\010_CreateWarnsTable.sql"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None>
</None> <None Update="Database\Migrations\Mysql\002_CreateFlagsTable.sql">
</ItemGroup> <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> <ItemGroup>
<None Update="Database\Migrations\*.sql" CopyToOutputDirectory="PreserveNewest" /> <None Update="Database\Migrations\*.sql" CopyToOutputDirectory="PreserveNewest" />
@@ -38,7 +145,8 @@
<ItemGroup> <ItemGroup>
<Reference Include="MenuManagerApi"> <Reference Include="MenuManagerApi">
<HintPath>3rd_party\MenuManagerApi.dll</HintPath> <HintPath>3rd_party\MenuManagerApi.dll</HintPath>
<Private>False</Private>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,86 +1,86 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Commands;
using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace CS2_SimpleAdmin; namespace CS2_SimpleAdmin;
public static class RegisterCommands 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 delegate void CommandCallback(CCSPlayerController? caller, CommandInfo.CommandCallback callback);
private static readonly string CommandsPath = Path.Combine(CS2_SimpleAdmin.ConfigDirectory, "Commands.json"); private static readonly string CommandsPath = Path.Combine(CS2_SimpleAdmin.ConfigDirectory, "Commands.json");
private static readonly List<CommandMapping> CommandMappings = private static readonly List<CommandMapping> CommandMappings =
[ [
new CommandMapping("css_ban", CS2_SimpleAdmin.Instance.OnBanCommand), new("css_ban", CS2_SimpleAdmin.Instance.OnBanCommand),
new CommandMapping("css_addban", CS2_SimpleAdmin.Instance.OnAddBanCommand), new("css_addban", CS2_SimpleAdmin.Instance.OnAddBanCommand),
new CommandMapping("css_banip", CS2_SimpleAdmin.Instance.OnBanIpCommand), new("css_banip", CS2_SimpleAdmin.Instance.OnBanIpCommand),
new CommandMapping("css_unban", CS2_SimpleAdmin.Instance.OnUnbanCommand), new("css_unban", CS2_SimpleAdmin.Instance.OnUnbanCommand),
new CommandMapping("css_warn", CS2_SimpleAdmin.Instance.OnWarnCommand), new("css_warn", CS2_SimpleAdmin.Instance.OnWarnCommand),
new CommandMapping("css_unwarn", CS2_SimpleAdmin.Instance.OnUnwarnCommand), new("css_unwarn", CS2_SimpleAdmin.Instance.OnUnwarnCommand),
new CommandMapping("css_asay", CS2_SimpleAdmin.Instance.OnAdminToAdminSayCommand), new("css_asay", CS2_SimpleAdmin.Instance.OnAdminToAdminSayCommand),
new CommandMapping("css_cssay", CS2_SimpleAdmin.Instance.OnAdminCustomSayCommand), new("css_cssay", CS2_SimpleAdmin.Instance.OnAdminCustomSayCommand),
new CommandMapping("css_say", CS2_SimpleAdmin.Instance.OnAdminSayCommand), new("css_say", CS2_SimpleAdmin.Instance.OnAdminSayCommand),
new CommandMapping("css_psay", CS2_SimpleAdmin.Instance.OnAdminPrivateSayCommand), new("css_psay", CS2_SimpleAdmin.Instance.OnAdminPrivateSayCommand),
new CommandMapping("css_csay", CS2_SimpleAdmin.Instance.OnAdminCenterSayCommand), new("css_csay", CS2_SimpleAdmin.Instance.OnAdminCenterSayCommand),
new CommandMapping("css_hsay", CS2_SimpleAdmin.Instance.OnAdminHudSayCommand), new("css_hsay", CS2_SimpleAdmin.Instance.OnAdminHudSayCommand),
new CommandMapping("css_penalties", CS2_SimpleAdmin.Instance.OnPenaltiesCommand), new("css_penalties", CS2_SimpleAdmin.Instance.OnPenaltiesCommand),
new CommandMapping("css_admin", CS2_SimpleAdmin.Instance.OnAdminCommand), new("css_admin", CS2_SimpleAdmin.Instance.OnAdminCommand),
new CommandMapping("css_adminhelp", CS2_SimpleAdmin.Instance.OnAdminHelpCommand), new("css_adminhelp", CS2_SimpleAdmin.Instance.OnAdminHelpCommand),
new CommandMapping("css_addadmin", CS2_SimpleAdmin.Instance.OnAddAdminCommand), new("css_addadmin", CS2_SimpleAdmin.Instance.OnAddAdminCommand),
new CommandMapping("css_deladmin", CS2_SimpleAdmin.Instance.OnDelAdminCommand), new("css_deladmin", CS2_SimpleAdmin.Instance.OnDelAdminCommand),
new CommandMapping("css_addgroup", CS2_SimpleAdmin.Instance.OnAddGroup), new("css_addgroup", CS2_SimpleAdmin.Instance.OnAddGroup),
new CommandMapping("css_delgroup", CS2_SimpleAdmin.Instance.OnDelGroupCommand), new("css_delgroup", CS2_SimpleAdmin.Instance.OnDelGroupCommand),
new CommandMapping("css_reloadadmins", CS2_SimpleAdmin.Instance.OnRelAdminCommand), new("css_reloadadmins", CS2_SimpleAdmin.Instance.OnRelAdminCommand),
new CommandMapping("css_hide", CS2_SimpleAdmin.Instance.OnHideCommand), new("css_reloadbans", CS2_SimpleAdmin.Instance.OnRelBans),
new CommandMapping("css_hidecomms", CS2_SimpleAdmin.Instance.OnHideCommsCommand), new("css_hide", CS2_SimpleAdmin.Instance.OnHideCommand),
new CommandMapping("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand), new("css_hidecomms", CS2_SimpleAdmin.Instance.OnHideCommsCommand),
new CommandMapping("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand), new("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand),
new CommandMapping("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand), new("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand),
new CommandMapping("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand), new("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand),
new CommandMapping("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand), new("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand),
new CommandMapping("css_map", CS2_SimpleAdmin.Instance.OnMapCommand), new("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand),
new CommandMapping("css_wsmap", CS2_SimpleAdmin.Instance.OnWorkshopMapCommand), new("css_map", CS2_SimpleAdmin.Instance.OnMapCommand),
new CommandMapping("css_cvar", CS2_SimpleAdmin.Instance.OnCvarCommand), new("css_wsmap", CS2_SimpleAdmin.Instance.OnWorkshopMapCommand),
new CommandMapping("css_rcon", CS2_SimpleAdmin.Instance.OnRconCommand), new("css_cvar", CS2_SimpleAdmin.Instance.OnCvarCommand),
new CommandMapping("css_rr", CS2_SimpleAdmin.Instance.OnRestartCommand), new("css_rcon", CS2_SimpleAdmin.Instance.OnRconCommand),
new("css_rr", CS2_SimpleAdmin.Instance.OnRestartCommand),
new CommandMapping("css_gag", CS2_SimpleAdmin.Instance.OnGagCommand), new("css_gag", CS2_SimpleAdmin.Instance.OnGagCommand),
new CommandMapping("css_addgag", CS2_SimpleAdmin.Instance.OnAddGagCommand), new("css_addgag", CS2_SimpleAdmin.Instance.OnAddGagCommand),
new CommandMapping("css_ungag", CS2_SimpleAdmin.Instance.OnUngagCommand), new("css_ungag", CS2_SimpleAdmin.Instance.OnUngagCommand),
new CommandMapping("css_mute", CS2_SimpleAdmin.Instance.OnMuteCommand), new("css_mute", CS2_SimpleAdmin.Instance.OnMuteCommand),
new CommandMapping("css_addmute", CS2_SimpleAdmin.Instance.OnAddMuteCommand), new("css_addmute", CS2_SimpleAdmin.Instance.OnAddMuteCommand),
new CommandMapping("css_unmute", CS2_SimpleAdmin.Instance.OnUnmuteCommand), new("css_unmute", CS2_SimpleAdmin.Instance.OnUnmuteCommand),
new CommandMapping("css_silence", CS2_SimpleAdmin.Instance.OnSilenceCommand), new("css_silence", CS2_SimpleAdmin.Instance.OnSilenceCommand),
new CommandMapping("css_addsilence", CS2_SimpleAdmin.Instance.OnAddSilenceCommand), new("css_addsilence", CS2_SimpleAdmin.Instance.OnAddSilenceCommand),
new CommandMapping("css_unsilence", CS2_SimpleAdmin.Instance.OnUnsilenceCommand), 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("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand),
new CommandMapping("css_freeze", CS2_SimpleAdmin.Instance.OnFreezeCommand), new("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand),
new CommandMapping("css_unfreeze", CS2_SimpleAdmin.Instance.OnUnfreezeCommand), new("css_team", CS2_SimpleAdmin.Instance.OnTeamCommand),
new CommandMapping("css_godmode", CS2_SimpleAdmin.Instance.OnGodCommand), new("css_rename", CS2_SimpleAdmin.Instance.OnRenameCommand),
new("css_prename", CS2_SimpleAdmin.Instance.OnPrenameCommand),
new CommandMapping("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand), new("css_tp", CS2_SimpleAdmin.Instance.OnGotoCommand),
new CommandMapping("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand), new("css_bring", CS2_SimpleAdmin.Instance.OnBringCommand),
new CommandMapping("css_give", CS2_SimpleAdmin.Instance.OnGiveCommand), new("css_pluginsmanager", CS2_SimpleAdmin.Instance.OnPluginManagerCommand),
new CommandMapping("css_strip", CS2_SimpleAdmin.Instance.OnStripCommand), new("css_adminvoice", CS2_SimpleAdmin.Instance.OnAdminVoiceCommand)
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)
]; ];
/// <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() public static void InitializeCommands()
{ {
if (!File.Exists(CommandsPath)) 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() private static void CreateConfig()
{ {
var commands = new CommandsConfig var commands = new CommandsConfig
@@ -120,6 +124,7 @@ public static class RegisterCommands
{ "css_addgroup", new Command { Aliases = ["css_addgroup"] } }, { "css_addgroup", new Command { Aliases = ["css_addgroup"] } },
{ "css_delgroup", new Command { Aliases = ["css_delgroup"] } }, { "css_delgroup", new Command { Aliases = ["css_delgroup"] } },
{ "css_reloadadmins", new Command { Aliases = ["css_reloadadmins"] } }, { "css_reloadadmins", new Command { Aliases = ["css_reloadadmins"] } },
{ "css_reloadbans", new Command { Aliases = ["css_reloadbans"] } },
{ "css_hide", new Command { Aliases = ["css_hide", "css_stealth"] } }, { "css_hide", new Command { Aliases = ["css_hide", "css_stealth"] } },
{ "css_hidecomms", new Command { Aliases = ["css_hidecomms"] } }, { "css_hidecomms", new Command { Aliases = ["css_hidecomms"] } },
{ "css_who", new Command { Aliases = ["css_who"] } }, { "css_who", new Command { Aliases = ["css_who"] } },
@@ -142,66 +147,88 @@ public static class RegisterCommands
{ "css_addsilence", new Command { Aliases = ["css_addsilence"] } }, { "css_addsilence", new Command { Aliases = ["css_addsilence"] } },
{ "css_unsilence", new Command { Aliases = ["css_unsilence"] } }, { "css_unsilence", new Command { Aliases = ["css_unsilence"] } },
{ "css_vote", new Command { Aliases = ["css_vote"] } }, { "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_slay", new Command { Aliases = ["css_slay"] } },
{ "css_slap", new Command { Aliases = ["css_slap"] } }, { "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_team", new Command { Aliases = ["css_team"] } },
{ "css_rename", new Command { Aliases = ["css_rename"] } }, { "css_rename", new Command { Aliases = ["css_rename"] } },
{ "css_prename", new Command { Aliases = ["css_prename"] } }, { "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_tp", new Command { Aliases = ["css_tp", "css_tpto", "css_goto"] } },
{ "css_bring", new Command { Aliases = ["css_bring", "css_tphere"] } }, { "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); 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() private static void Register()
{ {
var json = File.ReadAllText(CommandsPath); 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; if (commandsConfig?.Commands != null)
foreach (var command in commandsConfig.Commands)
{ {
if (command.Value.Aliases == null) continue; foreach (var command in commandsConfig.Commands)
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); 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 private class CommandsConfig
{ {
public Dictionary<string, Command>? Commands { get; init; } public Dictionary<string, Command>? Commands { get; init; }
} }
/// <summary>
/// Represents a command definition containing a list of aliases.
/// </summary>
private class Command private class Command
{ {
public string[]? Aliases { get; init; } 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) private class CommandMapping(string commandKey, CommandInfo.CommandCallback callback)
{ {
public string CommandKey { get; } = commandKey; public string CommandKey { get; } = commandKey;

View File

@@ -12,6 +12,11 @@ namespace CS2_SimpleAdmin;
public partial class 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")] [RequiresPermissions("@css/ban")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [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) 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; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
if (command.ArgCount < 2) if (command.ArgCount < 2)
return; return;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, IsHLTV: false }).ToList(); 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; return;
} }
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0) var reason = command.ArgCount >= 3
reason = command.GetArg(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 => playersToTarget.ForEach(player =>
{ {
if (!caller.CanTarget(player)) return; 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, DurationMenu.OpenMenu(caller, $"{_localizer?["sa_ban"] ?? "Ban"}: {player.PlayerName}", player,
ManagePlayersMenu.BanMenu); 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) 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 (!caller.CanTarget(player)) return;
if (!CheckValidBan(caller, time)) return; if (!CheckValidBan(caller, time)) return;
// Set default caller name if not provided // Set default caller name if not provided
callerName ??= _localizer?["sa_console"] ?? "Console"; callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
// Freeze player pawn if alive : (_localizer?["sa_console"] ?? "Console");
if (player.PawnIsAlive)
{
player.Pawn.Value?.Freeze();
}
// Get player and admin information // Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value]; var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle banning logic // Asynchronously handle banning logic
Task.Run(async () => 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 // Determine message keys and arguments based on ban time
var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = time == 0 var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = time == 0
? ("sa_player_ban_message_perm", "sa_admin_ban_message_perm", ? ("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 // Display admin activity message if necessary
if (caller == null || !SilentPlayers.Contains(caller.Slot)) if (caller == null || !SilentPlayers.Contains(caller.Slot))
{ {
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs); Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
} }
// Schedule a kick timer // Schedule a kick timer
if (player.UserId.HasValue) if (player.UserId.HasValue)
{ {
AddTimer(Config.OtherSettings.KickTime, () => Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED, 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);
} }
// Execute ban command if necessary // Execute ban command if necessary
@@ -125,14 +131,61 @@ public partial class CS2_SimpleAdmin
} }
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Ban, _localizer); 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")] [RequiresPermissions("@css/ban")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo command) public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo command)
{ {
if (Database == null) return; if (DatabaseProvider == null) return;
var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console"; var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return; if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null) if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
@@ -141,21 +194,21 @@ public partial class CS2_SimpleAdmin
return; return;
} }
var steamid = steamId.SteamId64.ToString(); var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3 && !string.IsNullOrEmpty(command.GetArg(3)) var reason = command.ArgCount >= 3
? command.GetArg(3) ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown"; : _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; if (!CheckValidBan(caller, time)) return;
var adminInfo = caller != null && caller.UserId.HasValue var adminInfo = caller != null && caller.UserId.HasValue
? PlayersInfo[caller.UserId.Value] ? PlayersInfo[caller.SteamID]
: null; : null;
var matches = Helper.GetPlayerFromSteamid64(steamid); var player = Helper.GetPlayerFromSteamid64(steamid);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
if (player != null && player.IsValid) if (player != null && player.IsValid)
{ {
if (!caller.CanTarget(player)) if (!caller.CanTarget(player))
@@ -166,13 +219,21 @@ public partial class CS2_SimpleAdmin
} }
else else
{ {
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous ban operation if player is not online or not found // Asynchronous ban operation if player is not online or not found
Task.Run(async () => 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."); 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) if (UnlockedCommands)
Server.ExecuteCommand($"banid 1 {steamId.SteamId3}"); 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")] [RequiresPermissions("@css/ban")]
[CommandHelper(minArgs: 1, usage: "<ip> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<ip> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnBanIpCommand(CCSPlayerController? caller, CommandInfo command) public void OnBanIpCommand(CCSPlayerController? caller, CommandInfo command)
{ {
if (Database == null) return; if (DatabaseProvider == null) return;
var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console"; var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return; if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
var ipAddress = command.GetArg(1); var ipAddress = command.GetArg(1);
@@ -200,26 +264,31 @@ public partial class CS2_SimpleAdmin
return; return;
} }
var reason = command.ArgCount >= 3 && !string.IsNullOrEmpty(command.GetArg(3)) var reason = command.ArgCount >= 3
? command.GetArg(3) ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown"; : _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; if (!CheckValidBan(caller, time)) return;
var adminInfo = caller != null && caller.UserId.HasValue var adminInfo = caller != null && caller.UserId.HasValue
? PlayersInfo[caller.UserId.Value] ? PlayersInfo[caller.SteamID]
: null; : null;
var matches = Helper.GetPlayerFromIp(ipAddress); var players = Helper.GetPlayerFromIp(ipAddress);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null; if (players.Count >= 1)
if (player != null && player.IsValid)
{ {
if (!caller.CanTarget(player)) foreach (var player in players)
return; {
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 else
{ {
@@ -235,13 +304,19 @@ public partial class CS2_SimpleAdmin
Helper.LogCommand(caller, command); 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) private bool CheckValidBan(CCSPlayerController? caller, int duration)
{ {
if (caller == null) return true; 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"]}"); caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}");
return false; return false;
@@ -253,14 +328,17 @@ public partial class CS2_SimpleAdmin
return false; 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")] [RequiresPermissions("@css/unban")]
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<steamid or name or ip> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnbanCommand(CCSPlayerController? caller, CommandInfo command) public void OnUnbanCommand(CCSPlayerController? caller, CommandInfo command)
{ {
if (Database == null) return; if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console"; var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
if (command.GetArg(1).Length <= 1) if (command.GetArg(1).Length <= 1)
{ {
command.ReplyToCommand($"Too short pattern to search."); command.ReplyToCommand($"Too short pattern to search.");
@@ -268,27 +346,31 @@ public partial class CS2_SimpleAdmin
} }
var pattern = command.GetArg(1); 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)); Task.Run(async () => await BanManager.UnbanPlayer(pattern, callerSteamId, reason));
Helper.LogCommand(caller, command); Helper.LogCommand(caller, command);
command.ReplyToCommand($"Unbanned player with pattern {pattern}."); 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")] [RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [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) public void OnWarnCommand(CCSPlayerController? caller, CommandInfo command)
{ {
if (Database == null) if (DatabaseProvider == null)
return; return;
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
if (command.ArgCount < 2) if (command.ArgCount < 2)
return; return;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected && !player.IsHLTV).ToList(); 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; return;
} }
WarnManager warnManager = new(Database); var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
var reason = command.ArgCount >= 3
int.TryParse(command.GetArg(2), out var time); ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0)
reason = command.GetArg(3); reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
playersToTarget.ForEach(player => playersToTarget.ForEach(player =>
{ {
if (caller!.CanTarget(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 (!caller.CanTarget(player)) return;
if (!CheckValidBan(caller, time)) return; if (!CheckValidBan(caller, time)) return;
// Set default caller name if not provided // 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 // 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 // Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value]; var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle warning logic // Asynchronously handle warning logic
Task.Run(async () => Task.Run(async () =>
{ {
warnManager ??= new WarnManager(Database); int? penaltyId = await WarnManager.WarnPlayer(playerInfo, adminInfo, reason, time);
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 // 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) if (Config.WarnThreshold.Count > 0)
{ {
string? punishCommand = null; string? punishCommand = null;
@@ -353,7 +451,7 @@ public partial class CS2_SimpleAdmin
if (!string.IsNullOrEmpty(punishCommand)) if (!string.IsNullOrEmpty(punishCommand))
{ {
await Server.NextFrameAsync(() => await Server.NextWorldUpdateAsync(() =>
{ {
Server.ExecuteCommand(punishCommand.Replace("USERID", playerInfo.UserId.ToString()).Replace("STEAMID64", playerInfo.SteamId?.ToString())); 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 // Display admin activity message if necessary
if (caller == null || !SilentPlayers.Contains(caller.Slot)) if (caller == null || !SilentPlayers.Contains(caller.Slot))
{ {
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs); Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
} }
// Log the warning command // Log the warning command
@@ -387,14 +485,86 @@ public partial class CS2_SimpleAdmin
// Send Discord notification for the warning // Send Discord notification for the warning
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Warn, _localizer); 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")] [RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<steamid or name or ip>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnwarnCommand(CCSPlayerController? caller, CommandInfo command) public void OnUnwarnCommand(CCSPlayerController? caller, CommandInfo command)
{ {
if (Database == null) return; if (DatabaseProvider == null) return;
if (command.GetArg(1).Length <= 1) if (command.GetArg(1).Length <= 1)
{ {
@@ -403,9 +573,7 @@ public partial class CS2_SimpleAdmin
} }
var pattern = command.GetArg(1); var pattern = command.GetArg(1);
Task.Run(async () => await WarnManager.UnwarnPlayer(pattern)); Task.Run(async () => await WarnManager.UnwarnPlayer(pattern));
Helper.LogCommand(caller, command); Helper.LogCommand(caller, command);
command.ReplyToCommand($"Unwarned player with pattern {pattern}."); 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.Memory;
using CounterStrikeSharp.API.Modules.Utils; using CounterStrikeSharp.API.Modules.Utils;
using System.Text; using System.Text;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin; namespace CS2_SimpleAdmin;
public partial class 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>")] [CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")] [RequiresPermissions("@css/chat")]
public void OnAdminToAdminSayCommand(CCSPlayerController? caller, CommandInfo command) public void OnAdminToAdminSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -20,7 +27,7 @@ public partial class CS2_SimpleAdmin
var utf8String = Encoding.UTF8.GetString(utf8BytesString); var utf8String = Encoding.UTF8.GetString(utf8BytesString);
foreach (var player in Helper.GetValidPlayers() 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) if (_localizer != null)
player.PrintToChat(_localizer["sa_adminchat_template_admin", 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>")] [CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")] [RequiresPermissions("@css/chat")]
public void OnAdminCustomSayCommand(CCSPlayerController? caller, CommandInfo command) 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>")] [CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")] [RequiresPermissions("@css/chat")]
public void OnAdminSayCommand(CCSPlayerController? caller, CommandInfo command) public void OnAdminSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -56,7 +73,6 @@ public partial class CS2_SimpleAdmin
var utf8String = Encoding.UTF8.GetString(utf8BytesString); var utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command); Helper.LogCommand(caller, command);
foreach (var player in Helper.GetValidPlayers()) foreach (var player in Helper.GetValidPlayers())
{ {
player.SendLocalizedMessage(_localizer, 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>")] [CommandHelper(2, "<#userid or name> <message>")]
[RequiresPermissions("@css/chat")] [RequiresPermissions("@css/chat")]
public void OnAdminPrivateSayCommand(CCSPlayerController? caller, CommandInfo command) public void OnAdminPrivateSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -91,6 +112,11 @@ public partial class CS2_SimpleAdmin
command.ReplyToCommand($" Private message sent!"); 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>")] [CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")] [RequiresPermissions("@css/chat")]
public void OnAdminCenterSayCommand(CCSPlayerController? caller, CommandInfo command) public void OnAdminCenterSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -99,10 +125,14 @@ public partial class CS2_SimpleAdmin
var utf8String = Encoding.UTF8.GetString(utf8BytesString); var utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command); Helper.LogCommand(caller, command);
Helper.PrintToCenterAll(utf8String.ReplaceColorTags()); 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>")] [CommandHelper(1, "<message>")]
[RequiresPermissions("@css/chat")] [RequiresPermissions("@css/chat")]
public void OnAdminHudSayCommand(CCSPlayerController? caller, CommandInfo command) public void OnAdminHudSayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -115,6 +145,6 @@ public partial class CS2_SimpleAdmin
VirtualFunctions.ClientPrintAll( VirtualFunctions.ClientPrintAll(
HudDestination.Alert, HudDestination.Alert,
utf8String.ReplaceColorTags(), 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.Core;
using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Managers; using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Menus; using CS2_SimpleAdmin.Menus;
using CS2_SimpleAdminApi; using CS2_SimpleAdminApi;
@@ -10,15 +11,18 @@ namespace CS2_SimpleAdmin;
public partial class 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [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) 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 callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList(); var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
@@ -28,13 +32,18 @@ public partial class CS2_SimpleAdmin
return; return;
} }
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0) var reason = command.ArgCount >= 3
reason = command.GetArg(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 => playersToTarget.ForEach(player =>
{ {
if (!caller!.CanTarget(player)) return; 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, DurationMenu.OpenMenu(caller, $"{_localizer?["sa_gag"] ?? "Gag"}: {player.PlayerName}", player,
ManagePlayersMenu.GagMenu); 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) 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 (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return; if (!CheckValidMute(caller, time)) return;
@@ -55,13 +74,18 @@ public partial class CS2_SimpleAdmin
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information // Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value]; var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle gag logic // Asynchronously handle gag logic
Task.Run(async () => 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 // Add penalty to the player's penalty manager
@@ -82,11 +106,11 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players // Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot)) 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 // Increment the player's total gags count
PlayersInfo[player.UserId.Value].TotalGags++; PlayersInfo[player.SteamID].TotalGags++;
// Log the gag command and send Discord notification // Log the gag command and send Discord notification
if (!silent) if (!silent)
@@ -98,14 +122,63 @@ public partial class CS2_SimpleAdmin
} }
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Gag, _localizer); 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddGagCommand(CCSPlayerController? caller, CommandInfo command) public void OnAddGagCommand(CCSPlayerController? caller, CommandInfo command)
{ {
if (Database == null) return; if (DatabaseProvider == null) return;
// Set caller name // Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
@@ -120,20 +193,21 @@ public partial class CS2_SimpleAdmin
return; return;
} }
var steamid = steamId.SteamId64.ToString(); var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3 && command.GetArg(3).Length > 0 var reason = command.ArgCount >= 3
? command.GetArg(3) ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: (_localizer?["sa_unknown"] ?? "Unknown"); : _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; if (!CheckValidMute(caller, time)) return;
// Get player and admin info // 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 // Attempt to match player based on SteamID
var matches = Helper.GetPlayerFromSteamid64(steamid); var player = Helper.GetPlayerFromSteamid64(steamid);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
if (player != null && player.IsValid) if (player != null && player.IsValid)
{ {
@@ -145,32 +219,48 @@ public partial class CS2_SimpleAdmin
} }
else else
{ {
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous gag operation for offline players // Asynchronous gag operation for offline players
Task.Run(async () => 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."); command.ReplyToCommand($"Player with steamid {steamid} is not online. Gag has been added offline.");
} }
// Log the gag command and respond to the command // Log the gag command and respond to the command
Helper.LogCommand(caller, 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<steamid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUngagCommand(CCSPlayerController? caller, CommandInfo command) 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 callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1); 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) if (pattern.Length <= 1)
{ {
command.ReplyToCommand($"Too short pattern to search."); command.ReplyToCommand($"Too short pattern to search.");
@@ -182,8 +272,7 @@ public partial class CS2_SimpleAdmin
// Check if pattern is a valid SteamID64 // Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null) if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{ {
var matches = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString()); var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
if (player != null && player.IsValid) if (player != null && player.IsValid)
{ {
@@ -207,8 +296,8 @@ public partial class CS2_SimpleAdmin
{ {
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Gag); PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Gag);
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalGags > 0) if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalGags > 0)
PlayersInfo[namePlayer.UserId.Value].TotalGags--; PlayersInfo[namePlayer.SteamID].TotalGags--;
Task.Run(async () => 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [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) 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 callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList(); var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
@@ -246,13 +338,18 @@ public partial class CS2_SimpleAdmin
return; return;
} }
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0) var reason = command.ArgCount >= 3
reason = command.GetArg(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 => playersToTarget.ForEach(player =>
{ {
if (!caller!.CanTarget(player)) return; 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, DurationMenu.OpenMenu(caller, $"{_localizer?["sa_mute"] ?? "Mute"}: {player.PlayerName}", player,
ManagePlayersMenu.MuteMenu); 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) 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 (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return; if (!CheckValidMute(caller, time)) return;
@@ -273,8 +380,8 @@ public partial class CS2_SimpleAdmin
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information // Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value]; var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Set player's voice flags to muted // Set player's voice flags to muted
player.VoiceFlags = VoiceFlags.Muted; player.VoiceFlags = VoiceFlags.Muted;
@@ -282,7 +389,12 @@ public partial class CS2_SimpleAdmin
// Asynchronously handle mute logic // Asynchronously handle mute logic
Task.Run(async () => 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 // Add penalty to the player's penalty manager
@@ -303,11 +415,11 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players // Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot)) 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 // Increment the player's total mutes count
PlayersInfo[player.UserId.Value].TotalMutes++; PlayersInfo[player.SteamID].TotalMutes++;
// Log the mute command and send Discord notification // Log the mute command and send Discord notification
if (!silent) if (!silent)
@@ -319,14 +431,18 @@ public partial class CS2_SimpleAdmin
} }
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Mute, _localizer); 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddMuteCommand(CCSPlayerController? caller, CommandInfo command) public void OnAddMuteCommand(CCSPlayerController? caller, CommandInfo command)
{ {
if (Database == null) return; if (DatabaseProvider == null) return;
// Set caller name // Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
@@ -341,20 +457,21 @@ public partial class CS2_SimpleAdmin
return; return;
} }
var steamid = steamId.SteamId64.ToString(); var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3 && command.GetArg(3).Length > 0 var reason = command.ArgCount >= 3
? command.GetArg(3) ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: (_localizer?["sa_unknown"] ?? "Unknown"); : _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; if (!CheckValidMute(caller, time)) return;
// Get player and admin info // 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 // Attempt to match player based on SteamID
var matches = Helper.GetPlayerFromSteamid64(steamid); var player = Helper.GetPlayerFromSteamid64(steamid);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
if (player != null && player.IsValid) if (player != null && player.IsValid)
{ {
@@ -366,32 +483,95 @@ public partial class CS2_SimpleAdmin
} }
else else
{ {
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous mute operation for offline players // Asynchronous mute operation for offline players
Task.Run(async () => 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."); command.ReplyToCommand($"Player with steamid {steamid} is not online. Mute has been added offline.");
} }
// Log the mute command and respond to the command // Log the mute command and respond to the command
Helper.LogCommand(caller, 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<steamid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnmuteCommand(CCSPlayerController? caller, CommandInfo command) 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 callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1); 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) if (pattern.Length <= 1)
{ {
command.ReplyToCommand("Too short pattern to search."); command.ReplyToCommand("Too short pattern to search.");
@@ -403,8 +583,7 @@ public partial class CS2_SimpleAdmin
// Check if pattern is a valid SteamID64 // Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null) if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{ {
var matches = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString()); var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
if (player != null && player.IsValid) if (player != null && player.IsValid)
{ {
@@ -430,8 +609,8 @@ public partial class CS2_SimpleAdmin
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Mute); PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Mute);
namePlayer.VoiceFlags = VoiceFlags.Normal; namePlayer.VoiceFlags = VoiceFlags.Normal;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalMutes > 0) if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalMutes > 0)
PlayersInfo[namePlayer.UserId.Value].TotalMutes--; PlayersInfo[namePlayer.SteamID].TotalMutes--;
Task.Run(async () => 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [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) 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 callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var reason = _localizer?["sa_unknown"] ?? "Unknown";
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList(); var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
@@ -469,13 +652,18 @@ public partial class CS2_SimpleAdmin
return; return;
} }
if (command.ArgCount >= 3 && command.GetArg(3).Length > 0) var reason = command.ArgCount >= 3
reason = command.GetArg(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 => playersToTarget.ForEach(player =>
{ {
if (!caller!.CanTarget(player)) return; 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, DurationMenu.OpenMenu(caller, $"{_localizer?["sa_silence"] ?? "Silence"}: {player.PlayerName}", player,
ManagePlayersMenu.SilenceMenu); 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) 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 (!caller.CanTarget(player)) return;
if (!CheckValidMute(caller, time)) return; if (!CheckValidMute(caller, time)) return;
@@ -496,13 +694,18 @@ public partial class CS2_SimpleAdmin
callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// Get player and admin information // Get player and admin information
var playerInfo = PlayersInfo[player.UserId.Value]; var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
// Asynchronously handle silence logic // Asynchronously handle silence logic
Task.Run(async () => 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 // Add penalty to the player's penalty manager
@@ -524,11 +727,11 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players // Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot)) 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 // Increment the player's total silences count
PlayersInfo[player.UserId.Value].TotalSilences++; PlayersInfo[player.SteamID].TotalSilences++;
// Log the silence command and send Discord notification // Log the silence command and send Discord notification
if (!silent) if (!silent)
@@ -540,14 +743,19 @@ public partial class CS2_SimpleAdmin
} }
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Silence, _localizer); 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [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) public void OnAddSilenceCommand(CCSPlayerController? caller, CommandInfo command)
{ {
if (Database == null) return; if (DatabaseProvider == null) return;
// Set caller name // Set caller name
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
@@ -562,20 +770,21 @@ public partial class CS2_SimpleAdmin
return; return;
} }
var steamid = steamId.SteamId64.ToString(); var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3 && command.GetArg(3).Length > 0 var reason = command.ArgCount >= 3
? command.GetArg(3) ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: (_localizer?["sa_unknown"] ?? "Unknown"); : _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; if (!CheckValidMute(caller, time)) return;
// Get player and admin info // 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 // Attempt to match player based on SteamID
var matches = Helper.GetPlayerFromSteamid64(steamid); var player = Helper.GetPlayerFromSteamid64(steamid);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
if (player != null && player.IsValid) if (player != null && player.IsValid)
{ {
@@ -587,45 +796,107 @@ public partial class CS2_SimpleAdmin
} }
else else
{ {
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
return;
// Asynchronous silence operation for offline players // Asynchronous silence operation for offline players
Task.Run(async () => 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."); command.ReplyToCommand($"Player with steamid {steamid} is not online. Silence has been added offline.");
} }
// Log the silence command and respond to the command // Log the silence command and respond to the command
Helper.LogCommand(caller, 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")] [RequiresPermissions("@css/chat")]
[CommandHelper(minArgs: 1, usage: "<steamid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<steamid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnsilenceCommand(CCSPlayerController? caller, CommandInfo command) 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 callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
var pattern = command.GetArg(1); 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) if (pattern.Length <= 1)
{ {
command.ReplyToCommand("Too short pattern to search."); command.ReplyToCommand("Too short pattern to search.");
return; return;
} }
Helper.LogCommand(caller, command); Helper.LogCommand(caller, command);
// Check if pattern is a valid SteamID64 // Check if pattern is a valid SteamID64
if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null) if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null)
{ {
var matches = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString()); var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64);
var player = matches.Count == 1 ? matches.FirstOrDefault() : null;
if (player != null && player.IsValid) if (player != null && player.IsValid)
{ {
@@ -655,8 +926,8 @@ public partial class CS2_SimpleAdmin
// Reset voice flags to normal // Reset voice flags to normal
namePlayer.VoiceFlags = VoiceFlags.Normal; namePlayer.VoiceFlags = VoiceFlags.Normal;
if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalSilences > 0) if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalSilences > 0)
PlayersInfo[namePlayer.UserId.Value].TotalSilences--; PlayersInfo[namePlayer.SteamID].TotalSilences--;
Task.Run(async () => 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) private bool CheckValidMute(CCSPlayerController? caller, int duration)
{ {
if (caller == null) return true; 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"]}"); caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_perm_restricted"]}");
return false; return false;

View File

@@ -8,6 +8,12 @@ namespace CS2_SimpleAdmin;
public partial class 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")] [RequiresPermissions("@css/generic")]
[CommandHelper(minArgs: 2, usage: "<question> [... options ...]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 2, usage: "<question> [... options ...]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnVoteCommand(CCSPlayerController? caller, CommandInfo command) public void OnVoteCommand(CCSPlayerController? caller, CommandInfo command)

View File

@@ -1,215 +1,307 @@
using CounterStrikeSharp.API.Core; // using System.Globalization;
using CounterStrikeSharp.API.Modules.Admin; // using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Modules.Commands; // using CounterStrikeSharp.API.Core;
// using CounterStrikeSharp.API.Modules.Admin;
namespace CS2_SimpleAdmin; // using CounterStrikeSharp.API.Modules.Commands;
//
public partial class CS2_SimpleAdmin // namespace CS2_SimpleAdmin;
{ //
[CommandHelper(1, "<#userid or name>")] // public partial class CS2_SimpleAdmin
[RequiresPermissions("@css/cheats")] // {
public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command) // /// <summary>
{ // /// Enables or disables no-clip mode for specified player(s).
var callerName = caller == null ? _localizer?["sa_console"] ?? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; // /// </summary>
// /// <param name="caller">The player issuing the command.</param>
var targets = GetTarget(command); // /// <param name="command">The command input containing targets.</param>
if (targets == null) return; // [CommandHelper(1, "<#userid or name>")]
var playersToTarget = targets.Players.Where(player => // [RequiresPermissions("@css/cheats")]
player.IsValid && // public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command)
player is { PawnIsAlive: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected }).ToList(); // {
// var callerName = caller == null ? _localizer?["sa_console"] ?? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
playersToTarget.ForEach(player => //
{ // var targets = GetTarget(command);
if (caller!.CanTarget(player)) // if (targets == null) return;
{ // var playersToTarget = targets.Players.Where(player =>
NoClip(caller, player, callerName); // player.IsValid &&
} // player is { IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
}); //
} // playersToTarget.ForEach(player =>
// {
internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) // if (caller!.CanTarget(player))
{ // {
if (!player.IsValid) return; // NoClip(caller, player, callerName);
if (!caller.CanTarget(player)) return; // }
// });
// Set default caller name if not provided //
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; // Helper.LogCommand(caller, command);
// }
// Toggle no-clip mode for the player //
player.Pawn.Value?.ToggleNoclip(); // /// <summary>
// /// Toggles no-clip mode for a player and shows admin activity messages.
// Determine message keys and arguments for the no-clip notification // /// </summary>
var (activityMessageKey, adminActivityArgs) = // /// <param name="caller">The player/admin toggling no-clip.</param>
("sa_admin_noclip_message", // /// <param name="player">The target player whose no-clip state changes.</param>
new object[] { "CALLER", player.PlayerName }); // /// <param name="callerName">Optional caller name for messages.</param>
// /// <param name="command">Optional command info for logging.</param>
// Display admin activity message to other players // internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
if (caller == null || !SilentPlayers.Contains(caller.Slot)) // {
{ // if (!player.IsValid) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs); // if (!caller.CanTarget(player)) return;
} //
// // Set default caller name if not provided
// Log the command // callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
if (command == null) //
{ // // Toggle no-clip mode for the player
Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); // player.Pawn.Value?.ToggleNoclip();
} //
else // // Determine message keys and arguments for the no-clip notification
{ // var (activityMessageKey, adminActivityArgs) =
Helper.LogCommand(caller, command); // ("sa_admin_noclip_message",
} // new object[] { "CALLER", player.PlayerName });
} //
// // Display admin activity message to other players
[RequiresPermissions("@css/cheats")] // if (caller == null || !SilentPlayers.Contains(caller.Slot))
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] // {
public void OnGodCommand(CCSPlayerController? caller, CommandInfo command) // Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
{ // }
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; //
var targets = GetTarget(command); // // Log the command
if (targets == null) return; // if (command == null)
// Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { PawnIsAlive: true, IsHLTV: false }).ToList(); // }
//
playersToTarget.ForEach(player => // /// <summary>
{ // /// Enables or disables god mode for specified player(s).
if (player.Connected != PlayerConnectedState.PlayerConnected) // /// </summary>
return; // /// <param name="caller">The player issuing the command.</param>
// /// <param name="command">The command input containing targets.</param>
if (caller!.CanTarget(player)) //
{ // [RequiresPermissions("@css/cheats")]
God(caller, player, command); // [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);
internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null) // if (targets == null) return;
{ //
if (!caller.CanTarget(player)) return; // var playersToTarget = targets.Players.Where(player => player.IsValid && player is {IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
// Set default caller name if not provided // playersToTarget.ForEach(player =>
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; // {
// if (player.Connected != PlayerConnectedState.PlayerConnected)
// Toggle god mode for the player // return;
if (!GodPlayers.Add(player.Slot)) //
{ // if (caller!.CanTarget(player))
GodPlayers.Remove(player.Slot); // {
} // God(caller, player, command);
// }
// Log the command // });
if (command == null) //
Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); // Helper.LogCommand(caller, command);
else // }
Helper.LogCommand(caller, command); //
// /// <summary>
// Determine message key and arguments for the god mode notification // /// Toggles god mode for a player and notifies admins.
var (activityMessageKey, adminActivityArgs) = // /// </summary>
("sa_admin_god_message", // /// <param name="caller">The player/admin toggling god mode.</param>
new object[] { "CALLER", player.PlayerName }); // /// <param name="player">The target player whose god mode changes.</param>
// /// <param name="command">Optional command info for logging.</param>
// Display admin activity message to other players // internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null)
if (caller == null || !SilentPlayers.Contains(caller.Slot)) // {
{ // if (!caller.CanTarget(player)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs); //
} // // Set default caller name if not provided
} // var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
//
[CommandHelper(1, "<#userid or name> [duration]")] // // Toggle god mode for the player
[RequiresPermissions("@css/slay")] // if (!GodPlayers.Add(player.Slot))
public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command) // {
{ // GodPlayers.Remove(player.Slot);
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; // }
int.TryParse(command.GetArg(2), out var time); //
// // Log the command
var targets = GetTarget(command); // if (command == null)
if (targets == null) return; // Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, PawnIsAlive: true, IsHLTV: false }).ToList(); //
// // Determine message key and arguments for the god mode notification
playersToTarget.ForEach(player => // var (activityMessageKey, adminActivityArgs) =
{ // ("sa_admin_god_message",
if (caller!.CanTarget(player)) // new object[] { "CALLER", player.PlayerName });
{ //
Freeze(caller, player, time, callerName, command); // // Display admin activity message to other players
} // if (caller == null || !SilentPlayers.Contains(caller.Slot))
}); // {
} // Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// }
internal static void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time, string? callerName = null, CommandInfo? command = null) // }
{ //
if (!player.IsValid) return; // /// <summary>
if (!caller.CanTarget(player)) return; // /// Freezes target player(s) for an optional specified duration.
// /// </summary>
// Set default caller name if not provided // /// <param name="caller">The player issuing the freeze command.</param>
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; // /// <param name="command">The command input containing targets and duration.</param>
// [CommandHelper(1, "<#userid or name> [duration]")]
// Freeze player pawn // [RequiresPermissions("@css/slay")]
player.Pawn.Value?.Freeze(); // public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// Determine message keys and arguments for the freeze notification // var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var (activityMessageKey, adminActivityArgs) = // int.TryParse(command.GetArg(2), out var time);
("sa_admin_freeze_message", //
new object[] { "CALLER", player.PlayerName }); // var targets = GetTarget(command);
// if (targets == null) return;
// Display admin activity message to other players // var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
if (caller == null || !SilentPlayers.Contains(caller.Slot)) //
{ // playersToTarget.ForEach(player =>
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs); // {
} // if (caller!.CanTarget(player))
// {
// Schedule unfreeze for the player if time is specified // Freeze(caller, player, time, callerName, command);
if (time > 0) // }
{ // });
Instance.AddTimer(time, () => player.Pawn.Value?.Unfreeze(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); //
} // Helper.LogCommand(caller, command);
// }
// Log the command and send Discord notification //
if (command == null) // /// <summary>
Helper.LogCommand(caller, $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}"); // /// Resizes the target player(s) models to a specified scale.
else // /// </summary>
Helper.LogCommand(caller, command); // /// <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]")]
[CommandHelper(1, "<#userid or name>")] // [RequiresPermissions("@css/slay")]
[RequiresPermissions("@css/slay")] // public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command)
public void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command) // {
{ // var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
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); // var targets = GetTarget(command);
if (targets == null) return; // if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, PawnIsAlive: true, IsHLTV: false }).ToList(); // var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
//
playersToTarget.ForEach(player => // playersToTarget.ForEach(player =>
{ // {
Unfreeze(caller, player, callerName, command); // if (!caller!.CanTarget(player)) return;
}); //
} // var sceneNode = player.PlayerPawn.Value!.CBodyComponent?.SceneNode;
// if (sceneNode == null) return;
internal static void Unfreeze(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) //
{ // sceneNode.GetSkeletonInstance().Scale = size;
if (!player.IsValid) return; // player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture));
if (!caller.CanTarget(player)) return; //
// Server.NextWorldUpdate(() =>
// Set default caller name if not provided // {
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; // Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent");
// });
// Unfreeze player pawn //
player.Pawn.Value?.Unfreeze(); // var (activityMessageKey, adminActivityArgs) =
// ("sa_admin_resize_message",
// Determine message keys and arguments for the unfreeze notification // new object[] { "CALLER", player.PlayerName });
var (activityMessageKey, adminActivityArgs) = //
("sa_admin_unfreeze_message", // // Display admin activity message to other players
new object[] { "CALLER", player.PlayerName }); // if (caller == null || !SilentPlayers.Contains(caller.Slot))
// {
// Display admin activity message to other players // Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
if (caller == null || !SilentPlayers.Contains(caller.Slot)) // }
{ // });
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs); //
} // Helper.LogCommand(caller, command);
// }
// Log the command and send Discord notification //
if (command == null) // /// <summary>
Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); // /// Freezes a single player and optionally schedules automatic unfreeze after a duration.
else // /// </summary>
Helper.LogCommand(caller, command); // /// <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 System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands;
@@ -10,9 +11,12 @@ namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin public partial class CS2_SimpleAdmin
{ {
internal static readonly Dictionary<int, float> SpeedPlayers = []; /// <summary>
internal static readonly Dictionary<CCSPlayerController, float> GravityPlayers = []; /// 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")] [RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSlayCommand(CCSPlayerController? caller, CommandInfo command) public void OnSlayCommand(CCSPlayerController? caller, CommandInfo command)
@@ -21,14 +25,23 @@ public partial class CS2_SimpleAdmin
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; 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 => playersToTarget.ForEach(player =>
{ {
Slay(caller, player, callerName, command); 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) internal static void Slay(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{ {
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) return; if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) return;
@@ -48,386 +61,19 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players // Display admin activity message to other players
if (caller == null || !SilentPlayers.Contains(caller.Slot)) 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 // Log the command and send Discord notification
if (command == null) if (command == null)
Helper.LogCommand(caller, $"css_slay {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); 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")] [RequiresPermissions("@css/slay")]
[CommandHelper(minArgs: 1, usage: "<#userid or name> [damage]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 1, usage: "<#userid or name> [damage]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSlapCommand(CCSPlayerController? caller, CommandInfo command) public void OnSlapCommand(CCSPlayerController? caller, CommandInfo command)
@@ -437,7 +83,7 @@ public partial class CS2_SimpleAdmin
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; 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) if (command.ArgCount >= 2)
{ {
@@ -454,8 +100,17 @@ public partial class CS2_SimpleAdmin
Slap(caller, player, damage, command); 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) internal static void Slap(CCSPlayerController? caller, CCSPlayerController player, int damage, CommandInfo? command = null)
{ {
if (!caller.CanTarget(player)) return; if (!caller.CanTarget(player)) return;
@@ -465,13 +120,12 @@ public partial class CS2_SimpleAdmin
// Apply slap damage to the player // Apply slap damage to the player
player.Pawn.Value?.Slap(damage); player.Pawn.Value?.Slap(damage);
player.EmitSound("BaseGrenade.JumpThrowM");
// Log the command // Log the command
if (command == null) if (command == null)
Helper.LogCommand(caller, $"css_slap {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {damage}"); 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 // Determine message key and arguments for the slap notification
var (activityMessageKey, adminActivityArgs) = var (activityMessageKey, adminActivityArgs) =
("sa_admin_slap_message", ("sa_admin_slap_message",
@@ -482,10 +136,15 @@ public partial class CS2_SimpleAdmin
if (_localizer != null) 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")] [RequiresPermissions("@css/kick")]
[CommandHelper(minArgs: 2, usage: "<#userid or name> [<ct/tt/spec>] [-k]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [CommandHelper(minArgs: 2, usage: "<#userid or name> [<ct/tt/spec>] [-k]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnTeamCommand(CCSPlayerController? caller, CommandInfo command) public void OnTeamCommand(CCSPlayerController? caller, CommandInfo command)
@@ -531,8 +190,19 @@ public partial class CS2_SimpleAdmin
{ {
ChangeTeam(caller, player, _teamName, teamNum, kill, command); 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) 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 // Check if the player is valid and connected
@@ -548,7 +218,7 @@ public partial class CS2_SimpleAdmin
// Change team based on the provided teamName and conditions // Change team based on the provided teamName and conditions
if (!teamName.Equals("swap", StringComparison.OrdinalIgnoreCase)) 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); player.SwitchTeam(teamNum);
else else
player.ChangeTeam(teamNum); player.ChangeTeam(teamNum);
@@ -559,7 +229,7 @@ public partial class CS2_SimpleAdmin
{ {
var _teamNum = (CsTeam)player.TeamNum == CsTeam.Terrorist ? CsTeam.CounterTerrorist : CsTeam.Terrorist; var _teamNum = (CsTeam)player.TeamNum == CsTeam.Terrorist ? CsTeam.CounterTerrorist : CsTeam.Terrorist;
teamName = _teamNum == CsTeam.Terrorist ? "TT" : "CT"; 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); player.SwitchTeam(_teamNum);
else else
player.ChangeTeam(_teamNum); player.ChangeTeam(_teamNum);
@@ -569,8 +239,6 @@ public partial class CS2_SimpleAdmin
// Log the command // Log the command
if (command == null) if (command == null)
Helper.LogCommand(caller, $"css_team {player.PlayerName} {teamName}"); Helper.LogCommand(caller, $"css_team {player.PlayerName} {teamName}");
else
Helper.LogCommand(caller, command);
// Determine message key and arguments for the team change notification // Determine message key and arguments for the team change notification
var activityMessageKey = "sa_admin_team_message"; var activityMessageKey = "sa_admin_team_message";
@@ -579,9 +247,14 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players // Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return; if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, 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>")] [CommandHelper(1, "<#userid or name> <new name>")]
[RequiresPermissions("@css/kick")] [RequiresPermissions("@css/kick")]
public void OnRenameCommand(CCSPlayerController? caller, CommandInfo command) public void OnRenameCommand(CCSPlayerController? caller, CommandInfo command)
@@ -620,13 +293,18 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players // Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return; if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
Helper.ShowAdminActivity(activityMessageKey, callerName, adminActivityArgs); Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
// Rename the player // Rename the player
player.Rename(newName); 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>")] [CommandHelper(1, "<#userid or name> <new name>")]
[RequiresPermissions("@css/ban")] [RequiresPermissions("@css/ban")]
public void OnPrenameCommand(CCSPlayerController? caller, CommandInfo command) public void OnPrenameCommand(CCSPlayerController? caller, CommandInfo command)
@@ -661,7 +339,7 @@ public partial class CS2_SimpleAdmin
// Display admin activity message to other players // Display admin activity message to other players
if (caller != null && !SilentPlayers.Contains(caller.Slot)) 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 // Determine if the new name is valid and update the renamed players list
@@ -677,146 +355,270 @@ public partial class CS2_SimpleAdmin
}); });
} }
[CommandHelper(1, "<#userid or name>")] /// <summary>
[RequiresPermissions("@css/cheats")] /// Teleports targeted player(s) to another player's location.
public void OnRespawnCommand(CCSPlayerController? caller, CommandInfo command) /// </summary>
{ /// <param name="caller">Admin issuing teleport command.</param>
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; /// <param name="command">Command containing teleport targets and destination.</param>
[CommandHelper(1, "<#userid or name> [#userid or name]")]
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>")]
[RequiresPermissions("@css/kick")] [RequiresPermissions("@css/kick")]
public void OnGotoCommand(CCSPlayerController? caller, CommandInfo command) public void OnGotoCommand(CCSPlayerController? caller, CommandInfo command)
{ {
// Check if the caller is valid and has a live pawn IEnumerable<CCSPlayerController> playersToTeleport;
if (caller == null || !caller.PawnIsAlive) return; CCSPlayerController? destinationPlayer;
// Get the target players
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null || targets.Count() > 1) return;
var playersToTarget = targets.Players if (command.ArgCount < 3)
.Where(player => player is { IsValid: true, IsHLTV: false }) {
.ToList(); 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); Helper.LogCommand(caller, command);
// Process each player to teleport foreach (var player in playersToTeleport)
foreach (var player in playersToTarget.Where(player => player is { Connected: PlayerConnectedState.PlayerConnected, PawnIsAlive: true }).Where(caller.CanTarget))
{ {
if (caller.PlayerPawn.Value == null) if (player.PlayerPawn?.Value == null || destinationPlayer?.PlayerPawn?.Value == null)
continue; continue;
// Teleport the caller to the player and toggle noclip player.TeleportPlayer(destinationPlayer);
caller.TeleportPlayer(player);
caller.PlayerPawn.Value.ToggleNoclip();
// Set a timer to toggle noclip back after 3 seconds player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
AddTimer(3, () => caller.PlayerPawn.Value.ToggleNoclip()); 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 destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
var activityMessageKey = "sa_admin_tp_message"; destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
var adminActivityArgs = new object[] { "CALLER", player.PlayerName }; Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
// Show admin activity AddTimer(4, () =>
if (!SilentPlayers.Contains(caller.Slot) && _localizer != null)
{ {
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")] [RequiresPermissions("@css/kick")]
public void OnBringCommand(CCSPlayerController? caller, CommandInfo command) public void OnBringCommand(CCSPlayerController? caller, CommandInfo command)
{ {
// Check if the caller is valid and has a live pawn IEnumerable<CCSPlayerController> playersToTeleport;
if (caller == null || !caller.PawnIsAlive) return; CCSPlayerController? destinationPlayer;
// Get the target players if (command.ArgCount < 3)
var targets = GetTarget(command); {
if (targets == null || targets.Count() > 1) return; if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
return;
var playersToTarget = targets.Players var targets = GetTarget(command);
.Where(player => player is { IsValid: true, IsHLTV: false }) if (targets == null || !targets.Any())
.ToList(); 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); Helper.LogCommand(caller, command);
// Process each player to teleport foreach (var player in playersToTeleport)
foreach (var player in playersToTarget.Where(player => player is { Connected: PlayerConnectedState.PlayerConnected, PawnIsAlive: true }).Where(caller.CanTarget))
{ {
if (caller.PlayerPawn.Value == null) if (player.PlayerPawn?.Value == null || destinationPlayer.PlayerPawn?.Value == null)
continue; continue;
// Teleport the player to the caller and toggle noclip // Teleport
player.TeleportPlayer(caller); player.TeleportPlayer(destinationPlayer);
caller.PlayerPawn.Value.ToggleNoclip();
// Set a timer to toggle noclip back after 3 seconds player.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
AddTimer(3, () => caller.PlayerPawn.Value.ToggleNoclip()); 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 destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
var activityMessageKey = "sa_admin_bring_message"; destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING;
var adminActivityArgs = new object[] { "CALLER", player.PlayerName }; Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup");
Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup");
// Show admin activity AddTimer(4, () =>
if (!SilentPlayers.Contains(caller.Slot) && _localizer != null)
{ {
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")] [JsonPropertyName("DiscordPenaltyBanSettings")]
public DiscordPenaltySetting[] DiscordPenaltyBanSettings { get; set; } = public DiscordPenaltySetting[] DiscordPenaltyBanSettings { get; set; } =
[ [
new DiscordPenaltySetting { Name = "Color", Value = "" }, new() { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" }, new() { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, new() { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, new() { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" }, new() { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, new() { Name = "Time", Value = "{relative}" },
]; ];
[JsonPropertyName("DiscordPenaltyMuteSettings")] [JsonPropertyName("DiscordPenaltyMuteSettings")]
public DiscordPenaltySetting[] DiscordPenaltyMuteSettings { get; set; } = public DiscordPenaltySetting[] DiscordPenaltyMuteSettings { get; set; } =
[ [
new DiscordPenaltySetting { Name = "Color", Value = "" }, new() { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" }, new() { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, new() { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, new() { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" }, new() { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, new() { Name = "Time", Value = "{relative}" },
]; ];
[JsonPropertyName("DiscordPenaltyGagSettings")] [JsonPropertyName("DiscordPenaltyGagSettings")]
public DiscordPenaltySetting[] DiscordPenaltyGagSettings { get; set; } = public DiscordPenaltySetting[] DiscordPenaltyGagSettings { get; set; } =
[ [
new DiscordPenaltySetting { Name = "Color", Value = "" }, new() { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" }, new() { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, new() { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, new() { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" }, new() { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, new() { Name = "Time", Value = "{relative}" },
]; ];
[JsonPropertyName("DiscordPenaltySilenceSettings")] [JsonPropertyName("DiscordPenaltySilenceSettings")]
public DiscordPenaltySetting[] DiscordPenaltySilenceSettings { get; set; } = public DiscordPenaltySetting[] DiscordPenaltySilenceSettings { get; set; } =
[ [
new DiscordPenaltySetting { Name = "Color", Value = "" }, new() { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" }, new() { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, new() { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, new() { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" }, new() { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, new() { Name = "Time", Value = "{relative}" },
]; ];
[JsonPropertyName("DiscordPenaltyWarnSettings")] [JsonPropertyName("DiscordPenaltyWarnSettings")]
public DiscordPenaltySetting[] DiscordPenaltyWarnSettings { get; set; } = public DiscordPenaltySetting[] DiscordPenaltyWarnSettings { get; set; } =
[ [
new DiscordPenaltySetting { Name = "Color", Value = "" }, new() { Name = "Color", Value = "" },
new DiscordPenaltySetting { Name = "Webhook", Value = "" }, new() { Name = "Webhook", Value = "" },
new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, new() { Name = "ThumbnailUrl", Value = "" },
new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, new() { Name = "ImageUrl", Value = "" },
new DiscordPenaltySetting { Name = "Footer", Value = "" }, new() { Name = "Footer", Value = "" },
new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, 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")] [JsonPropertyName("Durations")]
public DurationItem[] Durations { get; set; } = public DurationItem[] Durations { get; set; } =
[ [
new DurationItem { Name = "1 minute", Duration = 1 }, new() { Name = "1 minute", Duration = 1 },
new DurationItem { Name = "5 minutes", Duration = 5 }, new() { Name = "5 minutes", Duration = 5 },
new DurationItem { Name = "15 minutes", Duration = 15 }, new() { Name = "15 minutes", Duration = 15 },
new DurationItem { Name = "1 hour", Duration = 60 }, new() { Name = "1 hour", Duration = 60 },
new DurationItem { Name = "1 day", Duration = 60 * 24 }, new() { Name = "1 day", Duration = 60 * 24 },
new DurationItem { Name = "7 days", Duration = 60 * 24 * 7 }, new() { Name = "7 days", Duration = 60 * 24 * 7 },
new DurationItem { Name = "14 days", Duration = 60 * 24 * 14 }, new() { Name = "14 days", Duration = 60 * 24 * 14 },
new DurationItem { Name = "30 days", Duration = 60 * 24 * 30 }, new() { Name = "30 days", Duration = 60 * 24 * 30 },
new DurationItem { Name = "Permanent", Duration = 0 } new() { Name = "Permanent", Duration = 0 }
]; ];
[JsonPropertyName("BanReasons")] [JsonPropertyName("BanReasons")]
@@ -166,18 +177,18 @@ public class MenuConfig
[JsonPropertyName("AdminFlags")] [JsonPropertyName("AdminFlags")]
public AdminFlag[] AdminFlags { get; set; } = public AdminFlag[] AdminFlags { get; set; } =
[ [
new AdminFlag { Name = "Generic", Flag = "@css/generic" }, new() { Name = "Generic", Flag = "@css/generic" },
new AdminFlag { Name = "Chat", Flag = "@css/chat" }, new() { Name = "Chat", Flag = "@css/chat" },
new AdminFlag { Name = "Change Map", Flag = "@css/changemap" }, new() { Name = "Change Map", Flag = "@css/changemap" },
new AdminFlag { Name = "Slay", Flag = "@css/slay" }, new() { Name = "Slay", Flag = "@css/slay" },
new AdminFlag { Name = "Kick", Flag = "@css/kick" }, new() { Name = "Kick", Flag = "@css/kick" },
new AdminFlag { Name = "Ban", Flag = "@css/ban" }, new() { Name = "Ban", Flag = "@css/ban" },
new AdminFlag { Name = "Perm Ban", Flag = "@css/permban" }, new() { Name = "Perm Ban", Flag = "@css/permban" },
new AdminFlag { Name = "Unban", Flag = "@css/unban" }, new() { Name = "Unban", Flag = "@css/unban" },
new AdminFlag { Name = "Show IP", Flag = "@css/showip" }, new() { Name = "Show IP", Flag = "@css/showip" },
new AdminFlag { Name = "Cvar", Flag = "@css/cvar" }, new() { Name = "Cvar", Flag = "@css/cvar" },
new AdminFlag { Name = "Rcon", Flag = "@css/rcon" }, new() { Name = "Rcon", Flag = "@css/rcon" },
new AdminFlag { Name = "Root (all flags)", Flag = "@css/root" } new() { Name = "Root (all flags)", Flag = "@css/root" }
]; ];
} }
@@ -230,27 +241,17 @@ public class OtherSettings
[JsonPropertyName("AdditionalCommandsToLog")] [JsonPropertyName("AdditionalCommandsToLog")]
public List<string> AdditionalCommandsToLog { get; set; } = new(); public List<string> AdditionalCommandsToLog { get; set; } = new();
[JsonPropertyName("IgnoredIps")]
public List<string> IgnoredIps { get; set; } = new();
} }
public class CS2_SimpleAdminConfig : BasePluginConfig public class CS2_SimpleAdminConfig : BasePluginConfig
{ {
[JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 24; [JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 25;
[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("DatabaseConfig")]
public DatabaseConfig DatabaseConfig { get; set; } = new();
[JsonPropertyName("OtherSettings")] [JsonPropertyName("OtherSettings")]
public OtherSettings OtherSettings { get; set; } = new(); public OtherSettings OtherSettings { get; set; } = new();
@@ -287,4 +288,38 @@ public class CS2_SimpleAdminConfig : BasePluginConfig
[JsonPropertyName("MenuConfig")] [JsonPropertyName("MenuConfig")]
public MenuConfig MenuConfigs { get; set; } = new(); 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); var connection = new MySqlConnection(dbConnectionString);
connection.Open(); connection.Open();
// using var cmd = connection.CreateCommand();
// cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
// cmd.ExecuteNonQueryAsync();
return connection; return connection;
} }
catch (Exception ex) catch (Exception ex)
@@ -26,6 +31,11 @@ public class Database(string dbConnectionString)
{ {
var connection = new MySqlConnection(dbConnectionString); var connection = new MySqlConnection(dbConnectionString);
await connection.OpenAsync(); await connection.OpenAsync();
// await using var cmd = connection.CreateCommand();
// cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
// await cmd.ExecuteNonQueryAsync();
return connection; return connection;
} }
catch (Exception ex) catch (Exception ex)
@@ -35,11 +45,11 @@ public class Database(string dbConnectionString)
} }
} }
public void DatabaseMigration() // public async Task DatabaseMigration()
{ // {
Migration migrator = new(this); // Migration migrator = new(this);
migrator.ExecuteMigrations(); // await migrator.ExecuteMigrationsAsync();
} // }
public bool CheckDatabaseConnection(out string? exception) 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 System.Data.Common;
using MySqlConnector; using Microsoft.Extensions.Logging;
namespace CS2_SimpleAdmin.Database; 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") await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
.OrderBy(f => f); 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 var lastAppliedVersion = await GetLastAppliedVersionAsync(connection);
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);
foreach (var file in files) foreach (var file in files)
{ {
var version = Path.GetFileNameWithoutExtension(file); var version = Path.GetFileNameWithoutExtension(file);
if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0)
continue;
// Check if the migration has already been applied try
if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0) continue; {
var sqlScript = File.ReadAllText(file); var sqlScript = await File.ReadAllTextAsync(file);
using var cmdMigration = new MySqlCommand(sqlScript, connection); await using (var cmdMigration = connection.CreateCommand())
cmdMigration.ExecuteNonQuery(); {
cmdMigration.CommandText = sqlScript;
await cmdMigration.ExecuteNonQueryAsync();
}
// Update the last applied migration version await UpdateLastAppliedVersionAsync(connection, version);
UpdateLastAppliedVersion(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); await using var cmd = connection.CreateCommand();
var result = cmd.ExecuteScalar(); cmd.CommandText = "SELECT version FROM sa_migrations ORDER BY id DESC LIMIT 1;";
var result = await cmd.ExecuteScalarAsync();
return result?.ToString() ?? string.Empty; 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); await using var cmd = connection.CreateCommand();
cmd.Parameters.AddWithValue("@Version", version); cmd.CommandText = "INSERT INTO sa_migrations (version) VALUES (@Version);";
cmd.ExecuteNonQuery();
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`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`) INSERT IGNORE INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`)
VALUES (-1, 'Console', 'Console', '', '0', NULL, NULL, NOW()); VALUES (0, 'Console', 'Console', '', '0', NULL, NULL, NOW());
UPDATE `sa_admins` SET `id` = 0 WHERE `id` = -1; 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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `sa_admins` ADD `group_id` INT NULL AFTER `created`; 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_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_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; 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` ( CREATE TABLE IF NOT EXISTS `sa_players_ips` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`steamid` bigint(20) NOT NULL, `steamid` bigint(20) NOT NULL,
`address` varchar(64) NOT NULL, `address` varchar(64) NOT NULL,
`used_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `used_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`), PRIMARY KEY (`steamid`, `address`)
UNIQUE KEY `steamid` (`steamid`,`address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) 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` INTEGER 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,4 @@
TRUNCATE TABLE `sa_players_ips`;
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()
{