zio fork 1.8.2b-zio3: ban reasons on join, @css/bypassipban, spectators keep hidden
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.
This commit is contained in:
parent
6171162ae9
commit
2069c1a690
8 changed files with 142 additions and 25 deletions
|
|
@ -22,7 +22,7 @@ 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";
|
||||
public override string ModuleVersion => "1.8.2b-zio1";
|
||||
public override string ModuleVersion => "1.8.2b-zio3";
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ public partial class CS2_SimpleAdmin
|
|||
CachedPlayers.Remove(player);
|
||||
BotPlayers.Remove(player);
|
||||
SilentPlayers.Remove(player.Slot);
|
||||
PendingBanKicks.Remove(player.Slot);
|
||||
|
||||
if (player.IsBot)
|
||||
{
|
||||
|
|
@ -309,6 +310,10 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
var command = info.GetArg(0).ToLower();
|
||||
|
||||
// zio: a banned player waiting to be kicked can't talk or join a team.
|
||||
if (PendingBanKicks.Contains(player.Slot) && (command.Contains("say") || command == "jointeam"))
|
||||
return HookResult.Stop;
|
||||
|
||||
if (Config.OtherSettings.AdditionalCommandsToLog.Contains(command))
|
||||
Helper.LogCommand(player, info);
|
||||
|
||||
|
|
@ -405,21 +410,10 @@ public partial class CS2_SimpleAdmin
|
|||
if (remaining is null)
|
||||
player.SendLocalizedMessage(_localizer, $"sa_player_penalty_{kind}_active_perm", reason);
|
||||
else
|
||||
player.SendLocalizedMessage(_localizer, $"sa_player_penalty_{kind}_active", FormatRemaining(remaining.Value), reason);
|
||||
player.SendLocalizedMessage(_localizer, $"sa_player_penalty_{kind}_active", Helper.FormatRemaining(remaining.Value), reason);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatRemaining(TimeSpan remaining)
|
||||
{
|
||||
if (remaining.TotalMinutes < 1)
|
||||
return "less than a minute";
|
||||
var parts = new List<string>();
|
||||
if (remaining.Days > 0) parts.Add($"{remaining.Days}d");
|
||||
if (remaining.Hours > 0) parts.Add($"{remaining.Hours}h");
|
||||
if (remaining.Minutes > 0 && remaining.Days == 0) parts.Add($"{remaining.Minutes}m");
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
/*public HookResult OnCommandSay(CCSPlayerController? player, CommandInfo info)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.IsBot)
|
||||
|
|
@ -534,7 +528,9 @@ public partial class CS2_SimpleAdmin
|
|||
if (player == null || !player.IsValid || player.IsBot || !SilentPlayers.Contains(player.Slot))
|
||||
return HookResult.Continue;
|
||||
|
||||
if (@event is not { Oldteam: <= 1, Team: >= 1 }) return HookResult.Continue;
|
||||
// zio: only joining T or CT unhides. Moving to spectators doesn't, so a plugin can take a
|
||||
// hidden admin there (e.g. to close the team select screen) and keep them hidden.
|
||||
if (@event is not { Oldteam: <= 1, Team: >= 2 }) return HookResult.Continue;
|
||||
|
||||
SilentPlayers.Remove(player.Slot);
|
||||
SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false);
|
||||
|
|
|
|||
|
|
@ -132,6 +132,18 @@ internal static class Helper
|
|||
}
|
||||
}
|
||||
|
||||
// zio: "2d 4h", "3h 15m", "45m" or "less than a minute", for time left on a penalty.
|
||||
internal static string FormatRemaining(TimeSpan remaining)
|
||||
{
|
||||
if (remaining.TotalMinutes < 1)
|
||||
return "less than a minute";
|
||||
var parts = new List<string>();
|
||||
if (remaining.Days > 0) parts.Add($"{remaining.Days}d");
|
||||
if (remaining.Hours > 0) parts.Add($"{remaining.Hours}h");
|
||||
if (remaining.Minutes > 0 && remaining.Days == 0) parts.Add($"{remaining.Minutes}m");
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
public static void KickPlayer(int userId, NetworkDisconnectionReason reason = NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED, int delay = 0)
|
||||
{
|
||||
var player = Utilities.GetPlayerFromUserid(userId);
|
||||
|
|
|
|||
|
|
@ -478,8 +478,12 @@ internal class CacheManager: IDisposable
|
|||
/// <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)
|
||||
public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress) => IsPlayerBanned(playerName, steamId, ipAddress, out _);
|
||||
|
||||
// zio: also returns the ban that matched, so the player can be told why.
|
||||
public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress, out BanRecord? ban)
|
||||
{
|
||||
ban = null;
|
||||
BanRecord? record;
|
||||
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
|
||||
{
|
||||
|
|
@ -495,6 +499,7 @@ internal class CacheManager: IDisposable
|
|||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
}
|
||||
|
||||
ban = record;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -521,6 +526,7 @@ internal class CacheManager: IDisposable
|
|||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
}
|
||||
|
||||
ban = record;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -599,8 +605,12 @@ internal class CacheManager: IDisposable
|
|||
/// <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)
|
||||
public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress) => IsPlayerOrAnyIpBanned(playerName, steamId, ipAddress, out _);
|
||||
|
||||
// zio: also returns the ban that matched, so the player can be told why.
|
||||
public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress, out BanRecord? ban)
|
||||
{
|
||||
ban = null;
|
||||
if (_steamIdIndex.TryGetValue(steamId, out var steamBans))
|
||||
{
|
||||
var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
|
|
@ -612,6 +622,7 @@ internal class CacheManager: IDisposable
|
|||
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
|
||||
ban = activeBan;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -639,6 +650,7 @@ internal class CacheManager: IDisposable
|
|||
ipBan.PlayerName = playerName;
|
||||
ipBan.PlayerSteamId ??= steamId;
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
ban = ipBan;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -661,6 +673,7 @@ internal class CacheManager: IDisposable
|
|||
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
|
||||
cachedBan.StatusEnum != BanStatus.ACTIVE) continue;
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
ban = activeBan;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -696,6 +709,7 @@ internal class CacheManager: IDisposable
|
|||
continue;
|
||||
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
ban = activeBan;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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;
|
||||
|
|
@ -28,6 +29,51 @@ internal class PlayerManager
|
|||
/// 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)
|
||||
|
|
@ -43,6 +89,8 @@ internal class PlayerManager
|
|||
? 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;
|
||||
|
||||
|
|
@ -56,24 +104,52 @@ internal class PlayerManager
|
|||
await SavePlayerIpAddress(steamId, playerName, ipAddress);
|
||||
|
||||
// Always check bans first, regardless of PlayersInfo state
|
||||
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
|
||||
BanRecord? ban;
|
||||
bool isBanned;
|
||||
if (bypassIpBans)
|
||||
{
|
||||
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)
|
||||
};
|
||||
// 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}");
|
||||
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
|
||||
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;
|
||||
|
|
@ -299,9 +375,14 @@ internal class PlayerManager
|
|||
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;
|
||||
|
|
@ -352,7 +433,9 @@ internal class PlayerManager
|
|||
var steamId = player.SteamID;
|
||||
var ip = player.IpAddress?.Split(':')[0];
|
||||
|
||||
bool isBanned = banType switch
|
||||
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
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
// Player Management
|
||||
internal static readonly HashSet<int> SilentPlayers = [];
|
||||
// zio: banned players let in only to be told why, then kicked. They can't chat or join a team.
|
||||
internal static readonly HashSet<int> PendingBanKicks = [];
|
||||
internal static readonly Dictionary<ulong, string> RenamedPlayers = [];
|
||||
internal static readonly ConcurrentDictionary<ulong, PlayerInfo> PlayersInfo = [];
|
||||
internal static readonly List<CCSPlayerController> CachedPlayers = [];
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@
|
|||
"sa_player_penalty_silence_active": "{lightred}You are silenced for {grey}{0}{lightred}. Reason: {grey}{1}\n{grey}Team chat starting with {lightred}@{grey} still reaches online staff.",
|
||||
"sa_player_penalty_silence_active_perm": "{lightred}You are silenced permanently. Reason: {grey}{0}\n{grey}Team chat starting with {lightred}@{grey} still reaches online staff.",
|
||||
"sa_player_penalty_no_reason": "none given",
|
||||
"sa_player_ban_active": "{lightred}You are banned from this server for {grey}{0}{lightred}. Reason: {grey}{1}",
|
||||
"sa_player_ban_active_perm": "{lightred}You are banned from this server permanently. Reason: {grey}{0}",
|
||||
"sa_player_ban_active_other_account": "{grey}The ban is on another account that has played from your IP address.",
|
||||
"sa_player_ban_active_kick": "{grey}You will be disconnected in a few seconds.",
|
||||
|
||||
"sa_player_penalty_info_active_mute": "➔ Mute [{lightred}❌{default}] - Expire [{lightred}{0}{default}]",
|
||||
"sa_player_penalty_info_active_gag": "➔ Gag [{lightred}❌{default}] - Expire [{lightred}{0}{default}]",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code:
|
|||
place of "Your chat is blocked to: <end date>". New lang keys `sa_player_penalty_gag_active`,
|
||||
`..._gag_active_perm`, `..._silence_active`, `..._silence_active_perm` and
|
||||
`sa_player_penalty_no_reason`, in `lang/en.json` only (other languages fall back to it).
|
||||
- Moving to spectators doesn't unhide a hidden admin; only joining T or CT does.
|
||||
- A banned player is let in long enough to be told the reason and time left, then kicked after
|
||||
`KickTime`, instead of seeing only "Your client is not allowed to join this server".
|
||||
- New permission `@css/bypassipban`, for players or groups: IP bans, and bans on other accounts
|
||||
that share their IP, don't apply to them; a ban on their own SteamID still does. `@css/root`
|
||||
has it, as it has every `@css` flag.
|
||||
|
||||
Publish with `FORGEJO_TOKEN=... ./release.sh v<ModuleVersion>` (ModuleVersion is `1.8.2b-zioN`).
|
||||
It rebuilds from the committed source and uploads `SimpleAdmin-<tag>.tar.gz` to git.zio.sh
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue