Migrate menus and votes to Panorama UI Kit

Replace MenuManagerCS2 with Panorama UI Kit for the admin menus and
css_vote. Layout names (menu, vote, banner, toast) are configurable
under MenuConfig and preloaded on startup; MenuType is removed and
ConfigVersion bumped to 26.

- Drop MenuManagerApi.dll and the old chat menu classes
- Reference PanoramaUiKit.Shared.dll, bump CounterStrikeSharp to 1.0.376
- Add .editorconfig and CODING_CONVENTIONS.md, apply them codebase-wide
- Remove the CS2-SimpleAdmin-docs site
- Rewrite README for the fork
This commit is contained in:
Salvatore 2026-10-21 18:51:18 +02:00
parent 03a454b1b5
commit 460d8b2b85
112 changed files with 5559 additions and 36371 deletions

View file

@ -1,12 +1,10 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.ValveConstants.Protobuf;
using System.Data.Common;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
using MySqlConnector;
using System.Text;
using CS2_SimpleAdmin.Database;
namespace CS2_SimpleAdmin.Managers;
@ -22,19 +20,29 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
/// <returns>The newly created ban ID if successful, otherwise null.</returns>
public async Task<int?> BanPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
try
{
var sql = databaseProvider.GetAddBanQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
string? playerIp = null;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1)
{
playerIp = player.IpAddress;
}
string sql = databaseProvider.GetAddBanQuery();
int? banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
playerIp = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1 ? player.IpAddress : null,
playerIp,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
banReason = reason,
@ -46,7 +54,7 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
return banId;
}
catch(Exception ex)
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
return null;
@ -63,16 +71,19 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
/// <returns>The ID of the newly created ban if successful, otherwise null.</returns>
public async Task<int?> AddBanBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddBanBySteamIdQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddBanBySteamIdQuery();
int? banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
@ -83,10 +94,10 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
created = now,
serverid = CS2_SimpleAdmin.ServerId
});
return banId;
}
catch(Exception ex)
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
return null;
@ -102,18 +113,24 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
public async Task AddBanByIp(string playerIp, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(playerIp)) return;
if (string.IsNullOrEmpty(playerIp))
{
return;
}
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddBanByIpQuery();
string sql = databaseProvider.GetAddBanByIpQuery();
await connection.ExecuteAsync(sql, new
{
playerIp,
@ -129,179 +146,179 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
catch { }
}
// public async Task<bool> IsPlayerBanned(PlayerInfo player)
// {
// if (database == null) return false;
//
// if (player.IpAddress == null)
// {
// return false;
// }
//
// #if DEBUG
// if (CS2_SimpleAdmin._logger != null)
// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
// #endif
//
// int banCount;
//
// DateTime currentTime = Time.ActualDateTime();
//
// try
// {
// string sql;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// )
// ), 0) AS TotalBanCount;
// """ : """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// )
// ), 0) AS TotalBanCount;
// """;
// }
// else
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
// """ : """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
// """;
// }
//
// await using var connection = await database.GetConnectionAsync();
//
// var parameters = new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
// string.IsNullOrEmpty(player.IpAddress) ||
// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
// ? null
// : player.IpAddress,
// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
// CurrentTime = currentTime,
// CS2_SimpleAdmin.ServerId
// };
//
// banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
// }
// catch (Exception ex)
// {
// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
// player.Name, ex.Message);
// return false;
// }
//
// return banCount > 0;
// }
//
// public async Task<int> GetPlayerBans(PlayerInfo player)
// {
// if (database == null) return 0;
//
// try
// {
// string sql;
//
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
//
// int banCount;
//
// await using var connection = await database.GetConnectionAsync();
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = player.IpAddress,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
// else
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = DBNull.Value,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
//
// return banCount;
// }
// catch { }
//
// return 0;
// }
// public async Task<bool> IsPlayerBanned(PlayerInfo player)
// {
// if (database == null) return false;
//
// if (player.IpAddress == null)
// {
// return false;
// }
//
// #if DEBUG
// if (CS2_SimpleAdmin._logger != null)
// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
// #endif
//
// int banCount;
//
// DateTime currentTime = Time.ActualDateTime();
//
// try
// {
// string sql;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// )
// ), 0) AS TotalBanCount;
// """ : """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// )
// ), 0) AS TotalBanCount;
// """;
// }
// else
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
// """ : """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
// """;
// }
//
// await using var connection = await database.GetConnectionAsync();
//
// var parameters = new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
// string.IsNullOrEmpty(player.IpAddress) ||
// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
// ? null
// : player.IpAddress,
// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
// CurrentTime = currentTime,
// CS2_SimpleAdmin.ServerId
// };
//
// banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
// }
// catch (Exception ex)
// {
// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
// player.Name, ex.Message);
// return false;
// }
//
// return banCount > 0;
// }
//
// public async Task<int> GetPlayerBans(PlayerInfo player)
// {
// if (database == null) return 0;
//
// try
// {
// string sql;
//
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
//
// int banCount;
//
// await using var connection = await database.GetConnectionAsync();
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = player.IpAddress,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
// else
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = DBNull.Value,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
//
// return banCount;
// }
// catch { }
//
// return 0;
// }
/// <summary>
/// Unbans a player based on a pattern match of SteamID or IP address.
@ -309,46 +326,51 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
/// <param name="playerPattern">Pattern to match against player identifiers (e.g., partial SteamID).</param>
/// <param name="adminSteamId">SteamID64 of the admin performing the unban.</param>
/// <param name="reason">Optional reason for the unban. If null or empty, the unban reason is not stored.</param>
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
{
if (databaseProvider == null) return;
if (playerPattern is not { Length: > 1 })
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
var bansList = bans as dynamic[] ?? bans.ToArray();
if (bansList.Length == 0)
return;
var sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
var adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
foreach (var ban in bansList)
if (databaseProvider == null)
{
int banId = ban.id;
var sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
var unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
return;
}
// Apply immediately; the periodic cache refresh would otherwise keep rejecting the player for up to a minute
CS2_SimpleAdmin.Instance.CacheManager?.SetBanStatus(bansList.Select(b => (int)b.id), Models.BanStatus.UNBANNED);
// css_ban also issues a native banid when UnlockedCommands is on; clear it so the engine stops
// rejecting the player. Done here, after the rows are updated, and for whichever pattern matched them.
if (CS2_SimpleAdmin.UnlockedCommands)
if (playerPattern is not { Length: > 1 })
{
var steamIds = bansList
return;
}
try
{
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
IEnumerable<dynamic> bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
dynamic[] bansList = bans as dynamic[] ?? bans.ToArray();
if (bansList.Length == 0)
{
return;
}
string sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
int adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
foreach (dynamic ban in bansList)
{
int banId = ban.id;
string sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
int unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
string sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
}
// Apply immediately; the periodic cache refresh would otherwise keep rejecting the player for up to a minute
CS2_SimpleAdmin.Instance.CacheManager?.SetBanStatus(bansList.Select(b => (int)b.id), Models.BanStatus.UNBANNED);
// css_ban also issues a native banid when UnlockedCommands is on; clear it so the engine stops
// rejecting the player. Done here, after the rows are updated, and for whichever pattern matched them.
if (CS2_SimpleAdmin.UnlockedCommands)
{
List<ulong> steamIds = bansList
.Select(b => (object?)b.player_steamid)
.Where(id => id != null)
.Select(id => Convert.ToUInt64(id))
@ -356,19 +378,23 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
.Distinct()
.ToList();
if (steamIds.Count > 0)
await Server.NextWorldUpdateAsync(() =>
if (steamIds.Count > 0)
{
foreach (var steamId in steamIds)
await Server.NextWorldUpdateAsync(() =>
{
foreach (ulong steamId in steamIds)
{
Server.ExecuteCommand($"removeid {new SteamID(steamId).SteamId3}");
}
});
}
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to unban player: {exception}", ex.Message);
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to unban player: {exception}", ex.Message);
}
}
// public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
// {
@ -445,17 +471,21 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
/// </summary>
public async Task ExpireOldBans()
{
if (databaseProvider == null) return;
var currentTime = Time.ActualDateTime();
if (databaseProvider == null)
{
return;
}
DateTime currentTime = Time.ActualDateTime();
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { currentTime, serverid = CS2_SimpleAdmin.ServerId });
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans > 0)
{
var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
DateTime ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId });
@ -468,4 +498,4 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
CS2_SimpleAdmin._logger?.LogCritical("Unable to remove expired bans");
}
}
}
}

View file

@ -1,13 +1,12 @@
using System.Collections.Concurrent;
using CS2_SimpleAdmin.Database;
using System.Data.Common;
using CS2_SimpleAdmin.Models;
using Dapper;
using Microsoft.Extensions.Logging;
using ZLinq;
namespace CS2_SimpleAdmin.Managers;
internal class CacheManager: IDisposable
internal class CacheManager : IDisposable
{
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
// Rebuilt into fresh instances and swapped, so unlocked readers on the game thread never see a half-built index
@ -17,22 +16,33 @@ internal class CacheManager: IDisposable
private readonly ConcurrentDictionary<ulong, HashSet<IpRecord>> _playerIpsCache = [];
private HashSet<uint> _cachedIgnoredIps = [];
private DateTime _lastUpdateTime = DateTime.MinValue;
private DateTime? _lastDatabaseTime = null; // Track actual time from database
private bool _isInitialized;
private bool _disposed;
/// <summary>
/// Initializes and builds the ban and IP cache from the database. Loads bans, player IP history, and config settings.
/// </summary>
/// <returns>Asynchronous task representing the initialization process.</returns>
public async Task InitializeCacheAsync()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (!CS2_SimpleAdmin.ServerLoaded) return;
if (_isInitialized) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
if (!CS2_SimpleAdmin.ServerLoaded)
{
return;
}
if (_isInitialized)
{
return;
}
try
{
Clear();
@ -41,9 +51,9 @@ internal class CacheManager: IDisposable
.Select(IpHelper.IpToUint)
.ToHashSet();
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
List<BanRecord> bans;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
bans = (await connection.QueryAsync<BanRecord>(
@ -69,21 +79,21 @@ internal class CacheManager: IDisposable
status AS Status
FROM sa_bans
WHERE server_id = @serverId
""", new {serverId = CS2_SimpleAdmin.ServerId})).ToList();
""", new { serverId = CS2_SimpleAdmin.ServerId })).ToList();
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
// Optimization: Load IP history and build cache in single pass
var ipHistory = await connection.QueryAsync<IpHistoryRow>(
IEnumerable<IpHistoryRow> ipHistory = await connection.QueryAsync<IpHistoryRow>(
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY steamid, address, used_at DESC");
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var currentSteamId = 0UL;
var currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
var latestIpTimestamps = new Dictionary<uint, DateTime>();
string unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
ulong currentSteamId = 0UL;
HashSet<IpRecord> currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
Dictionary<uint, DateTime> latestIpTimestamps = new Dictionary<uint, DateTime>();
foreach (var record in ipHistory)
foreach (IpHistoryRow record in ipHistory)
{
// When we encounter a new steamid, save the previous one
if ((ulong)record.Steamid != currentSteamId && currentSteamId != 0)
@ -96,14 +106,14 @@ internal class CacheManager: IDisposable
currentSteamId = (ulong)record.Steamid;
// Only keep the latest timestamp for each IP
if (!latestIpTimestamps.TryGetValue(record.Address, out var existingTimestamp) ||
if (!latestIpTimestamps.TryGetValue(record.Address, out DateTime existingTimestamp) ||
record.Used_at > existingTimestamp)
{
latestIpTimestamps[record.Address] = record.Used_at;
currentIpSet.Add(new IpRecord(
record.Address,
record.Used_at,
string.IsNullOrEmpty(record.Name) ? unknownName : record.Name
NameOrDefault(record.Name, unknownName)
));
}
}
@ -115,11 +125,13 @@ internal class CacheManager: IDisposable
}
}
foreach (var ban in bans.AsValueEnumerable())
foreach (BanRecord? ban in bans.AsValueEnumerable())
{
_banCache.TryAdd(ban.Id, ban);
}
RebuildIndexes();
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
_isInitialized = true;
}
@ -128,7 +140,7 @@ internal class CacheManager: IDisposable
Console.WriteLine(e.ToString());
}
}
/// <summary>
/// Clears all cached data and reinitializes the cache from the database.
/// </summary>
@ -136,15 +148,15 @@ internal class CacheManager: IDisposable
public async Task ForceReInitializeCacheAsync()
{
_isInitialized = false;
_banCache.Clear();
_playerIpsCache.Clear();
_cachedIgnoredIps = [];
_lastUpdateTime = DateTime.MinValue;
await InitializeCacheAsync();
}
/// <summary>
/// Refreshes the in-memory cache with updated or new data from the database since the last update time.
/// Also updates multi-account IP history if enabled.
@ -152,16 +164,23 @@ internal class CacheManager: IDisposable
/// <returns>Asynchronous task representing the refresh operation.</returns>
public async Task RefreshCacheAsync()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (!_isInitialized) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
if (!_isInitialized)
{
return;
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<BanRecord> updatedBans;
// Get current time from database in local timezone (CURRENT_TIMESTAMP uses session timezone, not UTC)
var currentDatabaseTime = await connection.QueryFirstAsync<DateTime>("SELECT CURRENT_TIMESTAMP");
DateTime currentDatabaseTime = await connection.QueryFirstAsync<DateTime>("SELECT CURRENT_TIMESTAMP");
// Optimization: Only get IDs for comparison if we need to check for deletions
// Most of the time bans are just added/updated, not deleted
@ -170,10 +189,10 @@ internal class CacheManager: IDisposable
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
// Use previous database time or start from far past if first run
var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
DateTime lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
// Get recently updated bans by timestamp (using database time to avoid timezone issues)
var updatedBans_Query = (await connection.QueryAsync<BanRecord>(
List<BanRecord> updatedBans_Query = (await connection.QueryAsync<BanRecord>(
"""
SELECT id AS Id,
player_name AS PlayerName,
@ -186,10 +205,10 @@ internal class CacheManager: IDisposable
)).ToList();
// Detect changes: new bans or status changes
var updatedList = new List<BanRecord>();
foreach (var ban in updatedBans_Query)
List<BanRecord> updatedList = new List<BanRecord>();
foreach (BanRecord ban in updatedBans_Query)
{
if (!_banCache.TryGetValue(ban.Id, out var cachedBan))
if (!_banCache.TryGetValue(ban.Id, out BanRecord? cachedBan))
{
// New ban
updatedList.Add(ban);
@ -213,10 +232,10 @@ internal class CacheManager: IDisposable
else
{
// Use previous database time or start from far past if first run
var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
DateTime lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
// Get recently updated bans for this server by timestamp (using database time to avoid timezone issues)
var updatedBans_Query = (await connection.QueryAsync<BanRecord>(
List<BanRecord> updatedBans_Query = (await connection.QueryAsync<BanRecord>(
"""
SELECT id AS Id,
player_name AS PlayerName,
@ -229,10 +248,10 @@ internal class CacheManager: IDisposable
)).ToList();
// Detect changes: new bans or status changes
var updatedList = new List<BanRecord>();
foreach (var ban in updatedBans_Query)
List<BanRecord> updatedList = new List<BanRecord>();
foreach (BanRecord ban in updatedBans_Query)
{
if (!_banCache.TryGetValue(ban.Id, out var cachedBan))
if (!_banCache.TryGetValue(ban.Id, out BanRecord? cachedBan))
{
// New ban
updatedList.Add(ban);
@ -260,49 +279,56 @@ internal class CacheManager: IDisposable
// Optimization: Only process deletions if we have the full ID list
if (allIds != null)
{
foreach (var id in _banCache.Keys)
foreach (int id in _banCache.Keys)
{
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
if (allIds.Contains(id) || !_banCache.TryRemove(id, out BanRecord? ban))
{
continue;
}
if (ban.PlayerSteamId != null &&
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans))
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out List<BanRecord>? steamBans))
{
steamBans.RemoveAll(b => b.Id == id);
if (steamBans.Count == 0)
{
_steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _);
}
}
if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipBans))
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out uint ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipBans))
{
continue;
}
ipBans.RemoveAll(b => b.Id == id);
if (ipBans.Count == 0)
{
_ipIndex.TryRemove(ipUInt, out _);
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
var ipHistory = (await connection.QueryAsync<IpHistoryRow>(
IEnumerable<IpHistoryRow> ipHistory = (await connection.QueryAsync<IpHistoryRow>(
"SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300",
new { lastUpdate = _lastUpdateTime }));
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid))
foreach (IGrouping<long, IpHistoryRow>? group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid))
{
var ipSet = new HashSet<IpRecord>(
HashSet<IpRecord> ipSet = new HashSet<IpRecord>(
group
.GroupBy(x => x.Address)
.Select(g =>
{
var latest = g.MaxBy(x => x.Used_at)!; // group is never empty
IpHistoryRow latest = g.MaxBy(x => x.Used_at)!; // group is never empty
return new IpRecord(
g.Key,
latest.Used_at,
!string.IsNullOrEmpty(latest.Name)
? latest.Name
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
NameOrDefault(latest.Name, CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown")
);
}),
new IpRecordComparer()
@ -313,7 +339,7 @@ internal class CacheManager: IDisposable
_ => ipSet,
(_, existingSet) =>
{
foreach (var newEntry in ipSet)
foreach (IpRecord newEntry in ipSet)
{
existingSet.Remove(newEntry);
existingSet.Add(newEntry);
@ -325,10 +351,10 @@ internal class CacheManager: IDisposable
}
// Update cache with new/modified bans
var needsRebuild = false;
foreach (var ban in updatedBans)
bool needsRebuild = false;
foreach (BanRecord ban in updatedBans)
{
if (_banCache.TryGetValue(ban.Id, out var oldBan) && oldBan.Status != ban.Status)
if (_banCache.TryGetValue(ban.Id, out BanRecord? oldBan) && oldBan.Status != ban.Status)
{
// Ban status changed (e.g., ACTIVE -> EXPIRED/UNBANNED), need to rebuild indexes
needsRebuild = true;
@ -370,36 +396,43 @@ internal class CacheManager: IDisposable
/// <param name="status">New status.</param>
public void SetBanStatus(IEnumerable<int> banIds, BanStatus status)
{
var changed = false;
foreach (var id in banIds)
bool changed = false;
foreach (int id in banIds)
{
if (!_banCache.TryGetValue(id, out var ban) || ban.StatusEnum == status) continue;
if (!_banCache.TryGetValue(id, out BanRecord? ban) || ban.StatusEnum == status)
{
continue;
}
_banCache[id] = ban with { Status = status.ToString() };
changed = true;
}
if (changed) RebuildIndexes();
if (changed)
{
RebuildIndexes();
}
}
private void RebuildIndexesCore()
{
var steamIdIndex = new ConcurrentDictionary<ulong, List<BanRecord>>();
var ipIndex = new ConcurrentDictionary<uint, List<BanRecord>>();
ConcurrentDictionary<ulong, List<BanRecord>> steamIdIndex = new ConcurrentDictionary<ulong, List<BanRecord>>();
ConcurrentDictionary<uint, List<BanRecord>> ipIndex = new ConcurrentDictionary<uint, List<BanRecord>>();
// Optimization: Cache config value to avoid repeated property access
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
var checkIpBans = banType != 0;
int banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
bool checkIpBans = banType != 0;
// Optimization: Pre-filter only ACTIVE bans to avoid checking status in loop
var activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE);
IEnumerable<BanRecord> activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE);
foreach (var ban in activeBans)
foreach (BanRecord? ban in activeBans)
{
// Index by Steam ID
if (ban.PlayerSteamId.HasValue)
{
var steamId = ban.PlayerSteamId.Value;
if (!steamIdIndex.TryGetValue(steamId, out var steamList))
ulong steamId = ban.PlayerSteamId.Value;
if (!steamIdIndex.TryGetValue(steamId, out List<BanRecord>? steamList))
{
steamList = new List<BanRecord>();
steamIdIndex[steamId] = steamList;
@ -409,9 +442,9 @@ internal class CacheManager: IDisposable
// Index by IP (only if IP bans are enabled)
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
IpHelper.TryConvertIpToUint(ban.PlayerIp, out uint ipUInt))
{
if (!ipIndex.TryGetValue(ipUInt, out var ipList))
if (!ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipList))
{
ipList = new List<BanRecord>();
ipIndex[ipUInt] = ipList;
@ -424,19 +457,19 @@ internal class CacheManager: IDisposable
Volatile.Write(ref _steamIdIndex, steamIdIndex);
Volatile.Write(ref _ipIndex, ipIndex);
}
/// <summary>
/// Retrieves all ban records currently stored in the cache.
/// </summary>
/// <returns>List of all <see cref="BanRecord"/> objects.</returns>
public List<BanRecord> GetAllBans() => _banCache.Values.ToList();
/// <summary>
/// Retrieves only active ban records from the cache.
/// </summary>
/// <returns>List of active <see cref="BanRecord"/> objects.</returns>
public List<BanRecord> GetActiveBans() => _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE).ToList();
/// <summary>
/// Retrieves all ban records for a specific player by their Steam ID.
/// </summary>
@ -446,7 +479,7 @@ internal class CacheManager: IDisposable
// ponytail: O(n) over all cached bans per call; add an all-status index if this gets hot.
public List<BanRecord> GetPlayerBansBySteamId(ulong steamId) =>
_banCache.Values.Where(b => b.PlayerSteamId == steamId).ToList();
/// <summary>
/// Gets all known Steam accounts that have used the specified IP address.
/// </summary>
@ -456,16 +489,16 @@ internal class CacheManager: IDisposable
/// </returns>
public List<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
{
var ipAsUint = IpHelper.IpToUint(ipAddress);
var results = new List<(ulong, DateTime, string)>();
uint ipAsUint = IpHelper.IpToUint(ipAddress);
List<(ulong, DateTime, string)> results = new List<(ulong, DateTime, string)>();
// Optimization: Direct lookup using HashSet.Contains instead of TryGetValue
var searchRecord = new IpRecord(ipAsUint, default, null!);
IpRecord searchRecord = new IpRecord(ipAsUint, default, null!);
foreach (var (steamId, ipSet) in _playerIpsCache)
foreach ((ulong steamId, HashSet<IpRecord>? ipSet) in _playerIpsCache)
{
// Optimization: Single pass through the set
foreach (var entry in ipSet)
foreach (IpRecord entry in ipSet)
{
if (entry.Ip == ipAsUint)
{
@ -488,11 +521,15 @@ internal class CacheManager: IDisposable
private bool IsIpBanned(string ipAddress)
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
var ipUInt = IpHelper.IpToUint(ipAddress);
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
{
return false;
}
uint ipUInt = IpHelper.IpToUint(ipAddress);
return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt);
}
// public bool IsPlayerBanned(ulong? steamId, string? ipAddress)
// {
// if (steamId != null && _steamIdIndex.ContainsKey(steamId.Value))
@ -518,13 +555,13 @@ internal class CacheManager: IDisposable
public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress)
{
BanRecord? record;
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out List<BanRecord>? steamRecords))
{
record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record != null)
{
// Double-check the ban is still active in cache (handle race conditions)
if (_banCache.TryGetValue(record.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
if (_banCache.TryGetValue(record.Id, out BanRecord? cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
{
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue))
@ -536,21 +573,31 @@ internal class CacheManager: IDisposable
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
{
return false;
}
if (string.IsNullOrEmpty(ipAddress) ||
!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) ||
!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt) ||
_cachedIgnoredIps.Contains(ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false;
!_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipRecords))
{
return false;
}
record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record == null) return false;
if (record == null)
{
return false;
}
// Double-check the ban is still active in cache (handle race conditions)
if (!_banCache.TryGetValue(record.Id, out var cachedBanIp) || cachedBanIp.StatusEnum != BanStatus.ACTIVE)
if (!_banCache.TryGetValue(record.Id, out BanRecord? cachedBanIp) || cachedBanIp.StatusEnum != BanStatus.ACTIVE)
{
return false;
}
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue && steamId.HasValue))
@ -560,7 +607,7 @@ internal class CacheManager: IDisposable
return true;
}
// public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress)
// {
// if (_steamIdIndex.ContainsKey(steamId))
@ -638,16 +685,18 @@ internal class CacheManager: IDisposable
/// <returns>True if the player or their known IPs are banned, otherwise false.</returns>
public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress)
{
if (_steamIdIndex.TryGetValue(steamId, out var steamBans))
if (_steamIdIndex.TryGetValue(steamId, out List<BanRecord>? steamBans))
{
var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
BanRecord? activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan != null)
{
// Double-check the ban is still active in cache (handle race conditions)
if (_banCache.TryGetValue(activeBan.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
if (_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
{
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
}
return true;
}
@ -655,25 +704,34 @@ internal class CacheManager: IDisposable
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
{
return false;
}
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt))
if (!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt))
{
return false;
}
if (_cachedIgnoredIps.Contains(ipUInt))
{
return false;
}
// Direct ip ban (ban record has player_ip set)
if (_ipIndex.TryGetValue(ipUInt, out var ipBanRecords))
if (_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipBanRecords))
{
var ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out var cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE)
BanRecord? ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out BanRecord? cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE)
{
var expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans;
int expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans;
if (expireOldIpBans <= 0 || ipBan.Created >= Time.ActualDateTime().AddDays(-expireOldIpBans))
{
if (string.IsNullOrEmpty(ipBan.PlayerName))
{
ipBan.PlayerName = playerName;
}
ipBan.PlayerSteamId ??= steamId;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
@ -684,19 +742,32 @@ internal class CacheManager: IDisposable
// Multiaccount ban - check if other accounts using current ip are banned
if (!_playerIpsCache.IsEmpty)
{
foreach (var (otherSteamId, ipSet) in _playerIpsCache)
foreach ((ulong otherSteamId, HashSet<IpRecord>? ipSet) in _playerIpsCache)
{
// Skip current player
if (otherSteamId == steamId)
{
continue;
}
// Check if this ip is in the other accounts ip history
if (ipSet.All(record => record.Ip != ipUInt)) continue;
if (ipSet.All(record => record.Ip != ipUInt))
{
continue;
}
// Found another account using this ip - check if its banned
if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans)) continue;
var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
cachedBan.StatusEnum != BanStatus.ACTIVE) continue;
if (!_steamIdIndex.TryGetValue(otherSteamId, out List<BanRecord>? otherSteamBans))
{
continue;
}
BanRecord? activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) ||
cachedBan.StatusEnum != BanStatus.ACTIVE)
{
continue;
}
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
@ -705,32 +776,44 @@ internal class CacheManager: IDisposable
// Multiaccount ban - check if this player used any ip where other banned accounts are connected
// Search sa_players_ips for all accounts sharing the same ips as current player
if (!CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
return false;
}
if (!_playerIpsCache.TryGetValue(steamId, out var playerIps))
if (!_playerIpsCache.TryGetValue(steamId, out HashSet<IpRecord>? playerIps))
{
return false;
}
// For each ip the player used (current or historical)
foreach (var playerIpRecord in playerIps)
foreach (IpRecord playerIpRecord in playerIps)
{
// Search sa_players_ips for other accounts using this same ip (as uint)
foreach (var (otherSteamId, otherIpSet) in _playerIpsCache)
foreach ((ulong otherSteamId, HashSet<IpRecord>? otherIpSet) in _playerIpsCache)
{
if (otherSteamId == steamId)
{
continue;
}
// Check if this other account used the player ip
if (otherIpSet.All(record => record.Ip != playerIpRecord.Ip))
{
continue;
}
// Check if this other account is banned
if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans))
if (!_steamIdIndex.TryGetValue(otherSteamId, out List<BanRecord>? otherSteamBans))
{
continue;
}
var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
BanRecord? activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) ||
cachedBan.StatusEnum != BanStatus.ACTIVE)
{
continue;
}
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
@ -749,15 +832,19 @@ internal class CacheManager: IDisposable
public bool HasIpForPlayer(ulong steamId, string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
{
return false;
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUint))
return false;
}
return _playerIpsCache.TryGetValue(steamId, out var ipData) &&
if (!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUint))
{
return false;
}
return _playerIpsCache.TryGetValue(steamId, out HashSet<IpRecord>? ipData) &&
ipData.Contains(new IpRecord(ipUint, default, null!));
}
// public bool HasIpForPlayer(ulong steamId, string ipAddress)
// {
// if (string.IsNullOrWhiteSpace(ipAddress))
@ -778,9 +865,11 @@ internal class CacheManager: IDisposable
private async Task UpdatePlayerData(string? playerName, ulong? steamId, string? ipAddress)
{
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
var baseSql = """
string baseSql = """
UPDATE sa_bans
SET
player_ip = COALESCE(player_ip, @PlayerIP),
@ -796,44 +885,56 @@ internal class CacheManager: IDisposable
baseSql += " AND server_id = @ServerId;";
}
string? playerIp = ipAddress;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|| string.IsNullOrEmpty(ipAddress)
|| CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress))
{
playerIp = null;
}
var parameters = new
{
PlayerSteamID = steamId,
PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|| string.IsNullOrEmpty(ipAddress)
|| CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress)
? null
: ipAddress,
PlayerName = string.IsNullOrEmpty(playerName) ? string.Empty : playerName,
PlayerIP = playerIp,
PlayerName = playerName ?? string.Empty,
CurrentTime = Time.ActualDateTime(),
CS2_SimpleAdmin.ServerId
};
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await connection.ExecuteAsync(baseSql, parameters);
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out List<BanRecord>? steamRecords))
{
foreach (var rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
foreach (BanRecord? rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
{
if (string.IsNullOrEmpty(rec.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
{
rec.PlayerIp = ipAddress;
}
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
{
rec.PlayerName = playerName;
}
}
}
if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)
&& _ipIndex.TryGetValue(ipUInt, out var ipRecords))
if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt)
&& _ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipRecords))
{
foreach (var rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
foreach (BanRecord? rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
{
if (!rec.PlayerSteamId.HasValue && steamId.HasValue)
{
rec.PlayerSteamId = steamId;
}
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
{
rec.PlayerName = playerName;
}
}
}
}
@ -847,16 +948,30 @@ internal class CacheManager: IDisposable
_playerIpsCache.Clear();
_cachedIgnoredIps.Clear();
}
/// <summary>
/// Clears and disposes of all cached data and marks the object as disposed.
/// </summary>
public void Dispose()
{
if (_disposed) return;
if (_disposed)
{
return;
}
Clear();
_disposed = true;
}
private static string NameOrDefault(string? name, string defaultName)
{
if (string.IsNullOrEmpty(name))
{
return defaultName;
}
return name;
}
}
public class IpRecordComparer : IEqualityComparer<IpRecord>
@ -866,4 +981,4 @@ public class IpRecordComparer : IEqualityComparer<IpRecord>
public int GetHashCode(IpRecord obj)
=> obj.Ip.GetHashCode();
}
}

View file

@ -7,7 +7,7 @@ namespace CS2_SimpleAdmin.Managers;
public class DiscordManager(string webhookUrl)
{
/// <summary>
/// Sends a plain text message asynchronously to the configured Discord webhook URL.
/// </summary>
@ -16,23 +16,23 @@ public class DiscordManager(string webhookUrl)
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task SendMessageAsync(string message)
{
var client = CS2_SimpleAdmin.HttpClient;
var payload = new
HttpClient client = CS2_SimpleAdmin.HttpClient;
Dictionary<string, object> payload = new Dictionary<string, object>
{
content = message
["content"] = message
};
var options = new JsonSerializerOptions
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = false
};
var json = JsonSerializer.Serialize(payload, options);
var content = new StringContent(json, Encoding.UTF8, "application/json");
string json = JsonSerializer.Serialize(payload, options);
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
try
{
var response = await client.PostAsync(webhookUrl, content);
HttpResponseMessage response = await client.PostAsync(webhookUrl, content);
if (!response.IsSuccessStatusCode)
{
@ -54,48 +54,75 @@ public class DiscordManager(string webhookUrl)
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task SendEmbedAsync(Embed embed)
{
var httpClient = CS2_SimpleAdmin.HttpClient;
HttpClient httpClient = CS2_SimpleAdmin.HttpClient;
var payload = new
// Same JSON as before: every key is written, null when the part is empty
Dictionary<string, object?> embedPayload = new Dictionary<string, object?>
{
embeds = new[]
{
new
{
color = embed.Color,
title = !string.IsNullOrEmpty(embed.Title) ? embed.Title : null,
description = !string.IsNullOrEmpty(embed.Description) ? embed.Description : null,
thumbnail = !string.IsNullOrEmpty(embed.ThumbnailUrl) ? new { url = embed.ThumbnailUrl } : null,
image = !string.IsNullOrEmpty(embed.ImageUrl) ? new { url = embed.ImageUrl } : null,
footer = !string.IsNullOrEmpty(embed.Footer?.Text) ? new { text = embed.Footer.Text, icon_url = embed.Footer.IconUrl } : null,
timestamp = embed.Timestamp,
fields = embed.Fields.Count > 0 ? embed.Fields.Select(field => new
{
name = field.Name,
value = field.Value,
inline = field.Inline
}).ToArray() : null
}
}
["color"] = embed.Color,
["title"] = null,
["description"] = null,
["thumbnail"] = null,
["image"] = null,
["footer"] = null,
["timestamp"] = embed.Timestamp,
["fields"] = null
};
var options = new JsonSerializerOptions
if (!string.IsNullOrEmpty(embed.Title))
{
embedPayload["title"] = embed.Title;
}
if (!string.IsNullOrEmpty(embed.Description))
{
embedPayload["description"] = embed.Description;
}
if (!string.IsNullOrEmpty(embed.ThumbnailUrl))
{
embedPayload["thumbnail"] = new Dictionary<string, object?> { ["url"] = embed.ThumbnailUrl };
}
if (!string.IsNullOrEmpty(embed.ImageUrl))
{
embedPayload["image"] = new Dictionary<string, object?> { ["url"] = embed.ImageUrl };
}
if (embed.Footer != null && !string.IsNullOrEmpty(embed.Footer.Text))
{
embedPayload["footer"] = new Dictionary<string, object?> { ["text"] = embed.Footer.Text, ["icon_url"] = embed.Footer.IconUrl };
}
if (embed.Fields.Count > 0)
{
embedPayload["fields"] = embed.Fields
.Select(field => new Dictionary<string, object?> { ["name"] = field.Name, ["value"] = field.Value, ["inline"] = field.Inline })
.ToArray();
}
Dictionary<string, object> payload = new Dictionary<string, object>
{
["embeds"] = new[] { embedPayload }
};
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = false
};
var jsonPayload = JsonSerializer.Serialize(payload, options);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
string jsonPayload = JsonSerializer.Serialize(payload, options);
StringContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(webhookUrl, content);
HttpResponseMessage response = await httpClient.PostAsync(webhookUrl, content);
if (!response.IsSuccessStatusCode)
{
var errorMessage = await response.Content.ReadAsStringAsync();
string errorMessage = await response.Content.ReadAsStringAsync();
CS2_SimpleAdmin._logger?.LogError($"Failed to send embed: {response.StatusCode} - {errorMessage}");
}
}
/// <summary>
/// Converts a hexadecimal color string (e.g. "#FF0000") to its integer representation.
/// </summary>
@ -135,13 +162,13 @@ public class Embed
/// <param name="inline">Whether the field should be displayed inline with other fields.</param>
public void AddField(string name, string value, bool inline)
{
var field = new EmbedField
EmbedField field = new EmbedField
{
Name = name,
Value = value,
Inline = inline
};
Fields.Add(field);
}
}

View file

@ -1,4 +1,5 @@
using CS2_SimpleAdmin.Database;
using System.Data.Common;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
@ -18,12 +19,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Mute ID if successfully added, otherwise null.</returns>
public async Task<int?> MutePlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
var muteType = type switch
string muteType = type switch
{
1 => "MUTE",
2 => "SILENCE",
@ -32,10 +36,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddMuteQuery(true);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddMuteQuery(true);
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
int? muteId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
@ -69,12 +73,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Mute ID if successful, otherwise null.</returns>
public async Task<int?> AddMuteBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
var muteType = type switch
string muteType = type switch
{
1 => "MUTE",
2 => "SILENCE",
@ -83,10 +90,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddMuteQuery(false);
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddMuteQuery(false);
int? muteId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
@ -114,7 +121,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>List of active mute records; empty list if none or on error.</returns>
public async Task<List<dynamic>> IsPlayerMuted(string steamId)
{
if (databaseProvider == null) return [];
if (databaseProvider == null)
{
return [];
}
if (string.IsNullOrEmpty(steamId))
{
@ -123,18 +133,20 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
#if DEBUG
if (CS2_SimpleAdmin._logger != null)
{
CS2_SimpleAdmin._logger.LogCritical($"IsPlayerMuted for {steamId}");
}
#endif
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var currentTime = Time.ActualDateTime();
var sql = databaseProvider.GetIsMutedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
DateTime currentTime = Time.ActualDateTime();
string sql = databaseProvider.GetIsMutedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
var parameters = new { PlayerSteamID = steamId, CurrentTime = currentTime, serverid = CS2_SimpleAdmin.ServerId };
var activeMutes = (await connection.QueryAsync(sql, parameters)).ToList();
List<dynamic> activeMutes = (await connection.QueryAsync(sql, parameters)).ToList();
return activeMutes;
}
catch (Exception)
@ -153,14 +165,17 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// </returns>
public async Task<(int TotalMutes, int TotalGags, int TotalSilences)> GetPlayerMutes(PlayerInfo playerInfo)
{
if (databaseProvider == null) return (0,0,0);
if (databaseProvider == null)
{
return (0, 0, 0);
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetMuteStatsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new
string sql = databaseProvider.GetMuteStatsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
(int TotalMutes, int TotalGags, int TotalSilences) result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new
{
PlayerSteamID = playerInfo.SteamId.SteamId64,
CS2_SimpleAdmin.ServerId
@ -173,7 +188,7 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
return (0, 0, 0);
}
}
/// <summary>
/// Processes a batch of online players to update their mute status and remove expired penalties.
/// </summary>
@ -181,21 +196,24 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Task representing the asynchronous operation.</returns>
public async Task CheckOnlineModeMutes(List<(ulong SteamID, int? UserId, int Slot)> players)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
const int batchSize = 20;
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
string sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
for (var i = 0; i < players.Count; i += batchSize)
for (int i = 0; i < players.Count; i += batchSize)
{
var batch = players.Skip(i).Take(batchSize);
var parametersList = new List<object>();
IEnumerable<(ulong SteamID, int? UserId, int Slot)> batch = players.Skip(i).Take(batchSize);
List<object> parametersList = new List<object>();
foreach (var (steamId, _, _) in batch)
foreach ((ulong steamId, int? _, int _) in batch)
{
parametersList.Add(new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
}
@ -205,11 +223,11 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
sql = databaseProvider.GetCheckExpiredMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
foreach (var (steamId, _, slot) in players)
foreach ((ulong steamId, int? _, int slot) in players)
{
var muteRecords = await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
IEnumerable<dynamic> muteRecords = await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
foreach (var muteRecord in muteRecords)
foreach (dynamic muteRecord in muteRecords)
{
DateTime endDateTime = muteRecord.ends;
PlayerPenaltyManager.RemovePenaltiesByDateTime(slot, endDateTime);
@ -230,7 +248,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Task representing the asynchronous operation.</returns>
public async Task UnmutePlayer(string playerPattern, string adminSteamId, string reason, int type = 0)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (playerPattern.Length <= 1)
{
@ -239,36 +260,38 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var muteType = type switch
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string muteType = type switch
{
1 => "MUTE",
2 => "SILENCE",
_ => "GAG"
};
var sqlRetrieveMutes =
string sqlRetrieveMutes =
databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId });
IEnumerable<dynamic> mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId });
var mutesList = mutes as dynamic[] ?? mutes.ToArray();
dynamic[] mutesList = mutes as dynamic[] ?? mutes.ToArray();
if (mutesList.Length == 0)
{
return;
}
var sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery();
var sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason));
string sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery();
string sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason));
var sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
var adminId = sqlAdminId ?? 0;
int? sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
int adminId = sqlAdminId ?? 0;
foreach (var mute in mutesList)
foreach (dynamic mute in mutesList)
{
int muteId = mute.id;
int? unmuteId =
await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId, reason });
var sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery();
string sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery();
await connection.ExecuteAsync(sqlUpdateMute, new { unmuteId, muteId });
}
}
@ -284,12 +307,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Task representing the asynchronous expiration operation.</returns>
public async Task ExpireOldMutes()
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetExpireMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception)
@ -297,4 +323,4 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
CS2_SimpleAdmin._logger?.LogCritical("Unable to remove expired mutes");
}
}
}
}

View file

@ -1,12 +1,13 @@
using System.Collections.Concurrent;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Database;
using Dapper;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Text.Json;
using CounterStrikeSharp.API.Modules.Admin;
using System.Diagnostics.CodeAnalysis;
using CS2_SimpleAdmin.Database;
namespace CS2_SimpleAdmin.Managers;
@ -18,58 +19,60 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// Retrieves all players' flags and associated data asynchronously.
/// </summary>
/// <returns>A list of tuples containing player SteamID, name, flags, immunity, and expiration time.</returns>
private async Task<List<(ulong, string ,List<string>, int, DateTime?)>> GetAllPlayersFlags()
private async Task<List<(ulong, string, List<string>, int, DateTime?)>> GetAllPlayersFlags()
{
if (databaseProvider == null)
return new List<(ulong, string, List<string>, int, DateTime?)>();
if (databaseProvider == null)
{
return new List<(ulong, string, List<string>, int, DateTime?)>();
}
var now = Time.ActualDateTime();
DateTime now = Time.ActualDateTime();
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAdminsQuery();
var admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList();
try
{
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAdminsQuery();
List<dynamic> admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList();
var groupedPlayers = admins
.GroupBy(r => new { playerSteamId = r.player_steamid, playerName = r.player_name, r.immunity, r.ends })
.Select(g =>
{
ulong steamId = g.Key.playerSteamId switch
{
long l => (ulong)l,
int i => (ulong)i,
string s when ulong.TryParse(s, out var parsed) => parsed,
_ => 0UL
};
List<(ulong steamId, string playerName, List<string> flags, int immunity, DateTime? ends)> groupedPlayers = admins
.GroupBy(r => new { playerSteamId = r.player_steamid, playerName = r.player_name, r.immunity, r.ends })
.Select(g =>
{
ulong steamId = g.Key.playerSteamId switch
{
long l => (ulong)l,
int i => (ulong)i,
string s when ulong.TryParse(s, out ulong parsed) => parsed,
_ => 0UL
};
int immunity = g.Key.immunity switch
{
int i => i,
string s when int.TryParse(s, out var parsed) => parsed,
_ => 0
};
int immunity = g.Key.immunity switch
{
int i => i,
string s when int.TryParse(s, out int parsed) => parsed,
_ => 0
};
DateTime? ends = g.Key.ends as DateTime?;
DateTime? ends = g.Key.ends as DateTime?;
string playerName = g.Key.playerName as string ?? string.Empty;
string playerName = g.Key.playerName as string ?? string.Empty;
// Dapper returns string here, not dynamic
var flags = g.Select(r => r.flag as string ?? string.Empty)
.Distinct()
.ToList();
// Dapper returns string here, not dynamic
List<string> flags = g.Select(r => r.flag as string ?? string.Empty)
.Distinct()
.ToList();
return (steamId, playerName, flags, immunity, ends);
})
.ToList();
return (steamId, playerName, flags, immunity, ends);
})
.ToList();
return groupedPlayers;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
throw; // an empty list would strip every admin; abort the reload and keep the current permissions
}
return groupedPlayers;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
throw; // an empty list would strip every admin; abort the reload and keep the current permissions
}
}
/// <summary>
@ -78,24 +81,27 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <returns>A dictionary with group names as keys and tuples of flags and immunity as values.</returns>
private async Task<Dictionary<string, (List<string>, int)>> GetAllGroupsData()
{
if (databaseProvider == null) return [];
if (databaseProvider == null)
{
return [];
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetGroupsQuery();
var groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetGroupsQuery();
List<dynamic> groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
if (groupData.Count == 0)
{
return [];
}
var groupInfoDictionary = new Dictionary<string, (List<string>, int)>();
foreach (var row in groupData)
Dictionary<string, (List<string>, int)> groupInfoDictionary = new Dictionary<string, (List<string>, int)>();
foreach (dynamic row in groupData)
{
var groupName = (string)row.group_name;
var flag = (string)row.flag;
var immunity = (int)row.immunity;
string groupName = (string)row.group_name;
string flag = (string)row.flag;
int immunity = (int)row.immunity;
if (!groupInfoDictionary.TryGetValue(groupName, out (List<string>, int) value))
{
@ -121,12 +127,12 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task CreateGroupsJsonFile()
{
var groupsData = await GetAllGroupsData();
var jsonData = new Dictionary<string, object>();
Dictionary<string, (List<string>, int)> groupsData = await GetAllGroupsData();
Dictionary<string, object> jsonData = new Dictionary<string, object>();
foreach (var kvp in groupsData)
foreach (KeyValuePair<string, (List<string>, int)> kvp in groupsData)
{
var groupData = new Dictionary<string, object>
Dictionary<string, object> groupData = new Dictionary<string, object>
{
["flags"] = kvp.Value.Item1,
["immunity"] = kvp.Value.Item2
@ -135,14 +141,14 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
jsonData[kvp.Key] = groupData;
}
var options = new JsonSerializerOptions
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(jsonData, options);
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "groups.json");
string json = JsonSerializer.Serialize(jsonData, options);
string filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "groups.json");
await File.WriteAllTextAsync(filePath, json);
}
@ -153,55 +159,55 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
public async Task<List<(SteamID steamId, DateTime? ends, List<string> flags)>> CreateAdminsJsonFile()
{
List<(ulong identity, string name, List<string> flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags();
var validPlayers = allPlayers
List<(ulong identity, string name, List<string> flags, int immunity, DateTime? ends)> validPlayers = allPlayers
.Where(player => SteamID.TryParse(player.identity.ToString(), out _))
.ToList();
var jsonData = validPlayers
.GroupBy(player => player.name)
.ToDictionary(
group => group.Key,
object (group) =>
{
var consolidatedData = group.Aggregate(
new
{
identity = string.Empty,
immunity = 0,
flags = new List<string>(),
groups = new List<string>()
},
(acc, player) =>
{
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
{
acc = acc with { identity = player.identity.ToString() };
}
Dictionary<string, object> jsonData = validPlayers
.GroupBy(player => player.name)
.ToDictionary(
group => group.Key,
object (group) =>
{
var consolidatedData = group.Aggregate(
new
{
identity = string.Empty,
immunity = 0,
flags = new List<string>(),
groups = new List<string>()
},
(acc, player) =>
{
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
{
acc = acc with { identity = player.identity.ToString() };
}
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
acc = acc with
{
flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(),
groups = acc.groups.Concat(player.flags.Where(flag => flag.StartsWith($"#"))).Distinct().ToList()
};
acc = acc with
{
flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(),
groups = acc.groups.Concat(player.flags.Where(flag => flag.StartsWith($"#"))).Distinct().ToList()
};
return acc;
});
return acc;
});
return consolidatedData;
});
return consolidatedData;
});
var newCache = validPlayers.Select(player => (new SteamID(player.identity), player.ends, player.flags)).ToList();
List<(SteamID, DateTime? ends, List<string> flags)> newCache = validPlayers.Select(player => (new SteamID(player.identity), player.ends, player.flags)).ToList();
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(jsonData, options);
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
string json = JsonSerializer.Serialize(jsonData, options);
string filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
await File.WriteAllTextAsync(filePath, json);
return newCache;
@ -214,25 +220,34 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="admins">Admins loaded from the database.</param>
public static void ApplyAdminCache(List<(SteamID steamId, DateTime? ends, List<string> flags)> admins)
{
foreach (var steamId in AdminCache.Keys.ToList())
foreach (SteamID? steamId in AdminCache.Keys.ToList())
{
if (!AdminCache.TryRemove(steamId, out var cached)) continue;
if (!AdminCache.TryRemove(steamId, out (DateTime? ExpirationTime, List<string> Flags) cached))
{
continue;
}
var data = AdminManager.GetPlayerAdminData(steamId);
if (data == null) continue;
AdminData? data = AdminManager.GetPlayerAdminData(steamId);
if (data == null)
{
continue;
}
var flagsArray = cached.Flags.ToArray();
string[] flagsArray = cached.Flags.ToArray();
AdminManager.RemovePlayerPermissions(steamId, flagsArray);
AdminManager.RemovePlayerFromGroup(steamId, true, flagsArray);
data = AdminManager.GetPlayerAdminData(steamId);
if (data == null || (data.Flags.Count != 0 && data.Groups.Count != 0)) continue;
if (data == null || (data.Flags.Count != 0 && data.Groups.Count != 0))
{
continue;
}
AdminManager.ClearPlayerPermissions(steamId);
AdminManager.RemovePlayerAdminData(steamId);
}
foreach (var (steamId, ends, flags) in admins)
foreach ((SteamID? steamId, DateTime? ends, List<string>? flags) in admins)
{
AdminCache.TryAdd(steamId, (ends, flags));
}
@ -245,13 +260,20 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="globalDelete">Whether to delete the admin globally or only for the current server.</param>
public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false)
{
if (databaseProvider == null) return;
if (string.IsNullOrEmpty(playerSteamId)) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(playerSteamId))
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetDeleteAdminQuery(globalDelete);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetDeleteAdminQuery(globalDelete);
await connection.ExecuteAsync(sql, new { PlayerSteamID = playerSteamId, CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -271,31 +293,52 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="globalAdmin">Whether the admin is global or server-specific.</param>
public async Task AddAdminBySteamId(string playerSteamId, string playerName, List<string> flagsList, int immunity = 0, int time = 0, bool globalAdmin = false)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return;
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0)
{
return;
}
var now = Time.ActualDateTime();
DateTime? futureTime = time != 0 ? now.AddMinutes(time) : null;
DateTime now = Time.ActualDateTime();
DateTime? futureTime;
if (time != 0)
{
futureTime = now.AddMinutes(time);
}
else
{
futureTime = null;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var insertAdminSql = databaseProvider.GetAddAdminQuery();
var adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
int? serverId = CS2_SimpleAdmin.ServerId;
if (globalAdmin)
{
serverId = null;
}
string insertAdminSql = databaseProvider.GetAddAdminQuery();
int adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
{
playerSteamId,
playerName,
immunity,
ends = futureTime,
created = now,
serverid = globalAdmin ? null : CS2_SimpleAdmin.ServerId
serverid = serverId
});
foreach (var flag in flagsList)
foreach (string flag in flagsList)
{
var insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery();
string insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery();
await connection.ExecuteAsync(insertFlagsSql, new
{
adminId,
@ -323,24 +366,30 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="globalGroup">Whether the group is global or server-specific.</param>
public async Task AddGroup(string groupName, List<string> flagsList, int immunity = 0, bool globalGroup = false)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return;
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var insertGroup = databaseProvider.GetAddGroupQuery();
var groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
string insertGroup = databaseProvider.GetAddGroupQuery();
int groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
{
groupName,
immunity
});
foreach (var flag in flagsList)
foreach (string flag in flagsList)
{
var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
string insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
await connection.ExecuteAsync(insertFlagsSql, new
{
groupId,
@ -348,8 +397,14 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
});
}
var insertGroupServer = databaseProvider.GetAddGroupServerQuery();
await connection.ExecuteAsync(insertGroupServer, new { groupId, server_id = globalGroup ? null : CS2_SimpleAdmin.ServerId });
string insertGroupServer = databaseProvider.GetAddGroupServerQuery();
int? groupServerId = CS2_SimpleAdmin.ServerId;
if (globalGroup)
{
groupServerId = null;
}
await connection.ExecuteAsync(insertGroupServer, new { groupId, server_id = groupServerId });
await Server.NextWorldUpdateAsync(() =>
{
CS2_SimpleAdmin.Instance.ReloadAdmins(null);
@ -367,14 +422,20 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="groupName">Name of the group to delete.</param>
public async Task DeleteGroup(string groupName)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(groupName)) return;
if (string.IsNullOrEmpty(groupName))
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetDeleteGroupQuery();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetDeleteGroupQuery();
await connection.ExecuteAsync(sql, new { groupName });
}
catch (Exception ex)
@ -388,13 +449,16 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// </summary>
public async Task DeleteOldAdmins()
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetDeleteOldAdminsQuery();
string sql = databaseProvider.GetDeleteOldAdminsQuery();
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime() });
}
catch (Exception)

View file

@ -1,3 +1,4 @@
using System.Data.Common;
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
@ -37,20 +38,30 @@ internal class PlayerManager
return;
}
var userId = player.UserId.Value;
var slot = player.Slot;
var steamId = player.SteamID;
var playerName = !string.IsNullOrEmpty(player.PlayerName)
? player.PlayerName
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var ipAddress = player.IpAddress?.Split(":")[0];
int userId = player.UserId.Value;
int slot = player.Slot;
ulong steamId = player.SteamID;
string playerName;
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(steamId, out var renamedTo))
if (!string.IsNullOrEmpty(player.PlayerName))
{
playerName = player.PlayerName;
}
else
{
playerName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
}
string? ipAddress = player.IpAddress?.Split(":")[0];
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(steamId, out string? renamedTo))
{
player.Rename(renamedTo);
}
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null)
{
return;
}
Task.Run(async () =>
{
@ -62,14 +73,8 @@ internal class PlayerManager
await SavePlayerIpAddress(steamId, playerName, ipAddress);
// Always check bans first, regardless of PlayersInfo state
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
{
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null),
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId,
ipAddress)
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress)
};
bool isBanned = IsBanned(CS2_SimpleAdmin.Instance.CacheManager, CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType,
CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp, playerName, steamId, ipAddress);
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Player {playerName} ({steamId}) IP: {ipAddress} - BanType: {CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType} - CheckMultiAccounts: {CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp} - isBanned: {isBanned}");
@ -87,7 +92,7 @@ internal class PlayerManager
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
{
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
PlayerInfo playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null)
@ -109,8 +114,8 @@ internal class PlayerManager
if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(steamId, out PlayerInfo? value)) // Temp skip
{
var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false);
var (totalMutes, totalGags, totalSilences) =
List<dynamic> warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false);
(int totalMutes, int totalGags, int totalSilences) =
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(value);
value.TotalBans = CS2_SimpleAdmin.Instance.CacheManager
?.GetPlayerBansBySteamId(value.SteamId.SteamId64)
@ -120,13 +125,13 @@ internal class PlayerManager
value.TotalSilences = totalSilences;
value.TotalWarns = warns.Count;
var activeMutes =
List<dynamic> activeMutes =
await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(value.SteamId.SteamId64
.ToString());
if (activeMutes.Count > 0)
{
foreach (var mute in activeMutes)
foreach (dynamic mute in activeMutes)
{
string muteType = mute.type;
DateTime ends = mute.ends;
@ -171,7 +176,7 @@ internal class PlayerManager
{
await Server.NextWorldUpdateAsync(() =>
{
foreach (var admin in Helper.GetValidPlayers()
foreach (CCSPlayerController? admin in Helper.GetValidPlayers()
.Where(p => (AdminManager.PlayerHasPermissions(
new SteamID(p.SteamID),
"@css/kick") ||
@ -182,7 +187,11 @@ internal class PlayerManager
!CS2_SimpleAdmin.AdminDisabledJoinComms
.Contains(p.SteamID)))
{
if (CS2_SimpleAdmin._localizer == null || admin == player) continue;
if (CS2_SimpleAdmin._localizer == null || admin == player)
{
continue;
}
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
"sa_admin_penalty_info",
player.PlayerName,
@ -195,10 +204,10 @@ internal class PlayerManager
if (CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.Count >= 2)
{
var associatedAcccountsChunks =
List<IEnumerable<(ulong SteamId, string PlayerName)>> associatedAcccountsChunks =
CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.ChunkBy(5)
.ToList();
foreach (var chunk in associatedAcccountsChunks)
foreach (IEnumerable<(ulong SteamId, string PlayerName)> chunk in associatedAcccountsChunks)
{
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
"sa_admin_associated_accounts",
@ -235,17 +244,22 @@ internal class PlayerManager
/// <param name="type">Optional filter: bans, gags, mutes, silences or warns.</param>
public async Task<List<dynamic>> GetPenaltyHistory(ulong steamId, string? type = null)
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return [];
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return [];
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
var sql = CS2_SimpleAdmin.DatabaseProvider.GetPenaltyHistoryQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var rows = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId })).ToList();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
string sql = CS2_SimpleAdmin.DatabaseProvider.GetPenaltyHistoryQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
List<dynamic> rows = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId })).ToList();
// Filter word is the plural (bans, gags, mutes, silences, warns); rows carry BAN/GAG/MUTE/SILENCE/WARN
if (type != null)
{
rows.RemoveAll(r => !type.Equals((string)r.type + "s", StringComparison.OrdinalIgnoreCase));
}
return rows;
}
@ -263,7 +277,7 @@ internal class PlayerManager
value switch
{
DateTime d => d,
string s when DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) => parsed,
string s when DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsed) => parsed,
_ => null
};
@ -272,17 +286,22 @@ internal class PlayerManager
/// </summary>
public async Task LoadRenamedPlayers()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
var rows = await connection.QueryAsync<(long steamId, string name)>(CS2_SimpleAdmin.DatabaseProvider.GetRenamesQuery());
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<(long steamId, string name)> rows = await connection.QueryAsync<(long steamId, string name)>(CS2_SimpleAdmin.DatabaseProvider.GetRenamesQuery());
await Server.NextWorldUpdateAsync(() =>
{
foreach (var (steamId, name) in rows)
foreach ((long steamId, string? name) in rows)
{
CS2_SimpleAdmin.RenamedPlayers[(ulong)steamId] = name;
}
});
}
catch (Exception ex)
@ -298,15 +317,22 @@ internal class PlayerManager
/// <param name="name">Forced name, or null/empty to remove the rename.</param>
public async Task SaveRenamedPlayer(ulong steamId, string? name)
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
if (string.IsNullOrEmpty(name))
{
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetDeleteRenameQuery(), new { steamId });
}
else
{
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetUpsertRenameQuery(), new { steamId, name });
}
}
catch (Exception ex)
{
@ -321,16 +347,18 @@ internal class PlayerManager
private async Task SavePlayerIpAddress(ulong steamId, string playerName, string? ipAddress)
{
if (!_config.OtherSettings.CheckMultiAccountsByIp || ipAddress == null || CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
var steamId64 = steamId;
var ipUint = IpHelper.IpToUint(ipAddress);
ulong steamId64 = steamId;
uint ipUint = IpHelper.IpToUint(ipAddress);
var upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
string upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
await connection.ExecuteAsync(upsertQuery, new
{
@ -365,40 +393,46 @@ internal class PlayerManager
{
CS2_SimpleAdmin.Instance.AddTimer(5f, () =>
{
foreach (var (steamid, name) in CS2_SimpleAdmin.RenamedPlayers)
foreach ((ulong steamid, string? name) in CS2_SimpleAdmin.RenamedPlayers)
{
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player == null || !player.IsValid || player.PlayerName == name) continue;
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid);
if (player == null || !player.IsValid || player.PlayerName == name)
{
continue;
}
player.Rename(name);
}
}, TimerFlags.REPEAT);
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
{
#if DEBUG
CS2_SimpleAdmin._logger?.LogCritical("[OnMapStart] Expired check");
#endif
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
// Optimization: Get players once and avoid allocating anonymous types
var validPlayers = Helper.GetValidPlayers();
List<CCSPlayerController> validPlayers = Helper.GetValidPlayers();
// Use ValueTuple instead of anonymous type - better performance and less allocations
var tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
foreach (var p in validPlayers)
List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)> tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
foreach (CCSPlayerController p in validPlayers)
{
tempPlayers.Add((p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot));
}
var pluginInstance = CS2_SimpleAdmin.Instance;
var config = _config.OtherSettings; // Cache config access
CS2_SimpleAdmin pluginInstance = CS2_SimpleAdmin.Instance;
OtherSettings config = _config.OtherSettings; // Cache config access
_ = Task.Run(async () =>
{
try
{
// Run all expire tasks in parallel
var expireTasks = new[]
Task[] expireTasks = new[]
{
pluginInstance.BanManager.ExpireOldBans(),
pluginInstance.MuteManager.ExpireOldMutes(),
@ -415,7 +449,7 @@ internal class PlayerManager
if (ex is AggregateException aggregate)
{
foreach (var inner in aggregate.InnerExceptions)
foreach (Exception inner in aggregate.InnerExceptions)
{
CS2_SimpleAdmin._logger?.LogError($"Inner exception: {inner.Message}");
}
@ -423,28 +457,24 @@ internal class PlayerManager
}
if (pluginInstance.CacheManager == null)
{
return;
}
// Optimization: Cache ban type and multi-account check to avoid repeated config access
var banType = config.BanType;
var checkMultiAccounts = config.CheckMultiAccountsByIp;
int banType = config.BanType;
bool checkMultiAccounts = config.CheckMultiAccountsByIp;
var bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>();
List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)> bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>();
// Manual loop instead of LINQ - better performance
foreach (var player in tempPlayers)
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in tempPlayers)
{
var playerName = player.PlayerName;
var steamId = player.SteamID;
var ip = player.IpAddress?.Split(':')[0];
string playerName = player.PlayerName;
ulong steamId = player.SteamID;
string? ip = player.IpAddress?.Split(':')[0];
bool isBanned = banType switch
{
0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null),
_ => checkMultiAccounts
? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip)
: pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip)
};
bool isBanned = IsBanned(pluginInstance.CacheManager, banType, checkMultiAccounts, playerName, steamId, ip);
if (isBanned)
{
@ -454,23 +484,29 @@ internal class PlayerManager
if (bannedPlayers.Count > 0)
{
foreach (var player in bannedPlayers)
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in bannedPlayers)
{
if (!player.UserId.HasValue) continue;
if (!player.UserId.HasValue)
{
continue;
}
await Server.NextWorldUpdateAsync(() =>
{
if (Helper.GetPlayerFromSteamid64(player.SteamID) != null)
{
Helper.KickPlayer((int)player.UserId,
NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
}
});
}
}
if (config.TimeMode == 0)
{
// Optimization: Manual projection instead of LINQ
var onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count);
foreach (var player in tempPlayers)
List<(ulong, int?, int)> onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count);
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in tempPlayers)
{
onlinePlayers.Add((player.SteamID, player.UserId, player.Slot));
}
@ -485,14 +521,16 @@ internal class PlayerManager
try
{
// Optimization: Process penalties without LINQ allocations
var players = Helper.GetValidPlayers();
foreach (var player in players)
List<CCSPlayerController> players = Helper.GetValidPlayers();
foreach (CCSPlayerController player in players)
{
if (!PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
{
continue;
}
var isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
var isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
bool isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
bool isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
// Only reset voice flags if not muted or silenced
if (!isMuted && !isSilenced)
@ -509,4 +547,22 @@ internal class PlayerManager
}
}, TimerFlags.REPEAT);
}
}
/// <summary>
/// BanType 0 checks the SteamID only; otherwise the IP too, and every IP the player used when multi-account checks are on.
/// </summary>
private static bool IsBanned(CacheManager cacheManager, int banType, bool checkMultiAccounts, string playerName, ulong steamId, string? ipAddress)
{
if (banType == 0)
{
return cacheManager.IsPlayerBanned(playerName, steamId, null);
}
if (checkMultiAccounts)
{
return cacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ipAddress);
}
return cacheManager.IsPlayerBanned(playerName, steamId, ipAddress);
}
}

View file

@ -1,5 +1,5 @@
using CS2_SimpleAdminApi;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using CS2_SimpleAdminApi;
namespace CS2_SimpleAdmin.Managers;
@ -20,7 +20,7 @@ public static class PlayerPenaltyManager
Penalties.AddOrUpdate(slot,
(_) =>
{
var dict = new Dictionary<PenaltyType, List<(DateTime, int, bool)>>
Dictionary<PenaltyType, List<(DateTime, int, bool)>> dict = new Dictionary<PenaltyType, List<(DateTime, int, bool)>>
{
[penaltyType] = [(endDateTime, durationInMinutes, false)]
};
@ -28,7 +28,7 @@ public static class PlayerPenaltyManager
},
(_, existingDict) =>
{
if (!existingDict.TryGetValue(penaltyType, out var value))
if (!existingDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? value))
{
value = new List<(DateTime, int, bool)>();
existingDict[penaltyType] = value;
@ -50,21 +50,27 @@ public static class PlayerPenaltyManager
{
endDateTime = null;
if (!Penalties.TryGetValue(slot, out var penaltyDict) ||
!penaltyDict.TryGetValue(penaltyType, out var penaltiesList)) return false;
if (!Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) ||
!penaltyDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
{
return false;
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
{
if (penaltiesList.Count == 0) return false;
if (penaltiesList.Count == 0)
{
return false;
}
endDateTime = penaltiesList.First().EndDateTime;
return true;
}
var now = Time.ActualDateTime();
DateTime now = Time.ActualDateTime();
// Check if any active penalties exist
foreach (var penalty in penaltiesList.ToList())
foreach ((DateTime EndDateTime, int Duration, bool Passed) penalty in penaltiesList.ToList())
{
// Check if the penalty is still active
if (penalty.Duration > 0 && now >= penalty.EndDateTime)
@ -95,14 +101,14 @@ public static class PlayerPenaltyManager
/// <returns>A list of penalties if found, otherwise an empty list.</returns>
public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, PenaltyType penaltyType)
{
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
penaltyDict.TryGetValue(penaltyType, out var penaltiesList))
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) &&
penaltyDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
{
return penaltiesList;
}
return [];
}
/// <summary>
/// Retrieves all penalties for a player across multiple penalty types.
/// </summary>
@ -113,11 +119,11 @@ public static class PlayerPenaltyManager
{
List<(DateTime EndDateTime, int Duration, bool Passed)> result = [];
if (Penalties.TryGetValue(slot, out var penaltyDict))
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
{
foreach (var type in penaltyType)
foreach (PenaltyType type in penaltyType)
{
if (penaltyDict.TryGetValue(type, out var penaltiesList))
if (penaltyDict.TryGetValue(type, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
{
result.AddRange(penaltiesList);
}
@ -126,7 +132,7 @@ public static class PlayerPenaltyManager
return result;
}
/// <summary>
/// Retrieves all penalties for a player across all penalty types.
/// </summary>
@ -134,12 +140,12 @@ public static class PlayerPenaltyManager
/// <returns>A dictionary with penalty types as keys and lists of penalties as values.</returns>
public static Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetAllPlayerPenalties(int slot)
{
// Check if the player has any penalties in the dictionary
return Penalties.TryGetValue(slot, out var penaltyDict) ?
// Return all penalty types and their respective penalties for the player
penaltyDict :
// If the player has no penalties, return an empty dictionary
new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
{
return penaltyDict;
}
return new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
}
/// <summary>
@ -179,7 +185,7 @@ public static class PlayerPenaltyManager
/// <param name="penaltyType">The penalty type to remove.</param>
public static void RemovePenaltiesByType(int slot, PenaltyType penaltyType)
{
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) &&
penaltyDict.ContainsKey(penaltyType))
{
penaltyDict.Remove(penaltyType);
@ -193,15 +199,21 @@ public static class PlayerPenaltyManager
/// <param name="dateTime">The end datetime of penalties to mark as passed.</param>
public static void RemovePenaltiesByDateTime(int slot, DateTime dateTime)
{
if (!Penalties.TryGetValue(slot, out var penaltyDict)) return;
foreach (var penaltiesList in penaltyDict.Values)
if (!Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
{
for (var i = 0; i < penaltiesList.Count; i++)
return;
}
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)> penaltiesList in penaltyDict.Values)
{
for (int i = 0; i < penaltiesList.Count; i++)
{
if (penaltiesList[i].EndDateTime != dateTime) continue;
if (penaltiesList[i].EndDateTime != dateTime)
{
continue;
}
// Create a copy of the penalty
var penalty = penaltiesList[i];
(DateTime EndDateTime, int Duration, bool Passed) penalty = penaltiesList[i];
// Update the end datetime of the copied penalty to the current datetime
penalty.Passed = true;
@ -223,10 +235,10 @@ public static class PlayerPenaltyManager
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
{
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
foreach ((int playerSlot, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
{
// Remove expired penalties for the player
foreach (var penaltiesList in penaltyDict.Values)
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList in penaltyDict.Values)
{
penaltiesList.RemoveAll(p => p is { Duration: > 0, Passed: true });
}
@ -241,10 +253,10 @@ public static class PlayerPenaltyManager
return;
}
var now = Time.ActualDateTime();
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
DateTime now = Time.ActualDateTime();
foreach ((int playerSlot, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
{
foreach (var penaltiesList in penaltyDict.Values)
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList in penaltyDict.Values)
{
penaltiesList.RemoveAll(p => p.Duration > 0 && now >= p.EndDateTime);
}
@ -255,4 +267,4 @@ public static class PlayerPenaltyManager
}
}
}
}
}

View file

@ -1,4 +1,4 @@
using CounterStrikeSharp.API;
using System.Data.Common;
using CounterStrikeSharp.API.Modules.Cvars;
using Dapper;
using Microsoft.Extensions.Logging;
@ -15,13 +15,15 @@ public class ServerManager
/// </summary>
public static void CheckHibernationStatus()
{
var convar = ConVar.Find("sv_hibernate_when_empty");
ConVar? convar = ConVar.Find("sv_hibernate_when_empty");
if (convar == null || !convar.GetPrimitiveValue<bool>())
{
return;
}
CS2_SimpleAdmin._logger?.LogError("Detected setting \"sv_hibernate_when_empty true\", set false to make plugin work properly");
}
/// <summary>
/// Initiates the asynchronous process to load server data such as IP address, port, hostname, and RCON password.
/// Handles retry attempts if IP address is not immediately available.
@ -33,11 +35,14 @@ public class ServerManager
{
CS2_SimpleAdmin.Instance.AddTimer(1.0f, () =>
{
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return;
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
// Optimization: Get server IP once and reuse
var serverIp = Helper.GetServerIp();
var isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0");
string serverIp = Helper.GetServerIp();
bool isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0");
// Check if we've exceeded retry limit with invalid IP
if (_getIpTryCount > 32 && isInvalidIp)
@ -49,8 +54,8 @@ public class ServerManager
}
// Optimization: Cache ConVar lookups
var ipConVar = ConVar.Find("ip");
var ipAddress = ipConVar?.StringValue;
ConVar? ipConVar = ConVar.Find("ip");
string? ipAddress = ipConVar?.StringValue;
// Use Helper IP if ConVar IP is invalid
if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))
@ -67,20 +72,20 @@ public class ServerManager
}
// Optimization: Cache remaining ConVar lookups
var hostportConVar = ConVar.Find("hostport");
var hostnameConVar = ConVar.Find("hostname");
var rconPasswordConVar = ConVar.Find("rcon_password");
ConVar? hostportConVar = ConVar.Find("hostport");
ConVar? hostnameConVar = ConVar.Find("hostname");
ConVar? rconPasswordConVar = ConVar.Find("rcon_password");
var address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue<int>()}";
var hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var rconPassword = rconPasswordConVar?.StringValue ?? "";
string address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue<int>()}";
string hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
string rconPassword = rconPasswordConVar?.StringValue ?? "";
CS2_SimpleAdmin.IpAddress = address;
Task.Run(async () =>
{
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
int? serverId = await connection.ExecuteScalarAsync<int?>(
"SELECT id FROM sa_servers WHERE address = @address",
new { address });
@ -126,8 +131,8 @@ public class ServerManager
if (CS2_SimpleAdmin.Instance.Config.EnableMetrics)
{
var queryString = $"?address={address}&hostname={hostname}";
var client = CS2_SimpleAdmin.HttpClient;
string queryString = $"?address={address}&hostname={hostname}";
HttpClient client = CS2_SimpleAdmin.HttpClient;
try
{
@ -139,8 +144,8 @@ public class ServerManager
}
}
});
CS2_SimpleAdmin.SimpleAdminApi?.OnSimpleAdminReadyEvent();
});
}
}
}

View file

@ -1,4 +1,5 @@
using CS2_SimpleAdmin.Database;
using System.Data.Common;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
@ -17,17 +18,20 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
public async Task<int?> WarnPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddWarnQuery(true);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddWarnQuery(true);
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
int? warnId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
@ -42,7 +46,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
return warnId;
}
catch(Exception)
catch (Exception)
{
return null;
}
@ -58,17 +62,20 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
public async Task<int?> AddWarnBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddWarnQuery(false);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddWarnQuery(false);
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
int? warnId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
@ -96,15 +103,18 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>A list of dynamic objects representing warnings, or an empty list if none found or on failure.</returns>
public async Task<List<dynamic>> GetPlayerWarns(PlayerInfo player, bool active = true)
{
if (databaseProvider == null) return [];
if (databaseProvider == null)
{
return [];
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
string sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
var parameters = new { PlayerSteamID = player.SteamId.SteamId64, serverid = CS2_SimpleAdmin.ServerId };
var warns = await connection.QueryAsync<dynamic>(sql, parameters);
IEnumerable<dynamic> warns = await connection.QueryAsync<dynamic>(sql, parameters);
return warns.ToList();
}
@ -122,14 +132,17 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>The count of warnings as an integer, or 0 if none found or on failure.</returns>
public async Task<int> GetPlayerWarnsCount(ulong steamId, bool active = true)
{
if (databaseProvider == null) return 0;
if (databaseProvider == null)
{
return 0;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
var warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
string sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
int warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
return warnsCount;
}
catch (Exception)
@ -146,13 +159,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnwarnPlayer(PlayerInfo player, int warnId)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
string sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64, warnId, serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -160,7 +176,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
CS2_SimpleAdmin._logger?.LogCritical($"Unable to remove warn + {ex}");
}
}
/// <summary>
/// Removes the most recent warning matching a player pattern (usually SteamID string).
/// </summary>
@ -168,13 +184,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnwarnPlayer(string playerPattern)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
string sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { steamid = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -189,13 +208,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>A task representing the asynchronous operation.</returns>
public async Task ExpireOldWarns()
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
string sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -203,4 +225,4 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
CS2_SimpleAdmin._logger?.LogCritical($"Unable to remove expired warns + {ex}");
}
}
}
}