Compare commits

...

3 Commits

Author SHA1 Message Date
Dawid Bepierszcz
c2e8b4a898 Refactor player cache and ban checks, update version
Improves player cache handling and ban status checks for race condition safety. Removes unused GodPlayers logic and related event handler. Refactors event handlers for disconnect and team changes, and fixes warn reason field naming. Updates version to 1.7.8-beta-7.
2025-12-02 17:37:14 +01:00
Dawid Bepierszcz
9723a4faee :(
:(
2025-11-13 01:40:50 +01:00
Dawid Bepierszcz
4865b76262 Improve ban cache sync and update dependencies
Enhanced CacheManager to use database time for ban updates, improving multi-server consistency and handling of status changes. Increased PlayerManager's semaphore limit and improved player/bans refresh logic to ensure status changes are detected even when the server is empty. Updated author and version metadata, commented out duplicate event registration, and bumped CounterStrikeSharp.API dependency to 1.0.346.
2025-11-13 01:33:38 +01:00
10 changed files with 127 additions and 87 deletions

View File

@@ -21,13 +21,12 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
public override string ModuleAuthor => "daffyy & Dliix66";
public override string ModuleVersion => "1.7.8-beta-5";
public override string ModuleAuthor => "daffyy";
public override string ModuleVersion => "1.7.8-beta-7";
public override void Load(bool hotReload)
{
Instance = this;
if (hotReload)
{
ServerLoaded = false;
@@ -47,7 +46,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
CachedPlayers.Clear();
BotPlayers.Clear();
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsHLTV).ToArray())
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && p.Connected == PlayerConnectedState.PlayerConnected && !p.IsHLTV).ToArray())
{
if (!player.IsBot)
PlayerManager.LoadPlayerData(player, true);
@@ -261,6 +260,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
CacheManager = null;
PlayersTimer?.Kill();
PlayersTimer = null;
UnregisterEvents();
if (hotReload)

View File

@@ -15,7 +15,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
await cmd.ExecuteNonQueryAsync();
cmd.CommandText = "SET time_zone = '+00:00';";
// cmd.CommandText = "SET time_zone = '+00:00';";
await cmd.ExecuteNonQueryAsync();
return connection;

View File

@@ -23,7 +23,7 @@ public partial class CS2_SimpleAdmin
{
RegisterListener<Listeners.OnMapStart>(OnMapStart);
// RegisterListener<Listeners.OnClientConnect>(OnClientConnect);
RegisterListener<Listeners.OnClientConnect>(OnClientConnect);
// RegisterListener<Listeners.OnClientConnect>(OnClientConnect);
RegisterListener<Listeners.OnClientConnected>(OnClientConnected);
RegisterListener<Listeners.OnGameServerSteamAPIActivated>(OnGameServerSteamAPIActivated);
if (Config.OtherSettings.UserMessageGagChatType)
@@ -77,7 +77,7 @@ public partial class CS2_SimpleAdmin
new ServerManager().LoadServerData();
}
[GameEventHandler(HookMode.Pre)]
[GameEventHandler]
public HookResult OnClientDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
{
if (@event.Reason is 149 or 6)
@@ -91,14 +91,15 @@ public partial class CS2_SimpleAdmin
if (player == null || !player.IsValid || player.IsHLTV)
return HookResult.Continue;
BotPlayers.Remove(player);
CachedPlayers.Remove(player);
CachedPlayers.Remove(player);
BotPlayers.Remove(player);
SilentPlayers.Remove(player.Slot);
if (player.IsBot)
{
return HookResult.Continue;
}
#if DEBUG
Logger.LogCritical("[OnClientDisconnect] After Check");
@@ -176,6 +177,9 @@ public partial class CS2_SimpleAdmin
if (player == null || !player.IsValid || player.IsBot)
return;
if (!CachedPlayers.Contains(player))
CachedPlayers.Add(player);
PlayerManager.LoadPlayerData(player);
}
@@ -458,28 +462,11 @@ public partial class CS2_SimpleAdmin
// OnGameServerSteamAPIActivated();
// });
GodPlayers.Clear();
SilentPlayers.Clear();
PlayerPenaltyManager.RemoveAllPenalties();
}
[GameEventHandler]
public HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var player = @event.Userid;
if (player is null || @event.Attacker is null || player.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE || player.PlayerPawn.Value == null)
return HookResult.Continue;
if (!GodPlayers.Contains(player.Slot)) return HookResult.Continue;
player.PlayerPawn.Value.Health = player.PlayerPawn.Value.MaxHealth;
player.PlayerPawn.Value.ArmorValue = 100;
return HookResult.Continue;
}
[GameEventHandler]
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
@@ -512,17 +499,13 @@ public partial class CS2_SimpleAdmin
public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info)
{
var player = @event.Userid;
if (player == null || !player.IsValid || player.IsBot)
if (player == null || !player.IsValid || player.IsBot || !SilentPlayers.Contains(player.Slot))
return HookResult.Continue;
if (!SilentPlayers.Contains(player.Slot))
return HookResult.Continue;
if (@event is { Oldteam: <= 1, Team: >= 1 })
{
SilentPlayers.Remove(player.Slot);
SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false);
}
if (@event is not { Oldteam: <= 1, Team: >= 1 }) return HookResult.Continue;
SilentPlayers.Remove(player.Slot);
SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false);
return HookResult.Continue;
}

View File

@@ -64,7 +64,7 @@ internal static class Helper
public static List<CCSPlayerController> GetValidPlayers()
{
return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().Where(p => p.Connected == PlayerConnectedState.PlayerConnected).ToList();
return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().Where(p => p.IsValid && p.Connected == PlayerConnectedState.PlayerConnected).ToList();
}
public static List<CCSPlayerController> GetValidPlayersWithBots()

View File

@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Models;
using Dapper;
using Microsoft.Extensions.Logging;
using ZLinq;
namespace CS2_SimpleAdmin.Managers;
@@ -16,6 +17,7 @@ internal class CacheManager: IDisposable
private HashSet<uint> _cachedIgnoredIps = [];
private DateTime _lastUpdateTime = DateTime.MinValue;
private DateTime? _lastDatabaseTime = null; // Track actual time from database
private bool _isInitialized;
private bool _disposed;
@@ -156,13 +158,20 @@ internal class CacheManager: IDisposable
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<BanRecord> updatedBans;
// Get current time from database in local timezone (CURRENT_TIMESTAMP uses session timezone, not UTC)
var currentDatabaseTime = await connection.QueryFirstAsync<DateTime>("SELECT CURRENT_TIMESTAMP");
// Optimization: Only get IDs for comparison if we need to check for deletions
// Most of the time bans are just added/updated, not deleted
HashSet<int>? allIds = null;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
updatedBans = (await connection.QueryAsync<BanRecord>(
// Use previous database time or start from far past if first run
var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
// Get recently updated bans by timestamp (using database time to avoid timezone issues)
var updatedBans_Query = (await connection.QueryAsync<BanRecord>(
"""
SELECT id AS Id,
player_name AS PlayerName,
@@ -171,33 +180,68 @@ internal class CacheManager: IDisposable
status AS Status
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
""",
new { lastUpdate = _lastUpdateTime }
));
new { lastUpdate = lastCheckTime }
)).ToList();
// Detect changes: new bans or status changes
var updatedList = new List<BanRecord>();
foreach (var ban in updatedBans_Query)
{
if (!_banCache.TryGetValue(ban.Id, out var cachedBan))
{
// New ban
updatedList.Add(ban);
}
else if (cachedBan.Status != ban.Status)
{
// Status changed
updatedList.Add(ban);
}
}
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0)
{
allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
}
updatedBans = updatedList;
// Update last check time to current database time
_lastDatabaseTime = currentDatabaseTime;
}
else
{
updatedBans = (await connection.QueryAsync<BanRecord>(
// Use previous database time or start from far past if first run
var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
// Get recently updated bans for this server by timestamp (using database time to avoid timezone issues)
var updatedBans_Query = (await connection.QueryAsync<BanRecord>(
"""
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
FROM `sa_bans` WHERE server_id = @serverId AND (updated_at > @lastUpdate OR created > @lastUpdate) ORDER BY updated_at DESC
""",
new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId }
));
new { serverId = CS2_SimpleAdmin.ServerId, lastUpdate = lastCheckTime }
)).ToList();
// Detect changes: new bans or status changes
var updatedList = new List<BanRecord>();
foreach (var ban in updatedBans_Query)
{
if (!_banCache.TryGetValue(ban.Id, out var cachedBan))
{
// New ban
updatedList.Add(ban);
}
else if (cachedBan.Status != ban.Status)
{
// Status changed
updatedList.Add(ban);
}
}
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0)
{
allIds = (await connection.QueryAsync<int>(
@@ -206,6 +250,9 @@ internal class CacheManager: IDisposable
)).ToHashSet();
}
updatedBans = updatedList;
// Update last check time to current database time
_lastDatabaseTime = currentDatabaseTime;
}
// Optimization: Only process deletions if we have the full ID list
@@ -276,16 +323,19 @@ internal class CacheManager: IDisposable
}
// Update cache with new/modified bans
var hasUpdates = false;
var needsRebuild = false;
foreach (var ban in updatedBans)
{
if (_banCache.TryGetValue(ban.Id, out var oldBan) && oldBan.Status != ban.Status)
{
// Ban status changed (e.g., ACTIVE -> EXPIRED/UNBANNED), need to rebuild indexes
needsRebuild = true;
}
_banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban);
hasUpdates = true;
}
// Always rebuild indexes if there were any updates
// This ensures status changes (ACTIVE -> UNBANNED) are reflected
if (hasUpdates)
// Rebuild indexes if there were updates or status changes
if (updatedBans.Any() || needsRebuild)
{
RebuildIndexes();
}
@@ -436,32 +486,41 @@ internal class CacheManager: IDisposable
record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record != null)
{
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue))
// Double-check the ban is still active in cache (handle race conditions)
if (_banCache.TryGetValue(record.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue))
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
}
return true;
}
return true;
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
return false;
if (string.IsNullOrEmpty(ipAddress) ||
!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) ||
_cachedIgnoredIps.Contains(ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false;
record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record == null) return false;
// Double-check the ban is still active in cache (handle race conditions)
if (!_banCache.TryGetValue(record.Id, out var cachedBanIp) || cachedBanIp.StatusEnum != BanStatus.ACTIVE)
return false;
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue && steamId.HasValue))
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
}
return true;
}
@@ -547,14 +606,18 @@ internal class CacheManager: IDisposable
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;
// Double-check the ban is still active in cache (handle race conditions)
if (_banCache.TryGetValue(activeBan.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
{
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
return false;
if (!_playerIpsCache.TryGetValue(steamId, out var ipData))
@@ -576,16 +639,20 @@ internal class CacheManager: IDisposable
if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
continue;
if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords))
if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords))
continue;
var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null)
if (activeBan == null)
continue;
// Double-check the ban is still active in cache (handle race conditions)
if (!_banCache.TryGetValue(activeBan.Id, out var cachedBan) || cachedBan.StatusEnum != BanStatus.ACTIVE)
continue;
if (string.IsNullOrEmpty(activeBan.PlayerName))
activeBan.PlayerName = unknownName;
activeBan.PlayerSteamId ??= steamId;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));

View File

@@ -13,7 +13,7 @@ namespace CS2_SimpleAdmin.Managers;
internal class PlayerManager
{
private readonly SemaphoreSlim _loadPlayerSemaphore = new(5);
private readonly SemaphoreSlim _loadPlayerSemaphore = new(10);
private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config;
/// <summary>
@@ -51,7 +51,6 @@ internal class PlayerManager
try
{
await _loadPlayerSemaphore.WaitAsync();
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
{
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
@@ -81,13 +80,7 @@ internal class PlayerManager
{
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
await Server.NextWorldUpdateAsync(() =>
{
if (!CS2_SimpleAdmin.CachedPlayers.Contains(player))
CS2_SimpleAdmin.CachedPlayers.Add(player);
});
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null &&
CS2_SimpleAdmin.PlayersInfo[steamId] != null)
{
@@ -254,6 +247,7 @@ internal class PlayerManager
_loadPlayerSemaphore.Release();
}
});
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name))
{
player.Rename(name);
@@ -287,9 +281,6 @@ internal class PlayerManager
// Optimization: Get players once and avoid allocating anonymous types
var validPlayers = Helper.GetValidPlayers();
if (validPlayers.Count == 0)
return;
// Use ValueTuple instead of anonymous type - better performance and less allocations
var tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
foreach (var p in validPlayers)

View File

@@ -33,7 +33,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
playerName = player.Name,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason,
warnReason = reason,
duration = time,
ends = futureTime,
created = now,
@@ -42,7 +42,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
return warnId;
}
catch
catch(Exception e)
{
return null;
}
@@ -73,7 +73,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
muteReason = reason,
warnReason = reason,
duration = time,
ends = futureTime,
created = now,

View File

@@ -1 +1 @@
1.7.8-beta-5
1.7.8-beta-7

View File

@@ -40,7 +40,6 @@ public partial class CS2_SimpleAdmin
internal static readonly HashSet<ulong> AdminDisabledJoinComms = [];
// Player Management
private static readonly HashSet<int> GodPlayers = [];
internal static readonly HashSet<int> SilentPlayers = [];
internal static readonly Dictionary<ulong, string> RenamedPlayers = [];
internal static readonly ConcurrentDictionary<ulong, PlayerInfo> PlayersInfo = [];

View File

@@ -14,7 +14,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.340" />
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.346" />
</ItemGroup>
<ItemGroup>