Refactor fun commands to external module

Commented out fun command implementations (noclip, godmode, freeze, unfreeze, resize) in funcommands.cs and removed their registration from RegisterCommands.cs. These commands are now intended to be provided by the new CS2-SimpleAdmin_FunCommands external module, improving modularity and maintainability.
This commit is contained in:
Dawid Bepierszcz 2025-10-19 03:12:58 +02:00
parent 2edacc2b3f
commit 78318102fe
69 changed files with 5943 additions and 1493 deletions

View file

@ -29,7 +29,6 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
try
{
var sql = databaseProvider.GetAddBanQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64,

View file

@ -70,39 +70,44 @@ internal class CacheManager: IDisposable
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
var ipHistory =
(await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC")).ToList();
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
{
var ipSet = group
.GroupBy(x => x.address)
.Select(g =>
{
var latest = g.MaxBy(x => x.used_at);
return new IpRecord(
g.Key,
latest.used_at,
string.IsNullOrEmpty(latest.name)
? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
: latest.name
);
})
.ToHashSet(new IpRecordComparer());
// Optimization: Load IP history and build cache in single pass
var ipHistory = await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY steamid, address, used_at DESC");
_playerIpsCache.AddOrUpdate(
group.Key,
_ => ipSet,
(_, existingSet) =>
{
foreach (var ip in ipSet)
{
existingSet.Remove(ip);
existingSet.Add(ip);
}
return existingSet;
});
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var currentSteamId = 0UL;
var currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
var latestIpTimestamps = new Dictionary<uint, DateTime>();
foreach (var record in ipHistory)
{
// When we encounter a new steamid, save the previous one
if (record.steamid != currentSteamId && currentSteamId != 0)
{
_playerIpsCache[currentSteamId] = currentIpSet;
currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
latestIpTimestamps.Clear();
}
currentSteamId = record.steamid;
// Only keep the latest timestamp for each IP
if (!latestIpTimestamps.TryGetValue(record.address, out var existingTimestamp) ||
record.used_at > existingTimestamp)
{
latestIpTimestamps[record.address] = record.used_at;
currentIpSet.Add(new IpRecord(
record.address,
record.used_at,
string.IsNullOrEmpty(record.name) ? unknownName : record.name
));
}
}
// Don't forget the last steamid
if (currentSteamId != 0)
{
_playerIpsCache[currentSteamId] = currentIpSet;
}
}
@ -150,8 +155,10 @@ internal class CacheManager: IDisposable
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<BanRecord> updatedBans;
var allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
// Optimization: Only get IDs for comparison if we need to check for deletions
// Most of the time bans are just added/updated, not deleted
HashSet<int>? allIds = null;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
@ -161,11 +168,19 @@ internal class CacheManager: IDisposable
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
status AS Status
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
""",
new { lastUpdate = _lastUpdateTime }
));
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0)
{
allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
}
updatedBans = updatedList;
}
else
{
@ -175,32 +190,47 @@ internal class CacheManager: IDisposable
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
status AS Status
FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC
""",
new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId }
));
}
foreach (var id in _banCache.Keys)
{
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
if (ban.PlayerSteamId != null &&
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans))
{
steamBans.RemoveAll(b => b.Id == id);
if (steamBans.Count == 0)
_steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _);
}
if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue;
// Optimization: Only fetch all IDs if there were updates
var updatedList = updatedBans.ToList();
if (updatedList.Count > 0)
{
ipBans.RemoveAll(b => b.Id == id);
if (ipBans.Count == 0)
_ipIndex.TryRemove(ipUInt, out _);
allIds = (await connection.QueryAsync<int>(
"SELECT id FROM sa_bans WHERE server_id = @serverId",
new { serverId = CS2_SimpleAdmin.ServerId }
)).ToHashSet();
}
updatedBans = updatedList;
}
// Optimization: Only process deletions if we have the full ID list
if (allIds != null)
{
foreach (var id in _banCache.Keys)
{
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
if (ban.PlayerSteamId != null &&
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans))
{
steamBans.RemoveAll(b => b.Id == id);
if (steamBans.Count == 0)
_steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _);
}
if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue;
{
ipBans.RemoveAll(b => b.Id == id);
if (ipBans.Count == 0)
_ipIndex.TryRemove(ipUInt, out _);
}
}
}
@ -245,12 +275,21 @@ internal class CacheManager: IDisposable
}
}
// Update cache with new/modified bans
var hasUpdates = false;
foreach (var ban in updatedBans)
{
_banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban);
hasUpdates = true;
}
RebuildIndexes();
// Always rebuild indexes if there were any updates
// This ensures status changes (ACTIVE -> UNBANNED) are reflected
if (hasUpdates)
{
RebuildIndexes();
}
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
}
catch (Exception)
@ -266,38 +305,38 @@ internal class CacheManager: IDisposable
{
_steamIdIndex.Clear();
_ipIndex.Clear();
foreach (var ban in _banCache.Values)
{
if (ban.StatusEnum != BanStatus.ACTIVE)
continue;
if (ban.PlayerSteamId != null)
// Optimization: Cache config value to avoid repeated property access
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
var checkIpBans = banType != 0;
// Optimization: Pre-filter only ACTIVE bans to avoid checking status in loop
var activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE);
foreach (var ban in activeBans)
{
// Index by Steam ID
if (ban.PlayerSteamId.HasValue)
{
var steamId = ban.PlayerSteamId;
_steamIdIndex.AddOrUpdate(
steamId.Value,
key => [ban],
(key, list) =>
{
list.Add(ban);
return list;
});
var steamId = ban.PlayerSteamId.Value;
if (!_steamIdIndex.TryGetValue(steamId, out var steamList))
{
steamList = new List<BanRecord>();
_steamIdIndex[steamId] = steamList;
}
steamList.Add(ban);
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) continue;
if (ban.PlayerIp != null &&
// Index by IP (only if IP bans are enabled)
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
{
_ipIndex.AddOrUpdate(
ipUInt,
key => [ban],
(key, list) =>
{
list.Add(ban);
return list;
});
if (!_ipIndex.TryGetValue(ipUInt, out var ipList))
{
ipList = new List<BanRecord>();
_ipIndex[ipUInt] = ipList;
}
ipList.Add(ban);
}
}
}
@ -332,16 +371,16 @@ internal class CacheManager: IDisposable
{
var ipAsUint = IpHelper.IpToUint(ipAddress);
var results = new List<(ulong, DateTime, string)>();
var comparer = _playerIpsCache.Comparer;
// Optimization: Direct lookup using HashSet.Contains instead of TryGetValue
var searchRecord = new IpRecord(ipAsUint, default, null!);
foreach (var (steamId, ipSet) in _playerIpsCache)
{
if (!ipSet.TryGetValue(new IpRecord(ipAsUint, Time.ActualDateTime(), "Unknown"), out var actualEntry)) continue;
results.Add((steamId, actualEntry.UsedAt, actualEntry.PlayerName));
// Optimization: Single pass through the set
foreach (var entry in ipSet)
{
if (entry.Ip == ipAsUint && !Equals(entry, actualEntry))
if (entry.Ip == ipAsUint)
{
results.Add((steamId, entry.UsedAt, entry.PlayerName));
}

View file

@ -95,66 +95,36 @@ internal class PlayerManager
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
if (CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(
steamId, ipAddress))
{
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP,
name = @playerName
WHERE steamid = @SteamID AND address = @IPAddress;
""";
await connection.ExecuteAsync(updateQuery, new
{
playerName,
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
else
{
const string selectQuery =
"SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;";
var recordExists = await connection.ExecuteScalarAsync<int>(selectQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
// Eliminates the need for SELECT COUNT and duplicate UPDATE queries
var steamId64 = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64;
var ipUint = IpHelper.IpToUint(ipAddress);
if (recordExists > 0)
{
const string updateQuery = """
UPDATE `sa_players_ips`
SET used_at = CURRENT_TIMESTAMP,
name = @playerName
WHERE steamid = @SteamID AND address = @IPAddress;
""";
await connection.ExecuteAsync(updateQuery, new
{
playerName,
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
else
{
const string insertQuery = """
INSERT INTO `sa_players_ips` (steamid, name, address, used_at)
VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP);
""";
await connection.ExecuteAsync(insertQuery, new
{
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
playerName,
IPAddress = IpHelper.IpToUint(ipAddress)
});
}
}
// MySQL: INSERT ... ON DUPLICATE KEY UPDATE pattern
const string upsertQuery = """
INSERT INTO `sa_players_ips` (steamid, name, address, used_at)
VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
used_at = CURRENT_TIMESTAMP,
name = @playerName;
""";
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}");
$"Unable to save ip address for {playerInfo.Name} ({ipAddress}): {ex.Message}");
}
playerInfo.AccountsAssociated =
@ -313,31 +283,6 @@ internal class PlayerManager
/// </remarks>
public void CheckPlayersTimer()
{
CS2_SimpleAdmin.Instance.AddTimer(0.12f, () =>
{
if (CS2_SimpleAdmin.SpeedPlayers.Count > 0)
{
foreach (var (player, speed) in CS2_SimpleAdmin.SpeedPlayers)
{
if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.SetSpeed(speed);
}
}
}
if (CS2_SimpleAdmin.GravityPlayers.Count > 0)
{
foreach (var (player, gravity) in CS2_SimpleAdmin.GravityPlayers)
{
if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
{
player.SetGravity(gravity);
}
}
}
}, TimerFlags.REPEAT);
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
{
#if DEBUG
@ -346,18 +291,26 @@ internal class PlayerManager
if (CS2_SimpleAdmin.DatabaseProvider == null)
return;
var tempPlayers = Helper.GetValidPlayers()
.Select(p => new
{
p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot,
})
.ToList();
// Optimization: Get players once and avoid allocating anonymous types
var validPlayers = Helper.GetValidPlayers();
if (validPlayers.Count == 0)
return;
// 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);
foreach (var p in validPlayers)
{
tempPlayers.Add((p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot));
}
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(),
@ -384,22 +337,33 @@ internal class PlayerManager
if (pluginInstance.CacheManager == null)
return;
var bannedPlayers = tempPlayers.AsValueEnumerable()
.Where(player =>
{
var playerName = player.PlayerName;
var steamId = player.SteamID;
var ip = player.IpAddress?.Split(':')[0];
return CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
{
0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null),
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip)
: pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip)
};
}).ToList();
// 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 = 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)
{
@ -410,32 +374,38 @@ internal class PlayerManager
}
}
if (_config.OtherSettings.TimeMode == 0)
if (config.TimeMode == 0)
{
var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList();
if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return;
// 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();
var penalizedSlots = players
.Where(player => PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
.Select(player => new
{
Player = player,
IsMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _),
IsSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _),
IsGagged = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _)
});
foreach (var entry in penalizedSlots)
foreach (var player in players)
{
if (!entry.IsMuted && !entry.IsSilenced)
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)
{
entry.Player.VoiceFlags = VoiceFlags.Normal;
player.VoiceFlags = VoiceFlags.Normal;
}
}

View file

@ -35,29 +35,43 @@ public class ServerManager
{
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return;
if (_getIpTryCount > 32 && Helper.GetServerIp().StartsWith("0.0.0.0") || string.IsNullOrEmpty(Helper.GetServerIp()))
// Optimization: Get server IP once and reuse
var serverIp = Helper.GetServerIp();
var isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0");
// Check if we've exceeded retry limit with invalid IP
if (_getIpTryCount > 32 && isInvalidIp)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load server data - can't fetch ip address!");
return;
}
var ipAddress = ConVar.Find("ip")?.StringValue;
// Optimization: Cache ConVar lookups
var ipConVar = ConVar.Find("ip");
var ipAddress = ipConVar?.StringValue;
// Use Helper IP if ConVar IP is invalid
if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))
{
ipAddress = Helper.GetServerIp();
ipAddress = serverIp;
if (_getIpTryCount <= 32 && (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0")))
// Retry if still invalid and under retry limit
if (_getIpTryCount <= 32 && isInvalidIp)
{
_getIpTryCount++;
LoadServerData();
return;
}
}
var address = $"{ipAddress}:{ConVar.Find("hostport")?.GetPrimitiveValue<int>()}";
var hostname = ConVar.Find("hostname")?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var rconPassword = ConVar.Find("rcon_password")?.StringValue ?? "";
// Optimization: Cache remaining ConVar lookups
var hostportConVar = ConVar.Find("hostport");
var hostnameConVar = ConVar.Find("hostname");
var rconPasswordConVar = ConVar.Find("rcon_password");
var address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue<int>()}";
var hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var rconPassword = rconPasswordConVar?.StringValue ?? "";
CS2_SimpleAdmin.IpAddress = address;
Task.Run(async () =>
@ -118,6 +132,8 @@ public class ServerManager
}
}
});
CS2_SimpleAdmin.SimpleAdminApi?.OnSimpleAdminReadyEvent();
});
}
}