upstream to 1.7.7-alpha-10

This commit is contained in:
Sachin 2025-10-03 17:18:24 +05:30
commit 2de2924cef
106 changed files with 5897 additions and 3306 deletions

View file

@ -5,36 +5,37 @@ using Dapper;
using Microsoft.Extensions.Logging;
using MySqlConnector;
using System.Text;
using CS2_SimpleAdmin.Database;
namespace CS2_SimpleAdmin.Managers;
internal class BanManager(Database.Database? database)
internal class BanManager(IDatabaseProvider? databaseProvider)
{
/// <summary>
/// Bans an online player and inserts the ban record into the database.
/// </summary>
/// <param name="player">The player to be banned (must be currently online).</param>
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
/// <param name="reason">The reason for the ban.</param>
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
/// <returns>The newly created ban ID if successful, otherwise null.</returns>
public async Task<int?> BanPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
{
if (database == null) return null;
if (databaseProvider == null) return null;
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
await using MySqlConnection connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
try
{
const string sql = """
INSERT INTO `sa_bans`
(`player_steamid`, `player_name`, `player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @playerName, @playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
""";
var sql = databaseProvider.GetAddBanQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64.ToString(),
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
playerIp = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1 ? player.IpAddress : null,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
banReason = reason,
duration = time,
@ -45,37 +46,36 @@ internal class BanManager(Database.Database? database)
return banId;
}
catch
catch(Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
return null;
}
}
public async Task<int?> AddBanBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
/// <summary>
/// Adds a ban for an offline player identified by their SteamID.
/// </summary>
/// <param name="playerSteamId">The SteamID64 of the player to ban.</param>
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
/// <param name="reason">The reason for the ban.</param>
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
/// <returns>The ID of the newly created ban if successful, otherwise null.</returns>
public async Task<int?> AddBanBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
{
if (database == null) return null;
if (string.IsNullOrEmpty(playerSteamId)) return null;
if (databaseProvider == null) return null;
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using MySqlConnection connection = await database.GetConnectionAsync();
const string sql = """
INSERT INTO `sa_bans`
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
""";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddBanBySteamIdQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
banReason = reason,
duration = time,
@ -86,15 +86,23 @@ internal class BanManager(Database.Database? database)
return banId;
}
catch (Exception)
catch(Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
return null;
}
}
/// <summary>
/// Adds a ban for an offline player identified by their IP address.
/// </summary>
/// <param name="playerIp">The IP address of the player to ban.</param>
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
/// <param name="reason">The reason for the ban.</param>
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
public async Task AddBanByIp(string playerIp, PlayerInfo? issuer, string reason, int time = 0)
{
if (database == null) return;
if (databaseProvider == null) return;
if (string.IsNullOrEmpty(playerIp)) return;
@ -103,15 +111,13 @@ internal class BanManager(Database.Database? database)
try
{
await using MySqlConnection connection = await database.GetConnectionAsync();
var sql = "INSERT INTO `sa_bans` (`player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) " +
"VALUES (@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid)";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddBanByIpQuery();
await connection.ExecuteAsync(sql, new
{
playerIp,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
banReason = reason,
duration = time,
@ -123,370 +129,308 @@ internal class BanManager(Database.Database? database)
catch { }
}
public async Task<bool> IsPlayerBanned(PlayerInfo player)
// public async Task<bool> IsPlayerBanned(PlayerInfo player)
// {
// if (database == null) return false;
//
// if (player.IpAddress == null)
// {
// return false;
// }
//
// #if DEBUG
// if (CS2_SimpleAdmin._logger != null)
// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
// #endif
//
// int banCount;
//
// DateTime currentTime = Time.ActualDateTime();
//
// try
// {
// string sql;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// )
// ), 0) AS TotalBanCount;
// """ : """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// )
// ), 0) AS TotalBanCount;
// """;
// }
// else
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
// """ : """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
// """;
// }
//
// await using var connection = await database.GetConnectionAsync();
//
// var parameters = new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
// string.IsNullOrEmpty(player.IpAddress) ||
// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
// ? null
// : player.IpAddress,
// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
// CurrentTime = currentTime,
// CS2_SimpleAdmin.ServerId
// };
//
// banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
// }
// catch (Exception ex)
// {
// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
// player.Name, ex.Message);
// return false;
// }
//
// return banCount > 0;
// }
//
// public async Task<int> GetPlayerBans(PlayerInfo player)
// {
// if (database == null) return 0;
//
// try
// {
// string sql;
//
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
//
// int banCount;
//
// await using var connection = await database.GetConnectionAsync();
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = player.IpAddress,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
// else
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = DBNull.Value,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
//
// return banCount;
// }
// catch { }
//
// return 0;
// }
/// <summary>
/// Unbans a player based on a pattern match of SteamID or IP address.
/// </summary>
/// <param name="playerPattern">Pattern to match against player identifiers (e.g., partial SteamID).</param>
/// <param name="adminSteamId">SteamID64 of the admin performing the unban.</param>
/// <param name="reason">Optional reason for the unban. If null or empty, the unban reason is not stored.</param>
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
{
if (databaseProvider == null) return;
if (playerPattern is not { Length: > 1 })
{
if (database == null) return false;
if (player.IpAddress == null)
{
return false;
}
#if DEBUG
if (CS2_SimpleAdmin._logger != null)
CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
#endif
int banCount;
DateTime currentTime = Time.ActualDateTime();
try
{
string sql;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
{
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
SELECT COALESCE((
SELECT COUNT(*)
FROM sa_bans
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
), 0)
+
COALESCE((
SELECT COUNT(*)
FROM sa_bans
JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
WHERE sa_bans.status = 'ACTIVE'
AND sa_players_ips.address = @PlayerIP
AND NOT EXISTS (
SELECT 1
FROM sa_bans
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
)
), 0) AS TotalBanCount;
""" : """
SELECT COALESCE((
SELECT COUNT(*)
FROM sa_bans
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
AND server_id = @ServerId
), 0)
+
COALESCE((
SELECT COUNT(*)
FROM sa_bans
JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
WHERE sa_bans.status = 'ACTIVE'
AND sa_players_ips.address = @PlayerIP
AND NOT EXISTS (
SELECT 1
FROM sa_bans
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
AND server_id = @ServerId
)
), 0) AS TotalBanCount;
""";
}
else
{
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
UPDATE sa_bans
SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime);
SELECT COUNT(*) FROM sa_bans
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime);
""" : """
UPDATE sa_bans
SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
SELECT COUNT(*) FROM sa_bans
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
""";
}
await using var connection = await database.GetConnectionAsync();
var parameters = new
{
PlayerSteamID = player.SteamId.SteamId64.ToString(),
PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
string.IsNullOrEmpty(player.IpAddress) ||
CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
? null
: player.IpAddress,
PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
CurrentTime = currentTime,
CS2_SimpleAdmin.ServerId
};
banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
player.Name, ex.Message);
return false;
}
return banCount > 0;
return;
}
public async Task<int> GetPlayerBans(PlayerInfo player)
try
{
if (database == null) return 0;
await using var connection = await databaseProvider.CreateConnectionAsync();
var sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
try
{
string sql;
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
: "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
int banCount;
await using var connection = await database.GetConnectionAsync();
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
{
banCount = await connection.ExecuteScalarAsync<int>(sql,
new
{
PlayerSteamID = player.SteamId.SteamId64.ToString(),
PlayerIP = player.IpAddress,
serverid = CS2_SimpleAdmin.ServerId
});
}
else
{
banCount = await connection.ExecuteScalarAsync<int>(sql,
new
{
PlayerSteamID = player.SteamId.SteamId64.ToString(),
PlayerIP = DBNull.Value,
serverid = CS2_SimpleAdmin.ServerId
});
}
return banCount;
}
catch { }
return 0;
}
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
{
if (database == null) return;
if (playerPattern is not { Length: > 1 })
{
var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
var bansList = bans as dynamic[] ?? bans.ToArray();
if (bansList.Length == 0)
return;
}
try
var sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
var adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
foreach (var ban in bansList)
{
await using var connection = await database.GetConnectionAsync();
int banId = ban.id;
var sqlRetrieveBans = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
: "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
var sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
var unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
var bansList = bans as dynamic[] ?? bans.ToArray();
if (bansList.Length == 0)
return;
const string sqlAdmin = "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
var sqlInsertUnban = "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT LAST_INSERT_ID();";
var sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
var adminId = sqlAdminId ?? 0;
foreach (var ban in bansList)
{
int banId = ban.id;
int? unbanId;
if (reason != null)
{
unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
}
else
{
sqlInsertUnban = "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT LAST_INSERT_ID();";
unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId });
}
const string sqlUpdateBan = "UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId";
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
}
}
catch { }
}
public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
{
if (database == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
bool checkIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0;
var filteredPlayers = players.Where(p => p.UserId.HasValue).ToList();
var steamIds = filteredPlayers.Select(p => p.SteamID).Distinct().ToList();
var ipAddresses = filteredPlayers
.Where(p => !string.IsNullOrEmpty(p.IpAddress))
.Select(p => p.IpAddress)
.Distinct()
.ToList();
var sql = new StringBuilder();
sql.Append("SELECT `player_steamid`, `player_ip` FROM `sa_bans` WHERE `status` = 'ACTIVE' ");
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
sql.Append("AND (player_steamid IN @SteamIDs ");
if (checkIpBans && ipAddresses.Count != 0)
{
sql.Append("OR player_ip IN @IpAddresses");
}
sql.Append(')');
}
else
{
sql.Append("AND server_id = @ServerId AND (player_steamid IN @SteamIDs ");
if (checkIpBans && ipAddresses.Count != 0)
{
sql.Append("OR player_ip IN @IpAddresses");
}
sql.Append(')');
}
var bannedPlayers = await connection.QueryAsync<(ulong PlayerSteamID, string PlayerIP)>(
sql.ToString(),
new
{
SteamIDs = steamIds,
IpAddresses = checkIpBans ? ipAddresses : [],
CS2_SimpleAdmin.ServerId
});
var valueTuples = bannedPlayers.ToList();
var bannedSteamIds = valueTuples.Select(b => b.PlayerSteamID).ToHashSet();
var bannedIps = valueTuples.Select(b => b.PlayerIP).ToHashSet();
foreach (var player in filteredPlayers.Where(player => bannedSteamIds.Contains(player.SteamID) ||
(checkIpBans && bannedIps.Contains(player.IpAddress ?? ""))))
{
if (!player.UserId.HasValue || CS2_SimpleAdmin.PlayersInfo[player.UserId.Value].WaitingForKick) continue;
await Server.NextFrameAsync(() =>
{
Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED);
});
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}");
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
}
}
catch { }
}
// public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
// {
// if (database == null) return;
//
// try
// {
// await using var connection = await database.GetConnectionAsync();
// bool checkIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0;
//
// var filteredPlayers = players.Where(p => p.UserId.HasValue).ToList();
//
// var steamIds = filteredPlayers.Select(p => p.SteamID).Distinct().ToList();
// var ipAddresses = filteredPlayers
// .Where(p => !string.IsNullOrEmpty(p.IpAddress))
// .Select(p => p.IpAddress)
// .Distinct()
// .ToList();
//
// var sql = new StringBuilder();
// sql.Append("SELECT `player_steamid`, `player_ip` FROM `sa_bans` WHERE `status` = 'ACTIVE' ");
//
// if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
// {
// sql.Append("AND (player_steamid IN @SteamIDs ");
// if (checkIpBans && ipAddresses.Count != 0)
// {
// sql.Append("OR player_ip IN @IpAddresses");
// }
// sql.Append(')');
// }
// else
// {
// sql.Append("AND server_id = @ServerId AND (player_steamid IN @SteamIDs ");
// if (checkIpBans && ipAddresses.Count != 0)
// {
// sql.Append("OR player_ip IN @IpAddresses");
// }
// sql.Append(')');
// }
//
// var bannedPlayers = await connection.QueryAsync<(ulong PlayerSteamID, string PlayerIP)>(
// sql.ToString(),
// new
// {
// SteamIDs = steamIds,
// IpAddresses = checkIpBans ? ipAddresses : [],
// CS2_SimpleAdmin.ServerId
// });
//
// var valueTuples = bannedPlayers.ToList();
// var bannedSteamIds = valueTuples.Select(b => b.PlayerSteamID).ToHashSet();
// var bannedIps = valueTuples.Select(b => b.PlayerIP).ToHashSet();
//
// foreach (var player in filteredPlayers.Where(player => bannedSteamIds.Contains(player.SteamID) ||
// (checkIpBans && bannedIps.Contains(player.IpAddress ?? ""))))
// {
// if (!player.UserId.HasValue || CS2_SimpleAdmin.PlayersInfo[player.SteamID].WaitingForKick) continue;
//
// await Server.NextWorldUpdateAsync(() =>
// {
// Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED);
// });
// }
// }
// catch (Exception ex)
// {
// CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}");
// }
// }
/// <summary>
/// Expires all bans that have passed their end time, including optional cleanup of old IP bans.
/// </summary>
public async Task ExpireOldBans()
{
if (database == null) return;
if (databaseProvider == null) return;
var currentTime = Time.ActualDateTime();
try
{
await using var connection = await database.GetConnectionAsync();
/*
string sql = "";
await using MySqlConnection connection = await _database.GetConnectionAsync();
sql = "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime";
await connection.ExecuteAsync(sql, new { CurrentTime = DateTime.UtcNow });
*/
string sql;
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
UPDATE sa_bans
SET
status = 'EXPIRED'
WHERE
status = 'ACTIVE'
AND
`duration` > 0
AND
ends <= @currentTime
""" : """
UPDATE sa_bans
SET
status = 'EXPIRED'
WHERE
status = 'ACTIVE'
AND
`duration` > 0
AND
ends <= @currentTime
AND server_id = @serverid
""";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { currentTime, serverid = CS2_SimpleAdmin.ServerId });
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans > 0)
{
var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
UPDATE sa_bans
SET
player_ip = NULL
WHERE
status = 'ACTIVE'
AND
ends <= @ipBansTime
""" : """
UPDATE sa_bans
SET
player_ip = NULL
WHERE
status = 'ACTIVE'
AND
ends <= @ipBansTime
AND server_id = @serverid
""";
sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId });
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Models;
using Dapper;
using ZLinq;
@ -8,7 +9,7 @@ namespace CS2_SimpleAdmin.Managers;
internal class CacheManager: IDisposable
{
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
private readonly ConcurrentDictionary<string, List<BanRecord>> _steamIdIndex = [];
private readonly ConcurrentDictionary<ulong, List<BanRecord>> _steamIdIndex = [];
private readonly ConcurrentDictionary<uint, List<BanRecord>> _ipIndex = [];
private readonly ConcurrentDictionary<ulong, HashSet<IpRecord>> _playerIpsCache = [];
@ -17,21 +18,26 @@ internal class CacheManager: IDisposable
private DateTime _lastUpdateTime = DateTime.MinValue;
private bool _isInitialized;
private bool _disposed;
/// <summary>
/// Initializes and builds the ban and IP cache from the database. Loads bans, player IP history, and config settings.
/// </summary>
/// <returns>Asynchronous task representing the initialization process.</returns>
public async Task InitializeCacheAsync()
{
if (CS2_SimpleAdmin.Database == null) return;
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (!CS2_SimpleAdmin.ServerLoaded) return;
if (_isInitialized) return;
try
{
Clear();
_cachedIgnoredIps = new HashSet<uint>(
CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps
.Select(IpHelper.IpToUint));
_cachedIgnoredIps = CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps
.AsValueEnumerable()
.Select(IpHelper.IpToUint)
.ToHashSet();
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
List<BanRecord> bans;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
@ -40,6 +46,7 @@ internal class CacheManager: IDisposable
"""
SELECT
id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
@ -52,6 +59,7 @@ internal class CacheManager: IDisposable
"""
SELECT
id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
@ -59,20 +67,16 @@ internal class CacheManager: IDisposable
WHERE server_id = @serverId
""", new {serverId = CS2_SimpleAdmin.ServerId})).ToList();
}
var ipHistory =
await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC");
foreach (var ban in bans)
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
_banCache.TryAdd(ban.Id, ban);
}
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
{
var ipSet = new HashSet<IpRecord>(
group
var ipHistory =
(await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC")).ToList();
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
{
var ipSet = group
.GroupBy(x => x.address)
.Select(g =>
{
@ -80,32 +84,34 @@ internal class CacheManager: IDisposable
return new IpRecord(
g.Key,
latest.used_at,
!string.IsNullOrEmpty(latest.name)
? latest.name
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
string.IsNullOrEmpty(latest.name)
? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
: latest.name
);
}),
new IpRecordComparer()
);
})
.ToHashSet(new IpRecordComparer());
_playerIpsCache.AddOrUpdate(
group.Key,
_ => ipSet,
(_, existingSet) =>
{
foreach (var ip in ipSet)
_playerIpsCache.AddOrUpdate(
group.Key,
_ => ipSet,
(_, existingSet) =>
{
existingSet.Remove(ip);
existingSet.Add(ip);
}
return existingSet;
});
foreach (var ip in ipSet)
{
existingSet.Remove(ip);
existingSet.Add(ip);
}
return existingSet;
});
}
}
foreach (var ban in bans.AsValueEnumerable())
_banCache.TryAdd(ban.Id, ban);
RebuildIndexes();
_lastUpdateTime = DateTime.Now.AddSeconds(-1);
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
_isInitialized = true;
}
catch (Exception e)
@ -114,27 +120,36 @@ internal class CacheManager: IDisposable
}
}
/// <summary>
/// Clears all cached data and reinitializes the cache from the database.
/// </summary>
/// <returns>Asynchronous task representing the reinitialization process.</returns>
public async Task ForceReInitializeCacheAsync()
{
_isInitialized = false;
_banCache.Clear();
_playerIpsCache.Clear();
_cachedIgnoredIps.Clear();
_cachedIgnoredIps = [];
_lastUpdateTime = DateTime.MinValue;
await InitializeCacheAsync();
}
/// <summary>
/// Refreshes the in-memory cache with updated or new data from the database since the last update time.
/// Also updates multi-account IP history if enabled.
/// </summary>
/// <returns>Asynchronous task representing the refresh operation.</returns>
public async Task RefreshCacheAsync()
{
if (CS2_SimpleAdmin.Database == null) return;
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (!_isInitialized) return;
try
{
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
List<BanRecord> updatedBans;
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<BanRecord> updatedBans;
var allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
@ -143,45 +158,45 @@ internal class CacheManager: IDisposable
updatedBans = (await connection.QueryAsync<BanRecord>(
"""
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
""",
new { lastUpdate = _lastUpdateTime }
)).ToList();
));
}
else
{
updatedBans = (await connection.QueryAsync<BanRecord>(
"""
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC
""",
new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId }
)).ToList();
));
}
foreach (var id in _banCache.Keys)
{
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
// Remove from steamIdIndex
if (!string.IsNullOrWhiteSpace(ban.PlayerSteamId) &&
_steamIdIndex.TryGetValue(ban.PlayerSteamId, out var steamBans))
if (ban.PlayerSteamId != null &&
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans))
{
steamBans.RemoveAll(b => b.Id == id);
if (steamBans.Count == 0)
_steamIdIndex.TryRemove(ban.PlayerSteamId, out _);
_steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _);
}
// Remove from ipIndex
if (!string.IsNullOrWhiteSpace(ban.PlayerIp) &&
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) &&
_ipIndex.TryGetValue(ipUInt, out var ipBans))
if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue;
{
ipBans.RemoveAll(b => b.Id == id);
if (ipBans.Count == 0)
@ -189,59 +204,64 @@ internal class CacheManager: IDisposable
}
}
var ipHistory = (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300", new {lastUpdate = _lastUpdateTime})).ToList();
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
var ipSet = new HashSet<IpRecord>(
group
.GroupBy(x => x.address)
.Select(g =>
{
var latest = g.MaxBy(x => x.used_at);
return new IpRecord(
g.Key,
latest.used_at,
!string.IsNullOrEmpty(latest.name)
? latest.name
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
);
}),
new IpRecordComparer()
);
var ipHistory = (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300",
new { lastUpdate = _lastUpdateTime }));
_playerIpsCache.AddOrUpdate(
group.Key,
_ => ipSet,
(_, existingSet) =>
{
foreach (var newEntry in ipSet)
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
{
var ipSet = new HashSet<IpRecord>(
group
.GroupBy(x => x.address)
.Select(g =>
{
var latest = g.MaxBy(x => x.used_at);
return new IpRecord(
g.Key,
latest.used_at,
!string.IsNullOrEmpty(latest.name)
? latest.name
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
);
}),
new IpRecordComparer()
);
_playerIpsCache.AddOrUpdate(
group.Key,
_ => ipSet,
(_, existingSet) =>
{
existingSet.Remove(newEntry);
existingSet.Add(newEntry);
}
return existingSet;
});
foreach (var newEntry in ipSet)
{
existingSet.Remove(newEntry);
existingSet.Add(newEntry);
}
return existingSet;
});
}
}
if (updatedBans.Count == 0)
return;
foreach (var ban in updatedBans)
{
_banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban);
}
RebuildIndexes();
_lastUpdateTime = DateTime.Now.AddSeconds(-1);
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
}
catch (Exception)
{
// ignored
}
}
/// <summary>
/// Rebuilds the internal indexes for fast lookup of active bans by Steam ID and IP address.
/// Clears and repopulates both indexes based on the current in-memory ban cache.
/// </summary>
private void RebuildIndexes()
{
_steamIdIndex.Clear();
@ -249,14 +269,14 @@ internal class CacheManager: IDisposable
foreach (var ban in _banCache.Values)
{
if (ban.Status != "ACTIVE")
if (ban.StatusEnum != BanStatus.ACTIVE)
continue;
if (!string.IsNullOrWhiteSpace(ban.PlayerSteamId))
if (ban.PlayerSteamId != null)
{
var steamId = ban.PlayerSteamId;
_steamIdIndex.AddOrUpdate(
steamId,
steamId.Value,
key => [ban],
(key, list) =>
{
@ -264,7 +284,9 @@ internal class CacheManager: IDisposable
return list;
});
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) continue;
if (ban.PlayerIp != null &&
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
{
@ -280,71 +302,233 @@ internal class CacheManager: IDisposable
}
}
/// <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();
public List<BanRecord> GetActiveBans() => _banCache.Values.Where(b => b.Status == "ACTIVE").ToList();
public List<BanRecord> GetPlayerBansBySteamId(string steamId) => _steamIdIndex.TryGetValue(steamId, out var bans) ? bans : [];
/// <summary>
/// Retrieves only active ban records from the cache.
/// </summary>
/// <returns>List of active <see cref="BanRecord"/> objects.</returns>
public List<BanRecord> GetActiveBans() => _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE).ToList();
/// <summary>
/// Retrieves all ban records for a specific player by their Steam ID.
/// </summary>
/// <param name="steamId">64-bit Steam ID of the player.</param>
/// <returns>List of <see cref="BanRecord"/> objects associated with the Steam ID.</returns>
public List<BanRecord> GetPlayerBansBySteamId(ulong steamId) => _steamIdIndex.TryGetValue(steamId, out var bans) ? bans : [];
/// <summary>
/// Gets all known Steam accounts that have used the specified IP address.
/// </summary>
/// <param name="ipAddress">The IP address to search for, in string format.</param>
/// <returns>
/// List of tuples containing the Steam ID, last used time, and player name for each matching entry.
/// </returns>
public List<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
{
var ipAsUint = IpHelper.IpToUint(ipAddress);
var results = new List<(ulong, DateTime, string)>();
var comparer = _playerIpsCache.Comparer;
return _playerIpsCache.AsValueEnumerable()
.SelectMany(kvp => kvp.Value
.Where(entry => entry.Ip == ipAsUint)
.Select(entry => (kvp.Key, entry.UsedAt, entry.PlayerName)))
.ToList();
foreach (var (steamId, ipSet) in _playerIpsCache)
{
if (!ipSet.TryGetValue(new IpRecord(ipAsUint, Time.ActualDateTime(), "Unknown"), out var actualEntry)) continue;
results.Add((steamId, actualEntry.UsedAt, actualEntry.PlayerName));
foreach (var entry in ipSet)
{
if (entry.Ip == ipAsUint && !Equals(entry, actualEntry))
{
results.Add((steamId, entry.UsedAt, entry.PlayerName));
}
}
}
return results;
}
// public IEnumerable<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
// {
// var ipAsUint = IpHelper.IpToUint(ipAddress);
//
// return _playerIpsCache.SelectMany(kvp => kvp.Value
// .Where(entry => entry.Ip == ipAsUint)
// .Select(entry => (kvp.Key, entry.UsedAt, entry.PlayerName)));
// }
private bool IsIpBanned(string ipAddress)
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
var ipUInt = IpHelper.IpToUint(ipAddress);
return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt);
}
public bool IsPlayerBanned(string? steamId, string? ipAddress)
// public bool IsPlayerBanned(ulong? steamId, string? ipAddress)
// {
// if (steamId != null && _steamIdIndex.ContainsKey(steamId.Value))
// return true;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
// return false;
//
// if (string.IsNullOrEmpty(ipAddress) || !IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt))
// return false;
//
// return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt);
// }
/// <summary>
/// Checks if a player is currently banned by Steam ID or IP address.
/// If a partial ban record is found, updates it with the latest player information.
/// </summary>
/// <param name="playerName">Name of the player attempting to connect.</param>
/// <param name="steamId">Optional 64-bit Steam ID of the player.</param>
/// <param name="ipAddress">Optional IP address of the player.</param>
/// <returns>True if the player is banned, otherwise false.</returns>
public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress)
{
if (steamId != null && _steamIdIndex.ContainsKey(steamId))
return true;
BanRecord? record;
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
{
record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record != null)
{
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue))
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
}
return true;
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
if (ipAddress == null)
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
return false;
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt))
return false;
return !_cachedIgnoredIps.Contains(ipUInt) &&
_ipIndex.ContainsKey(ipUInt);
if (string.IsNullOrEmpty(ipAddress) ||
!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) ||
_cachedIgnoredIps.Contains(ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false;
record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record == null) return false;
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue && steamId.HasValue))
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
}
return true;
}
public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress)
// public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress)
// {
// if (_steamIdIndex.ContainsKey(steamId))
// return true;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
// return false;
//
// if (!_playerIpsCache.TryGetValue(steamId, out var ipData))
// return false;
//
// // var now = Time.ActualDateTime();
// var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
// var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
//
// if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint))
// {
// if (!_cachedIgnoredIps.Contains(ipAsUint))
// {
// ipData.Add(new IpRecord(
// ipAsUint,
// Time.ActualDateTime().AddSeconds(-2),
// unknownName
// ));
// }
// }
//
// // foreach (var ipRecord in ipData)
// // {
// // // Skip if too old or in ignored list
// // if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
// // continue;
// //
// // // Check if IP is banned
// // if (_ipIndex.ContainsKey(ipRecord.Ip))
// // return true;
// // }
//
// foreach (var ipRecord in ipData)
// {
// if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
// continue;
//
// if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords)) continue;
//
// var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
// if (activeBan == null) continue;
//
// if (!string.IsNullOrEmpty(activeBan.PlayerName) && activeBan.PlayerSteamId.HasValue) return true;
//
// _ = Task.Run(() => UpdatePlayerData(
// activeBan.PlayerName,
// steamId,
// ipAddress
// ));
//
// if (string.IsNullOrEmpty(activeBan.PlayerName) && !string.IsNullOrEmpty(unknownName))
// activeBan.PlayerName = unknownName;
//
// activeBan.PlayerSteamId ??= steamId;
//
// return true;
// }
//
// return false;
// }
/// <summary>
/// Checks if the player or any IP previously associated with them is currently banned.
/// Also updates ban records with missing player info if found.
/// </summary>
/// <param name="playerName">Current player name.</param>
/// <param name="steamId">64-bit Steam ID of the player.</param>
/// <param name="ipAddress">Current IP address of the player (optional).</param>
/// <returns>True if the player or their known IPs are banned, otherwise false.</returns>
public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress)
{
var steamIdStr = steamId.ToString();
if (_steamIdIndex.ContainsKey(steamIdStr))
return true;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
if (_steamIdIndex.TryGetValue(steamId, out var steamBans))
{
var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan != null)
{
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp))
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
return false;
if (!_playerIpsCache.TryGetValue(steamId, out var ipData))
return false;
var now = DateTime.Now;
var cutoff = now.AddDays(-7);
var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
if (ipAddress != null)
if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint))
{
var ipAsUint = IpHelper.IpToUint(ipAddress);
if (!_cachedIgnoredIps.Contains(ipAsUint))
{
ipData.Add(new IpRecord(
ipAsUint,
now.AddSeconds(-2), // artificially recent
CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
));
ipData.Add(new IpRecord(ipAsUint, Time.ActualDateTime().AddSeconds(-2), unknownName));
}
}
@ -353,20 +537,122 @@ internal class CacheManager: IDisposable
if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
continue;
if (_ipIndex.ContainsKey(ipRecord.Ip))
return true;
if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords))
continue;
var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null)
continue;
if (string.IsNullOrEmpty(activeBan.PlayerName))
activeBan.PlayerName = unknownName;
activeBan.PlayerSteamId ??= steamId;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
return false;
}
/// <summary>
/// Checks if the given IP address is known (previously recorded) for the specified Steam ID.
/// </summary>
/// <param name="steamId">64-bit Steam ID of the player.</param>
/// <param name="ipAddress">IP address to check.</param>
/// <returns>True if the IP is recorded for the player, otherwise false.</returns>
public bool HasIpForPlayer(ulong steamId, string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
return false;
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUint))
return false;
return _playerIpsCache.TryGetValue(steamId, out var ipData)
&& ipData.Any(x => x.Ip == IpHelper.IpToUint(ipAddress));
return _playerIpsCache.TryGetValue(steamId, out var ipData) &&
ipData.Contains(new IpRecord(ipUint, default, null!));
}
// public bool HasIpForPlayer(ulong steamId, string ipAddress)
// {
// if (string.IsNullOrWhiteSpace(ipAddress))
// return false;
//
// return _playerIpsCache.TryGetValue(steamId, out var ipData)
// && ipData.Any(x => x.Ip == IpHelper.IpToUint(ipAddress));
// }
/// <summary>
/// Updates existing active ban records in the database with the latest known player name and IP address.
/// Also updates in-memory cache to reflect these changes.
/// </summary>
/// <param name="playerName">Current player name.</param>
/// <param name="steamId">Optional Steam ID of the player.</param>
/// <param name="ipAddress">Optional IP address of the player.</param>
/// <returns>Asynchronous task representing the update operation.</returns>
private async Task UpdatePlayerData(string? playerName, ulong? steamId, string? ipAddress)
{
if (CS2_SimpleAdmin.DatabaseProvider == null)
return;
var baseSql = """
UPDATE sa_bans
SET
player_ip = COALESCE(player_ip, @PlayerIP),
player_name = COALESCE(player_name, @PlayerName)
WHERE
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
""";
if (!CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
baseSql += " AND server_id = @ServerId;";
}
var parameters = new
{
PlayerSteamID = steamId,
PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|| string.IsNullOrEmpty(ipAddress)
|| CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress)
? null
: ipAddress,
PlayerName = string.IsNullOrEmpty(playerName) ? string.Empty : playerName,
CurrentTime = Time.ActualDateTime(),
CS2_SimpleAdmin.ServerId
};
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await connection.ExecuteAsync(baseSql, parameters);
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
{
foreach (var rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
{
if (string.IsNullOrEmpty(rec.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
rec.PlayerIp = ipAddress;
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
rec.PlayerName = playerName;
}
}
if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)
&& _ipIndex.TryGetValue(ipUInt, out var ipRecords))
{
foreach (var rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
{
if (!rec.PlayerSteamId.HasValue && steamId.HasValue)
rec.PlayerSteamId = steamId;
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
rec.PlayerName = playerName;
}
}
}
private void Clear()
@ -379,6 +665,9 @@ internal class CacheManager: IDisposable
_cachedIgnoredIps.Clear();
}
/// <summary>
/// Clears and disposes of all cached data and marks the object as disposed.
/// </summary>
public void Dispose()
{
if (_disposed) return;

View file

@ -5,15 +5,15 @@ namespace CS2_SimpleAdmin.Managers;
public class ChatManager
{
public void AddChatMessageDB(string playerSteam64, string playerName, string message, bool? team)
public void AddChatMessageDB(string playerSteam64, string playerName, string message, bool? team)
{
if (CS2_SimpleAdmin.Database == null) return;
Task.Run(async () =>
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
Task.Run(async () =>
{
try
{
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
var sql = "INSERT INTO `sa_chatlogs` (`playerSteam64`, `playerName`, `message`, `team`, `created`, `serverId`) " +
"VALUES (@playerSteam64, @playerName, @message, @team, @created, @serverId)";

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -11,284 +11,372 @@ using ZLinq;
namespace CS2_SimpleAdmin.Managers;
public class PlayerManager
internal class PlayerManager
{
private readonly SemaphoreSlim _loadPlayerSemaphore = new(5);
private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config;
/// <summary>
/// Loads and initializes player data when a client connects.
/// </summary>
/// <param name="player">The <see cref="CCSPlayerController"/> instance representing the connecting player.</param>
/// <param name="fullConnect">
/// Determines whether to perform a full synchronization of player data.
/// If true, full checks (bans, IP history, penalties, warns, mutes) will be loaded and applied.
/// </param>
/// <remarks>
/// This method validates the player's identity, checks for bans, updates the IP history table,
/// loads penalties (mutes/gags/warns), and optionally notifies admin players about the connecting player's penalties.
/// </remarks>
public void LoadPlayerData(CCSPlayerController player, bool fullConnect = false)
{
if (player.IsBot || string.IsNullOrEmpty(player.IpAddress) || player.IpAddress.Contains("127.0.0.1"))
return;
if (!player.UserId.HasValue)
{
Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
return;
}
var ipAddress = player.IpAddress?.Split(":")[0];
CS2_SimpleAdmin.PlayersInfo[player.UserId.Value] =
new PlayerInfo(player.UserId.Value, player.Slot, new SteamID(player.SteamID), player.PlayerName, ipAddress);
// if (!player.UserId.HasValue)
// {
// Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
// return;
// }
var userId = player.UserId.Value;
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(userId))
{
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
}
var steamId64 = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64;
var steamId = steamId64.ToString();
if (CS2_SimpleAdmin.Database == null) return;
var slot = player.Slot;
var steamId = player.SteamID;
var playerName = !string.IsNullOrEmpty(player.PlayerName)
? player.PlayerName
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var ipAddress = player.IpAddress?.Split(":")[0];
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
// Perform asynchronous database operations within a single method
Task.Run(async () =>
{
var isBanned = CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
try
{
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(steamId, null),
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(steamId64, ipAddress)
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(steamId, ipAddress)
};
await _loadPlayerSemaphore.WaitAsync();
if (isBanned)
{
// Kick the player if banned
await Server.NextFrameAsync(() =>
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
{
if (!player.UserId.HasValue) return;
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
});
return;
}
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null)
{
try
{
if (CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(
CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64, ipAddress))
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
{
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP
WHERE steamid = @SteamID AND address = @IPAddress;
""";
await connection.ExecuteAsync(updateQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
else
{
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null),
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId,
ipAddress)
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress)
};
const string selectQuery =
"SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;";
var recordExists = await connection.ExecuteScalarAsync<int>(selectQuery, new
// CS2_SimpleAdmin._logger?.LogInformation($"Player {playerName} ({steamId} - {ipAddress}) is banned? {isBanned.ToString()}");
if (isBanned)
{
await Server.NextWorldUpdateAsync(() =>
{
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
// CS2_SimpleAdmin._logger?.LogInformation($"Kicking {playerName}");
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
});
if (recordExists > 0)
{
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP
WHERE steamid = @SteamID AND address = @IPAddress;
""";
await connection.ExecuteAsync(updateQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
else
{
const string insertQuery = """
INSERT INTO `sa_players_ips` (steamid, address, used_at)
VALUES (@SteamID, @IPAddress, CURRENT_TIMESTAMP);
""";
await connection.ExecuteAsync(insertQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(
$"Unable to save ip address for {CS2_SimpleAdmin.PlayersInfo[userId].Name} ({ipAddress}) {ex.Message}");
}
// Get all accounts associated to the player (ip address)
CS2_SimpleAdmin.PlayersInfo[userId].AccountsAssociated =
CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable().Select(x => (x.SteamId, x.PlayerName)).ToList() ?? [];
}
try
{
// var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(
// CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), null)
// : CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(CS2_SimpleAdmin
// .PlayersInfo[userId].SteamId.SteamId64)
// : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), ipAddress);
if (fullConnect || !fullConnect) // Temp skip
{
var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(CS2_SimpleAdmin.PlayersInfo[userId], false);
var (totalMutes, totalGags, totalSilences) =
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(CS2_SimpleAdmin.PlayersInfo[userId]);
CS2_SimpleAdmin.PlayersInfo[userId].TotalBans = CS2_SimpleAdmin.Instance.CacheManager?.GetPlayerBansBySteamId(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString()).Count ?? 0;
CS2_SimpleAdmin.PlayersInfo[userId].TotalMutes = totalMutes;
CS2_SimpleAdmin.PlayersInfo[userId].TotalGags = totalGags;
CS2_SimpleAdmin.PlayersInfo[userId].TotalSilences = totalSilences;
CS2_SimpleAdmin.PlayersInfo[userId].TotalWarns = warns.Count;
// Check if the player is muted
var activeMutes = await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString());
if (activeMutes.Count > 0)
{
foreach (var mute in activeMutes)
{
string muteType = mute.type;
DateTime ends = mute.ends;
int duration = mute.duration;
switch (muteType)
{
// Apply mute penalty based on mute type
case "GAG":
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Gag, ends, duration);
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
case "MUTE":
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Mute, ends, duration);
await Server.NextFrameAsync(() =>
{
player.VoiceFlags = VoiceFlags.Muted;
});
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Mute].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.InvariantCulture)]);
break;
default:
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Silence, ends, duration);
await Server.NextFrameAsync(() =>
{
player.VoiceFlags = VoiceFlags.Muted;
});
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Silence].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
}
}
return;
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.NotifyPenaltiesToAdminOnConnect && fullConnect)
if (fullConnect)
{
var associatedAcccountsChunks = CS2_SimpleAdmin.PlayersInfo[userId].AccountsAssociated.ChunkBy(5).ToList();
await Server.NextFrameAsync(() =>
{
foreach (var admin in Helper.GetValidPlayers()
.Where(p => (AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/kick") ||
AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/ban")) &&
p.Connected == PlayerConnectedState.PlayerConnected && !CS2_SimpleAdmin.AdminDisabledJoinComms.Contains(p.SteamID)))
{
if (CS2_SimpleAdmin._localizer != null && admin != player)
{
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, "sa_admin_penalty_info",
player.PlayerName,
CS2_SimpleAdmin.PlayersInfo[userId].TotalBans,
CS2_SimpleAdmin.PlayersInfo[userId].TotalGags,
CS2_SimpleAdmin.PlayersInfo[userId].TotalMutes,
CS2_SimpleAdmin.PlayersInfo[userId].TotalSilences,
CS2_SimpleAdmin.PlayersInfo[userId].TotalWarns
);
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
foreach (var chunk in associatedAcccountsChunks)
await Server.NextWorldUpdateAsync(() =>
{
if (!CS2_SimpleAdmin.CachedPlayers.Contains(player))
CS2_SimpleAdmin.CachedPlayers.Add(player);
});
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null &&
CS2_SimpleAdmin.PlayersInfo[steamId] != null)
{
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
if (CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(
steamId, ipAddress))
{
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP,
name = @playerName
WHERE steamid = @SteamID AND address = @IPAddress;
""";
await connection.ExecuteAsync(updateQuery, new
{
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, "sa_admin_associated_accounts",
player.PlayerName,
string.Join(", ",
chunk.Select(a => $"{a.PlayerName} ({a.SteamId})"))
);
playerName,
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
else
{
const string selectQuery =
"SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;";
var recordExists = await connection.ExecuteScalarAsync<int>(selectQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
if (recordExists > 0)
{
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP,
name = @playerName
WHERE steamid = @SteamID AND address = @IPAddress;
""";
await connection.ExecuteAsync(updateQuery, new
{
playerName,
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
else
{
const string insertQuery = """
INSERT INTO `sa_players_ips` (steamid, name, address, used_at)
VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP);
""";
await connection.ExecuteAsync(insertQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
playerName,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
}
}
});
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(
$"Unable to save ip address for {playerInfo.Name} ({ipAddress}) {ex.Message}");
}
playerInfo.AccountsAssociated =
CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable()
.Select(x => (x.SteamId, x.PlayerName)).ToList() ?? [];
}
try
{
// var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(
// CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), null)
// : CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(CS2_SimpleAdmin
// .PlayersInfo[userId].SteamId.SteamId64)
// : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), ipAddress);
if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(steamId, out PlayerInfo? value)) // Temp skip
{
var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false);
var (totalMutes, totalGags, totalSilences) =
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(value);
value.TotalBans = CS2_SimpleAdmin.Instance.CacheManager
?.GetPlayerBansBySteamId(value.SteamId.SteamId64)
.Count ?? 0;
value.TotalMutes = totalMutes;
value.TotalGags = totalGags;
value.TotalSilences = totalSilences;
value.TotalWarns = warns.Count;
var activeMutes =
await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(value.SteamId.SteamId64
.ToString());
if (activeMutes.Count > 0)
{
foreach (var mute in activeMutes)
{
string muteType = mute.type;
DateTime ends = mute.ends;
int duration = mute.duration;
switch (muteType)
{
// Apply mute penalty based on mute type
case "GAG":
PlayerPenaltyManager.AddPenalty(
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
PenaltyType.Gag, ends, duration);
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
case "MUTE":
PlayerPenaltyManager.AddPenalty(
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
PenaltyType.Mute, ends, duration);
await Server.NextWorldUpdateAsync(() =>
{
player.VoiceFlags = VoiceFlags.Muted;
});
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Mute].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.InvariantCulture)]);
break;
default:
PlayerPenaltyManager.AddPenalty(
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
PenaltyType.Silence, ends, duration);
await Server.NextWorldUpdateAsync(() =>
{
player.VoiceFlags = VoiceFlags.Muted;
});
// if (CS2_SimpleAdmin._localizer != null)
// mutesList[PenaltyType.Silence].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
break;
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.NotifyPenaltiesToAdminOnConnect)
{
await Server.NextWorldUpdateAsync(() =>
{
foreach (var admin in Helper.GetValidPlayers()
.Where(p => (AdminManager.PlayerHasPermissions(
new SteamID(p.SteamID),
"@css/kick") ||
AdminManager.PlayerHasPermissions(
new SteamID(p.SteamID),
"@css/ban")) &&
p.Connected == PlayerConnectedState.PlayerConnected &&
!CS2_SimpleAdmin.AdminDisabledJoinComms
.Contains(p.SteamID)))
{
if (CS2_SimpleAdmin._localizer == null || admin == player) continue;
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
"sa_admin_penalty_info",
player.PlayerName,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalBans,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalGags,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalMutes,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalSilences,
CS2_SimpleAdmin.PlayersInfo[steamId].TotalWarns
);
if (CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.Count >= 2)
{
var associatedAcccountsChunks =
CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.ChunkBy(5)
.ToList();
foreach (var chunk in associatedAcccountsChunks)
{
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
"sa_admin_associated_accounts",
player.PlayerName,
string.Join(", ",
chunk.Select(a => $"{a.PlayerName} ({a.SteamId})"))
);
}
}
}
});
}
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Error processing player connection: {exception}",
ex.Message);
}
}
}
catch (Exception ex)
finally
{
CS2_SimpleAdmin._logger?.LogError("Error processing player connection: {exception}", ex.Message);
_loadPlayerSemaphore.Release();
}
});
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name))
{
player.Rename(name);
}
}
/// <summary>
/// Periodically checks the status of online players and applies timers for speed, gravity,
/// and penalty expiration validation.
/// </summary>
/// <remarks>
/// This method registers two repeating timers:
/// <list type="bullet">
/// <item><description>One short-interval timer to update speed/gravity modifications applied to players.</description></item>
/// <item><description>
/// One long-interval timer (default 61 seconds) to expire bans, mutes, warns, refresh caches,
/// and remove outdated penalties from connected players.
/// </description></item>
/// </list>
/// Additionally, banned players still online are kicked, and admins may be updated about mute statuses based on the configured time mode.
/// </remarks>
public void CheckPlayersTimer()
{
CS2_SimpleAdmin.Instance.AddTimer(0.1f, () =>
CS2_SimpleAdmin.Instance.AddTimer(0.12f, () =>
{
if (CS2_SimpleAdmin.GravityPlayers.Count <= 0) return;
foreach (var value in CS2_SimpleAdmin.GravityPlayers.Where(value => value.Key is
{ IsValid: true, Connected: PlayerConnectedState.PlayerConnected } || value.Key.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE))
if (CS2_SimpleAdmin.SpeedPlayers.Count > 0)
{
value.Key.SetGravity(value.Value);
foreach (var (slot, speed) in CS2_SimpleAdmin.SpeedPlayers)
{
var player = Utilities.GetPlayerFromSlot(slot);
if (player is null) continue;
if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.SetSpeed(speed);
}
}
}
if (CS2_SimpleAdmin.GravityPlayers.Count > 0)
{
foreach (var (slot, gravity) in CS2_SimpleAdmin.GravityPlayers)
{
var player = Utilities.GetPlayerFromSlot(slot);
if (player is null) continue;
if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.SetGravity(gravity);
}
}
}
}, TimerFlags.REPEAT);
CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
{
#if DEBUG
CS2_SimpleAdmin._logger?.LogCritical("[OnMapStart] Expired check");
#endif
if (CS2_SimpleAdmin.Database == null)
if (CS2_SimpleAdmin.DatabaseProvider == null)
return;
var tempPlayers = Helper.GetValidPlayers()
.Select(p => new
{
p.SteamID, p.IpAddress, p.UserId, p.Slot,
p.PlayerName,
p.SteamID,
p.IpAddress,
p.UserId,
p.Slot,
})
.ToList();
var pluginInstance = CS2_SimpleAdmin.Instance;
_ = Task.Run(async () =>
{
try
{
var expireTasks = new Task[]
var expireTasks = new[]
{
CS2_SimpleAdmin.Instance.BanManager.ExpireOldBans(),
CS2_SimpleAdmin.Instance.MuteManager.ExpireOldMutes(),
CS2_SimpleAdmin.Instance.WarnManager.ExpireOldWarns(),
CS2_SimpleAdmin.Instance.CacheManager?.RefreshCacheAsync() ?? Task.CompletedTask,
CS2_SimpleAdmin.Instance.PermissionManager.DeleteOldAdmins()
pluginInstance.BanManager.ExpireOldBans(),
pluginInstance.MuteManager.ExpireOldMutes(),
pluginInstance.WarnManager.ExpireOldWarns(),
pluginInstance.CacheManager?.RefreshCacheAsync() ?? Task.CompletedTask,
pluginInstance.PermissionManager.DeleteOldAdmins()
};
await Task.WhenAll(expireTasks);
@ -305,56 +393,66 @@ public class PlayerManager
}
}
}
if (pluginInstance.CacheManager == null)
return;
var bannedPlayers = tempPlayers.AsValueEnumerable()
.Where(player =>
{
return CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
var playerName = player.PlayerName;
var steamId = player.SteamID;
var ip = player.IpAddress?.Split(':')[0];
return CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
{
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(player.SteamID.ToString(), null),
_ =>
CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(player.SteamID.ToString(), player.IpAddress?.Split(":")[0])
0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null),
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip)
: pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip)
};
})
.ToList();
foreach (var player in bannedPlayers)
}).ToList();
if (bannedPlayers.Count > 0)
{
if (player.UserId.HasValue)
await Server.NextFrameAsync(() => Helper.KickPlayer((int)player.UserId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED));
foreach (var player in bannedPlayers)
{
if (!player.UserId.HasValue) continue;
await Server.NextWorldUpdateAsync(() => Helper.KickPlayer((int)player.UserId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED));
}
}
var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList();
if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return;
if (_config.OtherSettings.TimeMode == 0)
{
await CS2_SimpleAdmin.Instance.MuteManager.CheckOnlineModeMutes(onlinePlayers);
var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList();
if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return;
await pluginInstance.MuteManager.CheckOnlineModeMutes(onlinePlayers);
}
});
try
{
var players = Helper.GetValidPlayers();
var penalizedSlots = players
.Where(player => PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
.Select(player => new
{
Player = player,
IsMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _),
IsSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _),
IsGagged = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _)
.Select(player => new
{
Player = player,
IsMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _),
IsSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _),
IsGagged = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _)
});
foreach (var entry in penalizedSlots)
foreach (var entry in penalizedSlots)
{
// If the player is not muted or silenced, set voice flags to normal
if (!entry.IsMuted && !entry.IsSilenced)
{
// If the player is not muted or silenced, set voice flags to normal
if (!entry.IsMuted && !entry.IsSilenced)
{
entry.Player.VoiceFlags = VoiceFlags.Normal;
if (entry.Player.VoiceFlags.HasFlag(VoiceFlags.Muted))
entry.Player.VoiceFlags &= ~VoiceFlags.Muted;
}
entry.Player.VoiceFlags = VoiceFlags.Normal;
if (entry.Player.VoiceFlags.HasFlag(VoiceFlags.Muted))
entry.Player.VoiceFlags &= ~VoiceFlags.Muted;
}
}
PlayerPenaltyManager.RemoveExpiredPenalties();
}
@ -362,7 +460,6 @@ public class PlayerManager
{
CS2_SimpleAdmin._logger?.LogError($"Unable to remove old penalties: {ex.Message}");
}
}, TimerFlags.REPEAT);
}
}

View file

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

View file

@ -9,21 +9,31 @@ public class ServerManager
{
private int _getIpTryCount;
/// <summary>
/// Checks whether the server setting <c>sv_hibernate_when_empty</c> is enabled.
/// Logs an error if this setting is true, since it prevents the plugin from working properly.
/// </summary>
public static void CheckHibernationStatus()
{
var convar = ConVar.Find("sv_hibernate_when_empty");
if (convar == null || !convar.GetPrimitiveValue<bool>())
return;
CS2_SimpleAdmin._logger?.LogError("Detected setting \"sv_hibernate_when_empty true\", set false to make plugin work properly");
}
/// <summary>
/// Initiates the asynchronous process to load server data such as IP address, port, hostname, and RCON password.
/// Handles retry attempts if IP address is not immediately available.
/// Updates or inserts the server record in the database accordingly.
/// After loading, triggers admin reload and cache initialization.
/// Also optionally sends plugin usage metrics if enabled in configuration.
/// </summary>
public void LoadServerData()
{
CS2_SimpleAdmin.Instance.AddTimer(1.2f, () =>
CS2_SimpleAdmin.Instance.AddTimer(2.0f, () =>
{
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.ServerId != null || CS2_SimpleAdmin.Database == null) return;
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return;
if (_getIpTryCount > 32 && Helper.GetServerIp().StartsWith("0.0.0.0") || string.IsNullOrEmpty(Helper.GetServerIp()))
{
@ -32,7 +42,6 @@ public class ServerManager
}
var ipAddress = ConVar.Find("ip")?.StringValue;
if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))
{
ipAddress = Helper.GetServerIp();
@ -58,7 +67,7 @@ public class ServerManager
{
try
{
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
var addressExists = await connection.ExecuteScalarAsync<bool>(
"SELECT COUNT(*) FROM sa_servers WHERE address = @address",
new { address });
@ -67,11 +76,11 @@ public class ServerManager
{
string query = "INSERT INTO sa_servers (address, hostname) VALUES (@address, @hostname)";
if(CS2_SimpleAdmin.Instance.Config.IsCSSPanel)
if (CS2_SimpleAdmin.Instance.Config.IsCSSPanel)
{
query = "INSERT INTO sa_servers (address, hostname, rcon) VALUES (@address, @hostname, @rcon)";
}
await connection.ExecuteAsync(
query,
new { address, hostname, rcon });
@ -80,11 +89,11 @@ public class ServerManager
{
string query = "UPDATE `sa_servers` SET `hostname` = @hostname, `id` = `id` WHERE `address` = @address";
if(CS2_SimpleAdmin.Instance.Config.IsCSSPanel)
if (CS2_SimpleAdmin.Instance.Config.IsCSSPanel)
{
query = "UPDATE `sa_servers` SET `hostname` = @hostname, rcon = @rcon, `id` = `id` WHERE `address` = @address";
}
await connection.ExecuteAsync(
query,
new { address, rcon, hostname });
@ -95,7 +104,6 @@ public class ServerManager
new { address });
CS2_SimpleAdmin.ServerId = serverId;
CS2_SimpleAdmin._logger?.LogInformation("Loaded server with ip {ip}", ipAddress);
if (CS2_SimpleAdmin.ServerId != null)

View file

@ -1,35 +1,37 @@
using CS2_SimpleAdminApi;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
namespace CS2_SimpleAdmin.Managers;
internal class WarnManager(Database.Database? database)
internal class WarnManager(IDatabaseProvider? databaseProvider)
{
/// <summary>
/// Adds a warning to a player with an optional issuer and reason.
/// </summary>
/// <param name="player">The player who is being warned.</param>
/// <param name="issuer">The player issuing the warning; null indicates console or system.</param>
/// <param name="reason">The reason for the warning.</param>
/// <param name="time">Optional duration of the warning in minutes (0 means permanent).</param>
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
public async Task<int?> WarnPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
{
if (database == null) return null;
if (databaseProvider == null) return null;
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
try
{
await using var connection = await database.GetConnectionAsync();
const string sql = """
INSERT INTO `sa_warns`
(`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
""";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddWarnQuery(true);
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64.ToString(),
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason,
duration = time,
@ -46,30 +48,30 @@ internal class WarnManager(Database.Database? database)
}
}
public async Task<int?> AddWarnBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
/// <summary>
/// Adds a warning to a player identified by SteamID with optional issuer and reason.
/// </summary>
/// <param name="playerSteamId">The SteamID64 of the player being warned.</param>
/// <param name="issuer">The player issuing the warning; null indicates console or system.</param>
/// <param name="reason">The reason for the warning.</param>
/// <param name="time">Optional duration of the warning in minutes (0 means permanent).</param>
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
public async Task<int?> AddWarnBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
{
if (database == null) return null;
if (string.IsNullOrEmpty(playerSteamId)) return null;
if (databaseProvider == null) return null;
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
try
{
await using var connection = await database.GetConnectionAsync();
const string sql = """
INSERT INTO `sa_warns`
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
""";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddWarnQuery(false);
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason,
duration = time,
@ -86,30 +88,22 @@ internal class WarnManager(Database.Database? database)
}
}
/// <summary>
/// Retrieves a list of warnings for a specific player.
/// </summary>
/// <param name="player">The player whose warnings to retrieve.</param>
/// <param name="active">If true, returns only active (non-expired) warnings; otherwise returns all warnings.</param>
/// <returns>A list of dynamic objects representing warnings, or an empty list if none found or on failure.</returns>
public async Task<List<dynamic>> GetPlayerWarns(PlayerInfo player, bool active = true)
{
if (database == null) return [];
if (databaseProvider == null) return [];
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
string sql;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
sql = active
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC"
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC";
}
else
{
sql = active
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC"
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
}
var parameters = new { PlayerSteamID = player.SteamId.SteamId64.ToString(), serverid = CS2_SimpleAdmin.ServerId };
var sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
var parameters = new { PlayerSteamID = player.SteamId.SteamId64, serverid = CS2_SimpleAdmin.ServerId };
var warns = await connection.QueryAsync<dynamic>(sql, parameters);
return warns.ToList();
@ -120,24 +114,23 @@ internal class WarnManager(Database.Database? database)
}
}
public async Task<int> GetPlayerWarnsCount(string steamId, bool active = true)
/// <summary>
/// Retrieves the count of warnings for a player specified by SteamID.
/// </summary>
/// <param name="steamId">The SteamID64 of the player.</param>
/// <param name="active">If true, counts only active (non-expired) warnings; otherwise counts all warnings.</param>
/// <returns>The count of warnings as an integer, or 0 if none found or on failure.</returns>
public async Task<int> GetPlayerWarnsCount(ulong steamId, bool active = true)
{
if (database == null) return 0;
if (databaseProvider == null) return 0;
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? active
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'"
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID"
: active
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'"
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID'";
var muteCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
return muteCount;
var sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
var warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
return warnsCount;
}
catch (Exception)
{
@ -145,19 +138,22 @@ internal class WarnManager(Database.Database? database)
}
}
/// <summary>
/// Removes a specific warning by its identifier from a player's record.
/// </summary>
/// <param name="player">The player whose warning will be removed.</param>
/// <param name="warnId">The identifier of the warning to remove.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnwarnPlayer(PlayerInfo player, int warnId)
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId"
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64.ToString(), warnId, serverid = CS2_SimpleAdmin.ServerId });
var sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64, warnId, serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
{
@ -165,36 +161,20 @@ internal class WarnManager(Database.Database? database)
}
}
/// <summary>
/// Removes the most recent warning matching a player pattern (usually SteamID string).
/// </summary>
/// <param name="playerPattern">The pattern identifying the player whose last warning should be removed.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnwarnPlayer(string playerPattern)
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? """
UPDATE sa_warns
JOIN (
SELECT MAX(id) AS max_id
FROM sa_warns
WHERE player_steamid = @steamid AND status = 'ACTIVE'
) AS subquery ON sa_warns.id = subquery.max_id
SET sa_warns.status = 'EXPIRED'
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid;
"""
: """
UPDATE sa_warns
JOIN (
SELECT MAX(id) AS max_id
FROM sa_warns
WHERE player_steamid = @steamid AND status = 'ACTIVE' AND server_id = @serverid
) AS subquery ON sa_warns.id = subquery.max_id
SET sa_warns.status = 'EXPIRED'
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid AND sa_warns.server_id = @serverid;
""";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { steamid = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -203,18 +183,19 @@ internal class WarnManager(Database.Database? database)
}
}
/// <summary>
/// Expires old warnings based on the current time, removing or marking them as inactive.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task ExpireOldWarns()
{
if (database == null) return;
if (databaseProvider == null) return;
try
{
await using var connection = await database.GetConnectionAsync();
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)