zio fork 1.8.2b-zio16: backport unban, rename and css_history from upstream 1.9.0
From upstream 530f603: - css_unban updates the ban cache at once (and clears the engine banid with UnlockedCommands) instead of waiting up to a minute for the cache refresh. - css_prename is saved in sa_renames (migration 017) and re-applied on connect and name change, so it survives restarts. css_rename from a hidden admin renames again. - New css_history command and Players > History menu entry. Commands missing from an existing Commands.json are registered under their default name. - Migrations are copied with a glob, so 017 reaches the build output. Upstream's admin-loading changes, css_say change and css_penalties fix are left out.
This commit is contained in:
parent
8bec658db4
commit
71d51d7b92
35 changed files with 539 additions and 143 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -14,3 +14,4 @@ CLAUDE.md
|
||||||
/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN
|
/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN
|
||||||
|
|
||||||
release-out/
|
release-out/
|
||||||
|
.claude/
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,36 @@ css_warns PlayerName
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Show Penalty History
|
||||||
|
|
||||||
|
Print every past and current penalty for a player to your console.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
css_history <#userid or name or steamid64> [bans|gags|mutes|silences|warns]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permission:** `@css/kick`
|
||||||
|
|
||||||
|
**Shows:**
|
||||||
|
- Bans, gags, mutes, silences and warns, newest first
|
||||||
|
- Status (active, expired, unbanned, unmuted), duration and reason
|
||||||
|
- Admin who issued it
|
||||||
|
- Who lifted it, when and why, for unbans and unmutes
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Accepts a SteamID64, so it works for players who are not online
|
||||||
|
- The optional second argument narrows the list to one penalty type
|
||||||
|
- Also available in the admin menu under Players > History
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
css_history #123
|
||||||
|
css_history PlayerName bans
|
||||||
|
css_history 76561198012345678
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Show Online Players
|
### Show Online Players
|
||||||
|
|
||||||
Show information about all online players.
|
Show information about all online players.
|
||||||
|
|
|
||||||
|
|
@ -251,9 +251,9 @@ css_prename PlayerName "NewIdentity"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
- Name is enforced even after reconnect
|
- Name is enforced even after reconnect, map change and server restart
|
||||||
- Stored in database
|
- Stored in the database (`sa_renames`) and shared by every server using that database
|
||||||
- Player cannot change it
|
- Player cannot change it; `css_prename <player>` with no name removes the rename
|
||||||
- Useful for offensive names
|
- Useful for offensive names
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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-zio15";
|
public override string ModuleVersion => "1.8.2b-zio16";
|
||||||
|
|
||||||
public override void Load(bool hotReload)
|
public override void Load(bool hotReload)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,106 +36,10 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
|
<None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<None Update="Database\Migrations\Mysql\001_CreateTables.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\002_CreateFlagsTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\003_ChangeColumnsPosition.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\004_MoveOldFlagsToFlagsTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\005_CreateUnbansTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\006_ServerGroupsFeature.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\007_ServerGroupsGlobal.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\008_OnlineTimeInPenalties.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\009_BanAllUsedIpAddress.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\010_CreateWarnsTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\011_AddRconColumnToServersTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\012_AddUpdatedAtColumnToSaBansTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\013_AddNameColumnToSaPlayerIpsTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\014_AddIndexesToMutesTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\015_steamidToBigInt.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Mysql\016_OptimizeTablesAndIndexes.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\001_CreateTables.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\002_CreateFlagsTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\003_ChangeColumnsPosition.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\004_MoveOldFlagsToFlagsTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\005_CreateUnbansTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\006_ServerGroupsFeature.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\007_ServerGroupsGlobal.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\008_OnlineTimeInPenalties.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\009_BanAllUsedIpAddress.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\010_CreateWarnsTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\011_AddRconColumnToServersTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\012_AddUpdatedAtColumnToSaBansTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\013_AddNameColumnToSaPlayerIpsTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\014_AddIndexesToMutesTable.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\015_steamidToBigInt.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Database\Migrations\Sqlite\016_OptimizeTablesAndIndexes.sql">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Database\Migrations\*.sql" CopyToOutputDirectory="PreserveNewest" />
|
<None Update="Database\Migrations\**\*.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ public static class RegisterCommands
|
||||||
new("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand),
|
new("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand),
|
||||||
new("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand),
|
new("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand),
|
||||||
new("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand),
|
new("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand),
|
||||||
|
new("css_history", CS2_SimpleAdmin.Instance.OnHistoryCommand),
|
||||||
new("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand),
|
new("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand),
|
||||||
new("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand),
|
new("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand),
|
||||||
new("css_map", CS2_SimpleAdmin.Instance.OnMapCommand),
|
new("css_map", CS2_SimpleAdmin.Instance.OnMapCommand),
|
||||||
|
|
@ -130,6 +131,7 @@ public static class RegisterCommands
|
||||||
{ "css_who", new Command { Aliases = ["css_who"] } },
|
{ "css_who", new Command { Aliases = ["css_who"] } },
|
||||||
{ "css_disconnected", new Command { Aliases = ["css_disconnected", "css_last"] } },
|
{ "css_disconnected", new Command { Aliases = ["css_disconnected", "css_last"] } },
|
||||||
{ "css_warns", new Command { Aliases = ["css_warns"] } },
|
{ "css_warns", new Command { Aliases = ["css_warns"] } },
|
||||||
|
{ "css_history", new Command { Aliases = ["css_history", "css_penaltyhistory"] } },
|
||||||
{ "css_players", new Command { Aliases = ["css_players"] } },
|
{ "css_players", new Command { Aliases = ["css_players"] } },
|
||||||
{ "css_kick", new Command { Aliases = ["css_kick"] } },
|
{ "css_kick", new Command { Aliases = ["css_kick"] } },
|
||||||
{ "css_map", new Command { Aliases = ["css_map", "css_changemap"] } },
|
{ "css_map", new Command { Aliases = ["css_map", "css_changemap"] } },
|
||||||
|
|
@ -198,6 +200,13 @@ public static class RegisterCommands
|
||||||
CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback);
|
CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commands added after Commands.json was generated are registered under their default name
|
||||||
|
foreach (var mapping in CommandMappings.Where(m => !commandsConfig.Commands.ContainsKey(m.CommandKey)))
|
||||||
|
{
|
||||||
|
CS2_SimpleAdmin._logger?.LogInformation($"Registering command: `{mapping.CommandKey}` (not in Commands.json, using default alias)");
|
||||||
|
CS2_SimpleAdmin.Instance.AddCommand(mapping.CommandKey, "", mapping.Callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (name, definitions) in _commandDefinitions)
|
foreach (var (name, definitions) in _commandDefinitions)
|
||||||
|
|
|
||||||
|
|
@ -846,6 +846,99 @@ public partial class CS2_SimpleAdmin
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prints a player's full penalty history (bans, gags, mutes, silences, warns) to the caller's console.
|
||||||
|
/// Accepts a SteamID64 so it also works for players who are not online.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="caller">The player issuing the command or null for console.</param>
|
||||||
|
/// <param name="command">The command containing the target and an optional penalty type filter.</param>
|
||||||
|
[RequiresPermissions("@css/kick")]
|
||||||
|
[CommandHelper(minArgs: 1, usage: "<#userid or name or steamid64> [bans|gags|mutes|silences|warns]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
|
||||||
|
public void OnHistoryCommand(CCSPlayerController? caller, CommandInfo command)
|
||||||
|
{
|
||||||
|
if (DatabaseProvider == null || _localizer == null) return;
|
||||||
|
|
||||||
|
var filter = command.ArgCount > 2 ? command.GetArg(2).ToLowerInvariant() : null;
|
||||||
|
if (filter is not (null or "bans" or "gags" or "mutes" or "silences" or "warns"))
|
||||||
|
{
|
||||||
|
command.ReplyToCommand("Usage: css_history <#userid or name or steamid64> [bans|gags|mutes|silences|warns]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong steamId;
|
||||||
|
string? name = null;
|
||||||
|
|
||||||
|
if (Helper.ValidateSteamId(command.GetArg(1), out var parsedSteamId) && parsedSteamId != null)
|
||||||
|
{
|
||||||
|
if (caller != null && !caller.CanTarget(parsedSteamId)) return;
|
||||||
|
|
||||||
|
steamId = parsedSteamId.SteamId64;
|
||||||
|
if (PlayersInfo.TryGetValue(steamId, out var info)) name = info.Name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var targets = GetTarget(command);
|
||||||
|
if (targets == null) return;
|
||||||
|
|
||||||
|
var players = targets.Players.Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false }).ToList();
|
||||||
|
if (players.Count != 1)
|
||||||
|
{
|
||||||
|
command.ReplyToCommand("Only one target is allowed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var player = players[0];
|
||||||
|
if (caller != null && !caller.CanTarget(player)) return;
|
||||||
|
|
||||||
|
steamId = player.SteamID;
|
||||||
|
name = player.PlayerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
Helper.LogCommand(caller, command);
|
||||||
|
Action<string> print = caller == null ? Server.PrintToConsole : caller.PrintToConsole;
|
||||||
|
var callerSteamId = caller?.SteamID;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var rows = await PlayerManager.GetPenaltyHistory(steamId, filter);
|
||||||
|
// Offline players are only known by SteamID; borrow the name stored on their latest record
|
||||||
|
name ??= rows.Count > 0 ? (string?)rows[0].player_name : null;
|
||||||
|
|
||||||
|
await Server.NextWorldUpdateAsync(() =>
|
||||||
|
{
|
||||||
|
// The slot may have been reused by another player while the query ran
|
||||||
|
if (caller != null && (!caller.IsValid || caller.SteamID != callerSteamId)) return;
|
||||||
|
PrintHistory(print, name ?? steamId.ToString(), steamId, rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes history rows from <see cref="PlayerManager.GetPenaltyHistory"/> as one console line each.
|
||||||
|
/// </summary>
|
||||||
|
internal void PrintHistory(Action<string> print, string name, ulong steamId, List<dynamic> rows)
|
||||||
|
{
|
||||||
|
if (_localizer == null) return;
|
||||||
|
|
||||||
|
if (rows.Count == 0)
|
||||||
|
{
|
||||||
|
print(_localizer["sa_history_none", name]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
print(_localizer["sa_history_header", name, steamId, rows.Count]);
|
||||||
|
foreach (var r in rows)
|
||||||
|
{
|
||||||
|
var duration = (int)r.duration == 0 ? "perm" : $"{(int)r.duration}m";
|
||||||
|
var line = $"[{Date(r.created)}] {(string)r.type,-7} {(string)r.status,-8} {duration,-6} by {(string)r.admin_name}: {(string)r.reason}";
|
||||||
|
if (r.lift_date != null)
|
||||||
|
line += $" | lifted {Date(r.lift_date)} by {(string?)r.lift_admin ?? "?"}: {(string?)r.lift_reason}";
|
||||||
|
print(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Date(object? value) => PlayerManager.ToDateTime(value)?.ToString("yyyy-MM-dd HH:mm") ?? "?";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lists players currently connected to the server with options to output JSON or filter duplicate IPs.
|
/// Lists players currently connected to the server with options to output JSON or filter duplicate IPs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -292,9 +292,8 @@ public partial class CS2_SimpleAdmin
|
||||||
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
|
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
|
||||||
|
|
||||||
// Display admin activity message to other players
|
// Display admin activity message to other players
|
||||||
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
|
if (caller != null && !SilentPlayers.Contains(caller.Slot))
|
||||||
|
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
|
||||||
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
|
|
||||||
|
|
||||||
// Rename the player
|
// Rename the player
|
||||||
player.Rename(newName);
|
player.Rename(newName);
|
||||||
|
|
@ -344,15 +343,18 @@ public partial class CS2_SimpleAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if the new name is valid and update the renamed players list
|
// Determine if the new name is valid and update the renamed players list
|
||||||
|
var steamId = player.SteamID;
|
||||||
if (!string.IsNullOrEmpty(newName))
|
if (!string.IsNullOrEmpty(newName))
|
||||||
{
|
{
|
||||||
RenamedPlayers[player.SteamID] = newName;
|
RenamedPlayers[steamId] = newName;
|
||||||
player.Rename(newName);
|
player.Rename(newName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
RenamedPlayers.Remove(player.SteamID);
|
RenamedPlayers.Remove(steamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task.Run(() => PlayerManager.SaveRenamedPlayer(steamId, newName));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,11 @@ public interface IDatabaseProvider
|
||||||
string GetExpireIpBansQuery(bool multiServer);
|
string GetExpireIpBansQuery(bool multiServer);
|
||||||
string GetExpireOldPlayerIpsQuery();
|
string GetExpireOldPlayerIpsQuery();
|
||||||
|
|
||||||
|
// Renames (css_prename)
|
||||||
|
string GetRenamesQuery();
|
||||||
|
string GetUpsertRenameQuery();
|
||||||
|
string GetDeleteRenameQuery();
|
||||||
|
|
||||||
// MuteManager
|
// MuteManager
|
||||||
string GetAddMuteQuery(bool includePlayerName);
|
string GetAddMuteQuery(bool includePlayerName);
|
||||||
string GetIsMutedQuery(bool multiServer, int timeMode);
|
string GetIsMutedQuery(bool multiServer, int timeMode);
|
||||||
|
|
@ -61,4 +66,7 @@ public interface IDatabaseProvider
|
||||||
string GetUnwarnByIdQuery(bool multiServer);
|
string GetUnwarnByIdQuery(bool multiServer);
|
||||||
string GetUnwarnLastQuery(bool multiServer);
|
string GetUnwarnLastQuery(bool multiServer);
|
||||||
string GetExpireWarnsQuery(bool multiServer);
|
string GetExpireWarnsQuery(bool multiServer);
|
||||||
|
|
||||||
|
// Penalty history (css_history)
|
||||||
|
string GetPenaltyHistoryQuery(bool multiServer);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS `sa_renames` (
|
||||||
|
`player_steamid` BIGINT NOT NULL,
|
||||||
|
`name` VARCHAR(128) NOT NULL,
|
||||||
|
PRIMARY KEY (`player_steamid`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS `sa_renames` (
|
||||||
|
`player_steamid` INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
`name` TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -217,8 +217,8 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
||||||
public string GetUnbanRetrieveBansQuery(bool multiServer)
|
public string GetUnbanRetrieveBansQuery(bool multiServer)
|
||||||
{
|
{
|
||||||
return multiServer
|
return multiServer
|
||||||
? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
|
? "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
|
||||||
: "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
: "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetUnbanAdminIdQuery()
|
public string GetUnbanAdminIdQuery()
|
||||||
|
|
@ -238,6 +238,15 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
||||||
return "UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId";
|
return "UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetRenamesQuery() =>
|
||||||
|
"SELECT player_steamid, name FROM sa_renames";
|
||||||
|
|
||||||
|
public string GetUpsertRenameQuery() =>
|
||||||
|
"INSERT INTO sa_renames (player_steamid, name) VALUES (@steamId, @name) ON DUPLICATE KEY UPDATE name = @name";
|
||||||
|
|
||||||
|
public string GetDeleteRenameQuery() =>
|
||||||
|
"DELETE FROM sa_renames WHERE player_steamid = @steamId";
|
||||||
|
|
||||||
public string GetExpireBansQuery(bool multiServer)
|
public string GetExpireBansQuery(bool multiServer)
|
||||||
{
|
{
|
||||||
return multiServer
|
return multiServer
|
||||||
|
|
@ -404,6 +413,29 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
||||||
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
|
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
|
||||||
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||||
|
|
||||||
|
public string GetPenaltyHistoryQuery(bool multiServer) =>
|
||||||
|
$"""
|
||||||
|
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
|
||||||
|
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
|
||||||
|
FROM sa_bans b
|
||||||
|
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
|
||||||
|
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
|
||||||
|
WHERE b.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND b.server_id = @serverid")}
|
||||||
|
UNION ALL
|
||||||
|
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
|
||||||
|
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
|
||||||
|
FROM sa_mutes m
|
||||||
|
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
|
||||||
|
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
|
||||||
|
WHERE m.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND m.server_id = @serverid")}
|
||||||
|
UNION ALL
|
||||||
|
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
|
||||||
|
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
|
||||||
|
FROM sa_warns w
|
||||||
|
WHERE w.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND w.server_id = @serverid")}
|
||||||
|
ORDER BY created DESC
|
||||||
|
""";
|
||||||
|
|
||||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,8 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
||||||
|
|
||||||
public string GetUnbanRetrieveBansQuery(bool multiServer) =>
|
public string GetUnbanRetrieveBansQuery(bool multiServer) =>
|
||||||
multiServer
|
multiServer
|
||||||
? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
|
? "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
|
||||||
: "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
: "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
||||||
|
|
||||||
public string GetUnbanAdminIdQuery() =>
|
public string GetUnbanAdminIdQuery() =>
|
||||||
"SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
|
"SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
|
||||||
|
|
@ -154,13 +154,14 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
||||||
? "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT last_insert_rowid();"
|
? "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT last_insert_rowid();"
|
||||||
: "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT last_insert_rowid();";
|
: "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT last_insert_rowid();";
|
||||||
|
|
||||||
|
// SQLite has no ON UPDATE CURRENT_TIMESTAMP, so updated_at must be set explicitly or the cache refresh never sees the change
|
||||||
public string GetUpdateBanStatusQuery() =>
|
public string GetUpdateBanStatusQuery() =>
|
||||||
"UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId";
|
"UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId, updated_at = CURRENT_TIMESTAMP WHERE id = @banId";
|
||||||
|
|
||||||
public string GetExpireBansQuery(bool multiServer) =>
|
public string GetExpireBansQuery(bool multiServer) =>
|
||||||
multiServer
|
multiServer
|
||||||
? "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime"
|
? "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime"
|
||||||
: "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
|
: "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
|
||||||
|
|
||||||
public string GetExpireIpBansQuery(bool multiServer) =>
|
public string GetExpireIpBansQuery(bool multiServer) =>
|
||||||
multiServer
|
multiServer
|
||||||
|
|
@ -170,6 +171,15 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
||||||
public string GetExpireOldPlayerIpsQuery() =>
|
public string GetExpireOldPlayerIpsQuery() =>
|
||||||
"DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
|
"DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
|
||||||
|
|
||||||
|
public string GetRenamesQuery() =>
|
||||||
|
"SELECT player_steamid, name FROM sa_renames";
|
||||||
|
|
||||||
|
public string GetUpsertRenameQuery() =>
|
||||||
|
"INSERT INTO sa_renames (player_steamid, name) VALUES (@steamId, @name) ON CONFLICT(player_steamid) DO UPDATE SET name = excluded.name";
|
||||||
|
|
||||||
|
public string GetDeleteRenameQuery() =>
|
||||||
|
"DELETE FROM sa_renames WHERE player_steamid = @steamId";
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -377,4 +387,27 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
||||||
multiServer
|
multiServer
|
||||||
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime"
|
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime"
|
||||||
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||||
|
|
||||||
|
public string GetPenaltyHistoryQuery(bool multiServer) =>
|
||||||
|
$"""
|
||||||
|
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
|
||||||
|
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
|
||||||
|
FROM sa_bans b
|
||||||
|
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
|
||||||
|
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
|
||||||
|
WHERE b.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND b.server_id = @serverid")}
|
||||||
|
UNION ALL
|
||||||
|
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
|
||||||
|
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
|
||||||
|
FROM sa_mutes m
|
||||||
|
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
|
||||||
|
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
|
||||||
|
WHERE m.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND m.server_id = @serverid")}
|
||||||
|
UNION ALL
|
||||||
|
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
|
||||||
|
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
|
||||||
|
FROM sa_warns w
|
||||||
|
WHERE w.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND w.server_id = @serverid")}
|
||||||
|
ORDER BY created DESC
|
||||||
|
""";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -543,18 +543,31 @@ public partial class CS2_SimpleAdmin
|
||||||
[GameEventHandler]
|
[GameEventHandler]
|
||||||
public HookResult OnPlayerInfo(EventPlayerInfo @event, GameEventInfo _)
|
public HookResult OnPlayerInfo(EventPlayerInfo @event, GameEventInfo _)
|
||||||
{
|
{
|
||||||
var player = @event.Userid;
|
EnforceRename(@event.Userid);
|
||||||
|
|
||||||
if (player is null || !player.IsValid || player.IsBot)
|
|
||||||
return HookResult.Continue;
|
|
||||||
|
|
||||||
if (!RenamedPlayers.TryGetValue(player.SteamID, out var name)) return HookResult.Continue;
|
|
||||||
|
|
||||||
if (player.PlayerName.Equals(name))
|
|
||||||
return HookResult.Continue;
|
|
||||||
|
|
||||||
player.Rename(name);
|
|
||||||
|
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GameEventHandler]
|
||||||
|
public HookResult OnPlayerChangename(EventPlayerChangename @event, GameEventInfo _)
|
||||||
|
{
|
||||||
|
EnforceRename(@event.Userid);
|
||||||
|
return HookResult.Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-applies a permanent rename. Checked after a short delay because the engine may not
|
||||||
|
/// have written the client's new name yet when the event fires, and Rename itself
|
||||||
|
/// takes 0.4s to settle (it briefly writes the name with a trailing space).
|
||||||
|
/// </summary>
|
||||||
|
private void EnforceRename(CCSPlayerController? player)
|
||||||
|
{
|
||||||
|
if (player is null || !player.IsValid || player.IsBot) return;
|
||||||
|
if (!RenamedPlayers.TryGetValue(player.SteamID, out var name)) return;
|
||||||
|
|
||||||
|
AddTimer(0.5f, () =>
|
||||||
|
{
|
||||||
|
if (player.IsValid && !player.PlayerName.Equals(name))
|
||||||
|
player.Rename(name);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using CounterStrikeSharp.API;
|
using CounterStrikeSharp.API;
|
||||||
using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
||||||
|
using CounterStrikeSharp.API.Modules.Entities;
|
||||||
using CS2_SimpleAdminApi;
|
using CS2_SimpleAdminApi;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -339,8 +340,34 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
|
||||||
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
|
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
|
||||||
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
|
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply immediately; the periodic cache refresh would otherwise keep rejecting the player for up to a minute
|
||||||
|
CS2_SimpleAdmin.Instance.CacheManager?.SetBanStatus(bansList.Select(b => (int)b.id), Models.BanStatus.UNBANNED);
|
||||||
|
|
||||||
|
// css_ban also issues a native banid when UnlockedCommands is on; clear it so the engine stops
|
||||||
|
// rejecting the player. Done here, after the rows are updated, and for whichever pattern matched them.
|
||||||
|
if (CS2_SimpleAdmin.UnlockedCommands)
|
||||||
|
{
|
||||||
|
var steamIds = bansList
|
||||||
|
.Select(b => (object?)b.player_steamid)
|
||||||
|
.Where(id => id != null)
|
||||||
|
.Select(id => Convert.ToUInt64(id))
|
||||||
|
.Where(id => id > 0)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (steamIds.Count > 0)
|
||||||
|
await Server.NextWorldUpdateAsync(() =>
|
||||||
|
{
|
||||||
|
foreach (var steamId in steamIds)
|
||||||
|
Server.ExecuteCommand($"removeid {new SteamID(steamId).SteamId3}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
CS2_SimpleAdmin._logger?.LogError("Unable to unban player: {exception}", ex.Message);
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
|
// public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ namespace CS2_SimpleAdmin.Managers;
|
||||||
internal class CacheManager: IDisposable
|
internal class CacheManager: IDisposable
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
|
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
|
||||||
private readonly ConcurrentDictionary<ulong, List<BanRecord>> _steamIdIndex = [];
|
// Rebuilt into fresh instances and swapped, so unlocked readers on the game thread never see a half-built index
|
||||||
private readonly ConcurrentDictionary<uint, List<BanRecord>> _ipIndex = [];
|
private ConcurrentDictionary<ulong, List<BanRecord>> _steamIdIndex = [];
|
||||||
|
private ConcurrentDictionary<uint, List<BanRecord>> _ipIndex = [];
|
||||||
|
private readonly object _indexLock = new();
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ulong, HashSet<IpRecord>> _playerIpsCache = [];
|
private readonly ConcurrentDictionary<ulong, HashSet<IpRecord>> _playerIpsCache = [];
|
||||||
private HashSet<uint> _cachedIgnoredIps = [];
|
private HashSet<uint> _cachedIgnoredIps = [];
|
||||||
|
|
@ -178,7 +180,7 @@ internal class CacheManager: IDisposable
|
||||||
player_steamid AS PlayerSteamId,
|
player_steamid AS PlayerSteamId,
|
||||||
player_ip AS PlayerIp,
|
player_ip AS PlayerIp,
|
||||||
status AS Status
|
status AS Status
|
||||||
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
|
FROM `sa_bans` WHERE updated_at >= @lastUpdate OR created >= @lastUpdate ORDER BY updated_at DESC
|
||||||
""",
|
""",
|
||||||
new { lastUpdate = lastCheckTime }
|
new { lastUpdate = lastCheckTime }
|
||||||
)).ToList();
|
)).ToList();
|
||||||
|
|
@ -221,7 +223,7 @@ internal class CacheManager: IDisposable
|
||||||
player_steamid AS PlayerSteamId,
|
player_steamid AS PlayerSteamId,
|
||||||
player_ip AS PlayerIp,
|
player_ip AS PlayerIp,
|
||||||
status AS Status
|
status AS Status
|
||||||
FROM `sa_bans` WHERE server_id = @serverId AND (updated_at > @lastUpdate OR created > @lastUpdate) ORDER BY updated_at DESC
|
FROM `sa_bans` WHERE server_id = @serverId AND (updated_at >= @lastUpdate OR created >= @lastUpdate) ORDER BY updated_at DESC
|
||||||
""",
|
""",
|
||||||
new { serverId = CS2_SimpleAdmin.ServerId, lastUpdate = lastCheckTime }
|
new { serverId = CS2_SimpleAdmin.ServerId, lastUpdate = lastCheckTime }
|
||||||
)).ToList();
|
)).ToList();
|
||||||
|
|
@ -294,7 +296,7 @@ internal class CacheManager: IDisposable
|
||||||
.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)!; // group is never empty
|
||||||
return new IpRecord(
|
return new IpRecord(
|
||||||
g.Key,
|
g.Key,
|
||||||
latest.Used_at,
|
latest.Used_at,
|
||||||
|
|
@ -353,8 +355,36 @@ internal class CacheManager: IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void RebuildIndexes()
|
private void RebuildIndexes()
|
||||||
{
|
{
|
||||||
_steamIdIndex.Clear();
|
// SetBanStatus (unban thread) and RefreshCacheAsync (timer thread) can both get here
|
||||||
_ipIndex.Clear();
|
lock (_indexLock)
|
||||||
|
{
|
||||||
|
RebuildIndexesCore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the status of the given bans in the cache and rebuilds the indexes,
|
||||||
|
/// so unbans take effect immediately instead of waiting for the next refresh.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="banIds">Ban ids to update.</param>
|
||||||
|
/// <param name="status">New status.</param>
|
||||||
|
public void SetBanStatus(IEnumerable<int> banIds, BanStatus status)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
foreach (var id in banIds)
|
||||||
|
{
|
||||||
|
if (!_banCache.TryGetValue(id, out var ban) || ban.StatusEnum == status) continue;
|
||||||
|
_banCache[id] = ban with { Status = status.ToString() };
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) RebuildIndexes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildIndexesCore()
|
||||||
|
{
|
||||||
|
var steamIdIndex = new ConcurrentDictionary<ulong, List<BanRecord>>();
|
||||||
|
var ipIndex = new ConcurrentDictionary<uint, List<BanRecord>>();
|
||||||
|
|
||||||
// Optimization: Cache config value to avoid repeated property access
|
// Optimization: Cache config value to avoid repeated property access
|
||||||
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
|
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
|
||||||
|
|
@ -369,10 +399,10 @@ internal class CacheManager: IDisposable
|
||||||
if (ban.PlayerSteamId.HasValue)
|
if (ban.PlayerSteamId.HasValue)
|
||||||
{
|
{
|
||||||
var steamId = ban.PlayerSteamId.Value;
|
var steamId = ban.PlayerSteamId.Value;
|
||||||
if (!_steamIdIndex.TryGetValue(steamId, out var steamList))
|
if (!steamIdIndex.TryGetValue(steamId, out var steamList))
|
||||||
{
|
{
|
||||||
steamList = new List<BanRecord>();
|
steamList = new List<BanRecord>();
|
||||||
_steamIdIndex[steamId] = steamList;
|
steamIdIndex[steamId] = steamList;
|
||||||
}
|
}
|
||||||
steamList.Add(ban);
|
steamList.Add(ban);
|
||||||
}
|
}
|
||||||
|
|
@ -381,14 +411,18 @@ internal class CacheManager: IDisposable
|
||||||
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
|
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
|
||||||
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
|
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
|
||||||
{
|
{
|
||||||
if (!_ipIndex.TryGetValue(ipUInt, out var ipList))
|
if (!ipIndex.TryGetValue(ipUInt, out var ipList))
|
||||||
{
|
{
|
||||||
ipList = new List<BanRecord>();
|
ipList = new List<BanRecord>();
|
||||||
_ipIndex[ipUInt] = ipList;
|
ipIndex[ipUInt] = ipList;
|
||||||
}
|
}
|
||||||
ipList.Add(ban);
|
ipList.Add(ban);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reference swap is atomic; readers finish on the old index or start on the complete new one
|
||||||
|
Volatile.Write(ref _steamIdIndex, steamIdIndex);
|
||||||
|
Volatile.Write(ref _ipIndex, ipIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using CounterStrikeSharp.API;
|
using CounterStrikeSharp.API;
|
||||||
using CounterStrikeSharp.API.Core;
|
using CounterStrikeSharp.API.Core;
|
||||||
using CounterStrikeSharp.API.Modules.Admin;
|
using CounterStrikeSharp.API.Modules.Admin;
|
||||||
|
|
@ -92,6 +93,11 @@ internal class PlayerManager
|
||||||
// zio: read on the main thread; the ban check below runs on a worker thread.
|
// zio: read on the main thread; the ban check below runs on a worker thread.
|
||||||
var bypassIpBans = AdminManager.PlayerHasPermissions(new SteamID(steamId), IpBanBypassPermission);
|
var bypassIpBans = AdminManager.PlayerHasPermissions(new SteamID(steamId), IpBanBypassPermission);
|
||||||
|
|
||||||
|
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(steamId, out var renamedTo))
|
||||||
|
{
|
||||||
|
player.Rename(renamedTo);
|
||||||
|
}
|
||||||
|
|
||||||
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
|
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
|
|
@ -297,10 +303,92 @@ internal class PlayerManager
|
||||||
_loadPlayerSemaphore.Release();
|
_loadPlayerSemaphore.Release();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name))
|
/// <summary>
|
||||||
|
/// Returns every ban, gag/mute/silence and warn ever recorded for a SteamID, newest first.
|
||||||
|
/// Rows carry: type, status, reason, duration, created, ends, admin_name, lift_reason, lift_date, lift_admin.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="steamId">SteamID64 of the player, online or not.</param>
|
||||||
|
/// <param name="type">Optional filter: bans, gags, mutes, silences or warns.</param>
|
||||||
|
public async Task<List<dynamic>> GetPenaltyHistory(ulong steamId, string? type = null)
|
||||||
|
{
|
||||||
|
if (CS2_SimpleAdmin.DatabaseProvider == null) return [];
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
player.Rename(name);
|
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||||
|
var sql = CS2_SimpleAdmin.DatabaseProvider.GetPenaltyHistoryQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||||
|
var rows = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId })).ToList();
|
||||||
|
|
||||||
|
// Filter word is the plural (bans, gags, mutes, silences, warns); rows carry BAN/GAG/MUTE/SILENCE/WARN
|
||||||
|
if (type != null)
|
||||||
|
rows.RemoveAll(r => !type.Equals((string)r.type + "s", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
CS2_SimpleAdmin._logger?.LogError("Unable to load penalty history: {exception}", ex.Message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dapper gives DateTime on MySQL but can give a string on SQLite; accept both.
|
||||||
|
/// </summary>
|
||||||
|
internal static DateTime? ToDateTime(object? value) =>
|
||||||
|
value switch
|
||||||
|
{
|
||||||
|
DateTime d => d,
|
||||||
|
string s when DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) => parsed,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads permanent renames from the database into memory.
|
||||||
|
/// </summary>
|
||||||
|
public async Task LoadRenamedPlayers()
|
||||||
|
{
|
||||||
|
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||||
|
var rows = await connection.QueryAsync<(long steamId, string name)>(CS2_SimpleAdmin.DatabaseProvider.GetRenamesQuery());
|
||||||
|
|
||||||
|
await Server.NextWorldUpdateAsync(() =>
|
||||||
|
{
|
||||||
|
foreach (var (steamId, name) in rows)
|
||||||
|
CS2_SimpleAdmin.RenamedPlayers[(ulong)steamId] = name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
CS2_SimpleAdmin._logger?.LogError("Unable to load renames: {exception}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persists or removes a permanent rename (css_prename) for a player.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="steamId">SteamID64 of the player.</param>
|
||||||
|
/// <param name="name">Forced name, or null/empty to remove the rename.</param>
|
||||||
|
public async Task SaveRenamedPlayer(ulong steamId, string? name)
|
||||||
|
{
|
||||||
|
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetDeleteRenameQuery(), new { steamId });
|
||||||
|
else
|
||||||
|
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetUpsertRenameQuery(), new { steamId, name });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
CS2_SimpleAdmin._logger?.LogError("Unable to save rename: {exception}", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ public class ServerManager
|
||||||
if (_getIpTryCount > 32 && isInvalidIp)
|
if (_getIpTryCount > 32 && isInvalidIp)
|
||||||
{
|
{
|
||||||
CS2_SimpleAdmin._logger?.LogError("Unable to load server data - can't fetch ip address!");
|
CS2_SimpleAdmin._logger?.LogError("Unable to load server data - can't fetch ip address!");
|
||||||
|
_ = CS2_SimpleAdmin.PlayerManager.LoadRenamedPlayers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +104,7 @@ 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;
|
CS2_SimpleAdmin.ServerLoaded = true;
|
||||||
|
|
||||||
if (CS2_SimpleAdmin.Instance.CacheManager != null)
|
if (CS2_SimpleAdmin.Instance.CacheManager != null)
|
||||||
{
|
{
|
||||||
|
|
@ -115,6 +116,9 @@ CS2_SimpleAdmin.ServerLoaded = true;
|
||||||
CS2_SimpleAdmin._logger?.LogCritical("Unable to create or get server_id: " + ex.Message);
|
CS2_SimpleAdmin._logger?.LogCritical("Unable to create or get server_id: " + ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renames are server-independent, so load them even when the server row could not be resolved
|
||||||
|
await CS2_SimpleAdmin.PlayerManager.LoadRenamedPlayers();
|
||||||
|
|
||||||
if (CS2_SimpleAdmin.Instance.Config.EnableMetrics)
|
if (CS2_SimpleAdmin.Instance.Config.EnableMetrics)
|
||||||
{
|
{
|
||||||
var queryString = $"?address={address}&hostname={hostname}";
|
var queryString = $"?address={address}&hostname={hostname}";
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ public abstract class BasicMenu
|
||||||
manager.RegisterMenu("players", "mute", "sa_mute", CreateMuteMenu, "@css/chat", "css_mute");
|
manager.RegisterMenu("players", "mute", "sa_mute", CreateMuteMenu, "@css/chat", "css_mute");
|
||||||
manager.RegisterMenu("players", "silence", "sa_silence", CreateSilenceMenu, "@css/chat", "css_silence");
|
manager.RegisterMenu("players", "silence", "sa_silence", CreateSilenceMenu, "@css/chat", "css_silence");
|
||||||
manager.RegisterMenu("players", "team", "sa_team_force", CreateForceTeamMenu, "@css/kick", "css_team");
|
manager.RegisterMenu("players", "team", "sa_team_force", CreateForceTeamMenu, "@css/kick", "css_team");
|
||||||
|
manager.RegisterMenu("players", "history", "sa_history", CreateHistoryMenu, "@css/kick", "css_history");
|
||||||
|
|
||||||
// Server category menus - using translation keys
|
// Server category menus - using translation keys
|
||||||
manager.RegisterMenu("server", "plugins", "sa_menu_pluginsmanager_title", CreatePluginsMenu, "@css/root");
|
manager.RegisterMenu("server", "plugins", "sa_menu_pluginsmanager_title", CreatePluginsMenu, "@css/root");
|
||||||
|
|
@ -128,6 +129,62 @@ public abstract class BasicMenu
|
||||||
return slayMenu.WithBackButton();
|
return slayMenu.WithBackButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the penalty history menu: pick an online player, then one entry per past penalty.
|
||||||
|
/// Selecting an entry prints its full details to the admin's console.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="admin">The admin player opening the menu.</param>
|
||||||
|
/// <returns>A MenuBuilder instance for the history menu.</returns>
|
||||||
|
private static MenuBuilder CreateHistoryMenu(CCSPlayerController admin)
|
||||||
|
{
|
||||||
|
var localizer = CS2_SimpleAdmin._localizer;
|
||||||
|
var historyMenu = new MenuBuilder("sa_history", admin, localizer);
|
||||||
|
|
||||||
|
var players = Helper.GetValidPlayers().Where(p => !p.IsBot && admin.CanTarget(p));
|
||||||
|
|
||||||
|
foreach (var player in players)
|
||||||
|
{
|
||||||
|
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
|
||||||
|
historyMenu.AddOption(playerName, _ =>
|
||||||
|
{
|
||||||
|
var steamId = player.SteamID;
|
||||||
|
var name = player.PlayerName;
|
||||||
|
var adminSteamId = admin.SteamID;
|
||||||
|
|
||||||
|
// The list is built after the DB round trip, so it is opened from the world update rather than returned here
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var rows = await CS2_SimpleAdmin.PlayerManager.GetPenaltyHistory(steamId);
|
||||||
|
await Server.NextWorldUpdateAsync(() =>
|
||||||
|
{
|
||||||
|
// The slot may have been reused by another player while the query ran
|
||||||
|
if (!admin.IsValid || admin.SteamID != adminSteamId) return;
|
||||||
|
|
||||||
|
if (rows.Count == 0)
|
||||||
|
{
|
||||||
|
CS2_SimpleAdmin.Instance.PrintHistory(admin.PrintToConsole, name, steamId, rows);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = new MenuBuilder("sa_history", admin, localizer);
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var active = (string)row.status == "ACTIVE";
|
||||||
|
var label = $"[{(active ? $"{ChatColors.LightRed}X" : $"{ChatColors.Lime}✔️")}{ChatColors.Default}] {(string)row.type} {(string)row.reason}";
|
||||||
|
list.AddOption(label.Length > 40 ? label[..40] : label,
|
||||||
|
_ => CS2_SimpleAdmin.Instance.PrintHistory(admin.PrintToConsole, name, steamId, [row]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built standalone, so the back button must be wired to the player list explicitly
|
||||||
|
list.WithBackAction(historyMenu.OpenMenu).OpenMenu(admin);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyMenu.WithBackButton();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates menu for kicking players with reason selection.
|
/// Creates menu for kicking players with reason selection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ public partial class CS2_SimpleAdmin
|
||||||
internal MuteManager MuteManager = new(DatabaseProvider);
|
internal MuteManager MuteManager = new(DatabaseProvider);
|
||||||
internal WarnManager WarnManager = new(DatabaseProvider);
|
internal WarnManager WarnManager = new(DatabaseProvider);
|
||||||
internal CacheManager? CacheManager = new();
|
internal CacheManager? CacheManager = new();
|
||||||
private static readonly PlayerManager PlayerManager = new();
|
internal static readonly PlayerManager PlayerManager = new();
|
||||||
|
|
||||||
// Timers
|
// Timers
|
||||||
internal Timer? PlayersTimer = null;
|
internal Timer? PlayersTimer = null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{GREEN}[ CS2-SimpleAdmin HELP ]{DEFAULT}
|
{GREEN}[ CS2-SimpleAdmin HELP ]{DEFAULT}
|
||||||
- css_who <#userid or name> - Display informations about player
|
- css_who <#userid or name> - Display informations about player
|
||||||
|
- css_history <#userid or name or steamid64> [bans|gags|mutes|silences|warns] - Print player's penalty history to console
|
||||||
- css_players - Display player list
|
- css_players - Display player list
|
||||||
- css_ban <#userid or name> [time in minutes/0 perm] [reason] - Ban player
|
- css_ban <#userid or name> [time in minutes/0 perm] [reason] - Ban player
|
||||||
- css_addban <steamid> [time in minutes/0 perm] [reason] - Ban player via steamid64
|
- css_addban <steamid> [time in minutes/0 perm] [reason] - Ban player via steamid64
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} نقل {lightred}{1}{default} إلى {lightred}{2}{default}!",
|
"sa_admin_team_message": "{lightred}{0}{default} نقل {lightred}{1}{default} إلى {lightred}{2}{default}!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}تحذيرات",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}تحذيرات",
|
||||||
"sa_admin_warns_unwarn": "{lime}تم إلغاء التحذير بنجاح{default} لـ {gold}{0} {default}بسبب {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}تم إلغاء التحذير بنجاح{default} لـ {gold}{0} {default}بسبب {gold}{1}{default}!",
|
||||||
|
"sa_history": "السجل",
|
||||||
|
"sa_history_header": "===== سجل العقوبات للاعب {0} ({1}) - {2} سجل =====",
|
||||||
|
"sa_history_none": "لم يتم العثور على عقوبات للاعب {0}.",
|
||||||
"sa_admin_vote_menu_title": "{lime}تصويت لـ {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}تصويت لـ {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} بدأ التصويت لـ {lightred}{1}{default}",
|
"sa_admin_vote_message": "{lightred}{0}{default} بدأ التصويت لـ {lightred}{1}{default}",
|
||||||
"sa_admin_vote_message_results": "{lime}نتائج التصويت لـ {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}نتائج التصويت لـ {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} hat {lightred}{1}{default} zu {lightred}{2}{default} transferiert!",
|
"sa_admin_team_message": "{lightred}{0}{default} hat {lightred}{1}{default} zu {lightred}{2}{default} transferiert!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}VERWARNUNGEN",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}VERWARNUNGEN",
|
||||||
"sa_admin_warns_unwarn": "{lime}Erfolgreich{default} Verwarnung von {gold}{0} {default}aufgehoben wegen {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Erfolgreich{default} Verwarnung von {gold}{0} {default}aufgehoben wegen {gold}{1}{default}!",
|
||||||
|
"sa_history": "Verlauf",
|
||||||
|
"sa_history_header": "===== Strafverlauf für {0} ({1}) - {2} Eintrag/Einträge =====",
|
||||||
|
"sa_history_none": "Keine Strafen für {0} gefunden.",
|
||||||
"sa_admin_vote_menu_title": "{lime}ABSTIMMUNG FÜR {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}ABSTIMMUNG FÜR {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} hat eine Abstimmung für {lightred}{1}{default} gestartet",
|
"sa_admin_vote_message": "{lightred}{0}{default} hat eine Abstimmung für {lightred}{1}{default} gestartet",
|
||||||
"sa_admin_vote_message_results": "{lime}ABSTIMMUNGSERGEBNISSE FÜR {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}ABSTIMMUNGSERGEBNISSE FÜR {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,9 @@
|
||||||
"sa_admin_team_message": "{red}{0}{default} transfered {yellow}{1}{default} to {grey}{2}",
|
"sa_admin_team_message": "{red}{0}{default} transfered {yellow}{1}{default} to {grey}{2}",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}WARNS",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}WARNS",
|
||||||
"sa_admin_warns_unwarn": "{lime}Successfully{default} unwarned {yellow}{0} {default}for {orange}{1}",
|
"sa_admin_warns_unwarn": "{lime}Successfully{default} unwarned {yellow}{0} {default}for {orange}{1}",
|
||||||
|
"sa_history": "History",
|
||||||
|
"sa_history_header": "===== Penalty history for {0} ({1}) - {2} record(s) =====",
|
||||||
|
"sa_history_none": "No penalties found for {0}.",
|
||||||
"sa_admin_vote_menu_title": "{lime}VOTING FOR {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}VOTING FOR {gold}{0}",
|
||||||
"sa_admin_vote_message": "{red}{0}{default} started voting for {orange}{1}",
|
"sa_admin_vote_message": "{red}{0}{default} started voting for {orange}{1}",
|
||||||
"sa_admin_vote_message_results": "{lime}VOTING RESULTS FOR {orange}{0}",
|
"sa_admin_vote_message_results": "{lime}VOTING RESULTS FOR {orange}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} transfirió a {lightred}{1}{default} al {lightred}{2}{default}!",
|
"sa_admin_team_message": "{lightred}{0}{default} transfirió a {lightred}{1}{default} al {lightred}{2}{default}!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}ADVERTENCIAS",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}ADVERTENCIAS",
|
||||||
"sa_admin_warns_unwarn": "{lime}Desadvertencia{default} exitosa de {gold}{0} {default}por {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Desadvertencia{default} exitosa de {gold}{0} {default}por {gold}{1}{default}!",
|
||||||
|
"sa_history": "Historial",
|
||||||
|
"sa_history_header": "===== Historial de sanciones de {0} ({1}) - {2} registro(s) =====",
|
||||||
|
"sa_history_none": "No se encontraron sanciones para {0}.",
|
||||||
"sa_admin_vote_menu_title": "{lime}VOTACIÓN PARA {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}VOTACIÓN PARA {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} inició una votación para {lightred}{1}{default}",
|
"sa_admin_vote_message": "{lightred}{0}{default} inició una votación para {lightred}{1}{default}",
|
||||||
"sa_admin_vote_message_results": "{lime}RESULTADOS DE LA VOTACIÓN PARA {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}RESULTADOS DE LA VOTACIÓN PARA {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} {lightred}{1}{default} را به {lightred}{2}{default} انتقال داد!",
|
"sa_admin_team_message": "{lightred}{0}{default} {lightred}{1}{default} را به {lightred}{2}{default} انتقال داد!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}هشدارها",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}هشدارها",
|
||||||
"sa_admin_warns_unwarn": "{lime}هشدار با موفقیت{default} برای {gold}{0} {default}لغو شد به دلیل {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}هشدار با موفقیت{default} برای {gold}{0} {default}لغو شد به دلیل {gold}{1}{default}!",
|
||||||
|
"sa_history": "تاریخچه",
|
||||||
|
"sa_history_header": "===== تاریخچه مجازاتهای {0} ({1}) - {2} مورد =====",
|
||||||
|
"sa_history_none": "هیچ مجازاتی برای {0} یافت نشد.",
|
||||||
"sa_admin_vote_menu_title": "{lime}رأیگیری برای {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}رأیگیری برای {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} رأیگیری برای {lightred}{1}{default} را شروع کرد",
|
"sa_admin_vote_message": "{lightred}{0}{default} رأیگیری برای {lightred}{1}{default} را شروع کرد",
|
||||||
"sa_admin_vote_message_results": "{lime}نتایج رأیگیری برای {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}نتایج رأیگیری برای {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} a transféré {lightred}{1}{default} à l'équipe {lightred}{2}{default}!",
|
"sa_admin_team_message": "{lightred}{0}{default} a transféré {lightred}{1}{default} à l'équipe {lightred}{2}{default}!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}AVERTISSEMENTS",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}AVERTISSEMENTS",
|
||||||
"sa_admin_warns_unwarn": "{lime}Avertissement annulé{default} avec succès pour {gold}{0} {default}en raison de {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Avertissement annulé{default} avec succès pour {gold}{0} {default}en raison de {gold}{1}{default}!",
|
||||||
|
"sa_history": "Historique",
|
||||||
|
"sa_history_header": "===== Historique des sanctions de {0} ({1}) - {2} entrée(s) =====",
|
||||||
|
"sa_history_none": "Aucune sanction trouvée pour {0}.",
|
||||||
"sa_admin_vote_menu_title": "{lime}VOTE POUR {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}VOTE POUR {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} a lancé un vote pour {lightred}{1}{default}",
|
"sa_admin_vote_message": "{lightred}{0}{default} a lancé un vote pour {lightred}{1}{default}",
|
||||||
"sa_admin_vote_message_results": "{lime}RÉSULTATS DU VOTE POUR {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}RÉSULTATS DU VOTE POUR {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} pārvietoja {lightred}{1}{default} uz {lightred}{2}{default} komandu!",
|
"sa_admin_team_message": "{lightred}{0}{default} pārvietoja {lightred}{1}{default} uz {lightred}{2}{default} komandu!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}BRĪDINĀJUMI",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}BRĪDINĀJUMI",
|
||||||
"sa_admin_warns_unwarn": "{lime}Brīdinājums veiksmīgi{default} atsaukts priekš {gold}{0} {default}dēļ {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Brīdinājums veiksmīgi{default} atsaukts priekš {gold}{0} {default}dēļ {gold}{1}{default}!",
|
||||||
|
"sa_history": "Vēsture",
|
||||||
|
"sa_history_header": "===== Sodu vēsture spēlētājam {0} ({1}) - {2} ieraksts(-i) =====",
|
||||||
|
"sa_history_none": "Spēlētājam {0} nav atrasts neviens sods.",
|
||||||
"sa_admin_vote_menu_title": "{lime}BALSOŠANA PAR {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}BALSOŠANA PAR {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} uzsāka balsošanu par {lightred}{1}{default}",
|
"sa_admin_vote_message": "{lightred}{0}{default} uzsāka balsošanu par {lightred}{1}{default}",
|
||||||
"sa_admin_vote_message_results": "{lime}BALSOŠANAS REZULTĀTI PAR {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}BALSOŠANAS REZULTĀTI PAR {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} przerzucił {lightred}{1}{default} do {lightred}{2}{default}!",
|
"sa_admin_team_message": "{lightred}{0}{default} przerzucił {lightred}{1}{default} do {lightred}{2}{default}!",
|
||||||
"sa_admin_warns_menu_title": "{lime}OSTRZEŻENIA {gold}{0}",
|
"sa_admin_warns_menu_title": "{lime}OSTRZEŻENIA {gold}{0}",
|
||||||
"sa_admin_warns_unwarn": "{lime}Pomyślnie{default} usunięto ostrzeżenie dla {gold}{0} {default}za {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Pomyślnie{default} usunięto ostrzeżenie dla {gold}{0} {default}za {gold}{1}{default}!",
|
||||||
|
"sa_history": "Historia",
|
||||||
|
"sa_history_header": "===== Historia kar gracza {0} ({1}) - {2} wpis(ow) =====",
|
||||||
|
"sa_history_none": "Nie znaleziono kar dla {0}.",
|
||||||
"sa_admin_vote_menu_title": "{lime}GŁOSOWANIE NA {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}GŁOSOWANIE NA {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} rozpoczął głosowanie na {lightred}{1}{default}",
|
"sa_admin_vote_message": "{lightred}{0}{default} rozpoczął głosowanie na {lightred}{1}{default}",
|
||||||
"sa_admin_vote_message_results": "{lime}WYNIKI GŁOSOWANIA {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}WYNIKI GŁOSOWANIA {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} transferiu {lightred}{1}{default} para a equipe {lightred}{2}{default}!",
|
"sa_admin_team_message": "{lightred}{0}{default} transferiu {lightred}{1}{default} para a equipe {lightred}{2}{default}!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}ADVERTÊNCIAS",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}ADVERTÊNCIAS",
|
||||||
"sa_admin_warns_unwarn": "{lime}Advertência removida com sucesso{default} para {gold}{0} {default}por {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Advertência removida com sucesso{default} para {gold}{0} {default}por {gold}{1}{default}!",
|
||||||
|
"sa_history": "Histórico",
|
||||||
|
"sa_history_header": "===== Histórico de punições de {0} ({1}) - {2} registro(s) =====",
|
||||||
|
"sa_history_none": "Nenhuma punição encontrada para {0}.",
|
||||||
"sa_admin_vote_menu_title": "{lime}VOTAÇÃO PARA {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}VOTAÇÃO PARA {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} iniciou uma votação para {lightred}{1}{default}",
|
"sa_admin_vote_message": "{lightred}{0}{default} iniciou uma votação para {lightred}{1}{default}",
|
||||||
"sa_admin_vote_message_results": "{lime}RESULTADOS DA VOTAÇÃO PARA {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}RESULTADOS DA VOTAÇÃO PARA {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} transferiu {lightred}{1}{default} para a equipe {lightred}{2}{default}!",
|
"sa_admin_team_message": "{lightred}{0}{default} transferiu {lightred}{1}{default} para a equipe {lightred}{2}{default}!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}ADVERTÊNCIAS",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}ADVERTÊNCIAS",
|
||||||
"sa_admin_warns_unwarn": "{lime}Advertência removida com sucesso{default} para {gold}{0} {default}por {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Advertência removida com sucesso{default} para {gold}{0} {default}por {gold}{1}{default}!",
|
||||||
|
"sa_history": "Histórico",
|
||||||
|
"sa_history_header": "===== Histórico de punições de {0} ({1}) - {2} registo(s) =====",
|
||||||
|
"sa_history_none": "Nenhuma punição encontrada para {0}.",
|
||||||
"sa_admin_vote_menu_title": "{lime}VOTAÇÃO PARA {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}VOTAÇÃO PARA {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} iniciou uma votação para {lightred}{1}{default}",
|
"sa_admin_vote_message": "{lightred}{0}{default} iniciou uma votação para {lightred}{1}{default}",
|
||||||
"sa_admin_vote_message_results": "{lime}RESULTADOS DA VOTAÇÃO PARA {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}RESULTADOS DA VOTAÇÃO PARA {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} перевел {lightred}{1}{default} в команду {lightred}{2}{default}!",
|
"sa_admin_team_message": "{lightred}{0}{default} перевел {lightred}{1}{default} в команду {lightred}{2}{default}!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}ПРЕДУПРЕЖДЕНИЯ",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}ПРЕДУПРЕЖДЕНИЯ",
|
||||||
"sa_admin_warns_unwarn": "{lime}Предупреждение успешно удалено{default} для {gold}{0} {default}за {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Предупреждение успешно удалено{default} для {gold}{0} {default}за {gold}{1}{default}!",
|
||||||
|
"sa_history": "История",
|
||||||
|
"sa_history_header": "===== История наказаний игрока {0} ({1}) - записей: {2} =====",
|
||||||
|
"sa_history_none": "Наказания для {0} не найдены.",
|
||||||
"sa_admin_vote_menu_title": "{lime}ГОЛОСОВАНИЕ ЗА {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}ГОЛОСОВАНИЕ ЗА {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} начал голосование за {lightred}{1}{default}",
|
"sa_admin_vote_message": "{lightred}{0}{default} начал голосование за {lightred}{1}{default}",
|
||||||
"sa_admin_vote_message_results": "{lime}РЕЗУЛЬТАТЫ ГОЛОСОВАНИЯ ЗА {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}РЕЗУЛЬТАТЫ ГОЛОСОВАНИЯ ЗА {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} {lightred}{1}{default}'i {lightred}{2}{default} takımına transfer etti!",
|
"sa_admin_team_message": "{lightred}{0}{default} {lightred}{1}{default}'i {lightred}{2}{default} takımına transfer etti!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}UYARILAR",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}UYARILAR",
|
||||||
"sa_admin_warns_unwarn": "{lime}Uyarı başarıyla kaldırıldı{default} {gold}{0} {default} için {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}Uyarı başarıyla kaldırıldı{default} {gold}{0} {default} için {gold}{1}{default}!",
|
||||||
|
"sa_history": "Geçmiş",
|
||||||
|
"sa_history_header": "===== {0} ({1}) ceza geçmişi - {2} kayıt =====",
|
||||||
|
"sa_history_none": "{0} için ceza bulunamadı.",
|
||||||
"sa_admin_vote_menu_title": "{lime}OY VERME {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}OY VERME {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} {lightred}{1}{default} için oy kullanmaya başladı",
|
"sa_admin_vote_message": "{lightred}{0}{default} {lightred}{1}{default} için oy kullanmaya başladı",
|
||||||
"sa_admin_vote_message_results": "{lime}OY SONUÇLARI {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}OY SONUÇLARI {gold}{0}",
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,9 @@
|
||||||
"sa_admin_team_message": "{lightred}{0}{default} 将 {lightred}{1}{default} 转移到 {lightred}{2}{default} 队伍!",
|
"sa_admin_team_message": "{lightred}{0}{default} 将 {lightred}{1}{default} 转移到 {lightred}{2}{default} 队伍!",
|
||||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}警告",
|
"sa_admin_warns_menu_title": "{gold}{0} {lime}警告",
|
||||||
"sa_admin_warns_unwarn": "{lime}成功移除警告{default} {gold}{0} 对于 {gold}{1}{default}!",
|
"sa_admin_warns_unwarn": "{lime}成功移除警告{default} {gold}{0} 对于 {gold}{1}{default}!",
|
||||||
|
"sa_history": "历史记录",
|
||||||
|
"sa_history_header": "===== {0} ({1}) 的处罚记录 - 共 {2} 条 =====",
|
||||||
|
"sa_history_none": "未找到 {0} 的处罚记录。",
|
||||||
"sa_admin_vote_menu_title": "{lime}投票 {gold}{0}",
|
"sa_admin_vote_menu_title": "{lime}投票 {gold}{0}",
|
||||||
"sa_admin_vote_message": "{lightred}{0}{default} 发起了 [{lightred}{1}{default}] 的投票",
|
"sa_admin_vote_message": "{lightred}{0}{default} 发起了 [{lightred}{1}{default}] 的投票",
|
||||||
"sa_admin_vote_message_results": "{lime}投票结果 {gold}{0}",
|
"sa_admin_vote_message_results": "{lime}投票结果 {gold}{0}",
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -42,6 +42,18 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code:
|
||||||
`css_unsilence` match online players by part of the name too (upstream needed the full name),
|
`css_unsilence` match online players by part of the name too (upstream needed the full name),
|
||||||
falling back to the offline search only when no one online matches.
|
falling back to the offline search only when no one online matches.
|
||||||
|
|
||||||
|
Backported from upstream 1.9.0 (commit `530f603`), not marked `zio:`:
|
||||||
|
|
||||||
|
- `css_unban` applies at once: the ban cache is updated straight away instead of on the next
|
||||||
|
61-second refresh, and `removeid` clears the engine ban when `UnlockedCommands` is on.
|
||||||
|
- `css_prename` survives restarts: renames are stored in the new `sa_renames` table (migration
|
||||||
|
017) and re-applied on connect and name change. `css_rename` from a hidden admin renames again.
|
||||||
|
- New `css_history <#userid|name|steamid64> [bans|gags|mutes|silences|warns]` (`@css/kick`), and
|
||||||
|
Players > History in the admin menu. Commands missing from an existing `Commands.json` are
|
||||||
|
registered under their default name.
|
||||||
|
- Left out: upstream's admin-loading changes (ours differ, see `1.8.2b-zio11`), its `css_say`
|
||||||
|
change and its `css_penalties` fix.
|
||||||
|
|
||||||
Publish with `FORGEJO_TOKEN=... ./release.sh v<ModuleVersion>` (ModuleVersion is `1.8.2b-zioN`).
|
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
|
It rebuilds from the committed source and uploads `SimpleAdmin-<tag>.tar.gz` to git.zio.sh
|
||||||
(`cs2/SimpleAdmin`), laid out like `game/csgo/`. It holds the whole plugin (its libraries, all lang
|
(`cs2/SimpleAdmin`), laid out like `game/csgo/`. It holds the whole plugin (its libraries, all lang
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue