Compare commits
No commits in common. "6171162ae9e954e2fe068a23ecc8dd8bac2c0bbb" and "3d23b8981b0a3418fdb4c0117603349b6ca472d8" have entirely different histories.
6171162ae9
...
3d23b8981b
28 changed files with 461 additions and 490 deletions
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Get Version
|
||||
id: get_version
|
||||
|
|
@ -67,12 +67,28 @@ jobs:
|
|||
- name: Zip Main Build Output
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CS2-SimpleAdmin-Build-Artifacts
|
||||
path: |
|
||||
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:
|
||||
needs: build
|
||||
|
|
@ -93,6 +109,8 @@ jobs:
|
|||
with:
|
||||
artifacts: |
|
||||
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 }}"
|
||||
tag: "build-${{ needs.build.outputs.build_version }}"
|
||||
body: |
|
||||
|
|
@ -103,5 +121,8 @@ jobs:
|
|||
After the first launch, configure the plugin using the JSON config file at:
|
||||
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.
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -10,7 +10,3 @@ Modules/CS2-SimpleAdmin_ExampleModule/CS2-SimpleAdmin_ExampleModule.sln.DotSetti
|
|||
CS2-SimpleAdmin_BanSoundModule — kopia
|
||||
*.user
|
||||
CLAUDE.md
|
||||
/Modules/CS2-SimpleAdmin_BanSoundModule
|
||||
/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN
|
||||
|
||||
release-out/
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ using MySqlConnector;
|
|||
|
||||
namespace CS2_SimpleAdmin;
|
||||
|
||||
[MinimumApiVersion(369)]
|
||||
[MinimumApiVersion(300)]
|
||||
public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdminConfig>
|
||||
{
|
||||
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 ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
|
||||
public override string ModuleAuthor => "daffyy";
|
||||
public override string ModuleVersion => "1.8.2b-zio1";
|
||||
public override string ModuleVersion => "1.7.8-beta-10b";
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
|
|
@ -46,7 +46,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
CachedPlayers.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)
|
||||
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"));
|
||||
|
||||
|
|
@ -83,7 +85,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
|
||||
RegisterEvents();
|
||||
AddTimer(0.5f, RegisterCommands.InitializeCommands);
|
||||
AddTimer(3.0f, () => ReloadAdmins(null));
|
||||
AddTimer(1.0f, () => ReloadAdmins(null));
|
||||
|
||||
if (!CoreConfig.UnlockConCommands)
|
||||
{
|
||||
|
|
@ -93,11 +95,20 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
$"to rejoin the server for 60 seconds. " +
|
||||
$"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)
|
||||
{
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
Environment.FailFast(":(!");
|
||||
|
||||
Helper.UpdateConfig(config);
|
||||
|
||||
_logger = Logger;
|
||||
|
|
@ -153,10 +164,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
}
|
||||
|
||||
if (missing)
|
||||
{
|
||||
Server.ExecuteCommand($"css_plugins unload {ModuleDirectory}");
|
||||
return;
|
||||
}
|
||||
Server.ExecuteCommand($"css_plugins unload {ModuleName}");
|
||||
|
||||
Instance = this;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>CS2_SimpleAdmin</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
|
@ -19,16 +19,16 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.376">
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.361">
|
||||
<PrivateAssets>none</PrivateAssets>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Dapper" Version="2.1.79" />
|
||||
<PackageReference Include="MySqlConnector" Version="2.6.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="MySqlConnector" Version="2.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||
<PackageReference Include="System.Linq.Async" Version="7.0.1" />
|
||||
<PackageReference Include="ZLinq" Version="1.5.6" />
|
||||
<PackageReference Include="System.Linq.Async" Version="7.0.0" />
|
||||
<PackageReference Include="ZLinq" Version="1.5.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
var targets = GetTarget(command);
|
||||
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)
|
||||
{
|
||||
|
|
@ -373,7 +373,7 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
var targets = GetTarget(command);
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -501,7 +501,7 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await PermissionManager.CreateGroupsJsonFile();
|
||||
await PermissionManager.CrateGroupsJsonFile();
|
||||
await PermissionManager.CreateAdminsJsonFile();
|
||||
|
||||
var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json");
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public partial class CS2_SimpleAdmin
|
|||
});
|
||||
|
||||
// Add penalty to the player's penalty manager
|
||||
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Gag, Time.ActualDateTime().AddMinutes(time), time, reason);
|
||||
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Gag, Time.ActualDateTime().AddMinutes(time), time);
|
||||
|
||||
// Determine message keys and arguments based on gag time (permanent or timed)
|
||||
var (messageKey, activityMessageKey, playerArgs, adminActivityArgs) = time == 0
|
||||
|
|
@ -398,7 +398,7 @@ public partial class CS2_SimpleAdmin
|
|||
});
|
||||
|
||||
// Add penalty to the player's penalty manager
|
||||
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Mute, Time.ActualDateTime().AddMinutes(time), time, reason);
|
||||
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Mute, Time.ActualDateTime().AddMinutes(time), time);
|
||||
|
||||
// Determine message keys and arguments based on mute time (permanent or timed)
|
||||
var (messageKey, activityMessageKey, playerArgs, adminActivityArgs) = time == 0
|
||||
|
|
@ -709,7 +709,7 @@ public partial class CS2_SimpleAdmin
|
|||
});
|
||||
|
||||
// Add penalty to the player's penalty manager
|
||||
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Silence, Time.ActualDateTime().AddMinutes(time), time, reason);
|
||||
PlayerPenaltyManager.AddPenalty(player.Slot, PenaltyType.Silence, Time.ActualDateTime().AddMinutes(time), time);
|
||||
player.VoiceFlags = VoiceFlags.Muted;
|
||||
|
||||
// Determine message keys and arguments based on silence time (permanent or timed)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public partial class CS2_SimpleAdmin
|
|||
/// <param name="command">Optional command info for logging.</param>
|
||||
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;
|
||||
|
||||
// Set default caller name if not provided
|
||||
|
|
@ -93,7 +93,7 @@ public partial class CS2_SimpleAdmin
|
|||
|
||||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
if (player.Connected != PlayerConnectedState.Connected)
|
||||
if (player.Connected != PlayerConnectedState.PlayerConnected)
|
||||
return;
|
||||
|
||||
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)
|
||||
{
|
||||
// Check if the player is valid and connected
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.Connected)
|
||||
if (!player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected)
|
||||
return;
|
||||
|
||||
// Ensure the caller can target the player
|
||||
|
|
@ -284,7 +284,7 @@ public partial class CS2_SimpleAdmin
|
|||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
// 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;
|
||||
|
||||
// Determine message key and arguments for the rename notification
|
||||
|
|
@ -330,7 +330,7 @@ public partial class CS2_SimpleAdmin
|
|||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
// 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;
|
||||
|
||||
// Determine message key and arguments for the rename notification
|
||||
|
|
@ -379,7 +379,7 @@ public partial class CS2_SimpleAdmin
|
|||
return;
|
||||
|
||||
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)
|
||||
return;
|
||||
|
|
@ -399,7 +399,7 @@ public partial class CS2_SimpleAdmin
|
|||
return;
|
||||
|
||||
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();
|
||||
|
||||
if (!playersToTeleport.Any())
|
||||
|
|
@ -476,7 +476,7 @@ public partial class CS2_SimpleAdmin
|
|||
destinationPlayer = caller;
|
||||
|
||||
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();
|
||||
}
|
||||
else
|
||||
|
|
@ -486,7 +486,7 @@ public partial class CS2_SimpleAdmin
|
|||
return;
|
||||
|
||||
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)
|
||||
return;
|
||||
|
|
@ -497,7 +497,7 @@ public partial class CS2_SimpleAdmin
|
|||
return;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ public interface IDatabaseProvider
|
|||
string GetUpdateBanStatusQuery();
|
||||
string GetExpireBansQuery(bool multiServer);
|
||||
string GetExpireIpBansQuery(bool multiServer);
|
||||
string GetExpireOldPlayerIpsQuery();
|
||||
|
||||
// MuteManager
|
||||
string GetAddMuteQuery(bool includePlayerName);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
public string GetExpireOldPlayerIpsQuery()
|
||||
{
|
||||
return "DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
|
||||
}
|
||||
|
||||
public string GetAddMuteQuery(bool includePlayerName) =>
|
||||
includePlayerName
|
||||
? """
|
||||
|
|
|
|||
|
|
@ -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 AND server_id = @serverid";
|
||||
|
||||
public string GetExpireOldPlayerIpsQuery() =>
|
||||
"DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
|
||||
|
||||
public string GetAdminsQuery() =>
|
||||
"""
|
||||
SELECT sa_admins.player_steamid, sa_admins.player_name, sa_admins_flags.flag, sa_admins.immunity, sa_admins.ends
|
||||
|
|
|
|||
|
|
@ -253,13 +253,13 @@ public partial class CS2_SimpleAdmin
|
|||
player.DiePosition = null;
|
||||
}
|
||||
|
||||
AddTimer(0.5f, () =>
|
||||
AddTimer(0.41f, () =>
|
||||
{
|
||||
foreach (var list in RenamedPlayers)
|
||||
{
|
||||
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;
|
||||
|
||||
if (player.PlayerName.Equals(list.Value))
|
||||
|
|
@ -328,7 +328,7 @@ public partial class CS2_SimpleAdmin
|
|||
? Utilities.GetPlayerFromUserid(userId)
|
||||
: null;
|
||||
|
||||
if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.Connected)
|
||||
if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.PlayerConnected)
|
||||
return HookResult.Continue;
|
||||
|
||||
return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue;
|
||||
|
|
@ -347,15 +347,13 @@ public partial class CS2_SimpleAdmin
|
|||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
// zio: gagged players can still reach staff with @ in team chat, handled below.
|
||||
var staffChat = command == "say_team" && info.GetArg(1).StartsWith('@');
|
||||
// if (!Config.OtherSettings.UserMessageGagChatType)
|
||||
// {
|
||||
if (!staffChat && (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out DateTime? endDateTime) ||
|
||||
PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out endDateTime)))
|
||||
if (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out DateTime? endDateTime) ||
|
||||
PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out endDateTime))
|
||||
{
|
||||
if (_localizer != null && endDateTime is not null)
|
||||
SendChatPenaltyMessage(player, endDateTime.Value);
|
||||
player.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", player.GetLanguage()));
|
||||
return HookResult.Stop;
|
||||
}
|
||||
// }
|
||||
|
|
@ -390,36 +388,6 @@ public partial class CS2_SimpleAdmin
|
|||
return HookResult.Stop;
|
||||
}
|
||||
|
||||
// zio: says how long the gag (or silence) has left and why, in place of its end date.
|
||||
private void SendChatPenaltyMessage(CCSPlayerController player, DateTime endDateTime)
|
||||
{
|
||||
var type = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out var gagEnd) && gagEnd == endDateTime
|
||||
? PenaltyType.Gag
|
||||
: PenaltyType.Silence;
|
||||
var (remaining, reason) = PlayerPenaltyManager.GetPenaltyDetails(player.Slot, type, endDateTime);
|
||||
var kind = type == PenaltyType.Gag ? "gag" : "silence";
|
||||
using (new WithTemporaryCulture(player.GetLanguage()))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reason))
|
||||
reason = _localizer!["sa_player_penalty_no_reason"];
|
||||
if (remaining is null)
|
||||
player.SendLocalizedMessage(_localizer, $"sa_player_penalty_{kind}_active_perm", reason);
|
||||
else
|
||||
player.SendLocalizedMessage(_localizer, $"sa_player_penalty_{kind}_active", FormatRemaining(remaining.Value), reason);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatRemaining(TimeSpan remaining)
|
||||
{
|
||||
if (remaining.TotalMinutes < 1)
|
||||
return "less than a minute";
|
||||
var parts = new List<string>();
|
||||
if (remaining.Days > 0) parts.Add($"{remaining.Days}d");
|
||||
if (remaining.Hours > 0) parts.Add($"{remaining.Hours}h");
|
||||
if (remaining.Minutes > 0 && remaining.Days == 0) parts.Add($"{remaining.Minutes}m");
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
/*public HookResult OnCommandSay(CCSPlayerController? player, CommandInfo info)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.IsBot)
|
||||
|
|
@ -505,7 +473,7 @@ public partial class CS2_SimpleAdmin
|
|||
var player = @event.Userid;
|
||||
|
||||
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)
|
||||
return HookResult.Continue;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,14 +11,12 @@ using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
|||
using CS2_SimpleAdminApi;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using CounterStrikeSharp.API.Core.Plugin.Host;
|
||||
using CounterStrikeSharp.API.Modules.Entities.Constants;
|
||||
|
|
@ -66,7 +64,7 @@ internal static class Helper
|
|||
|
||||
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()
|
||||
|
|
@ -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
|
||||
var configType = config.GetType();
|
||||
var newCfgVersion = ((BasePluginConfig)Activator.CreateInstance(configType)!).Version;
|
||||
var newCfgVersion = new T().Version;
|
||||
|
||||
// loaded config is up to date
|
||||
if (config.Version == newCfgVersion)
|
||||
return;
|
||||
|
||||
// Load existing JSON file and update version property
|
||||
if (!File.Exists(CfgPath))
|
||||
return;
|
||||
// update the version
|
||||
config.Version = newCfgVersion;
|
||||
|
||||
var json = File.ReadAllText(CfgPath);
|
||||
var node = JsonNode.Parse(json);
|
||||
|
||||
if (node != null)
|
||||
{
|
||||
node["Version"] = newCfgVersion;
|
||||
var updatedJsonContent = node.ToJsonString(new JsonSerializerOptions
|
||||
// serialize the updated config back to json
|
||||
var updatedJsonContent = JsonSerializer.Serialize(config,
|
||||
new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
});
|
||||
|
||||
File.WriteAllText(CfgPath, updatedJsonContent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TryLogCommandOnDiscord(CCSPlayerController? caller, string commandString)
|
||||
{
|
||||
|
|
@ -1011,10 +1001,6 @@ public static class PluginInfo
|
|||
Console.WriteLine("|_______||___| |_| |_||___| |_______||_______||__| |__||______| |_| |_||___| |_| |__|");
|
||||
Console.WriteLine(" >> Version: " + moduleVersion);
|
||||
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(" ");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -431,9 +431,6 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
|
|||
var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
|
||||
sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId });
|
||||
|
||||
sql = databaseProvider.GetExpireOldPlayerIpsQuery();
|
||||
await connection.ExecuteAsync(sql, new { ipBansTime });
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ internal class CacheManager: IDisposable
|
|||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
|
||||
{
|
||||
// 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");
|
||||
|
||||
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
|
|
@ -84,24 +84,24 @@ internal class CacheManager: IDisposable
|
|||
foreach (var record in ipHistory)
|
||||
{
|
||||
// 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;
|
||||
currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
|
||||
latestIpTimestamps.Clear();
|
||||
}
|
||||
|
||||
currentSteamId = (ulong)record.Steamid;
|
||||
currentSteamId = record.steamid;
|
||||
|
||||
// Only keep the latest timestamp for each IP
|
||||
if (!latestIpTimestamps.TryGetValue(record.Address, out var existingTimestamp) ||
|
||||
record.Used_at > existingTimestamp)
|
||||
if (!latestIpTimestamps.TryGetValue(record.address, out var existingTimestamp) ||
|
||||
record.used_at > existingTimestamp)
|
||||
{
|
||||
latestIpTimestamps[record.Address] = record.Used_at;
|
||||
latestIpTimestamps[record.address] = record.used_at;
|
||||
currentIpSet.Add(new IpRecord(
|
||||
record.Address,
|
||||
record.Used_at,
|
||||
string.IsNullOrEmpty(record.Name) ? unknownName : record.Name
|
||||
record.address,
|
||||
record.used_at,
|
||||
string.IsNullOrEmpty(record.name) ? unknownName : record.name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -272,34 +272,34 @@ internal class CacheManager: IDisposable
|
|||
|
||||
if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
|
||||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
|
||||
!_ipIndex.TryGetValue(ipUInt, out var ipBans))
|
||||
continue;
|
||||
|
||||
!_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue;
|
||||
{
|
||||
ipBans.RemoveAll(b => b.Id == id);
|
||||
if (ipBans.Count == 0)
|
||||
_ipIndex.TryRemove(ipUInt, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
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>(
|
||||
group
|
||||
.GroupBy(x => x.Address)
|
||||
.GroupBy(x => x.address)
|
||||
.Select(g =>
|
||||
{
|
||||
var latest = g.MaxBy(x => x.Used_at);
|
||||
var latest = g.MaxBy(x => x.used_at);
|
||||
return new IpRecord(
|
||||
g.Key,
|
||||
latest.Used_at,
|
||||
!string.IsNullOrEmpty(latest.Name)
|
||||
? latest.Name
|
||||
latest.used_at,
|
||||
!string.IsNullOrEmpty(latest.name)
|
||||
? latest.name
|
||||
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
|
||||
);
|
||||
}),
|
||||
|
|
@ -307,7 +307,7 @@ internal class CacheManager: IDisposable
|
|||
);
|
||||
|
||||
_playerIpsCache.AddOrUpdate(
|
||||
(ulong)group.Key,
|
||||
group.Key,
|
||||
_ => ipSet,
|
||||
(_, existingSet) =>
|
||||
{
|
||||
|
|
@ -626,81 +626,34 @@ internal class CacheManager: IDisposable
|
|||
if (_cachedIgnoredIps.Contains(ipUInt))
|
||||
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);
|
||||
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;
|
||||
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))
|
||||
ipBan.PlayerName = playerName;
|
||||
|
||||
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));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -12,8 +12,54 @@ namespace CS2_SimpleAdmin.Managers;
|
|||
|
||||
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 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>
|
||||
/// Retrieves all players' flags and associated data asynchronously.
|
||||
/// </summary>
|
||||
|
|
@ -54,7 +100,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
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)
|
||||
.Distinct()
|
||||
.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>
|
||||
/// Retrieves all groups' data including flags and immunity asynchronously.
|
||||
/// </summary>
|
||||
|
|
@ -80,9 +192,13 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
{
|
||||
if (databaseProvider == null) return [];
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
;
|
||||
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 groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
|
||||
if (groupData.Count == 0)
|
||||
|
|
@ -97,9 +213,11 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
var flag = (string)row.flag;
|
||||
var immunity = (int)row.immunity;
|
||||
|
||||
// Check if the group name already exists in the dictionary
|
||||
if (!groupInfoDictionary.TryGetValue(groupName, out (List<string>, int) value))
|
||||
{
|
||||
value = ([], immunity);
|
||||
// If it doesn't exist, add a new entry with an empty list of flags and immunity
|
||||
groupInfoDictionary[groupName] = value;
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +238,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// Creates a JSON file containing groups data asynchronously.
|
||||
/// </summary>
|
||||
[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 jsonData = new Dictionary<string, object>();
|
||||
|
|
@ -147,6 +265,66 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
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>
|
||||
/// Creates a JSON file containing admins data asynchronously.
|
||||
/// </summary>
|
||||
|
|
@ -158,12 +336,25 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
.Where(player => SteamID.TryParse(player.identity.ToString(), out _))
|
||||
.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
|
||||
.GroupBy(player => player.name)
|
||||
.GroupBy(player => player.name) // Group by player name
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Key, // Use the player name as key
|
||||
object (group) =>
|
||||
{
|
||||
// Consolidate data for players with same name
|
||||
var consolidatedData = group.Aggregate(
|
||||
new
|
||||
{
|
||||
|
|
@ -174,13 +365,16 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
},
|
||||
(acc, player) =>
|
||||
{
|
||||
// Merge identities
|
||||
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(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) };
|
||||
|
||||
// Combine flags and groups
|
||||
acc = acc with
|
||||
{
|
||||
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;
|
||||
});
|
||||
|
||||
|
|
@ -239,7 +464,10 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
var json = JsonSerializer.Serialize(jsonData, options);
|
||||
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
|
||||
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
|
||||
//await File.WriteAllTextAsync(CS2_SimpleAdmin.Instance.ModuleDirectory + "/data/admins.json", json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -252,6 +480,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
if (databaseProvider == null) return;
|
||||
if (string.IsNullOrEmpty(playerSteamId)) return;
|
||||
|
||||
//_adminCache.TryRemove(playerSteamId, out _);
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
|
@ -280,12 +510,18 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return;
|
||||
|
||||
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
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
// Insert admin into sa_admins table
|
||||
var insertAdminSql = databaseProvider.GetAddAdminQuery();
|
||||
var adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
|
||||
{
|
||||
|
|
@ -297,8 +533,28 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
serverid = globalAdmin ? null : CS2_SimpleAdmin.ServerId
|
||||
});
|
||||
|
||||
// Insert flags into sa_admins_flags table
|
||||
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();
|
||||
await connection.ExecuteAsync(insertFlagsSql, new
|
||||
{
|
||||
|
|
@ -331,10 +587,10 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return;
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
// Insert group into sa_groups table
|
||||
var insertGroup = databaseProvider.GetAddGroupQuery();
|
||||
var groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
|
||||
{
|
||||
|
|
@ -342,9 +598,11 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
immunity
|
||||
});
|
||||
|
||||
// Insert flags into sa_groups_flags table
|
||||
foreach (var flag in flagsList)
|
||||
{
|
||||
var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
|
||||
|
||||
await connection.ExecuteAsync(insertFlagsSql, new
|
||||
{
|
||||
groupId,
|
||||
|
|
@ -358,6 +616,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
{
|
||||
CS2_SimpleAdmin.Instance.ReloadAdmins(null);
|
||||
});
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -375,9 +634,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
if (string.IsNullOrEmpty(groupName)) return;
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetDeleteGroupQuery();
|
||||
await connection.ExecuteAsync(sql, new { groupName });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace CS2_SimpleAdmin.Managers;
|
|||
|
||||
internal class PlayerManager
|
||||
{
|
||||
private readonly SemaphoreSlim _loadPlayerSemaphore = new(6);
|
||||
private readonly SemaphoreSlim _loadPlayerSemaphore = new(10);
|
||||
private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -51,11 +51,8 @@ internal class PlayerManager
|
|||
try
|
||||
{
|
||||
await _loadPlayerSemaphore.WaitAsync();
|
||||
|
||||
// Save ip address before ban check
|
||||
await SavePlayerIpAddress(steamId, playerName, ipAddress);
|
||||
|
||||
// Always check bans first, regardless of PlayersInfo state
|
||||
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
|
||||
{
|
||||
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
|
||||
{
|
||||
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._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)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] KICKING {playerName} ({steamId})");
|
||||
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);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
|
||||
if (fullConnect)
|
||||
{
|
||||
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
|
||||
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 =
|
||||
CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable()
|
||||
.Select(x => (x.SteamId, x.PlayerName)).ToList() ?? [];
|
||||
|
|
@ -125,22 +153,20 @@ internal class PlayerManager
|
|||
string muteType = mute.type;
|
||||
DateTime ends = mute.ends;
|
||||
int duration = mute.duration;
|
||||
string? reason = mute.reason;
|
||||
int passed = mute.passed is null ? 0 : (int)mute.passed;
|
||||
switch (muteType)
|
||||
{
|
||||
// Apply mute penalty based on mute type
|
||||
case "GAG":
|
||||
PlayerPenaltyManager.AddPenalty(
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
|
||||
PenaltyType.Gag, ends, duration, reason, duration - passed);
|
||||
PenaltyType.Gag, ends, duration);
|
||||
// if (CS2_SimpleAdmin._localizer != null)
|
||||
// mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
|
||||
break;
|
||||
case "MUTE":
|
||||
PlayerPenaltyManager.AddPenalty(
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
|
||||
PenaltyType.Mute, ends, duration, reason, duration - passed);
|
||||
PenaltyType.Mute, ends, duration);
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
player.VoiceFlags = VoiceFlags.Muted;
|
||||
|
|
@ -151,7 +177,7 @@ internal class PlayerManager
|
|||
default:
|
||||
PlayerPenaltyManager.AddPenalty(
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
|
||||
PenaltyType.Silence, ends, duration, reason, duration - passed);
|
||||
PenaltyType.Silence, ends, duration);
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
player.VoiceFlags = VoiceFlags.Muted;
|
||||
|
|
@ -174,7 +200,7 @@ internal class PlayerManager
|
|||
AdminManager.PlayerHasPermissions(
|
||||
new SteamID(p.SteamID),
|
||||
"@css/ban")) &&
|
||||
p.Connected == PlayerConnectedState.Connected &&
|
||||
p.Connected == PlayerConnectedState.PlayerConnected &&
|
||||
!CS2_SimpleAdmin.AdminDisabledJoinComms
|
||||
.Contains(p.SteamID)))
|
||||
{
|
||||
|
|
@ -228,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>
|
||||
/// Periodically checks the status of online players and applies timers for speed, gravity,
|
||||
/// and penalty expiration validation.
|
||||
|
|
@ -277,16 +271,6 @@ internal class PlayerManager
|
|||
/// </remarks>
|
||||
public void CheckPlayersTimer()
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddTimer(5f, () =>
|
||||
{
|
||||
foreach (var (steamid, name) in CS2_SimpleAdmin.RenamedPlayers)
|
||||
{
|
||||
var player = Helper.GetPlayerFromSteamid64(steamid);
|
||||
if (player == null || !player.IsValid || player.PlayerName == name) continue;
|
||||
player.Rename(name);
|
||||
}
|
||||
}, TimerFlags.REPEAT);
|
||||
|
||||
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
|
||||
{
|
||||
#if DEBUG
|
||||
|
|
|
|||
|
|
@ -8,46 +8,6 @@ public static class PlayerPenaltyManager
|
|||
private static readonly ConcurrentDictionary<int, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>> Penalties =
|
||||
new();
|
||||
|
||||
// zio: the reason and online-time bookkeeping for each penalty, kept beside Penalties so the
|
||||
// tuple type the API exposes doesn't change.
|
||||
private static readonly ConcurrentDictionary<(int Slot, PenaltyType Type, DateTime EndDateTime), (string? Reason, DateTime AddedAt, int MinutesLeft)> Details =
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a penalty and remembers its reason, for telling the player why they're penalized.
|
||||
/// </summary>
|
||||
/// <param name="minutesLeft">Minutes left when added: the duration, less any already served
|
||||
/// online when TimeMode is 0.</param>
|
||||
public static void AddPenalty(int slot, PenaltyType penaltyType, DateTime endDateTime, int durationInMinutes, string? reason, int? minutesLeft = null)
|
||||
{
|
||||
AddPenalty(slot, penaltyType, endDateTime, durationInMinutes);
|
||||
Details[(slot, penaltyType, endDateTime)] = (reason, Time.ActualDateTime(), minutesLeft ?? durationInMinutes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns how long the penalty IsPenalized found has left (null if permanent) and its reason.
|
||||
/// </summary>
|
||||
public static (TimeSpan? Remaining, string? Reason) GetPenaltyDetails(int slot, PenaltyType penaltyType, DateTime endDateTime)
|
||||
{
|
||||
var penalty = GetPlayerPenalties(slot, penaltyType).FirstOrDefault(p => p.EndDateTime == endDateTime);
|
||||
var hasDetails = Details.TryGetValue((slot, penaltyType, endDateTime), out var details);
|
||||
if (penalty.Duration == 0)
|
||||
return (null, details.Reason);
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
// TimeMode 0 only counts time online, and the player has been online since it was added.
|
||||
var remaining = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0 && hasDetails
|
||||
? TimeSpan.FromMinutes(details.MinutesLeft) - (now - details.AddedAt)
|
||||
: endDateTime - now;
|
||||
return (remaining < TimeSpan.Zero ? TimeSpan.Zero : remaining, details.Reason);
|
||||
}
|
||||
|
||||
private static void RemoveDetails(Func<(int Slot, PenaltyType Type, DateTime EndDateTime), bool> match)
|
||||
{
|
||||
foreach (var key in Details.Keys.Where(match).ToList())
|
||||
Details.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a penalty for a specific player slot and penalty type.
|
||||
/// </summary>
|
||||
|
|
@ -202,7 +162,6 @@ public static class PlayerPenaltyManager
|
|||
{
|
||||
Penalties.TryRemove(slot, out _);
|
||||
}
|
||||
RemoveDetails(k => k.Slot == slot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -211,7 +170,6 @@ public static class PlayerPenaltyManager
|
|||
public static void RemoveAllPenalties()
|
||||
{
|
||||
Penalties.Clear();
|
||||
Details.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -226,7 +184,6 @@ public static class PlayerPenaltyManager
|
|||
{
|
||||
penaltyDict.Remove(penaltyType);
|
||||
}
|
||||
RemoveDetails(k => k.Slot == slot && k.Type == penaltyType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
|
|||
|
||||
return warnId;
|
||||
}
|
||||
catch(Exception)
|
||||
catch(Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
1.8.2b
|
||||
1.7.8-beta-10b
|
||||
|
|
@ -65,11 +65,6 @@
|
|||
"sa_discord_penalty_unknown": "Unknown registered",
|
||||
|
||||
"sa_player_penalty_chat_active": "{lightred}Your chat is blocked to: {grey}{0}",
|
||||
"sa_player_penalty_gag_active": "{lightred}You are gagged for {grey}{0}{lightred}. Reason: {grey}{1}\n{grey}Team chat starting with {lightred}@{grey} still reaches online staff.",
|
||||
"sa_player_penalty_gag_active_perm": "{lightred}You are gagged permanently. Reason: {grey}{0}\n{grey}Team chat starting with {lightred}@{grey} still reaches online staff.",
|
||||
"sa_player_penalty_silence_active": "{lightred}You are silenced for {grey}{0}{lightred}. Reason: {grey}{1}\n{grey}Team chat starting with {lightred}@{grey} still reaches online staff.",
|
||||
"sa_player_penalty_silence_active_perm": "{lightred}You are silenced permanently. Reason: {grey}{0}\n{grey}Team chat starting with {lightred}@{grey} still reaches online staff.",
|
||||
"sa_player_penalty_no_reason": "none given",
|
||||
|
||||
"sa_player_penalty_info_active_mute": "➔ Mute [{lightred}❌{default}] - Expire [{lightred}{0}{default}]",
|
||||
"sa_player_penalty_info_active_gag": "➔ Gag [{lightred}❌{default}] - Expire [{lightred}{0}{default}]",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>CS2_SimpleAdminApi</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369" />
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.361" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public class CS2_SimpleAdmin_BanSoundModule: BasePlugin
|
|||
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsBot))
|
||||
{
|
||||
var filter = new RecipientFilter(player);
|
||||
player?.EmitSound("bansound", volume: 0.75f, recipients: filter);
|
||||
player?.EmitSound("bansound", volume: 0.9f, recipients: filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
18
README.md
18
README.md
|
|
@ -1,21 +1,3 @@
|
|||
# zio.sh fork
|
||||
|
||||
This is zio.sh's fork of CS2-SimpleAdmin 1.8.2b (`upstream` is daffyyyy/CS2-SimpleAdmin, branched
|
||||
from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code:
|
||||
|
||||
- A gagged or silenced player can still send `@message` in team chat to reach online staff.
|
||||
- The gag/silence chat message says how long is left (or that it's permanent) and the reason, in
|
||||
place of "Your chat is blocked to: <end date>". New lang keys `sa_player_penalty_gag_active`,
|
||||
`..._gag_active_perm`, `..._silence_active`, `..._silence_active_perm` and
|
||||
`sa_player_penalty_no_reason`, in `lang/en.json` only (other languages fall back to it).
|
||||
|
||||
Publish with `FORGEJO_TOKEN=... ./release.sh v<ModuleVersion>` (ModuleVersion is `1.8.2b-zioN`).
|
||||
It rebuilds from the committed source and uploads `SimpleAdmin-<tag>.tar.gz` to git.zio.sh
|
||||
(`cs2/SimpleAdmin`), holding only `CS2-SimpleAdmin.dll` and `lang/en.json`. The rest of the plugin
|
||||
and `shared/CS2-SimpleAdminApi` stay as the upstream 1.8.2b release installed them.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/daffyyyy/CS2-SimpleAdmin/actions/workflows/build.yml">
|
||||
<img src="https://github.com/daffyyyy/CS2-SimpleAdmin/actions/workflows/build.yml/badge.svg" alt="Build and Publish" />
|
||||
|
|
|
|||
117
release.sh
117
release.sh
|
|
@ -1,117 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build our CS2-SimpleAdmin fork, package it as one tarball and publish it as a Forgejo release.
|
||||
#
|
||||
# ./release.sh <tag> e.g. ./release.sh v1.8.2b-zio1
|
||||
#
|
||||
# Commit first: the plugin is rebuilt here from the committed source, so the release always matches
|
||||
# the tag. The tag is created on HEAD and pushed to origin if it doesn't exist yet. Re-running with
|
||||
# the same tag replaces that release's attachment.
|
||||
#
|
||||
# Asset: SimpleAdmin-<tag>.tar.gz, laid out like the server's game/csgo/ - extract it there:
|
||||
# tar -xzf SimpleAdmin-<tag>.tar.gz -C /srv/cs2/game/csgo
|
||||
#
|
||||
# addons/counterstrikesharp/plugins/CS2-SimpleAdmin/ CS2-SimpleAdmin.dll, lang/en.json
|
||||
#
|
||||
# Only the files the fork changes. Everything else in the plugin folder (its libraries, other lang
|
||||
# files, migrations) and shared/CS2-SimpleAdminApi stay as the upstream 1.8.2b release installed
|
||||
# them; the fork doesn't touch the API. No configs/: those stay the server's.
|
||||
set -euo pipefail
|
||||
|
||||
FORGEJO_URL="https://git.zio.sh"
|
||||
OWNER="cs2"
|
||||
REPO="SimpleAdmin"
|
||||
# Placeholder - replace, or set FORGEJO_TOKEN in the environment instead. Needs write:repository.
|
||||
# Only uploading needs it; downloading from the public repo doesn't. Don't commit a real token.
|
||||
FORGEJO_TOKEN="${FORGEJO_TOKEN:-CHANGE_ME_FORGEJO_TOKEN}"
|
||||
|
||||
# Only these ship from the publish output. CounterStrikeSharp.API.dll is excluded by the csproj
|
||||
# (ExcludeAssets=runtime); the server already provides it.
|
||||
PLUGIN_FILES=(
|
||||
CS2-SimpleAdmin.dll
|
||||
lang/en.json
|
||||
)
|
||||
|
||||
TAG="${1:-}"
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "usage: $0 <tag> (e.g. v1.8.2b-zio1)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$FORGEJO_TOKEN" == "CHANGE_ME_FORGEJO_TOKEN" ]]; then
|
||||
echo "Set FORGEJO_TOKEN (edit release.sh or export it) first." >&2
|
||||
exit 1
|
||||
fi
|
||||
for tool in podman jq curl git; do
|
||||
command -v "$tool" >/dev/null || { echo "'$tool' is required." >&2; exit 1; }
|
||||
done
|
||||
|
||||
cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||
|
||||
if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then
|
||||
echo "Tracked files have uncommitted changes; commit them so the tag matches the release." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The tag should name the version the plugin reports in `css_plugins list`.
|
||||
version="$(sed -n 's/.*ModuleVersion => "\(.*\)";.*/\1/p' CS2-SimpleAdmin/CS2-SimpleAdmin.cs)"
|
||||
if [[ "${TAG#v}" != "$version" ]]; then
|
||||
echo "Tag $TAG doesn't match ModuleVersion $version in CS2-SimpleAdmin/CS2-SimpleAdmin.cs." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Build -----------------------------------------------------------------------
|
||||
OUT="release-out"
|
||||
PUBLISH="$OUT/publish"
|
||||
rm -rf CS2-SimpleAdmin/bin CS2-SimpleAdmin/obj CS2-SimpleAdminApi/bin CS2-SimpleAdminApi/obj "$OUT"
|
||||
# dotnet build, as upstream's CI does: the csproj sets PublishTrimmed, which a plugin mustn't be.
|
||||
podman run --rm -v "$(pwd)":/src:Z -w /src \
|
||||
mcr.microsoft.com/dotnet/sdk:10.0 dotnet build CS2-SimpleAdmin/CS2-SimpleAdmin.csproj -c Release -o "/src/$PUBLISH"
|
||||
|
||||
# --- Stage (layout of game/csgo/) and package ----------------------------------------
|
||||
STAGE="$OUT/stage"
|
||||
PLUGIN_DIR="$STAGE/addons/counterstrikesharp/plugins/CS2-SimpleAdmin"
|
||||
mkdir -p "$PLUGIN_DIR"
|
||||
for file in "${PLUGIN_FILES[@]}"; do
|
||||
mkdir -p "$PLUGIN_DIR/$(dirname "$file")"
|
||||
cp "$PUBLISH/$file" "$PLUGIN_DIR/$file"
|
||||
done
|
||||
|
||||
TARBALL="SimpleAdmin-${TAG}.tar.gz"
|
||||
tar -czf "$OUT/$TARBALL" -C "$STAGE" addons
|
||||
rm -rf "$STAGE" "$PUBLISH"
|
||||
|
||||
echo "Packaged:"
|
||||
tar -tzf "$OUT/$TARBALL"
|
||||
|
||||
# --- Tag ---------------------------------------------------------------------------
|
||||
if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||
git tag -a "$TAG" -m "$TAG"
|
||||
fi
|
||||
git push origin "refs/tags/$TAG"
|
||||
|
||||
# --- Release -----------------------------------------------------------------------
|
||||
API="$FORGEJO_URL/api/v1/repos/$OWNER/$REPO"
|
||||
AUTH=(-H "Authorization: token $FORGEJO_TOKEN")
|
||||
|
||||
release_json="$(curl -fsS "${AUTH[@]}" "$API/releases/tags/$TAG" 2>/dev/null || true)"
|
||||
if [[ -z "$release_json" ]]; then
|
||||
body="$(git log -1 --format=%B "$TAG")"
|
||||
release_json="$(jq -n --arg tag "$TAG" --arg body "$body" \
|
||||
'{tag_name: $tag, name: $tag, body: $body, draft: false, prerelease: false}' |
|
||||
curl -fsS "${AUTH[@]}" -H "Content-Type: application/json" -X POST --data @- "$API/releases")"
|
||||
echo "Created release $TAG"
|
||||
else
|
||||
echo "Release $TAG already exists, replacing its attachment"
|
||||
fi
|
||||
release_id="$(jq -r '.id' <<<"$release_json")"
|
||||
|
||||
existing_id="$(jq -r --arg n "$TARBALL" '.assets[]? | select(.name == $n) | .id' <<<"$release_json")"
|
||||
if [[ -n "$existing_id" ]]; then
|
||||
curl -fsS "${AUTH[@]}" -X DELETE "$API/releases/$release_id/assets/$existing_id" >/dev/null
|
||||
fi
|
||||
curl -fsS "${AUTH[@]}" -X POST -F "attachment=@$OUT/$TARBALL" \
|
||||
"$API/releases/$release_id/assets?name=$TARBALL" >/dev/null
|
||||
echo "Uploaded $TARBALL"
|
||||
|
||||
echo
|
||||
echo "Download URL:"
|
||||
echo " $FORGEJO_URL/$OWNER/$REPO/releases/download/$TAG/$TARBALL"
|
||||
Loading…
Add table
Add a link
Reference in a new issue