Compare commits

...

15 Commits

Author SHA1 Message Date
Dawid Bepierszcz
eea700bfb4 Version bump and multi-account/IP ban fixes
Bump version to 1.7.9a and update package references (CounterStrikeSharp.API, Dapper, System.Linq.Async, ZLinq). Replace usages of PlayerConnectedState.PlayerConnected with PlayerConnectedState.Connected across commands, events, and helpers. Add DB method GetExpireOldPlayerIpsQuery (MySQL/SQLite) and call it to purge old player IP records during ban expiration. Improve CacheManager multi-account/ip ban detection (direct IP bans, cross-account IP checks) and adjust ban logic to respect expiration window. In PlayerManager: reduce semaphore, save player IP to DB before performing ban checks (add SavePlayerIpAddress), and always perform ban checks on connect. Simplify config upgrade logic in Helper.UpdateConfig to update the JSON file Version via JsonNode. Misc: adjust ban sound volume, minor exception catch cleanup, and add module folder to .gitignore.
2026-04-23 22:15:44 +02:00
Dawid Bepierszcz
91615ffc67 Update VERSION 2026-01-27 13:50:27 +01:00
Dawid Bepierszcz
eb9b438315 Update CS2-SimpleAdmin.cs 2026-01-27 13:47:41 +01:00
Dawid Bepierszcz
3d23b8981b Update VERSION 2026-01-27 12:21:07 +01:00
Dawid Bepierszcz
39cbfdab1e Update CS2-SimpleAdmin.cs 2026-01-27 12:20:56 +01:00
Dawid Bepierszcz
4599f08fd7 Update basecommands.cs 2026-01-27 12:20:29 +01:00
Dawid Bepierszcz
4c43f14c82 Update VERSION 2026-01-27 09:52:44 +01:00
Dawid Bepierszcz
f16f8cf1a5 Update CS2-SimpleAdmin.cs 2026-01-27 09:52:31 +01:00
Dawid Bepierszcz
193685826c Update ServerManager.cs 2026-01-27 09:51:12 +01:00
Dawid Bepierszcz
fe73fa9917 Refactor admin and server loading timing logic
Adjusted timers for admin and server data loading to improve reliability and reduce delays. Admin data is now loaded more promptly on map changes and during command execution. ServerManager now initializes the cache after setting the server ID, and redundant admin reloads have been removed. Version bumped to 1.7.8-beta-10.
2026-01-26 01:19:25 +01:00
Dawid Bepierszcz
bdada2df1e Update VERSION 2026-01-25 14:41:33 +01:00
Dawid Bepierszcz
310a43fcd9 Update CS2-SimpleAdmin.cs 2026-01-25 14:39:01 +01:00
Dawid Bepierszcz
58243e813a Update StatusBlocker plugin binaries
Replaced StatusBlocker-v1.1.4 binaries for both Linux and Windows in the METAMOD PLUGIN directory. This may include bug fixes or improvements in the updated plugin versions.
2026-01-25 14:36:47 +01:00
Dawid Bepierszcz
d53446e0fe Bump version to 1.7.8-beta-8-recompiled
Updated the ModuleVersion string to reflect a recompiled build. No other changes were made.
2026-01-25 14:10:57 +01:00
Dawid Bepierszcz
2404c1bc03 Update dependencies and StatusBlocker plugin version
Upgraded several NuGet packages in CS2-SimpleAdmin and CS2-SimpleAdminApi projects, including CounterStrikeSharp.API, MySqlConnector, System.Linq.Async, and ZLinq. Replaced StatusBlocker v1.1.3 plugin files with v1.1.4 for both Linux and Windows in the StealthModule.
2026-01-25 14:09:12 +01:00
23 changed files with 198 additions and 126 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.sln.DotSetti
CS2-SimpleAdmin_BanSoundModule — kopia CS2-SimpleAdmin_BanSoundModule — kopia
*.user *.user
CLAUDE.md CLAUDE.md
/Modules/CS2-SimpleAdmin_BanSoundModule

View File

@@ -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 ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
public override string ModuleAuthor => "daffyy"; public override string ModuleAuthor => "daffyy";
public override string ModuleVersion => "1.7.8-beta-8"; public override string ModuleVersion => "1.7.9a";
public override void Load(bool hotReload) public override void Load(bool hotReload)
{ {
@@ -46,7 +46,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
CachedPlayers.Clear(); CachedPlayers.Clear();
BotPlayers.Clear(); BotPlayers.Clear();
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && p.Connected == PlayerConnectedState.PlayerConnected && !p.IsHLTV).ToArray()) foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && p is { Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToArray())
{ {
if (!player.IsBot) if (!player.IsBot)
PlayerManager.LoadPlayerData(player, true); PlayerManager.LoadPlayerData(player, true);
@@ -83,9 +83,9 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
Unload(false); Unload(false);
} }
AddTimer(6.0f, () => ReloadAdmins(null));
RegisterEvents(); RegisterEvents();
AddTimer(0.5f, RegisterCommands.InitializeCommands); AddTimer(0.5f, RegisterCommands.InitializeCommands);
AddTimer(3.0f, () => ReloadAdmins(null));
if (!CoreConfig.UnlockConCommands) if (!CoreConfig.UnlockConCommands)
{ {

View File

@@ -19,16 +19,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.346"> <PackageReference Include="CounterStrikeSharp.API" Version="1.0.367">
<PrivateAssets>none</PrivateAssets> <PrivateAssets>none</PrivateAssets>
<ExcludeAssets>runtime</ExcludeAssets> <ExcludeAssets>runtime</ExcludeAssets>
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Dapper" Version="2.1.66" /> <PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="MySqlConnector" Version="2.5.0-beta.1" /> <PackageReference Include="MySqlConnector" Version="2.5.0" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Linq.Async" Version="7.0.0-preview.1.g24680b5469" /> <PackageReference Include="System.Linq.Async" Version="7.0.1" />
<PackageReference Include="ZLinq" Version="1.5.3" /> <PackageReference Include="ZLinq" Version="1.5.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -27,7 +27,7 @@ public partial class CS2_SimpleAdmin
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, IsHLTV: false }).ToList(); var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToList();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0) if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{ {
@@ -373,7 +373,7 @@ public partial class CS2_SimpleAdmin
var targets = GetTarget(command); var targets = GetTarget(command);
if (targets == null) return; if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected && !player.IsHLTV).ToList(); var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.Connected && !player.IsHLTV).ToList();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0) if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{ {

View File

@@ -507,7 +507,7 @@ public partial class CS2_SimpleAdmin
var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json"); var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json");
var groupsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/groups.json"); var groupsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/groups.json");
await Server.NextWorldUpdateAsync(() => await Server.NextWorldUpdateAsync(() =>
{ {
AddTimer(1, () => AddTimer(1, () =>
{ {
@@ -521,7 +521,7 @@ public partial class CS2_SimpleAdmin
_logger?.LogInformation("Loaded admins!"); _logger?.LogInformation("Loaded admins!");
}); });
}); });
}); });
//_ = _adminManager.GiveAllGroupsFlags(); //_ = _adminManager.GiveAllGroupsFlags();
//_ = _adminManager.GiveAllFlags(); //_ = _adminManager.GiveAllFlags();

View File

@@ -44,7 +44,7 @@ public partial class CS2_SimpleAdmin
/// <param name="command">Optional command info for logging.</param> /// <param name="command">Optional command info for logging.</param>
internal static void Slay(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null) internal static void Slay(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{ {
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) return; if (!player.IsValid || player.Connected != PlayerConnectedState.Connected) return;
if (!caller.CanTarget(player)) return; if (!caller.CanTarget(player)) return;
// Set default caller name if not provided // Set default caller name if not provided
@@ -93,7 +93,7 @@ public partial class CS2_SimpleAdmin
playersToTarget.ForEach(player => playersToTarget.ForEach(player =>
{ {
if (player.Connected != PlayerConnectedState.PlayerConnected) if (player.Connected != PlayerConnectedState.Connected)
return; return;
if (caller!.CanTarget(player)) if (caller!.CanTarget(player))
@@ -207,7 +207,7 @@ public partial class CS2_SimpleAdmin
internal static void ChangeTeam(CCSPlayerController? caller, CCSPlayerController player, string teamName, CsTeam teamNum, bool kill, CommandInfo? command = null) internal static void ChangeTeam(CCSPlayerController? caller, CCSPlayerController player, string teamName, CsTeam teamNum, bool kill, CommandInfo? command = null)
{ {
// Check if the player is valid and connected // Check if the player is valid and connected
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) if (!player.IsValid || player.Connected != PlayerConnectedState.Connected)
return; return;
// Ensure the caller can target the player // Ensure the caller can target the player
@@ -284,7 +284,7 @@ public partial class CS2_SimpleAdmin
playersToTarget.ForEach(player => playersToTarget.ForEach(player =>
{ {
// Check if the player is connected and can be targeted // Check if the player is connected and can be targeted
if (player.Connected != PlayerConnectedState.PlayerConnected || !caller!.CanTarget(player)) if (player.Connected != PlayerConnectedState.Connected || !caller!.CanTarget(player))
return; return;
// Determine message key and arguments for the rename notification // Determine message key and arguments for the rename notification
@@ -330,7 +330,7 @@ public partial class CS2_SimpleAdmin
playersToTarget.ForEach(player => playersToTarget.ForEach(player =>
{ {
// Check if the player is connected and can be targeted // Check if the player is connected and can be targeted
if (player.Connected != PlayerConnectedState.PlayerConnected || !caller!.CanTarget(player)) if (player.Connected != PlayerConnectedState.Connected || !caller!.CanTarget(player))
return; return;
// Determine message key and arguments for the rename notification // Determine message key and arguments for the rename notification
@@ -379,7 +379,7 @@ public partial class CS2_SimpleAdmin
return; return;
destinationPlayer = targets.Players.FirstOrDefault(p => destinationPlayer = targets.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null || !caller.CanTarget(destinationPlayer) || caller.PlayerPawn.Value == null) if (destinationPlayer == null || !caller.CanTarget(destinationPlayer) || caller.PlayerPawn.Value == null)
return; return;
@@ -399,7 +399,7 @@ public partial class CS2_SimpleAdmin
return; return;
playersToTeleport = targets.Players playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p))
.ToList(); .ToList();
if (!playersToTeleport.Any()) if (!playersToTeleport.Any())
@@ -476,7 +476,7 @@ public partial class CS2_SimpleAdmin
destinationPlayer = caller; destinationPlayer = caller;
playersToTeleport = targets.Players playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p))
.ToList(); .ToList();
} }
else else
@@ -486,7 +486,7 @@ public partial class CS2_SimpleAdmin
return; return;
destinationPlayer = destination.Players.FirstOrDefault(p => destinationPlayer = destination.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null) if (destinationPlayer == null)
return; return;
@@ -497,7 +497,7 @@ public partial class CS2_SimpleAdmin
return; return;
playersToTeleport = targets.Players playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p)) .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p))
.ToList(); .ToList();
} }

View File

@@ -40,6 +40,7 @@ public interface IDatabaseProvider
string GetUpdateBanStatusQuery(); string GetUpdateBanStatusQuery();
string GetExpireBansQuery(bool multiServer); string GetExpireBansQuery(bool multiServer);
string GetExpireIpBansQuery(bool multiServer); string GetExpireIpBansQuery(bool multiServer);
string GetExpireOldPlayerIpsQuery();
// MuteManager // MuteManager
string GetAddMuteQuery(bool includePlayerName); string GetAddMuteQuery(bool includePlayerName);

View File

@@ -251,6 +251,11 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime" ? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime"
: "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid"; : "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
} }
public string GetExpireOldPlayerIpsQuery()
{
return "DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
}
public string GetAddMuteQuery(bool includePlayerName) => public string GetAddMuteQuery(bool includePlayerName) =>
includePlayerName includePlayerName

View File

@@ -166,6 +166,9 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
multiServer multiServer
? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime" ? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime"
: "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid"; : "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
public string GetExpireOldPlayerIpsQuery() =>
"DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
public string GetAdminsQuery() => public string GetAdminsQuery() =>
""" """

View File

@@ -259,7 +259,7 @@ public partial class CS2_SimpleAdmin
{ {
var player = Utilities.GetPlayerFromSteamId(list.Key); var player = Utilities.GetPlayerFromSteamId(list.Key);
if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.Connected)
continue; continue;
if (player.PlayerName.Equals(list.Value)) if (player.PlayerName.Equals(list.Value))
@@ -328,7 +328,7 @@ public partial class CS2_SimpleAdmin
? Utilities.GetPlayerFromUserid(userId) ? Utilities.GetPlayerFromUserid(userId)
: null; : null;
if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.PlayerConnected) if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.Connected)
return HookResult.Continue; return HookResult.Continue;
return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue; return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue;
@@ -448,13 +448,13 @@ public partial class CS2_SimpleAdmin
private void OnMapStart(string mapName) private void OnMapStart(string mapName)
{ {
if (!ServerLoaded || ServerId == null)
AddTimer(2.0f, OnGameServerSteamAPIActivated);
if (Config.OtherSettings.ReloadAdminsEveryMapChange && ServerLoaded && ServerId != null) if (Config.OtherSettings.ReloadAdminsEveryMapChange && ServerLoaded && ServerId != null)
AddTimer(5.0f, () => ReloadAdmins(null)); ReloadAdmins(null);
AddTimer(1.0f, ServerManager.CheckHibernationStatus); AddTimer(1.0f, ServerManager.CheckHibernationStatus);
if (!ServerLoaded || ServerId == null)
AddTimer(1.5f, OnGameServerSteamAPIActivated);
// AddTimer(34, () => // AddTimer(34, () =>
// { // {
@@ -473,7 +473,7 @@ public partial class CS2_SimpleAdmin
var player = @event.Userid; var player = @event.Userid;
if (player?.UserId == null || !player.IsValid || player.IsHLTV || if (player?.UserId == null || !player.IsValid || player.IsHLTV ||
player.Connected != PlayerConnectedState.PlayerConnected || !PlayersInfo.ContainsKey(player.SteamID) || player.Connected != PlayerConnectedState.Connected || !PlayersInfo.ContainsKey(player.SteamID) ||
@event.Attacker == null) @event.Attacker == null)
return HookResult.Continue; return HookResult.Continue;

View File

@@ -11,12 +11,14 @@ using CounterStrikeSharp.API.ValveConstants.Protobuf;
using CS2_SimpleAdminApi; using CS2_SimpleAdminApi;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using CounterStrikeSharp.API.Core.Plugin.Host; using CounterStrikeSharp.API.Core.Plugin.Host;
using CounterStrikeSharp.API.Modules.Entities.Constants; using CounterStrikeSharp.API.Modules.Entities.Constants;
@@ -64,7 +66,7 @@ internal static class Helper
public static List<CCSPlayerController> GetValidPlayers() public static List<CCSPlayerController> GetValidPlayers()
{ {
return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().Where(p => p.IsValid && p.Connected == PlayerConnectedState.PlayerConnected).ToList(); return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().Where(p => p.IsValid && p.Connected == PlayerConnectedState.Connected).ToList();
} }
public static List<CCSPlayerController> GetValidPlayersWithBots() public static List<CCSPlayerController> GetValidPlayersWithBots()
@@ -855,26 +857,34 @@ internal static class Helper
} }
} }
public static void UpdateConfig<T>(T config) where T : BasePluginConfig, new() public static void UpdateConfig(BasePluginConfig config)
{ {
// get newest config version // get newest config version
var newCfgVersion = new T().Version; var configType = config.GetType();
var newCfgVersion = ((BasePluginConfig)Activator.CreateInstance(configType)!).Version;
// loaded config is up to date // loaded config is up to date
if (config.Version == newCfgVersion) if (config.Version == newCfgVersion)
return; return;
// update the version // Load existing JSON file and update version property
config.Version = newCfgVersion; if (!File.Exists(CfgPath))
return;
// serialize the updated config back to json var json = File.ReadAllText(CfgPath);
var updatedJsonContent = JsonSerializer.Serialize(config, var node = JsonNode.Parse(json);
new JsonSerializerOptions
if (node != null)
{
node["Version"] = newCfgVersion;
var updatedJsonContent = node.ToJsonString(new JsonSerializerOptions
{ {
WriteIndented = true, WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}); });
File.WriteAllText(CfgPath, updatedJsonContent);
File.WriteAllText(CfgPath, updatedJsonContent);
}
} }
public static void TryLogCommandOnDiscord(CCSPlayerController? caller, string commandString) public static void TryLogCommandOnDiscord(CCSPlayerController? caller, string commandString)

View File

@@ -431,6 +431,9 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans); var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode); sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId }); await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId });
sql = databaseProvider.GetExpireOldPlayerIpsQuery();
await connection.ExecuteAsync(sql, new { ipBansTime });
} }
} }
catch (Exception) catch (Exception)

View File

@@ -626,34 +626,81 @@ internal class CacheManager: IDisposable
if (_cachedIgnoredIps.Contains(ipUInt)) if (_cachedIgnoredIps.Contains(ipUInt))
return false; return false;
if (!_ipIndex.TryGetValue(ipUInt, out var ipBanRecords)) // Direct ip ban (ban record has player_ip set)
return false; if (_ipIndex.TryGetValue(ipUInt, out var ipBanRecords))
var ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (ipBan == null)
return false;
if (!_banCache.TryGetValue(ipBan.Id, out var cachedIpBan) || cachedIpBan.StatusEnum != BanStatus.ACTIVE)
return false;
var expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans;
if (expireOldIpBans > 0)
{ {
var cutoff = Time.ActualDateTime().AddDays(-expireOldIpBans); var ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (ipBan.Created < cutoff) if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out var cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE)
return false; {
var 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;
}
}
} }
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; // Multiaccount ban - check if other accounts using current ip are banned
if (!_playerIpsCache.IsEmpty)
{
foreach (var (otherSteamId, ipSet) in _playerIpsCache)
{
// Skip current player
if (otherSteamId == steamId)
continue;
if (string.IsNullOrEmpty(ipBan.PlayerName)) // Check if this ip is in the other accounts ip history
ipBan.PlayerName = playerName; if (ipSet.All(record => record.Ip != ipUInt)) continue;
// Found another account using this ip - check if its banned
if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans)) continue;
var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
cachedBan.StatusEnum != BanStatus.ACTIVE) continue;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
}
ipBan.PlayerSteamId ??= steamId; // 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;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress)); if (!_playerIpsCache.TryGetValue(steamId, out var playerIps))
return false;
return true; // For each ip the player used (current or historical)
foreach (var playerIpRecord in playerIps)
{
// Search sa_players_ips for other accounts using this same ip (as uint)
foreach (var (otherSteamId, 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 var otherSteamBans))
continue;
var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
cachedBan.StatusEnum != BanStatus.ACTIVE)
continue;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
}
return false;
} }
/// <summary> /// <summary>

View File

@@ -13,7 +13,7 @@ namespace CS2_SimpleAdmin.Managers;
internal class PlayerManager internal class PlayerManager
{ {
private readonly SemaphoreSlim _loadPlayerSemaphore = new(10); private readonly SemaphoreSlim _loadPlayerSemaphore = new(6);
private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config; private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config;
/// <summary> /// <summary>
@@ -51,69 +51,41 @@ internal class PlayerManager
try try
{ {
await _loadPlayerSemaphore.WaitAsync(); await _loadPlayerSemaphore.WaitAsync();
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
// Save ip address before ban check
await SavePlayerIpAddress(steamId, playerName, ipAddress);
// Always check bans first, regardless of PlayersInfo state
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
{ {
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch 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)
};
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)
{
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] KICKING {playerName} ({steamId})");
await Server.NextWorldUpdateAsync(() =>
{ {
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null), CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Executing kick for {playerName}");
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, });
ipAddress)
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress)
};
// CS2_SimpleAdmin._logger?.LogInformation($"Player {playerName} ({steamId} - {ipAddress}) is banned? {isBanned.ToString()}"); return;
if (isBanned)
{
await Server.NextWorldUpdateAsync(() =>
{
// CS2_SimpleAdmin._logger?.LogInformation($"Kicking {playerName}");
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
});
return;
}
} }
if (fullConnect) if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
{ {
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress); var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo; CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null && if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null)
CS2_SimpleAdmin.PlayersInfo[steamId] != null)
{ {
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
// Eliminates the need for SELECT COUNT and duplicate UPDATE queries
var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64;
var ipUint = IpHelper.IpToUint(ipAddress);
// Use database-specific UPSERT query (handles MySQL vs SQLite syntax differences)
var upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
await connection.ExecuteAsync(upsertQuery, new
{
SteamID = steamId64,
playerName,
IPAddress = ipUint
});
// // Cache will be updated on next refresh cycle
// if (!CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(steamId, ipAddress))
// {
// // IP association will be reflected after cache refresh
// }
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(
$"Unable to save ip address for {playerInfo.Name} ({ipAddress}): {ex.Message}");
}
playerInfo.AccountsAssociated = playerInfo.AccountsAssociated =
CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable() CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable()
.Select(x => (x.SteamId, x.PlayerName)).ToList() ?? []; .Select(x => (x.SteamId, x.PlayerName)).ToList() ?? [];
@@ -200,7 +172,7 @@ internal class PlayerManager
AdminManager.PlayerHasPermissions( AdminManager.PlayerHasPermissions(
new SteamID(p.SteamID), new SteamID(p.SteamID),
"@css/ban")) && "@css/ban")) &&
p.Connected == PlayerConnectedState.PlayerConnected && p.Connected == PlayerConnectedState.Connected &&
!CS2_SimpleAdmin.AdminDisabledJoinComms !CS2_SimpleAdmin.AdminDisabledJoinComms
.Contains(p.SteamID))) .Contains(p.SteamID)))
{ {
@@ -247,13 +219,45 @@ internal class PlayerManager
_loadPlayerSemaphore.Release(); _loadPlayerSemaphore.Release();
} }
}); });
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name)) if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name))
{ {
player.Rename(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> /// <summary>
/// Periodically checks the status of online players and applies timers for speed, gravity, /// Periodically checks the status of online players and applies timers for speed, gravity,
/// and penalty expiration validation. /// and penalty expiration validation.

View File

@@ -31,7 +31,7 @@ public class ServerManager
/// </summary> /// </summary>
public void LoadServerData() public void LoadServerData()
{ {
CS2_SimpleAdmin.Instance.AddTimer(2.0f, () => CS2_SimpleAdmin.Instance.AddTimer(1.0f, () =>
{ {
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return; if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return;
@@ -103,14 +103,12 @@ public class ServerManager
CS2_SimpleAdmin.ServerId = serverId; CS2_SimpleAdmin.ServerId = serverId;
CS2_SimpleAdmin._logger?.LogInformation("Loaded server with ip {ip}", ipAddress); CS2_SimpleAdmin._logger?.LogInformation("Loaded server with ip {ip}", ipAddress);
if (CS2_SimpleAdmin.ServerId != null) CS2_SimpleAdmin.ServerLoaded = true;
{
await Server.NextWorldUpdateAsync(() => CS2_SimpleAdmin.Instance.ReloadAdmins(null));
}
CS2_SimpleAdmin.ServerLoaded = true;
if (CS2_SimpleAdmin.Instance.CacheManager != null) if (CS2_SimpleAdmin.Instance.CacheManager != null)
{
await CS2_SimpleAdmin.Instance.CacheManager.InitializeCacheAsync(); await CS2_SimpleAdmin.Instance.CacheManager.InitializeCacheAsync();
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -42,7 +42,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
return warnId; return warnId;
} }
catch(Exception e) catch(Exception)
{ {
return null; return null;
} }

View File

@@ -1 +1 @@
1.7.8-beta-8 1.7.9a

View File

@@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.346" /> <PackageReference Include="CounterStrikeSharp.API" Version="1.0.367" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -45,7 +45,7 @@ public class CS2_SimpleAdmin_BanSoundModule: BasePlugin
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsBot)) foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsBot))
{ {
var filter = new RecipientFilter(player); var filter = new RecipientFilter(player);
player?.EmitSound("bansound", volume: 0.9f, recipients: filter); player?.EmitSound("bansound", volume: 0.75f, recipients: filter);
} }
} }
} }