using System.Collections.Concurrent; using System.Data.Common; using CS2_SimpleAdmin.Models; using Dapper; using ZLinq; namespace CS2_SimpleAdmin.Managers; internal class CacheManager : IDisposable { private readonly ConcurrentDictionary _banCache = []; // Rebuilt into fresh instances and swapped, so unlocked readers on the game thread never see a half-built index private ConcurrentDictionary> _steamIdIndex = []; private ConcurrentDictionary> _ipIndex = []; private readonly object _indexLock = new(); private readonly ConcurrentDictionary> _playerIpsCache = []; private HashSet _cachedIgnoredIps = []; private DateTime _lastUpdateTime = DateTime.MinValue; private DateTime? _lastDatabaseTime = null; // Track actual time from database private bool _isInitialized; private bool _disposed; /// /// Initializes and builds the ban and IP cache from the database. Loads bans, player IP history, and config settings. /// /// Asynchronous task representing the initialization process. public async Task InitializeCacheAsync() { if (CS2_SimpleAdmin.DatabaseProvider == null) { return; } if (!CS2_SimpleAdmin.ServerLoaded) { return; } if (_isInitialized) { return; } try { Clear(); _cachedIgnoredIps = CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps .AsValueEnumerable() .Select(IpHelper.IpToUint) .ToHashSet(); await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); List bans; if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) { bans = (await connection.QueryAsync( """ SELECT id AS Id, player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status FROM sa_bans """)).ToList(); } else { bans = (await connection.QueryAsync( """ SELECT id AS Id, player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status FROM sa_bans WHERE server_id = @serverId """, new { serverId = CS2_SimpleAdmin.ServerId })).ToList(); } if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) { // Optimization: Load IP history and build cache in single pass IEnumerable ipHistory = await connection.QueryAsync( "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY steamid, address, used_at DESC"); string unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; ulong currentSteamId = 0UL; HashSet currentIpSet = new HashSet(new IpRecordComparer()); Dictionary latestIpTimestamps = new Dictionary(); foreach (IpHistoryRow record in ipHistory) { // When we encounter a new steamid, save the previous one if ((ulong)record.Steamid != currentSteamId && currentSteamId != 0) { _playerIpsCache[currentSteamId] = currentIpSet; currentIpSet = new HashSet(new IpRecordComparer()); latestIpTimestamps.Clear(); } currentSteamId = (ulong)record.Steamid; // Only keep the latest timestamp for each IP if (!latestIpTimestamps.TryGetValue(record.Address, out DateTime existingTimestamp) || record.Used_at > existingTimestamp) { latestIpTimestamps[record.Address] = record.Used_at; currentIpSet.Add(new IpRecord( record.Address, record.Used_at, NameOrDefault(record.Name, unknownName) )); } } // Don't forget the last steamid if (currentSteamId != 0) { _playerIpsCache[currentSteamId] = currentIpSet; } } foreach (BanRecord? ban in bans.AsValueEnumerable()) { _banCache.TryAdd(ban.Id, ban); } RebuildIndexes(); _lastUpdateTime = Time.ActualDateTime().AddSeconds(-1); _isInitialized = true; } catch (Exception e) { Console.WriteLine(e.ToString()); } } /// /// Clears all cached data and reinitializes the cache from the database. /// /// Asynchronous task representing the reinitialization process. public async Task ForceReInitializeCacheAsync() { _isInitialized = false; _banCache.Clear(); _playerIpsCache.Clear(); _cachedIgnoredIps = []; _lastUpdateTime = DateTime.MinValue; await InitializeCacheAsync(); } /// /// Refreshes the in-memory cache with updated or new data from the database since the last update time. /// Also updates multi-account IP history if enabled. /// /// Asynchronous task representing the refresh operation. public async Task RefreshCacheAsync() { if (CS2_SimpleAdmin.DatabaseProvider == null) { return; } if (!_isInitialized) { return; } try { await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); IEnumerable updatedBans; // Get current time from database in local timezone (CURRENT_TIMESTAMP uses session timezone, not UTC) DateTime currentDatabaseTime = await connection.QueryFirstAsync("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? allIds = null; if (CS2_SimpleAdmin.Instance.Config.MultiServerMode) { // Use previous database time or start from far past if first run DateTime lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue; // Get recently updated bans by timestamp (using database time to avoid timezone issues) List updatedBans_Query = (await connection.QueryAsync( """ SELECT id AS Id, player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status FROM `sa_bans` WHERE updated_at >= @lastUpdate OR created >= @lastUpdate ORDER BY updated_at DESC """, new { lastUpdate = lastCheckTime } )).ToList(); // Detect changes: new bans or status changes List updatedList = new List(); foreach (BanRecord ban in updatedBans_Query) { if (!_banCache.TryGetValue(ban.Id, out BanRecord? cachedBan)) { // New ban updatedList.Add(ban); } else if (cachedBan.Status != ban.Status) { // Status changed updatedList.Add(ban); } } if (updatedList.Count > 0) { allIds = (await connection.QueryAsync("SELECT id FROM sa_bans")).ToHashSet(); } updatedBans = updatedList; // Update last check time to current database time _lastDatabaseTime = currentDatabaseTime; } else { // Use previous database time or start from far past if first run DateTime lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue; // Get recently updated bans for this server by timestamp (using database time to avoid timezone issues) List updatedBans_Query = (await connection.QueryAsync( """ SELECT id AS Id, player_name AS PlayerName, player_steamid AS PlayerSteamId, player_ip AS PlayerIp, status AS Status FROM `sa_bans` WHERE server_id = @serverId AND (updated_at >= @lastUpdate OR created >= @lastUpdate) ORDER BY updated_at DESC """, new { serverId = CS2_SimpleAdmin.ServerId, lastUpdate = lastCheckTime } )).ToList(); // Detect changes: new bans or status changes List updatedList = new List(); foreach (BanRecord ban in updatedBans_Query) { if (!_banCache.TryGetValue(ban.Id, out BanRecord? cachedBan)) { // New ban updatedList.Add(ban); } else if (cachedBan.Status != ban.Status) { // Status changed updatedList.Add(ban); } } if (updatedList.Count > 0) { allIds = (await connection.QueryAsync( "SELECT id FROM sa_bans WHERE server_id = @serverId", new { serverId = CS2_SimpleAdmin.ServerId } )).ToHashSet(); } updatedBans = updatedList; // Update last check time to current database time _lastDatabaseTime = currentDatabaseTime; } // Optimization: Only process deletions if we have the full ID list if (allIds != null) { foreach (int id in _banCache.Keys) { if (allIds.Contains(id) || !_banCache.TryRemove(id, out BanRecord? ban)) { continue; } if (ban.PlayerSteamId != null && _steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out List? steamBans)) { steamBans.RemoveAll(b => b.Id == id); if (steamBans.Count == 0) { _steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _); } } if (string.IsNullOrWhiteSpace(ban.PlayerIp) || !IpHelper.TryConvertIpToUint(ban.PlayerIp, out uint ipUInt) || !_ipIndex.TryGetValue(ipUInt, out List? ipBans)) { continue; } ipBans.RemoveAll(b => b.Id == id); if (ipBans.Count == 0) { _ipIndex.TryRemove(ipUInt, out _); } } } if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) { IEnumerable ipHistory = (await connection.QueryAsync( "SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300", new { lastUpdate = _lastUpdateTime })); foreach (IGrouping? group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid)) { HashSet ipSet = new HashSet( group .GroupBy(x => x.Address) .Select(g => { IpHistoryRow latest = g.MaxBy(x => x.Used_at)!; // group is never empty return new IpRecord( g.Key, latest.Used_at, NameOrDefault(latest.Name, CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown") ); }), new IpRecordComparer() ); _playerIpsCache.AddOrUpdate( (ulong)group.Key, _ => ipSet, (_, existingSet) => { foreach (IpRecord newEntry in ipSet) { existingSet.Remove(newEntry); existingSet.Add(newEntry); } return existingSet; }); } } // Update cache with new/modified bans bool needsRebuild = false; foreach (BanRecord ban in updatedBans) { if (_banCache.TryGetValue(ban.Id, out BanRecord? oldBan) && oldBan.Status != ban.Status) { // Ban status changed (e.g., ACTIVE -> EXPIRED/UNBANNED), need to rebuild indexes needsRebuild = true; } _banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban); } // Rebuild indexes if there were updates or status changes if (updatedBans.Any() || needsRebuild) { RebuildIndexes(); } _lastUpdateTime = Time.ActualDateTime().AddSeconds(-1); } catch (Exception) { } } /// /// Rebuilds the internal indexes for fast lookup of active bans by Steam ID and IP address. /// Clears and repopulates both indexes based on the current in-memory ban cache. /// private void RebuildIndexes() { // SetBanStatus (unban thread) and RefreshCacheAsync (timer thread) can both get here lock (_indexLock) { RebuildIndexesCore(); } } /// /// Updates the status of the given bans in the cache and rebuilds the indexes, /// so unbans take effect immediately instead of waiting for the next refresh. /// /// Ban ids to update. /// New status. public void SetBanStatus(IEnumerable banIds, BanStatus status) { bool changed = false; foreach (int id in banIds) { if (!_banCache.TryGetValue(id, out BanRecord? ban) || ban.StatusEnum == status) { continue; } _banCache[id] = ban with { Status = status.ToString() }; changed = true; } if (changed) { RebuildIndexes(); } } private void RebuildIndexesCore() { ConcurrentDictionary> steamIdIndex = new ConcurrentDictionary>(); ConcurrentDictionary> ipIndex = new ConcurrentDictionary>(); // Optimization: Cache config value to avoid repeated property access int banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType; bool checkIpBans = banType != 0; // Optimization: Pre-filter only ACTIVE bans to avoid checking status in loop IEnumerable activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE); foreach (BanRecord? ban in activeBans) { // Index by Steam ID if (ban.PlayerSteamId.HasValue) { ulong steamId = ban.PlayerSteamId.Value; if (!steamIdIndex.TryGetValue(steamId, out List? steamList)) { steamList = new List(); steamIdIndex[steamId] = steamList; } steamList.Add(ban); } // Index by IP (only if IP bans are enabled) if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) && IpHelper.TryConvertIpToUint(ban.PlayerIp, out uint ipUInt)) { if (!ipIndex.TryGetValue(ipUInt, out List? ipList)) { ipList = new List(); ipIndex[ipUInt] = ipList; } ipList.Add(ban); } } // Reference swap is atomic; readers finish on the old index or start on the complete new one Volatile.Write(ref _steamIdIndex, steamIdIndex); Volatile.Write(ref _ipIndex, ipIndex); } /// /// Retrieves all ban records currently stored in the cache. /// /// List of all objects. public List GetAllBans() => _banCache.Values.ToList(); /// /// Retrieves only active ban records from the cache. /// /// List of active objects. public List GetActiveBans() => _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE).ToList(); /// /// Retrieves all ban records for a specific player by their Steam ID. /// /// 64-bit Steam ID of the player. /// List of objects associated with the Steam ID. // _steamIdIndex holds only ACTIVE bans, so scan the full cache for history (expired/unbanned included). // ponytail: O(n) over all cached bans per call; add an all-status index if this gets hot. public List GetPlayerBansBySteamId(ulong steamId) => _banCache.Values.Where(b => b.PlayerSteamId == steamId).ToList(); /// /// Gets all known Steam accounts that have used the specified IP address. /// /// The IP address to search for, in string format. /// /// List of tuples containing the Steam ID, last used time, and player name for each matching entry. /// public List<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress) { uint ipAsUint = IpHelper.IpToUint(ipAddress); List<(ulong, DateTime, string)> results = new List<(ulong, DateTime, string)>(); // Optimization: Direct lookup using HashSet.Contains instead of TryGetValue IpRecord searchRecord = new IpRecord(ipAsUint, default, null!); foreach ((ulong steamId, HashSet? ipSet) in _playerIpsCache) { // Optimization: Single pass through the set foreach (IpRecord entry in ipSet) { if (entry.Ip == ipAsUint) { 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; } uint ipUInt = IpHelper.IpToUint(ipAddress); return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt); } // public bool IsPlayerBanned(ulong? steamId, string? ipAddress) // { // if (steamId != null && _steamIdIndex.ContainsKey(steamId.Value)) // return true; // // if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) // return false; // // if (string.IsNullOrEmpty(ipAddress) || !IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)) // return false; // // return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt); // } /// /// Checks if a player is currently banned by Steam ID or IP address. /// If a partial ban record is found, updates it with the latest player information. /// /// Name of the player attempting to connect. /// Optional 64-bit Steam ID of the player. /// Optional IP address of the player. /// True if the player is banned, otherwise false. public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress) { BanRecord? record; if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out List? steamRecords)) { record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); if (record != null) { // Double-check the ban is still active in cache (handle race conditions) if (_banCache.TryGetValue(record.Id, out BanRecord? cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE) { 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 || string.IsNullOrEmpty(ipAddress)) { return false; } if (string.IsNullOrEmpty(ipAddress) || !IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt) || _cachedIgnoredIps.Contains(ipUInt) || !_ipIndex.TryGetValue(ipUInt, out List? 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 BanRecord? 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; } // public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress) // { // if (_steamIdIndex.ContainsKey(steamId)) // return true; // // if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) // return false; // // if (!_playerIpsCache.TryGetValue(steamId, out var ipData)) // return false; // // // var now = Time.ActualDateTime(); // var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans); // var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; // // if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint)) // { // if (!_cachedIgnoredIps.Contains(ipAsUint)) // { // ipData.Add(new IpRecord( // ipAsUint, // Time.ActualDateTime().AddSeconds(-2), // unknownName // )); // } // } // // // foreach (var ipRecord in ipData) // // { // // // Skip if too old or in ignored list // // if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip)) // // continue; // // // // // Check if IP is banned // // if (_ipIndex.ContainsKey(ipRecord.Ip)) // // return true; // // } // // foreach (var ipRecord in ipData) // { // if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip)) // continue; // // if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords)) continue; // // var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); // if (activeBan == null) continue; // // if (!string.IsNullOrEmpty(activeBan.PlayerName) && activeBan.PlayerSteamId.HasValue) return true; // // _ = Task.Run(() => UpdatePlayerData( // activeBan.PlayerName, // steamId, // ipAddress // )); // // if (string.IsNullOrEmpty(activeBan.PlayerName) && !string.IsNullOrEmpty(unknownName)) // activeBan.PlayerName = unknownName; // // activeBan.PlayerSteamId ??= steamId; // // return true; // } // // return false; // } /// /// Checks if the player or any IP previously associated with them is currently banned. /// Also updates ban records with missing player info if found. /// /// Current player name. /// 64-bit Steam ID of the player. /// Current IP address of the player (optional). /// True if the player or their known IPs are banned, otherwise false. public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress) { if (_steamIdIndex.TryGetValue(steamId, out List? steamBans)) { BanRecord? activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE); if (activeBan != null) { // Double-check the ban is still active in cache (handle race conditions) if (_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE) { if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) { _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); } return true; } } } if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress)) { return false; } if (!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt)) { return false; } if (_cachedIgnoredIps.Contains(ipUInt)) { return false; } // Direct ip ban (ban record has player_ip set) if (_ipIndex.TryGetValue(ipUInt, out List? ipBanRecords)) { BanRecord? ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out BanRecord? cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE) { int expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans; if (expireOldIpBans <= 0 || ipBan.Created >= Time.ActualDateTime().AddDays(-expireOldIpBans)) { if (string.IsNullOrEmpty(ipBan.PlayerName)) { ipBan.PlayerName = playerName; } ipBan.PlayerSteamId ??= steamId; _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); return true; } } } // Multiaccount ban - check if other accounts using current ip are banned if (!_playerIpsCache.IsEmpty) { foreach ((ulong otherSteamId, HashSet? ipSet) in _playerIpsCache) { // Skip current player if (otherSteamId == steamId) { continue; } // Check if this ip is in the other accounts ip history if (ipSet.All(record => record.Ip != ipUInt)) { continue; } // Found another account using this ip - check if its banned if (!_steamIdIndex.TryGetValue(otherSteamId, out List? otherSteamBans)) { continue; } BanRecord? activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE); if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) || cachedBan.StatusEnum != BanStatus.ACTIVE) { continue; } _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); return true; } } // Multiaccount ban - check if this player used any ip where other banned accounts are connected // Search sa_players_ips for all accounts sharing the same ips as current player if (!CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) { return false; } if (!_playerIpsCache.TryGetValue(steamId, out HashSet? playerIps)) { return false; } // For each ip the player used (current or historical) foreach (IpRecord playerIpRecord in playerIps) { // Search sa_players_ips for other accounts using this same ip (as uint) foreach ((ulong otherSteamId, HashSet? otherIpSet) in _playerIpsCache) { if (otherSteamId == steamId) { continue; } // Check if this other account used the player ip if (otherIpSet.All(record => record.Ip != playerIpRecord.Ip)) { continue; } // Check if this other account is banned if (!_steamIdIndex.TryGetValue(otherSteamId, out List? otherSteamBans)) { continue; } BanRecord? activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE); if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) || cachedBan.StatusEnum != BanStatus.ACTIVE) { continue; } _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); return true; } } return false; } /// /// Checks if the given IP address is known (previously recorded) for the specified Steam ID. /// /// 64-bit Steam ID of the player. /// IP address to check. /// True if the IP is recorded for the player, otherwise false. public bool HasIpForPlayer(ulong steamId, string ipAddress) { if (string.IsNullOrWhiteSpace(ipAddress)) { return false; } if (!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUint)) { return false; } return _playerIpsCache.TryGetValue(steamId, out HashSet? ipData) && ipData.Contains(new IpRecord(ipUint, default, null!)); } // public bool HasIpForPlayer(ulong steamId, string ipAddress) // { // if (string.IsNullOrWhiteSpace(ipAddress)) // return false; // // return _playerIpsCache.TryGetValue(steamId, out var ipData) // && ipData.Any(x => x.Ip == IpHelper.IpToUint(ipAddress)); // } /// /// Updates existing active ban records in the database with the latest known player name and IP address. /// Also updates in-memory cache to reflect these changes. /// /// Current player name. /// Optional Steam ID of the player. /// Optional IP address of the player. /// Asynchronous task representing the update operation. private async Task UpdatePlayerData(string? playerName, ulong? steamId, string? ipAddress) { if (CS2_SimpleAdmin.DatabaseProvider == null) { return; } string 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;"; } string? playerIp = ipAddress; if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress) || CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress)) { playerIp = null; } var parameters = new { PlayerSteamID = steamId, PlayerIP = playerIp, PlayerName = playerName ?? string.Empty, CurrentTime = Time.ActualDateTime(), CS2_SimpleAdmin.ServerId }; await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync(); await connection.ExecuteAsync(baseSql, parameters); if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out List? steamRecords)) { foreach (BanRecord? rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE)) { if (string.IsNullOrEmpty(rec.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) { rec.PlayerIp = ipAddress; } if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName)) { rec.PlayerName = playerName; } } } if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt) && _ipIndex.TryGetValue(ipUInt, out List? ipRecords)) { foreach (BanRecord? rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE)) { if (!rec.PlayerSteamId.HasValue && steamId.HasValue) { rec.PlayerSteamId = steamId; } if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName)) { rec.PlayerName = playerName; } } } } private void Clear() { _steamIdIndex.Clear(); _ipIndex.Clear(); _banCache.Clear(); _playerIpsCache.Clear(); _cachedIgnoredIps.Clear(); } /// /// Clears and disposes of all cached data and marks the object as disposed. /// public void Dispose() { if (_disposed) { return; } Clear(); _disposed = true; } private static string NameOrDefault(string? name, string defaultName) { if (string.IsNullOrEmpty(name)) { return defaultName; } return name; } } public class IpRecordComparer : IEqualityComparer { public bool Equals(IpRecord x, IpRecord y) => x.Ip == y.Ip; public int GetHashCode(IpRecord obj) => obj.Ip.GetHashCode(); }