From upstream 530f603: - css_unban updates the ban cache at once (and clears the engine banid with UnlockedCommands) instead of waiting up to a minute for the cache refresh. - css_prename is saved in sa_renames (migration 017) and re-applied on connect and name change, so it survives restarts. css_rename from a hidden admin renames again. - New css_history command and Players > History menu entry. Commands missing from an existing Commands.json are registered under their default name. - Migrations are copied with a glob, so 017 reaches the build output. Upstream's admin-loading changes, css_say change and css_penalties fix are left out.
597 lines
No EOL
29 KiB
C#
597 lines
No EOL
29 KiB
C#
using System.Globalization;
|
|
using CounterStrikeSharp.API;
|
|
using CounterStrikeSharp.API.Core;
|
|
using CounterStrikeSharp.API.Modules.Admin;
|
|
using CounterStrikeSharp.API.Modules.Entities;
|
|
using CounterStrikeSharp.API.Modules.Timers;
|
|
using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
|
using CS2_SimpleAdmin.Models;
|
|
using CS2_SimpleAdminApi;
|
|
using Dapper;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZLinq;
|
|
|
|
namespace CS2_SimpleAdmin.Managers;
|
|
|
|
internal class PlayerManager
|
|
{
|
|
private readonly SemaphoreSlim _loadPlayerSemaphore = new(6);
|
|
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>
|
|
// zio: players or groups with this aren't held back by IP bans, or by bans on other accounts that
|
|
// share their IP; only a ban on their own SteamID applies. @css/root has it too, as it has every
|
|
// @css flag.
|
|
internal const string IpBanBypassPermission = "@css/bypassipban";
|
|
|
|
// zio: the reason and time left of the ban that matched, which the ban cache doesn't hold.
|
|
private static async Task<(string? Reason, int Duration, DateTime? Ends)> GetBanDetails(int banId)
|
|
{
|
|
try
|
|
{
|
|
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
|
return default;
|
|
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
|
return await connection.QueryFirstOrDefaultAsync<(string? Reason, int Duration, DateTime? Ends)>(
|
|
"SELECT reason, duration, ends FROM sa_bans WHERE id = @Id", new { Id = banId });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogError($"Couldn't read ban {banId}: {ex.Message}");
|
|
return default;
|
|
}
|
|
}
|
|
|
|
// zio: tells a banned player why, in chat and on screen, before they're kicked.
|
|
private static void TellBanned(CCSPlayerController player, (string? Reason, int Duration, DateTime? Ends) ban, bool otherAccount)
|
|
{
|
|
var localizer = CS2_SimpleAdmin._localizer;
|
|
if (localizer == null)
|
|
return;
|
|
|
|
var reason = string.IsNullOrWhiteSpace(ban.Reason) ? localizer["sa_player_penalty_no_reason"].Value : ban.Reason;
|
|
var left = ban.Duration == 0 || ban.Ends == null
|
|
? null
|
|
: Helper.FormatRemaining(ban.Ends.Value - Time.ActualDateTime());
|
|
|
|
if (left == null)
|
|
player.SendLocalizedMessage(localizer, "sa_player_ban_active_perm", reason);
|
|
else
|
|
player.SendLocalizedMessage(localizer, "sa_player_ban_active", left, reason);
|
|
if (otherAccount)
|
|
player.SendLocalizedMessage(localizer, "sa_player_ban_active_other_account");
|
|
player.SendLocalizedMessage(localizer, "sa_player_ban_active_kick");
|
|
player.PrintToCenter(left == null ? $"Banned permanently: {reason}" : $"Banned for {left}: {reason}");
|
|
}
|
|
|
|
public void LoadPlayerData(CCSPlayerController player, bool fullConnect = false)
|
|
{
|
|
if (!player.UserId.HasValue)
|
|
{
|
|
Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
|
|
return;
|
|
}
|
|
|
|
var userId = player.UserId.Value;
|
|
var slot = player.Slot;
|
|
var steamId = player.SteamID;
|
|
var playerName = !string.IsNullOrEmpty(player.PlayerName)
|
|
? player.PlayerName
|
|
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
|
var ipAddress = player.IpAddress?.Split(":")[0];
|
|
// zio: read on the main thread; the ban check below runs on a worker thread.
|
|
var bypassIpBans = AdminManager.PlayerHasPermissions(new SteamID(steamId), IpBanBypassPermission);
|
|
|
|
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(steamId, out var renamedTo))
|
|
{
|
|
player.Rename(renamedTo);
|
|
}
|
|
|
|
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
|
|
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await _loadPlayerSemaphore.WaitAsync();
|
|
|
|
// Save ip address before ban check
|
|
await SavePlayerIpAddress(steamId, playerName, ipAddress);
|
|
|
|
// Always check bans first, regardless of PlayersInfo state
|
|
BanRecord? ban;
|
|
bool isBanned;
|
|
if (bypassIpBans)
|
|
{
|
|
// zio: only a ban on their own SteamID applies, never an IP ban or a ban on another
|
|
// account that shares their IP.
|
|
ban = CS2_SimpleAdmin.Instance.CacheManager.GetPlayerBansBySteamId(steamId)
|
|
.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
|
isBanned = ban != null;
|
|
}
|
|
else
|
|
{
|
|
isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
|
|
{
|
|
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null, out ban),
|
|
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
|
|
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId,
|
|
ipAddress, out ban)
|
|
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress, out ban)
|
|
};
|
|
}
|
|
|
|
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Player {playerName} ({steamId}) IP: {ipAddress} - BanType: {CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType} - CheckMultiAccounts: {CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp} - isBanned: {isBanned}");
|
|
|
|
if (isBanned)
|
|
{
|
|
// zio: the disconnect screen can't carry a reason, so let them finish connecting,
|
|
// tell them why in chat and on screen, then kick after KickTime.
|
|
if (!fullConnect)
|
|
return;
|
|
|
|
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] KICKING {playerName} ({steamId})");
|
|
var details = ban == null ? default : await GetBanDetails(ban.Id);
|
|
var otherAccount = CS2_SimpleAdmin.Instance.CacheManager.GetPlayerBansBySteamId(steamId)
|
|
.All(b => b.StatusEnum != BanStatus.ACTIVE);
|
|
await Server.NextWorldUpdateAsync(() =>
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Executing kick for {playerName}");
|
|
var banned = Utilities.GetPlayerFromUserid(userId);
|
|
if (banned is { IsValid: true })
|
|
{
|
|
CS2_SimpleAdmin.PendingBanKicks.Add(banned.Slot);
|
|
TellBanned(banned, details, otherAccount);
|
|
}
|
|
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED,
|
|
Math.Max(1, CS2_SimpleAdmin.Instance.Config.OtherSettings.KickTime));
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
|
|
{
|
|
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
|
|
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
|
|
|
|
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null)
|
|
{
|
|
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;
|
|
string? reason = mute.reason;
|
|
int passed = mute.passed is null ? 0 : (int)mute.passed;
|
|
switch (muteType)
|
|
{
|
|
// Apply mute penalty based on mute type
|
|
case "GAG":
|
|
PlayerPenaltyManager.AddPenalty(
|
|
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
|
|
PenaltyType.Gag, ends, duration, reason, duration - passed);
|
|
// 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, reason, duration - passed);
|
|
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, reason, duration - passed);
|
|
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.Connected &&
|
|
!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);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_loadPlayerSemaphore.Release();
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns every ban, gag/mute/silence and warn ever recorded for a SteamID, newest first.
|
|
/// Rows carry: type, status, reason, duration, created, ends, admin_name, lift_reason, lift_date, lift_admin.
|
|
/// </summary>
|
|
/// <param name="steamId">SteamID64 of the player, online or not.</param>
|
|
/// <param name="type">Optional filter: bans, gags, mutes, silences or warns.</param>
|
|
public async Task<List<dynamic>> GetPenaltyHistory(ulong steamId, string? type = null)
|
|
{
|
|
if (CS2_SimpleAdmin.DatabaseProvider == null) return [];
|
|
|
|
try
|
|
{
|
|
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
|
var sql = CS2_SimpleAdmin.DatabaseProvider.GetPenaltyHistoryQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
|
var rows = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId })).ToList();
|
|
|
|
// Filter word is the plural (bans, gags, mutes, silences, warns); rows carry BAN/GAG/MUTE/SILENCE/WARN
|
|
if (type != null)
|
|
rows.RemoveAll(r => !type.Equals((string)r.type + "s", StringComparison.OrdinalIgnoreCase));
|
|
|
|
return rows;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogError("Unable to load penalty history: {exception}", ex.Message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dapper gives DateTime on MySQL but can give a string on SQLite; accept both.
|
|
/// </summary>
|
|
internal static DateTime? ToDateTime(object? value) =>
|
|
value switch
|
|
{
|
|
DateTime d => d,
|
|
string s when DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) => parsed,
|
|
_ => null
|
|
};
|
|
|
|
/// <summary>
|
|
/// Loads permanent renames from the database into memory.
|
|
/// </summary>
|
|
public async Task LoadRenamedPlayers()
|
|
{
|
|
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
|
|
|
try
|
|
{
|
|
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
|
var rows = await connection.QueryAsync<(long steamId, string name)>(CS2_SimpleAdmin.DatabaseProvider.GetRenamesQuery());
|
|
|
|
await Server.NextWorldUpdateAsync(() =>
|
|
{
|
|
foreach (var (steamId, name) in rows)
|
|
CS2_SimpleAdmin.RenamedPlayers[(ulong)steamId] = name;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogError("Unable to load renames: {exception}", ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Persists or removes a permanent rename (css_prename) for a player.
|
|
/// </summary>
|
|
/// <param name="steamId">SteamID64 of the player.</param>
|
|
/// <param name="name">Forced name, or null/empty to remove the rename.</param>
|
|
public async Task SaveRenamedPlayer(ulong steamId, string? name)
|
|
{
|
|
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
|
|
|
try
|
|
{
|
|
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
|
if (string.IsNullOrEmpty(name))
|
|
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetDeleteRenameQuery(), new { steamId });
|
|
else
|
|
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetUpsertRenameQuery(), new { steamId, name });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogError("Unable to save rename: {exception}", ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves player's IP address to the database for multi-account detection.
|
|
/// This is called before ban checks to ensure IP is recorded even if player is banned.
|
|
/// </summary>
|
|
private async Task SavePlayerIpAddress(ulong steamId, string playerName, string? ipAddress)
|
|
{
|
|
if (!_config.OtherSettings.CheckMultiAccountsByIp || ipAddress == null || CS2_SimpleAdmin.DatabaseProvider == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
|
|
|
var steamId64 = steamId;
|
|
var ipUint = IpHelper.IpToUint(ipAddress);
|
|
|
|
var upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
|
|
|
|
await connection.ExecuteAsync(upsertQuery, new
|
|
{
|
|
SteamID = steamId64,
|
|
playerName,
|
|
IPAddress = ipUint
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogError(
|
|
$"Unable to save ip address for {playerName} ({ipAddress}): {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <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(5f, () =>
|
|
{
|
|
foreach (var (steamid, name) in CS2_SimpleAdmin.RenamedPlayers)
|
|
{
|
|
var player = Helper.GetPlayerFromSteamid64(steamid);
|
|
if (player == null || !player.IsValid || player.PlayerName == name) continue;
|
|
player.Rename(name);
|
|
}
|
|
}, TimerFlags.REPEAT);
|
|
|
|
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
|
|
{
|
|
#if DEBUG
|
|
CS2_SimpleAdmin._logger?.LogCritical("[OnMapStart] Expired check");
|
|
#endif
|
|
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
|
return;
|
|
|
|
// Optimization: Get players once and avoid allocating anonymous types
|
|
var validPlayers = Helper.GetValidPlayers();
|
|
// 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);
|
|
// zio: players with IpBanBypassPermission, read here on the main thread, only count bans on
|
|
// their own SteamID.
|
|
var bypassSteamIds = new HashSet<ulong>();
|
|
foreach (var p in validPlayers)
|
|
{
|
|
tempPlayers.Add((p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot));
|
|
if (AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), IpBanBypassPermission))
|
|
bypassSteamIds.Add(p.SteamID);
|
|
}
|
|
|
|
var pluginInstance = CS2_SimpleAdmin.Instance;
|
|
var config = _config.OtherSettings; // Cache config access
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
// Run all expire tasks in parallel
|
|
var expireTasks = new[]
|
|
{
|
|
pluginInstance.BanManager.ExpireOldBans(),
|
|
pluginInstance.MuteManager.ExpireOldMutes(),
|
|
pluginInstance.WarnManager.ExpireOldWarns(),
|
|
pluginInstance.CacheManager?.RefreshCacheAsync() ?? Task.CompletedTask,
|
|
pluginInstance.PermissionManager.DeleteOldAdmins()
|
|
};
|
|
|
|
await Task.WhenAll(expireTasks);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogError($"Error processing players timer tasks: {ex.Message}");
|
|
|
|
if (ex is AggregateException aggregate)
|
|
{
|
|
foreach (var inner in aggregate.InnerExceptions)
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogError($"Inner exception: {inner.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pluginInstance.CacheManager == null)
|
|
return;
|
|
|
|
// Optimization: Cache ban type and multi-account check to avoid repeated config access
|
|
var banType = config.BanType;
|
|
var checkMultiAccounts = config.CheckMultiAccountsByIp;
|
|
|
|
var bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>();
|
|
|
|
// Manual loop instead of LINQ - better performance
|
|
foreach (var player in tempPlayers)
|
|
{
|
|
var playerName = player.PlayerName;
|
|
var steamId = player.SteamID;
|
|
var ip = player.IpAddress?.Split(':')[0];
|
|
|
|
bool isBanned = bypassSteamIds.Contains(steamId)
|
|
? pluginInstance.CacheManager.GetPlayerBansBySteamId(steamId).Any(b => b.StatusEnum == BanStatus.ACTIVE)
|
|
: banType switch
|
|
{
|
|
0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null),
|
|
_ => checkMultiAccounts
|
|
? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip)
|
|
: pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip)
|
|
};
|
|
|
|
if (isBanned)
|
|
{
|
|
bannedPlayers.Add(player);
|
|
}
|
|
}
|
|
|
|
if (bannedPlayers.Count > 0)
|
|
{
|
|
foreach (var player in bannedPlayers)
|
|
{
|
|
if (!player.UserId.HasValue) continue;
|
|
await Server.NextWorldUpdateAsync(() =>
|
|
{
|
|
if (Helper.GetPlayerFromSteamid64(player.SteamID) != null)
|
|
Helper.KickPlayer((int)player.UserId,
|
|
NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (config.TimeMode == 0)
|
|
{
|
|
// Optimization: Manual projection instead of LINQ
|
|
var onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count);
|
|
foreach (var player in tempPlayers)
|
|
{
|
|
onlinePlayers.Add((player.SteamID, player.UserId, player.Slot));
|
|
}
|
|
|
|
if (onlinePlayers.Count > 0)
|
|
{
|
|
await pluginInstance.MuteManager.CheckOnlineModeMutes(onlinePlayers);
|
|
}
|
|
}
|
|
});
|
|
|
|
try
|
|
{
|
|
// Optimization: Process penalties without LINQ allocations
|
|
var players = Helper.GetValidPlayers();
|
|
foreach (var player in players)
|
|
{
|
|
if (!PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
|
|
continue;
|
|
|
|
var isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
|
|
var isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
|
|
|
|
// Only reset voice flags if not muted or silenced
|
|
if (!isMuted && !isSilenced)
|
|
{
|
|
player.VoiceFlags = VoiceFlags.Normal;
|
|
}
|
|
}
|
|
|
|
PlayerPenaltyManager.RemoveExpiredPenalties();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CS2_SimpleAdmin._logger?.LogError($"Unable to remove old penalties: {ex.Message}");
|
|
}
|
|
}, TimerFlags.REPEAT);
|
|
}
|
|
} |