Banned players are let in long enough to be told the ban's reason and time left, then kicked after KickTime, instead of only seeing "Your client is not allowed to join this server"; they can't chat, talk or join a team meanwhile. New permission @css/bypassipban (players or groups, implied by @css/root) skips IP and shared-IP bans, leaving only bans on their own SteamID, on join and in the periodic check. Moving to spectators no longer unhides a hidden admin; only joining T or CT does.
509 lines
No EOL
26 KiB
C#
509 lines
No EOL
26 KiB
C#
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.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();
|
|
}
|
|
});
|
|
|
|
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name))
|
|
{
|
|
player.Rename(name);
|
|
}
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
} |