diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57a42c2..5e89866 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build +name: Build and Publish on: push: @@ -11,85 +11,111 @@ on: - '**/README.md' env: - BUILD_NUMBER: ${{ github.run_number }} PROJECT_PATH_CS2_SIMPLEADMIN: "CS2-SimpleAdmin/CS2-SimpleAdmin.csproj" - PROJECT_PATH_CS2_SIMPLEADMINAPI: "CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj" PROJECT_NAME_CS2_SIMPLEADMIN: "CS2-SimpleAdmin" + PROJECT_PATH_CS2_SIMPLEADMINAPI: "CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj" PROJECT_NAME_CS2_SIMPLEADMINAPI: "CS2-SimpleAdminApi" + PROJECT_PATH_STEALTHMODULE: "Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.csproj" + PROJECT_NAME_STEALTHMODULE: "CS2-SimpleAdmin_StealthModule" OUTPUT_PATH: "./counterstrikesharp" TMP_PATH: "./tmp" jobs: build: - permissions: write-all runs-on: ubuntu-latest - + permissions: write-all + outputs: + build_version: ${{ steps.get_version.outputs.VERSION }} steps: - - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Restore CS2-SimpleAdmin - run: dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} - - name: Build CS2-SimpleAdmin - run: dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }} - - name: Restore CS2-SimpleAdminApi - run: dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} - - name: Build CS2-SimpleAdminApi - run: dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }} + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Get Version + id: get_version + run: echo "VERSION=$(cat CS2-SimpleAdmin/VERSION)" >> $GITHUB_OUTPUT + + - name: Restore & Build All Projects + run: | + dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} + dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }} + dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} + dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }} + dotnet restore ${{ env.PROJECT_PATH_STEALTHMODULE }} + dotnet build ${{ env.PROJECT_PATH_STEALTHMODULE }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_STEALTHMODULE }} + + - name: Combine projects + run: | + mkdir -p ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin + mkdir -p ${{ env.OUTPUT_PATH }}/shared/CS2-SimpleAdminApi + mkdir -p ${{ env.OUTPUT_PATH }}/shared/CS2-SimpleAdminApi + + cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/* ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin/ + cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_STEALTHMODULE }}/* ${{ env.OUTPUT_PATH }}/plugins/CS2-SimpleAdmin_StealthModule + cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/* ${{ env.OUTPUT_PATH }}/shared/CS2-SimpleAdminApi/ + + - name: Zip Main Build Output + run: zip -r CS2-SimpleAdmin${{ steps.get_version.outputs.VERSION }}.zip ${{ env.OUTPUT_PATH }} + + - name: Extract & Zip StatusBlocker Linux + run: | + mkdir -p statusblocker-linux && + tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-linux.tar.gz -C statusblocker-linux && + cd statusblocker-linux && + zip -r ../StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip ./* + + - name: Extract & Zip StatusBlocker Windows + run: | + mkdir -p statusblocker-windows && + tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-windows.tar.gz -C statusblocker-windows && + cd statusblocker-windows && + zip -r ../StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip ./* + + - name: Upload all artifacts + uses: actions/upload-artifact@v4 + with: + name: CS2-SimpleAdmin-Build-Artifacts + path: | + CS2-SimpleAdmin${{ steps.get_version.outputs.VERSION }}.zip + StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip + StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip publish: - if: github.event_name == 'push' - permissions: write-all - runs-on: ubuntu-latest needs: build + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: write-all steps: - - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Restore CS2-SimpleAdmin - run: dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} - - name: Build CS2-SimpleAdmin - run: dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMIN }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }} - - name: Restore CS2-SimpleAdminApi - run: dotnet restore ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} - - name: Build CS2-SimpleAdminApi - run: dotnet build ${{ env.PROJECT_PATH_CS2_SIMPLEADMINAPI }} -c Release -o ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }} - - name: Clean files - run: | - rm -f \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/CounterStrikeSharp.API.dll \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/McMaster.NETCore.Plugins.dll \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/Microsoft.DotNet.PlatformAbstractions.dll \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/Microsoft.Extensions.DependencyModel.dll \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/CS2-SimpleAdminApi.* \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}/Microsoft.* \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/CounterStrikeSharp.API.dll \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/McMaster.NETCore.Plugins.dll \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/Microsoft.DotNet.PlatformAbstractions.dll \ - ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }}/Microsoft.Extensions.DependencyModel.dll - - name: Combine projects - run: | - mkdir -p ${{ env.OUTPUT_PATH }}/plugins - mkdir -p ${{ env.OUTPUT_PATH }}/shared - cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }} ${{ env.OUTPUT_PATH }}/plugins/ - cp -r ${{ env.TMP_PATH }}/${{ env.PROJECT_NAME_CS2_SIMPLEADMINAPI }} ${{ env.OUTPUT_PATH }}/shared/ - - name: Zip combined - uses: thedoctor0/zip-release@0.7.6 - with: - type: 'zip' - filename: '${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}-${{ env.BUILD_NUMBER }}.zip' - path: ${{ env.OUTPUT_PATH }} - - name: Publish combined release - uses: ncipollo/release-action@v1.14.0 - with: - artifacts: "${{ env.PROJECT_NAME_CS2_SIMPLEADMIN }}-${{ env.BUILD_NUMBER }}.zip" - name: "CS2-SimpleAdmin - Build ${{ env.BUILD_NUMBER }}" - tag: "build-${{ env.BUILD_NUMBER }}" - body: | - Place files in addons/counterstrikesharp - After first launch, configure the plugins in the respective configs: - - CS2-SimpleAdmin: addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json + - uses: actions/checkout@v4 + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: CS2-SimpleAdmin-Build-Artifacts + path: . + - name: Unzip main build artifact + run: unzip CS2-SimpleAdmin${{ needs.build.outputs.build_version }}.zip -d ./counterstrikesharp + - name: Publish combined release + uses: ncipollo/release-action@v1.14.0 + with: + artifacts: | + CS2-SimpleAdmin${{ needs.build.outputs.build_version }}.zip + StatusBlocker-linux-${{ needs.build.outputs.build_version }}.zip + StatusBlocker-windows-${{ needs.build.outputs.build_version }}.zip + name: "CS2-SimpleAdmin Build #${{ needs.build.outputs.build_version }}" + tag: "build-${{ needs.build.outputs.build_version }}" + body: | + Place the files in your server as follows: + + - CS2-SimpleAdmin: + Place the files inside the addons/counterstrikesharp directory. + After the first launch, configure the plugin using the JSON config file at: + addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json + + - StatusBlocker: + Place the plugin files directly into the addons directory. + This plugin is a Metamod module for the StealthModule and does not require a subfolder. + + Remember to restart or reload your game server after installing and configuring the plugins. diff --git a/.gitignore b/.gitignore index 79b4f27..8eebf6c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ obj/ Modules/CS2-SimpleAdmin_PlayTimeModule CS2-SimpleAdmin.sln.DotSettings.user Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.sln.DotSettings.user +CS2-SimpleAdmin_BanSoundModule — kopia +*.user diff --git a/CS2-SimpleAdmin/3rd_party/MenuManagerApi — old.dll b/CS2-SimpleAdmin/3rd_party/MenuManagerApi — old.dll deleted file mode 100644 index c4f723a..0000000 Binary files a/CS2-SimpleAdmin/3rd_party/MenuManagerApi — old.dll and /dev/null differ diff --git a/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs b/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs index 4729e28..56c1b7f 100644 --- a/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs +++ b/CS2-SimpleAdmin/Api/CS2_SimpleAdminApi.cs @@ -1,4 +1,5 @@ using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Entities; using CS2_SimpleAdmin.Managers; @@ -13,7 +14,7 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi if (!player.UserId.HasValue) throw new KeyNotFoundException("Player with specific UserId not found"); - return CS2_SimpleAdmin.PlayersInfo[player.UserId.Value]; + return CS2_SimpleAdmin.PlayersInfo[player.SteamID]; } public string GetConnectionString() => CS2_SimpleAdmin.Instance.DbConnectionString; @@ -28,6 +29,7 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi public event Action? OnPlayerPenaltied; public event Action? OnPlayerPenaltiedAdded; public event Action? OnAdminShowActivity; + public event Action? OnAdminToggleSilent; public void OnPlayerPenaltiedEvent(PlayerInfo player, PlayerInfo? admin, PenaltyType penaltyType, string reason, int duration, int? penaltyId) => OnPlayerPenaltied?.Invoke(player, admin, penaltyType, reason, duration, penaltyId, CS2_SimpleAdmin.ServerId); @@ -37,6 +39,8 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi 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) @@ -123,8 +127,42 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi public bool IsAdminSilent(CCSPlayerController player) { return CS2_SimpleAdmin.SilentPlayers.Contains(player.Slot); + } + + public HashSet 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(); + 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 void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs) { Helper.ShowAdminActivity(messageKey, callerName, dontPublish, messageArgs); diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 2a9a98e..bdcecbc 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -1,9 +1,12 @@ -using CounterStrikeSharp.API.Core; +using System.Reflection; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Capabilities; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Commands.Targeting; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; +using CS2_SimpleAdmin.Database; using CS2_SimpleAdmin.Managers; using CS2_SimpleAdminApi; using Microsoft.Extensions.Logging; @@ -19,47 +22,55 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; public override string ModuleAuthor => "daffyy & Dliix66"; - public override string ModuleVersion => "1.7.7-alpha"; + public override string ModuleVersion => "1.7.7-alpha-9"; public override void Load(bool hotReload) { Instance = this; - - RegisterEvents(); - + if (hotReload) { ServerLoaded = false; _serverLoading = false; + CacheManager?.Dispose(); CacheManager = new CacheManager(); - OnGameServerSteamAPIActivated(); + + // OnGameServerSteamAPIActivated(); OnMapStart(string.Empty); - AddTimer(2.0f, () => + AddTimer(6.0f, () => { - if (Database == null) return; + if (DatabaseProvider == null) return; - var playerManager = new PlayerManager(); - - foreach (var player in Helper.GetValidPlayers()) + PlayersInfo.Clear(); + CachedPlayers.Clear(); + BotPlayers.Clear(); + + foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsHLTV).ToArray()) { - playerManager.LoadPlayerData(player); + if (!player.IsBot) + PlayerManager.LoadPlayerData(player, true); + else + BotPlayers.Add(player); }; }); + + PlayersTimer?.Kill(); + PlayersTimer = null; } _cBasePlayerControllerSetPawnFunc = new MemoryFunctionVoid(GameData.GetSignature("CBasePlayerController_SetPawn")); SimpleAdminApi = new Api.CS2_SimpleAdminApi(); Capabilities.RegisterPluginCapability(ICS2_SimpleAdminApi.PluginCapability, () => SimpleAdminApi); - new PlayerManager().CheckPlayersTimer(); + PlayersTimer?.Kill(); + PlayersTimer = null; + PlayerManager.CheckPlayersTimer(); } public override void OnAllPluginsLoaded(bool hotReload) { - AddTimer(5.0f, () => ReloadAdmins(null)); - try { MenuApi = MenuCapability.Get(); @@ -67,51 +78,141 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig ReloadAdmins(null)); + RegisterEvents(); 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" + ); + } } public void OnConfigParsed(CS2_SimpleAdminConfig config) { - Instance = this; - _logger = Logger; + if (System.Diagnostics.Debugger.IsAttached) + Environment.FailFast(":(!"); - if (config.DatabaseHost.Length < 1 || config.DatabaseName.Length < 1 || config.DatabaseUser.Length < 1) + Helper.UpdateConfig(config); + + _logger = Logger; + Config = config; + + bool missing = false; + var cssPath = Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp"); + var pluginsPath = Path.Combine(cssPath, "plugins"); + var sharedPath = Path.Combine(cssPath, "shared"); + + foreach (var plugin in _requiredPlugins) { - throw new Exception("[CS2-SimpleAdmin] You need to setup Database credentials in config!"); + var pluginDirPath = Path.Combine(pluginsPath, plugin); + var pluginDllPath = Path.Combine(pluginDirPath, $"{plugin}.dll"); + + if (!Directory.Exists(pluginDirPath)) + { + _logger?.LogError( + $"❌ Plugin directory '{plugin}' missing at: {pluginDirPath}" + ); + missing = true; + } + + if (!File.Exists(pluginDllPath)) + { + _logger?.LogError( + $"❌ Plugin DLL '{plugin}.dll' missing at: {pluginDllPath}" + ); + missing = true; + } + } + + foreach (var shared in _requiredShared) + { + var sharedDirPath = Path.Combine(sharedPath, shared); + var sharedDllPath = Path.Combine(sharedDirPath, $"{shared}.dll"); + + if (!Directory.Exists(sharedDirPath)) + { + _logger?.LogError( + $"❌ Shared library directory '{shared}' missing at: {sharedDirPath}" + ); + missing = true; + } + + if (!File.Exists(sharedDllPath)) + { + _logger?.LogError( + $"❌ Shared library DLL '{shared}.dll' missing at: {sharedDllPath}" + ); + missing = true; + } } - MySqlConnectionStringBuilder builder = new() + if (missing) + Server.ExecuteCommand($"css_plugins unload {ModuleName}"); + + Instance = this; + + if (Config.DatabaseConfig.DatabaseType.Contains("mysql", StringComparison.CurrentCultureIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseHost) || + string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseName) || + string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseUser)) { - Server = config.DatabaseHost, - Database = config.DatabaseName, - UserID = config.DatabaseUser, - Password = config.DatabasePassword, - Port = (uint)config.DatabasePort, - SslMode = Enum.TryParse(config.DatabaseSSlMode, true, out MySqlSslMode sslMode) ? sslMode : MySqlSslMode.Preferred, + throw new Exception("[CS2-SimpleAdmin] You need to setup MySQL credentials in config!"); + } + + var builder = new MySqlConnectionStringBuilder() + { + Server = config.DatabaseConfig.DatabaseHost, + Database = config.DatabaseConfig.DatabaseName, + UserID = config.DatabaseConfig.DatabaseUser, + Password = config.DatabaseConfig.DatabasePassword, + Port = (uint)config.DatabaseConfig.DatabasePort, + SslMode = Enum.TryParse(config.DatabaseConfig.DatabaseSSlMode, true, out MySqlSslMode sslMode) + ? sslMode + : MySqlSslMode.Preferred, Pooling = true, - MinimumPoolSize = 0, - MaximumPoolSize = 640, }; DbConnectionString = builder.ConnectionString; - Database = new Database.Database(DbConnectionString); - - if (!Database.CheckDatabaseConnection(out var exception)) + DatabaseProvider = new MySqlDatabaseProvider(DbConnectionString); + } + else + { + if (string.IsNullOrWhiteSpace(config.DatabaseConfig.SqliteFilePath)) { - if (exception != null) - Logger.LogError("Problem with database connection! \n{exception}", exception); - - Unload(false); - return; + throw new Exception("[CS2-SimpleAdmin] You need to specify SQLite file path in config!"); } - - Task.Run(() => Database.DatabaseMigration()); - Config = config; - Helper.UpdateConfig(config); + DatabaseProvider = new SqliteDatabaseProvider(ModuleDirectory + "/" + config.DatabaseConfig.SqliteFilePath); + } + var (success, exception) = Task.Run(() => DatabaseProvider.CheckConnectionAsync()).GetAwaiter().GetResult(); + if (!success) + { + if (exception != null) + Logger.LogError("Problem with database connection! \n{exception}", exception); + + Unload(false); + return; + } + + Task.Run(() => DatabaseProvider.DatabaseMigrationAsync()); + if (!Directory.Exists(ModuleDirectory + "/data")) { Directory.CreateDirectory(ModuleDirectory + "/data"); @@ -122,33 +223,32 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig await PluginInfo.CheckVersion(ModuleVersion, _logger)); + Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, Logger)); - PermissionManager = new PermissionManager(Database); - BanManager = new BanManager(Database); - MuteManager = new MuteManager(Database); - WarnManager = new WarnManager(Database); + PermissionManager = new PermissionManager(DatabaseProvider); + BanManager = new BanManager(DatabaseProvider); + MuteManager = new MuteManager(DatabaseProvider); + WarnManager = new WarnManager(DatabaseProvider); } - private static TargetResult? GetTarget(CommandInfo command) + private static TargetResult? GetTarget(CommandInfo command, int argument = 1) { - var matches = command.GetArgTargetResult(1); + var matches = command.GetArgTargetResult(argument); if (!matches.Any()) { - command.ReplyToCommand($"Target {command.GetArg(1)} not found."); + command.ReplyToCommand($"Target {command.GetArg(argument)} not found."); return null; } - if (command.GetArg(1).StartsWith('@')) + if (command.GetArg(argument).StartsWith('@')) return matches; if (matches.Count() == 1) return matches; - command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(1)}\"."); + command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(argument)}\"."); return null; } @@ -156,5 +256,13 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfigenable true true + - - - + + none + false + true + false + false + + + + + none + runtime + compile; build; native; contentfiles; analyzers; buildtransitive + + - - + + + + + + + + + + + + + + - - PreserveNewest - - + PreserveNewest - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + PreserveNewest @@ -45,7 +151,8 @@ 3rd_party\MenuManagerApi.dll + False - + diff --git a/CS2-SimpleAdmin/Commands/RegisterCommands.cs b/CS2-SimpleAdmin/Commands/RegisterCommands.cs index 625ad4e..d323e29 100644 --- a/CS2-SimpleAdmin/Commands/RegisterCommands.cs +++ b/CS2-SimpleAdmin/Commands/RegisterCommands.cs @@ -1,89 +1,99 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Commands; using CounterStrikeSharp.API.Modules.Commands; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace CS2_SimpleAdmin; public static class RegisterCommands { + internal static readonly Dictionary> _commandDefinitions = + new(StringComparer.InvariantCultureIgnoreCase); + private delegate void CommandCallback(CCSPlayerController? caller, CommandInfo.CommandCallback callback); private static readonly string CommandsPath = Path.Combine(CS2_SimpleAdmin.ConfigDirectory, "Commands.json"); private static readonly List CommandMappings = [ - new CommandMapping("css_ban", CS2_SimpleAdmin.Instance.OnBanCommand), - new CommandMapping("css_addban", CS2_SimpleAdmin.Instance.OnAddBanCommand), - new CommandMapping("css_banip", CS2_SimpleAdmin.Instance.OnBanIpCommand), - new CommandMapping("css_unban", CS2_SimpleAdmin.Instance.OnUnbanCommand), - new CommandMapping("css_warn", CS2_SimpleAdmin.Instance.OnWarnCommand), - new CommandMapping("css_unwarn", CS2_SimpleAdmin.Instance.OnUnwarnCommand), + new("css_ban", CS2_SimpleAdmin.Instance.OnBanCommand), + new("css_addban", CS2_SimpleAdmin.Instance.OnAddBanCommand), + new("css_banip", CS2_SimpleAdmin.Instance.OnBanIpCommand), + new("css_unban", CS2_SimpleAdmin.Instance.OnUnbanCommand), + new("css_warn", CS2_SimpleAdmin.Instance.OnWarnCommand), + new("css_unwarn", CS2_SimpleAdmin.Instance.OnUnwarnCommand), - new CommandMapping("css_asay", CS2_SimpleAdmin.Instance.OnAdminToAdminSayCommand), - new CommandMapping("css_cssay", CS2_SimpleAdmin.Instance.OnAdminCustomSayCommand), - new CommandMapping("css_say", CS2_SimpleAdmin.Instance.OnAdminSayCommand), - new CommandMapping("css_psay", CS2_SimpleAdmin.Instance.OnAdminPrivateSayCommand), - new CommandMapping("css_csay", CS2_SimpleAdmin.Instance.OnAdminCenterSayCommand), - new CommandMapping("css_hsay", CS2_SimpleAdmin.Instance.OnAdminHudSayCommand), + new("css_asay", CS2_SimpleAdmin.Instance.OnAdminToAdminSayCommand), + new("css_cssay", CS2_SimpleAdmin.Instance.OnAdminCustomSayCommand), + new("css_say", CS2_SimpleAdmin.Instance.OnAdminSayCommand), + new("css_psay", CS2_SimpleAdmin.Instance.OnAdminPrivateSayCommand), + new("css_csay", CS2_SimpleAdmin.Instance.OnAdminCenterSayCommand), + new("css_hsay", CS2_SimpleAdmin.Instance.OnAdminHudSayCommand), - new CommandMapping("css_penalties", CS2_SimpleAdmin.Instance.OnPenaltiesCommand), - new CommandMapping("css_admin", CS2_SimpleAdmin.Instance.OnAdminCommand), - new CommandMapping("css_adminhelp", CS2_SimpleAdmin.Instance.OnAdminHelpCommand), - new CommandMapping("css_addadmin", CS2_SimpleAdmin.Instance.OnAddAdminCommand), - new CommandMapping("css_deladmin", CS2_SimpleAdmin.Instance.OnDelAdminCommand), - new CommandMapping("css_addgroup", CS2_SimpleAdmin.Instance.OnAddGroup), - new CommandMapping("css_delgroup", CS2_SimpleAdmin.Instance.OnDelGroupCommand), - new CommandMapping("css_reloadadmins", CS2_SimpleAdmin.Instance.OnRelAdminCommand), - new CommandMapping("css_reloadbans", CS2_SimpleAdmin.Instance.OnRelBans), - new CommandMapping("css_hide", CS2_SimpleAdmin.Instance.OnHideCommand), - new CommandMapping("css_hidecomms", CS2_SimpleAdmin.Instance.OnHideCommsCommand), - new CommandMapping("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand), - new CommandMapping("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand), - new CommandMapping("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand), - new CommandMapping("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand), - new CommandMapping("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand), - new CommandMapping("css_map", CS2_SimpleAdmin.Instance.OnMapCommand), - new CommandMapping("css_wsmap", CS2_SimpleAdmin.Instance.OnWorkshopMapCommand), - new CommandMapping("css_cvar", CS2_SimpleAdmin.Instance.OnCvarCommand), - new CommandMapping("css_rcon", CS2_SimpleAdmin.Instance.OnRconCommand), - new CommandMapping("css_rr", CS2_SimpleAdmin.Instance.OnRestartCommand), + new("css_penalties", CS2_SimpleAdmin.Instance.OnPenaltiesCommand), + new("css_admin", CS2_SimpleAdmin.Instance.OnAdminCommand), + new("css_adminhelp", CS2_SimpleAdmin.Instance.OnAdminHelpCommand), + new("css_addadmin", CS2_SimpleAdmin.Instance.OnAddAdminCommand), + new("css_deladmin", CS2_SimpleAdmin.Instance.OnDelAdminCommand), + new("css_addgroup", CS2_SimpleAdmin.Instance.OnAddGroup), + new("css_delgroup", CS2_SimpleAdmin.Instance.OnDelGroupCommand), + new("css_reloadadmins", CS2_SimpleAdmin.Instance.OnRelAdminCommand), + new("css_reloadbans", CS2_SimpleAdmin.Instance.OnRelBans), + new("css_hide", CS2_SimpleAdmin.Instance.OnHideCommand), + new("css_hidecomms", CS2_SimpleAdmin.Instance.OnHideCommsCommand), + new("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand), + new("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand), + new("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand), + new("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand), + new("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand), + new("css_map", CS2_SimpleAdmin.Instance.OnMapCommand), + new("css_wsmap", CS2_SimpleAdmin.Instance.OnWorkshopMapCommand), + new("css_cvar", CS2_SimpleAdmin.Instance.OnCvarCommand), + new("css_rcon", CS2_SimpleAdmin.Instance.OnRconCommand), + new("css_rr", CS2_SimpleAdmin.Instance.OnRestartCommand), - new CommandMapping("css_gag", CS2_SimpleAdmin.Instance.OnGagCommand), - new CommandMapping("css_addgag", CS2_SimpleAdmin.Instance.OnAddGagCommand), - new CommandMapping("css_ungag", CS2_SimpleAdmin.Instance.OnUngagCommand), - new CommandMapping("css_mute", CS2_SimpleAdmin.Instance.OnMuteCommand), - new CommandMapping("css_addmute", CS2_SimpleAdmin.Instance.OnAddMuteCommand), - new CommandMapping("css_unmute", CS2_SimpleAdmin.Instance.OnUnmuteCommand), - new CommandMapping("css_silence", CS2_SimpleAdmin.Instance.OnSilenceCommand), - new CommandMapping("css_addsilence", CS2_SimpleAdmin.Instance.OnAddSilenceCommand), - new CommandMapping("css_unsilence", CS2_SimpleAdmin.Instance.OnUnsilenceCommand), + new("css_gag", CS2_SimpleAdmin.Instance.OnGagCommand), + new("css_addgag", CS2_SimpleAdmin.Instance.OnAddGagCommand), + new("css_ungag", CS2_SimpleAdmin.Instance.OnUngagCommand), + new("css_mute", CS2_SimpleAdmin.Instance.OnMuteCommand), + new("css_addmute", CS2_SimpleAdmin.Instance.OnAddMuteCommand), + new("css_unmute", CS2_SimpleAdmin.Instance.OnUnmuteCommand), + new("css_silence", CS2_SimpleAdmin.Instance.OnSilenceCommand), + new("css_addsilence", CS2_SimpleAdmin.Instance.OnAddSilenceCommand), + new("css_unsilence", CS2_SimpleAdmin.Instance.OnUnsilenceCommand), - new CommandMapping("css_vote", CS2_SimpleAdmin.Instance.OnVoteCommand), + new("css_vote", CS2_SimpleAdmin.Instance.OnVoteCommand), - new CommandMapping("css_noclip", CS2_SimpleAdmin.Instance.OnNoclipCommand), - new CommandMapping("css_freeze", CS2_SimpleAdmin.Instance.OnFreezeCommand), - new CommandMapping("css_unfreeze", CS2_SimpleAdmin.Instance.OnUnfreezeCommand), - new CommandMapping("css_godmode", CS2_SimpleAdmin.Instance.OnGodCommand), + new("css_noclip", CS2_SimpleAdmin.Instance.OnNoclipCommand), + new("css_freeze", CS2_SimpleAdmin.Instance.OnFreezeCommand), + new("css_unfreeze", CS2_SimpleAdmin.Instance.OnUnfreezeCommand), + new("css_godmode", CS2_SimpleAdmin.Instance.OnGodCommand), - new CommandMapping("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand), - new CommandMapping("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand), - new CommandMapping("css_give", CS2_SimpleAdmin.Instance.OnGiveCommand), - new CommandMapping("css_strip", CS2_SimpleAdmin.Instance.OnStripCommand), - new CommandMapping("css_hp", CS2_SimpleAdmin.Instance.OnHpCommand), - new CommandMapping("css_speed", CS2_SimpleAdmin.Instance.OnSpeedCommand), - new CommandMapping("css_gravity", CS2_SimpleAdmin.Instance.OnGravityCommand), - new CommandMapping("css_resize", CS2_SimpleAdmin.Instance.OnResizeCommand), - new CommandMapping("css_money", CS2_SimpleAdmin.Instance.OnMoneyCommand), - new CommandMapping("css_team", CS2_SimpleAdmin.Instance.OnTeamCommand), - new CommandMapping("css_rename", CS2_SimpleAdmin.Instance.OnRenameCommand), - new CommandMapping("css_prename", CS2_SimpleAdmin.Instance.OnPrenameCommand), - new CommandMapping("css_respawn", CS2_SimpleAdmin.Instance.OnRespawnCommand), - new CommandMapping("css_tp", CS2_SimpleAdmin.Instance.OnGotoCommand), - new CommandMapping("css_bring", CS2_SimpleAdmin.Instance.OnBringCommand), - new CommandMapping("css_pluginsmanager", CS2_SimpleAdmin.Instance.OnPluginManagerCommand), - new CommandMapping("css_adminvoice", CS2_SimpleAdmin.Instance.OnAdminVoiceCommand) + new("css_slay", CS2_SimpleAdmin.Instance.OnSlayCommand), + new("css_slap", CS2_SimpleAdmin.Instance.OnSlapCommand), + new("css_give", CS2_SimpleAdmin.Instance.OnGiveCommand), + new("css_strip", CS2_SimpleAdmin.Instance.OnStripCommand), + new("css_hp", CS2_SimpleAdmin.Instance.OnHpCommand), + new("css_speed", CS2_SimpleAdmin.Instance.OnSpeedCommand), + new("css_gravity", CS2_SimpleAdmin.Instance.OnGravityCommand), + new("css_resize", CS2_SimpleAdmin.Instance.OnResizeCommand), + new("css_money", CS2_SimpleAdmin.Instance.OnMoneyCommand), + new("css_team", CS2_SimpleAdmin.Instance.OnTeamCommand), + new("css_rename", CS2_SimpleAdmin.Instance.OnRenameCommand), + new("css_prename", CS2_SimpleAdmin.Instance.OnPrenameCommand), + new("css_respawn", CS2_SimpleAdmin.Instance.OnRespawnCommand), + new("css_tp", CS2_SimpleAdmin.Instance.OnGotoCommand), + new("css_bring", CS2_SimpleAdmin.Instance.OnBringCommand), + new("css_pluginsmanager", CS2_SimpleAdmin.Instance.OnPluginManagerCommand), + new("css_adminvoice", CS2_SimpleAdmin.Instance.OnAdminVoiceCommand) ]; + /// + /// 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. + /// public static void InitializeCommands() { if (!File.Exists(CommandsPath)) @@ -97,6 +107,10 @@ public static class RegisterCommands } } + /// + /// Creates the default commands configuration JSON file with built-in commands and aliases. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] private static void CreateConfig() { var commands = new CommandsConfig @@ -170,14 +184,26 @@ public static class RegisterCommands } }; - 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); } - + + /// + /// Reads the command configuration JSON file and registers all commands and their aliases with their callbacks. + /// Also registers any custom commands previously stored. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] private static void Register() { var json = File.ReadAllText(CommandsPath); - var commandsConfig = JsonConvert.DeserializeObject(json); + var commandsConfig = JsonSerializer.Deserialize(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (commandsConfig?.Commands == null) return; @@ -195,19 +221,37 @@ public static class RegisterCommands { CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback); } - } + } + + foreach (var (name, definitions) in RegisterCommands._commandDefinitions) + { + foreach (var definition in definitions) + { + CS2_SimpleAdmin._logger?.LogInformation($"Registering custom command: `{name}`"); + CS2_SimpleAdmin.Instance.AddCommand(name, definition.Description, definition.Callback); + } + } } + /// + /// Represents the JSON configuration structure for commands. + /// private class CommandsConfig { public Dictionary? Commands { get; init; } } + /// + /// Represents a command definition containing a list of aliases. + /// private class Command { public string[]? Aliases { get; init; } } + /// + /// Maps a command key to its respective command callback handler. + /// private class CommandMapping(string commandKey, CommandInfo.CommandCallback callback) { public string CommandKey { get; } = commandKey; diff --git a/CS2-SimpleAdmin/Commands/basebans.cs b/CS2-SimpleAdmin/Commands/basebans.cs index f71e757..7b15b55 100644 --- a/CS2-SimpleAdmin/Commands/basebans.cs +++ b/CS2-SimpleAdmin/Commands/basebans.cs @@ -12,6 +12,11 @@ namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { + /// + /// Handles the 'ban' command, allowing admins to ban one or more valid connected players. + /// + /// The player issuing the ban command, or null for console. + /// The command information including arguments. [RequiresPermissions("@css/ban")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnBanCommand(CCSPlayerController? caller, CommandInfo command) @@ -51,9 +56,20 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Core logic to ban a specific player, scheduling database updates, notifications, and kicks. + /// + /// The player issuing the ban, or null for console. + /// The player to be banned. + /// Ban duration in minutes; 0 means permanent. + /// Reason for the ban. + /// Optional caller name string. If null, defaults to player name or console. + /// Optional BanManager to handle ban persistence. + /// Optional command info object for logging. + /// If true, suppresses command logging. internal void Ban(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, BanManager? banManager = null, CommandInfo? command = null, bool silent = false) { - if (Database == null || !player.IsValid || !player.UserId.HasValue) return; + if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return; if (!caller.CanTarget(player)) return; if (!CheckValidBan(caller, time)) return; @@ -63,14 +79,17 @@ public partial class CS2_SimpleAdmin : (_localizer?["sa_console"] ?? "Console"); // Get player and admin information - var playerInfo = PlayersInfo[player.UserId.Value]; - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var playerInfo = PlayersInfo[player.SteamID]; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Asynchronously handle banning logic Task.Run(async () => { int? penaltyId = await BanManager.BanPlayer(playerInfo, adminInfo, reason, time); - SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Ban, reason, time, penaltyId); + await Server.NextWorldUpdateAsync(() => + { + SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Ban, reason, time, penaltyId); + }); }); // Determine message keys and arguments based on ban time @@ -114,6 +133,14 @@ public partial class CS2_SimpleAdmin Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Ban, _localizer); } + /// + /// Adds a ban for a player by their SteamID, including offline bans. + /// + /// The player issuing the ban command. + /// SteamID of the player to ban. + /// Ban duration in minutes (0 means permanent). + /// Reason for banning. + /// Optional ban manager for database operations. internal void AddBan(CCSPlayerController? caller, SteamID steamid, int time, string reason, BanManager? banManager = null) { // Set default caller name if not provided @@ -121,15 +148,12 @@ public partial class CS2_SimpleAdmin ? caller.PlayerName : (_localizer?["sa_console"] ?? "Console"); - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; - - var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64.ToString()); - + 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}."); } @@ -137,23 +161,31 @@ public partial class CS2_SimpleAdmin { 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.ToString(), adminInfo, reason, time); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Ban, reason, time, penaltyId); + 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); } } + /// + /// Handles banning a player by specifying their SteamID via command. + /// + /// The player issuing the command, or null if console. + /// Command information including arguments (SteamID, time, reason). [RequiresPermissions("@css/ban")] [CommandHelper(minArgs: 1, usage: " [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console"; if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return; if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null) @@ -162,7 +194,7 @@ public partial class CS2_SimpleAdmin return; } - var steamid = steamId.SteamId64.ToString(); + var steamid = steamId.SteamId64; var reason = command.ArgCount >= 3 ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim() : _localizer?["sa_unknown"] ?? "Unknown"; @@ -170,15 +202,13 @@ public partial class CS2_SimpleAdmin reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason; var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2))); - if (!CheckValidBan(caller, time)) return; var adminInfo = caller != null && caller.UserId.HasValue - ? PlayersInfo[caller.UserId.Value] + ? PlayersInfo[caller.SteamID] : null; var player = Helper.GetPlayerFromSteamid64(steamid); - if (player != null && player.IsValid) { if (!caller.CanTarget(player)) @@ -196,10 +226,14 @@ public partial class CS2_SimpleAdmin Task.Run(async () => { int? penaltyId = await BanManager.AddBanBySteamid(steamid, adminInfo, reason, time); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Ban, reason, time, penaltyId); + 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."); } @@ -210,11 +244,16 @@ public partial class CS2_SimpleAdmin Server.ExecuteCommand($"banid 1 {steamId.SteamId3}"); } + /// + /// Handles banning a player by their IP address, supporting offline banning if player is not online. + /// + /// The player issuing the ban command. + /// The command containing the IP, time, and reason arguments. [RequiresPermissions("@css/ban")] [CommandHelper(minArgs: 1, usage: " [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnBanIpCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console"; if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return; var ipAddress = command.GetArg(1); @@ -236,17 +275,20 @@ public partial class CS2_SimpleAdmin if (!CheckValidBan(caller, time)) return; var adminInfo = caller != null && caller.UserId.HasValue - ? PlayersInfo[caller.UserId.Value] + ? PlayersInfo[caller.SteamID] : null; - var player = Helper.GetPlayerFromIp(ipAddress); - - if (player != null && player.IsValid) + var players = Helper.GetPlayerFromIp(ipAddress); + if (players.Count >= 1) { - if (!caller.CanTarget(player)) - return; + foreach (var player in players) + { + if (player == null || !player.IsValid) continue; + if (!caller.CanTarget(player)) + return; - Ban(caller, player, time, reason, callerName, silent: true); + Ban(caller, player, time, reason, callerName, silent: true); + } } else { @@ -262,6 +304,12 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Checks whether the ban duration is valid based on the caller's permissions and configured limits. + /// + /// The player issuing the ban command. + /// Requested ban duration in minutes. + /// True if ban duration is valid; otherwise, false. private bool CheckValidBan(CCSPlayerController? caller, int duration) { if (caller == null) return true; @@ -280,14 +328,17 @@ public partial class CS2_SimpleAdmin return false; } + /// + /// Handles unbanning players by pattern (steamid, name, or IP). + /// + /// The player issuing the unban command. + /// Command containing target pattern and optional reason. [RequiresPermissions("@css/unban")] [CommandHelper(minArgs: 1, usage: " [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnUnbanCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; - + if (DatabaseProvider == null) return; var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console"; - if (command.GetArg(1).Length <= 1) { command.ReplyToCommand($"Too short pattern to search."); @@ -300,19 +351,21 @@ public partial class CS2_SimpleAdmin : _localizer?["sa_unknown"] ?? "Unknown"; reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason; - Task.Run(async () => await BanManager.UnbanPlayer(pattern, callerSteamId, reason)); - Helper.LogCommand(caller, command); - command.ReplyToCommand($"Unbanned player with pattern {pattern}."); } + /// + /// Handles warning players, supporting multiple targets and warning durations. + /// + /// The player issuing the warn command. + /// The command containing target, time, and reason parameters. [RequiresPermissions("@css/kick")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnWarnCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) + if (DatabaseProvider == null) return; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; if (command.ArgCount < 2) @@ -327,8 +380,6 @@ public partial class CS2_SimpleAdmin return; } - WarnManager warnManager = new(Database); - var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2))); var reason = command.ArgCount >= 3 ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim() @@ -340,14 +391,23 @@ public partial class CS2_SimpleAdmin { 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) + /// + /// Issues a warning penalty to a specific player with optional duration and reason. + /// + /// The player issuing the warning. + /// The player to warn. + /// Duration of the warning in minutes. + /// Reason for the warning. + /// Optional display name of the caller. + /// Optional command info for logging. + internal void Warn(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null) { - if (Database == null || !player.IsValid || !player.UserId.HasValue) return; + if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return; if (!caller.CanTarget(player)) return; if (!CheckValidBan(caller, time)) return; @@ -364,18 +424,21 @@ public partial class CS2_SimpleAdmin } // Get player and admin information - var playerInfo = PlayersInfo[player.UserId.Value]; - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var playerInfo = PlayersInfo[player.SteamID]; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Asynchronously handle warning logic Task.Run(async () => { - warnManager ??= new WarnManager(Database); - int? penaltyId = await warnManager.WarnPlayer(playerInfo, adminInfo, reason, time); - SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Warn, reason, time, penaltyId); + int? penaltyId = await WarnManager.WarnPlayer(playerInfo, adminInfo, reason, time); + await Server.NextWorldUpdateAsync(() => + { + SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Warn, reason, time, + penaltyId); + }); // Check for warn thresholds and execute punish command if applicable - var totalWarns = await warnManager.GetPlayerWarnsCount(player.SteamID.ToString()); + var totalWarns = await WarnManager.GetPlayerWarnsCount(player.SteamID); if (Config.WarnThreshold.Count > 0) { string? punishCommand = null; @@ -388,7 +451,7 @@ public partial class CS2_SimpleAdmin if (!string.IsNullOrEmpty(punishCommand)) { - await Server.NextFrameAsync(() => + await Server.NextWorldUpdateAsync(() => { Server.ExecuteCommand(punishCommand.Replace("USERID", playerInfo.UserId.ToString()).Replace("STEAMID64", playerInfo.SteamId?.ToString())); }); @@ -424,6 +487,14 @@ public partial class CS2_SimpleAdmin Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Warn, _localizer); } + /// + /// Adds a warning to a player by their SteamID, including support for offline players. + /// + /// The player issuing the warn command. + /// SteamID of the player to warn. + /// Warning duration in minutes. + /// Reason for the warning. + /// Optional warn manager instance. internal void AddWarn(CCSPlayerController? caller, SteamID steamid, int time, string reason, WarnManager? warnManager = null) { // Set default caller name if not provided @@ -431,9 +502,9 @@ public partial class CS2_SimpleAdmin ? caller.PlayerName : (_localizer?["sa_console"] ?? "Console"); - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; - var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64.ToString()); + var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64); if (player != null && player.IsValid) { @@ -451,11 +522,15 @@ public partial class CS2_SimpleAdmin // Asynchronous ban operation if player is not online or not found Task.Run(async () => { - int? penaltyId = await WarnManager.AddWarnBySteamid(steamid.SteamId64.ToString(), adminInfo, reason, time); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Warn, reason, time, penaltyId); + 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.ToString()); + var totalWarns = await WarnManager.GetPlayerWarnsCount(steamid.SteamId64); if (Config.WarnThreshold.Count > 0) { string? punishCommand = null; @@ -468,7 +543,7 @@ public partial class CS2_SimpleAdmin if (!string.IsNullOrEmpty(punishCommand)) { - await Server.NextFrameAsync(() => + await Server.NextWorldUpdateAsync(() => { Server.ExecuteCommand(punishCommand.Replace("STEAMID64", steamid.SteamId64.ToString())); }); @@ -480,11 +555,16 @@ public partial class CS2_SimpleAdmin } } + /// + /// Handles removing a warning (unwarn) by a pattern string. + /// + /// The player issuing the unwarn command. + /// The command containing target pattern. [RequiresPermissions("@css/kick")] [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnUnwarnCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; if (command.GetArg(1).Length <= 1) { @@ -493,9 +573,7 @@ public partial class CS2_SimpleAdmin } var pattern = command.GetArg(1); - Task.Run(async () => await WarnManager.UnwarnPlayer(pattern)); - Helper.LogCommand(caller, command); command.ReplyToCommand($"Unwarned player with pattern {pattern}."); } diff --git a/CS2-SimpleAdmin/Commands/basechat.cs b/CS2-SimpleAdmin/Commands/basechat.cs index 58b6c3c..9b0662e 100644 --- a/CS2-SimpleAdmin/Commands/basechat.cs +++ b/CS2-SimpleAdmin/Commands/basechat.cs @@ -11,6 +11,12 @@ namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { + /// + /// Sends a chat message only to admins that have chat permission. + /// The message is encoded properly to handle UTF-8 characters. + /// + /// The admin player sending the message, or null for console. + /// The command input containing the message. [CommandHelper(1, "")] [RequiresPermissions("@css/chat")] public void OnAdminToAdminSayCommand(CCSPlayerController? caller, CommandInfo command) @@ -30,6 +36,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Sends a custom chat message to all players with color tags processed. + /// + /// The admin or console sending the message. + /// The command input containing the message. [CommandHelper(1, "")] [RequiresPermissions("@css/chat")] public void OnAdminCustomSayCommand(CCSPlayerController? caller, CommandInfo command) @@ -47,6 +58,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Sends a chat message to all players with localization prefix and color tags handled. + /// + /// The admin or console sending the message. + /// The command input containing the message. [CommandHelper(1, "")] [RequiresPermissions("@css/chat")] public void OnAdminSayCommand(CCSPlayerController? caller, CommandInfo command) @@ -57,7 +73,6 @@ public partial class CS2_SimpleAdmin var utf8String = Encoding.UTF8.GetString(utf8BytesString); Helper.LogCommand(caller, command); - foreach (var player in Helper.GetValidPlayers()) { player.SendLocalizedMessage(_localizer, @@ -66,6 +81,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Sends a private chat message from the caller to the specified target player(s). + /// + /// The admin or console sending the private message. + /// The command input containing target and message. [CommandHelper(2, "<#userid or name> ")] [RequiresPermissions("@css/chat")] public void OnAdminPrivateSayCommand(CCSPlayerController? caller, CommandInfo command) @@ -92,6 +112,11 @@ public partial class CS2_SimpleAdmin command.ReplyToCommand($" Private message sent!"); } + /// + /// Broadcasts a center-screen message to all players. + /// + /// The admin or console sending the message. + /// The command input containing the message. [CommandHelper(1, "")] [RequiresPermissions("@css/chat")] public void OnAdminCenterSayCommand(CCSPlayerController? caller, CommandInfo command) @@ -103,6 +128,11 @@ public partial class CS2_SimpleAdmin Helper.PrintToCenterAll(utf8String.ReplaceColorTags()); } + /// + /// Sends a HUD alert message to all players. + /// + /// The admin or console sending the message. + /// The command input containing the message. [CommandHelper(1, "")] [RequiresPermissions("@css/chat")] public void OnAdminHudSayCommand(CCSPlayerController? caller, CommandInfo command) @@ -115,6 +145,6 @@ public partial class CS2_SimpleAdmin VirtualFunctions.ClientPrintAll( HudDestination.Alert, utf8String.ReplaceColorTags(), - 0, 0, 0, 0); + 0, 0, 0, 0, 0); } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index aa4b32e..290bbd0 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -12,23 +12,32 @@ using CS2_SimpleAdmin.Managers; using CS2_SimpleAdmin.Menus; using CS2_SimpleAdminApi; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using System.Globalization; using System.Reflection; +using System.Text.Json; using CounterStrikeSharp.API.ValveConstants.Protobuf; +using CS2_SimpleAdmin.Models; using MenuManager; +using System.Diagnostics.CodeAnalysis; namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { + /// + /// Handles the command that shows active penalties and warns for the caller or specified player. + /// Queries warnings and mute status, formats them locally, and sends the result to caller's chat. + /// + /// The player issuing this command. + /// Command input parameters. [CommandHelper(usage: "[#userid or name]", whoCanExecute: CommandUsage.CLIENT_ONLY)] public void OnPenaltiesCommand(CCSPlayerController? caller, CommandInfo command) { - if (caller == null || caller.IsValid == false || !caller.UserId.HasValue || Database == null) + if (caller == null || caller.IsValid == false || !caller.UserId.HasValue || DatabaseProvider == null) return; var userId = caller.UserId.Value; + var steamId = caller.SteamID; if (!string.IsNullOrEmpty(command.GetArg(1)) && AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/kick")) { @@ -51,10 +60,10 @@ public partial class CS2_SimpleAdmin { try { - var warns = await WarnManager.GetPlayerWarns(PlayersInfo[userId], false); + var warns = await WarnManager.GetPlayerWarns(PlayersInfo[steamId], false); // Check if the player is muted - var activeMutes = await MuteManager.IsPlayerMuted(PlayersInfo[userId].SteamId.SteamId64.ToString()); + var activeMutes = await MuteManager.IsPlayerMuted(PlayersInfo[steamId].SteamId.SteamId64.ToString()); Dictionary> mutesList = new() { @@ -119,16 +128,16 @@ public partial class CS2_SimpleAdmin mutesList[PenaltyType.Silence].Add(_localizer["sa_player_penalty_info_no_active_silence"]); } - await Server.NextFrameAsync(() => + await Server.NextWorldUpdateAsync(() => { caller.SendLocalizedMessage(_localizer, "sa_player_penalty_info", [ - PlayersInfo[userId].Name, - PlayersInfo[userId].TotalBans, - PlayersInfo[userId].TotalGags, - PlayersInfo[userId].TotalMutes, - PlayersInfo[userId].TotalSilences, - PlayersInfo[userId].TotalWarns, + PlayersInfo[steamId].Name, + PlayersInfo[steamId].TotalBans, + PlayersInfo[steamId].TotalGags, + PlayersInfo[steamId].TotalMutes, + PlayersInfo[steamId].TotalSilences, + PlayersInfo[steamId].TotalWarns, string.Join("\n", mutesList.SelectMany(kvp => kvp.Value)), string.Join("\n", warnsList) ]); @@ -141,6 +150,12 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Toggles the admin voice listening mode or mutes/unmutes all players' voice. + /// Sends confirmation messages accordingly. + /// + /// The player issuing this command. + /// Command input parameters. [RequiresPermissions("@css/chat")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void OnAdminVoiceCommand(CCSPlayerController? caller, CommandInfo command) @@ -152,6 +167,7 @@ public partial class CS2_SimpleAdmin { if (command.GetArg(2).ToLower().Equals("muteAll")) { + caller.SendLocalizedMessage(_localizer, "sa_admin_voice_mute_all"); foreach (var player in Helper.GetValidPlayers().Where(p => p != caller && !AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat"))) { player.VoiceFlags = VoiceFlags.Muted; @@ -160,19 +176,31 @@ public partial class CS2_SimpleAdmin if (command.GetArg(2).ToLower().Equals("unmuteAll")) { + caller.SendLocalizedMessage(_localizer, "sa_admin_voice_unmute_all"); foreach (var player in Helper.GetValidPlayers().Where(p => p != caller)) { - if (PlayerPenaltyManager.GetPlayerPenalties(player.Slot, PenaltyType.Mute).Count == 0) + if (PlayerPenaltyManager.GetPlayerPenalties(player.Slot, [PenaltyType.Silence, PenaltyType.Mute]).Count == 0) player.VoiceFlags = VoiceFlags.Normal; } } return; } - - caller.VoiceFlags = caller.VoiceFlags == VoiceFlags.All ? VoiceFlags.Normal : VoiceFlags.All; + + var enabled = caller.VoiceFlags.HasFlag(VoiceFlags.ListenAll); + var messageKey = enabled + ? "sa_admin_voice_unlisten_all" + : "sa_admin_voice_listen_all"; + + caller.SendLocalizedMessage(_localizer, messageKey); + caller.VoiceFlags ^= VoiceFlags.ListenAll; } + /// + /// Opens the admin menu for the caller. + /// + /// The player issuing the command. + /// Command input parameters. [RequiresPermissions("@css/generic")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void OnAdminCommand(CCSPlayerController? caller, CommandInfo command) @@ -183,6 +211,12 @@ public partial class CS2_SimpleAdmin AdminMenu.OpenMenu(caller); } + /// + /// Displays admin help text read from a file. + /// Outputs lines one at a time as replies to the command. + /// + /// The player issuing the command. + /// Command input parameters. [RequiresPermissions("@css/generic")] public void OnAdminHelpCommand(CCSPlayerController? caller, CommandInfo command) { @@ -194,12 +228,16 @@ public partial class CS2_SimpleAdmin } } + /// + /// Handles adding a new admin with specified SteamID, name, flags, immunity, and duration. + /// + /// The player issuing the command. + /// Command input parameters. [CommandHelper(minArgs: 4, usage: " ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [RequiresPermissions("@css/root")] public void OnAddAdminCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; - + if (DatabaseProvider == null) return; if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null) { @@ -230,9 +268,20 @@ public partial class CS2_SimpleAdmin AddAdmin(caller, steamid, name, flags, immunity, time, globalAdmin, command); } + /// + /// Adds admin permissions and groups for a player. + /// + /// The player issuing the command. + /// SteamID as string identifying the player. + /// Player's name. + /// Comma-separated admin flags/groups. + /// Admin immunity level. + /// Duration of permission (default 0 = permanent). + /// Whether admin is global. + /// Optional command info for confirmation messages. public static void AddAdmin(CCSPlayerController? caller, string steamid, string name, string flags, int immunity, int time = 0, bool globalAdmin = false, CommandInfo? command = null) { - if (Database == null) return; + if (DatabaseProvider == null) return; var flagsList = flags.Split(',').Select(flag => flag.Trim()).ToList(); _ = Instance.PermissionManager.AddAdminBySteamId(steamid, name, flagsList, immunity, time, globalAdmin); @@ -248,11 +297,16 @@ public partial class CS2_SimpleAdmin Server.PrintToConsole(msg); } + /// + /// Handles removing an admin's flags and groups by SteamID. + /// + /// The player issuing the command. + /// Command input parameters. [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [RequiresPermissions("@css/root")] public void OnDelAdminCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null) { @@ -265,9 +319,16 @@ public partial class CS2_SimpleAdmin RemoveAdmin(caller, steamId.SteamId64.ToString(), globalDelete, command); } + /// + /// Removes admin permissions and groups for a player. + /// + /// The player issuing the command. + /// SteamID as string identifying the player. + /// Whether to delete globally. + /// Optional command info. public void RemoveAdmin(CCSPlayerController? caller, string steamid, bool globalDelete = false, CommandInfo? command = null) { - if (Database == null) return; + if (DatabaseProvider == null) return; _ = PermissionManager.DeleteAdminBySteamId(steamid, globalDelete); AddTimer(2, () => @@ -294,11 +355,16 @@ public partial class CS2_SimpleAdmin Server.PrintToConsole(msg); } + /// + /// Adds a new admin group with specified flags and immunity settings. + /// + /// The player issuing the command. + /// Command input parameters. [CommandHelper(minArgs: 3, usage: " ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [RequiresPermissions("@css/root")] public void OnAddGroup(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; if (!command.GetArg(1).StartsWith("#")) { @@ -320,9 +386,18 @@ public partial class CS2_SimpleAdmin AddGroup(caller, groupName, flags, immunity, globalGroup, command); } + /// + /// Adds a new admin group with specified flags and immunity level. + /// + /// The player issuing the command. + /// Group name (prefix with #). + /// Comma-separated flags/groups string. + /// Immunity level. + /// Whether group is global. + /// Optional command info. private static void AddGroup(CCSPlayerController? caller, string name, string flags, int immunity, bool globalGroup, CommandInfo? command = null) { - if (Database == null) return; + if (DatabaseProvider == null) return; var flagsList = flags.Split(',').Select(flag => flag.Trim()).ToList(); _ = Instance.PermissionManager.AddGroup(name, flagsList, immunity, globalGroup); @@ -338,11 +413,16 @@ public partial class CS2_SimpleAdmin Server.PrintToConsole(msg); } + /// + /// Handles removing a group by name. + /// + /// The player issuing the command. + /// Command input parameters. [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [RequiresPermissions("@css/root")] public void OnDelGroupCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; if (!command.GetArg(1).StartsWith($"#")) { @@ -355,9 +435,15 @@ public partial class CS2_SimpleAdmin RemoveGroup(caller, groupName, command); } + /// + /// Removes a group. + /// + /// The player issuing the command. + /// The group name to remove. + /// Optional command info for confirmation. private void RemoveGroup(CCSPlayerController? caller, string name, CommandInfo? command = null) { - if (Database == null) return; + if (DatabaseProvider == null) return; _ = PermissionManager.DeleteGroup(name); AddTimer(2, () => @@ -376,30 +462,42 @@ public partial class CS2_SimpleAdmin Server.PrintToConsole(msg); } + /// + /// Reloads admin and group data from database and json files. + /// + /// The player issuing the reload command. + /// Command input parameters. [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [RequiresPermissions("@css/root")] public void OnRelAdminCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; - + if (DatabaseProvider == null) return; ReloadAdmins(caller); - command.ReplyToCommand("Reloaded sql admins and groups"); } + /// + /// Reloads bans cache. + /// + /// The player issuing the reload command. + /// Command input parameters. [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [RequiresPermissions("@css/root")] public void OnRelBans(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; _ = Instance.CacheManager?.ForceReInitializeCacheAsync(); command.ReplyToCommand("Reloaded bans"); } + /// + /// Reloads admin data asynchronously and updates admin caches. + /// + /// The player issuing the reload command. public void ReloadAdmins(CCSPlayerController? caller) { - if (Database == null) return; + if (DatabaseProvider == null) return; Task.Run(async () => { @@ -411,14 +509,17 @@ public partial class CS2_SimpleAdmin await Server.NextWorldUpdateAsync(() => { - if (!string.IsNullOrEmpty(adminsFile)) - AddTimer(1.3f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); - if (!string.IsNullOrEmpty(groupsFile)) - AddTimer(2.5f, () => AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json")); - if (!string.IsNullOrEmpty(adminsFile)) - AddTimer(3.5f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); + AddTimer(1, () => + { + if (!string.IsNullOrEmpty(adminsFile)) + AddTimer(2.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); + if (!string.IsNullOrEmpty(groupsFile)) + AddTimer(3.0f, () => AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json")); + if (!string.IsNullOrEmpty(adminsFile)) + AddTimer(4.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); - _logger?.LogInformation("Loaded admins!"); + _logger?.LogInformation("Loaded admins!"); + }); }); }); @@ -426,6 +527,11 @@ public partial class CS2_SimpleAdmin //_ = _adminManager.GiveAllFlags(); } + /// + /// Toggles player visibility on the server, hiding or revealing them. + /// + /// The player issuing the hide command. + /// Command input parameters. [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] [RequiresPermissions("@css/kick")] public void OnHideCommand(CCSPlayerController? caller, CommandInfo command) @@ -438,7 +544,9 @@ public partial class CS2_SimpleAdmin { SilentPlayers.Remove(caller.Slot); caller.PrintToChat($"You aren't hidden now!"); - caller.ChangeTeam(CsTeam.Spectator); + if (caller.TeamNum <= 1) + caller.ChangeTeam(CsTeam.Spectator); + SimpleAdminApi?.OnAdminToggleSilentEvent(caller.Slot, false); } else { @@ -447,13 +555,20 @@ public partial class CS2_SimpleAdmin if (caller.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE) caller.PlayerPawn.Value?.CommitSuicide(true, false); - AddTimer(1.0f, () => { Server.NextFrame(() => caller.ChangeTeam(CsTeam.Spectator)); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); - AddTimer(1.4f, () => { Server.NextFrame(() => caller.ChangeTeam(CsTeam.None)); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); caller.PrintToChat($"You are hidden now!"); - AddTimer(2.0f, () => { Server.NextFrame(() => Server.ExecuteCommand("sv_disable_teamselect_menu 0")); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + if (caller.TeamNum > 1) + AddTimer(0.15f, () => { Server.NextWorldUpdate(() => caller.ChangeTeam(CsTeam.Spectator)); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + AddTimer(0.26f, () => { Server.NextWorldUpdate(() => caller.ChangeTeam(CsTeam.None)); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + AddTimer(0.50f, () => { Server.NextWorldUpdate(() => Server.ExecuteCommand("sv_disable_teamselect_menu 0")); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + SimpleAdminApi?.OnAdminToggleSilentEvent(caller.Slot, true); } } + /// + /// Toggles penalty notification visibility to admins. + /// + /// The player toggling notification visibility. + /// Command input parameters. [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] [RequiresPermissions("@css/kick")] public void OnHideCommsCommand(CCSPlayerController? caller, CommandInfo command) @@ -473,11 +588,16 @@ public partial class CS2_SimpleAdmin } } + /// + /// Displays detailed information about target players, including admin groups, permissions, and penalties. + /// + /// The player issuing the command. + /// Command input parameters including targets. [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [RequiresPermissions("@css/generic")] public void OnWhoCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var targets = GetTarget(command); if (targets == null) return; @@ -491,39 +611,41 @@ public partial class CS2_SimpleAdmin if (!player.UserId.HasValue) return; if (!caller!.CanTarget(player)) return; - var playerInfo = PlayersInfo[player.UserId.Value]; + var playerInfo = PlayersInfo[player.SteamID]; Task.Run(async () => { - await Server.NextFrameAsync(() => + await Server.NextWorldUpdateAsync(() => { Action printMethod = caller == null ? Server.PrintToConsole : caller.PrintToConsole; - + + var adminData = AdminManager.GetPlayerAdminData(new SteamID(player.SteamID)); + printMethod($"--------- INFO ABOUT \"{playerInfo.Name}\" ---------"); - printMethod($"• Clan: \"{player.Clan}\" Name: \"{playerInfo.Name}\""); printMethod($"• UserID: \"{playerInfo.UserId}\""); printMethod($"• SteamID64: \"{playerInfo.SteamId.SteamId64}\""); - if (player.Connected == PlayerConnectedState.PlayerConnected) + if (adminData != null) { - printMethod($"• SteamID2: \"{playerInfo.SteamId.SteamId2}\""); - printMethod($"• Community link: \"{playerInfo.SteamId.ToCommunityUrl()}\""); + var flags = string.Join(",", adminData._flags); + var groups = string.Join(",", adminData.Groups); + + printMethod($"• Groups/Flags: \"{groups}{flags}\""); } + printMethod($"• SteamID2: \"{playerInfo.SteamId.SteamId2}\""); + printMethod($"• Community link: \"{playerInfo.SteamId.ToCommunityUrl()}\""); if (playerInfo.IpAddress != null && AdminManager.PlayerHasPermissions(new SteamID(caller!.SteamID), "@css/showip")) printMethod($"• IP Address: \"{playerInfo.IpAddress}\""); printMethod($"• Ping: \"{player.Ping}\""); - if (player.Connected == PlayerConnectedState.PlayerConnected) - { - printMethod($"• Total Bans: \"{playerInfo.TotalBans}\""); - printMethod($"• Total Gags: \"{playerInfo.TotalGags}\""); - printMethod($"• Total Mutes: \"{playerInfo.TotalMutes}\""); - printMethod($"• Total Silences: \"{playerInfo.TotalSilences}\""); - printMethod($"• Total Warns: \"{playerInfo.TotalWarns}\""); + printMethod($"• Total Bans: \"{playerInfo.TotalBans}\""); + printMethod($"• Total Gags: \"{playerInfo.TotalGags}\""); + printMethod($"• Total Mutes: \"{playerInfo.TotalMutes}\""); + printMethod($"• Total Silences: \"{playerInfo.TotalSilences}\""); + printMethod($"• Total Warns: \"{playerInfo.TotalWarns}\""); - var chunkedAccounts = playerInfo.AccountsAssociated.ChunkBy(3).ToList(); - foreach (var chunk in chunkedAccounts) - printMethod($"• Associated Accounts: \"{string.Join(", ", chunk.Select(a => $"{a.PlayerName} ({a.SteamId})"))}\""); - } + var chunkedAccounts = playerInfo.AccountsAssociated.ChunkBy(3).ToList(); + foreach (var chunk in chunkedAccounts) + printMethod($"• Associated Accounts: \"{string.Join(", ", chunk.Select(a => $"{a.PlayerName} ({a.SteamId})"))}\""); printMethod($"--------- END INFO ABOUT \"{player.PlayerName}\" ---------"); }); @@ -531,6 +653,11 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Displays a menu with disconnected players, allowing the caller to apply penalties like ban, mute, gag, or silence. + /// + /// The player issuing the command. + /// The command containing parameters. [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] [RequiresPermissions("@css/kick")] public void OnDisconnectedCommand(CCSPlayerController? caller, CommandInfo command) @@ -652,11 +779,17 @@ public partial class CS2_SimpleAdmin disconnectedMenu?.Open(caller); } + /// + /// Displays the warning menu for a player specified by a command argument, + /// showing active and past warns with options to remove them. + /// + /// The player issuing the command. + /// The command containing target player identifier. [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_ONLY)] [RequiresPermissions("@css/kick")] public void OnWarnsCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null || _localizer == null || caller == null) return; + if (DatabaseProvider == null || _localizer == null || caller == null) return; var targets = GetTarget(command); if (targets == null) return; @@ -673,12 +806,13 @@ public partial class CS2_SimpleAdmin if (!caller.CanTarget(player)) return; var userId = player.UserId.Value; + var steamId = player.SteamID; IMenu? warnsMenu = Helper.CreateMenu(_localizer["sa_admin_warns_menu_title", player.PlayerName]); Task.Run(async () => { - var warnsList = await WarnManager.GetPlayerWarns(PlayersInfo[userId], false); + var warnsList = await WarnManager.GetPlayerWarns(PlayersInfo[steamId], false); var sortedWarns = warnsList .OrderBy(warn => (string)warn.status == "ACTIVE" ? 0 : 1) .ThenByDescending(warn => (int)warn.id) @@ -689,12 +823,12 @@ public partial class CS2_SimpleAdmin warnsMenu?.AddMenuOption($"[{((string)w.status == "ACTIVE" ? $"{ChatColors.LightRed}X" : $"{ChatColors.Lime}✔️")}{ChatColors.Default}] {(string)w.reason}", (controller, option) => { - _ = WarnManager.UnwarnPlayer(PlayersInfo[userId], (int)w.id); + _ = WarnManager.UnwarnPlayer(PlayersInfo[steamId], (int)w.id); player.PrintToChat(_localizer["sa_admin_warns_unwarn", player.PlayerName, (string)w.reason]); }); }); - await Server.NextFrameAsync(() => + await Server.NextWorldUpdateAsync(() => { warnsMenu?.Open(caller); }); @@ -702,8 +836,14 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Lists players currently connected to the server with options to output JSON or filter duplicate IPs. + /// + /// The player issuing the command or null for console. + /// The command containing output options. [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] [RequiresPermissions("@css/generic")] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public void OnPlayersCommand(CCSPlayerController? caller, CommandInfo command) { var isJson = command.GetArg(1).ToLower().Equals("-json"); @@ -740,27 +880,39 @@ public partial class CS2_SimpleAdmin } else { - var playersJson = JsonConvert.SerializeObject(playersToTarget.Select(player => + var options = new JsonSerializerOptions { - var matchStats = player.ActionTrackingServices?.MatchStats; - - return new + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + + var playerDtos = playersToTarget + .Where(player => player.UserId.HasValue) + .Select(player => { - player.UserId, - Name = player.PlayerName, - SteamId = player.SteamID.ToString(), - IpAddress = AdminManager.PlayerHasPermissions(new SteamID(caller!.SteamID), "@css/showip") ? player.IpAddress?.Split(":")[0] ?? "Unknown" : "Unknown", - player.Ping, - IsAdmin = AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/ban") || AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/generic"), - Stats = new - { - player.Score, - Kills = matchStats?.Kills ?? 0, - Deaths = matchStats?.Deaths ?? 0, - player.MVPs - } - }; - })); + var matchStats = player.ActionTrackingServices?.MatchStats; + + return new PlayerDto( + player.UserId.GetValueOrDefault(0), + player.PlayerName, + player.SteamID.ToString(), + AdminManager.PlayerHasPermissions(new SteamID(caller!.SteamID), "@css/showip") + ? player.IpAddress?.Split(":")[0] ?? "Unknown" + : "Unknown", + player.Ping, + AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/ban") + || AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/generic"), + new PlayerStats( + player.Score, + matchStats?.Kills ?? 0, + matchStats?.Deaths ?? 0, + player.MVPs + ) + ); + }) + .ToList(); + + var playersJson = JsonSerializer.Serialize(playerDtos, options); if (caller != null) caller.PrintToConsole(playersJson); @@ -769,6 +921,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Issues a kick to one or multiple players specified in the command arguments. + /// + /// The player issuing the kick command. + /// The command with target player(s) and optional reason. [RequiresPermissions("@css/kick")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnKickCommand(CCSPlayerController? caller, CommandInfo command) @@ -806,6 +963,15 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + + /// + /// Kicks a specified player immediately with reason, notifying the server and logging the action. + /// + /// The player issuing the kick. + /// The player to be kicked. + /// The reason for the kick. + /// Optional name of the kick issuer for notifications. + /// Optional command for logging. public void Kick(CCSPlayerController? caller, CCSPlayerController player, string? reason = "Unknown", string? callerName = null, CommandInfo? command = null) { if (!player.IsValid) return; @@ -816,8 +982,8 @@ public partial class CS2_SimpleAdmin callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; reason ??= _localizer?["sa_unknown"] ?? "Unknown"; - var playerInfo = PlayersInfo[player.UserId.Value]; - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var playerInfo = PlayersInfo[player.SteamID]; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Determine message keys and arguments for the kick notification var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = @@ -847,6 +1013,11 @@ public partial class CS2_SimpleAdmin SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Kick, reason, -1, null); } + /// + /// Changes the current map to the specified map name or workshop map ID. + /// + /// The player issuing the map change. + /// The command containing the map name or ID. [RequiresPermissions("@css/changemap")] [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnMapCommand(CCSPlayerController? caller, CommandInfo command) @@ -855,6 +1026,12 @@ public partial class CS2_SimpleAdmin ChangeMap(caller, map, command); } + /// + /// Changes to a specified map, validating it or handling workshop maps, and notifying the server and admins. + /// + /// The player issuing the change. + /// The map name or identifier. + /// Optional command object for logging and replies. public void ChangeMap(CCSPlayerController? caller, string map, CommandInfo? command = null) { var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console"; @@ -904,6 +1081,11 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command?.GetCommandString ?? $"css_map {map}"); } + /// + /// Changes the current map to a workshop map specified by name or ID. + /// + /// The player issuing the command. + /// The command containing the workshop map identifier. [CommandHelper(1, "")] [RequiresPermissions("@css/changemap")] public void OnWorkshopMapCommand(CCSPlayerController? caller, CommandInfo command) @@ -912,6 +1094,12 @@ public partial class CS2_SimpleAdmin ChangeWorkshopMap(caller, map, command); } + /// + /// Changes to a specified workshop map by name or ID and notifies admins. + /// + /// The player issuing the command. + /// The workshop map identifier. + /// Optional command for logging. public void ChangeWorkshopMap(CCSPlayerController? caller, string map, CommandInfo? command = null) { map = map.ToLower(); @@ -941,6 +1129,11 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command?.GetCommandString ?? $"css_wsmap {map}"); } + /// + /// Allows changing a console variable's value. + /// + /// The player issuing the command. + /// The command with cvar name and value. [CommandHelper(2, " ")] [RequiresPermissions("@css/cvar")] public void OnCvarCommand(CCSPlayerController? caller, CommandInfo command) @@ -961,28 +1154,33 @@ public partial class CS2_SimpleAdmin } Helper.LogCommand(caller, command); - var value = command.GetArg(2); - Server.ExecuteCommand($"{cvar.Name} {value}"); - command.ReplyToCommand($"{callerName} changed cvar {cvar.Name} to {value}."); Logger.LogInformation($"{callerName} changed cvar {cvar.Name} to {value}."); } + /// + /// Executes an RCON command on the server. + /// + /// The player issuing the command. + /// The command string to execute via RCON. [CommandHelper(1, "")] [RequiresPermissions("@css/rcon")] public void OnRconCommand(CCSPlayerController? caller, CommandInfo command) { var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; - Helper.LogCommand(caller, command); - Server.ExecuteCommand(command.ArgString); command.ReplyToCommand($"{callerName} executed command {command.ArgString}."); Logger.LogInformation($"{callerName} executed command ({command.ArgString})."); } + /// + /// Restarts the game. + /// + /// The player or console initiating the restart. + /// The restart command info. [RequiresPermissions("@css/generic")] [CommandHelper(minArgs: 0, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnRestartCommand(CCSPlayerController? caller, CommandInfo command) @@ -990,6 +1188,11 @@ public partial class CS2_SimpleAdmin RestartGame(caller); } + /// + /// Opens plugin manager menu for the caller with options to load or unload plugins. + /// + /// The player opening the plugin manager. + /// The command parameters. [RequiresPermissions("@css/root")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void OnPluginManagerCommand(CCSPlayerController? caller, CommandInfo commandInfo) @@ -1060,6 +1263,10 @@ public partial class CS2_SimpleAdmin pluginsMenu?.Open(caller); } + /// + /// Restarts the game process by issuing the restart game command to the server and logging the action. + /// + /// The admin or console requesting the restart. public static void RestartGame(CCSPlayerController? admin) { Helper.LogCommand(admin, "css_restartgame"); diff --git a/CS2-SimpleAdmin/Commands/basecomms.cs b/CS2-SimpleAdmin/Commands/basecomms.cs index c30b832..0cc9a8c 100644 --- a/CS2-SimpleAdmin/Commands/basecomms.cs +++ b/CS2-SimpleAdmin/Commands/basecomms.cs @@ -11,11 +11,16 @@ namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { + /// + /// Processes the 'gag' command, applying a muted penalty to target players with optional time and reason. + /// + /// The player issuing the gag command or null for console. + /// The command input containing targets, time, and reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnGagCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; var targets = GetTarget(command); @@ -49,9 +54,19 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Applies the gag penalty logic to an individual player, performing permission checks, notification, and logging. + /// + /// The player issuing the gag. + /// The player to gag. + /// Duration of the gag in minutes, 0 is permanent. + /// Reason for the gag. + /// Optional caller name for notifications. + /// Optional command info for logging. + /// If true, suppresses logging notifications. internal void Gag(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false) { - if (Database == null || !player.IsValid || !player.UserId.HasValue) return; + if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return; if (!caller.CanTarget(player)) return; if (!CheckValidMute(caller, time)) return; @@ -59,14 +74,18 @@ public partial class CS2_SimpleAdmin callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; // Get player and admin information - var playerInfo = PlayersInfo[player.UserId.Value]; - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var playerInfo = PlayersInfo[player.SteamID]; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Asynchronously handle gag logic Task.Run(async () => { int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time); - SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Gag, reason, time, penaltyId); + await Server.NextWorldUpdateAsync(() => + { + SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Gag, reason, time, + penaltyId); + }); }); // Add penalty to the player's penalty manager @@ -91,7 +110,7 @@ public partial class CS2_SimpleAdmin } // Increment the player's total gags count - PlayersInfo[player.UserId.Value].TotalGags++; + PlayersInfo[player.SteamID].TotalGags++; // Log the gag command and send Discord notification if (!silent) @@ -105,16 +124,23 @@ public partial class CS2_SimpleAdmin Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Gag, _localizer); } - internal void AddGag(CCSPlayerController? caller, SteamID steamid, int time, string reason, MuteManager? muteManager = null) + /// + /// Adds a gag penalty to a player identified by SteamID, supporting offline players. + /// + /// The player issuing the command or null for console. + /// SteamID of the target player. + /// Duration in minutes (0 for permanent). + /// Reason for the gag. + 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.UserId.Value] : null; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; - var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64.ToString()); + var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64); if (player != null && player.IsValid) { @@ -131,19 +157,28 @@ public partial class CS2_SimpleAdmin // Asynchronous ban operation if player is not online or not found Task.Run(async () => { - int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64.ToString(), adminInfo, reason, time, 3); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Gag, reason, time, penaltyId); + 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); } } + /// + /// Handles the 'addgag' command, which adds a gag penalty to a player specified by SteamID. + /// + /// The player issuing the command or null for console. + /// Command input that includes SteamID, optional time, and reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: " [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnAddGagCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; // Set caller name var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; @@ -158,7 +193,7 @@ public partial class CS2_SimpleAdmin return; } - var steamid = steamId.SteamId64.ToString(); + var steamid = steamId.SteamId64; var reason = command.ArgCount >= 3 ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim() : _localizer?["sa_unknown"] ?? "Unknown"; @@ -169,7 +204,7 @@ public partial class CS2_SimpleAdmin if (!CheckValidMute(caller, time)) return; // Get player and admin info - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Attempt to match player based on SteamID var player = Helper.GetPlayerFromSteamid64(steamid); @@ -191,10 +226,14 @@ public partial class CS2_SimpleAdmin Task.Run(async () => { int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Gag, reason, time, penaltyId); + 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."); } @@ -203,11 +242,16 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Handles removing a gag penalty ('ungag') of a player, either by SteamID or pattern match. + /// + /// The player issuing the ungag command or null for console. + /// Command input containing SteamID or player name and optional reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: " [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnUngagCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console"; var pattern = command.GetArg(1); @@ -228,7 +272,7 @@ public partial class CS2_SimpleAdmin // Check if pattern is a valid SteamID64 if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null) { - var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString()); + var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64); if (player != null && player.IsValid) { @@ -252,8 +296,8 @@ public partial class CS2_SimpleAdmin { PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Gag); - if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalGags > 0) - PlayersInfo[namePlayer.UserId.Value].TotalGags--; + if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalGags > 0) + PlayersInfo[namePlayer.SteamID].TotalGags--; Task.Run(async () => { @@ -273,11 +317,16 @@ public partial class CS2_SimpleAdmin } } + /// + /// Processes the 'mute' command, applying a voice mute penalty to target players with optional time and reason. + /// + /// The player issuing the mute command or null for console. + /// The command input containing targets, time, and reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnMuteCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; var targets = GetTarget(command); @@ -311,9 +360,19 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Applies the mute penalty logic to an individual player, handling permissions, notifications, logging, and countdown timers. + /// + /// The player issuing the mute. + /// The player to mute. + /// Duration in minutes, 0 indicates permanent mute. + /// Reason for the mute. + /// Optional caller name for notification messages. + /// Optional command info for logging. + /// If true, suppresses some logging. internal void Mute(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false) { - if (Database == null || !player.IsValid || !player.UserId.HasValue) return; + if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return; if (!caller.CanTarget(player)) return; if (!CheckValidMute(caller, time)) return; @@ -321,8 +380,8 @@ public partial class CS2_SimpleAdmin callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; // Get player and admin information - var playerInfo = PlayersInfo[player.UserId.Value]; - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var playerInfo = PlayersInfo[player.SteamID]; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Set player's voice flags to muted player.VoiceFlags = VoiceFlags.Muted; @@ -331,7 +390,11 @@ public partial class CS2_SimpleAdmin Task.Run(async () => { int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time, 1); - SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Mute, reason, time, penaltyId); + await Server.NextWorldUpdateAsync(() => + { + SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Mute, reason, time, + penaltyId); + }); }); // Add penalty to the player's penalty manager @@ -356,7 +419,7 @@ public partial class CS2_SimpleAdmin } // Increment the player's total mutes count - PlayersInfo[player.UserId.Value].TotalMutes++; + PlayersInfo[player.SteamID].TotalMutes++; // Log the mute command and send Discord notification if (!silent) @@ -370,11 +433,16 @@ public partial class CS2_SimpleAdmin Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Mute, _localizer); } + /// + /// Handles the 'addmute' command that adds a mute penalty to a player specified by SteamID. + /// + /// The player issuing the command or null for console. + /// Command input includes SteamID, optional time, and reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: " [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnAddMuteCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; // Set caller name var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; @@ -389,7 +457,7 @@ public partial class CS2_SimpleAdmin return; } - var steamid = steamId.SteamId64.ToString(); + var steamid = steamId.SteamId64; var reason = command.ArgCount >= 3 ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim() : _localizer?["sa_unknown"] ?? "Unknown"; @@ -400,7 +468,7 @@ public partial class CS2_SimpleAdmin if (!CheckValidMute(caller, time)) return; // Get player and admin info - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Attempt to match player based on SteamID var player = Helper.GetPlayerFromSteamid64(steamid); @@ -422,10 +490,14 @@ public partial class CS2_SimpleAdmin Task.Run(async () => { int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time, 1); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Mute, reason, time, penaltyId); + 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."); } @@ -434,6 +506,14 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Asynchronously adds a mute penalty to a player by Steam ID. Handles both online and offline players. + /// + /// The admin/player issuing the mute. + /// The Steam ID of the player to mute. + /// Duration of the mute in minutes. + /// Reason for the mute. + /// Optional mute manager instance for handling database ops. internal void AddMute(CCSPlayerController? caller, SteamID steamid, int time, string reason, MuteManager? muteManager = null) { // Set default caller name if not provided @@ -441,9 +521,9 @@ public partial class CS2_SimpleAdmin ? caller.PlayerName : (_localizer?["sa_console"] ?? "Console"); - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; - var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64.ToString()); + var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64); if (player != null && player.IsValid) { @@ -460,19 +540,29 @@ public partial class CS2_SimpleAdmin // Asynchronous ban operation if player is not online or not found Task.Run(async () => { - int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64.ToString(), adminInfo, reason, time, 1); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Mute, reason, time, penaltyId); + 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); } } + /// + /// Handles the unmute command - removes mute penalty from player identified by SteamID or name. + /// Can target both online and offline players. + /// + /// The admin/player issuing the unmute. + /// The command arguments including target identifier and optional reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnUnmuteCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console"; var pattern = command.GetArg(1); @@ -493,7 +583,7 @@ public partial class CS2_SimpleAdmin // Check if pattern is a valid SteamID64 if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null) { - var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString()); + var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64); if (player != null && player.IsValid) { @@ -519,8 +609,8 @@ public partial class CS2_SimpleAdmin PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Mute); namePlayer.VoiceFlags = VoiceFlags.Normal; - if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalMutes > 0) - PlayersInfo[namePlayer.UserId.Value].TotalMutes--; + if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalMutes > 0) + PlayersInfo[namePlayer.SteamID].TotalMutes--; Task.Run(async () => { @@ -540,11 +630,17 @@ public partial class CS2_SimpleAdmin } } + /// + /// Issue a 'silence' penalty to a player - disables voice communication. + /// Handles online and offline players, with duration and reason specified. + /// + /// The admin/player issuing the silence. + /// Command containing target, duration, and optional reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnSilenceCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; var targets = GetTarget(command); @@ -578,9 +674,19 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Applies silence logical processing for a player - updates database and notifies. + /// + /// Admin/player applying the silence. + /// Target player. + /// Duration of silence. + /// Reason for silence. + /// Optional name of silent admin or console. + /// Optional command details for logging. + /// If true, suppresses logging notifications. internal void Silence(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null, bool silent = false) { - if (Database == null || !player.IsValid || !player.UserId.HasValue) return; + if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return; if (!caller.CanTarget(player)) return; if (!CheckValidMute(caller, time)) return; @@ -588,14 +694,18 @@ public partial class CS2_SimpleAdmin callerName ??= caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; // Get player and admin information - var playerInfo = PlayersInfo[player.UserId.Value]; - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var playerInfo = PlayersInfo[player.SteamID]; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Asynchronously handle silence logic Task.Run(async () => { - int? penaltyId = await MuteManager.MutePlayer(playerInfo, adminInfo, reason, time, 2); // Assuming 2 is the type for silence - SimpleAdminApi?.OnPlayerPenaltiedEvent(playerInfo, adminInfo, PenaltyType.Silence, reason, time, penaltyId); + 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 @@ -621,7 +731,7 @@ public partial class CS2_SimpleAdmin } // Increment the player's total silences count - PlayersInfo[player.UserId.Value].TotalSilences++; + PlayersInfo[player.SteamID].TotalSilences++; // Log the silence command and send Discord notification if (!silent) @@ -635,11 +745,17 @@ public partial class CS2_SimpleAdmin Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Silence, _localizer); } + /// + /// Handles the 'AddSilence' command, applying a silence penalty to a player specified by SteamID, + /// with support for offline player penalties. + /// + /// The player/admin issuing the command. + /// The command input containing SteamID, optional time, and reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnAddSilenceCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; // Set caller name var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName; @@ -654,7 +770,7 @@ public partial class CS2_SimpleAdmin return; } - var steamid = steamId.SteamId64.ToString(); + var steamid = steamId.SteamId64; var reason = command.ArgCount >= 3 ? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim() : _localizer?["sa_unknown"] ?? "Unknown"; @@ -665,7 +781,7 @@ public partial class CS2_SimpleAdmin if (!CheckValidMute(caller, time)) return; // Get player and admin info - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; // Attempt to match player based on SteamID var player = Helper.GetPlayerFromSteamid64(steamid); @@ -687,10 +803,14 @@ public partial class CS2_SimpleAdmin Task.Run(async () => { int? penaltyId = await MuteManager.AddMuteBySteamid(steamid, adminInfo, reason, time, 2); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamId, adminInfo, PenaltyType.Silence, reason, time, penaltyId); + 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."); } @@ -699,6 +819,14 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Adds a silence penalty to a player by Steam ID. Manages both online and offline player cases. + /// + /// Admin/player initiating the silence. + /// Steam ID of player. + /// Duration of silence. + /// Reason for the penalty. + /// Optional mute manager for DB operations. internal void AddSilence(CCSPlayerController? caller, SteamID steamid, int time, string reason, MuteManager? muteManager = null) { // Set default caller name if not provided @@ -706,9 +834,9 @@ public partial class CS2_SimpleAdmin ? caller.PlayerName : (_localizer?["sa_console"] ?? "Console"); - var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.UserId.Value] : null; + var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null; - var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64.ToString()); + var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64); if (player != null && player.IsValid) { @@ -725,19 +853,29 @@ public partial class CS2_SimpleAdmin // Asynchronous ban operation if player is not online or not found Task.Run(async () => { - int? penaltyId = await MuteManager.AddMuteBySteamid(steamid.SteamId64.ToString(), adminInfo, reason, time, 2); - SimpleAdminApi?.OnPlayerPenaltiedAddedEvent(steamid, adminInfo, PenaltyType.Silence, reason, time, penaltyId); + 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); } } + /// + /// Removes the silence penalty from a player, either by SteamID, name, or offline pattern. + /// Resets voice settings and updates notices accordingly. + /// + /// Admin/player issuing the unsilence. + /// Command arguments with target pattern and optional reason. [RequiresPermissions("@css/chat")] [CommandHelper(minArgs: 1, usage: " [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnUnsilenceCommand(CCSPlayerController? caller, CommandInfo command) { - if (Database == null) return; + if (DatabaseProvider == null) return; var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console"; var pattern = command.GetArg(1); @@ -758,7 +896,7 @@ public partial class CS2_SimpleAdmin // Check if pattern is a valid SteamID64 if (Helper.ValidateSteamId(pattern, out var steamId) && steamId != null) { - var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64.ToString()); + var player = Helper.GetPlayerFromSteamid64(steamId.SteamId64); if (player != null && player.IsValid) { @@ -788,8 +926,8 @@ public partial class CS2_SimpleAdmin // Reset voice flags to normal namePlayer.VoiceFlags = VoiceFlags.Normal; - if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.UserId.Value].TotalSilences > 0) - PlayersInfo[namePlayer.UserId.Value].TotalSilences--; + if (namePlayer.UserId.HasValue && PlayersInfo[namePlayer.SteamID].TotalSilences > 0) + PlayersInfo[namePlayer.SteamID].TotalSilences--; Task.Run(async () => { @@ -809,6 +947,12 @@ public partial class CS2_SimpleAdmin } } + /// + /// Validates mute penalty duration based on admin privileges and configured max duration. + /// + /// Admin/player issuing the mute. + /// Requested duration in minutes. + /// True if mute penalty duration is allowed; false otherwise. private bool CheckValidMute(CCSPlayerController? caller, int duration) { if (caller == null) return true; diff --git a/CS2-SimpleAdmin/Commands/basevotes.cs b/CS2-SimpleAdmin/Commands/basevotes.cs index 68908d7..38404f6 100644 --- a/CS2-SimpleAdmin/Commands/basevotes.cs +++ b/CS2-SimpleAdmin/Commands/basevotes.cs @@ -8,6 +8,12 @@ namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { + /// + /// Handles the vote command, creates voting menu for players, and collects answers. + /// Displays results after timeout and resets voting state. + /// + /// The player/admin who initiated the vote, or null for console. + /// Command object containing question and options. [RequiresPermissions("@css/generic")] [CommandHelper(minArgs: 2, usage: " [... options ...]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnVoteCommand(CCSPlayerController? caller, CommandInfo command) diff --git a/CS2-SimpleAdmin/Commands/funcommands.cs b/CS2-SimpleAdmin/Commands/funcommands.cs index efa1f9d..5f4b64f 100644 --- a/CS2-SimpleAdmin/Commands/funcommands.cs +++ b/CS2-SimpleAdmin/Commands/funcommands.cs @@ -8,6 +8,11 @@ namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { + /// + /// Enables or disables no-clip mode for specified player(s). + /// + /// The player issuing the command. + /// The command input containing targets. [CommandHelper(1, "<#userid or name>")] [RequiresPermissions("@css/cheats")] public void OnNoclipCommand(CCSPlayerController? caller, CommandInfo command) @@ -31,6 +36,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Toggles no-clip mode for a player and shows admin activity messages. + /// + /// The player/admin toggling no-clip. + /// The target player whose no-clip state changes. + /// Optional caller name for messages. + /// Optional command info for logging. internal static void NoClip(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) { if (!player.IsValid) return; @@ -58,6 +70,12 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); } + /// + /// Enables or disables god mode for specified player(s). + /// + /// The player issuing the command. + /// The command input containing targets. + [RequiresPermissions("@css/cheats")] [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnGodCommand(CCSPlayerController? caller, CommandInfo command) @@ -82,6 +100,12 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Toggles god mode for a player and notifies admins. + /// + /// The player/admin toggling god mode. + /// The target player whose god mode changes. + /// Optional command info for logging. internal static void God(CCSPlayerController? caller, CCSPlayerController player, CommandInfo? command = null) { if (!caller.CanTarget(player)) return; @@ -111,6 +135,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Freezes target player(s) for an optional specified duration. + /// + /// The player issuing the freeze command. + /// The command input containing targets and duration. [CommandHelper(1, "<#userid or name> [duration]")] [RequiresPermissions("@css/slay")] public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command) @@ -133,6 +162,11 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Resizes the target player(s) models to a specified scale. + /// + /// The player issuing the resize command. + /// The command input containing targets and scale factor. [CommandHelper(1, "<#userid or name> [size]")] [RequiresPermissions("@css/slay")] public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command) @@ -153,8 +187,8 @@ public partial class CS2_SimpleAdmin sceneNode.GetSkeletonInstance().Scale = size; player.PlayerPawn.Value.AcceptInput("SetScale", null, null, size.ToString(CultureInfo.InvariantCulture)); - - Server.NextFrame(() => + + Server.NextWorldUpdate(() => { Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity", "m_CBodyComponent"); }); @@ -173,6 +207,14 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Freezes a single player and optionally schedules automatic unfreeze after a duration. + /// + /// The player/admin freezing the player. + /// The player to freeze. + /// Duration of freeze in seconds. + /// Optional name for notifications. + /// Optional command info for logging. internal static void Freeze(CCSPlayerController? caller, CCSPlayerController player, int time, string? callerName = null, CommandInfo? command = null) { if (!player.IsValid) return; @@ -206,6 +248,11 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}"); } + /// + /// Unfreezes target player(s). + /// + /// The player issuing the unfreeze command. + /// The command input with targets. [CommandHelper(1, "<#userid or name>")] [RequiresPermissions("@css/slay")] public void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command) @@ -224,6 +271,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Unfreezes a single player and notifies admins. + /// + /// The player/admin unfreezing the player. + /// The player to unfreeze. + /// Optional name for notifications. + /// Optional command info for logging. internal static void Unfreeze(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) { if (!player.IsValid) return; diff --git a/CS2-SimpleAdmin/Commands/playercommands.cs b/CS2-SimpleAdmin/Commands/playercommands.cs index 3be0b36..b66adf2 100644 --- a/CS2-SimpleAdmin/Commands/playercommands.cs +++ b/CS2-SimpleAdmin/Commands/playercommands.cs @@ -11,9 +11,15 @@ namespace CS2_SimpleAdmin; public partial class CS2_SimpleAdmin { - internal static readonly Dictionary SpeedPlayers = []; + internal static readonly Dictionary SpeedPlayers = []; internal static readonly Dictionary GravityPlayers = []; + /// + /// Executes the 'slay' command, forcing the targeted players to commit suicide. + /// Checks player validity and permissions. + /// + /// Player or console issuing the command. + /// Command details, including targets. [RequiresPermissions("@css/slay")] [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnSlayCommand(CCSPlayerController? caller, CommandInfo command) @@ -32,6 +38,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Performs the actual slay action on a player, with notification and logging. + /// + /// Admin or console issuing the slay. + /// Target player to slay. + /// Optional name to display as the slayer. + /// Optional command info for logging. internal static void Slay(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) { if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) return; @@ -59,6 +72,12 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, $"css_slay {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}"); } + /// + /// Executes the 'give' command to provide a specified weapon to targeted players. + /// Enforces server rules for prohibited weapons. + /// + /// Player or console issuing the command. + /// Command details, including targets and weapon name. [RequiresPermissions("@css/cheats")] [CommandHelper(minArgs: 2, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnGiveCommand(CCSPlayerController? caller, CommandInfo command) @@ -70,13 +89,6 @@ public partial class CS2_SimpleAdmin var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).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")) { @@ -98,6 +110,15 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + + /// + /// Gives a weapon identified by name to a player, handling ambiguous matches and logging. + /// + /// Admin issuing the command. + /// Target player to receive the weapon. + /// Weapon name or partial name. + /// Optional name to display in notifications. + /// Optional command info for logging. private static void GiveWeapon(CCSPlayerController? caller, CCSPlayerController player, string weaponName, string? callerName = null, CommandInfo? command = null) { if (!caller.CanTarget(player)) return; @@ -137,6 +158,14 @@ public partial class CS2_SimpleAdmin } } + /// + /// Gives a specific weapon to a player, with notifications and logging. + /// + /// Admin issuing the command. + /// Target player. + /// Weapon item object. + /// Optional caller name for notifications. + /// Optional command info. internal static void GiveWeapon(CCSPlayerController? caller, CCSPlayerController player, CsItem weapon, string? callerName = null, CommandInfo? command = null) { if (!caller.CanTarget(player)) return; @@ -163,6 +192,12 @@ public partial class CS2_SimpleAdmin } } + /// + /// Executes the 'strip' command, removing all weapons from targeted players. + /// Checks player validity and permissions. + /// + /// Player or console issuing the command. + /// Command details including targets. [RequiresPermissions("@css/slay")] [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnStripCommand(CCSPlayerController? caller, CommandInfo command) @@ -184,6 +219,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Removes all weapons from a player, with notifications and logging. + /// + /// Admin or console issuing the strip command. + /// Target player. + /// Optional caller name. + /// Optional command info for logging. internal static void StripWeapons(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) { if (!caller.CanTarget(player)) return; @@ -214,6 +256,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Sets health value on targeted players. + /// + /// Admin or console issuing the command. + /// Command details including targets and health value. [RequiresPermissions("@css/slay")] [CommandHelper(minArgs: 1, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnHpCommand(CCSPlayerController? caller, CommandInfo command) @@ -236,6 +283,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Changes health of a player and logs the action. + /// + /// Admin or console calling the method. + /// Target player. + /// Health value to set. + /// Optional command info. internal static void SetHp(CCSPlayerController? caller, CCSPlayerController player, int health, CommandInfo? command = null) { if (!player.IsValid || player.IsHLTV) return; @@ -263,6 +317,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Sets movement speed on targeted players. + /// + /// Admin or console issuing the command. + /// Command details including targets and speed. [RequiresPermissions("@css/slay")] [CommandHelper(minArgs: 1, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnSpeedCommand(CCSPlayerController? caller, CommandInfo command) @@ -288,6 +347,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Changes speed of a player and logs the action. + /// + /// Admin or console calling the method. + /// Target player. + /// Speed value to set. + /// Optional command info. internal static void SetSpeed(CCSPlayerController? caller, CCSPlayerController player, float speed, CommandInfo? command = null) { if (!caller.CanTarget(player)) return; @@ -299,9 +365,9 @@ public partial class CS2_SimpleAdmin player.SetSpeed(speed); if (speed == 1f) - SpeedPlayers.Remove(player.Slot); + SpeedPlayers.Remove(player); else - SpeedPlayers[player.Slot] = speed; + SpeedPlayers[player] = speed; // Log the command if (command == null) @@ -319,6 +385,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Sets gravity on targeted players. + /// + /// Admin or console issuing the command. + /// Command details including targets and gravity value. [RequiresPermissions("@css/slay")] [CommandHelper(minArgs: 1, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnGravityCommand(CCSPlayerController? caller, CommandInfo command) @@ -344,6 +415,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Changes gravity of a player and logs the action. + /// + /// Admin or console calling the method. + /// Target player. + /// Gravity value to set. + /// Optional command info. internal static void SetGravity(CCSPlayerController? caller, CCSPlayerController player, float gravity, CommandInfo? command = null) { if (!caller.CanTarget(player)) return; @@ -375,6 +453,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Sets the money amount for the targeted players. + /// + /// The player/admin executing the command. + /// The command containing target player and money value. [RequiresPermissions("@css/slay")] [CommandHelper(minArgs: 1, usage: "<#userid or name> ", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnMoneyCommand(CCSPlayerController? caller, CommandInfo command) @@ -401,6 +484,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Applies money value to a single targeted player and logs the operation. + /// + /// The player/admin setting the money. + /// The player whose money will be set. + /// The value of money to set. + /// Optional command info for logging. internal static void SetMoney(CCSPlayerController? caller, CCSPlayerController player, int money, CommandInfo? command = null) { if (!caller.CanTarget(player)) return; @@ -427,6 +517,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Applies damage as a slap effect to the targeted players. + /// + /// The player/admin executing the slap command. + /// The command including targets and optional damage value. [RequiresPermissions("@css/slay")] [CommandHelper(minArgs: 1, usage: "<#userid or name> [damage]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnSlapCommand(CCSPlayerController? caller, CommandInfo command) @@ -457,6 +552,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Applies slap damage to a specific player with notifications and logging. + /// + /// The player/admin applying the slap effect. + /// The target player to slap. + /// The damage amount to apply. + /// Optional command info for logging. internal static void Slap(CCSPlayerController? caller, CCSPlayerController player, int damage, CommandInfo? command = null) { if (!caller.CanTarget(player)) return; @@ -485,6 +587,11 @@ public partial class CS2_SimpleAdmin } } + /// + /// Changes the team of targeted players with optional kill on switch. + /// + /// The player/admin issuing the command. + /// The command containing targets, team info, and optional kill flag. [RequiresPermissions("@css/kick")] [CommandHelper(minArgs: 2, usage: "<#userid or name> [] [-k]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] public void OnTeamCommand(CCSPlayerController? caller, CommandInfo command) @@ -534,6 +641,15 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Changes the team of a player with various conditions and logs the operation. + /// + /// The player/admin issuing the change. + /// The target player. + /// Team name string. + /// Team enumeration value. + /// If true, kills player on team change. + /// Optional command info for logging. 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 @@ -581,6 +697,11 @@ public partial class CS2_SimpleAdmin Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); } + /// + /// Renames targeted players to a new name. + /// + /// The admin issuing the rename command. + /// The command including targets and new name. [CommandHelper(1, "<#userid or name> ")] [RequiresPermissions("@css/kick")] public void OnRenameCommand(CCSPlayerController? caller, CommandInfo command) @@ -626,6 +747,11 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Renames permamently targeted players to a new name. + /// + /// The admin issuing the pre-rename command. + /// The command containing targets and new alias. [CommandHelper(1, "<#userid or name> ")] [RequiresPermissions("@css/ban")] public void OnPrenameCommand(CCSPlayerController? caller, CommandInfo command) @@ -676,6 +802,11 @@ public partial class CS2_SimpleAdmin }); } + /// + /// Respawns targeted players, restoring their state. + /// + /// The admin or player issuing respawn. + /// The command including target players. [CommandHelper(1, "<#userid or name>")] [RequiresPermissions("@css/cheats")] public void OnRespawnCommand(CCSPlayerController? caller, CommandInfo command) @@ -700,6 +831,13 @@ public partial class CS2_SimpleAdmin Helper.LogCommand(caller, command); } + /// + /// Respawns a specified player and updates admin notifications. + /// + /// Admin or player executing respawn. + /// Player to respawn. + /// Optional admin name. + /// Optional command info. internal static void Respawn(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) { // Check if the caller can target the player @@ -715,8 +853,8 @@ public partial class CS2_SimpleAdmin var playerPawn = player.PlayerPawn.Value; _cBasePlayerControllerSetPawnFunc.Invoke(player, playerPawn, true, false); VirtualFunction.CreateVoid(player.Handle, GameData.GetOffset("CCSPlayerController_Respawn"))(player); - - if (player.UserId.HasValue && PlayersInfo.TryGetValue(player.UserId.Value, out var value) && value.DiePosition != null) + + if (player.UserId.HasValue && PlayersInfo.TryGetValue(player.SteamID, out var value) && value.DiePosition != null) playerPawn.Teleport(value.DiePosition?.Position, value.DiePosition?.Angle); // Log the command @@ -733,146 +871,270 @@ public partial class CS2_SimpleAdmin Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs); } - [CommandHelper(1, "<#userid or name>")] + /// + /// Teleports targeted player(s) to another player's location. + /// + /// Admin issuing teleport command. + /// Command containing teleport targets and destination. + [CommandHelper(1, "<#userid or name> [#userid or name]")] [RequiresPermissions("@css/kick")] public void OnGotoCommand(CCSPlayerController? caller, CommandInfo command) { - // Check if the caller is valid and has a live pawn - if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) return; + IEnumerable playersToTeleport; + CCSPlayerController? destinationPlayer; - // Get the target players var targets = GetTarget(command); - if (targets == null || targets.Count() > 1) return; - var playersToTarget = targets.Players - .Where(player => player is { IsValid: true, IsHLTV: false }) - .ToList(); + if (command.ArgCount < 3) + { + if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) + return; - // Log the command + if (targets == null || targets.Count() != 1) + return; + + destinationPlayer = targets.Players.FirstOrDefault(p => + p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); + + if (destinationPlayer == null || !caller.CanTarget(destinationPlayer) || caller.PlayerPawn.Value == null) + return; + + playersToTeleport = [caller]; + } + else + { + var destination = GetTarget(command, 2); + if (targets == null || destination == null || destination.Count() != 1) + return; + + destinationPlayer = destination.Players.FirstOrDefault(p => + p is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); + + if (destinationPlayer == null) + return; + + playersToTeleport = targets.Players + .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) + .ToList(); + + if (!playersToTeleport.Any()) + return; + } + + // Log command Helper.LogCommand(caller, command); - // Process each player to teleport - foreach (var player in playersToTarget.Where(player => player is { Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).Where(caller.CanTarget)) + foreach (var player in playersToTeleport) { - if (caller.PlayerPawn.Value == null || player.PlayerPawn.Value == null) + if (player.PlayerPawn?.Value == null || destinationPlayer?.PlayerPawn?.Value == null) continue; - // Teleport the caller to the player and toggle noclip - caller.TeleportPlayer(player); - // caller.PlayerPawn.Value.ToggleNoclip(); + player.TeleportPlayer(destinationPlayer); - 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 + destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING; + destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING; + Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup"); + Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup"); + AddTimer(4, () => { - if (!caller.IsValid || caller.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"); + 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"); + } }); - // Prepare message key and arguments for the teleport notification - var activityMessageKey = "sa_admin_tp_message"; - var adminActivityArgs = new object[] { "CALLER", player.PlayerName }; - - // Show admin activity - if (!SilentPlayers.Contains(caller.Slot) && _localizer != null) + if (caller != null && !SilentPlayers.Contains(caller.Slot) && _localizer != null) { - Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, false, adminActivityArgs); + Helper.ShowAdminActivity("sa_admin_tp_message", player.PlayerName, false, "CALLER", destinationPlayer.PlayerName); } } } - - [CommandHelper(1, "<#userid or name>")] + + /// + /// Brings targeted player(s) to the caller or specified destination player's location. + /// + /// Player issuing the bring command. + /// Command containing the destination and targets. + [CommandHelper(1, "<#destination or name> [#userid or name...]")] [RequiresPermissions("@css/kick")] public void OnBringCommand(CCSPlayerController? caller, CommandInfo command) { - // Check if the caller is valid and has a live pawn - if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) + IEnumerable playersToTeleport; + CCSPlayerController? destinationPlayer; + + if (command.ArgCount < 3) + { + if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) + return; + + var targets = GetTarget(command); + if (targets == null || !targets.Any()) + return; + + 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; - // 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 + // Log 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)) + foreach (var player in playersToTeleport) { - if (caller.PlayerPawn.Value == null || player.PlayerPawn.Value == null) + if (player.PlayerPawn?.Value == null || destinationPlayer.PlayerPawn?.Value == null) continue; - // Teleport the player to the caller and toggle noclip - player.TeleportPlayer(caller); - // caller.PlayerPawn.Value.ToggleNoclip(); - - 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"); - + // Teleport + player.TeleportPlayer(destinationPlayer); + 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 + destinationPlayer.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING; + destinationPlayer.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DISSOLVING; + Utilities.SetStateChanged(destinationPlayer, "CCollisionProperty", "m_CollisionGroup"); + Utilities.SetStateChanged(destinationPlayer, "VPhysicsCollisionAttribute_t", "m_nCollisionGroup"); + 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"); + 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"); + } }); - // 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) + if (caller != null && !SilentPlayers.Contains(caller.Slot) && _localizer != null) { - Helper.ShowAdminActivity(activityMessageKey, caller.PlayerName, false, adminActivityArgs); + 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); + // } + // } + // } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Config.cs b/CS2-SimpleAdmin/Config.cs index 3b7e041..50e55fb 100644 --- a/CS2-SimpleAdmin/Config.cs +++ b/CS2-SimpleAdmin/Config.cs @@ -38,67 +38,67 @@ public class Discord [JsonPropertyName("DiscordPenaltyBanSettings")] public DiscordPenaltySetting[] DiscordPenaltyBanSettings { get; set; } = [ - new DiscordPenaltySetting { Name = "Color", Value = "" }, - new DiscordPenaltySetting { Name = "Webhook", Value = "" }, - new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, - new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, - new DiscordPenaltySetting { Name = "Footer", Value = "" }, - new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, + new() { Name = "Color", Value = "" }, + new() { Name = "Webhook", Value = "" }, + new() { Name = "ThumbnailUrl", Value = "" }, + new() { Name = "ImageUrl", Value = "" }, + new() { Name = "Footer", Value = "" }, + new() { Name = "Time", Value = "{relative}" }, ]; [JsonPropertyName("DiscordPenaltyMuteSettings")] public DiscordPenaltySetting[] DiscordPenaltyMuteSettings { get; set; } = [ - new DiscordPenaltySetting { Name = "Color", Value = "" }, - new DiscordPenaltySetting { Name = "Webhook", Value = "" }, - new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, - new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, - new DiscordPenaltySetting { Name = "Footer", Value = "" }, - new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, + new() { Name = "Color", Value = "" }, + new() { Name = "Webhook", Value = "" }, + new() { Name = "ThumbnailUrl", Value = "" }, + new() { Name = "ImageUrl", Value = "" }, + new() { Name = "Footer", Value = "" }, + new() { Name = "Time", Value = "{relative}" }, ]; [JsonPropertyName("DiscordPenaltyGagSettings")] public DiscordPenaltySetting[] DiscordPenaltyGagSettings { get; set; } = [ - new DiscordPenaltySetting { Name = "Color", Value = "" }, - new DiscordPenaltySetting { Name = "Webhook", Value = "" }, - new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, - new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, - new DiscordPenaltySetting { Name = "Footer", Value = "" }, - new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, + new() { Name = "Color", Value = "" }, + new() { Name = "Webhook", Value = "" }, + new() { Name = "ThumbnailUrl", Value = "" }, + new() { Name = "ImageUrl", Value = "" }, + new() { Name = "Footer", Value = "" }, + new() { Name = "Time", Value = "{relative}" }, ]; [JsonPropertyName("DiscordPenaltySilenceSettings")] public DiscordPenaltySetting[] DiscordPenaltySilenceSettings { get; set; } = [ - new DiscordPenaltySetting { Name = "Color", Value = "" }, - new DiscordPenaltySetting { Name = "Webhook", Value = "" }, - new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, - new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, - new DiscordPenaltySetting { Name = "Footer", Value = "" }, - new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, + new() { Name = "Color", Value = "" }, + new() { Name = "Webhook", Value = "" }, + new() { Name = "ThumbnailUrl", Value = "" }, + new() { Name = "ImageUrl", Value = "" }, + new() { Name = "Footer", Value = "" }, + new() { Name = "Time", Value = "{relative}" }, ]; [JsonPropertyName("DiscordPenaltyWarnSettings")] public DiscordPenaltySetting[] DiscordPenaltyWarnSettings { get; set; } = [ - new DiscordPenaltySetting { Name = "Color", Value = "" }, - new DiscordPenaltySetting { Name = "Webhook", Value = "" }, - new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, - new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, - new DiscordPenaltySetting { Name = "Footer", Value = "" }, - new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, + new() { Name = "Color", Value = "" }, + new() { Name = "Webhook", Value = "" }, + new() { Name = "ThumbnailUrl", Value = "" }, + new() { Name = "ImageUrl", Value = "" }, + new() { Name = "Footer", Value = "" }, + new() { Name = "Time", Value = "{relative}" }, ]; [JsonPropertyName("DiscordAssociatedAccountsSettings")] public DiscordPenaltySetting[] DiscordAssociatedAccountsSettings { get; set; } = [ - new DiscordPenaltySetting { Name = "Color", Value = "" }, - new DiscordPenaltySetting { Name = "Webhook", Value = "" }, - new DiscordPenaltySetting { Name = "ThumbnailUrl", Value = "" }, - new DiscordPenaltySetting { Name = "ImageUrl", Value = "" }, - new DiscordPenaltySetting { Name = "Footer", Value = "" }, - new DiscordPenaltySetting { Name = "Time", Value = "{relative}" }, + new() { Name = "Color", Value = "" }, + new() { Name = "Webhook", Value = "" }, + new() { Name = "ThumbnailUrl", Value = "" }, + new() { Name = "ImageUrl", Value = "" }, + new() { Name = "Footer", Value = "" }, + new() { Name = "Time", Value = "{relative}" }, ]; } @@ -124,15 +124,15 @@ public class MenuConfig [JsonPropertyName("Durations")] public DurationItem[] Durations { get; set; } = [ - new DurationItem { Name = "1 minute", Duration = 1 }, - new DurationItem { Name = "5 minutes", Duration = 5 }, - new DurationItem { Name = "15 minutes", Duration = 15 }, - new DurationItem { Name = "1 hour", Duration = 60 }, - new DurationItem { Name = "1 day", Duration = 60 * 24 }, - new DurationItem { Name = "7 days", Duration = 60 * 24 * 7 }, - new DurationItem { Name = "14 days", Duration = 60 * 24 * 14 }, - new DurationItem { Name = "30 days", Duration = 60 * 24 * 30 }, - new DurationItem { Name = "Permanent", Duration = 0 } + new() { Name = "1 minute", Duration = 1 }, + new() { Name = "5 minutes", Duration = 5 }, + new() { Name = "15 minutes", Duration = 15 }, + new() { Name = "1 hour", Duration = 60 }, + new() { Name = "1 day", Duration = 60 * 24 }, + new() { Name = "7 days", Duration = 60 * 24 * 7 }, + new() { Name = "14 days", Duration = 60 * 24 * 14 }, + new() { Name = "30 days", Duration = 60 * 24 * 30 }, + new() { Name = "Permanent", Duration = 0 } ]; [JsonPropertyName("BanReasons")] @@ -177,18 +177,18 @@ public class MenuConfig [JsonPropertyName("AdminFlags")] public AdminFlag[] AdminFlags { get; set; } = [ - new AdminFlag { Name = "Generic", Flag = "@css/generic" }, - new AdminFlag { Name = "Chat", Flag = "@css/chat" }, - new AdminFlag { Name = "Change Map", Flag = "@css/changemap" }, - new AdminFlag { Name = "Slay", Flag = "@css/slay" }, - new AdminFlag { Name = "Kick", Flag = "@css/kick" }, - new AdminFlag { Name = "Ban", Flag = "@css/ban" }, - new AdminFlag { Name = "Perm Ban", Flag = "@css/permban" }, - new AdminFlag { Name = "Unban", Flag = "@css/unban" }, - new AdminFlag { Name = "Show IP", Flag = "@css/showip" }, - new AdminFlag { Name = "Cvar", Flag = "@css/cvar" }, - new AdminFlag { Name = "Rcon", Flag = "@css/rcon" }, - new AdminFlag { Name = "Root (all flags)", Flag = "@css/root" } + new() { Name = "Generic", Flag = "@css/generic" }, + new() { Name = "Chat", Flag = "@css/chat" }, + new() { Name = "Change Map", Flag = "@css/changemap" }, + new() { Name = "Slay", Flag = "@css/slay" }, + new() { Name = "Kick", Flag = "@css/kick" }, + new() { Name = "Ban", Flag = "@css/ban" }, + new() { Name = "Perm Ban", Flag = "@css/permban" }, + new() { Name = "Unban", Flag = "@css/unban" }, + new() { Name = "Show IP", Flag = "@css/showip" }, + new() { Name = "Cvar", Flag = "@css/cvar" }, + new() { Name = "Rcon", Flag = "@css/rcon" }, + new() { Name = "Root (all flags)", Flag = "@css/root" } ]; } @@ -247,26 +247,11 @@ public class OtherSettings public class CS2_SimpleAdminConfig : BasePluginConfig { - [JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 24; - - [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"; + [JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 25; + [JsonPropertyName("DatabaseConfig")] + public DatabaseConfig DatabaseConfig { get; set; } = new(); + [JsonPropertyName("OtherSettings")] public OtherSettings OtherSettings { get; set; } = new(); @@ -303,4 +288,38 @@ public class CS2_SimpleAdminConfig : BasePluginConfig [JsonPropertyName("MenuConfig")] public MenuConfig MenuConfigs { get; set; } = new(); -} \ No newline at end of file +} + + +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 +} diff --git a/CS2-SimpleAdmin/Database/Database.cs b/CS2-SimpleAdmin/Database/Database.cs index bb7926f..64735c2 100644 --- a/CS2-SimpleAdmin/Database/Database.cs +++ b/CS2-SimpleAdmin/Database/Database.cs @@ -11,6 +11,11 @@ public class Database(string dbConnectionString) { var connection = new MySqlConnection(dbConnectionString); connection.Open(); + + // using var cmd = connection.CreateCommand(); + // cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';"; + // cmd.ExecuteNonQueryAsync(); + return connection; } catch (Exception ex) @@ -26,6 +31,11 @@ public class Database(string dbConnectionString) { var connection = new MySqlConnection(dbConnectionString); await connection.OpenAsync(); + + // await using var cmd = connection.CreateCommand(); + // cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';"; + // await cmd.ExecuteNonQueryAsync(); + return connection; } catch (Exception ex) @@ -35,11 +45,11 @@ public class Database(string dbConnectionString) } } - public void DatabaseMigration() - { - Migration migrator = new(this); - migrator.ExecuteMigrations(); - } + // public async Task DatabaseMigration() + // { + // Migration migrator = new(this); + // await migrator.ExecuteMigrationsAsync(); + // } public bool CheckDatabaseConnection(out string? exception) { diff --git a/CS2-SimpleAdmin/Database/IDatabaseProvider.cs b/CS2-SimpleAdmin/Database/IDatabaseProvider.cs new file mode 100644 index 0000000..25bc129 --- /dev/null +++ b/CS2-SimpleAdmin/Database/IDatabaseProvider.cs @@ -0,0 +1,60 @@ +using System.Data.Common; + +namespace CS2_SimpleAdmin.Database; + +public interface IDatabaseProvider +{ + Task 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); +} \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migration.cs b/CS2-SimpleAdmin/Database/Migration.cs index 81e1097..7a25747 100644 --- a/CS2-SimpleAdmin/Database/Migration.cs +++ b/CS2-SimpleAdmin/Database/Migration.cs @@ -1,61 +1,94 @@ -using Microsoft.Extensions.Logging; -using MySqlConnector; +using System.Data.Common; +using Microsoft.Extensions.Logging; namespace CS2_SimpleAdmin.Database; -public class Migration(Database database) +public class Migration(string migrationsPath) { - public void ExecuteMigrations() + /// + /// 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. + /// + public async Task ExecuteMigrationsAsync() { - var migrationsDirectory = CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations"; + if (CS2_SimpleAdmin.DatabaseProvider == null) return; + var files = Directory.GetFiles(migrationsPath, "*.sql").OrderBy(f => f).ToList(); + if (files.Count == 0) return; - var files = Directory.GetFiles(migrationsDirectory, "*.sql") - .OrderBy(f => f); + await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); - using var connection = database.GetConnection(); + await using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = """ + CREATE TABLE IF NOT EXISTS sa_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version TEXT NOT NULL + ); + + """; - // Create sa_migrations table if not exists - using var cmd = new MySqlCommand(""" - CREATE TABLE IF NOT EXISTS `sa_migrations` ( - `id` INT PRIMARY KEY AUTO_INCREMENT, - `version` VARCHAR(255) NOT NULL - ); - """, connection); + await cmd.ExecuteNonQueryAsync(); + } - cmd.ExecuteNonQuery(); - - // Get the last applied migration version - var lastAppliedVersion = GetLastAppliedVersion(connection); + var lastAppliedVersion = await GetLastAppliedVersionAsync(connection); foreach (var file in files) { var version = Path.GetFileNameWithoutExtension(file); + if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0) + continue; - // Check if the migration has already been applied - if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0) continue; - var sqlScript = File.ReadAllText(file); + try + { + var sqlScript = await File.ReadAllTextAsync(file); - using var cmdMigration = new MySqlCommand(sqlScript, connection); - cmdMigration.ExecuteNonQuery(); + await using (var cmdMigration = connection.CreateCommand()) + { + cmdMigration.CommandText = sqlScript; + await cmdMigration.ExecuteNonQueryAsync(); + } - // Update the last applied migration version - UpdateLastAppliedVersion(connection, version); + await UpdateLastAppliedVersionAsync(connection, version); - CS2_SimpleAdmin._logger?.LogInformation($"Migration \"{version}\" successfully applied."); + CS2_SimpleAdmin._logger?.LogInformation($"Migration \"{version}\" successfully applied."); + } + catch (Exception ex) + { + CS2_SimpleAdmin._logger?.LogError(ex, $"Error applying migration \"{version}\"."); + break; + } } } - private static string GetLastAppliedVersion(MySqlConnection connection) + /// + /// Retrieves the version string of the last applied migration from the database. + /// + /// The open database connection. + /// The version string of the last applied migration, or empty string if none. + private static async Task GetLastAppliedVersionAsync(DbConnection connection) { - using var cmd = new MySqlCommand("SELECT `version` FROM `sa_migrations` ORDER BY `id` DESC LIMIT 1;", connection); - var result = cmd.ExecuteScalar(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT version FROM sa_migrations ORDER BY id DESC LIMIT 1;"; + var result = await cmd.ExecuteScalarAsync(); return result?.ToString() ?? string.Empty; } - private static void UpdateLastAppliedVersion(MySqlConnection connection, string version) + /// + /// Inserts a record tracking the successful application of a migration version. + /// + /// The open database connection. + /// The version string of the migration applied. + private static async Task UpdateLastAppliedVersionAsync(DbConnection connection, string version) { - using var cmd = new MySqlCommand("INSERT INTO `sa_migrations` (`version`) VALUES (@Version);", connection); - cmd.Parameters.AddWithValue("@Version", version); - cmd.ExecuteNonQuery(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = "INSERT INTO sa_migrations (version) VALUES (@Version);"; + + var param = cmd.CreateParameter(); + param.ParameterName = "@Version"; + param.Value = version; + cmd.Parameters.Add(param); + + await cmd.ExecuteNonQueryAsync(); } } diff --git a/CS2-SimpleAdmin/Database/Migrations/013_AddNameColumnToSaPlayerIpsTable.sql b/CS2-SimpleAdmin/Database/Migrations/013_AddNameColumnToSaPlayerIpsTable.sql deleted file mode 100644 index 17c9647..0000000 --- a/CS2-SimpleAdmin/Database/Migrations/013_AddNameColumnToSaPlayerIpsTable.sql +++ /dev/null @@ -1,4 +0,0 @@ -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 `name` VARCHAR(64) NULL DEFAULT NULL AFTER `steamid`; -ALTER TABLE `sa_players_ips` ADD INDEX(`used_at`); diff --git a/CS2-SimpleAdmin/Database/Migrations/001_CreateTables.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/001_CreateTables.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/001_CreateTables.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/001_CreateTables.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/002_CreateFlagsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/002_CreateFlagsTable.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/002_CreateFlagsTable.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/002_CreateFlagsTable.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/003_ChangeColumnsPosition.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/003_ChangeColumnsPosition.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/003_ChangeColumnsPosition.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/003_ChangeColumnsPosition.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/004_MoveOldFlagsToFlagsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/004_MoveOldFlagsToFlagsTable.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/004_MoveOldFlagsToFlagsTable.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/004_MoveOldFlagsToFlagsTable.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/005_CreateUnbansTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/005_CreateUnbansTable.sql similarity index 87% rename from CS2-SimpleAdmin/Database/Migrations/005_CreateUnbansTable.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/005_CreateUnbansTable.sql index 6666539..7000cd5 100644 --- a/CS2-SimpleAdmin/Database/Migrations/005_CreateUnbansTable.sql +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/005_CreateUnbansTable.sql @@ -16,8 +16,8 @@ CREATE TABLE IF NOT EXISTS `sa_unmutes` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; -INSERT INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`) -VALUES (-1, 'Console', 'Console', '', '0', NULL, NULL, NOW()); +INSERT IGNORE INTO `sa_admins` (`id`, `player_name`, `player_steamid`, `flags`, `immunity`, `server_id`, `ends`, `created`) +VALUES (0, 'Console', 'Console', '', '0', NULL, NULL, NOW()); UPDATE `sa_admins` SET `id` = 0 WHERE `id` = -1; diff --git a/CS2-SimpleAdmin/Database/Migrations/006_ServerGroupsFeature.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/006_ServerGroupsFeature.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/006_ServerGroupsFeature.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/006_ServerGroupsFeature.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/007_ServerGroupsGlobal.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/007_ServerGroupsGlobal.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/007_ServerGroupsGlobal.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/007_ServerGroupsGlobal.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/008_OnlineTimeInPenalties.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/008_OnlineTimeInPenalties.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/008_OnlineTimeInPenalties.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/008_OnlineTimeInPenalties.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/009_BanAllUsedIpAddress.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/009_BanAllUsedIpAddress.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/009_BanAllUsedIpAddress.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/009_BanAllUsedIpAddress.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/010_CreateWarnsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/010_CreateWarnsTable.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/010_CreateWarnsTable.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/010_CreateWarnsTable.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/011_AddRconColumnToServersTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/011_AddRconColumnToServersTable.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/011_AddRconColumnToServersTable.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/011_AddRconColumnToServersTable.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/012_AddUpdatedAtColumnToSaBansTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/012_AddUpdatedAtColumnToSaBansTable.sql similarity index 100% rename from CS2-SimpleAdmin/Database/Migrations/012_AddUpdatedAtColumnToSaBansTable.sql rename to CS2-SimpleAdmin/Database/Migrations/Mysql/012_AddUpdatedAtColumnToSaBansTable.sql diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/013_AddNameColumnToSaPlayerIpsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/013_AddNameColumnToSaPlayerIpsTable.sql new file mode 100644 index 0000000..81a26a6 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/013_AddNameColumnToSaPlayerIpsTable.sql @@ -0,0 +1,13 @@ +DELETE FROM `sa_players_ips` +WHERE `id` NOT IN ( + SELECT * FROM ( + SELECT MIN(`id`) + FROM `sa_players_ips` + GROUP BY `steamid` + ) AS `keep_ids` +); +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`; \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/014_AddIndexesToMutesTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/014_AddIndexesToMutesTable.sql new file mode 100644 index 0000000..b70cba2 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/014_AddIndexesToMutesTable.sql @@ -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); diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/015_steamidToBigInt.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/015_steamidToBigInt.sql new file mode 100644 index 0000000..be13d0f --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/015_steamidToBigInt.sql @@ -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; + diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/001_CreateTables.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/001_CreateTables.sql new file mode 100644 index 0000000..24e6df0 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/001_CreateTables.sql @@ -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`) + ); \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/002_CreateFlagsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/002_CreateFlagsTable.sql new file mode 100644 index 0000000..6ea1e0b --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/002_CreateFlagsTable.sql @@ -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 + ); \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/003_ChangeColumnsPosition.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/003_ChangeColumnsPosition.sql new file mode 100644 index 0000000..e69de29 diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/004_MoveOldFlagsToFlagsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/004_MoveOldFlagsToFlagsTable.sql new file mode 100644 index 0000000..9f1b059 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/004_MoveOldFlagsToFlagsTable.sql @@ -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) != ''; \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/005_CreateUnbansTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/005_CreateUnbansTable.sql new file mode 100644 index 0000000..00dabbe --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/005_CreateUnbansTable.sql @@ -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; \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/006_ServerGroupsFeature.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/006_ServerGroupsFeature.sql new file mode 100644 index 0000000..8406f5b --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/006_ServerGroupsFeature.sql @@ -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; \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/007_ServerGroupsGlobal.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/007_ServerGroupsGlobal.sql new file mode 100644 index 0000000..e69de29 diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/008_OnlineTimeInPenalties.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/008_OnlineTimeInPenalties.sql new file mode 100644 index 0000000..588a2f8 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/008_OnlineTimeInPenalties.sql @@ -0,0 +1 @@ +ALTER TABLE `sa_mutes` ADD `passed` INTEGER NULL; diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql new file mode 100644 index 0000000..e28a4b3 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/009_BanAllUsedIpAddress.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `sa_players_ips` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `steamid` INTEGER NOT NULL, + `address` VARCHAR(64) NOT NULL, + `used_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (`steamid`, `address`) + ); \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/010_CreateWarnsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/010_CreateWarnsTable.sql new file mode 100644 index 0000000..99879bf --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/010_CreateWarnsTable.sql @@ -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' + ); \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/011_AddRconColumnToServersTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/011_AddRconColumnToServersTable.sql new file mode 100644 index 0000000..b3235bc --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/011_AddRconColumnToServersTable.sql @@ -0,0 +1 @@ +ALTER TABLE `sa_servers` ADD `rcon_password` VARCHAR(128) NULL; \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/012_AddUpdatedAtColumnToSaBansTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/012_AddUpdatedAtColumnToSaBansTable.sql new file mode 100644 index 0000000..fa99b22 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/012_AddUpdatedAtColumnToSaBansTable.sql @@ -0,0 +1 @@ +ALTER TABLE `sa_bans` ADD COLUMN `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/013_AddNameColumnToSaPlayerIpsTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/013_AddNameColumnToSaPlayerIpsTable.sql new file mode 100644 index 0000000..0c70054 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/013_AddNameColumnToSaPlayerIpsTable.sql @@ -0,0 +1,9 @@ +DELETE FROM `sa_players_ips` +WHERE `id` NOT IN ( + SELECT MIN(`id`) + FROM `sa_players_ips` + GROUP BY `steamid` +); + +ALTER TABLE `sa_players_ips` ADD `name` VARCHAR(64) NULL DEFAULT NULL; +CREATE INDEX IF NOT EXISTS `idx_sa_players_ips_used_at` ON `sa_players_ips` (`used_at` DESC); \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/014_AddIndexesToMutesTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/014_AddIndexesToMutesTable.sql new file mode 100644 index 0000000..cfb7634 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/014_AddIndexesToMutesTable.sql @@ -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`); \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/015_steamidToBigInt.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/015_steamidToBigInt.sql new file mode 100644 index 0000000..9e1eb71 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/015_steamidToBigInt.sql @@ -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]*'; \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs new file mode 100644 index 0000000..91de7bb --- /dev/null +++ b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs @@ -0,0 +1,384 @@ +using System.Data.Common; +using MySqlConnector; + +namespace CS2_SimpleAdmin.Database; + +public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider +{ + public async Task CreateConnectionAsync() + { + var connection = new MySqlConnection(connectionString); + await connection.OpenAsync(); + 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; +} + diff --git a/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs b/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs new file mode 100644 index 0000000..bd63d7b --- /dev/null +++ b/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs @@ -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 CreateConnectionAsync() + { + var conn = new SQLiteConnection(_connectionString); + await conn.OpenAsync(); + return conn; + } + + public async Task<(bool Success, string? Exception)> CheckConnectionAsync() + { + try + { + await using var conn = await CreateConnectionAsync(); + await using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + await cmd.ExecuteScalarAsync(); + return (true, null); + } + catch (Exception ex) + { + return (false, ex.Message); + } + } + + public Task DatabaseMigrationAsync() + { + var migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Sqlite"); + return migration.ExecuteMigrationsAsync(); + } + + public string GetBanSelectQuery(bool multiServer) => + multiServer + ? """ + SELECT id AS Id, + player_name AS PlayerName, + player_steamid AS PlayerSteamId, + player_ip AS PlayerIp, + status AS Status + FROM sa_bans + """ + : """ + SELECT id AS Id, + player_name AS PlayerName, + player_steamid AS PlayerSteamId, + player_ip AS PlayerIp, + status AS Status + FROM sa_bans + WHERE server_id = @serverId + """; + + public static string GetBanUpdatedSelectQuery(bool multiServer) => + multiServer + ? """ + SELECT id AS Id, + player_name AS PlayerName, + player_steamid AS PlayerSteamId, + player_ip AS PlayerIp, + status AS Status + FROM sa_bans + WHERE updated_at > @lastUpdate OR created > @lastUpdate + ORDER BY updated_at DESC + """ + : """ + SELECT id AS Id, + player_name AS PlayerName, + player_steamid AS PlayerSteamId, + player_ip AS PlayerIp, + status AS Status + FROM sa_bans + WHERE (updated_at > @lastUpdate OR created > @lastUpdate) + AND server_id = @serverId + ORDER BY updated_at DESC + """; + + public string GetIpHistoryQuery() => + "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC"; + + public string GetBanUpdateQuery(bool multiServer) => + multiServer + ? """ + UPDATE sa_bans + SET player_ip = COALESCE(player_ip, @PlayerIP), + player_name = COALESCE(player_name, @PlayerName) + WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) + AND status = 'ACTIVE' + AND (duration = 0 OR ends > @CurrentTime) + """ + : """ + UPDATE sa_bans + SET player_ip = COALESCE(player_ip, @PlayerIP), + player_name = COALESCE(player_name, @PlayerName) + WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) + AND status = 'ACTIVE' + AND (duration = 0 OR ends > @CurrentTime) + AND server_id = @ServerId + """; + + public string GetAddBanQuery() => + """ + INSERT INTO sa_bans + (player_steamid, player_name, player_ip, admin_steamid, admin_name, reason, duration, ends, created, server_id) + VALUES + (@playerSteamid, @playerName, @playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid); + SELECT last_insert_rowid(); + """; + + public string GetAddBanBySteamIdQuery() => + """ + INSERT INTO sa_bans + (player_steamid, admin_steamid, admin_name, reason, duration, ends, created, server_id) + VALUES + (@playerSteamid, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid); + SELECT last_insert_rowid(); + """; + + public string GetAddBanByIpQuery() => + """ + INSERT INTO sa_bans + (player_ip, admin_steamid, admin_name, reason, duration, ends, created, server_id) + VALUES + (@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid); + SELECT last_insert_rowid(); + """; + + public string GetUnbanRetrieveBansQuery(bool multiServer) => + multiServer + ? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'" + : "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid"; + + public string GetUnbanAdminIdQuery() => + "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId"; + + public string GetInsertUnbanQuery(bool includeReason) => + includeReason + ? "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT last_insert_rowid();" + : "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT last_insert_rowid();"; + + public string GetUpdateBanStatusQuery() => + "UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId"; + + public string GetExpireBansQuery(bool multiServer) => + multiServer + ? "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime" + : "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid"; + + public string GetExpireIpBansQuery(bool multiServer) => + multiServer + ? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime" + : "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid"; + + public string GetAdminsQuery() => + """ + SELECT sa_admins.player_steamid, sa_admins.player_name, sa_admins_flags.flag, sa_admins.immunity, sa_admins.ends + FROM sa_admins_flags + JOIN sa_admins ON sa_admins_flags.admin_id = sa_admins.id + WHERE (sa_admins.ends IS NULL OR sa_admins.ends > @CurrentTime) + AND (sa_admins.server_id IS NULL OR sa_admins.server_id = @serverid) + ORDER BY sa_admins.player_steamid + """; + + public string GetDeleteAdminQuery(bool globalDelete) => + globalDelete + ? "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID" + : "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId"; + + public string GetAddAdminQuery() => + """ + INSERT INTO sa_admins (player_steamid, player_name, immunity, ends, created, server_id) + VALUES (@playerSteamId, @playerName, @immunity, @ends, @created, @serverid); + SELECT last_insert_rowid(); + """; + + public string GetGroupsQuery() => + """ + SELECT g.group_id, sg.name AS group_name, sg.immunity, f.flag + FROM sa_groups_flags f + JOIN sa_groups_servers g ON f.group_id = g.group_id + JOIN sa_groups sg ON sg.id = g.group_id + WHERE (g.server_id = @serverid OR server_id IS NULL) + """; + + public string GetAddAdminFlagsQuery() => + "INSERT INTO sa_admins_flags (admin_id, flag) VALUES (@adminId, @flag);"; + + public string GetUpdateAdminGroupQuery() => + "UPDATE sa_admins SET group_id = @groupId WHERE id = @adminId;"; + + public string GetAddGroupQuery() => + """ + INSERT INTO sa_groups (name, immunity) VALUES (@groupName, @immunity); + SELECT last_insert_rowid(); + """; + + public string GetGroupIdByNameQuery() => + """ + SELECT sgs.group_id + FROM sa_groups_servers sgs + JOIN sa_groups sg ON sgs.group_id = sg.id + WHERE sg.name = @groupName + ORDER BY (sgs.server_id = @serverId) DESC, sgs.server_id ASC + LIMIT 1; + """; + + public string GetAddGroupFlagsQuery() => + "INSERT INTO sa_groups_flags (group_id, flag) VALUES (@groupId, @flag);"; + + public string GetAddGroupServerQuery() => + "INSERT INTO sa_groups_servers (group_id, server_id) VALUES (@groupId, @server_id);"; + + public string GetDeleteGroupQuery() => + "DELETE FROM sa_groups WHERE name = @groupName;"; + + public string GetDeleteOldAdminsQuery() => + "DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime;"; + + public string GetAddMuteQuery(bool includePlayerName) => + includePlayerName + ? """ + INSERT INTO sa_mutes + (player_steamid, player_name, admin_steamid, admin_name, reason, duration, ends, created, type, server_id) + VALUES (@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid); + SELECT last_insert_rowid(); + """ + : """ + INSERT INTO sa_mutes + (player_steamid, admin_steamid, admin_name, reason, duration, ends, created, type, server_id) + VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid); + SELECT last_insert_rowid(); + """; + + public string GetIsMutedQuery(bool multiServer, int timeMode) => + multiServer + ? (timeMode == 1 + ? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)" + : "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))") + : (timeMode == 1 + ? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid" + : "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid"); + + public string GetMuteStatsQuery(bool multiServer) => + multiServer + ? """ + SELECT + COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes, + COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags, + COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences + FROM sa_mutes + WHERE player_steamid = @PlayerSteamID; + """ + : """ + SELECT + COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes, + COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags, + COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences + FROM sa_mutes + WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId; + """; + + public string GetUpdateMutePassedQuery(bool multiServer) => + multiServer + ? "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'" + : "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid"; + + public string GetCheckExpiredMutesQuery(bool multiServer) => + multiServer + ? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'" + : "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid"; + + public string GetRetrieveMutesQuery(bool multiServer) => + multiServer + ? "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'" + : "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid"; + + public string GetUnmuteAdminIdQuery() => + "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId"; + + public string GetInsertUnmuteQuery(bool includeReason) => + includeReason + ? "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT last_insert_rowid();" + : "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT last_insert_rowid();"; + + public string GetUpdateMuteStatusQuery() => + "UPDATE sa_mutes SET status = 'UNMUTED', unmute_id = @unmuteId WHERE id = @muteId"; + + public string GetExpireMutesQuery(bool multiServer, int timeMode) => + multiServer + ? (timeMode == 1 + ? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime" + : "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration") + : (timeMode == 1 + ? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid" + : "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration AND server_id = @serverid"); + + public string GetAddWarnQuery(bool includePlayerName) => + includePlayerName + ? """ + INSERT INTO sa_warns + (player_steamid, player_name, admin_steamid, admin_name, reason, duration, ends, created, server_id) + VALUES + (@playerSteamid, @playerName, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid); + SELECT last_insert_rowid(); + """ + : """ + INSERT INTO sa_warns + (player_steamid, admin_steamid, admin_name, reason, duration, ends, created, server_id) + VALUES + (@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid); + SELECT last_insert_rowid(); + """; + + public string GetPlayerWarnsQuery(bool multiServer, bool active) => + multiServer + ? active + ? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC" + : "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC" + : active + ? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC" + : "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC"; + + public string GetPlayerWarnsCountQuery(bool multiServer, bool active) => + multiServer + ? active + ? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'" + : "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID" + : active + ? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'" + : "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid"; + + public string GetUnwarnByIdQuery(bool multiServer) => + multiServer + ? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId" + : "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid"; + + public string GetUnwarnLastQuery(bool multiServer) => + multiServer + ? """ + UPDATE sa_warns + SET status = 'EXPIRED' + WHERE status = 'ACTIVE' + AND player_steamid = @steamid + ORDER BY id DESC + LIMIT 1 + """ + : """ + UPDATE sa_warns + SET status = 'EXPIRED' + WHERE status = 'ACTIVE' + AND player_steamid = @steamid + AND server_id = @serverid + ORDER BY id DESC + LIMIT 1 + """; + + public string GetExpireWarnsQuery(bool multiServer) => + multiServer + ? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime" + : "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid"; +} diff --git a/CS2-SimpleAdmin/Events.cs b/CS2-SimpleAdmin/Events.cs index d872795..68847f4 100644 --- a/CS2-SimpleAdmin/Events.cs +++ b/CS2-SimpleAdmin/Events.cs @@ -1,9 +1,9 @@ -using CounterStrikeSharp.API; +using System.Numerics; +using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Entities; -using CounterStrikeSharp.API.Modules.Utils; using CS2_SimpleAdmin.Managers; using CS2_SimpleAdmin.Models; using CS2_SimpleAdminApi; @@ -12,7 +12,6 @@ using System.Text; using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.UserMessages; -using CounterStrikeSharp.API.ValveConstants.Protobuf; namespace CS2_SimpleAdmin; @@ -23,7 +22,9 @@ public partial class CS2_SimpleAdmin private void RegisterEvents() { RegisterListener(OnMapStart); + // RegisterListener(OnClientConnect); RegisterListener(OnClientConnect); + RegisterListener(OnClientConnected); RegisterListener(OnGameServerSteamAPIActivated); if (Config.OtherSettings.UserMessageGagChatType) HookUserMessage(118, HookUmChat); @@ -34,6 +35,22 @@ public partial class CS2_SimpleAdmin // AddCommandListener("say_team", OnCommandTeamSay); } + private void UnregisterEvents() + { + RemoveListener(OnMapStart); + RemoveListener(OnClientConnect); + RemoveListener(OnClientConnected); + RemoveListener(OnGameServerSteamAPIActivated); + if (Config.OtherSettings.UserMessageGagChatType) + UnhookUserMessage(118, HookUmChat); + + RemoveCommandListener(null!, ComamndListenerHandler, HookMode.Pre); + // AddCommandListener("callvote", OnCommandCallVote); + // AddCommandListener("say", OnCommandSay); + // AddCommandListener("say_team", OnCommandTeamSay); + } + + // private HookResult OnCommandCallVote(CCSPlayerController? caller, CommandInfo info) // { // var voteType = info.GetArg(1).ToLower(); @@ -53,7 +70,7 @@ public partial class CS2_SimpleAdmin private void OnGameServerSteamAPIActivated() { - if (_serverLoading) + if (ServerLoaded || _serverLoading) return; _serverLoading = true; @@ -72,7 +89,18 @@ public partial class CS2_SimpleAdmin Logger.LogCritical("[OnClientDisconnect] Before"); #endif - if (player == null || !player.IsValid || player.IsBot) + if (player == null || !player.IsValid || player.IsHLTV) + return HookResult.Continue; + + BotPlayers.Remove(player); + CachedPlayers.Remove(player); + + SilentPlayers.Remove(player.Slot); + GodPlayers.Remove(player.Slot); + SpeedPlayers.Remove(player); + GravityPlayers.Remove(player); + + if (player.IsBot) return HookResult.Continue; #if DEBUG @@ -94,32 +122,28 @@ public partial class CS2_SimpleAdmin } else { - DisconnectedPlayers.Add(new DisconnectedPlayer(steamId, player.PlayerName, player.IpAddress?.Split(":")[0], Time.ActualDateTime())); + DisconnectedPlayers.Add(new DisconnectedPlayer(steamId, player.PlayerName, + player.IpAddress?.Split(":")[0], Time.ActualDateTime())); } - - PlayerPenaltyManager.RemoveAllPenalties(player.Slot); - - SilentPlayers.Remove(player.Slot); - GodPlayers.Remove(player.Slot); - SpeedPlayers.Remove(player.Slot); - GravityPlayers.Remove(player); - - if (player.UserId.HasValue) - PlayersInfo.TryRemove(player.UserId.Value, out _); - if (!PermissionManager.AdminCache.TryGetValue(steamId, out var data) + PlayerPenaltyManager.RemoveAllPenalties(player.Slot); + + if (player.UserId.HasValue) + PlayersInfo.TryRemove(player.SteamID, out _); + + if (!PermissionManager.AdminCache.TryGetValue(steamId, out var data) || !(data.ExpirationTime <= Time.ActualDateTime())) { return HookResult.Continue; } - + AdminManager.RemovePlayerPermissions(steamId, PermissionManager.AdminCache[steamId].Flags.ToArray()); AdminManager.RemovePlayerFromGroup(steamId, true, PermissionManager.AdminCache[steamId].Flags.ToArray()); var adminData = AdminManager.GetPlayerAdminData(steamId); - + if (adminData == null || data.Flags.ToList().Count != 0 && adminData.Groups.ToList().Count != 0) return HookResult.Continue; - + AdminManager.ClearPlayerPermissions(steamId); AdminManager.RemovePlayerAdminData(steamId); @@ -131,37 +155,68 @@ public partial class CS2_SimpleAdmin return HookResult.Continue; } } - - private void OnClientConnect(int playerslot, string name, string ipaddress) + + private void OnClientConnect(int playerslot, string name, string ipAddress) { #if DEBUG Logger.LogCritical("[OnClientConnect]"); #endif - if (Config.OtherSettings.BanType == 0) + + var player = Utilities.GetPlayerFromSlot(playerslot); + if (player == null || !player.IsValid || player.IsBot) return; - if (Instance.CacheManager != null && !Instance.CacheManager.IsPlayerBanned(null, ipaddress.Split(":")[0])) - return; - - Server.NextFrame((() => - { - var player = Utilities.GetPlayerFromSlot(playerslot); - if (player == null || !player.IsValid || player.IsBot) - return; - - Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED); - })); - - // Server.NextFrame(() => - // { - // var player = Utilities.GetPlayerFromSlot(playerslot); - // - // if (player == null || !player.IsValid || player.IsBot) - // return; - // - // new PlayerManager().LoadPlayerData(player); - // }); + PlayerManager.LoadPlayerData(player); } + + private void OnClientConnected(int playerslot) + { +#if DEBUG + Logger.LogCritical("[OnClientConnected]"); +#endif + + var player = Utilities.GetPlayerFromSlot(playerslot); + if (player == null || !player.IsValid || player.IsBot) + return; + + PlayerManager.LoadPlayerData(player); + } + +// private void OnClientConnect(int playerslot, string name, string ipaddress) +// { +// #if DEBUG +// Logger.LogCritical("[OnClientConnect]"); +// #endif +// if (Config.OtherSettings.BanType == 0) +// return; +// +// if (Instance.CacheManager != null && !Instance.CacheManager.IsPlayerBanned(null, ipaddress.Split(":")[0])) +// return; +// +// var testPlayer = Utilities.GetPlayerFromSlot(playerslot); +// if (testPlayer == null) +// return; +// Logger.LogInformation($"Gracz {testPlayer.PlayerName} ({testPlayer.SteamID.ToString()}) Czas: {DateTime.Now}"); +// +// Server.NextFrame((() => +// { +// var player = Utilities.GetPlayerFromSlot(playerslot); +// if (player == null || !player.IsValid || player.IsBot) +// return; +// +// Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED); +// })); +// +// // Server.NextFrame(() => +// // { +// // var player = Utilities.GetPlayerFromSlot(playerslot); +// // +// // if (player == null || !player.IsValid || player.IsBot) +// // return; +// // +// // new PlayerManager().LoadPlayerData(player); +// // }); +// } [GameEventHandler] public HookResult OnPlayerFullConnect(EventPlayerConnectFull @event, GameEventInfo info) @@ -172,15 +227,16 @@ public partial class CS2_SimpleAdmin var player = @event.Userid; - if (player == null || !player.IsValid || player.IsBot) + if (player == null || !player.IsValid) return HookResult.Continue; -// if (player.UserId.HasValue && PlayersInfo.TryGetValue(player.UserId.Value, out PlayerInfo? value) && -// value.WaitingForKick) -// return HookResult.Continue; - - new PlayerManager().LoadPlayerData(player); + if (player is { IsBot: true, IsHLTV: false }) + { + BotPlayers.Add(player); + return HookResult.Continue; + } + PlayerManager.LoadPlayerData(player, true); return HookResult.Continue; } @@ -228,26 +284,25 @@ public partial class CS2_SimpleAdmin if (!PlayerPenaltyManager.IsPenalized(author.Slot, PenaltyType.Gag, out DateTime? endDateTime) && !PlayerPenaltyManager.IsPenalized(author.Slot, PenaltyType.Silence, out endDateTime)) return HookResult.Continue; + + if (_localizer == null || endDateTime == null) + return HookResult.Continue; var message = um.ReadString("param2"); - - if (_localizer == null || endDateTime is null) return HookResult.Continue; - - if (CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger).Any(trigger => message.StartsWith(trigger))) + var triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger); + if (!triggers.Any(trigger => message.StartsWith(trigger))) return HookResult.Stop; + + for (var i = um.Recipients.Count - 1; i >= 0; i--) { - foreach (var recipient in um.Recipients) + if (um.Recipients[i] != author) { - if (recipient == author) - continue; - - um.Recipients.Remove(recipient); + um.Recipients.RemoveAt(i); } - - return HookResult.Continue; } - author.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", author.GetLanguage())); - return HookResult.Stop; + return HookResult.Continue; + + // author.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", author.GetLanguage())); } private HookResult ComamndListenerHandler(CCSPlayerController? player, CommandInfo info) @@ -286,8 +341,17 @@ public partial class CS2_SimpleAdmin if (!command.Contains("say")) return HookResult.Continue; - if (!Config.OtherSettings.UserMessageGagChatType) + if (info.GetArg(1).Length == 0) + return HookResult.Stop; + + var triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger); + if (triggers.Any(trigger => info.GetArg(1).StartsWith(trigger))) { + return HookResult.Continue; + } + + // if (!Config.OtherSettings.UserMessageGagChatType) + // { if (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out DateTime? endDateTime) || PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out endDateTime)) { @@ -295,17 +359,9 @@ public partial class CS2_SimpleAdmin player.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", player.GetLanguage())); return HookResult.Stop; } - } - - if (info.GetArg(1).StartsWith($"/") - || info.GetArg(1).StartsWith($"!")) - return HookResult.Continue; - - if (info.GetArg(1).Length == 0) - return HookResult.Stop; + // } - if (command == "say" && info.GetArg(1).StartsWith($"@") && - AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat")) + if (AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat") && command == "say" && info.GetArg(1).StartsWith($"@")) { player.ExecuteClientCommandFromServer($"css_say {info.GetArg(1).Remove(0, 1)}"); return HookResult.Stop; @@ -395,10 +451,13 @@ public partial class CS2_SimpleAdmin private void OnMapStart(string mapName) { + if (!ServerLoaded || ServerId == null) + AddTimer(2.0f, OnGameServerSteamAPIActivated); + if (Config.OtherSettings.ReloadAdminsEveryMapChange && ServerLoaded && ServerId != null) AddTimer(5.0f, () => ReloadAdmins(null)); - AddTimer(1.0f, () => ServerManager.CheckHibernationStatus()); + AddTimer(1.0f, ServerManager.CheckHibernationStatus); // AddTimer(34, () => // { @@ -421,9 +480,6 @@ public partial class CS2_SimpleAdmin if (player is null || @event.Attacker is null || player.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE || player.PlayerPawn.Value == null) return HookResult.Continue; - - if (SpeedPlayers.TryGetValue(player.Slot, out var speedPlayer)) - AddTimer(0.15f, () => player.SetSpeed(speedPlayer)); if (!GodPlayers.Contains(player.Slot)) return HookResult.Continue; @@ -441,22 +497,21 @@ public partial class CS2_SimpleAdmin if (player?.UserId == null || !player.IsValid || player.IsHLTV || player.Connected != PlayerConnectedState.PlayerConnected) return HookResult.Continue; - SpeedPlayers.Remove(player.Slot); + SpeedPlayers.Remove(player); GravityPlayers.Remove(player); - if (!PlayersInfo.ContainsKey(player.UserId.Value) || @event.Attacker == null) + if (!PlayersInfo.ContainsKey(player.SteamID) || @event.Attacker == null) return HookResult.Continue; var playerPosition = player.PlayerPawn.Value?.AbsOrigin; var playerRotation = player.PlayerPawn.Value?.AbsRotation; - - PlayersInfo[player.UserId.Value].DiePosition = new DiePosition( - new Vector( + PlayersInfo[player.SteamID].DiePosition = new DiePosition( + new Vector3( playerPosition?.X ?? 0, playerPosition?.Y ?? 0, playerPosition?.Z ?? 0 ), - new QAngle( + new Vector3( playerRotation?.X ?? 0, playerRotation?.Y ?? 0, playerRotation?.Z ?? 0 @@ -470,17 +525,17 @@ public partial class CS2_SimpleAdmin public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info) { var player = @event.Userid; - if (player == null || !player.IsValid || player.IsBot) return HookResult.Continue; if (!SilentPlayers.Contains(player.Slot)) return HookResult.Continue; - info.DontBroadcast = true; - - if (@event.Team > 1) + if (@event is { Oldteam: <= 1, Team: >= 1 }) + { SilentPlayers.Remove(player.Slot); + SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false); + } return HookResult.Continue; } diff --git a/CS2-SimpleAdmin/Extensions/PlayerExtensions.cs b/CS2-SimpleAdmin/Extensions/PlayerExtensions.cs index 93088af..0477263 100644 --- a/CS2-SimpleAdmin/Extensions/PlayerExtensions.cs +++ b/CS2-SimpleAdmin/Extensions/PlayerExtensions.cs @@ -1,4 +1,6 @@ -using CounterStrikeSharp.API; +using System.Drawing; +using System.Numerics; +using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Translations; using CounterStrikeSharp.API.Modules.Admin; @@ -13,11 +15,21 @@ namespace CS2_SimpleAdmin; public static class PlayerExtensions { + /// + /// Slaps the player pawn by applying optional damage and adding a random velocity knockback. + /// + /// The player pawn to slap. + /// The amount of damage to apply (default is 0). public static void Slap(this CBasePlayerPawn pawn, int damage = 0) { PerformSlap(pawn, damage); } + /// + /// Prints a localized chat message to the player with a prefix. + /// + /// The player controller to send the message to. + /// The message string. public static void Print(this CCSPlayerController controller, string message = "") { StringBuilder _message = new(CS2_SimpleAdmin._localizer!["sa_prefix"]); @@ -25,6 +37,12 @@ public static class PlayerExtensions controller.PrintToChat(_message.ToString()); } + /// + /// Determines if the player controller can target another player controller, respecting admin permissions and immunity. + /// + /// The player controller who wants to target. + /// The player controller being targeted. + /// True if targeting is allowed, false otherwise. public static bool CanTarget(this CCSPlayerController? controller, CCSPlayerController? target) { if (controller is null || target is null) return true; @@ -36,6 +54,12 @@ public static class PlayerExtensions AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(target); } + /// + /// Checks if the controller can target a player by SteamID, considering targeting permissions and immunities. + /// + /// The attacker player controller. + /// The SteamID of the target player. + /// True if targeting is permitted, false otherwise. public static bool CanTarget(this CCSPlayerController? controller, SteamID steamId) { if (controller is null) return true; @@ -44,6 +68,11 @@ public static class PlayerExtensions AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(steamId); } + /// + /// Sets the movement speed modifier of the player controller. + /// + /// The player controller. + /// The speed modifier value. public static void SetSpeed(this CCSPlayerController? controller, float speed) { var playerPawnValue = controller?.PlayerPawn.Value; @@ -52,14 +81,24 @@ public static class PlayerExtensions playerPawnValue.VelocityModifier = speed; } + /// + /// Sets the gravity scale for the player controller. + /// + /// The player controller. + /// The gravity scale. public static void SetGravity(this CCSPlayerController? controller, float gravity) { var playerPawnValue = controller?.PlayerPawn.Value; if (playerPawnValue == null) return; - playerPawnValue.GravityScale = gravity; + playerPawnValue.ActualGravityScale = gravity; } + /// + /// Sets the player's in-game money amount. + /// + /// The player controller. + /// The amount of money to set. public static void SetMoney(this CCSPlayerController? controller, int money) { var moneyServices = controller?.InGameMoneyServices; @@ -70,6 +109,11 @@ public static class PlayerExtensions if (controller != null) Utilities.SetStateChanged(controller, "CCSPlayerController", "m_pInGameMoneyServices"); } + /// + /// Sets the player's health points. + /// + /// The player controller. + /// The health value, default is 100. public static void SetHp(this CCSPlayerController? controller, int health = 100) { if (controller == null) return; @@ -85,36 +129,76 @@ public static class PlayerExtensions Utilities.SetStateChanged(controller.PlayerPawn.Value, "CBaseEntity", "m_iHealth"); } + /// + /// Buries the player pawn by moving it down by a depth offset. + /// + /// The player pawn to bury. + /// The depth offset (default 10 units). public static void Bury(this CBasePlayerPawn pawn, float depth = 10f) { - var newPos = new Vector(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y, + var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y, pawn.AbsOrigin!.Z - depth); + var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z); + var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z); - pawn.Teleport(newPos, pawn.AbsRotation!, pawn.AbsVelocity); + pawn.Teleport(newPos, newRotation, newVelocity); } + /// + /// Unburies the player pawn by moving it up by a depth offset. + /// + /// The player pawn to unbury. + /// The depth offset (default 15 units). public static void Unbury(this CBasePlayerPawn pawn, float depth = 15f) { - var newPos = new Vector(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y, + var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y, pawn.AbsOrigin!.Z + depth); + var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z); + var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z); - pawn.Teleport(newPos, pawn.AbsRotation!, pawn.AbsVelocity); + pawn.Teleport(newPos, newRotation, newVelocity); } + /// + /// Freezes the player pawn, disabling movement. + /// + /// The player pawn to freeze. public static void Freeze(this CBasePlayerPawn pawn) { pawn.MoveType = MoveType_t.MOVETYPE_INVALID; Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 11); // invalid Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType"); } - + + /// + /// Unfreezes the player pawn, enabling movement. + /// + /// The player pawn to unfreeze. public static void Unfreeze(this CBasePlayerPawn pawn) { pawn.MoveType = MoveType_t.MOVETYPE_WALK; Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 2); // walk Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType"); } + + /// + /// Changes the player's color tint to specified RGBA values. + /// + /// The pawn to colorize. + /// Red component (0-255). + /// Green component (0-255). + /// Blue component (0-255). + /// Alpha (transparency) component (0-255). + public static void Colorize(this CBasePlayerPawn pawn, int r = 255, int g = 255, int b = 255, int a = 255) + { + pawn.Render = Color.FromArgb(a, r, g, b); + Utilities.SetStateChanged(pawn, "CBaseModelEntity", "m_clrRender"); + } + /// + /// Toggles noclip mode for the player pawn. + /// + /// The player pawn. public static void ToggleNoclip(this CBasePlayerPawn pawn) { if (pawn.MoveType == MoveType_t.MOVETYPE_NOCLIP) @@ -131,6 +215,11 @@ public static class PlayerExtensions } } + /// + /// Renames the player controller to a new name, with fallback to a localized "Unknown". + /// + /// The player controller to rename. + /// The new name to assign. public static void Rename(this CCSPlayerController? controller, string newName = "Unknown") { newName ??= CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; @@ -158,6 +247,11 @@ public static class PlayerExtensions }); } + /// + /// Teleports a player controller to the position, rotation, and velocity of another player controller. + /// + /// The controller to teleport. + /// The target controller whose position to copy. public static void TeleportPlayer(this CCSPlayerController? controller, CCSPlayerController? target) { if (controller?.PlayerPawn.Value == null && target?.PlayerPawn.Value == null) @@ -176,6 +270,11 @@ public static class PlayerExtensions } } + /// + /// Applies a slap effect to the given player pawn, optionally inflicting damage and adding velocity knockback. + /// + /// The player pawn to slap. + /// The amount of damage to deal (default is 0). private static void PerformSlap(CBasePlayerPawn pawn, int damage = 0) { if (pawn.LifeState != (int)LifeState_t.LIFE_ALIVE) @@ -186,7 +285,7 @@ public static class PlayerExtensions /* Teleport in a random direction - thank you, Mani!*/ /* Thank you AM & al!*/ var random = new Random(); - var vel = new Vector(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z); + var vel = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z); vel.X += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1); vel.Y += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1); @@ -217,6 +316,16 @@ public static class PlayerExtensions pawn.CommitSuicide(true, true); } + /// + /// Sends a localized chat message to the player controller. + /// The message is retrieved from the specified localizer using the given message key and optional formatting arguments. + /// Each line of the message is prefixed with a localized prefix string. + /// The message respects the player's configured language for proper localization. + /// + /// The target player controller to receive the message. + /// The string localizer used for localization. + /// The key identifying the localized message. + /// Optional arguments to format the localized message. public static void SendLocalizedMessage(this CCSPlayerController? controller, IStringLocalizer? localizer, string messageKey, params object[] messageArgs) { @@ -235,6 +344,16 @@ public static class PlayerExtensions } } + /// + /// Sends a localized chat message to the player controller, centered horizontally on the player's screen. + /// The message is retrieved from the specified localizer using the given message key and optional formatting arguments. + /// Each line of the message is centered and prefixed with a localized prefix string. + /// The message respects the player's configured language for localization. + /// + /// The target player controller to receive the message. + /// The string localizer used for localization. + /// The key identifying the localized message. + /// Optional arguments to format the localized message. public static void SendLocalizedMessageCenter(this CCSPlayerController? controller, IStringLocalizer? localizer, string messageKey, params object[] messageArgs) { diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 8b81c8a..989c9dd 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -49,33 +49,29 @@ internal static class Helper public static List GetPlayerFromName(string name) { - return Utilities.GetPlayers().FindAll(x => x.PlayerName.Equals(name, StringComparison.OrdinalIgnoreCase)); + return GetValidPlayers().FindAll(x => x.PlayerName.Equals(name, StringComparison.OrdinalIgnoreCase)); } - public static CCSPlayerController? GetPlayerFromSteamid64(string steamid) + public static CCSPlayerController? GetPlayerFromSteamid64(ulong steamid) { - return GetValidPlayers().FirstOrDefault(x => x.SteamID.ToString().Equals(steamid, StringComparison.OrdinalIgnoreCase)); + return GetValidPlayers().FirstOrDefault(x => x.SteamID == steamid); } - public static CCSPlayerController? GetPlayerFromIp(string ipAddress) + public static List GetPlayerFromIp(string ipAddress) { - return GetValidPlayers().FirstOrDefault(x => x.IpAddress != null && x.IpAddress.Split(":")[0].Equals(ipAddress)); + return CS2_SimpleAdmin.CachedPlayers.FindAll(x => x.IpAddress != null && x.IpAddress.Split(":")[0].Equals(ipAddress)); } public static List GetValidPlayers() { - return Utilities.GetPlayers().AsValueEnumerable() - .Where(p => p is { IsValid: true, IsBot: false, Connected: PlayerConnectedState.PlayerConnected }) - .ToList(); + return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().ToList(); } public static List GetValidPlayersWithBots() { - return Utilities.GetPlayers().AsValueEnumerable() - .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected }).ToList(); + return CS2_SimpleAdmin.CachedPlayers.Concat(CS2_SimpleAdmin.BotPlayers).AsValueEnumerable().ToList(); } - // public static bool IsValidSteamId64(string input) // { // const string pattern = @"^\d{17}$"; @@ -141,10 +137,36 @@ internal static class Helper if (player == null || !player.IsValid || player.IsHLTV) return; - if (player.UserId.HasValue && CS2_SimpleAdmin.PlayersInfo.TryGetValue(player.UserId.Value, out var value)) + if (player.UserId.HasValue && CS2_SimpleAdmin.PlayersInfo.TryGetValue(player.SteamID, out var value)) value.WaitingForKick = true; - player.CommitSuicide(true, true); + // player.CommitSuicide(true, true); + player.VoiceFlags = VoiceFlags.Muted; + var playerPawn = player.PlayerPawn.Value; + + if (playerPawn != null && playerPawn.LifeState == (int)LifeState_t.LIFE_ALIVE) + { + playerPawn.Freeze(); + playerPawn.Colorize(255, 0, 0); + + var weaponServices = playerPawn.WeaponServices; + if (weaponServices == null) + return; + + foreach (var _weap in weaponServices.MyWeapons) + { + var weapon = _weap.Value;; + if (weapon == null || !weapon.IsValid) + continue; + if (weapon.DesignerName.Contains("c4") || weapon.DesignerName.Contains("healthshot")) + continue; + + weapon.NextPrimaryAttackTick = Server.TickCount + 999; + weapon.NextSecondaryAttackTick = Server.TickCount + 999; + Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick"); + Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick"); + } + } if (delay > 0) { @@ -155,6 +177,7 @@ internal static class Helper // Server.ExecuteCommand($"kickid {player.UserId}"); + playerPawn?.Colorize(); player.Disconnect(reason); }); } @@ -162,6 +185,7 @@ internal static class Helper { // Server.ExecuteCommand($"kickid {player.UserId}"); + playerPawn?.Colorize(); player.Disconnect(reason); } @@ -186,11 +210,41 @@ internal static class Helper if (!player.IsValid || player.IsHLTV) return; - if (player.UserId.HasValue && CS2_SimpleAdmin.PlayersInfo.TryGetValue(player.UserId.Value, out var value)) + if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(player.SteamID, out var value)) + { + if (value.WaitingForKick) + return; + value.WaitingForKick = true; + } - player.CommitSuicide(true, true); - + player.VoiceFlags = VoiceFlags.Muted; + var playerPawn = player.PlayerPawn.Value; + if (playerPawn != null && playerPawn.LifeState == (int)LifeState_t.LIFE_ALIVE) + { + playerPawn.Freeze(); + playerPawn.Colorize(255, 0, 0); + + var weaponServices = playerPawn.WeaponServices; + if (weaponServices == null) + return; + + foreach (var _weap in weaponServices.MyWeapons) + { + var weapon = _weap.Value; + ; + if (weapon == null || !weapon.IsValid) + continue; + if (weapon.DesignerName.Contains("c4") || weapon.DesignerName.Contains("healthshot")) + continue; + + weapon.NextPrimaryAttackTick = Server.TickCount + 999; + weapon.NextSecondaryAttackTick = Server.TickCount + 999; + Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick"); + Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick"); + } + } + if (delay > 0) { CS2_SimpleAdmin.Instance.AddTimer(delay, () => @@ -237,6 +291,7 @@ internal static class Helper public static int ParsePenaltyTime(string time) { + time = time.ToLower(); if (string.IsNullOrWhiteSpace(time) || !time.Any(char.IsDigit)) { // CS2_SimpleAdmin._logger?.LogError("Time string cannot be null or empty."); @@ -256,7 +311,6 @@ internal static class Helper { "y", 525600 } // Year (365 * 24 * 60) }; - // Check if the input is purely numeric (e.g., "10" for 10 minutes) if (int.TryParse(time, out var numericMinutes)) { @@ -309,7 +363,6 @@ internal static class Helper return; var playerName = caller?.PlayerName ?? CS2_SimpleAdmin._localizer["sa_console"]; - var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer["sa_unknown"]; CS2_SimpleAdmin.Instance.Logger.LogInformation($"{CS2_SimpleAdmin._localizer[ @@ -454,16 +507,13 @@ internal static class Helper { if (CS2_SimpleAdmin._localizer == null) return; - // Determine the localized message key var localizedMessageKey = $"{messageKey}"; var formattedMessageArgs = messageArgs.Select(arg => arg?.ToString() ?? string.Empty).ToArray(); - // Replace placeholder based on showActivityType for (var i = 0; i < formattedMessageArgs.Length; i++) { - var arg = formattedMessageArgs[i]; // Convert argument to string if not null - // Replace "CALLER" placeholder in the argument string + var arg = formattedMessageArgs[i]; formattedMessageArgs[i] = CS2_SimpleAdmin.Instance.Config.OtherSettings.ShowActivityType switch { 1 => arg.Replace("CALLER", CS2_SimpleAdmin._localizer["sa_admin"]), @@ -471,7 +521,6 @@ internal static class Helper }; } - // Print the localized message to the center of the screen for the player using (new WithTemporaryCulture(player.GetLanguage())) { player.PrintToCenter(CS2_SimpleAdmin._localizer[localizedMessageKey, formattedMessageArgs.Cast().ToArray()]); @@ -535,10 +584,8 @@ internal static class Helper bool[] inlineFlags = [true, true, true, false, false]; var hostname = ConVar.Find("hostname")?.StringValue ?? localizer["sa_unknown"]; - var colorHex = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value ?? "#FFFFFF"; - - if (string.IsNullOrEmpty(colorHex)) - colorHex = "#FFFFFF"; + var colorValue = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value; + var colorHex = string.IsNullOrWhiteSpace(colorValue) ? "#FFFFFF" : colorValue.Trim(); var embed = new Embed { @@ -631,7 +678,8 @@ internal static class Helper bool[] inlineFlags = [true, true, true, false, false]; var hostname = ConVar.Find("hostname")?.StringValue ?? localizer["sa_unknown"]; - var colorHex = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value ?? "#FFFFFF"; + var colorValue = penaltySetting.FirstOrDefault(s => s.Name.Equals("Color"))?.Value; + var colorHex = string.IsNullOrWhiteSpace(colorValue) ? "#FFFFFF" : colorValue.Trim(); var embed = new Embed { @@ -803,9 +851,11 @@ public static class PluginInfo { internal static async Task CheckVersion(string localVersion, ILogger logger) { + ShowAd(localVersion); const string versionUrl = "https://raw.githubusercontent.com/daffyyyy/CS2-SimpleAdmin/main/CS2-SimpleAdmin/VERSION"; var client = CS2_SimpleAdmin.HttpClient; - + client.Timeout = TimeSpan.FromSeconds(3); + try { var response = await client.GetAsync(versionUrl); @@ -844,8 +894,8 @@ public static class PluginInfo logger.LogError(ex, "An error occurred while checking version."); } } - - internal static void ShowAd(string moduleVersion) + + private static void ShowAd(string moduleVersion) { Console.WriteLine(" "); Console.WriteLine(" _______ ___ __ __ _______ ___ _______ _______ ______ __ __ ___ __ _ "); @@ -975,8 +1025,21 @@ public static class IpHelper { public static uint IpToUint(string ipAddress) { - return (uint)BitConverter.ToInt32(System.Net.IPAddress.Parse(ipAddress).GetAddressBytes().Reverse().ToArray(), - 0); + if (string.IsNullOrWhiteSpace(ipAddress)) + throw new ArgumentException("IP address cannot be null or empty.", nameof(ipAddress)); + + if (!System.Net.IPAddress.TryParse(ipAddress, out var ip)) + throw new FormatException($"Invalid IP address format: {ipAddress}"); + + // Ensure it's IPv4 (IPv6 will throw) + if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + throw new FormatException("Only IPv4 addresses are supported."); + + byte[] bytes = ip.GetAddressBytes(); + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); // Ensure big-endian (network order) + + return BitConverter.ToUInt32(bytes, 0); } public static bool TryConvertIpToUint(string ipString, out uint ipUint) diff --git a/CS2-SimpleAdmin/Managers/BanManager.cs b/CS2-SimpleAdmin/Managers/BanManager.cs index 368f8a8..038a6d0 100644 --- a/CS2-SimpleAdmin/Managers/BanManager.cs +++ b/CS2-SimpleAdmin/Managers/BanManager.cs @@ -5,36 +5,37 @@ using Dapper; using Microsoft.Extensions.Logging; using MySqlConnector; using System.Text; +using CS2_SimpleAdmin.Database; namespace CS2_SimpleAdmin.Managers; -internal class BanManager(Database.Database? database) +internal class BanManager(IDatabaseProvider? databaseProvider) { + /// + /// Bans an online player and inserts the ban record into the database. + /// + /// The player to be banned (must be currently online). + /// The admin issuing the ban. Can be null if issued from console. + /// The reason for the ban. + /// Ban duration in minutes. If 0, the ban is permanent. + /// The newly created ban ID if successful, otherwise null. public async Task BanPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0) { - if (database == null) return null; - + if (databaseProvider == null) return null; DateTime now = Time.ActualDateTime(); DateTime futureTime = now.AddMinutes(time); - await using MySqlConnection connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); try { - const string sql = """ - - INSERT INTO `sa_bans` - (`player_steamid`, `player_name`, `player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) - VALUES - (@playerSteamid, @playerName, @playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid); - SELECT LAST_INSERT_ID(); - """; - + var sql = databaseProvider.GetAddBanQuery(); + var banId = await connection.ExecuteScalarAsync(sql, new { - playerSteamid = player.SteamId.SteamId64.ToString(), + playerSteamid = player.SteamId.SteamId64, playerName = player.Name, playerIp = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1 ? player.IpAddress : null, - adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", + adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", banReason = reason, duration = time, @@ -45,37 +46,36 @@ internal class BanManager(Database.Database? database) return banId; } - catch + catch(Exception ex) { + CS2_SimpleAdmin._logger?.LogError(ex, ex.Message); return null; } } - public async Task AddBanBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0) + /// + /// Adds a ban for an offline player identified by their SteamID. + /// + /// The SteamID64 of the player to ban. + /// The admin issuing the ban. Can be null if issued from console. + /// The reason for the ban. + /// Ban duration in minutes. If 0, the ban is permanent. + /// The ID of the newly created ban if successful, otherwise null. + public async Task AddBanBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0) { - if (database == null) return null; - if (string.IsNullOrEmpty(playerSteamId)) return null; + if (databaseProvider == null) return null; DateTime now = Time.ActualDateTime(); DateTime futureTime = now.AddMinutes(time); try { - await using MySqlConnection connection = await database.GetConnectionAsync(); - - const string sql = """ - - INSERT INTO `sa_bans` - (`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) - VALUES - (@playerSteamid, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid); - SELECT LAST_INSERT_ID(); - """; - + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetAddBanBySteamIdQuery(); var banId = await connection.ExecuteScalarAsync(sql, new { playerSteamid = playerSteamId, - adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", + adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", banReason = reason, duration = time, @@ -86,15 +86,23 @@ internal class BanManager(Database.Database? database) return banId; } - catch (Exception) + catch(Exception ex) { + CS2_SimpleAdmin._logger?.LogError(ex, ex.Message); return null; } } + /// + /// Adds a ban for an offline player identified by their IP address. + /// + /// The IP address of the player to ban. + /// The admin issuing the ban. Can be null if issued from console. + /// The reason for the ban. + /// Ban duration in minutes. If 0, the ban is permanent. public async Task AddBanByIp(string playerIp, PlayerInfo? issuer, string reason, int time = 0) { - if (database == null) return; + if (databaseProvider == null) return; if (string.IsNullOrEmpty(playerIp)) return; @@ -103,15 +111,13 @@ internal class BanManager(Database.Database? database) try { - await using MySqlConnection connection = await database.GetConnectionAsync(); - - var sql = "INSERT INTO `sa_bans` (`player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) " + - "VALUES (@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid)"; + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetAddBanByIpQuery(); await connection.ExecuteAsync(sql, new { playerIp, - adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", + adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", banReason = reason, duration = time, @@ -123,370 +129,308 @@ internal class BanManager(Database.Database? database) catch { } } - public async Task IsPlayerBanned(PlayerInfo player) +// public async Task IsPlayerBanned(PlayerInfo player) +// { +// if (database == null) return false; +// +// if (player.IpAddress == null) +// { +// return false; +// } +// +// #if DEBUG +// if (CS2_SimpleAdmin._logger != null) +// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}"); +// #endif +// +// int banCount; +// +// DateTime currentTime = Time.ActualDateTime(); +// +// try +// { +// string sql; +// +// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)) +// { +// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """ +// SELECT COALESCE(( +// SELECT COUNT(*) +// FROM sa_bans +// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) +// AND status = 'ACTIVE' +// AND (duration = 0 OR ends > @CurrentTime) +// ), 0) +// + +// COALESCE(( +// SELECT COUNT(*) +// FROM sa_bans +// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid +// WHERE sa_bans.status = 'ACTIVE' +// AND sa_players_ips.address = @PlayerIP +// AND NOT EXISTS ( +// SELECT 1 +// FROM sa_bans +// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) +// AND status = 'ACTIVE' +// AND (duration = 0 OR ends > @CurrentTime) +// ) +// ), 0) AS TotalBanCount; +// """ : """ +// SELECT COALESCE(( +// SELECT COUNT(*) +// FROM sa_bans +// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) +// AND status = 'ACTIVE' +// AND (duration = 0 OR ends > @CurrentTime) +// AND server_id = @ServerId +// ), 0) +// + +// COALESCE(( +// SELECT COUNT(*) +// FROM sa_bans +// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid +// WHERE sa_bans.status = 'ACTIVE' +// AND sa_players_ips.address = @PlayerIP +// AND NOT EXISTS ( +// SELECT 1 +// FROM sa_bans +// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) +// AND status = 'ACTIVE' +// AND (duration = 0 OR ends > @CurrentTime) +// AND server_id = @ServerId +// ) +// ), 0) AS TotalBanCount; +// """; +// } +// else +// { +// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """ +// UPDATE sa_bans +// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END, +// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END +// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) +// AND status = 'ACTIVE' +// AND (duration = 0 OR ends > @CurrentTime); +// +// SELECT COUNT(*) FROM sa_bans +// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) +// AND status = 'ACTIVE' +// AND (duration = 0 OR ends > @CurrentTime); +// """ : """ +// UPDATE sa_bans +// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END, +// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END +// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) +// AND status = 'ACTIVE' +// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId; +// +// SELECT COUNT(*) FROM sa_bans +// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) +// AND status = 'ACTIVE' +// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId; +// """; +// } +// +// await using var connection = await database.GetConnectionAsync(); +// +// var parameters = new +// { +// PlayerSteamID = player.SteamId.SteamId64, +// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || +// string.IsNullOrEmpty(player.IpAddress) || +// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress) +// ? null +// : player.IpAddress, +// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty, +// CurrentTime = currentTime, +// CS2_SimpleAdmin.ServerId +// }; +// +// banCount = await connection.ExecuteScalarAsync(sql, parameters); +// } +// catch (Exception ex) +// { +// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})", +// player.Name, ex.Message); +// return false; +// } +// +// return banCount > 0; +// } +// +// public async Task GetPlayerBans(PlayerInfo player) +// { +// if (database == null) return 0; +// +// try +// { +// string sql; +// +// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode +// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)" +// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid"; +// +// int banCount; +// +// await using var connection = await database.GetConnectionAsync(); +// +// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress)) +// { +// banCount = await connection.ExecuteScalarAsync(sql, +// new +// { +// PlayerSteamID = player.SteamId.SteamId64, +// PlayerIP = player.IpAddress, +// serverid = CS2_SimpleAdmin.ServerId +// }); +// } +// else +// { +// banCount = await connection.ExecuteScalarAsync(sql, +// new +// { +// PlayerSteamID = player.SteamId.SteamId64, +// PlayerIP = DBNull.Value, +// serverid = CS2_SimpleAdmin.ServerId +// }); +// } +// +// return banCount; +// } +// catch { } +// +// return 0; +// } + + /// + /// Unbans a player based on a pattern match of SteamID or IP address. + /// + /// Pattern to match against player identifiers (e.g., partial SteamID). + /// SteamID64 of the admin performing the unban. + /// Optional reason for the unban. If null or empty, the unban reason is not stored. +public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason) +{ + if (databaseProvider == null) return; + + if (playerPattern is not { Length: > 1 }) { - if (database == null) return false; - - if (player.IpAddress == null) - { - return false; - } - -#if DEBUG - if (CS2_SimpleAdmin._logger != null) - CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}"); -#endif - - int banCount; - - DateTime currentTime = Time.ActualDateTime(); - - try - { - string sql; - - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)) - { - sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """ - SELECT COALESCE(( - SELECT COUNT(*) - FROM sa_bans - WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) - AND status = 'ACTIVE' - AND (duration = 0 OR ends > @CurrentTime) - ), 0) - + - COALESCE(( - SELECT COUNT(*) - FROM sa_bans - JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid - WHERE sa_bans.status = 'ACTIVE' - AND sa_players_ips.address = @PlayerIP - AND NOT EXISTS ( - SELECT 1 - FROM sa_bans - WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) - AND status = 'ACTIVE' - AND (duration = 0 OR ends > @CurrentTime) - ) - ), 0) AS TotalBanCount; - """ : """ - SELECT COALESCE(( - SELECT COUNT(*) - FROM sa_bans - WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) - AND status = 'ACTIVE' - AND (duration = 0 OR ends > @CurrentTime) - AND server_id = @ServerId - ), 0) - + - COALESCE(( - SELECT COUNT(*) - FROM sa_bans - JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid - WHERE sa_bans.status = 'ACTIVE' - AND sa_players_ips.address = @PlayerIP - AND NOT EXISTS ( - SELECT 1 - FROM sa_bans - WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) - AND status = 'ACTIVE' - AND (duration = 0 OR ends > @CurrentTime) - AND server_id = @ServerId - ) - ), 0) AS TotalBanCount; - """; - } - else - { - sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """ - UPDATE sa_bans - SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END, - player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END - WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) - AND status = 'ACTIVE' - AND (duration = 0 OR ends > @CurrentTime); - - SELECT COUNT(*) FROM sa_bans - WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) - AND status = 'ACTIVE' - AND (duration = 0 OR ends > @CurrentTime); - """ : """ - UPDATE sa_bans - SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END, - player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END - WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) - AND status = 'ACTIVE' - AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId; - - SELECT COUNT(*) FROM sa_bans - WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) - AND status = 'ACTIVE' - AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId; - """; - } - - await using var connection = await database.GetConnectionAsync(); - - var parameters = new - { - PlayerSteamID = player.SteamId.SteamId64.ToString(), - PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || - string.IsNullOrEmpty(player.IpAddress) || - CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress) - ? null - : player.IpAddress, - PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty, - CurrentTime = currentTime, - CS2_SimpleAdmin.ServerId - }; - - banCount = await connection.ExecuteScalarAsync(sql, parameters); - } - catch (Exception ex) - { - CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})", - player.Name, ex.Message); - return false; - } - - return banCount > 0; + return; } - - public async Task GetPlayerBans(PlayerInfo player) + try { - if (database == null) return 0; + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); - try - { - string sql; - - sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)" - : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid"; - - int banCount; - - await using var connection = await database.GetConnectionAsync(); - - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress)) - { - banCount = await connection.ExecuteScalarAsync(sql, - new - { - PlayerSteamID = player.SteamId.SteamId64.ToString(), - PlayerIP = player.IpAddress, - serverid = CS2_SimpleAdmin.ServerId - }); - } - else - { - banCount = await connection.ExecuteScalarAsync(sql, - new - { - PlayerSteamID = player.SteamId.SteamId64.ToString(), - PlayerIP = DBNull.Value, - serverid = CS2_SimpleAdmin.ServerId - }); - } - - return banCount; - } - catch { } - - return 0; - } - - public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason) - { - if (database == null) return; - - if (playerPattern is not { Length: > 1 }) - { + var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId }); + var bansList = bans as dynamic[] ?? bans.ToArray(); + if (bansList.Length == 0) return; - } - try + + var sqlAdminId = databaseProvider.GetUnbanAdminIdQuery(); + var adminId = await connection.ExecuteScalarAsync(sqlAdminId, new { adminSteamId }) ?? 0; + + foreach (var ban in bansList) { - await using var connection = await database.GetConnectionAsync(); + int banId = ban.id; - var sqlRetrieveBans = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'" - : "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid"; + var sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null); + var unbanId = await connection.ExecuteScalarAsync(sqlInsertUnban, new { banId, adminId, reason }); - var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId }); - - var bansList = bans as dynamic[] ?? bans.ToArray(); - if (bansList.Length == 0) - return; - - const string sqlAdmin = "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId"; - var sqlInsertUnban = "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT LAST_INSERT_ID();"; - - var sqlAdminId = await connection.ExecuteScalarAsync(sqlAdmin, new { adminSteamId }); - var adminId = sqlAdminId ?? 0; - - foreach (var ban in bansList) - { - int banId = ban.id; - int? unbanId; - - if (reason != null) - { - unbanId = await connection.ExecuteScalarAsync(sqlInsertUnban, new { banId, adminId, reason }); - } - else - { - sqlInsertUnban = "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT LAST_INSERT_ID();"; - unbanId = await connection.ExecuteScalarAsync(sqlInsertUnban, new { banId, adminId }); - } - - const string sqlUpdateBan = "UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId"; - await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId }); - } - - } - catch { } - } - - public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players) - { - if (database == null) return; - - try - { - await using var connection = await database.GetConnectionAsync(); - bool checkIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0; - - var filteredPlayers = players.Where(p => p.UserId.HasValue).ToList(); - - var steamIds = filteredPlayers.Select(p => p.SteamID).Distinct().ToList(); - var ipAddresses = filteredPlayers - .Where(p => !string.IsNullOrEmpty(p.IpAddress)) - .Select(p => p.IpAddress) - .Distinct() - .ToList(); - - var sql = new StringBuilder(); - sql.Append("SELECT `player_steamid`, `player_ip` FROM `sa_bans` WHERE `status` = 'ACTIVE' "); - - if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) - { - sql.Append("AND (player_steamid IN @SteamIDs "); - if (checkIpBans && ipAddresses.Count != 0) - { - sql.Append("OR player_ip IN @IpAddresses"); - } - sql.Append(')'); - } - else - { - sql.Append("AND server_id = @ServerId AND (player_steamid IN @SteamIDs "); - if (checkIpBans && ipAddresses.Count != 0) - { - sql.Append("OR player_ip IN @IpAddresses"); - } - sql.Append(')'); - } - - var bannedPlayers = await connection.QueryAsync<(ulong PlayerSteamID, string PlayerIP)>( - sql.ToString(), - new - { - SteamIDs = steamIds, - IpAddresses = checkIpBans ? ipAddresses : [], - CS2_SimpleAdmin.ServerId - }); - - var valueTuples = bannedPlayers.ToList(); - var bannedSteamIds = valueTuples.Select(b => b.PlayerSteamID).ToHashSet(); - var bannedIps = valueTuples.Select(b => b.PlayerIP).ToHashSet(); - - foreach (var player in filteredPlayers.Where(player => bannedSteamIds.Contains(player.SteamID) || - (checkIpBans && bannedIps.Contains(player.IpAddress ?? "")))) - { - if (!player.UserId.HasValue || CS2_SimpleAdmin.PlayersInfo[player.UserId.Value].WaitingForKick) continue; - - await Server.NextFrameAsync(() => - { - Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED); - }); - } - } - catch (Exception ex) - { - CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}"); + var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery(); + await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId }); } } + catch { } +} + // public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players) + // { + // if (database == null) return; + // + // try + // { + // await using var connection = await database.GetConnectionAsync(); + // bool checkIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0; + // + // var filteredPlayers = players.Where(p => p.UserId.HasValue).ToList(); + // + // var steamIds = filteredPlayers.Select(p => p.SteamID).Distinct().ToList(); + // var ipAddresses = filteredPlayers + // .Where(p => !string.IsNullOrEmpty(p.IpAddress)) + // .Select(p => p.IpAddress) + // .Distinct() + // .ToList(); + // + // var sql = new StringBuilder(); + // sql.Append("SELECT `player_steamid`, `player_ip` FROM `sa_bans` WHERE `status` = 'ACTIVE' "); + // + // if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) + // { + // sql.Append("AND (player_steamid IN @SteamIDs "); + // if (checkIpBans && ipAddresses.Count != 0) + // { + // sql.Append("OR player_ip IN @IpAddresses"); + // } + // sql.Append(')'); + // } + // else + // { + // sql.Append("AND server_id = @ServerId AND (player_steamid IN @SteamIDs "); + // if (checkIpBans && ipAddresses.Count != 0) + // { + // sql.Append("OR player_ip IN @IpAddresses"); + // } + // sql.Append(')'); + // } + // + // var bannedPlayers = await connection.QueryAsync<(ulong PlayerSteamID, string PlayerIP)>( + // sql.ToString(), + // new + // { + // SteamIDs = steamIds, + // IpAddresses = checkIpBans ? ipAddresses : [], + // CS2_SimpleAdmin.ServerId + // }); + // + // var valueTuples = bannedPlayers.ToList(); + // var bannedSteamIds = valueTuples.Select(b => b.PlayerSteamID).ToHashSet(); + // var bannedIps = valueTuples.Select(b => b.PlayerIP).ToHashSet(); + // + // foreach (var player in filteredPlayers.Where(player => bannedSteamIds.Contains(player.SteamID) || + // (checkIpBans && bannedIps.Contains(player.IpAddress ?? "")))) + // { + // if (!player.UserId.HasValue || CS2_SimpleAdmin.PlayersInfo[player.SteamID].WaitingForKick) continue; + // + // await Server.NextWorldUpdateAsync(() => + // { + // Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED); + // }); + // } + // } + // catch (Exception ex) + // { + // CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}"); + // } + // } + + /// + /// Expires all bans that have passed their end time, including optional cleanup of old IP bans. + /// public async Task ExpireOldBans() { - if (database == null) return; + if (databaseProvider == null) return; var currentTime = Time.ActualDateTime(); try { - await using var connection = await database.GetConnectionAsync(); - /* - string sql = ""; - await using MySqlConnection connection = await _database.GetConnectionAsync(); - - sql = "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"; - await connection.ExecuteAsync(sql, new { CurrentTime = DateTime.UtcNow }); - */ - - string sql; - - sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """ - - UPDATE sa_bans - SET - status = 'EXPIRED' - WHERE - status = 'ACTIVE' - AND - `duration` > 0 - AND - ends <= @currentTime - """ : """ - - UPDATE sa_bans - SET - status = 'EXPIRED' - WHERE - status = 'ACTIVE' - AND - `duration` > 0 - AND - ends <= @currentTime - AND server_id = @serverid - """; - + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); await connection.ExecuteAsync(sql, new { currentTime, serverid = CS2_SimpleAdmin.ServerId }); - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans > 0) { var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans); - sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """ - - UPDATE sa_bans - SET - player_ip = NULL - WHERE - status = 'ACTIVE' - AND - ends <= @ipBansTime - """ : """ - - UPDATE sa_bans - SET - player_ip = NULL - WHERE - status = 'ACTIVE' - AND - ends <= @ipBansTime - AND server_id = @serverid - """; - + sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId }); } } diff --git a/CS2-SimpleAdmin/Managers/CacheManager.cs b/CS2-SimpleAdmin/Managers/CacheManager.cs index be097d0..7398eb1 100644 --- a/CS2-SimpleAdmin/Managers/CacheManager.cs +++ b/CS2-SimpleAdmin/Managers/CacheManager.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using CS2_SimpleAdmin.Database; using CS2_SimpleAdmin.Models; using Dapper; using ZLinq; @@ -8,7 +9,7 @@ namespace CS2_SimpleAdmin.Managers; internal class CacheManager: IDisposable { private readonly ConcurrentDictionary _banCache = []; - private readonly ConcurrentDictionary> _steamIdIndex = []; + private readonly ConcurrentDictionary> _steamIdIndex = []; private readonly ConcurrentDictionary> _ipIndex = []; private readonly ConcurrentDictionary> _playerIpsCache = []; @@ -17,21 +18,26 @@ internal class CacheManager: IDisposable private DateTime _lastUpdateTime = DateTime.MinValue; private bool _isInitialized; private bool _disposed; - + + /// + /// Initializes and builds the ban and IP cache from the database. Loads bans, player IP history, and config settings. + /// + /// Asynchronous task representing the initialization process. public async Task InitializeCacheAsync() { - if (CS2_SimpleAdmin.Database == null) return; + if (CS2_SimpleAdmin.DatabaseProvider == null) return; if (!CS2_SimpleAdmin.ServerLoaded) return; if (_isInitialized) return; try { Clear(); - _cachedIgnoredIps = new HashSet( - CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps - .Select(IpHelper.IpToUint)); + _cachedIgnoredIps = CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps + .AsValueEnumerable() + .Select(IpHelper.IpToUint) + .ToHashSet(); - await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync(); + await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); List bans; if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) @@ -40,6 +46,7 @@ internal class CacheManager: IDisposable """ SELECT id AS Id, + player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status @@ -52,6 +59,7 @@ internal class CacheManager: IDisposable """ SELECT id AS Id, + player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status @@ -59,20 +67,16 @@ internal class CacheManager: IDisposable WHERE server_id = @serverId """, new {serverId = CS2_SimpleAdmin.ServerId})).ToList(); } - - var ipHistory = - await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>( - "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC"); - foreach (var ban in bans) + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) { - _banCache.TryAdd(ban.Id, ban); - } - - foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid)) - { - var ipSet = new HashSet( - group + var ipHistory = + (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>( + "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC")).ToList(); + + foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid)) + { + var ipSet = group .GroupBy(x => x.address) .Select(g => { @@ -80,32 +84,34 @@ internal class CacheManager: IDisposable return new IpRecord( g.Key, latest.used_at, - !string.IsNullOrEmpty(latest.name) - ? latest.name - : CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown" + string.IsNullOrEmpty(latest.name) + ? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown" + : latest.name ); - }), - new IpRecordComparer() - ); + }) + .ToHashSet(new IpRecordComparer()); - _playerIpsCache.AddOrUpdate( - group.Key, - _ => ipSet, - (_, existingSet) => - { - foreach (var ip in ipSet) + _playerIpsCache.AddOrUpdate( + group.Key, + _ => ipSet, + (_, existingSet) => { - existingSet.Remove(ip); - existingSet.Add(ip); - } - - return existingSet; - }); + foreach (var ip in ipSet) + { + existingSet.Remove(ip); + existingSet.Add(ip); + } + return existingSet; + }); + } } + + foreach (var ban in bans.AsValueEnumerable()) + _banCache.TryAdd(ban.Id, ban); RebuildIndexes(); - _lastUpdateTime = DateTime.Now.AddSeconds(-1); + _lastUpdateTime = Time.ActualDateTime().AddSeconds(-1); _isInitialized = true; } catch (Exception e) @@ -114,27 +120,36 @@ internal class CacheManager: IDisposable } } + /// + /// Clears all cached data and reinitializes the cache from the database. + /// + /// Asynchronous task representing the reinitialization process. public async Task ForceReInitializeCacheAsync() { _isInitialized = false; _banCache.Clear(); _playerIpsCache.Clear(); - _cachedIgnoredIps.Clear(); + _cachedIgnoredIps = []; _lastUpdateTime = DateTime.MinValue; await InitializeCacheAsync(); } - + + /// + /// Refreshes the in-memory cache with updated or new data from the database since the last update time. + /// Also updates multi-account IP history if enabled. + /// + /// Asynchronous task representing the refresh operation. public async Task RefreshCacheAsync() { - if (CS2_SimpleAdmin.Database == null) return; + if (CS2_SimpleAdmin.DatabaseProvider == null) return; if (!_isInitialized) return; try { - await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync(); - List updatedBans; + await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); + IEnumerable updatedBans; var allIds = (await connection.QueryAsync("SELECT id FROM sa_bans")).ToHashSet(); @@ -143,45 +158,45 @@ internal class CacheManager: IDisposable updatedBans = (await connection.QueryAsync( """ SELECT id AS Id, + player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC """, new { lastUpdate = _lastUpdateTime } - )).ToList(); + )); } else { updatedBans = (await connection.QueryAsync( """ SELECT id AS Id, + player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC """, new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId } - )).ToList(); + )); } foreach (var id in _banCache.Keys) { if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue; - // Remove from steamIdIndex - if (!string.IsNullOrWhiteSpace(ban.PlayerSteamId) && - _steamIdIndex.TryGetValue(ban.PlayerSteamId, out var steamBans)) + if (ban.PlayerSteamId != null && + _steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans)) { steamBans.RemoveAll(b => b.Id == id); if (steamBans.Count == 0) - _steamIdIndex.TryRemove(ban.PlayerSteamId, out _); + _steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _); } - // Remove from ipIndex - if (!string.IsNullOrWhiteSpace(ban.PlayerIp) && - IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) && - _ipIndex.TryGetValue(ipUInt, out var ipBans)) + if (string.IsNullOrWhiteSpace(ban.PlayerIp) || + !IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) || + !_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue; { ipBans.RemoveAll(b => b.Id == id); if (ipBans.Count == 0) @@ -189,59 +204,64 @@ internal class CacheManager: IDisposable } } - var ipHistory = (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>( - "SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300", new {lastUpdate = _lastUpdateTime})).ToList(); - - foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid)) + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) { - var ipSet = new HashSet( - group - .GroupBy(x => x.address) - .Select(g => - { - var latest = g.MaxBy(x => x.used_at); - return new IpRecord( - g.Key, - latest.used_at, - !string.IsNullOrEmpty(latest.name) - ? latest.name - : CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown" - ); - }), - new IpRecordComparer() - ); + var ipHistory = (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>( + "SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300", + new { lastUpdate = _lastUpdateTime })); - _playerIpsCache.AddOrUpdate( - group.Key, - _ => ipSet, - (_, existingSet) => - { - foreach (var newEntry in ipSet) + foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid)) + { + var ipSet = new HashSet( + group + .GroupBy(x => x.address) + .Select(g => + { + var latest = g.MaxBy(x => x.used_at); + return new IpRecord( + g.Key, + latest.used_at, + !string.IsNullOrEmpty(latest.name) + ? latest.name + : CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown" + ); + }), + new IpRecordComparer() + ); + + _playerIpsCache.AddOrUpdate( + group.Key, + _ => ipSet, + (_, existingSet) => { - existingSet.Remove(newEntry); - existingSet.Add(newEntry); - } - return existingSet; - }); + foreach (var newEntry in ipSet) + { + existingSet.Remove(newEntry); + existingSet.Add(newEntry); + } + + return existingSet; + }); + } } - - if (updatedBans.Count == 0) - return; - + foreach (var ban in updatedBans) { _banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban); } RebuildIndexes(); - _lastUpdateTime = DateTime.Now.AddSeconds(-1); + _lastUpdateTime = Time.ActualDateTime().AddSeconds(-1); } - catch (Exception e) + catch (Exception) { - // ignored } } + /// + /// Rebuilds the internal indexes for fast lookup of active bans by Steam ID and IP address. + /// Clears and repopulates both indexes based on the current in-memory ban cache. + /// private void RebuildIndexes() { _steamIdIndex.Clear(); @@ -249,14 +269,14 @@ internal class CacheManager: IDisposable foreach (var ban in _banCache.Values) { - if (ban.Status != "ACTIVE") + if (ban.StatusEnum != BanStatus.ACTIVE) continue; - if (!string.IsNullOrWhiteSpace(ban.PlayerSteamId)) + if (ban.PlayerSteamId != null) { var steamId = ban.PlayerSteamId; _steamIdIndex.AddOrUpdate( - steamId, + steamId.Value, key => [ban], (key, list) => { @@ -264,7 +284,9 @@ internal class CacheManager: IDisposable return list; }); } - + + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) continue; + if (ban.PlayerIp != null && IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt)) { @@ -280,71 +302,233 @@ internal class CacheManager: IDisposable } } + /// + /// Retrieves all ban records currently stored in the cache. + /// + /// List of all objects. public List GetAllBans() => _banCache.Values.ToList(); - public List GetActiveBans() => _banCache.Values.Where(b => b.Status == "ACTIVE").ToList(); - public List GetPlayerBansBySteamId(string steamId) => _steamIdIndex.TryGetValue(steamId, out var bans) ? bans : []; + + /// + /// Retrieves only active ban records from the cache. + /// + /// List of active objects. + public List GetActiveBans() => _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE).ToList(); + + /// + /// Retrieves all ban records for a specific player by their Steam ID. + /// + /// 64-bit Steam ID of the player. + /// List of objects associated with the Steam ID. + public List GetPlayerBansBySteamId(ulong steamId) => _steamIdIndex.TryGetValue(steamId, out var bans) ? bans : []; + + /// + /// Gets all known Steam accounts that have used the specified IP address. + /// + /// The IP address to search for, in string format. + /// + /// List of tuples containing the Steam ID, last used time, and player name for each matching entry. + /// public List<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress) { var ipAsUint = IpHelper.IpToUint(ipAddress); + var results = new List<(ulong, DateTime, string)>(); + var comparer = _playerIpsCache.Comparer; - return _playerIpsCache.AsValueEnumerable() - .SelectMany(kvp => kvp.Value - .Where(entry => entry.Ip == ipAsUint) - .Select(entry => (kvp.Key, entry.UsedAt, entry.PlayerName))) - .ToList(); + foreach (var (steamId, ipSet) in _playerIpsCache) + { + if (!ipSet.TryGetValue(new IpRecord(ipAsUint, Time.ActualDateTime(), "Unknown"), out var actualEntry)) continue; + results.Add((steamId, actualEntry.UsedAt, actualEntry.PlayerName)); + + foreach (var entry in ipSet) + { + if (entry.Ip == ipAsUint && !Equals(entry, actualEntry)) + { + results.Add((steamId, entry.UsedAt, entry.PlayerName)); + } + } + } + + return results; } + // public IEnumerable<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress) + // { + // var ipAsUint = IpHelper.IpToUint(ipAddress); + // + // return _playerIpsCache.SelectMany(kvp => kvp.Value + // .Where(entry => entry.Ip == ipAsUint) + // .Select(entry => (kvp.Key, entry.UsedAt, entry.PlayerName))); + // } + private bool IsIpBanned(string ipAddress) { if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false; var ipUInt = IpHelper.IpToUint(ipAddress); return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt); } - - public bool IsPlayerBanned(string? steamId, string? ipAddress) + // public bool IsPlayerBanned(ulong? steamId, string? ipAddress) + // { + // if (steamId != null && _steamIdIndex.ContainsKey(steamId.Value)) + // return true; + // + // if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) + // return false; + // + // if (string.IsNullOrEmpty(ipAddress) || !IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)) + // return false; + // + // return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt); + // } + + /// + /// Checks if a player is currently banned by Steam ID or IP address. + /// If a partial ban record is found, updates it with the latest player information. + /// + /// Name of the player attempting to connect. + /// Optional 64-bit Steam ID of the player. + /// Optional IP address of the player. + /// True if the player is banned, otherwise false. + public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress) { - if (steamId != null && _steamIdIndex.ContainsKey(steamId)) - return true; + BanRecord? record; + if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords)) + { + record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); + if (record != null) + { + if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) || + (!record.PlayerSteamId.HasValue)) + { + _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); + } + + return true; + } + } - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false; - - if (ipAddress == null) + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false; - if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)) - return false; - - return !_cachedIgnoredIps.Contains(ipUInt) && - _ipIndex.ContainsKey(ipUInt); + if (string.IsNullOrEmpty(ipAddress) || + !IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) || + _cachedIgnoredIps.Contains(ipUInt) || + !_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false; + + record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); + if (record == null) return false; + if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) || + (!record.PlayerSteamId.HasValue && steamId.HasValue)) + { + _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); + } + + return true; } - public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress) + // public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress) + // { + // if (_steamIdIndex.ContainsKey(steamId)) + // return true; + // + // if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) + // return false; + // + // if (!_playerIpsCache.TryGetValue(steamId, out var ipData)) + // return false; + // + // // var now = Time.ActualDateTime(); + // var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans); + // var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; + // + // if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint)) + // { + // if (!_cachedIgnoredIps.Contains(ipAsUint)) + // { + // ipData.Add(new IpRecord( + // ipAsUint, + // Time.ActualDateTime().AddSeconds(-2), + // unknownName + // )); + // } + // } + // + // // foreach (var ipRecord in ipData) + // // { + // // // Skip if too old or in ignored list + // // if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip)) + // // continue; + // // + // // // Check if IP is banned + // // if (_ipIndex.ContainsKey(ipRecord.Ip)) + // // return true; + // // } + // + // foreach (var ipRecord in ipData) + // { + // if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip)) + // continue; + // + // if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords)) continue; + // + // var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); + // if (activeBan == null) continue; + // + // if (!string.IsNullOrEmpty(activeBan.PlayerName) && activeBan.PlayerSteamId.HasValue) return true; + // + // _ = Task.Run(() => UpdatePlayerData( + // activeBan.PlayerName, + // steamId, + // ipAddress + // )); + // + // if (string.IsNullOrEmpty(activeBan.PlayerName) && !string.IsNullOrEmpty(unknownName)) + // activeBan.PlayerName = unknownName; + // + // activeBan.PlayerSteamId ??= steamId; + // + // return true; + // } + // + // return false; + // } + + /// + /// Checks if the player or any IP previously associated with them is currently banned. + /// Also updates ban records with missing player info if found. + /// + /// Current player name. + /// 64-bit Steam ID of the player. + /// Current IP address of the player (optional). + /// True if the player or their known IPs are banned, otherwise false. + public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress) { - var steamIdStr = steamId.ToString(); - - if (_steamIdIndex.ContainsKey(steamIdStr)) - return true; - - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false; + if (_steamIdIndex.TryGetValue(steamId, out var steamBans)) + { + var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE); + if (activeBan != null) + { + if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp)) + _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); + + return true; + } + } + + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) + return false; if (!_playerIpsCache.TryGetValue(steamId, out var ipData)) return false; - var now = DateTime.Now; - var cutoff = now.AddDays(-7); + var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans); + var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; - if (ipAddress != null) + if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint)) { - var ipAsUint = IpHelper.IpToUint(ipAddress); - if (!_cachedIgnoredIps.Contains(ipAsUint)) { - ipData.Add(new IpRecord( - ipAsUint, - now.AddSeconds(-2), // artificially recent - CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown" - )); + ipData.Add(new IpRecord(ipAsUint, Time.ActualDateTime().AddSeconds(-2), unknownName)); } } @@ -353,20 +537,122 @@ internal class CacheManager: IDisposable if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip)) continue; - if (_ipIndex.ContainsKey(ipRecord.Ip)) - return true; + if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords)) + continue; + + var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); + if (activeBan == null) + continue; + + if (string.IsNullOrEmpty(activeBan.PlayerName)) + activeBan.PlayerName = unknownName; + + activeBan.PlayerSteamId ??= steamId; + + _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); + + return true; } return false; } - + + /// + /// Checks if the given IP address is known (previously recorded) for the specified Steam ID. + /// + /// 64-bit Steam ID of the player. + /// IP address to check. + /// True if the IP is recorded for the player, otherwise false. public bool HasIpForPlayer(ulong steamId, string ipAddress) { if (string.IsNullOrWhiteSpace(ipAddress)) return false; + + if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUint)) + return false; - return _playerIpsCache.TryGetValue(steamId, out var ipData) - && ipData.Any(x => x.Ip == IpHelper.IpToUint(ipAddress)); + return _playerIpsCache.TryGetValue(steamId, out var ipData) && + ipData.Contains(new IpRecord(ipUint, default, null!)); + } + + // public bool HasIpForPlayer(ulong steamId, string ipAddress) + // { + // if (string.IsNullOrWhiteSpace(ipAddress)) + // return false; + // + // return _playerIpsCache.TryGetValue(steamId, out var ipData) + // && ipData.Any(x => x.Ip == IpHelper.IpToUint(ipAddress)); + // } + + /// + /// Updates existing active ban records in the database with the latest known player name and IP address. + /// Also updates in-memory cache to reflect these changes. + /// + /// Current player name. + /// Optional Steam ID of the player. + /// Optional IP address of the player. + /// Asynchronous task representing the update operation. + private async Task UpdatePlayerData(string? playerName, ulong? steamId, string? ipAddress) + { + if (CS2_SimpleAdmin.DatabaseProvider == null) + return; + + var baseSql = """ + UPDATE sa_bans + SET + player_ip = COALESCE(player_ip, @PlayerIP), + player_name = COALESCE(player_name, @PlayerName) + WHERE + (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) + AND status = 'ACTIVE' + AND (duration = 0 OR ends > @CurrentTime) + """; + + if (!CS2_SimpleAdmin.Instance.Config.MultiServerMode) + { + baseSql += " AND server_id = @ServerId;"; + } + + var parameters = new + { + PlayerSteamID = steamId, + PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 + || string.IsNullOrEmpty(ipAddress) + || CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress) + ? null + : ipAddress, + PlayerName = string.IsNullOrEmpty(playerName) ? string.Empty : playerName, + CurrentTime = Time.ActualDateTime(), + CS2_SimpleAdmin.ServerId + }; + + await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); + await connection.ExecuteAsync(baseSql, parameters); + + if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords)) + { + foreach (var rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE)) + { + if (string.IsNullOrEmpty(rec.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) + rec.PlayerIp = ipAddress; + + if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName)) + rec.PlayerName = playerName; + } + } + + if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) + && _ipIndex.TryGetValue(ipUInt, out var ipRecords)) + { + foreach (var rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE)) + { + if (!rec.PlayerSteamId.HasValue && steamId.HasValue) + rec.PlayerSteamId = steamId; + + if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName)) + rec.PlayerName = playerName; + } + } } private void Clear() @@ -379,6 +665,9 @@ internal class CacheManager: IDisposable _cachedIgnoredIps.Clear(); } + /// + /// Clears and disposes of all cached data and marks the object as disposed. + /// public void Dispose() { if (_disposed) return; diff --git a/CS2-SimpleAdmin/Managers/DiscordManager.cs b/CS2-SimpleAdmin/Managers/DiscordManager.cs index 60ce5d8..e264c2d 100644 --- a/CS2-SimpleAdmin/Managers/DiscordManager.cs +++ b/CS2-SimpleAdmin/Managers/DiscordManager.cs @@ -1,21 +1,33 @@ +using System.Diagnostics.CodeAnalysis; using System.Text; +using System.Text.Json; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace CS2_SimpleAdmin.Managers; public class DiscordManager(string webhookUrl) { + + /// + /// Sends a plain text message asynchronously to the configured Discord webhook URL. + /// + /// The text message to send to Discord. + /// A task representing the asynchronous operation. + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public async Task SendMessageAsync(string message) { var client = CS2_SimpleAdmin.HttpClient; - var payload = new { content = message }; - var json = JsonConvert.SerializeObject(payload); + var options = new JsonSerializerOptions + { + WriteIndented = false + }; + + var json = JsonSerializer.Serialize(payload, options); var content = new StringContent(json, Encoding.UTF8, "application/json"); try @@ -34,6 +46,12 @@ public class DiscordManager(string webhookUrl) } } + /// + /// Sends an embed message asynchronously to the configured Discord webhook URL. + /// + /// The embed object containing rich content to send. + /// A task representing the asynchronous operation. + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public async Task SendEmbedAsync(Embed embed) { var httpClient = CS2_SimpleAdmin.HttpClient; @@ -61,7 +79,12 @@ public class DiscordManager(string webhookUrl) } }; - var jsonPayload = JsonConvert.SerializeObject(payload); + var options = new JsonSerializerOptions + { + WriteIndented = false + }; + + var jsonPayload = JsonSerializer.Serialize(payload, options); var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync(webhookUrl, content); @@ -73,6 +96,11 @@ public class DiscordManager(string webhookUrl) } } + /// + /// Converts a hexadecimal color string (e.g. "#FF0000") to its integer representation. + /// + /// The hexadecimal color string, optionally starting with '#'. + /// An integer representing the color. public static int ColorFromHex(string hex) { if (hex.StartsWith($"#")) @@ -84,6 +112,9 @@ public class DiscordManager(string webhookUrl) } } +/// +/// Represents a Discord embed message containing rich content such as title, description, fields, and images. +/// public class Embed { public int Color { get; init; } @@ -96,6 +127,12 @@ public class Embed public List Fields { get; } = []; + /// + /// Adds a field to the embed message. + /// + /// The name of the field. + /// The value or content of the field. + /// Whether the field should be displayed inline with other fields. public void AddField(string name, string value, bool inline) { var field = new EmbedField @@ -109,12 +146,18 @@ public class Embed } } +/// +/// Represents the footer section of a Discord embed message, including optional text and icon URL. +/// public class Footer { public string? Text { get; init; } public string? IconUrl { get; set; } } +/// +/// Represents a field inside a Discord embed message. +/// public class EmbedField { public string? Name { get; init; } diff --git a/CS2-SimpleAdmin/Managers/MuteManager.cs b/CS2-SimpleAdmin/Managers/MuteManager.cs index 55df4b6..01d83df 100644 --- a/CS2-SimpleAdmin/Managers/MuteManager.cs +++ b/CS2-SimpleAdmin/Managers/MuteManager.cs @@ -1,14 +1,24 @@ -using CS2_SimpleAdminApi; +using CS2_SimpleAdmin.Database; +using CS2_SimpleAdminApi; using Dapper; using Microsoft.Extensions.Logging; namespace CS2_SimpleAdmin.Managers; -internal class MuteManager(Database.Database? database) +internal class MuteManager(IDatabaseProvider? databaseProvider) { + /// + /// Adds a mute entry for a specified player with detailed information. + /// + /// Player to be muted. + /// Admin issuing the mute; null if issued from console. + /// Reason for muting the player. + /// Duration of the mute in minutes. Zero means permanent mute. + /// Mute type: 0 = GAG, 1 = MUTE, 2 = SILENCE. + /// Mute ID if successfully added, otherwise null. public async Task MutePlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0, int type = 0) { - if (database == null) return null; + if (databaseProvider == null) return null; var now = Time.ActualDateTime(); var futureTime = now.AddMinutes(time); @@ -22,21 +32,14 @@ internal class MuteManager(Database.Database? database) try { - await using var connection = await database.GetConnectionAsync(); - const string sql = """ - - INSERT INTO `sa_mutes` - (`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`) - VALUES - (@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid); - SELECT LAST_INSERT_ID(); - """; + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetAddMuteQuery(true); var muteId = await connection.ExecuteScalarAsync(sql, new { - playerSteamid = player.SteamId.SteamId64.ToString(), + playerSteamid = player.SteamId.SteamId64, playerName = player.Name, - adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", + adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", muteReason = reason, duration = time, @@ -55,10 +58,18 @@ internal class MuteManager(Database.Database? database) } } - public async Task AddMuteBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0, int type = 0) + /// + /// Adds a mute entry for a offline player identified by their SteamID. + /// + /// SteamID64 of the player to mute. + /// Admin issuing the mute; can be null if from console. + /// Reason for the mute. + /// Mute duration in minutes; 0 for permanent. + /// Mute type: 0 = GAG, 1 = MUTE, 2 = SILENCE. + /// Mute ID if successful, otherwise null. + public async Task AddMuteBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0, int type = 0) { - if (database == null) return null; - if (string.IsNullOrEmpty(playerSteamId)) return null; + if (databaseProvider == null) return null; var now = Time.ActualDateTime(); var futureTime = now.AddMinutes(time); @@ -72,20 +83,13 @@ internal class MuteManager(Database.Database? database) try { - await using var connection = await database.GetConnectionAsync(); - const string sql = """ - - INSERT INTO `sa_mutes` - (`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`) - VALUES - (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid); - SELECT LAST_INSERT_ID(); - """; - + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetAddMuteQuery(false); + var muteId = await connection.ExecuteScalarAsync(sql, new { playerSteamid = playerSteamId, - adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", + adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", muteReason = reason, duration = time, @@ -103,9 +107,14 @@ internal class MuteManager(Database.Database? database) } } + /// + /// Checks if a player with the given SteamID currently has any active mutes. + /// + /// SteamID64 of the player to check. + /// List of active mute records; empty list if none or on error. public async Task> IsPlayerMuted(string steamId) { - if (database == null) return []; + if (databaseProvider == null) return []; if (string.IsNullOrEmpty(steamId)) { @@ -119,24 +128,11 @@ internal class MuteManager(Database.Database? database) try { - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); var currentTime = Time.ActualDateTime(); - var sql = ""; - - if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) - { - sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 1 - ? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)" - : "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))"; - } - else - { - sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 1 - ? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid" - : "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid"; - - } - + + var sql = databaseProvider.GetIsMutedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode); + var parameters = new { PlayerSteamID = steamId, CurrentTime = currentTime, serverid = CS2_SimpleAdmin.ServerId }; var activeMutes = (await connection.QueryAsync(sql, parameters)).ToList(); return activeMutes; @@ -147,35 +143,26 @@ internal class MuteManager(Database.Database? database) } } + /// + /// Retrieves counts of total mutes, gags, and silences for a given player. + /// + /// Information about the player. + /// + /// Tuple containing total mutes, total gags, and total silences respectively. + /// Returns zeros if no data or on error. + /// public async Task<(int TotalMutes, int TotalGags, int TotalSilences)> GetPlayerMutes(PlayerInfo playerInfo) { - if (database == null) return (0,0,0); + if (databaseProvider == null) return (0,0,0); try { - await using var connection = await database.GetConnectionAsync(); - - var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? """ - SELECT - COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes, - COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags, - COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences - FROM sa_mutes - WHERE player_steamid = @PlayerSteamID; - """ - : """ - SELECT - COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes, - COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags, - COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences - FROM sa_mutes - WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId; - """; + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); var result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new { - PlayerSteamID = playerInfo.SteamId.SteamId64.ToString(), + PlayerSteamID = playerInfo.SteamId.SteamId64, CS2_SimpleAdmin.ServerId }); @@ -187,18 +174,21 @@ internal class MuteManager(Database.Database? database) } } + /// + /// Processes a batch of online players to update their mute status and remove expired penalties. + /// + /// List of tuples containing player SteamID, optional UserID, and slot index. + /// Task representing the asynchronous operation. public async Task CheckOnlineModeMutes(List<(ulong SteamID, int? UserId, int Slot)> players) { - if (database == null) return; + if (databaseProvider == null) return; try { const int batchSize = 20; - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); - var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'" - : "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid"; + var sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); for (var i = 0; i < players.Count; i += batchSize) { @@ -213,10 +203,7 @@ internal class MuteManager(Database.Database? database) await connection.ExecuteAsync(sql, parametersList); } - sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'" - : "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid"; - + sql = databaseProvider.GetCheckExpiredMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); foreach (var (steamId, _, slot) in players) { @@ -233,9 +220,17 @@ internal class MuteManager(Database.Database? database) catch { } } + /// + /// Removes active mutes for players matching the specified pattern. + /// + /// Pattern to match player names or identifiers. + /// SteamID64 of the admin performing the unmute. + /// Reason for unmuting the player(s). + /// Mute type to remove: 0 = GAG, 1 = MUTE, 2 = SILENCE. + /// Task representing the asynchronous operation. public async Task UnmutePlayer(string playerPattern, string adminSteamId, string reason, int type = 0) { - if (database == null) return; + if (databaseProvider == null) return; if (playerPattern.Length <= 1) { @@ -244,8 +239,7 @@ internal class MuteManager(Database.Database? database) try { - await using var connection = await database.GetConnectionAsync(); - + await using var connection = await databaseProvider.CreateConnectionAsync(); var muteType = type switch { 1 => "MUTE", @@ -253,27 +247,16 @@ internal class MuteManager(Database.Database? database) _ => "GAG" }; - string sqlRetrieveMutes; - - if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) - { - sqlRetrieveMutes = "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND " + - "type = @muteType AND status = 'ACTIVE'"; - } - else - { - sqlRetrieveMutes = "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND " + - "type = @muteType AND status = 'ACTIVE' AND server_id = @serverid"; - } - + var sqlRetrieveMutes = + databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); var mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId }); var mutesList = mutes as dynamic[] ?? mutes.ToArray(); if (mutesList.Length == 0) return; - const string sqlAdmin = "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId"; - var sqlInsertUnmute = "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT LAST_INSERT_ID();"; + var sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery(); + var sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason)); var sqlAdminId = await connection.ExecuteScalarAsync(sqlAdmin, new { adminSteamId }); var adminId = sqlAdminId ?? 0; @@ -281,21 +264,11 @@ internal class MuteManager(Database.Database? database) foreach (var mute in mutesList) { int muteId = mute.id; - int? unmuteId; - // Insert into sa_unmutes - if (reason != null) - { - unmuteId = await connection.ExecuteScalarAsync(sqlInsertUnmute, new { muteId, adminId, reason }); - } - else - { - sqlInsertUnmute = "INSERT INTO sa_unmutes (muteId, admin_id) VALUES (@muteId, @adminId); SELECT LAST_INSERT_ID();"; - unmuteId = await connection.ExecuteScalarAsync(sqlInsertUnmute, new { muteId, adminId }); - } + int? unmuteId = + await connection.ExecuteScalarAsync(sqlInsertUnmute, new { muteId, adminId, reason }); - // Update sa_mutes to set unmute_id - const string sqlUpdateMute = "UPDATE sa_mutes SET status = 'UNMUTED', unmute_id = @unmuteId WHERE id = @muteId"; + var sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery(); await connection.ExecuteAsync(sqlUpdateMute, new { unmuteId, muteId }); } } @@ -305,28 +278,18 @@ internal class MuteManager(Database.Database? database) } } + /// + /// Expires all old mutes that have passed their duration according to current time. + /// + /// Task representing the asynchronous expiration operation. public async Task ExpireOldMutes() { - if (database == null) return; + if (databaseProvider == null) return; try { - await using var connection = await database.GetConnectionAsync(); - string sql; - - if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) - { - sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 1 - ? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime" - : "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration`"; - } - else - { - sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 1 - ? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid" - : "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration` AND server_id = @serverid"; - } - + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetExpireMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode); await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId }); } catch (Exception) diff --git a/CS2-SimpleAdmin/Managers/PermissionManager.cs b/CS2-SimpleAdmin/Managers/PermissionManager.cs index bd8fb46..c9f2b41 100644 --- a/CS2-SimpleAdmin/Managers/PermissionManager.cs +++ b/CS2-SimpleAdmin/Managers/PermissionManager.cs @@ -2,14 +2,15 @@ using CounterStrikeSharp.API.Modules.Entities; using Dapper; using Microsoft.Extensions.Logging; -using MySqlConnector; -using Newtonsoft.Json; using System.Collections.Concurrent; +using System.Text.Json; using CounterStrikeSharp.API.Modules.Admin; +using System.Diagnostics.CodeAnalysis; +using CS2_SimpleAdmin.Database; namespace CS2_SimpleAdmin.Managers; -public class PermissionManager(Database.Database? database) +public class PermissionManager(IDatabaseProvider? databaseProvider) { // Unused for now //public static readonly ConcurrentDictionary> _adminCache = new ConcurrentDictionary>(); @@ -59,59 +60,65 @@ public class PermissionManager(Database.Database? database) } */ - private async Task, int, DateTime?)>> GetAllPlayersFlags() + /// + /// Retrieves all players' flags and associated data asynchronously. + /// + /// A list of tuples containing player SteamID, name, flags, immunity, and expiration time. + private async Task, int, DateTime?)>> GetAllPlayersFlags() { - if (database == null) return []; + if (databaseProvider == null) + return new List<(ulong, string, List, int, DateTime?)>(); - var now = Time.ActualDateTime(); + var now = Time.ActualDateTime(); - try - { - await using var connection = await database.GetConnectionAsync(); + try + { + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetAdminsQuery(); + var admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList(); - const string sql = """ - SELECT sa_admins.player_steamid, sa_admins.player_name, sa_admins_flags.flag, sa_admins.immunity, sa_admins.ends - FROM sa_admins_flags - JOIN sa_admins ON sa_admins_flags.admin_id = sa_admins.id - WHERE (sa_admins.ends IS NULL OR sa_admins.ends > @CurrentTime) - AND (sa_admins.server_id IS NULL OR sa_admins.server_id = @serverid) - ORDER BY sa_admins.player_steamid - """; + var groupedPlayers = admins + .GroupBy(r => new { playerSteamId = r.player_steamid, playerName = r.player_name, r.immunity, r.ends }) + .Select(g => + { + ulong steamId = g.Key.playerSteamId switch + { + long l => (ulong)l, + int i => (ulong)i, + string s when ulong.TryParse(s, out var parsed) => parsed, + _ => 0UL + }; - var admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList(); + int immunity = g.Key.immunity switch + { + int i => i, + string s when int.TryParse(s, out var parsed) => parsed, + _ => 0 + }; - // Group by player_steamid and aggregate the flags - var groupedPlayers = admins - .GroupBy(r => new { r.player_steamid, r.player_name, r.immunity, r.ends }) - .Select(g => ( - PlayerSteamId: (string)g.Key.player_steamid, - PlayerName: (string)g.Key.player_name, - Flags: g.Select(r => (string)r.flag).Distinct().ToList(), - Immunity: g.Key.immunity is int i ? i : int.TryParse((string)g.Key.immunity, out var immunity) ? immunity : 0, - Ends: g.Key.ends is DateTime dateTime ? dateTime : (DateTime?)null - )) - .ToList(); + DateTime? ends = g.Key.ends as DateTime?; - - // foreach (var player in groupedPlayers) - // { - // Console.WriteLine($"Player SteamID: {player.PlayerSteamId}, Name: {player.PlayerName}, Flags: {string.Join(", ", player.Flags)}, Immunity: {player.Immunity}, Ends: {player.Ends}"); - // } - - List<(string, string, List, int, DateTime?)> filteredFlagsWithImmunity = []; + string playerName = g.Key.playerName as string ?? string.Empty; - // Add the grouped players to the list - filteredFlagsWithImmunity.AddRange(groupedPlayers); + // tutaj zakładamy, że Dapper zwraca już string (nie dynamic) + var flags = g.Select(r => r.flag as string ?? string.Empty) + .Distinct() + .ToList(); - return filteredFlagsWithImmunity; - } - catch (Exception ex) - { - CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message); - return []; - } + return (steamId, playerName, flags, immunity, ends); + }) + .ToList(); + + return groupedPlayers; + } + catch (Exception ex) + { + CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message); + return []; + } } + /* public async Task, List>, int>>> GetAllGroupsFlags() { @@ -177,33 +184,29 @@ public class PermissionManager(Database.Database? database) } */ + /// + /// Retrieves all groups' data including flags and immunity asynchronously. + /// + /// A dictionary with group names as keys and tuples of flags and immunity as values. private async Task, int)>> GetAllGroupsData() { - if (database == null) return []; + if (databaseProvider == null) return []; - await using MySqlConnection connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); + ; try { - var sql = "SELECT group_id FROM sa_groups_servers WHERE (server_id = @serverid OR server_id IS NULL)"; - var groupDataSql = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList(); - - sql = """ - SELECT g.group_id, sg.name AS group_name, sg.immunity, f.flag - FROM sa_groups_flags f - JOIN sa_groups_servers g ON f.group_id = g.group_id - JOIN sa_groups sg ON sg.id = g.group_id - WHERE (g.server_id = @serverid OR server_id IS NULL) - """; + // var sql = "SELECT group_id FROM sa_groups_servers WHERE (server_id = @serverid OR server_id IS NULL)"; + // var groupDataSql = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList(); + var sql = databaseProvider.GetGroupsQuery(); var groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList(); - - if (groupDataSql.Count == 0 || groupData.Count == 0) + if (groupData.Count == 0) { return []; } var groupInfoDictionary = new Dictionary, int)>(); - foreach (var row in groupData) { var groupName = (string)row.group_name; @@ -231,10 +234,13 @@ public class PermissionManager(Database.Database? database) return []; } + /// + /// Creates a JSON file containing groups data asynchronously. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public async Task CrateGroupsJsonFile() { var groupsData = await GetAllGroupsData(); - var jsonData = new Dictionary(); foreach (var kvp in groupsData) @@ -248,7 +254,13 @@ public class PermissionManager(Database.Database? database) jsonData[kvp.Key] = groupData; } - var json = JsonConvert.SerializeObject(jsonData, Formatting.Indented); + var options = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var json = JsonSerializer.Serialize(jsonData, options); var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "groups.json"); await File.WriteAllTextAsync(filePath, json); } @@ -313,11 +325,15 @@ public class PermissionManager(Database.Database? database) } */ + /// + /// Creates a JSON file containing admins data asynchronously. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] public async Task CreateAdminsJsonFile() { - List<(string identity, string name, List flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags(); + List<(ulong identity, string name, List flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags(); var validPlayers = allPlayers - .Where(player => SteamID.TryParse(player.identity, out _)) + .Where(player => SteamID.TryParse(player.identity.ToString(), out _)) .ToList(); // foreach (var player in allPlayers) @@ -335,10 +351,10 @@ public class PermissionManager(Database.Database? database) var jsonData = validPlayers .GroupBy(player => player.name) // Group by player name .ToDictionary( - group => group.Key, // Use the player name as the key + group => group.Key, // Use the player name as key object (group) => { - // Consolidate data for players with the same name + // Consolidate data for players with same name var consolidatedData = group.Aggregate( new { @@ -349,16 +365,16 @@ public class PermissionManager(Database.Database? database) }, (acc, player) => { - // Merge identities and use the latest or first non-null identity - if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity)) + // Merge identities + if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString())) { - acc = acc with { identity = player.identity }; + acc = acc with { identity = player.identity.ToString() }; } - // Combine immunities by taking the maximum value + // Combine immunities by maximum value acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) }; - // Combine flags and groups, ensuring no duplicates + // Combine flags and groups acc = acc with { flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(), @@ -368,7 +384,7 @@ public class PermissionManager(Database.Database? database) return acc; }); - Server.NextFrameAsync(() => + Server.NextWorldUpdate(() => { var keysToRemove = new List(); @@ -399,7 +415,7 @@ public class PermissionManager(Database.Database? database) foreach (var player in group) { - if (SteamID.TryParse(player.identity, out var steamId) && steamId != null) + if (SteamID.TryParse(player.identity.ToString(), out var steamId) && steamId != null) { AdminCache.TryAdd(steamId, (player.ends, player.flags)); } @@ -440,7 +456,13 @@ public class PermissionManager(Database.Database? database) return consolidatedData; }); - var json = JsonConvert.SerializeObject(jsonData, Formatting.Indented); + var options = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var json = JsonSerializer.Serialize(jsonData, options); var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json"); await File.WriteAllTextAsync(filePath, json); @@ -448,32 +470,42 @@ public class PermissionManager(Database.Database? database) //await File.WriteAllTextAsync(CS2_SimpleAdmin.Instance.ModuleDirectory + "/data/admins.json", json); } + /// + /// Deletes an admin by their SteamID from the database asynchronously. + /// + /// The SteamID of the admin to delete. + /// Whether to delete the admin globally or only for the current server. public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false) { - if (database == null) return; + if (databaseProvider == null) return; if (string.IsNullOrEmpty(playerSteamId)) return; //_adminCache.TryRemove(playerSteamId, out _); try { - await using var connection = await database.GetConnectionAsync(); - - var sql = globalDelete - ? "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID" - : "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId"; - + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetDeleteAdminQuery(globalDelete); await connection.ExecuteAsync(sql, new { PlayerSteamID = playerSteamId, CS2_SimpleAdmin.ServerId }); } catch (Exception ex) { - CS2_SimpleAdmin._logger?.LogError(ex.ToString()); + CS2_SimpleAdmin._logger?.LogError(ex.Message); } } + /// + /// Adds a new admin with specified details asynchronously. + /// + /// SteamID of the admin. + /// Name of the admin. + /// List of flags assigned to the admin. + /// Immunity level. + /// Duration in minutes for admin expiration; 0 means permanent. + /// Whether the admin is global or server-specific. public async Task AddAdminBySteamId(string playerSteamId, string playerName, List flagsList, int immunity = 0, int time = 0, bool globalAdmin = false) { - if (database == null) return; + if (databaseProvider == null) return; if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return; @@ -487,12 +519,10 @@ public class PermissionManager(Database.Database? database) try { - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); // Insert admin into sa_admins table - const string insertAdminSql = "INSERT INTO `sa_admins` (`player_steamid`, `player_name`, `immunity`, `ends`, `created`, `server_id`) " + - "VALUES (@playerSteamid, @playerName, @immunity, @ends, @created, @serverid); SELECT LAST_INSERT_ID();"; - + var insertAdminSql = databaseProvider.GetAddAdminQuery(); var adminId = await connection.ExecuteScalarAsync(insertAdminSql, new { playerSteamId, @@ -506,36 +536,26 @@ public class PermissionManager(Database.Database? database) // Insert flags into sa_admins_flags table foreach (var flag in flagsList) { - if (flag.StartsWith($"#")) - { - // const string sql = "SELECT id FROM `sa_groups` WHERE name = @groupName"; - // var groupId = await connection.QuerySingleOrDefaultAsync(sql, new { groupName = flag }); - - const string sql = """ - 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 - """; - - var groupId = await connection.QuerySingleOrDefaultAsync(sql, new { groupName = flag, CS2_SimpleAdmin.ServerId }); - - if (groupId != null) - { - const string updateAdminGroup = "UPDATE `sa_admins` SET group_id = @groupId WHERE id = @adminId"; - await connection.ExecuteAsync(updateAdminGroup, new - { - groupId, - adminId - }); - } - } - - const string insertFlagsSql = "INSERT INTO `sa_admins_flags` (`admin_id`, `flag`) " + - "VALUES (@adminId, @flag)"; + // if (flag.StartsWith($"#")) + // { + // // const string sql = "SELECT id FROM `sa_groups` WHERE name = @groupName"; + // // var groupId = await connection.QuerySingleOrDefaultAsync(sql, new { groupName = flag }); + // + // var sql = databaseProvider.GetGroupIdByNameQuery(); + // var groupId = await connection.QuerySingleOrDefaultAsync(sql, new { groupName = flag, CS2_SimpleAdmin.ServerId }); + // + // if (groupId != null) + // { + // var updateAdminGroup = "UPDATE `sa_admins` SET group_id = @groupId WHERE id = @adminId"; + // await connection.ExecuteAsync(updateAdminGroup, new + // { + // groupId, + // adminId + // }); + // } + // } + var insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery(); await connection.ExecuteAsync(insertFlagsSql, new { adminId, @@ -554,18 +574,24 @@ public class PermissionManager(Database.Database? database) } } + /// + /// Adds a new group with flags and immunity asynchronously. + /// + /// Name of the group. + /// List of flags assigned to the group. + /// Immunity level of the group. + /// Whether the group is global or server-specific. public async Task AddGroup(string groupName, List flagsList, int immunity = 0, bool globalGroup = false) { - if (database == null) return; + if (databaseProvider == null) return; if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return; - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); try { // Insert group into sa_groups table - const string insertGroup = "INSERT INTO `sa_groups` (`name`, `immunity`) " + - "VALUES (@groupName, @immunity); SELECT LAST_INSERT_ID();"; + var insertGroup = databaseProvider.GetAddGroupQuery(); var groupId = await connection.ExecuteScalarAsync(insertGroup, new { groupName, @@ -575,8 +601,7 @@ public class PermissionManager(Database.Database? database) // Insert flags into sa_groups_flags table foreach (var flag in flagsList) { - const string insertFlagsSql = "INSERT INTO `sa_groups_flags` (`group_id`, `flag`) " + - "VALUES (@groupId, @flag)"; + var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery(); await connection.ExecuteAsync(insertFlagsSql, new { @@ -585,11 +610,8 @@ public class PermissionManager(Database.Database? database) }); } - const string insertGroupServer = "INSERT INTO `sa_groups_servers` (`group_id`, `server_id`) " + - "VALUES (@groupId, @server_id)"; - + var insertGroupServer = databaseProvider.GetAddGroupServerQuery(); await connection.ExecuteAsync(insertGroupServer, new { groupId, server_id = globalGroup ? null : CS2_SimpleAdmin.ServerId }); - await Server.NextWorldUpdateAsync(() => { CS2_SimpleAdmin.Instance.ReloadAdmins(null); @@ -602,16 +624,20 @@ public class PermissionManager(Database.Database? database) } } + /// + /// Deletes a group by name asynchronously. + /// + /// Name of the group to delete. public async Task DeleteGroup(string groupName) { - if (database == null) return; + if (databaseProvider == null) return; if (string.IsNullOrEmpty(groupName)) return; - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); try { - const string sql = "DELETE FROM `sa_groups` WHERE name = @groupName"; + var sql = databaseProvider.GetDeleteGroupQuery(); await connection.ExecuteAsync(sql, new { groupName }); } catch (Exception ex) @@ -620,15 +646,18 @@ public class PermissionManager(Database.Database? database) } } + /// + /// Deletes admins whose permissions have expired asynchronously. + /// public async Task DeleteOldAdmins() { - if (database == null) return; + if (databaseProvider == null) return; try { - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); - const string sql = "DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime"; + var sql = databaseProvider.GetDeleteOldAdminsQuery(); await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime() }); } catch (Exception) diff --git a/CS2-SimpleAdmin/Managers/PlayerManager.cs b/CS2-SimpleAdmin/Managers/PlayerManager.cs index 7b4c0de..6787471 100644 --- a/CS2-SimpleAdmin/Managers/PlayerManager.cs +++ b/CS2-SimpleAdmin/Managers/PlayerManager.cs @@ -11,285 +11,360 @@ using ZLinq; namespace CS2_SimpleAdmin.Managers; -public class PlayerManager +internal class PlayerManager { + private readonly SemaphoreSlim _loadPlayerSemaphore = new(5); private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config; + /// + /// Loads and initializes player data when a client connects. + /// + /// The instance representing the connecting player. + /// + /// Determines whether to perform a full synchronization of player data. + /// If true, full checks (bans, IP history, penalties, warns, mutes) will be loaded and applied. + /// + /// + /// This method validates the player's identity, checks for bans, updates the IP history table, + /// loads penalties (mutes/gags/warns), and optionally notifies admin players about the connecting player's penalties. + /// public void LoadPlayerData(CCSPlayerController player, bool fullConnect = false) { - if (player.IsBot || string.IsNullOrEmpty(player.IpAddress) || player.IpAddress.Contains("127.0.0.1")) - return; - if (!player.UserId.HasValue) { Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION); return; } - var ipAddress = player.IpAddress?.Split(":")[0]; - CS2_SimpleAdmin.PlayersInfo[player.UserId.Value] = - new PlayerInfo(player.UserId.Value, player.Slot, new SteamID(player.SteamID), player.PlayerName, ipAddress); - - - // if (!player.UserId.HasValue) - // { - // Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION); - // return; - // } - var userId = player.UserId.Value; - if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(userId)) - { - Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION); - } - - var steamId64 = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64; - var steamId = steamId64.ToString(); - - if (CS2_SimpleAdmin.Database == null) return; + var slot = player.Slot; + var steamId = player.SteamID; + var playerName = !string.IsNullOrEmpty(player.PlayerName) + ? player.PlayerName + : CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; + var ipAddress = player.IpAddress?.Split(":")[0]; + + if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return; - // Perform asynchronous database operations within a single method Task.Run(async () => { - var isBanned = CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch + try { - 0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(steamId, null), - _ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp - ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(steamId64, ipAddress) - : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(steamId, ipAddress) - }; + await _loadPlayerSemaphore.WaitAsync(); - if (isBanned) - { - // Kick the player if banned - await Server.NextFrameAsync(() => + if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId)) { - if (!player.UserId.HasValue) return; - Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED); - }); - - return; - } - - if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null) - { - try - { - if (CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer( - CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64, ipAddress)) + var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch { - await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync(); - - const string updateQuery = """ - UPDATE `sa_players_ips` - SET used_at = CURRENT_TIMESTAMP - WHERE steamid = @SteamID AND address = @IPAddress; - """; - await connection.ExecuteAsync(updateQuery, new - { - SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64, - IPAddress = IpHelper.IpToUint(ipAddress) - }); - } - else - { - await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync(); + 0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null), + _ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp + ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, + ipAddress) + : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress) + }; - const string selectQuery = - "SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;"; - var recordExists = await connection.ExecuteScalarAsync(selectQuery, new + // CS2_SimpleAdmin._logger?.LogInformation($"Player {playerName} ({steamId} - {ipAddress}) is banned? {isBanned.ToString()}"); + + if (isBanned) + { + await Server.NextWorldUpdateAsync(() => { - SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64, - IPAddress = IpHelper.IpToUint(ipAddress) + // CS2_SimpleAdmin._logger?.LogInformation($"Kicking {playerName}"); + Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED); }); - if (recordExists > 0) - { - const string updateQuery = """ - UPDATE `sa_players_ips` - SET used_at = CURRENT_TIMESTAMP - WHERE steamid = @SteamID AND address = @IPAddress; - """; - await connection.ExecuteAsync(updateQuery, new - { - SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64, - IPAddress = IpHelper.IpToUint(ipAddress) - }); - } - else - { - const string insertQuery = """ - INSERT INTO `sa_players_ips` (steamid, address, used_at) - VALUES (@SteamID, @IPAddress, CURRENT_TIMESTAMP); - """; - await connection.ExecuteAsync(insertQuery, new - { - SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64, - IPAddress = IpHelper.IpToUint(ipAddress) - }); - } - } - } - catch (Exception ex) - { - CS2_SimpleAdmin._logger?.LogError( - $"Unable to save ip address for {CS2_SimpleAdmin.PlayersInfo[userId].Name} ({ipAddress}) {ex.Message}"); - } - - // Get all accounts associated to the player (ip address) - CS2_SimpleAdmin.PlayersInfo[userId].AccountsAssociated = - CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable().Select(x => (x.SteamId, x.PlayerName)).ToList() ?? []; - } - - try - { - // var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 - // ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned( - // CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), null) - // : CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp - // ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(CS2_SimpleAdmin - // .PlayersInfo[userId].SteamId.SteamId64) - // : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), ipAddress); - - if (fullConnect || !fullConnect) // Temp skip - { - var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(CS2_SimpleAdmin.PlayersInfo[userId], false); - var (totalMutes, totalGags, totalSilences) = - await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(CS2_SimpleAdmin.PlayersInfo[userId]); - - CS2_SimpleAdmin.PlayersInfo[userId].TotalBans = CS2_SimpleAdmin.Instance.CacheManager?.GetPlayerBansBySteamId(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString()).Count ?? 0; - CS2_SimpleAdmin.PlayersInfo[userId].TotalMutes = totalMutes; - CS2_SimpleAdmin.PlayersInfo[userId].TotalGags = totalGags; - CS2_SimpleAdmin.PlayersInfo[userId].TotalSilences = totalSilences; - CS2_SimpleAdmin.PlayersInfo[userId].TotalWarns = warns.Count; - - // Check if the player is muted - var activeMutes = await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString()); - - if (activeMutes.Count > 0) - { - foreach (var mute in activeMutes) - { - string muteType = mute.type; - DateTime ends = mute.ends; - int duration = mute.duration; - switch (muteType) - { - // Apply mute penalty based on mute type - case "GAG": - PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Gag, ends, duration); - // if (CS2_SimpleAdmin._localizer != null) - // mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]); - break; - case "MUTE": - PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Mute, ends, duration); - await Server.NextFrameAsync(() => - { - player.VoiceFlags = VoiceFlags.Muted; - }); - // if (CS2_SimpleAdmin._localizer != null) - // mutesList[PenaltyType.Mute].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.InvariantCulture)]); - break; - default: - PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Silence, ends, duration); - await Server.NextFrameAsync(() => - { - player.VoiceFlags = VoiceFlags.Muted; - }); - // if (CS2_SimpleAdmin._localizer != null) - // mutesList[PenaltyType.Silence].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]); - break; - } - } + return; } } - if (CS2_SimpleAdmin.Instance.Config.OtherSettings.NotifyPenaltiesToAdminOnConnect && fullConnect) + if (fullConnect) { - var associatedAcccountsChunks = CS2_SimpleAdmin.PlayersInfo[userId].AccountsAssociated.ChunkBy(5).ToList(); - - await Server.NextFrameAsync(() => - { - foreach (var admin in Helper.GetValidPlayers() - .Where(p => (AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/kick") || - AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/ban")) && - p.Connected == PlayerConnectedState.PlayerConnected && !CS2_SimpleAdmin.AdminDisabledJoinComms.Contains(p.SteamID))) - { - if (CS2_SimpleAdmin._localizer != null && admin != player) - { - admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, "sa_admin_penalty_info", - player.PlayerName, - CS2_SimpleAdmin.PlayersInfo[userId].TotalBans, - CS2_SimpleAdmin.PlayersInfo[userId].TotalGags, - CS2_SimpleAdmin.PlayersInfo[userId].TotalMutes, - CS2_SimpleAdmin.PlayersInfo[userId].TotalSilences, - CS2_SimpleAdmin.PlayersInfo[userId].TotalWarns - ); + var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress); + CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo; - foreach (var chunk in associatedAcccountsChunks) + await Server.NextWorldUpdateAsync(() => + { + if (!CS2_SimpleAdmin.CachedPlayers.Contains(player)) + CS2_SimpleAdmin.CachedPlayers.Add(player); + }); + + if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null && + CS2_SimpleAdmin.PlayersInfo[steamId] != null) + { + try + { + await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); + + if (CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer( + steamId, ipAddress)) + { + const string updateQuery = """ + UPDATE `sa_players_ips` + SET used_at = CURRENT_TIMESTAMP, + name = @playerName + WHERE steamid = @SteamID AND address = @IPAddress; + """; + await connection.ExecuteAsync(updateQuery, new { - admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, "sa_admin_associated_accounts", - player.PlayerName, - string.Join(", ", - chunk.Select(a => $"{a.PlayerName} ({a.SteamId})")) - ); + playerName, + SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64, + IPAddress = IpHelper.IpToUint(ipAddress) + }); + } + else + { + const string selectQuery = + "SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;"; + var recordExists = await connection.ExecuteScalarAsync(selectQuery, new + { + SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64, + IPAddress = IpHelper.IpToUint(ipAddress) + }); + + if (recordExists > 0) + { + const string updateQuery = """ + UPDATE `sa_players_ips` + SET used_at = CURRENT_TIMESTAMP, + name = @playerName + WHERE steamid = @SteamID AND address = @IPAddress; + """; + await connection.ExecuteAsync(updateQuery, new + { + playerName, + SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64, + IPAddress = IpHelper.IpToUint(ipAddress) + }); + } + else + { + const string insertQuery = """ + INSERT INTO `sa_players_ips` (steamid, name, address, used_at) + VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP); + """; + await connection.ExecuteAsync(insertQuery, new + { + SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64, + playerName, + IPAddress = IpHelper.IpToUint(ipAddress) + }); } } } - }); + catch (Exception ex) + { + CS2_SimpleAdmin._logger?.LogError( + $"Unable to save ip address for {playerInfo.Name} ({ipAddress}) {ex.Message}"); + } + + playerInfo.AccountsAssociated = + CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable() + .Select(x => (x.SteamId, x.PlayerName)).ToList() ?? []; + } + + try + { + // var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 + // ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned( + // CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), null) + // : CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp + // ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(CS2_SimpleAdmin + // .PlayersInfo[userId].SteamId.SteamId64) + // : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), ipAddress); + + if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(steamId, out PlayerInfo? value)) // Temp skip + { + var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false); + var (totalMutes, totalGags, totalSilences) = + await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(value); + value.TotalBans = CS2_SimpleAdmin.Instance.CacheManager + ?.GetPlayerBansBySteamId(value.SteamId.SteamId64) + .Count ?? 0; + value.TotalMutes = totalMutes; + value.TotalGags = totalGags; + value.TotalSilences = totalSilences; + value.TotalWarns = warns.Count; + + var activeMutes = + await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(value.SteamId.SteamId64 + .ToString()); + + if (activeMutes.Count > 0) + { + foreach (var mute in activeMutes) + { + string muteType = mute.type; + DateTime ends = mute.ends; + int duration = mute.duration; + switch (muteType) + { + // Apply mute penalty based on mute type + case "GAG": + PlayerPenaltyManager.AddPenalty( + CS2_SimpleAdmin.PlayersInfo[steamId].Slot, + PenaltyType.Gag, ends, duration); + // if (CS2_SimpleAdmin._localizer != null) + // mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]); + break; + case "MUTE": + PlayerPenaltyManager.AddPenalty( + CS2_SimpleAdmin.PlayersInfo[steamId].Slot, + PenaltyType.Mute, ends, duration); + await Server.NextWorldUpdateAsync(() => + { + player.VoiceFlags = VoiceFlags.Muted; + }); + // if (CS2_SimpleAdmin._localizer != null) + // mutesList[PenaltyType.Mute].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.InvariantCulture)]); + break; + default: + PlayerPenaltyManager.AddPenalty( + CS2_SimpleAdmin.PlayersInfo[steamId].Slot, + PenaltyType.Silence, ends, duration); + await Server.NextWorldUpdateAsync(() => + { + player.VoiceFlags = VoiceFlags.Muted; + }); + // if (CS2_SimpleAdmin._localizer != null) + // mutesList[PenaltyType.Silence].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]); + break; + } + } + } + + if (CS2_SimpleAdmin.Instance.Config.OtherSettings.NotifyPenaltiesToAdminOnConnect) + { + await Server.NextWorldUpdateAsync(() => + { + foreach (var admin in Helper.GetValidPlayers() + .Where(p => (AdminManager.PlayerHasPermissions( + new SteamID(p.SteamID), + "@css/kick") || + AdminManager.PlayerHasPermissions( + new SteamID(p.SteamID), + "@css/ban")) && + p.Connected == PlayerConnectedState.PlayerConnected && + !CS2_SimpleAdmin.AdminDisabledJoinComms + .Contains(p.SteamID))) + { + if (CS2_SimpleAdmin._localizer == null || admin == player) continue; + admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, + "sa_admin_penalty_info", + player.PlayerName, + CS2_SimpleAdmin.PlayersInfo[steamId].TotalBans, + CS2_SimpleAdmin.PlayersInfo[steamId].TotalGags, + CS2_SimpleAdmin.PlayersInfo[steamId].TotalMutes, + CS2_SimpleAdmin.PlayersInfo[steamId].TotalSilences, + CS2_SimpleAdmin.PlayersInfo[steamId].TotalWarns + ); + + if (CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.Count >= 2) + { + var associatedAcccountsChunks = + CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.ChunkBy(5) + .ToList(); + foreach (var chunk in associatedAcccountsChunks) + { + admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, + "sa_admin_associated_accounts", + player.PlayerName, + string.Join(", ", + chunk.Select(a => $"{a.PlayerName} ({a.SteamId})")) + ); + } + } + } + }); + } + } + } + catch (Exception ex) + { + CS2_SimpleAdmin._logger?.LogError("Error processing player connection: {exception}", + ex.Message); + } } } - catch (Exception ex) + finally { - CS2_SimpleAdmin._logger?.LogError("Error processing player connection: {exception}", ex.Message); + _loadPlayerSemaphore.Release(); } }); - if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name)) { player.Rename(name); } } + /// + /// Periodically checks the status of online players and applies timers for speed, gravity, + /// and penalty expiration validation. + /// + /// + /// This method registers two repeating timers: + /// + /// One short-interval timer to update speed/gravity modifications applied to players. + /// + /// One long-interval timer (default 61 seconds) to expire bans, mutes, warns, refresh caches, + /// and remove outdated penalties from connected players. + /// + /// + /// Additionally, banned players still online are kicked, and admins may be updated about mute statuses based on the configured time mode. + /// public void CheckPlayersTimer() { - CS2_SimpleAdmin.Instance.AddTimer(0.1f, () => + CS2_SimpleAdmin.Instance.AddTimer(0.12f, () => { - if (CS2_SimpleAdmin.GravityPlayers.Count <= 0) return; - - foreach (var value in CS2_SimpleAdmin.GravityPlayers.Where(value => value.Key is - { IsValid: true, Connected: PlayerConnectedState.PlayerConnected } || value.Key.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)) + if (CS2_SimpleAdmin.SpeedPlayers.Count > 0) { - value.Key.SetGravity(value.Value); + foreach (var (player, speed) in CS2_SimpleAdmin.SpeedPlayers) + { + if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }) + { + player.SetSpeed(speed); + } + } + } + + if (CS2_SimpleAdmin.GravityPlayers.Count > 0) + { + foreach (var (player, gravity) in CS2_SimpleAdmin.GravityPlayers) + { + if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }) + { + player.SetGravity(gravity); + } + } } }, TimerFlags.REPEAT); - CS2_SimpleAdmin.Instance.AddTimer(61.0f, () => + CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () => { #if DEBUG CS2_SimpleAdmin._logger?.LogCritical("[OnMapStart] Expired check"); #endif - if (CS2_SimpleAdmin.Database == null) + if (CS2_SimpleAdmin.DatabaseProvider == null) return; var tempPlayers = Helper.GetValidPlayers() .Select(p => new { - p.SteamID, p.IpAddress, p.UserId, p.Slot, + p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot, }) .ToList(); + var pluginInstance = CS2_SimpleAdmin.Instance; _ = Task.Run(async () => { try { - var expireTasks = new Task[] + var expireTasks = new[] { - CS2_SimpleAdmin.Instance.BanManager.ExpireOldBans(), - CS2_SimpleAdmin.Instance.MuteManager.ExpireOldMutes(), - CS2_SimpleAdmin.Instance.WarnManager.ExpireOldWarns(), - CS2_SimpleAdmin.Instance.CacheManager?.RefreshCacheAsync() ?? Task.CompletedTask, - CS2_SimpleAdmin.Instance.PermissionManager.DeleteOldAdmins() + pluginInstance.BanManager.ExpireOldBans(), + pluginInstance.MuteManager.ExpireOldMutes(), + pluginInstance.WarnManager.ExpireOldWarns(), + pluginInstance.CacheManager?.RefreshCacheAsync() ?? Task.CompletedTask, + pluginInstance.PermissionManager.DeleteOldAdmins() }; await Task.WhenAll(expireTasks); @@ -306,30 +381,40 @@ public class PlayerManager } } } + + if (pluginInstance.CacheManager == null) + return; var bannedPlayers = tempPlayers.AsValueEnumerable() .Where(player => { - return CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch + var playerName = player.PlayerName; + var steamId = player.SteamID; + var ip = player.IpAddress?.Split(':')[0]; + + return CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch { - 0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(player.SteamID.ToString(), null), - _ => - CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(player.SteamID.ToString(), player.IpAddress?.Split(":")[0]) + 0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null), + _ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp + ? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip) + : pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip) }; - }) - .ToList(); - - foreach (var player in bannedPlayers) + }).ToList(); + + if (bannedPlayers.Count > 0) { - if (player.UserId.HasValue) - await Server.NextFrameAsync(() => Helper.KickPlayer((int)player.UserId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED)); + foreach (var player in bannedPlayers) + { + if (!player.UserId.HasValue) continue; + await Server.NextWorldUpdateAsync(() => Helper.KickPlayer((int)player.UserId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED)); + } } - var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList(); - if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return; if (_config.OtherSettings.TimeMode == 0) { - await CS2_SimpleAdmin.Instance.MuteManager.CheckOnlineModeMutes(onlinePlayers); + var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList(); + if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return; + await pluginInstance.MuteManager.CheckOnlineModeMutes(onlinePlayers); } }); @@ -348,7 +433,6 @@ public class PlayerManager foreach (var entry in penalizedSlots) { - // If the player is not muted or silenced, set voice flags to normal if (!entry.IsMuted && !entry.IsSilenced) { entry.Player.VoiceFlags = VoiceFlags.Normal; @@ -361,7 +445,6 @@ public class PlayerManager { CS2_SimpleAdmin._logger?.LogError($"Unable to remove old penalties: {ex.Message}"); } - }, TimerFlags.REPEAT); } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Managers/PlayerPenaltyManager.cs b/CS2-SimpleAdmin/Managers/PlayerPenaltyManager.cs index afc9b7b..2aab682 100644 --- a/CS2-SimpleAdmin/Managers/PlayerPenaltyManager.cs +++ b/CS2-SimpleAdmin/Managers/PlayerPenaltyManager.cs @@ -8,7 +8,13 @@ public static class PlayerPenaltyManager private static readonly ConcurrentDictionary>> Penalties = new(); - // Add a penalty for a player + /// + /// Adds a penalty for a specific player slot and penalty type. + /// + /// The player slot where the penalty should be applied. + /// The type of penalty to apply (e.g. gag, mute, silence). + /// The validity expiration date/time of the penalty. + /// The duration of the penalty in minutes (0 for permanent). public static void AddPenalty(int slot, PenaltyType penaltyType, DateTime endDateTime, int durationInMinutes) { Penalties.AddOrUpdate(slot, @@ -33,6 +39,13 @@ public static class PlayerPenaltyManager }); } + /// + /// Determines whether a player is currently penalized with the given penalty type. + /// + /// The player slot to check. + /// The penalty type to check. + /// The out-parameter returning the end datetime of the penalty if active. + /// True if the player has an active penalty, false otherwise. public static bool IsPenalized(int slot, PenaltyType penaltyType, out DateTime? endDateTime) { endDateTime = null; @@ -74,7 +87,12 @@ public static class PlayerPenaltyManager return false; } - // Get the end datetime and duration of penalties for a player and penalty type + /// + /// Retrieves all penalties for a player of a specific penalty type. + /// + /// The player slot. + /// The penalty type to retrieve. + /// A list of penalties if found, otherwise an empty list. public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, PenaltyType penaltyType) { if (Penalties.TryGetValue(slot, out var penaltyDict) && @@ -85,6 +103,35 @@ public static class PlayerPenaltyManager return []; } + /// + /// Retrieves all penalties for a player across multiple penalty types. + /// + /// The player slot. + /// A list of penalty types to retrieve. + /// A combined list of penalties of all requested types. + public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, List penaltyType) + { + List<(DateTime EndDateTime, int Duration, bool Passed)> result = []; + + if (Penalties.TryGetValue(slot, out var penaltyDict)) + { + foreach (var type in penaltyType) + { + if (penaltyDict.TryGetValue(type, out var penaltiesList)) + { + result.AddRange(penaltiesList); + } + } + } + + return result; + } + + /// + /// Retrieves all penalties for a player across all penalty types. + /// + /// The player slot. + /// A dictionary with penalty types as keys and lists of penalties as values. public static Dictionary> GetAllPlayerPenalties(int slot) { // Check if the player has any penalties in the dictionary @@ -95,12 +142,20 @@ public static class PlayerPenaltyManager new Dictionary>(); } + /// + /// Checks if a given slot has any penalties assigned. + /// + /// The player slot. + /// True if the player has any penalties, false otherwise. public static bool IsSlotInPenalties(int slot) { return Penalties.ContainsKey(slot); } - // Remove all penalties for a player slot + /// + /// Removes all penalties assigned to a specific player slot. + /// + /// The player slot. public static void RemoveAllPenalties(int slot) { if (Penalties.ContainsKey(slot)) @@ -109,13 +164,19 @@ public static class PlayerPenaltyManager } } - // Remove all penalties + /// + /// Removes all penalties for all players. + /// public static void RemoveAllPenalties() { Penalties.Clear(); } - // Remove all penalties of a selected type from a specific player + /// + /// Removes all penalties of a specific type from a player. + /// + /// The player slot. + /// The penalty type to remove. public static void RemovePenaltiesByType(int slot, PenaltyType penaltyType) { if (Penalties.TryGetValue(slot, out var penaltyDict) && @@ -125,6 +186,11 @@ public static class PlayerPenaltyManager } } + /// + /// Marks penalties with a specific end datetime as "passed" for a player. + /// + /// The player slot. + /// The end datetime of penalties to mark as passed. public static void RemovePenaltiesByDateTime(int slot, DateTime dateTime) { if (!Penalties.TryGetValue(slot, out var penaltyDict)) return; @@ -146,7 +212,13 @@ public static class PlayerPenaltyManager } } - // Remove all expired penalties for all players and penalty types + /// + /// Removes or expires penalties automatically across all players based on their duration or "passed" flag. + /// + /// + /// If TimeMode == 0, penalties are considered passed manually and are removed if flagged as such. + /// Otherwise, expired penalties are removed based on the current datetime compared with their end time. + /// public static void RemoveExpiredPenalties() { if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0) @@ -172,13 +244,11 @@ public static class PlayerPenaltyManager var now = Time.ActualDateTime(); foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating { - // Remove expired penalties for the player foreach (var penaltiesList in penaltyDict.Values) { penaltiesList.RemoveAll(p => p.Duration > 0 && now >= p.EndDateTime); } - // Remove player slot if no penalties left if (penaltyDict.Count == 0) { Penalties.TryRemove(playerSlot, out _); diff --git a/CS2-SimpleAdmin/Managers/ServerManager.cs b/CS2-SimpleAdmin/Managers/ServerManager.cs index 66131a0..ccde9cf 100644 --- a/CS2-SimpleAdmin/Managers/ServerManager.cs +++ b/CS2-SimpleAdmin/Managers/ServerManager.cs @@ -9,22 +9,32 @@ public class ServerManager { private int _getIpTryCount; + /// + /// Checks whether the server setting sv_hibernate_when_empty is enabled. + /// Logs an error if this setting is true, since it prevents the plugin from working properly. + /// public static void CheckHibernationStatus() { var convar = ConVar.Find("sv_hibernate_when_empty"); - if (convar == null || !convar.GetPrimitiveValue()) return; CS2_SimpleAdmin._logger?.LogError("Detected setting \"sv_hibernate_when_empty true\", set false to make plugin work properly"); } + /// + /// Initiates the asynchronous process to load server data such as IP address, port, hostname, and RCON password. + /// Handles retry attempts if IP address is not immediately available. + /// Updates or inserts the server record in the database accordingly. + /// After loading, triggers admin reload and cache initialization. + /// Also optionally sends plugin usage metrics if enabled in configuration. + /// public void LoadServerData() { - CS2_SimpleAdmin.Instance.AddTimer(1.2f, () => + CS2_SimpleAdmin.Instance.AddTimer(2.0f, () => { - if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.ServerId != null || CS2_SimpleAdmin.Database == null) return; - + if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return; + if (_getIpTryCount > 32 && Helper.GetServerIp().StartsWith("0.0.0.0") || string.IsNullOrEmpty(Helper.GetServerIp())) { CS2_SimpleAdmin._logger?.LogError("Unable to load server data - can't fetch ip address!"); @@ -32,7 +42,6 @@ public class ServerManager } var ipAddress = ConVar.Find("ip")?.StringValue; - if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0")) { ipAddress = Helper.GetServerIp(); @@ -40,23 +49,22 @@ public class ServerManager if (_getIpTryCount <= 32 && (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))) { _getIpTryCount++; - + LoadServerData(); return; } } var address = $"{ipAddress}:{ConVar.Find("hostport")?.GetPrimitiveValue()}"; - var hostname = ConVar.Find("hostname")!.StringValue; - var rconPassword = ConVar.Find("rcon_password")!.StringValue; + var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; + var rconPassword = ConVar.Find("rcon_password")?.StringValue ?? ""; CS2_SimpleAdmin.IpAddress = address; Task.Run(async () => { try { - await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync(); - + await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); int? serverId = await connection.ExecuteScalarAsync( "SELECT id FROM sa_servers WHERE address = @address", new { address }); @@ -79,7 +87,6 @@ public class ServerManager } CS2_SimpleAdmin.ServerId = serverId; - CS2_SimpleAdmin._logger?.LogInformation("Loaded server with ip {ip}", ipAddress); if (CS2_SimpleAdmin.ServerId != null) diff --git a/CS2-SimpleAdmin/Managers/WarnManager.cs b/CS2-SimpleAdmin/Managers/WarnManager.cs index 60c7dde..0f58b84 100644 --- a/CS2-SimpleAdmin/Managers/WarnManager.cs +++ b/CS2-SimpleAdmin/Managers/WarnManager.cs @@ -1,35 +1,37 @@ -using CS2_SimpleAdminApi; +using CS2_SimpleAdmin.Database; +using CS2_SimpleAdminApi; using Dapper; using Microsoft.Extensions.Logging; namespace CS2_SimpleAdmin.Managers; -internal class WarnManager(Database.Database? database) +internal class WarnManager(IDatabaseProvider? databaseProvider) { + /// + /// Adds a warning to a player with an optional issuer and reason. + /// + /// The player who is being warned. + /// The player issuing the warning; null indicates console or system. + /// The reason for the warning. + /// Optional duration of the warning in minutes (0 means permanent). + /// The identifier of the inserted warning, or null if the operation failed. public async Task WarnPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0) { - if (database == null) return null; + if (databaseProvider == null) return null; var now = Time.ActualDateTime(); var futureTime = now.AddMinutes(time); try { - await using var connection = await database.GetConnectionAsync(); - const string sql = """ - - INSERT INTO `sa_warns` - (`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) - VALUES - (@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @serverid); - SELECT LAST_INSERT_ID(); - """; + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetAddWarnQuery(true); var warnId = await connection.ExecuteScalarAsync(sql, new { - playerSteamid = player.SteamId.SteamId64.ToString(), + playerSteamid = player.SteamId.SteamId64, playerName = player.Name, - adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", + adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", muteReason = reason, duration = time, @@ -46,30 +48,30 @@ internal class WarnManager(Database.Database? database) } } - public async Task AddWarnBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0) + /// + /// Adds a warning to a player identified by SteamID with optional issuer and reason. + /// + /// The SteamID64 of the player being warned. + /// The player issuing the warning; null indicates console or system. + /// The reason for the warning. + /// Optional duration of the warning in minutes (0 means permanent). + /// The identifier of the inserted warning, or null if the operation failed. + public async Task AddWarnBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0) { - if (database == null) return null; - if (string.IsNullOrEmpty(playerSteamId)) return null; + if (databaseProvider == null) return null; var now = Time.ActualDateTime(); var futureTime = now.AddMinutes(time); try { - await using var connection = await database.GetConnectionAsync(); - const string sql = """ - - INSERT INTO `sa_warns` - (`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) - VALUES - (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @serverid); - SELECT LAST_INSERT_ID(); - """; + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetAddWarnQuery(false); var warnId = await connection.ExecuteScalarAsync(sql, new { playerSteamid = playerSteamId, - adminSteamid = issuer?.SteamId.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", + adminSteamid = issuer?.SteamId.SteamId64 ?? 0, adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console", muteReason = reason, duration = time, @@ -86,30 +88,22 @@ internal class WarnManager(Database.Database? database) } } + /// + /// Retrieves a list of warnings for a specific player. + /// + /// The player whose warnings to retrieve. + /// If true, returns only active (non-expired) warnings; otherwise returns all warnings. + /// A list of dynamic objects representing warnings, or an empty list if none found or on failure. public async Task> GetPlayerWarns(PlayerInfo player, bool active = true) { - if (database == null) return []; + if (databaseProvider == null) return []; try { - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); - string sql; - - if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) - { - sql = active - ? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC" - : "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC"; - } - else - { - sql = active - ? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC" - : "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC"; - } - - var parameters = new { PlayerSteamID = player.SteamId.SteamId64.ToString(), serverid = CS2_SimpleAdmin.ServerId }; + var sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active); + var parameters = new { PlayerSteamID = player.SteamId.SteamId64, serverid = CS2_SimpleAdmin.ServerId }; var warns = await connection.QueryAsync(sql, parameters); return warns.ToList(); @@ -120,24 +114,23 @@ internal class WarnManager(Database.Database? database) } } - public async Task GetPlayerWarnsCount(string steamId, bool active = true) + /// + /// Retrieves the count of warnings for a player specified by SteamID. + /// + /// The SteamID64 of the player. + /// If true, counts only active (non-expired) warnings; otherwise counts all warnings. + /// The count of warnings as an integer, or 0 if none found or on failure. + public async Task GetPlayerWarnsCount(ulong steamId, bool active = true) { - if (database == null) return 0; + if (databaseProvider == null) return 0; try { - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); - var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? active - ? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'" - : "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID" - : active - ? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'" - : "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID'"; - - var muteCount = await connection.ExecuteScalarAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId }); - return muteCount; + var sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active); + var warnsCount = await connection.ExecuteScalarAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId }); + return warnsCount; } catch (Exception) { @@ -145,19 +138,22 @@ internal class WarnManager(Database.Database? database) } } + /// + /// Removes a specific warning by its identifier from a player's record. + /// + /// The player whose warning will be removed. + /// The identifier of the warning to remove. + /// A task representing the asynchronous operation. public async Task UnwarnPlayer(PlayerInfo player, int warnId) { - if (database == null) return; + if (databaseProvider == null) return; try { - await using var connection = await database.GetConnectionAsync(); + await using var connection = await databaseProvider.CreateConnectionAsync(); - var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId" - : "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid"; - - await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64.ToString(), warnId, serverid = CS2_SimpleAdmin.ServerId }); + var sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); + await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64, warnId, serverid = CS2_SimpleAdmin.ServerId }); } catch (Exception ex) { @@ -165,36 +161,20 @@ internal class WarnManager(Database.Database? database) } } + /// + /// Removes the most recent warning matching a player pattern (usually SteamID string). + /// + /// The pattern identifying the player whose last warning should be removed. + /// A task representing the asynchronous operation. public async Task UnwarnPlayer(string playerPattern) { - if (database == null) return; + if (databaseProvider == null) return; try { - await using var connection = await database.GetConnectionAsync(); - - var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? """ - UPDATE sa_warns - 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; - """; + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); await connection.ExecuteAsync(sql, new { steamid = playerPattern, serverid = CS2_SimpleAdmin.ServerId }); } catch (Exception ex) @@ -203,18 +183,19 @@ internal class WarnManager(Database.Database? database) } } + /// + /// Expires old warnings based on the current time, removing or marking them as inactive. + /// + /// A task representing the asynchronous operation. public async Task ExpireOldWarns() { - if (database == null) return; + if (databaseProvider == null) return; try { - await using var connection = await database.GetConnectionAsync(); - - var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode - ? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime" - : "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid"; + await using var connection = await databaseProvider.CreateConnectionAsync(); + var sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId }); } catch (Exception ex) diff --git a/CS2-SimpleAdmin/Menus/AdminMenu.cs b/CS2-SimpleAdmin/Menus/AdminMenu.cs index 454128d..77c7ba3 100644 --- a/CS2-SimpleAdmin/Menus/AdminMenu.cs +++ b/CS2-SimpleAdmin/Menus/AdminMenu.cs @@ -45,9 +45,9 @@ public static class AdminMenu var menu = CreateMenu(localizer?["sa_title"] ?? "SimpleAdmin"); List options = [ - new ChatMenuOptionData(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)), - new ChatMenuOptionData(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)), - new ChatMenuOptionData(localizer?["sa_menu_fun_commands"] ?? "Fun Commands", () => FunActionsMenu.OpenMenu(admin)), + new(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)), + new(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)), + new(localizer?["sa_menu_fun_commands"] ?? "Fun Commands", () => FunActionsMenu.OpenMenu(admin)), ]; var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands; diff --git a/CS2-SimpleAdmin/Menus/ManageAdminsMenu.cs b/CS2-SimpleAdmin/Menus/ManageAdminsMenu.cs index 1b68955..30bbf95 100644 --- a/CS2-SimpleAdmin/Menus/ManageAdminsMenu.cs +++ b/CS2-SimpleAdmin/Menus/ManageAdminsMenu.cs @@ -24,12 +24,12 @@ public static class ManageAdminsMenu var menu = AdminMenu.CreateMenu(localizer?["sa_menu_admins_manage"] ?? "Admins Manage"); List options = [ - new ChatMenuOptionData(localizer?["sa_admin_add"] ?? "Add Admin", + new(localizer?["sa_admin_add"] ?? "Add Admin", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_admin_add"] ?? "Add Admin", AddAdminMenu)), - new ChatMenuOptionData(localizer?["sa_admin_remove"] ?? "Remove Admin", + new(localizer?["sa_admin_remove"] ?? "Remove Admin", () => PlayersMenu.OpenAdminPlayersMenu(admin, localizer?["sa_admin_remove"] ?? "Remove Admin", RemoveAdmin, player => player != admin && admin.CanTarget(player))), - new ChatMenuOptionData(localizer?["sa_admin_reload"] ?? "Reload Admins", () => ReloadAdmins(admin)) + new(localizer?["sa_admin_reload"] ?? "Reload Admins", () => ReloadAdmins(admin)) ]; foreach (var menuOptionData in options) diff --git a/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs b/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs index 8b64944..4c60555 100644 --- a/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs +++ b/CS2-SimpleAdmin/Menus/ManagePlayersMenu.cs @@ -90,12 +90,12 @@ public static class ManagePlayersMenu List options = [ // options added in order - new ChatMenuOptionData("0 hp", () => ApplySlapAndKeepMenu(admin, player, 0)), - new ChatMenuOptionData("1 hp", () => ApplySlapAndKeepMenu(admin, player, 1)), - new ChatMenuOptionData("5 hp", () => ApplySlapAndKeepMenu(admin, player, 5)), - new ChatMenuOptionData("10 hp", () => ApplySlapAndKeepMenu(admin, player, 10)), - new ChatMenuOptionData("50 hp", () => ApplySlapAndKeepMenu(admin, player, 50)), - new ChatMenuOptionData("100 hp", () => ApplySlapAndKeepMenu(admin, player, 100)), + new("0 hp", () => ApplySlapAndKeepMenu(admin, player, 0)), + new("1 hp", () => ApplySlapAndKeepMenu(admin, player, 1)), + new("5 hp", () => ApplySlapAndKeepMenu(admin, player, 5)), + new("10 hp", () => ApplySlapAndKeepMenu(admin, player, 10)), + new("50 hp", () => ApplySlapAndKeepMenu(admin, player, 50)), + new("100 hp", () => ApplySlapAndKeepMenu(admin, player, 100)), ]; foreach (var menuOptionData in options) @@ -312,10 +312,10 @@ public static class ManagePlayersMenu var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_team_force"] ?? "Force Team"} {player.PlayerName}"); List options = [ - new ChatMenuOptionData(CS2_SimpleAdmin._localizer?["sa_team_ct"] ?? "CT", () => ForceTeam(admin, player, "ct", CsTeam.CounterTerrorist)), - new ChatMenuOptionData(CS2_SimpleAdmin._localizer?["sa_team_t"] ?? "T", () => ForceTeam(admin, player, "t", CsTeam.Terrorist)), - new ChatMenuOptionData(CS2_SimpleAdmin._localizer?["sa_team_swap"] ?? "Swap", () => ForceTeam(admin, player, "swap", CsTeam.Spectator)), - new ChatMenuOptionData(CS2_SimpleAdmin._localizer?["sa_team_spec"] ?? "Spec", () => ForceTeam(admin, player, "spec", CsTeam.Spectator)), + new(CS2_SimpleAdmin._localizer?["sa_team_ct"] ?? "CT", () => ForceTeam(admin, player, "ct", CsTeam.CounterTerrorist)), + new(CS2_SimpleAdmin._localizer?["sa_team_t"] ?? "T", () => ForceTeam(admin, player, "t", CsTeam.Terrorist)), + new(CS2_SimpleAdmin._localizer?["sa_team_swap"] ?? "Swap", () => ForceTeam(admin, player, "swap", CsTeam.Spectator)), + new(CS2_SimpleAdmin._localizer?["sa_team_spec"] ?? "Spec", () => ForceTeam(admin, player, "spec", CsTeam.Spectator)), ]; foreach (var menuOptionData in options) diff --git a/CS2-SimpleAdmin/Menus/ManageServerMenu.cs b/CS2-SimpleAdmin/Menus/ManageServerMenu.cs index dd60a67..b8cf52a 100644 --- a/CS2-SimpleAdmin/Menus/ManageServerMenu.cs +++ b/CS2-SimpleAdmin/Menus/ManageServerMenu.cs @@ -24,7 +24,6 @@ public static class ManageServerMenu var menu = AdminMenu.CreateMenu(localizer?["sa_menu_server_manage"] ?? "Server Manage"); List options = []; - // permissions var hasMap = AdminManager.CommandIsOverriden("css_map") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_map")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/changemap"); var hasPlugins = AdminManager.CommandIsOverriden("css_pluginsmanager") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_pluginsmanager")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"); diff --git a/CS2-SimpleAdmin/Models/BanRecord.cs b/CS2-SimpleAdmin/Models/BanRecord.cs index 479a26a..026ffd6 100644 --- a/CS2-SimpleAdmin/Models/BanRecord.cs +++ b/CS2-SimpleAdmin/Models/BanRecord.cs @@ -1,18 +1,39 @@ +using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; namespace CS2_SimpleAdmin.Models; +public enum BanStatus +{ + [Description("ACTIVE")] ACTIVE, + [Description("UNBANNED")] UNBANNED, + [Description("EXPIRED")] EXPIRED, + [Description("")] UNKNOWN +} + public record BanRecord { [Column("id")] - public int Id { get; set; } + public int Id { get; init; } + + [Column("player_name")] + public string? PlayerName { get; set; } [Column("player_steamid")] - public string? PlayerSteamId { get; set; } + public ulong? PlayerSteamId { get; set; } [Column("player_ip")] public string? PlayerIp { get; set; } [Column("status")] - public string Status { get; set; } + public required string Status { get; init; } + + [NotMapped] + public BanStatus StatusEnum => Status.ToUpper() switch + { + "ACTIVE" => BanStatus.ACTIVE, + "UNBANNED" => BanStatus.UNBANNED, + "EXPIRED" => BanStatus.EXPIRED, + _ => BanStatus.UNKNOWN + }; } diff --git a/CS2-SimpleAdmin/Models/PlayerDto.cs b/CS2-SimpleAdmin/Models/PlayerDto.cs new file mode 100644 index 0000000..ffe052e --- /dev/null +++ b/CS2-SimpleAdmin/Models/PlayerDto.cs @@ -0,0 +1,12 @@ +namespace CS2_SimpleAdmin.Models; + +public record PlayerStats(int Score, int Kills, int Deaths, int MVPs); +public record PlayerDto( + int UserId, + string Name, + string SteamId, + string IpAddress, + uint Ping, + bool IsAdmin, + PlayerStats Stats +); diff --git a/CS2-SimpleAdmin/VERSION b/CS2-SimpleAdmin/VERSION index 58630bc..b4a9838 100644 --- a/CS2-SimpleAdmin/VERSION +++ b/CS2-SimpleAdmin/VERSION @@ -1 +1 @@ -1.7.7-alpha \ No newline at end of file +1.7.7-alpha-9 \ No newline at end of file diff --git a/CS2-SimpleAdmin/Variables.cs b/CS2-SimpleAdmin/Variables.cs index 39c53a3..a35cc75 100644 --- a/CS2-SimpleAdmin/Variables.cs +++ b/CS2-SimpleAdmin/Variables.cs @@ -7,7 +7,9 @@ using MenuManager; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; +using CS2_SimpleAdmin.Database; using CS2_SimpleAdmin.Managers; +using Timer = CounterStrikeSharp.API.Modules.Timers.Timer; namespace CS2_SimpleAdmin; @@ -15,12 +17,13 @@ public partial class CS2_SimpleAdmin { // Config public CS2_SimpleAdminConfig Config { get; set; } = new(); - + // HttpClient internal static readonly HttpClient HttpClient = new(); - + // Paths - internal static readonly string ConfigDirectory = Path.Combine(Application.RootDirectory, "configs/plugins/CS2-SimpleAdmin"); + internal static readonly string ConfigDirectory = + Path.Combine(Application.RootDirectory, "configs/plugins/CS2-SimpleAdmin"); // Localization public static IStringLocalizer? _localizer; @@ -40,7 +43,9 @@ public partial class CS2_SimpleAdmin private static readonly HashSet GodPlayers = []; internal static readonly HashSet SilentPlayers = []; internal static readonly Dictionary RenamedPlayers = []; - internal static readonly ConcurrentDictionary PlayersInfo = []; + internal static readonly ConcurrentDictionary PlayersInfo = []; + internal static readonly List CachedPlayers = []; + internal static readonly List BotPlayers = []; private static readonly List DisconnectedPlayers = []; // Discord Integration @@ -48,25 +53,35 @@ public partial class CS2_SimpleAdmin // Database Settings internal string DbConnectionString = string.Empty; - internal static Database.Database? Database; + // internal static Database.Database? Database; + internal static IDatabaseProvider? DatabaseProvider; // Logger internal static ILogger? _logger; // Memory Function (Game-related) - private static MemoryFunctionVoid? _cBasePlayerControllerSetPawnFunc; + private static MemoryFunctionVoid? + _cBasePlayerControllerSetPawnFunc; // Menu API and Capabilities internal static IMenuApi? MenuApi; private static readonly PluginCapability MenuCapability = new("menu:nfcore"); // Shared API - internal static Api.CS2_SimpleAdminApi? SimpleAdminApi { get; set; } - + internal static Api.CS2_SimpleAdminApi? SimpleAdminApi { get; private set; } + // Managers - internal PermissionManager PermissionManager = new(Database); - internal BanManager BanManager = new(Database); - internal MuteManager MuteManager = new(Database); - internal WarnManager WarnManager = new(Database); + internal PermissionManager PermissionManager = new(DatabaseProvider); + internal BanManager BanManager = new(DatabaseProvider); + internal MuteManager MuteManager = new(DatabaseProvider); + internal WarnManager WarnManager = new(DatabaseProvider); internal CacheManager? CacheManager = new(); + private static readonly PlayerManager PlayerManager = new(); + + // Timers + internal Timer? PlayersTimer = null; + + // Funny list + private readonly List _requiredPlugins = ["MenuManagerCore", "PlayerSettings"]; + private readonly List _requiredShared = ["MenuManagerApi", "PlayerSettingsApi", "AnyBaseLib", "CS2-SimpleAdminApi"]; } \ No newline at end of file diff --git a/CS2-SimpleAdmin/lang/ar.json b/CS2-SimpleAdmin/lang/ar.json index 9a35b90..4533510 100644 --- a/CS2-SimpleAdmin/lang/ar.json +++ b/CS2-SimpleAdmin/lang/ar.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} بدأ التصويت لـ {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}نتائج التصويت لـ {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}لقد قمت بكتم {default}جميع اللاعبين أثناء حديثك", + "sa_admin_voice_unmute_all": "{Lime}لقد ألغيت كتم {default}جميع اللاعبين", + "sa_admin_voice_listen_all": "{Default}أنت الآن تسمع {lime}جميع {default}اللاعبين", + "sa_admin_voice_unlisten_all": "{Default}أنت الآن لا تسمع {lime}جميع {default}اللاعبين", "sa_adminsay_prefix": "{RED}الإداري: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(إداري) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(لاعب) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/de.json b/CS2-SimpleAdmin/lang/de.json index a58b49f..f32db52 100644 --- a/CS2-SimpleAdmin/lang/de.json +++ b/CS2-SimpleAdmin/lang/de.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} hat eine Abstimmung für {lightred}{1}{default} gestartet", "sa_admin_vote_message_results": "{lime}ABSTIMMUNGSERGEBNISSE FÜR {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}Du hast {default}alle Spieler während deiner Ansprache stummgeschaltet", + "sa_admin_voice_unmute_all": "{Lime}Du hast {default}alle Spieler wieder aktiviert", + "sa_admin_voice_listen_all": "{Default}Du hörst jetzt {lime}alle {default}Spieler", + "sa_admin_voice_unlisten_all": "{Default}Du hörst jetzt {lime}nicht mehr {default}alle Spieler", "sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(SPIELER) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/en.json b/CS2-SimpleAdmin/lang/en.json index b3d87cf..a768d22 100644 --- a/CS2-SimpleAdmin/lang/en.json +++ b/CS2-SimpleAdmin/lang/en.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} started voting for {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}VOTING RESULTS FOR {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}You muted {default}all players during your speech", + "sa_admin_voice_unmute_all": "{Lime}You unmuted {default}all players", + "sa_admin_voice_listen_all": "{Default}You can now hear {lime}all {default}players", + "sa_admin_voice_unlisten_all": "{Default}You no longer hear {lime}all {default}players", "sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(PLAYER) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/es.json b/CS2-SimpleAdmin/lang/es.json index 525bd3d..e5b0edd 100644 --- a/CS2-SimpleAdmin/lang/es.json +++ b/CS2-SimpleAdmin/lang/es.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} inició una votación para {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}RESULTADOS DE LA VOTACIÓN PARA {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}Has silenciado a {default}todos los jugadores durante tu discurso", + "sa_admin_voice_unmute_all": "{Lime}Has reactivado el sonido de {default}todos los jugadores", + "sa_admin_voice_listen_all": "{Default}Ahora puedes escuchar a {lime}todos {default}los jugadores", + "sa_admin_voice_unlisten_all": "{Default}Ya no escuchas a {lime}todos {default}los jugadores", "sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(JUGADOR) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/fa.json b/CS2-SimpleAdmin/lang/fa.json index 20ba95a..77d8273 100644 --- a/CS2-SimpleAdmin/lang/fa.json +++ b/CS2-SimpleAdmin/lang/fa.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} رأی‌گیری برای {lightred}{1}{default} را شروع کرد", "sa_admin_vote_message_results": "{lime}نتایج رأی‌گیری برای {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}شما {default}همه بازیکنان را در طول سخنرانی بی‌صدا کردید", + "sa_admin_voice_unmute_all": "{Lime}شما {default}همه بازیکنان را از حالت بی‌صدا خارج کردید", + "sa_admin_voice_listen_all": "{Default}اکنون می‌توانید {lime}همه {default}بازیکنان را بشنوید", + "sa_admin_voice_unlisten_all": "{Default}شما دیگر {lime}همه {default}بازیکنان را نمی‌شنوید", "sa_adminsay_prefix": "{RED}ادمین: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ادمین) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(بازیکن) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/fr.json b/CS2-SimpleAdmin/lang/fr.json index 303ae38..27a4731 100644 --- a/CS2-SimpleAdmin/lang/fr.json +++ b/CS2-SimpleAdmin/lang/fr.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} a lancé un vote pour {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}RÉSULTATS DU VOTE POUR {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}Vous avez mis en sourdine {default}tous les joueurs pendant votre discours", + "sa_admin_voice_unmute_all": "{Lime}Vous avez réactivé le son de {default}tous les joueurs", + "sa_admin_voice_listen_all": "{Default}Vous entendez maintenant {lime}tous {default}les joueurs", + "sa_admin_voice_unlisten_all": "{Default}Vous n'entendez plus {lime}tous {default}les joueurs", "sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(JOUEUR) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/lv.json b/CS2-SimpleAdmin/lang/lv.json index 70f4678..e27aed8 100644 --- a/CS2-SimpleAdmin/lang/lv.json +++ b/CS2-SimpleAdmin/lang/lv.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} uzsāka balsošanu par {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}BALSOŠANAS REZULTĀTI PAR {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}Tu esi apklusinājis {default}visus spēlētājus runas laikā", + "sa_admin_voice_unmute_all": "{Lime}Tu atvienoji {default}visus spēlētājus", + "sa_admin_voice_listen_all": "{Default}Tu tagad dzirdi {lime}visus {default}spēlētājus", + "sa_admin_voice_unlisten_all": "{Default}Tu vairs nedzirdi {lime}visus {default}spēlētājus", "sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(SPĒLĒTĀJS) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/pl.json b/CS2-SimpleAdmin/lang/pl.json index c7f7bbf..3b6b3a7 100644 --- a/CS2-SimpleAdmin/lang/pl.json +++ b/CS2-SimpleAdmin/lang/pl.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} rozpoczął głosowanie na {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}WYNIKI GŁOSOWANIA {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}[{1}]", + "sa_admin_voice_mute_all": "{Lightred}Wyciszyłeś {default}wszystkich graczy na czas przemówienia", + "sa_admin_voice_unmute_all": "{Lime}Odciszyłeś {default}wszystkich graczy", + "sa_admin_voice_listen_all": "{Default}Słyszysz teraz {lime}wszystkich {default}graczy", + "sa_admin_voice_unlisten_all": "{Default}Nie słyszysz teraz {lime}wszystkich {default}graczy", "sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(GRACZ) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/pt-BR.json b/CS2-SimpleAdmin/lang/pt-BR.json index ed6d9ba..299ebc2 100644 --- a/CS2-SimpleAdmin/lang/pt-BR.json +++ b/CS2-SimpleAdmin/lang/pt-BR.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} iniciou uma votação para {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}RESULTADOS DA VOTAÇÃO PARA {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}Você silenciou {default}todos os jogadores durante o seu discurso", + "sa_admin_voice_unmute_all": "{Lime}Você reativou o som de {default}todos os jogadores", + "sa_admin_voice_listen_all": "{Default}Agora você pode ouvir {lime}todos {default}os jogadores", + "sa_admin_voice_unlisten_all": "{Default}Você não ouve mais {lime}todos {default}os jogadores", "sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(JOGADOR) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/pt-PT.json b/CS2-SimpleAdmin/lang/pt-PT.json index 3ec755c..94cce8c 100644 --- a/CS2-SimpleAdmin/lang/pt-PT.json +++ b/CS2-SimpleAdmin/lang/pt-PT.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} iniciou uma votação para {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}RESULTADOS DA VOTAÇÃO PARA {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}Silenciaste {default}todos os jogadores durante o teu discurso", + "sa_admin_voice_unmute_all": "{Lime}Ativaste novamente o som de {default}todos os jogadores", + "sa_admin_voice_listen_all": "{Default}Agora consegues ouvir {lime}todos {default}os jogadores", + "sa_admin_voice_unlisten_all": "{Default}Já não ouves {lime}todos {default}os jogadores", "sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(JOGADOR) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/ru.json b/CS2-SimpleAdmin/lang/ru.json index 7418422..f92fb5a 100644 --- a/CS2-SimpleAdmin/lang/ru.json +++ b/CS2-SimpleAdmin/lang/ru.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} начал голосование за {lightred}{1}{default}", "sa_admin_vote_message_results": "{lime}РЕЗУЛЬТАТЫ ГОЛОСОВАНИЯ ЗА {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}Вы заглушили {default}всех игроков во время своей речи", + "sa_admin_voice_unmute_all": "{Lime}Вы включили звук {default}всем игрокам", + "sa_admin_voice_listen_all": "{Default}Теперь вы слышите {lime}всех {default}игроков", + "sa_admin_voice_unlisten_all": "{Default}Вы больше не слышите {lime}всех {default}игроков", "sa_adminsay_prefix": "{RED}АДМИН: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(АДМИН) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(ИГРОК) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/tr.json b/CS2-SimpleAdmin/lang/tr.json index 08e7cb4..c520397 100644 --- a/CS2-SimpleAdmin/lang/tr.json +++ b/CS2-SimpleAdmin/lang/tr.json @@ -126,6 +126,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} {lightred}{1}{default} için oy kullanmaya başladı", "sa_admin_vote_message_results": "{lime}OY SONUÇLARI {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}Konuşman sırasında {default}tüm oyuncuları susturdun", + "sa_admin_voice_unmute_all": "{Lime}Tüm oyuncuların sesini {default}açtın", + "sa_admin_voice_listen_all": "{Default}Artık {lime}tüm {default}oyuncuları duyabiliyorsun", + "sa_admin_voice_unlisten_all": "{Default}Artık {lime}tüm {default}oyuncuları duymuyorsun", "sa_adminsay_prefix": "{RED}Yönetici: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(Yönetici) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(Oyuncu) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdmin/lang/zh-Hans.json b/CS2-SimpleAdmin/lang/zh-Hans.json index 53ac6d0..c3d2331 100644 --- a/CS2-SimpleAdmin/lang/zh-Hans.json +++ b/CS2-SimpleAdmin/lang/zh-Hans.json @@ -124,6 +124,10 @@ "sa_admin_vote_message": "{lightred}{0}{default} 发起了 [{lightred}{1}{default}] 的投票", "sa_admin_vote_message_results": "{lime}投票结果 {gold}{0}", "sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}", + "sa_admin_voice_mute_all": "{Lightred}你已在讲话期间静音了{default}所有玩家", + "sa_admin_voice_unmute_all": "{Lime}你已取消了{default}所有玩家的静音", + "sa_admin_voice_listen_all": "{Default}你现在可以听到{lime}所有{default}玩家了", + "sa_admin_voice_unlisten_all": "{Default}你现在不再听到{lime}所有{default}玩家了", "sa_adminsay_prefix": "{RED}管理员: {lightred}{0}{default}", "sa_adminchat_template_admin": "{LIME}(管理员) {lightred}{0}{default}: {lightred}{1}{default}", "sa_adminchat_template_player": "{SILVER}(玩家) {lightred}{0}{default}: {lightred}{1}{default}", diff --git a/CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj b/CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj index 97feaf5..d1d66f0 100644 --- a/CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj +++ b/CS2-SimpleAdminApi/CS2-SimpleAdminApi.csproj @@ -5,10 +5,11 @@ CS2_SimpleAdminApi enable enable + true - + diff --git a/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs b/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs index 32c0669..3d4c9f6 100644 --- a/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs +++ b/CS2-SimpleAdminApi/ICS2-SimpleAdminApi.cs @@ -9,23 +9,97 @@ public interface ICS2_SimpleAdminApi { public static readonly PluginCapability PluginCapability = new("simpleadmin:api"); + /// + /// Gets player information associated with the specified player controller. + /// + /// The player controller. + /// PlayerInfo object representing player data. public PlayerInfo GetPlayerInfo(CCSPlayerController player); + /// + /// Returns the database connection string used by the plugin. + /// public string GetConnectionString(); + + /// + /// Returns the configured server IP address with port. + /// public string GetServerAddress(); + + /// + /// Returns the internal server ID assigned in the plugin's database. + /// public int? GetServerId(); + /// + /// Returns mute-related penalties for the specified player. + /// + /// The player controller. + /// A dictionary mapping penalty types to lists of penalties with end date, duration, and pass state. public Dictionary> GetPlayerMuteStatus(CCSPlayerController player); + /// + /// Event fired when a player receives a penalty. + /// public event Action? OnPlayerPenaltied; + + /// + /// Event fired when a penalty is added to a player by SteamID. + /// public event Action? OnPlayerPenaltiedAdded; + + /// + /// Event to show admin activity messages. + /// public event Action? OnAdminShowActivity; + /// + /// Event fired when an admin toggles silent mode. + /// + public event Action? OnAdminToggleSilent; + + /// + /// Issues a penalty to a player controller with specified type, reason, and optional duration. + /// public void IssuePenalty(CCSPlayerController player, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1); + + /// + /// Issues a penalty to a player identified by SteamID with specified type, reason, and optional duration. + /// public void IssuePenalty(SteamID steamid, CCSPlayerController? admin, PenaltyType penaltyType, string reason, int duration = -1); + + /// + /// Logs a command invoked by a caller with the command string. + /// public void LogCommand(CCSPlayerController? caller, string command); + + /// + /// Logs a command invoked by a caller with the command info object. + /// public void LogCommand(CCSPlayerController? caller, CommandInfo command); + + /// + /// Shows an admin activity message, optionally suppressing broadcasting. + /// public void ShowAdminActivity(string messageKey, string? callerName = null, bool dontPublish = false, params object[] messageArgs); + /// + /// Returns true if the specified admin player is in silent mode (not broadcasting activity). + /// public bool IsAdminSilent(CCSPlayerController player); + + /// + /// Returns a set of player slots representing admins currently in silent mode. + /// + public HashSet ListSilentAdminsSlots(); + + /// + /// Registers a new command with the specified name, description, and callback. + /// + public void RegisterCommand(string name, string? description, CommandInfo.CommandCallback callback); + + /// + /// Unregisters an existing command by its name. + /// + public void UnRegisterCommand(string name); } \ No newline at end of file diff --git a/CS2-SimpleAdminApi/PlayerInfo.cs b/CS2-SimpleAdminApi/PlayerInfo.cs index b492322..b331def 100644 --- a/CS2-SimpleAdminApi/PlayerInfo.cs +++ b/CS2-SimpleAdminApi/PlayerInfo.cs @@ -1,5 +1,5 @@ +using System.Numerics; using CounterStrikeSharp.API.Modules.Entities; -using CounterStrikeSharp.API.Modules.Utils; namespace CS2_SimpleAdminApi; @@ -28,10 +28,13 @@ public class PlayerInfo( public bool WaitingForKick { get; set; } = false; public List<(ulong SteamId, string PlayerName)> AccountsAssociated { get; set; } = []; public DiePosition? DiePosition { get; set; } + public bool IsLoaded { get; set; } } -public struct DiePosition(Vector position, QAngle angle) +public class DiePosition(Vector3 position, Vector3 angle) { - public Vector Position { get; set; } = position; - public QAngle Angle { get; set; } = angle; + public Vector3 Position { get; } = position; + public Vector3 Angle { get; } = angle; } + + diff --git a/Modules/AntiDLL-CS2-SimpleAdmin/CS2-SimpleAdminApi.dll b/Modules/AntiDLL-CS2-SimpleAdmin/CS2-SimpleAdminApi.dll index 20c9407..0a8f33a 100644 Binary files a/Modules/AntiDLL-CS2-SimpleAdmin/CS2-SimpleAdminApi.dll and b/Modules/AntiDLL-CS2-SimpleAdmin/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdminApi.dll new file mode 100644 index 0000000..ac69984 Binary files /dev/null and b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.cs b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.cs new file mode 100644 index 0000000..9379f33 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.cs @@ -0,0 +1,51 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Capabilities; +using CounterStrikeSharp.API.Modules.Utils; +using CS2_SimpleAdminApi; +using Microsoft.Extensions.Logging; + +namespace CS2_SimpleAdmin_BanSoundModule; + +public class CS2_SimpleAdmin_BanSoundModule: BasePlugin +{ + public override string ModuleName => "[CS2-SimpleAdmin] BanSound Module"; + public override string ModuleVersion => "v1.0.0"; + public override string ModuleAuthor => "daffyy"; + + private static ICS2_SimpleAdminApi? _sharedApi; + private readonly PluginCapability _pluginCapability = new("simpleadmin:api"); + + public override void OnAllPluginsLoaded(bool hotReload) + { + _sharedApi = _pluginCapability.Get(); + if (_sharedApi == null) + { + Logger.LogError("CS2-SimpleAdmin SharedApi not found"); + Unload(false); + return; + } + + RegisterListener(OnServerPrecacheResources); + + _sharedApi.OnPlayerPenaltied += OnPlayerPenaltied; + } + + private void OnServerPrecacheResources(ResourceManifest manifest) + { + manifest.AddResource("soundevents/soundevents_addon.vsndevts"); + } + + private void OnPlayerPenaltied(PlayerInfo playerInfo, PlayerInfo? admin, PenaltyType penaltyType, + string reason, int duration, int? penaltyId, int? serverId) + { + if (penaltyType != PenaltyType.Ban || admin == null) + return; + + foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsBot)) + { + var filter = new RecipientFilter(player); + player?.EmitSound("bansound", volume: 0.9f, recipients: filter); + } + } +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.csproj b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.csproj new file mode 100644 index 0000000..353bd6a --- /dev/null +++ b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + CS2_SimpleAdmin_ExampleModule + enable + enable + + + + + + + + + CS2-SimpleAdminApi.dll + + + + diff --git a/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.sln b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.sln new file mode 100644 index 0000000..926dc2f --- /dev/null +++ b/Modules/CS2-SimpleAdmin_BanSoundModule/CS2-SimpleAdmin_BanSoundModule.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CS2-SimpleAdmin_BanSoundModule", "CS2-SimpleAdmin_BanSoundModule.csproj", "{D940F3E9-0E3F-467A-B336-149E3A624FB6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D940F3E9-0E3F-467A-B336-149E3A624FB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D940F3E9-0E3F-467A-B336-149E3A624FB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D940F3E9-0E3F-467A-B336-149E3A624FB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D940F3E9-0E3F-467A-B336-149E3A624FB6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Modules/CS2-SimpleAdmin_CleanModule/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_CleanModule/CS2-SimpleAdminApi.dll index 20c9407..0a8f33a 100644 Binary files a/Modules/CS2-SimpleAdmin_CleanModule/CS2-SimpleAdminApi.dll and b/Modules/CS2-SimpleAdmin_CleanModule/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll index 20c9407..0a8f33a 100644 Binary files a/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll and b/Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdminApi.dll index 20c9407..0a8f33a 100644 Binary files a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdminApi.dll and b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdminApi.dll deleted file mode 100644 index ad16a31..0000000 Binary files a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdminApi.dll and /dev/null differ diff --git a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.deps.json b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.deps.json deleted file mode 100644 index 9c1d661..0000000 --- a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.deps.json +++ /dev/null @@ -1,939 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETCoreApp,Version=v8.0", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETCoreApp,Version=v8.0": { - "CS2-SimpleAdmin_RedisInform/1.0.0": { - "dependencies": { - "CounterStrikeSharp.API": "1.0.305", - "StackExchange.Redis": "2.8.24", - "CS2-SimpleAdminApi": "1.0.0.0" - }, - "runtime": { - "CS2-SimpleAdmin_RedisInform.dll": {} - } - }, - "CounterStrikeSharp.API/1.0.305": { - "dependencies": { - "McMaster.NETCore.Plugins": "1.4.0", - "Microsoft.CSharp": "4.7.0", - "Microsoft.DotNet.ApiCompat.Task": "8.0.203", - "Microsoft.Extensions.Hosting": "8.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", - "Microsoft.Extensions.Localization.Abstractions": "8.0.3", - "Microsoft.Extensions.Logging": "8.0.0", - "Scrutor": "4.2.2", - "Serilog.Extensions.Logging": "8.0.0", - "Serilog.Sinks.Console": "5.0.0", - "Serilog.Sinks.File": "5.0.0", - "System.Data.DataSetExtensions": "4.5.0" - }, - "runtime": { - "lib/net8.0/CounterStrikeSharp.API.dll": { - "assemblyVersion": "1.0.305.0", - "fileVersion": "1.0.305.0" - } - } - }, - "McMaster.NETCore.Plugins/1.4.0": { - "dependencies": { - "Microsoft.DotNet.PlatformAbstractions": "3.1.6", - "Microsoft.Extensions.DependencyModel": "6.0.0" - }, - "runtime": { - "lib/netcoreapp3.1/McMaster.NETCore.Plugins.dll": { - "assemblyVersion": "1.4.0.0", - "fileVersion": "1.4.0.0" - } - } - }, - "Microsoft.CSharp/4.7.0": {}, - "Microsoft.DotNet.ApiCompat.Task/8.0.203": {}, - "Microsoft.DotNet.PlatformAbstractions/3.1.6": { - "runtime": { - "lib/netstandard2.0/Microsoft.DotNet.PlatformAbstractions.dll": { - "assemblyVersion": "3.1.6.0", - "fileVersion": "3.100.620.31604" - } - } - }, - "Microsoft.Extensions.Configuration/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Configuration.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Configuration.Abstractions/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Configuration.Abstractions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Configuration.Binder/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Configuration.Binder.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Configuration.CommandLine/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Configuration.CommandLine.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Configuration.EnvironmentVariables/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Configuration.EnvironmentVariables.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Configuration.FileExtensions/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Physical": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Configuration.FileExtensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Configuration.Json/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "System.Text.Json": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Configuration.Json.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Configuration.UserSecrets/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Json": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Physical": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Configuration.UserSecrets.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.DependencyInjection/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.DependencyInjection.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": { - "runtime": { - "lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.DependencyModel/6.0.0": { - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "8.0.0", - "System.Text.Json": "8.0.0" - }, - "runtime": { - "lib/netstandard2.0/Microsoft.Extensions.DependencyModel.dll": { - "assemblyVersion": "6.0.0.0", - "fileVersion": "6.0.21.52210" - } - } - }, - "Microsoft.Extensions.Diagnostics/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Diagnostics.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Diagnostics.DiagnosticSource": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Diagnostics.Abstractions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.FileProviders.Abstractions/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.FileProviders.Abstractions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.FileProviders.Physical/8.0.0": { - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.FileSystemGlobbing": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.FileProviders.Physical.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.FileSystemGlobbing/8.0.0": { - "runtime": { - "lib/net8.0/Microsoft.Extensions.FileSystemGlobbing.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Hosting/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", - "Microsoft.Extensions.Configuration.Json": "8.0.0", - "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Physical": "8.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Configuration": "8.0.0", - "Microsoft.Extensions.Logging.Console": "8.0.0", - "Microsoft.Extensions.Logging.Debug": "8.0.0", - "Microsoft.Extensions.Logging.EventLog": "8.0.0", - "Microsoft.Extensions.Logging.EventSource": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Hosting.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Hosting.Abstractions/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Hosting.Abstractions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Localization.Abstractions/8.0.3": { - "runtime": { - "lib/net8.0/Microsoft.Extensions.Localization.Abstractions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.324.11615" - } - } - }, - "Microsoft.Extensions.Logging/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Logging.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Logging.Abstractions/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Logging.Configuration/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Logging.Configuration.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Logging.Console/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Configuration": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Text.Json": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Logging.Console.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Logging.Debug/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Logging.Debug.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Logging.EventLog/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Diagnostics.EventLog": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Logging.EventLog.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Logging.EventSource/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0", - "System.Text.Json": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Logging.EventSource.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Options/8.0.0": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Options.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - }, - "runtime": { - "lib/net8.0/Microsoft.Extensions.Options.ConfigurationExtensions.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Microsoft.Extensions.Primitives/8.0.0": { - "runtime": { - "lib/net8.0/Microsoft.Extensions.Primitives.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "Pipelines.Sockets.Unofficial/2.2.8": { - "dependencies": { - "System.IO.Pipelines": "5.0.1" - }, - "runtime": { - "lib/net5.0/Pipelines.Sockets.Unofficial.dll": { - "assemblyVersion": "1.0.0.0", - "fileVersion": "2.2.8.1080" - } - } - }, - "Scrutor/4.2.2": { - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyModel": "6.0.0" - }, - "runtime": { - "lib/net6.0/Scrutor.dll": { - "assemblyVersion": "4.0.0.0", - "fileVersion": "4.0.0.0" - } - } - }, - "Serilog/3.1.1": { - "runtime": { - "lib/net7.0/Serilog.dll": { - "assemblyVersion": "2.0.0.0", - "fileVersion": "3.1.1.0" - } - } - }, - "Serilog.Extensions.Logging/8.0.0": { - "dependencies": { - "Microsoft.Extensions.Logging": "8.0.0", - "Serilog": "3.1.1" - }, - "runtime": { - "lib/net8.0/Serilog.Extensions.Logging.dll": { - "assemblyVersion": "7.0.0.0", - "fileVersion": "8.0.0.0" - } - } - }, - "Serilog.Sinks.Console/5.0.0": { - "dependencies": { - "Serilog": "3.1.1" - }, - "runtime": { - "lib/net7.0/Serilog.Sinks.Console.dll": { - "assemblyVersion": "5.0.0.0", - "fileVersion": "5.0.0.0" - } - } - }, - "Serilog.Sinks.File/5.0.0": { - "dependencies": { - "Serilog": "3.1.1" - }, - "runtime": { - "lib/net5.0/Serilog.Sinks.File.dll": { - "assemblyVersion": "5.0.0.0", - "fileVersion": "5.0.0.0" - } - } - }, - "StackExchange.Redis/2.8.24": { - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Pipelines.Sockets.Unofficial": "2.2.8" - }, - "runtime": { - "lib/net8.0/StackExchange.Redis.dll": { - "assemblyVersion": "2.0.0.0", - "fileVersion": "2.8.24.3255" - } - } - }, - "System.Buffers/4.5.1": {}, - "System.Data.DataSetExtensions/4.5.0": {}, - "System.Diagnostics.DiagnosticSource/8.0.0": {}, - "System.Diagnostics.EventLog/8.0.0": { - "runtime": { - "lib/net8.0/System.Diagnostics.EventLog.dll": { - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - }, - "runtimeTargets": { - "runtimes/win/lib/net8.0/System.Diagnostics.EventLog.Messages.dll": { - "rid": "win", - "assetType": "runtime", - "assemblyVersion": "8.0.0.0", - "fileVersion": "0.0.0.0" - }, - "runtimes/win/lib/net8.0/System.Diagnostics.EventLog.dll": { - "rid": "win", - "assetType": "runtime", - "assemblyVersion": "8.0.0.0", - "fileVersion": "8.0.23.53103" - } - } - }, - "System.IO.Pipelines/5.0.1": { - "runtime": { - "lib/netcoreapp3.0/System.IO.Pipelines.dll": { - "assemblyVersion": "5.0.0.1", - "fileVersion": "5.0.120.57516" - } - } - }, - "System.Memory/4.5.4": {}, - "System.Runtime.CompilerServices.Unsafe/6.0.0": {}, - "System.Text.Encodings.Web/8.0.0": {}, - "System.Text.Json/8.0.0": { - "dependencies": { - "System.Text.Encodings.Web": "8.0.0" - } - }, - "CS2-SimpleAdminApi/1.0.0.0": { - "runtime": { - "CS2-SimpleAdminApi.dll": { - "assemblyVersion": "1.0.0.0", - "fileVersion": "1.0.0.0" - } - } - } - } - }, - "libraries": { - "CS2-SimpleAdmin_RedisInform/1.0.0": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "CounterStrikeSharp.API/1.0.305": { - "type": "package", - "serviceable": true, - "sha512": "sha512-WoeI/sQ85HM2UG0ADvtfm7JaWNSETPn4gwTvKTKdX7uRoNPavVuemW1jB7dCKQQMW/7So96FVL0qbqYhE91Jpw==", - "path": "counterstrikesharp.api/1.0.305", - "hashPath": "counterstrikesharp.api.1.0.305.nupkg.sha512" - }, - "McMaster.NETCore.Plugins/1.4.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-UKw5Z2/QHhkR7kiAJmqdCwVDMQV0lwsfj10+FG676r8DsJWIpxtachtEjE0qBs9WoK5GUQIqxgyFeYUSwuPszg==", - "path": "mcmaster.netcore.plugins/1.4.0", - "hashPath": "mcmaster.netcore.plugins.1.4.0.nupkg.sha512" - }, - "Microsoft.CSharp/4.7.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==", - "path": "microsoft.csharp/4.7.0", - "hashPath": "microsoft.csharp.4.7.0.nupkg.sha512" - }, - "Microsoft.DotNet.ApiCompat.Task/8.0.203": { - "type": "package", - "serviceable": true, - "sha512": "sha512-nPEGMojf1mj1oVixe0aiBimSn6xUoZswSjpMPZFMkZ+znYm2GEM5tWGZEWb6OSNIo5gWKyDi1WcI4IL7YiL1Zw==", - "path": "microsoft.dotnet.apicompat.task/8.0.203", - "hashPath": "microsoft.dotnet.apicompat.task.8.0.203.nupkg.sha512" - }, - "Microsoft.DotNet.PlatformAbstractions/3.1.6": { - "type": "package", - "serviceable": true, - "sha512": "sha512-jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==", - "path": "microsoft.dotnet.platformabstractions/3.1.6", - "hashPath": "microsoft.dotnet.platformabstractions.3.1.6.nupkg.sha512" - }, - "Microsoft.Extensions.Configuration/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", - "path": "microsoft.extensions.configuration/8.0.0", - "hashPath": "microsoft.extensions.configuration.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Configuration.Abstractions/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", - "path": "microsoft.extensions.configuration.abstractions/8.0.0", - "hashPath": "microsoft.extensions.configuration.abstractions.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Configuration.Binder/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==", - "path": "microsoft.extensions.configuration.binder/8.0.0", - "hashPath": "microsoft.extensions.configuration.binder.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Configuration.CommandLine/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", - "path": "microsoft.extensions.configuration.commandline/8.0.0", - "hashPath": "microsoft.extensions.configuration.commandline.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Configuration.EnvironmentVariables/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==", - "path": "microsoft.extensions.configuration.environmentvariables/8.0.0", - "hashPath": "microsoft.extensions.configuration.environmentvariables.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Configuration.FileExtensions/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==", - "path": "microsoft.extensions.configuration.fileextensions/8.0.0", - "hashPath": "microsoft.extensions.configuration.fileextensions.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Configuration.Json/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==", - "path": "microsoft.extensions.configuration.json/8.0.0", - "hashPath": "microsoft.extensions.configuration.json.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Configuration.UserSecrets/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", - "path": "microsoft.extensions.configuration.usersecrets/8.0.0", - "hashPath": "microsoft.extensions.configuration.usersecrets.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.DependencyInjection/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", - "path": "microsoft.extensions.dependencyinjection/8.0.0", - "hashPath": "microsoft.extensions.dependencyinjection.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==", - "path": "microsoft.extensions.dependencyinjection.abstractions/8.0.0", - "hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.DependencyModel/6.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", - "path": "microsoft.extensions.dependencymodel/6.0.0", - "hashPath": "microsoft.extensions.dependencymodel.6.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Diagnostics/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", - "path": "microsoft.extensions.diagnostics/8.0.0", - "hashPath": "microsoft.extensions.diagnostics.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Diagnostics.Abstractions/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", - "path": "microsoft.extensions.diagnostics.abstractions/8.0.0", - "hashPath": "microsoft.extensions.diagnostics.abstractions.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.FileProviders.Abstractions/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", - "path": "microsoft.extensions.fileproviders.abstractions/8.0.0", - "hashPath": "microsoft.extensions.fileproviders.abstractions.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.FileProviders.Physical/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==", - "path": "microsoft.extensions.fileproviders.physical/8.0.0", - "hashPath": "microsoft.extensions.fileproviders.physical.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.FileSystemGlobbing/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==", - "path": "microsoft.extensions.filesystemglobbing/8.0.0", - "hashPath": "microsoft.extensions.filesystemglobbing.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Hosting/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", - "path": "microsoft.extensions.hosting/8.0.0", - "hashPath": "microsoft.extensions.hosting.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Hosting.Abstractions/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", - "path": "microsoft.extensions.hosting.abstractions/8.0.0", - "hashPath": "microsoft.extensions.hosting.abstractions.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Localization.Abstractions/8.0.3": { - "type": "package", - "serviceable": true, - "sha512": "sha512-k/kUPm1FQBxcs9/vsM1eF4qIOg2Sovqh/+KUGHur5Mc0Y3OFGuoz9ktBX7LA0gPz53SZhW3W3oaSaMFFcjgM6Q==", - "path": "microsoft.extensions.localization.abstractions/8.0.3", - "hashPath": "microsoft.extensions.localization.abstractions.8.0.3.nupkg.sha512" - }, - "Microsoft.Extensions.Logging/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", - "path": "microsoft.extensions.logging/8.0.0", - "hashPath": "microsoft.extensions.logging.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Logging.Abstractions/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", - "path": "microsoft.extensions.logging.abstractions/8.0.0", - "hashPath": "microsoft.extensions.logging.abstractions.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Logging.Configuration/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==", - "path": "microsoft.extensions.logging.configuration/8.0.0", - "hashPath": "microsoft.extensions.logging.configuration.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Logging.Console/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==", - "path": "microsoft.extensions.logging.console/8.0.0", - "hashPath": "microsoft.extensions.logging.console.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Logging.Debug/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==", - "path": "microsoft.extensions.logging.debug/8.0.0", - "hashPath": "microsoft.extensions.logging.debug.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Logging.EventLog/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", - "path": "microsoft.extensions.logging.eventlog/8.0.0", - "hashPath": "microsoft.extensions.logging.eventlog.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Logging.EventSource/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", - "path": "microsoft.extensions.logging.eventsource/8.0.0", - "hashPath": "microsoft.extensions.logging.eventsource.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Options/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", - "path": "microsoft.extensions.options/8.0.0", - "hashPath": "microsoft.extensions.options.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Options.ConfigurationExtensions/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", - "path": "microsoft.extensions.options.configurationextensions/8.0.0", - "hashPath": "microsoft.extensions.options.configurationextensions.8.0.0.nupkg.sha512" - }, - "Microsoft.Extensions.Primitives/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==", - "path": "microsoft.extensions.primitives/8.0.0", - "hashPath": "microsoft.extensions.primitives.8.0.0.nupkg.sha512" - }, - "Pipelines.Sockets.Unofficial/2.2.8": { - "type": "package", - "serviceable": true, - "sha512": "sha512-zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", - "path": "pipelines.sockets.unofficial/2.2.8", - "hashPath": "pipelines.sockets.unofficial.2.2.8.nupkg.sha512" - }, - "Scrutor/4.2.2": { - "type": "package", - "serviceable": true, - "sha512": "sha512-t5VIYA7WJXoJJo7s4DoHakMGwTu+MeEnZumMOhTCH7kz9xWha24G7dJNxWrHPlu0ZdZAS4jDZCxxAnyaBh7uYw==", - "path": "scrutor/4.2.2", - "hashPath": "scrutor.4.2.2.nupkg.sha512" - }, - "Serilog/3.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-P6G4/4Kt9bT635bhuwdXlJ2SCqqn2nhh4gqFqQueCOr9bK/e7W9ll/IoX1Ter948cV2Z/5+5v8pAfJYUISY03A==", - "path": "serilog/3.1.1", - "hashPath": "serilog.3.1.1.nupkg.sha512" - }, - "Serilog.Extensions.Logging/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-YEAMWu1UnWgf1c1KP85l1SgXGfiVo0Rz6x08pCiPOIBt2Qe18tcZLvdBUuV5o1QHvrs8FAry9wTIhgBRtjIlEg==", - "path": "serilog.extensions.logging/8.0.0", - "hashPath": "serilog.extensions.logging.8.0.0.nupkg.sha512" - }, - "Serilog.Sinks.Console/5.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-IZ6bn79k+3SRXOBpwSOClUHikSkp2toGPCZ0teUkscv4dpDg9E2R2xVsNkLmwddE4OpNVO3N0xiYsAH556vN8Q==", - "path": "serilog.sinks.console/5.0.0", - "hashPath": "serilog.sinks.console.5.0.0.nupkg.sha512" - }, - "Serilog.Sinks.File/5.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", - "path": "serilog.sinks.file/5.0.0", - "hashPath": "serilog.sinks.file.5.0.0.nupkg.sha512" - }, - "StackExchange.Redis/2.8.24": { - "type": "package", - "serviceable": true, - "sha512": "sha512-GWllmsFAtLyhm4C47cOCipGxyEi1NQWTFUHXnJ8hiHOsK/bH3T5eLkWPVW+LRL6jDiB3g3izW3YEHgLuPoJSyA==", - "path": "stackexchange.redis/2.8.24", - "hashPath": "stackexchange.redis.2.8.24.nupkg.sha512" - }, - "System.Buffers/4.5.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==", - "path": "system.buffers/4.5.1", - "hashPath": "system.buffers.4.5.1.nupkg.sha512" - }, - "System.Data.DataSetExtensions/4.5.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-221clPs1445HkTBZPL+K9sDBdJRB8UN8rgjO3ztB0CQ26z//fmJXtlsr6whGatscsKGBrhJl5bwJuKSA8mwFOw==", - "path": "system.data.datasetextensions/4.5.0", - "hashPath": "system.data.datasetextensions.4.5.0.nupkg.sha512" - }, - "System.Diagnostics.DiagnosticSource/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==", - "path": "system.diagnostics.diagnosticsource/8.0.0", - "hashPath": "system.diagnostics.diagnosticsource.8.0.0.nupkg.sha512" - }, - "System.Diagnostics.EventLog/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==", - "path": "system.diagnostics.eventlog/8.0.0", - "hashPath": "system.diagnostics.eventlog.8.0.0.nupkg.sha512" - }, - "System.IO.Pipelines/5.0.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==", - "path": "system.io.pipelines/5.0.1", - "hashPath": "system.io.pipelines.5.0.1.nupkg.sha512" - }, - "System.Memory/4.5.4": { - "type": "package", - "serviceable": true, - "sha512": "sha512-1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", - "path": "system.memory/4.5.4", - "hashPath": "system.memory.4.5.4.nupkg.sha512" - }, - "System.Runtime.CompilerServices.Unsafe/6.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==", - "path": "system.runtime.compilerservices.unsafe/6.0.0", - "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512" - }, - "System.Text.Encodings.Web/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", - "path": "system.text.encodings.web/8.0.0", - "hashPath": "system.text.encodings.web.8.0.0.nupkg.sha512" - }, - "System.Text.Json/8.0.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==", - "path": "system.text.json/8.0.0", - "hashPath": "system.text.json.8.0.0.nupkg.sha512" - }, - "CS2-SimpleAdminApi/1.0.0.0": { - "type": "reference", - "serviceable": false, - "sha512": "" - } - } -} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.dll b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.dll deleted file mode 100644 index aaa1acc..0000000 Binary files a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.dll and /dev/null differ diff --git a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.pdb b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.pdb deleted file mode 100644 index 488e323..0000000 Binary files a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform.pdb and /dev/null differ diff --git a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/Pipelines.Sockets.Unofficial.dll b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/Pipelines.Sockets.Unofficial.dll deleted file mode 100644 index c5b223d..0000000 Binary files a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/Pipelines.Sockets.Unofficial.dll and /dev/null differ diff --git a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/StackExchange.Redis.dll b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/StackExchange.Redis.dll deleted file mode 100644 index dea5559..0000000 Binary files a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/StackExchange.Redis.dll and /dev/null differ diff --git a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/System.IO.Pipelines.dll b/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/System.IO.Pipelines.dll deleted file mode 100644 index d2ab03f..0000000 Binary files a/Modules/CS2-SimpleAdmin_RedisInform/CS2-SimpleAdmin_RedisInform/System.IO.Pipelines.dll and /dev/null differ diff --git a/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdminApi.dll b/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdminApi.dll new file mode 100644 index 0000000..35aa7b7 Binary files /dev/null and b/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdminApi.dll differ diff --git a/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.cs b/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.cs new file mode 100644 index 0000000..a58457a --- /dev/null +++ b/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.cs @@ -0,0 +1,219 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Attributes.Registration; +using CounterStrikeSharp.API.Core.Capabilities; +using CounterStrikeSharp.API.Modules.Admin; +using CounterStrikeSharp.API.Modules.Entities; +using CounterStrikeSharp.API.Modules.Utils; +using CS2_SimpleAdminApi; +using Microsoft.Extensions.Logging; + +namespace CS2_SimpleAdmin_StealthModule; + +public class CS2_SimpleAdmin_StealthModule: BasePlugin, IPluginConfig +{ + public override string ModuleName => "[CS2-SimpleAdmin] Stealth Module"; + public override string ModuleVersion => "v1.0.2"; + public override string ModuleAuthor => "daffyy"; + + private static ICS2_SimpleAdminApi? _sharedApi; + private readonly PluginCapability _pluginCapability = new("simpleadmin:api"); + + internal static readonly HashSet Players = []; + // private readonly HashSet _admins = []; + // private readonly HashSet _spectatedPlayers = []; + + public PluginConfig Config { get; set; } + + public override void Load(bool hotReload) + { + RegisterListener(OnCheckTransmit); + + // Old method + // if (Config.BlockStatusCommand) + // RegisterListener(OnServerPostEntityThink); + + if (hotReload) + { + Players.Clear(); + var players = Utilities.GetPlayers().Where(p => p.IsValid && !p.IsBot).ToList(); + + foreach (var player in players) + { + var steamId = new SteamID(player.SteamID); + // if (Config.Permissions.Any(permission => AdminManager.PlayerHasPermissions(steamId, permission))) + // _admins.Add(player.Slot); + + Players.Add(player); + } + } + + // Old method + // AddTimer(3, RefreshSpectatedPlayers, TimerFlags.REPEAT); + } + + // Old method + // private void RefreshSpectatedPlayers() + // { + // _spectatedPlayers.Clear(); + // + // foreach (var admin in _admins) + // { + // var observer = admin.GetSpectatingPlayer(); + // if (observer != null) + // _spectatedPlayers.Add(observer); + // } + // } + + public void OnConfigParsed(PluginConfig config) + { + Config = config; + } + + public override void OnAllPluginsLoaded(bool hotReload) + { + try + { + _sharedApi = _pluginCapability.Get(); + if (_sharedApi == null) throw new NullReferenceException("_sharedApi is null"); + + if (Config.BlockStatusCommand) + _sharedApi.OnAdminToggleSilent += OnAdminToggleSilent; + } + catch (Exception) + { + Logger.LogError("CS2-SimpleAdmin SharedApi not found"); + Unload(false); + } + } + + private void OnAdminToggleSilent(int slot, bool status) + { + Server.ExecuteCommand(status ? $"mm_excludeslot {slot}" : $"mm_removeexcludeslot {slot}"); + } + + // Old method + // private void OnServerPostEntityThink() + // { + // if (Players.Count <= 2 || _sharedApi is not null && _sharedApi.ListSilentAdminsSlots().Count == 0) return; + // foreach (var player in _spectatedPlayers) + // { + // if (player?.IsValid != true || player.IsBot) continue; + // player.PrintToConsole(" "); + // } + // } + + private void OnCheckTransmit(CCheckTransmitInfoList infolist) + { + if (Players.Count <= 2 || _sharedApi is not null && _sharedApi.ListSilentAdminsSlots().Count == 0) return; + + var validObserverPawns = Players + .Select(p => new { p.Slot, ObserverPawn = p.ObserverPawn.Value }) + .Where(p => p.ObserverPawn?.IsValid == true) // safe check + .ToArray(); + + foreach (var (info, player) in infolist) + { + if (player == null || player.IsHLTV) + continue; + + var entities = info.TransmitEntities; + foreach (var target in validObserverPawns) + { + if (target.Slot == player.Slot) + continue; + + var observer = target.ObserverPawn; + if (observer == null) continue; // extra safety + + if (entities.Contains((int)observer.Index)) + entities.Remove((int)observer.Index); + } + } + } + + [GameEventHandler(HookMode.Pre)] + public HookResult EventPlayerTeam(EventPlayerTeam @event, GameEventInfo info) + { + if (ShouldSuppressBroadcast(@event.Userid)) + info.DontBroadcast = true; + + return HookResult.Continue; + } + + [GameEventHandler(HookMode.Pre)] + public HookResult EventPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info) + { + var player = @event.Userid; + if (player?.IsValid != true || player.IsBot) return HookResult.Continue; + + Players.Add(player); + + var steamId = new SteamID(player.SteamID); + if (!Config.Permissions.Any(permission => AdminManager.PlayerHasPermissions(steamId, permission))) + return HookResult.Continue; + + // _admins.Add(player.Slot); + + if (!Config.HideAdminsOnJoin) return HookResult.Continue; + + AddTimer(0.75f, () => + { + player.ChangeTeam(CsTeam.Spectator); + }); + + AddTimer(1.25f, () => + { + player.ExecuteClientCommandFromServer("css_hide"); + }); + + return HookResult.Continue; + } + + [GameEventHandler(HookMode.Pre)] + public HookResult EventPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info) + { + var player = @event.Userid; + if (player?.IsValid != true || player.IsBot) return HookResult.Continue; + + if (Config.BlockStatusCommand && _sharedApi != null && _sharedApi.IsAdminSilent(player)) + Server.ExecuteCommand($"mm_removeexcludeslot {player.Slot}"); + + Players.Remove(player); + // _admins.Remove(player.Slot); + + if (ShouldSuppressBroadcast(@event.Userid)) + info.DontBroadcast = true; + + return HookResult.Continue; + } + + [GameEventHandler(HookMode.Pre)] + public HookResult EventPlayerDeath(EventPlayerDeath @event, GameEventInfo info) + { + if (ShouldSuppressBroadcast(@event.Userid, true)) + info.DontBroadcast = true; + + return HookResult.Continue; + } + + private bool ShouldSuppressBroadcast(CCSPlayerController? player, bool checkTeam = false) + { + if (player?.IsValid != true || player.IsBot) + return false; + + if (_sharedApi is not null && _sharedApi.IsAdminSilent(player)) + { + return true; + } + + if (checkTeam && player.TeamNum > 1) + return false; + + var steamId = new SteamID(player.SteamID); + return Config.Permissions.Any(permission => AdminManager.PlayerHasPermissions(steamId, permission)); + } + + // private IEnumerable GetNonAdmins() + // => Players.Where(p => !_admins.Contains(p)); +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.csproj b/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.csproj new file mode 100644 index 0000000..a2c6c74 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + CS2_SimpleAdmin_ExampleModule + enable + enable + + + + + + + + + CS2-SimpleAdminApi.dll + + + + diff --git a/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.sln b/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.sln new file mode 100644 index 0000000..321fbfe --- /dev/null +++ b/Modules/CS2-SimpleAdmin_StealthModule/CS2-SimpleAdmin_StealthModule.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CS2-SimpleAdmin_StealthModule", "CS2-SimpleAdmin_StealthModule.csproj", "{25E0ED51-29BB-4FDD-B40A-62F894A56815}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {25E0ED51-29BB-4FDD-B40A-62F894A56815}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25E0ED51-29BB-4FDD-B40A-62F894A56815}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25E0ED51-29BB-4FDD-B40A-62F894A56815}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25E0ED51-29BB-4FDD-B40A-62F894A56815}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Modules/CS2-SimpleAdmin_StealthModule/Extensions.cs b/Modules/CS2-SimpleAdmin_StealthModule/Extensions.cs new file mode 100644 index 0000000..5d92873 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_StealthModule/Extensions.cs @@ -0,0 +1,31 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; + +namespace CS2_SimpleAdmin_StealthModule; + +public static class Extensions +{ + public static CCSPlayerController? GetSpectatingPlayer(this CCSPlayerController player) + { + if (player.Pawn.Value is not { IsValid: true } pawn) + return null; + + if (player.ControllingBot) + return null; + + if (pawn.ObserverServices is not { } observerServices) + return null; + + if (observerServices.ObserverTarget?.Value?.As() is not { IsValid: true } observerPawn) + return null; + + return observerPawn.OriginalController.Value is not { IsValid: true } observerController ? null : observerController; + } + + public static List GetSpectators(this CCSPlayerController player) + { + return CS2_SimpleAdmin_StealthModule.Players + .Where(p => p.GetSpectatingPlayer()?.Slot == player.Slot) + .ToList(); + } +} \ No newline at end of file diff --git a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-linux.tar.gz b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-linux.tar.gz new file mode 100644 index 0000000..61f7be3 Binary files /dev/null and b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-linux.tar.gz differ diff --git a/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-windows.tar.gz b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-windows.tar.gz new file mode 100644 index 0000000..050a3a9 Binary files /dev/null and b/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN/StatusBlocker-v1.0.3-windows.tar.gz differ diff --git a/Modules/CS2-SimpleAdmin_StealthModule/PluginConfig.cs b/Modules/CS2-SimpleAdmin_StealthModule/PluginConfig.cs new file mode 100644 index 0000000..0261405 --- /dev/null +++ b/Modules/CS2-SimpleAdmin_StealthModule/PluginConfig.cs @@ -0,0 +1,11 @@ +using CounterStrikeSharp.API.Core; + +namespace CS2_SimpleAdmin_StealthModule; + +public class PluginConfig : IBasePluginConfig +{ + public int Version { get; set; } = 1; + public List Permissions { get; set; } = ["@css/ban"]; + public bool BlockStatusCommand { get; set; } = true; + public bool HideAdminsOnJoin { get; set; } = true; +} \ No newline at end of file diff --git a/README.md b/README.md index d62e0bf..aa1bfca 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,40 @@ # CS2-SimpleAdmin +--- + > **Manage your Counter-Strike 2 server with simple commands!** > CS2-SimpleAdmin is a plugin designed to help you easily manage your Counter-Strike 2 server with user-friendly commands. Whether you're banning players, managing teams, or configuring server settings, CS2-SimpleAdmin has you covered. -## 📜 Features -- **Simple Commands**: Manage your server with an easy-to-use command system. -- **MySQL Integration**: Store and retrieve player data seamlessly with MySQL. -- **Ongoing Development**: New features and improvements are added whenever possible. -- **Performance Optimization**: Lightweight and optimized for minimal impact on server performance. -- **Extensible API**: Extend the functionality of CS2-SimpleAdmin by integrating it with your own plugins using the API. Create custom commands, automate tasks, and interact with the plugin programmatically to meet the specific needs of your server. +--- + +## 🚀 Features + +- 🎮 **Simple, Intuitive Commands:** Manage players, teams, bans, and server settings using an easy command system. +- 🗄 **Full Database Integration:** Reliable MySQL backend with optional experimental SQLite support for persistent data storage. +- ⚡ **Efficient and Lightweight:** Designed to minimize server resource usage while maintaining robust functionality. +- 🚨 **Real-time Notifications:** Instant in-game and Discord notifications to keep admins and players informed. +- 🎛 **User-Friendly Admin Interface:** Plugin menus for quick and efficient management without complex commands. +- 🔄 **Multi-Server Compatibility:** Manage and sync data across multiple servers seamlessly. +- 🧩 **Modular and Extensible:** Tailor the plugin with API access to add custom commands and automation. +- 📜 **Complete Auditing Logs:** Track all administrative actions rigorously for accountability. +- 🌐 **Discord Integration:** Stream logs and alerts to your Discord channels for centralized monitoring. +- 🤝 **Community Driven:** Open-source with ongoing contributions from a passionate community. + +--- ## ⚙️ Requirements + **Ensure all the following dependencies are installed before proceeding** - [CounterStrikeSharp](https://github.com/roflmuffin/CounterStrikeSharp) - [PlayerSettings](https://github.com/NickFox007/PlayerSettingsCS2) - Required by MenuManagerCS2 - [AnyBaseLibCS2](https://github.com/NickFox007/AnyBaseLibCS2) - Required by PlayerSettings - [MenuManagerCS2](https://github.com/NickFox007/MenuManagerCS2) -- MySQL database +- MySQL database / SQLite + +--- ## 🚀 Getting Started + 1. **Clone or Download the Repository**: Download or clone the repository and publish to your `addons/counterstrikesharp/plugins/` directory. @@ -32,22 +48,48 @@ 3. **Enjoy Managing Your Server!** Use the commands provided by the plugin to easily manage your server. +--- + ## 📁 Configuration + The configuration file (`CS2-SimpleAdmin.json`) will be auto-generated after the first launch. It contains settings for MySQL connections, command permissions, and other plugin-specific configurations. -## 📙 Wiki -For detailed documentation, guides, and tutorials, please visit [Wiki](https://cs2-simpleadmin.daffyy.dev). +--- + +## 📚 Documentation & Support + +Access full documentation, guides, tutorials, and developer API references here: +[CS2-SimpleAdmin Wiki](https://cs2-simpleadmin.daffyy.dev) + +--- ## 🛠️ Development + This project started as a base for other plugins but has grown into a standalone admin management tool. Contributions are welcome! If you'd like to help with development or have ideas for new features, feel free to submit a pull request or open an issue. +--- + +## 🤝 Contributing & Feedback + +Help improve CS2-SimpleAdmin by: +- Reporting bugs or requesting features on GitHub +- Submitting pull requests with improvements +- Participating in discussions and sharing ideas + +--- + ## 💡 Credits + This project is inspired by the work of [Hackmastr](https://github.com/Hackmastr/css-basic-admin/). Thanks for laying the groundwork! -## ❤️ Support the Project -If you find this plugin helpful and would like to support further development, consider buying me a cup of tea! Your support is greatly appreciated. +--- -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y4THKXG) +## ☕ Support Development + +If you find CS2-SimpleAdmin useful, consider supporting the ongoing development: +[![Buy me a coffee](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y4THKXG) + +--- ## 📄 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.