Compare commits

..

No commits in common. "main" and "build-1.7.8-beta-10" have entirely different histories.

24 changed files with 459 additions and 269 deletions

View file

@ -35,7 +35,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 10.0.x dotnet-version: 8.0.x
- name: Get Version - name: Get Version
id: get_version id: get_version
@ -67,12 +67,28 @@ jobs:
- name: Zip Main Build Output - name: Zip Main Build Output
run: zip -r CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip ${{ env.OUTPUT_PATH }} run: zip -r CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip ${{ env.OUTPUT_PATH }}
- name: Extract & Zip StatusBlocker Linux
run: |
mkdir -p statusblocker-linux &&
tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-linux.tar.gz -C statusblocker-linux &&
cd statusblocker-linux &&
zip -r ../StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip ./*
- name: Extract & Zip StatusBlocker Windows
run: |
mkdir -p statusblocker-windows &&
tar -xzf Modules/CS2-SimpleAdmin_StealthModule/METAMOD\ PLUGIN/StatusBlocker-v*-windows.tar.gz -C statusblocker-windows &&
cd statusblocker-windows &&
zip -r ../StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip ./*
- name: Upload all artifacts - name: Upload all artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: CS2-SimpleAdmin-Build-Artifacts name: CS2-SimpleAdmin-Build-Artifacts
path: | path: |
CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip CS2-SimpleAdmin-${{ steps.get_version.outputs.VERSION }}.zip
StatusBlocker-linux-${{ steps.get_version.outputs.VERSION }}.zip
StatusBlocker-windows-${{ steps.get_version.outputs.VERSION }}.zip
publish: publish:
needs: build needs: build
@ -93,6 +109,8 @@ jobs:
with: with:
artifacts: | artifacts: |
CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}.zip CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}.zip
StatusBlocker-linux-${{ needs.build.outputs.build_version }}.zip
StatusBlocker-windows-${{ needs.build.outputs.build_version }}.zip
name: "CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}" name: "CS2-SimpleAdmin-${{ needs.build.outputs.build_version }}"
tag: "build-${{ needs.build.outputs.build_version }}" tag: "build-${{ needs.build.outputs.build_version }}"
body: | body: |
@ -103,5 +121,8 @@ jobs:
After the first launch, configure the plugin using the JSON config file at: After the first launch, configure the plugin using the JSON config file at:
addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json
- StatusBlocker:
Place the plugin files directly into the addons directory.
This plugin is a Metamod module for the StealthModule and does not require a subfolder.
Remember to restart or reload your game server after installing and configuring the plugins. Remember to restart or reload your game server after installing and configuring the plugins.

2
.gitignore vendored
View file

@ -10,5 +10,3 @@ 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
/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN

View file

@ -14,7 +14,7 @@ using MySqlConnector;
namespace CS2_SimpleAdmin; namespace CS2_SimpleAdmin;
[MinimumApiVersion(369)] [MinimumApiVersion(300)]
public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdminConfig> public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdminConfig>
{ {
internal static CS2_SimpleAdmin Instance { get; private set; } = new(); internal static CS2_SimpleAdmin Instance { get; private set; } = new();
@ -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.8.2b"; public override string ModuleVersion => "1.7.8-beta-10";
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 is { Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToArray()) foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && p.Connected == PlayerConnectedState.PlayerConnected && !p.IsHLTV).ToArray())
{ {
if (!player.IsBot) if (!player.IsBot)
PlayerManager.LoadPlayerData(player, true); PlayerManager.LoadPlayerData(player, true);
@ -55,6 +55,8 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
}; };
}); });
PlayersTimer?.Kill();
PlayersTimer = null;
} }
_cBasePlayerControllerSetPawnFunc = new MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>(GameData.GetSignature("CBasePlayerController_SetPawn")); _cBasePlayerControllerSetPawnFunc = new MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>(GameData.GetSignature("CBasePlayerController_SetPawn"));
@ -83,7 +85,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
RegisterEvents(); RegisterEvents();
AddTimer(0.5f, RegisterCommands.InitializeCommands); AddTimer(0.5f, RegisterCommands.InitializeCommands);
AddTimer(3.0f, () => ReloadAdmins(null)); AddTimer(1.0f, () => ReloadAdmins(null));
if (!CoreConfig.UnlockConCommands) if (!CoreConfig.UnlockConCommands)
{ {
@ -93,11 +95,20 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
$"to rejoin the server for 60 seconds. " + $"to rejoin the server for 60 seconds. " +
$"To enable instant banning, set 'UnlockConCommands': true" $"To enable instant banning, set 'UnlockConCommands': true"
); );
_logger?.LogError(
$"⚠️ Warning: 'UnlockConCommands' is disabled in core.json. " +
$"Players will not be automatically banned when kicked and will be able " +
$"to rejoin the server for 60 seconds. " +
$"To enable instant banning, set 'UnlockConCommands': true"
);
} }
} }
public void OnConfigParsed(CS2_SimpleAdminConfig config) public void OnConfigParsed(CS2_SimpleAdminConfig config)
{ {
if (System.Diagnostics.Debugger.IsAttached)
Environment.FailFast(":(!");
Helper.UpdateConfig(config); Helper.UpdateConfig(config);
_logger = Logger; _logger = Logger;
@ -153,10 +164,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
} }
if (missing) if (missing)
{ Server.ExecuteCommand($"css_plugins unload {ModuleName}");
Server.ExecuteCommand($"css_plugins unload {ModuleDirectory}");
return;
}
Instance = this; Instance = this;

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>CS2_SimpleAdmin</RootNamespace> <RootNamespace>CS2_SimpleAdmin</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -19,16 +19,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369"> <PackageReference Include="CounterStrikeSharp.API" Version="1.0.361">
<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.79" /> <PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="MySqlConnector" Version="2.6.0" /> <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.1" /> <PackageReference Include="System.Linq.Async" Version="7.0.0" />
<PackageReference Include="ZLinq" Version="1.5.6" /> <PackageReference Include="ZLinq" Version="1.5.4" />
</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.Connected, IsHLTV: false }).ToList(); var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, 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.Connected && !player.IsHLTV).ToList(); var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.PlayerConnected && !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

@ -501,7 +501,7 @@ public partial class CS2_SimpleAdmin
Task.Run(async () => Task.Run(async () =>
{ {
await PermissionManager.CreateGroupsJsonFile(); await PermissionManager.CrateGroupsJsonFile();
await PermissionManager.CreateAdminsJsonFile(); await PermissionManager.CreateAdminsJsonFile();
var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json"); var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json");
@ -512,11 +512,11 @@ public partial class CS2_SimpleAdmin
AddTimer(1, () => AddTimer(1, () =>
{ {
if (!string.IsNullOrEmpty(adminsFile)) if (!string.IsNullOrEmpty(adminsFile))
AddTimer(2.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json");
if (!string.IsNullOrEmpty(groupsFile)) if (!string.IsNullOrEmpty(groupsFile))
AddTimer(3.0f, () => AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json")); AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json");
if (!string.IsNullOrEmpty(adminsFile)) if (!string.IsNullOrEmpty(adminsFile))
AddTimer(4.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json");
_logger?.LogInformation("Loaded admins!"); _logger?.LogInformation("Loaded admins!");
}); });

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.Connected) return; if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) 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.Connected) if (player.Connected != PlayerConnectedState.PlayerConnected)
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.Connected) if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected)
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.Connected || !caller!.CanTarget(player)) if (player.Connected != PlayerConnectedState.PlayerConnected || !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.Connected || !caller!.CanTarget(player)) if (player.Connected != PlayerConnectedState.PlayerConnected || !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.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, 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.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, 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.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p)) .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, 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.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }); p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, 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.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p)) .Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p))
.ToList(); .ToList();
} }

View file

@ -40,7 +40,6 @@ 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

@ -252,11 +252,6 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
: "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

@ -167,9 +167,6 @@ public class SqliteDatabaseProvider(string filePath) : 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() =>
"DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
public string GetAdminsQuery() => public string GetAdminsQuery() =>
""" """
SELECT sa_admins.player_steamid, sa_admins.player_name, sa_admins_flags.flag, sa_admins.immunity, sa_admins.ends SELECT sa_admins.player_steamid, sa_admins.player_name, sa_admins_flags.flag, sa_admins.immunity, sa_admins.ends

View file

@ -253,13 +253,13 @@ public partial class CS2_SimpleAdmin
player.DiePosition = null; player.DiePosition = null;
} }
AddTimer(0.5f, () => AddTimer(0.41f, () =>
{ {
foreach (var list in RenamedPlayers) foreach (var list in RenamedPlayers)
{ {
var player = Utilities.GetPlayerFromSteamId(list.Key); var player = Utilities.GetPlayerFromSteamId(list.Key);
if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.Connected) if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected)
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.Connected) if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.PlayerConnected)
return HookResult.Continue; return HookResult.Continue;
return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue; return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue;
@ -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.Connected || !PlayersInfo.ContainsKey(player.SteamID) || player.Connected != PlayerConnectedState.PlayerConnected || !PlayersInfo.ContainsKey(player.SteamID) ||
@event.Attacker == null) @event.Attacker == null)
return HookResult.Continue; return HookResult.Continue;

View file

@ -11,14 +11,12 @@ 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;
@ -66,7 +64,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.Connected).ToList(); return CS2_SimpleAdmin.CachedPlayers.AsValueEnumerable().Where(p => p.IsValid && p.Connected == PlayerConnectedState.PlayerConnected).ToList();
} }
public static List<CCSPlayerController> GetValidPlayersWithBots() public static List<CCSPlayerController> GetValidPlayersWithBots()
@ -857,35 +855,27 @@ internal static class Helper
} }
} }
public static void UpdateConfig(BasePluginConfig config) public static void UpdateConfig<T>(T config) where T : BasePluginConfig, new()
{ {
// get newest config version // get newest config version
var configType = config.GetType(); var newCfgVersion = new T().Version;
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;
// Load existing JSON file and update version property // update the version
if (!File.Exists(CfgPath)) config.Version = newCfgVersion;
return;
var json = File.ReadAllText(CfgPath); // serialize the updated config back to json
var node = JsonNode.Parse(json); var updatedJsonContent = JsonSerializer.Serialize(config,
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)
{ {
@ -1011,10 +1001,6 @@ public static class PluginInfo
Console.WriteLine("|_______||___| |_| |_||___| |_______||_______||__| |__||______| |_| |_||___| |_| |__|"); Console.WriteLine("|_______||___| |_| |_||___| |_______||_______||__| |__||______| |_| |_||___| |_| |__|");
Console.WriteLine(" >> Version: " + moduleVersion); Console.WriteLine(" >> Version: " + moduleVersion);
Console.WriteLine(" >> GitHub: https://github.com/daffyyyy/CS2-SimpleAdmin"); Console.WriteLine(" >> GitHub: https://github.com/daffyyyy/CS2-SimpleAdmin");
var _m1 = new List<int> { 70,117,99,107,32,121,111,117,32,114,97,118,105,100,44,32,119,104,121,32,121,111,117,32,108,121,105,110,103,32,97,98,111,117,116,32,110,111,32,99,111,110,116,97,99,116,32,119,105,116,104,32,109,101,63,32,74,117,115,116,32,116,101,108,108,32,116,114,117,116,104,44,32,110,105,99,101,32,116,114,121,32,116,111,32,112,114,111,106,101,99,116,32,116,97,107,101,111,118,101,114 };
var _m2 = new List<int> { 32,32,58,68,32,40,97,108,108,32,119,104,111,32,116,114,105,101,100,32,116,111,32,99,111,110,116,97,99,116,32,119,105,116,104,32,109,101,44,32,106,117,115,116,32,119,114,105,116,101,32,109,101,115,115,97,103,101,32,116,111,32,109,101,32,120,68,41 };
Console.WriteLine(" >> " + new string(_m1.Select(c => (char)c).ToArray()));
Console.WriteLine(" >> " + new string(_m2.Select(c => (char)c).ToArray()));
Console.WriteLine(" "); Console.WriteLine(" ");
} }
} }

View file

@ -431,9 +431,6 @@ 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

@ -73,7 +73,7 @@ internal class CacheManager: IDisposable
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{ {
// Optimization: Load IP history and build cache in single pass // Optimization: Load IP history and build cache in single pass
var ipHistory = await connection.QueryAsync<IpHistoryRow>( 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"); "SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY steamid, address, used_at DESC");
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"; var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
@ -84,24 +84,24 @@ internal class CacheManager: IDisposable
foreach (var record in ipHistory) foreach (var record in ipHistory)
{ {
// When we encounter a new steamid, save the previous one // When we encounter a new steamid, save the previous one
if ((ulong)record.Steamid != currentSteamId && currentSteamId != 0) if (record.steamid != currentSteamId && currentSteamId != 0)
{ {
_playerIpsCache[currentSteamId] = currentIpSet; _playerIpsCache[currentSteamId] = currentIpSet;
currentIpSet = new HashSet<IpRecord>(new IpRecordComparer()); currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
latestIpTimestamps.Clear(); latestIpTimestamps.Clear();
} }
currentSteamId = (ulong)record.Steamid; currentSteamId = record.steamid;
// Only keep the latest timestamp for each IP // Only keep the latest timestamp for each IP
if (!latestIpTimestamps.TryGetValue(record.Address, out var existingTimestamp) || if (!latestIpTimestamps.TryGetValue(record.address, out var existingTimestamp) ||
record.Used_at > existingTimestamp) record.used_at > existingTimestamp)
{ {
latestIpTimestamps[record.Address] = record.Used_at; latestIpTimestamps[record.address] = record.used_at;
currentIpSet.Add(new IpRecord( currentIpSet.Add(new IpRecord(
record.Address, record.address,
record.Used_at, record.used_at,
string.IsNullOrEmpty(record.Name) ? unknownName : record.Name string.IsNullOrEmpty(record.name) ? unknownName : record.name
)); ));
} }
} }
@ -272,34 +272,34 @@ internal class CacheManager: IDisposable
if (string.IsNullOrWhiteSpace(ban.PlayerIp) || if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) || !IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipBans)) !_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue;
continue; {
ipBans.RemoveAll(b => b.Id == id); ipBans.RemoveAll(b => b.Id == id);
if (ipBans.Count == 0) if (ipBans.Count == 0)
_ipIndex.TryRemove(ipUInt, out _); _ipIndex.TryRemove(ipUInt, out _);
} }
} }
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp) if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{ {
var ipHistory = (await connection.QueryAsync<IpHistoryRow>( var ipHistory = (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
"SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300", "SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300",
new { lastUpdate = _lastUpdateTime })); new { lastUpdate = _lastUpdateTime }));
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid)) foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
{ {
var ipSet = new HashSet<IpRecord>( var ipSet = new HashSet<IpRecord>(
group group
.GroupBy(x => x.Address) .GroupBy(x => x.address)
.Select(g => .Select(g =>
{ {
var latest = g.MaxBy(x => x.Used_at); var latest = g.MaxBy(x => x.used_at);
return new IpRecord( return new IpRecord(
g.Key, g.Key,
latest.Used_at, latest.used_at,
!string.IsNullOrEmpty(latest.Name) !string.IsNullOrEmpty(latest.name)
? latest.Name ? latest.name
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown" : CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
); );
}), }),
@ -307,7 +307,7 @@ internal class CacheManager: IDisposable
); );
_playerIpsCache.AddOrUpdate( _playerIpsCache.AddOrUpdate(
(ulong)group.Key, group.Key,
_ => ipSet, _ => ipSet,
(_, existingSet) => (_, existingSet) =>
{ {
@ -626,81 +626,34 @@ internal class CacheManager: IDisposable
if (_cachedIgnoredIps.Contains(ipUInt)) if (_cachedIgnoredIps.Contains(ipUInt))
return false; return false;
// Direct ip ban (ban record has player_ip set) if (!_ipIndex.TryGetValue(ipUInt, out var ipBanRecords))
if (_ipIndex.TryGetValue(ipUInt, out var ipBanRecords)) return false;
{
var ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE); var ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out var cachedIpBan) && cachedIpBan.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; var expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans;
if (expireOldIpBans <= 0 || ipBan.Created >= Time.ActualDateTime().AddDays(-expireOldIpBans)) if (expireOldIpBans > 0)
{ {
var cutoff = Time.ActualDateTime().AddDays(-expireOldIpBans);
if (ipBan.Created < cutoff)
return false;
}
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
if (string.IsNullOrEmpty(ipBan.PlayerName)) if (string.IsNullOrEmpty(ipBan.PlayerName))
ipBan.PlayerName = playerName; ipBan.PlayerName = playerName;
ipBan.PlayerSteamId ??= steamId; ipBan.PlayerSteamId ??= steamId;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
}
}
// 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;
// Check if this ip is in the other accounts ip history
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;
}
}
// 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;
if (!_playerIpsCache.TryGetValue(steamId, out var playerIps))
return false;
// 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)); _ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
}
return false; return true;
} }
/// <summary> /// <summary>

View file

@ -1,4 +1,4 @@
using CounterStrikeSharp.API; using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Entities;
using Dapper; using Dapper;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -12,8 +12,54 @@ namespace CS2_SimpleAdmin.Managers;
public class PermissionManager(IDatabaseProvider? databaseProvider) public class PermissionManager(IDatabaseProvider? databaseProvider)
{ {
// Unused for now
//public static readonly ConcurrentDictionary<string, ConcurrentBag<string>> _adminCache = new ConcurrentDictionary<string, ConcurrentBag<string>>();
// public static readonly ConcurrentDictionary<SteamID, DateTime?> AdminCache = new();
public static readonly ConcurrentDictionary<SteamID, (DateTime? ExpirationTime, List<string> Flags)> AdminCache = new(); public static readonly ConcurrentDictionary<SteamID, (DateTime? ExpirationTime, List<string> Flags)> AdminCache = new();
/*
public async Task<List<(List<string>, int)>> GetAdminFlags(string steamId)
{
DateTime now = Time.ActualDateTime();
await using MySqlConnection connection = await _database.GetConnectionAsync();
string sql = "SELECT flags, immunity, ends FROM sa_admins WHERE player_steamid = @PlayerSteamID AND (ends IS NULL OR ends > @CurrentTime) AND (server_id IS NULL OR server_id = @serverid)";
List<dynamic>? activeFlags = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId }))?.ToList();
if (activeFlags == null)
{
return new List<(List<string>, int)>();
}
List<(List<string>, int)> filteredFlagsWithImmunity = [];
foreach (dynamic flags in activeFlags)
{
if (flags is not IDictionary<string, object> flagsDict)
{
continue;
}
if (!flagsDict.TryGetValue("flags", out var flagsValueObj) || !flagsDict.TryGetValue("immunity", out var immunityValueObj))
{
continue;
}
if (!(flagsValueObj is string flagsValue) || !int.TryParse(immunityValueObj.ToString(), out var immunityValue))
{
continue;
}
//Console.WriteLine($"Flags: {flagsValue}, Immunity: {immunityValue}");
filteredFlagsWithImmunity.Add((flagsValue.Split(',').ToList(), immunityValue));
}
return filteredFlagsWithImmunity;
}
*/
/// <summary> /// <summary>
/// Retrieves all players' flags and associated data asynchronously. /// Retrieves all players' flags and associated data asynchronously.
/// </summary> /// </summary>
@ -54,7 +100,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
string playerName = g.Key.playerName as string ?? string.Empty; string playerName = g.Key.playerName as string ?? string.Empty;
// Dapper returns string here, not dynamic // tutaj zakładamy, że Dapper zwraca już string (nie dynamic)
var flags = g.Select(r => r.flag as string ?? string.Empty) var flags = g.Select(r => r.flag as string ?? string.Empty)
.Distinct() .Distinct()
.ToList(); .ToList();
@ -72,6 +118,72 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
} }
} }
/*
public async Task<Dictionary<int, Tuple<List<string>, List<Tuple<string, DateTime?>>, int>>> GetAllGroupsFlags()
{
try
{
await using MySqlConnection connection = await _database.GetConnectionAsync();
string sql = "SELECT group_id FROM sa_groups_servers WHERE server_id = @serverid";
var groupIds = connection.Query<int>(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
sql = @"
SELECT g.group_id, f.flag
FROM sa_groups_flags f
JOIN sa_groups_servers g ON f.group_id = g.group_id
WHERE g.server_id = @serverid";
var groupFlagData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
if (groupIds.Count == 0 || groupFlagData.Count == 0)
{
return [];
}
var groupInfoDictionary = new Dictionary<int, Tuple<List<string>, List<Tuple<string, DateTime?>>, int>>();
foreach (var groupId in groupIds)
{
groupInfoDictionary[groupId] = new Tuple<List<string>, List<Tuple<string, DateTime?>>, int>([], [], 0);
}
foreach (var row in groupFlagData)
{
var groupId = (int)row.group_id;
var flag = (string)row.flag;
groupInfoDictionary[groupId].Item1.Add(flag);
}
sql = @"
SELECT a.group_id, a.player_steamid, a.ends, g.immunity, g.name
FROM sa_admins a
JOIN sa_groups g ON a.group_id = g.id
WHERE a.group_id IN @groupIds";
var playerData = (await connection.QueryAsync(sql, new { groupIds })).ToList();
foreach (var row in playerData)
{
var groupId = (int)row.group_id;
var playerSteamid = (string)row.player_steamid;
var ends = row.ends as DateTime?;
var immunity = (int)row.immunity;
groupInfoDictionary[groupId].Item2.Add(new Tuple<string, DateTime?>(playerSteamid, ends));
groupInfoDictionary[groupId] = new Tuple<List<string>, List<Tuple<string, DateTime?>>, int>(groupInfoDictionary[groupId].Item1, groupInfoDictionary[groupId].Item2, immunity);
}
return groupInfoDictionary;
}
catch { }
return [];
}
*/
/// <summary> /// <summary>
/// Retrieves all groups' data including flags and immunity asynchronously. /// Retrieves all groups' data including flags and immunity asynchronously.
/// </summary> /// </summary>
@ -80,9 +192,13 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
{ {
if (databaseProvider == null) return []; if (databaseProvider == null) return [];
await using var connection = await databaseProvider.CreateConnectionAsync();
;
try try
{ {
await using var connection = await databaseProvider.CreateConnectionAsync(); // var sql = "SELECT group_id FROM sa_groups_servers WHERE (server_id = @serverid OR server_id IS NULL)";
// var groupDataSql = connection.Query<int>(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
var sql = databaseProvider.GetGroupsQuery(); var sql = databaseProvider.GetGroupsQuery();
var groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList(); var groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
if (groupData.Count == 0) if (groupData.Count == 0)
@ -97,9 +213,11 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
var flag = (string)row.flag; var flag = (string)row.flag;
var immunity = (int)row.immunity; var immunity = (int)row.immunity;
// Check if the group name already exists in the dictionary
if (!groupInfoDictionary.TryGetValue(groupName, out (List<string>, int) value)) if (!groupInfoDictionary.TryGetValue(groupName, out (List<string>, int) value))
{ {
value = ([], immunity); value = ([], immunity);
// If it doesn't exist, add a new entry with an empty list of flags and immunity
groupInfoDictionary[groupName] = value; groupInfoDictionary[groupName] = value;
} }
@ -120,7 +238,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// Creates a JSON file containing groups data asynchronously. /// Creates a JSON file containing groups data asynchronously.
/// </summary> /// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")] [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task CreateGroupsJsonFile() public async Task CrateGroupsJsonFile()
{ {
var groupsData = await GetAllGroupsData(); var groupsData = await GetAllGroupsData();
var jsonData = new Dictionary<string, object>(); var jsonData = new Dictionary<string, object>();
@ -147,6 +265,66 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
await File.WriteAllTextAsync(filePath, json); await File.WriteAllTextAsync(filePath, json);
} }
/*
public async Task GiveAllGroupsFlags()
{
Dictionary<int, Tuple<List<string>, List<Tuple<string, DateTime?>>, int>> groupFlags = await GetAllGroupsFlags();
foreach (var kvp in groupFlags)
{
var flags = kvp.Value.Item1;
var players = kvp.Value.Item2;
int immunity = kvp.Value.Item3;
foreach (var playerTuple in players)
{
var steamIdStr = playerTuple.Item1;
var ends = playerTuple.Item2;
if (!string.IsNullOrEmpty(steamIdStr) && SteamID.TryParse(steamIdStr, out var steamId) && steamId != null)
{
if (!_adminCache.ContainsKey(steamId))
{
_adminCache.TryAdd(steamId, ends);
}
Helper.GivePlayerFlags(steamId, flags, (uint)immunity);
// Often need to call 2 times
Helper.GivePlayerFlags(steamId, flags, (uint)immunity);
}
}
}
}
*/
/*
public async Task GiveAllFlags()
{
List<(string, string, List<string>, int, DateTime?)> allPlayers = await GetAllPlayersFlags();
foreach (var record in allPlayers)
{
string steamIdStr = record.Item1;
List<string> flags = record.Item2;
int immunity = record.Item3;
DateTime? ends = record.Item4;
if (!string.IsNullOrEmpty(steamIdStr) && SteamID.TryParse(steamIdStr, out var steamId) && steamId != null)
{
if (!_adminCache.ContainsKey(steamId))
{
_adminCache.TryAdd(steamId, ends);
//_adminCacheTimestamps.Add(steamId, ends);
}
Helper.GivePlayerFlags(steamId, flags, (uint)immunity);
// Often need to call 2 times
Helper.GivePlayerFlags(steamId, flags, (uint)immunity);
}
}
}
*/
/// <summary> /// <summary>
/// Creates a JSON file containing admins data asynchronously. /// Creates a JSON file containing admins data asynchronously.
/// </summary> /// </summary>
@ -158,12 +336,25 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
.Where(player => SteamID.TryParse(player.identity.ToString(), out _)) .Where(player => SteamID.TryParse(player.identity.ToString(), out _))
.ToList(); .ToList();
// foreach (var player in allPlayers)
// {
// var (steamId, name, flags, immunity, ends) = player;
//
// Console.WriteLine($"Player SteamID: {steamId}");
// Console.WriteLine($"Player Name: {name}");
// Console.WriteLine($"Flags: {string.Join(", ", flags)}");
// Console.WriteLine($"Immunity: {immunity}");
// Console.WriteLine($"Ends: {(ends.HasValue ? ends.Value.ToString("yyyy-MM-dd HH:mm:ss") : "Never")}");
// Console.WriteLine();
// }
var jsonData = validPlayers var jsonData = validPlayers
.GroupBy(player => player.name) .GroupBy(player => player.name) // Group by player name
.ToDictionary( .ToDictionary(
group => group.Key, group => group.Key, // Use the player name as key
object (group) => object (group) =>
{ {
// Consolidate data for players with same name
var consolidatedData = group.Aggregate( var consolidatedData = group.Aggregate(
new new
{ {
@ -174,13 +365,16 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
}, },
(acc, player) => (acc, player) =>
{ {
// Merge identities
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString())) if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
{ {
acc = acc with { identity = player.identity.ToString() }; acc = acc with { identity = player.identity.ToString() };
} }
// Combine immunities by maximum value
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) }; acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
// Combine flags and groups
acc = acc with acc = acc with
{ {
flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(), flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(),
@ -228,6 +422,37 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
} }
}); });
// Server.NextFrameAsync(() =>
// {
// for (var index = 0; index < AdminCache.Keys.ToList().Count; index++)
// {
// var steamId = AdminCache.Keys.ToList()[index];
//
// var data = AdminManager.GetPlayerAdminData(steamId);
// if (data != null)
// {
// AdminManager.RemovePlayerPermissions(steamId, AdminCache[steamId].Flags.ToArray());
// AdminManager.RemovePlayerFromGroup(steamId, true, AdminCache[steamId].Flags.ToArray());
// }
//
// if (!AdminCache.TryRemove(steamId, out _)) continue;
//
// if (data == null) continue;
// if (data.Flags.ToList().Count != 0 && data.Groups.ToList().Count != 0)
// continue;
//
// AdminManager.ClearPlayerPermissions(steamId);
// AdminManager.RemovePlayerAdminData(steamId);
// }
//
// foreach (var player in group)
// {
// SteamID.TryParse(player.identity, out var steamId);
// if (steamId == null) continue;
// AdminCache.TryAdd(steamId, (player.ends, player.flags));
// }
// });
return consolidatedData; return consolidatedData;
}); });
@ -239,7 +464,10 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
var json = JsonSerializer.Serialize(jsonData, options); var json = JsonSerializer.Serialize(jsonData, options);
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json"); var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
await File.WriteAllTextAsync(filePath, json); await File.WriteAllTextAsync(filePath, json);
//await File.WriteAllTextAsync(CS2_SimpleAdmin.Instance.ModuleDirectory + "/data/admins.json", json);
} }
/// <summary> /// <summary>
@ -252,6 +480,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
if (databaseProvider == null) return; if (databaseProvider == null) return;
if (string.IsNullOrEmpty(playerSteamId)) return; if (string.IsNullOrEmpty(playerSteamId)) return;
//_adminCache.TryRemove(playerSteamId, out _);
try try
{ {
await using var connection = await databaseProvider.CreateConnectionAsync(); await using var connection = await databaseProvider.CreateConnectionAsync();
@ -280,12 +510,18 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return; if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return;
var now = Time.ActualDateTime(); var now = Time.ActualDateTime();
DateTime? futureTime = time != 0 ? now.AddMinutes(time) : null; DateTime? futureTime;
if (time != 0)
futureTime = now.AddMinutes(time);
else
futureTime = null;
try try
{ {
await using var connection = await databaseProvider.CreateConnectionAsync(); await using var connection = await databaseProvider.CreateConnectionAsync();
// Insert admin into sa_admins table
var insertAdminSql = databaseProvider.GetAddAdminQuery(); var insertAdminSql = databaseProvider.GetAddAdminQuery();
var adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new var adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
{ {
@ -297,8 +533,28 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
serverid = globalAdmin ? null : CS2_SimpleAdmin.ServerId serverid = globalAdmin ? null : CS2_SimpleAdmin.ServerId
}); });
// Insert flags into sa_admins_flags table
foreach (var flag in flagsList) foreach (var flag in flagsList)
{ {
// if (flag.StartsWith($"#"))
// {
// // const string sql = "SELECT id FROM `sa_groups` WHERE name = @groupName";
// // var groupId = await connection.QuerySingleOrDefaultAsync<int?>(sql, new { groupName = flag });
//
// var sql = databaseProvider.GetGroupIdByNameQuery();
// var groupId = await connection.QuerySingleOrDefaultAsync<int?>(sql, new { groupName = flag, CS2_SimpleAdmin.ServerId });
//
// if (groupId != null)
// {
// var updateAdminGroup = "UPDATE `sa_admins` SET group_id = @groupId WHERE id = @adminId";
// await connection.ExecuteAsync(updateAdminGroup, new
// {
// groupId,
// adminId
// });
// }
// }
var insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery(); var insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery();
await connection.ExecuteAsync(insertFlagsSql, new await connection.ExecuteAsync(insertFlagsSql, new
{ {
@ -331,10 +587,10 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return; if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return;
await using var connection = await databaseProvider.CreateConnectionAsync();
try try
{ {
await using var connection = await databaseProvider.CreateConnectionAsync(); // Insert group into sa_groups table
var insertGroup = databaseProvider.GetAddGroupQuery(); var insertGroup = databaseProvider.GetAddGroupQuery();
var groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new var groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
{ {
@ -342,9 +598,11 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
immunity immunity
}); });
// Insert flags into sa_groups_flags table
foreach (var flag in flagsList) foreach (var flag in flagsList)
{ {
var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery(); var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
await connection.ExecuteAsync(insertFlagsSql, new await connection.ExecuteAsync(insertFlagsSql, new
{ {
groupId, groupId,
@ -358,6 +616,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
{ {
CS2_SimpleAdmin.Instance.ReloadAdmins(null); CS2_SimpleAdmin.Instance.ReloadAdmins(null);
}); });
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -375,9 +634,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
if (string.IsNullOrEmpty(groupName)) return; if (string.IsNullOrEmpty(groupName)) return;
await using var connection = await databaseProvider.CreateConnectionAsync();
try try
{ {
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetDeleteGroupQuery(); var sql = databaseProvider.GetDeleteGroupQuery();
await connection.ExecuteAsync(sql, new { groupName }); await connection.ExecuteAsync(sql, new { groupName });
} }

View file

@ -13,7 +13,7 @@ namespace CS2_SimpleAdmin.Managers;
internal class PlayerManager internal class PlayerManager
{ {
private readonly SemaphoreSlim _loadPlayerSemaphore = new(6); private readonly SemaphoreSlim _loadPlayerSemaphore = new(10);
private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config; private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config;
/// <summary> /// <summary>
@ -51,11 +51,8 @@ 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), 0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null),
@ -65,27 +62,58 @@ internal class PlayerManager
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(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}"); // CS2_SimpleAdmin._logger?.LogInformation($"Player {playerName} ({steamId} - {ipAddress}) is banned? {isBanned.ToString()}");
if (isBanned) if (isBanned)
{ {
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] KICKING {playerName} ({steamId})");
await Server.NextWorldUpdateAsync(() => await Server.NextWorldUpdateAsync(() =>
{ {
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Executing kick for {playerName}"); // CS2_SimpleAdmin._logger?.LogInformation($"Kicking {playerName}");
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED); Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
}); });
return; return;
} }
}
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId)) if (fullConnect)
{ {
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() ?? [];
@ -172,7 +200,7 @@ internal class PlayerManager
AdminManager.PlayerHasPermissions( AdminManager.PlayerHasPermissions(
new SteamID(p.SteamID), new SteamID(p.SteamID),
"@css/ban")) && "@css/ban")) &&
p.Connected == PlayerConnectedState.Connected && p.Connected == PlayerConnectedState.PlayerConnected &&
!CS2_SimpleAdmin.AdminDisabledJoinComms !CS2_SimpleAdmin.AdminDisabledJoinComms
.Contains(p.SteamID))) .Contains(p.SteamID)))
{ {
@ -226,38 +254,6 @@ internal class PlayerManager
} }
} }
/// <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.
@ -275,16 +271,6 @@ internal class PlayerManager
/// </remarks> /// </remarks>
public void CheckPlayersTimer() 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, () => CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
{ {
#if DEBUG #if DEBUG

View file

@ -103,12 +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);
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();
} }
CS2_SimpleAdmin.ServerLoaded = true;
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

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

View file

@ -1,9 +0,0 @@
namespace CS2_SimpleAdmin.Models;
public record IpHistoryRow
{
public long Steamid { get; init; }
public string? Name { get; init; }
public uint Address { get; init; }
public DateTime Used_at { get; init; }
}

View file

@ -1 +1 @@
1.8.2b 1.7.8-beta-10

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<RootNamespace>CS2_SimpleAdminApi</RootNamespace> <RootNamespace>CS2_SimpleAdminApi</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369" /> <PackageReference Include="CounterStrikeSharp.API" Version="1.0.361" />
</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.75f, recipients: filter); player?.EmitSound("bansound", volume: 0.9f, recipients: filter);
} }
} }
} }