Compare commits
13 commits
v1.8.2b-zi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4420d41048 | |||
| 1a1253b865 | |||
| 71d51d7b92 | |||
| 8bec658db4 | |||
| dc5ad30952 | |||
| 7b3d0ed452 | |||
| bbb72757e7 | |||
| 395e513f44 | |||
| d042aab1c0 | |||
| f46ccf18ce | |||
| cf1090cbd2 | |||
| 65a413fa52 | |||
| 703893a908 |
43 changed files with 905 additions and 420 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -14,3 +14,4 @@ CLAUDE.md
|
|||
/Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN
|
||||
|
||||
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 information about all online players.
|
||||
|
|
|
|||
|
|
@ -251,9 +251,9 @@ css_prename PlayerName "NewIdentity"
|
|||
```
|
||||
|
||||
**Notes:**
|
||||
- Name is enforced even after reconnect
|
||||
- Stored in database
|
||||
- Player cannot change it
|
||||
- Name is enforced even after reconnect, map change and server restart
|
||||
- Stored in the database (`sa_renames`) and shared by every server using that database
|
||||
- Player cannot change it; `css_prename <player>` with no name removes the rename
|
||||
- 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 ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
|
||||
public override string ModuleAuthor => "daffyy";
|
||||
public override string ModuleVersion => "1.8.2b-zio6";
|
||||
public override string ModuleVersion => "1.8.2b-zio17";
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
|
|
@ -217,8 +217,8 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
if (!string.IsNullOrEmpty(Config.Discord.DiscordLogWebhook))
|
||||
DiscordWebhookClientLog = new DiscordManager(Config.Discord.DiscordLogWebhook);
|
||||
|
||||
if (Config.EnableUpdateCheck)
|
||||
Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, Logger));
|
||||
// zio: no update check or startup banner (upstream's PluginInfo, removed). EnableUpdateCheck
|
||||
// is left in the config so existing files still load, and does nothing.
|
||||
|
||||
PermissionManager = new PermissionManager(DatabaseProvider);
|
||||
BanManager = new BanManager(DatabaseProvider);
|
||||
|
|
@ -242,7 +242,13 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
|
|||
if (matches.Count() == 1)
|
||||
return matches;
|
||||
|
||||
command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(argument)}\".");
|
||||
// zio: a name that is exactly the pattern wins over names that only contain it, so "duck"
|
||||
// still finds "Duck" when "Ducky" is also on.
|
||||
var exact = matches.Players.Where(p => p.PlayerName.Equals(command.GetArg(argument), StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (exact.Count == 1)
|
||||
return new TargetResult { Players = exact };
|
||||
|
||||
command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(argument)}\": {string.Join(", ", matches.Players.Select(p => p.PlayerName))}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,106 +36,10 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<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>
|
||||
<None Update="Database\Migrations\*.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Update="Database\Migrations\**\*.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public static class RegisterCommands
|
|||
new("css_who", CS2_SimpleAdmin.Instance.OnWhoCommand),
|
||||
new("css_disconnected", CS2_SimpleAdmin.Instance.OnDisconnectedCommand),
|
||||
new("css_warns", CS2_SimpleAdmin.Instance.OnWarnsCommand),
|
||||
new("css_history", CS2_SimpleAdmin.Instance.OnHistoryCommand),
|
||||
new("css_players", CS2_SimpleAdmin.Instance.OnPlayersCommand),
|
||||
new("css_kick", CS2_SimpleAdmin.Instance.OnKickCommand),
|
||||
new("css_map", CS2_SimpleAdmin.Instance.OnMapCommand),
|
||||
|
|
@ -130,6 +131,7 @@ public static class RegisterCommands
|
|||
{ "css_who", new Command { Aliases = ["css_who"] } },
|
||||
{ "css_disconnected", new Command { Aliases = ["css_disconnected", "css_last"] } },
|
||||
{ "css_warns", new Command { Aliases = ["css_warns"] } },
|
||||
{ "css_history", new Command { Aliases = ["css_history", "css_penaltyhistory"] } },
|
||||
{ "css_players", new Command { Aliases = ["css_players"] } },
|
||||
{ "css_kick", new Command { Aliases = ["css_kick"] } },
|
||||
{ "css_map", new Command { Aliases = ["css_map", "css_changemap"] } },
|
||||
|
|
@ -198,6 +200,13 @@ public static class RegisterCommands
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public partial class CS2_SimpleAdmin
|
|||
if (_localizer == null) continue;
|
||||
// zio: players get ">[STAFF][RANK] name: message", like @ in team chat.
|
||||
player.PrintToChat(caller == null
|
||||
? _localizer["sa_adminchat_template_admin", _localizer["sa_console"], utf8String]
|
||||
? _localizer["sa_adminchat_template_admin", _localizer["sa_console"], utf8String.Trim()]
|
||||
: StaffChatMessage(caller, utf8String.Trim()));
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ public partial class CS2_SimpleAdmin
|
|||
{
|
||||
player.SendLocalizedMessage(_localizer,
|
||||
"sa_adminsay_prefix",
|
||||
utf8String.ReplaceColorTags());
|
||||
utf8String.Trim().ReplaceColorTags()); // zio: Trim, the message starts with the space after the command
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,9 +128,16 @@ public partial class CS2_SimpleAdmin
|
|||
var utf8BytesString = Encoding.UTF8.GetBytes(message);
|
||||
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
|
||||
|
||||
// zio: from a player, "(PM) [RANK] name: message".
|
||||
var rank = caller == null ? null : Helper.GetStaffRankTag(caller);
|
||||
var line = caller == null || _localizer == null
|
||||
? $"({callerName}) {utf8String.Trim()}".ReplaceColorTags()
|
||||
: rank == null
|
||||
? _localizer["sa_psay_template_norank", callerName, utf8String.Trim()]
|
||||
: _localizer["sa_psay_template", rank, callerName, utf8String.Trim()];
|
||||
playersToTarget.ForEach(player =>
|
||||
{
|
||||
player.PrintToChat($"({callerName}) {utf8String}".ReplaceColorTags());
|
||||
player.PrintToChat(line);
|
||||
});
|
||||
|
||||
command.ReplyToCommand($" Private message sent!");
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ public partial class CS2_SimpleAdmin
|
|||
var userId = caller.UserId.Value;
|
||||
var steamId = caller.SteamID;
|
||||
|
||||
if (!string.IsNullOrEmpty(command.GetArg(1)) && AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/kick"))
|
||||
// zio: other players' penalties need @zio/penalties (or root), not @css/kick.
|
||||
if (!string.IsNullOrEmpty(command.GetArg(1)) && (AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@zio/penalties")
|
||||
|| AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/root")))
|
||||
{
|
||||
var targets = GetTarget(command);
|
||||
|
||||
|
|
@ -284,7 +286,9 @@ public partial class CS2_SimpleAdmin
|
|||
if (DatabaseProvider == null) return;
|
||||
|
||||
var flagsList = flags.Split(',').Select(flag => flag.Trim()).ToList();
|
||||
_ = Instance.PermissionManager.AddAdminBySteamId(steamid, name, flagsList, immunity, time, globalAdmin);
|
||||
// zio: who did it, for the rank log (null caller is Console, which includes RCON).
|
||||
_ = Instance.PermissionManager.AddAdminBySteamId(steamid, name, flagsList, immunity, time, globalAdmin,
|
||||
caller?.SteamID ?? 0, caller?.PlayerName ?? "Console");
|
||||
|
||||
Helper.LogCommand(caller, $"css_addadmin {steamid} {name} {flags} {immunity} {time}");
|
||||
|
||||
|
|
@ -329,7 +333,8 @@ public partial class CS2_SimpleAdmin
|
|||
public void RemoveAdmin(CCSPlayerController? caller, string steamid, bool globalDelete = false, CommandInfo? command = null)
|
||||
{
|
||||
if (DatabaseProvider == null) return;
|
||||
_ = PermissionManager.DeleteAdminBySteamId(steamid, globalDelete);
|
||||
// zio: who did it, for the rank log (null caller is Console, which includes RCON).
|
||||
_ = PermissionManager.DeleteAdminBySteamId(steamid, globalDelete, caller?.SteamID ?? 0, caller?.PlayerName ?? "Console");
|
||||
|
||||
AddTimer(2, () =>
|
||||
{
|
||||
|
|
@ -502,26 +507,31 @@ public partial class CS2_SimpleAdmin
|
|||
Task.Run(async () =>
|
||||
{
|
||||
await PermissionManager.CreateGroupsJsonFile();
|
||||
await PermissionManager.CreateAdminsJsonFile();
|
||||
|
||||
var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json");
|
||||
var groupsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/groups.json");
|
||||
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
AddTimer(1, () =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(adminsFile))
|
||||
AddTimer(2.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json"));
|
||||
if (!string.IsNullOrEmpty(groupsFile))
|
||||
AddTimer(3.0f, () => AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json"));
|
||||
if (!string.IsNullOrEmpty(adminsFile))
|
||||
AddTimer(4.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json"));
|
||||
var admins = await PermissionManager.CreateAdminsJsonFile();
|
||||
if (admins == null) return;
|
||||
|
||||
_logger?.LogInformation("Loaded admins!");
|
||||
});
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
// zio: CSS's loaders only ever add, so first remove everyone loaded last time (removed or
|
||||
// demoted admins kept their old rank until a restart otherwise), then load the new set,
|
||||
// all in one tick so nobody is left without their flags in between. Groups can't be
|
||||
// removed from CSS, so flags taken off a group still need a restart.
|
||||
foreach (var steamId in PermissionManager.AdminCache.Keys.ToList())
|
||||
AdminManager.RemovePlayerAdminData(steamId);
|
||||
PermissionManager.AdminCache.Clear();
|
||||
|
||||
foreach (var admin in admins.GroupBy(a => a.SteamId))
|
||||
{
|
||||
var ends = admin.Any(a => a.Ends == null) ? null : admin.Max(a => a.Ends);
|
||||
PermissionManager.AdminCache[admin.Key] = (ends, admin.SelectMany(a => a.Flags).Distinct().ToList());
|
||||
}
|
||||
|
||||
AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json");
|
||||
AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json");
|
||||
|
||||
_logger?.LogInformation("Loaded {count} admins!", PermissionManager.AdminCache.Count);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//_ = _adminManager.GiveAllGroupsFlags();
|
||||
//_ = _adminManager.GiveAllFlags();
|
||||
|
|
@ -836,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>
|
||||
/// Lists players currently connected to the server with options to output JSON or filter duplicate IPs.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -292,6 +292,13 @@ public partial class CS2_SimpleAdmin
|
|||
var nameMatches = Helper.GetPlayerFromName(pattern);
|
||||
var namePlayer = nameMatches.Count == 1 ? nameMatches.FirstOrDefault() : null;
|
||||
|
||||
// zio: several online players match part of the name; don't fall through to the offline search.
|
||||
if (nameMatches.Count > 1)
|
||||
{
|
||||
command.ReplyToCommand($"Multiple targets found for \"{pattern}\": {string.Join(", ", nameMatches.Select(p => p.PlayerName))}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (namePlayer != null && namePlayer.IsValid)
|
||||
{
|
||||
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Gag);
|
||||
|
|
@ -604,6 +611,13 @@ public partial class CS2_SimpleAdmin
|
|||
var nameMatches = Helper.GetPlayerFromName(pattern);
|
||||
var namePlayer = nameMatches.Count == 1 ? nameMatches.FirstOrDefault() : null;
|
||||
|
||||
// zio: several online players match part of the name; don't fall through to the offline search.
|
||||
if (nameMatches.Count > 1)
|
||||
{
|
||||
command.ReplyToCommand($"Multiple targets found for \"{pattern}\": {string.Join(", ", nameMatches.Select(p => p.PlayerName))}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (namePlayer != null && namePlayer.IsValid)
|
||||
{
|
||||
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Mute);
|
||||
|
|
@ -919,6 +933,13 @@ public partial class CS2_SimpleAdmin
|
|||
var nameMatches = Helper.GetPlayerFromName(pattern);
|
||||
var namePlayer = nameMatches.Count == 1 ? nameMatches.FirstOrDefault() : null;
|
||||
|
||||
// zio: several online players match part of the name; don't fall through to the offline search.
|
||||
if (nameMatches.Count > 1)
|
||||
{
|
||||
command.ReplyToCommand($"Multiple targets found for \"{pattern}\": {string.Join(", ", nameMatches.Select(p => p.PlayerName))}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (namePlayer != null && namePlayer.IsValid)
|
||||
{
|
||||
PlayerPenaltyManager.RemovePenaltiesByType(namePlayer.Slot, PenaltyType.Silence);
|
||||
|
|
|
|||
|
|
@ -292,10 +292,9 @@ public partial class CS2_SimpleAdmin
|
|||
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
|
||||
|
||||
// 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
|
||||
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
|
||||
var steamId = player.SteamID;
|
||||
if (!string.IsNullOrEmpty(newName))
|
||||
{
|
||||
RenamedPlayers[player.SteamID] = newName;
|
||||
RenamedPlayers[steamId] = newName;
|
||||
player.Rename(newName);
|
||||
}
|
||||
else
|
||||
{
|
||||
RenamedPlayers.Remove(player.SteamID);
|
||||
RenamedPlayers.Remove(steamId);
|
||||
}
|
||||
|
||||
Task.Run(() => PlayerManager.SaveRenamedPlayer(steamId, newName));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,12 @@ public interface IDatabaseProvider
|
|||
string GetExpireBansQuery(bool multiServer);
|
||||
string GetExpireIpBansQuery(bool multiServer);
|
||||
string GetExpireOldPlayerIpsQuery();
|
||||
|
||||
|
||||
// Renames (css_prename)
|
||||
string GetRenamesQuery();
|
||||
string GetUpsertRenameQuery();
|
||||
string GetDeleteRenameQuery();
|
||||
|
||||
// MuteManager
|
||||
string GetAddMuteQuery(bool includePlayerName);
|
||||
string GetIsMutedQuery(bool multiServer, int timeMode);
|
||||
|
|
@ -61,4 +66,7 @@ public interface IDatabaseProvider
|
|||
string GetUnwarnByIdQuery(bool multiServer);
|
||||
string GetUnwarnLastQuery(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)
|
||||
{
|
||||
return multiServer
|
||||
? "SELECT id 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'"
|
||||
: "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()
|
||||
|
|
@ -238,6 +238,15 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
|
|||
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)
|
||||
{
|
||||
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 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,8 +143,8 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
|
||||
public string GetUnbanRetrieveBansQuery(bool multiServer) =>
|
||||
multiServer
|
||||
? "SELECT id 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'"
|
||||
: "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() =>
|
||||
"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) 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() =>
|
||||
"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) =>
|
||||
multiServer
|
||||
? "UPDATE sa_bans SET status = 'EXPIRED' 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"
|
||||
: "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) =>
|
||||
multiServer
|
||||
|
|
@ -169,7 +170,16 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
|
|||
|
||||
public string GetExpireOldPlayerIpsQuery() =>
|
||||
"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() =>
|
||||
"""
|
||||
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
|
||||
? "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";
|
||||
|
||||
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]
|
||||
public HookResult OnPlayerInfo(EventPlayerInfo @event, GameEventInfo _)
|
||||
{
|
||||
var player = @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);
|
||||
|
||||
EnforceRename(@event.Userid);
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -49,9 +49,12 @@ internal static class Helper
|
|||
}
|
||||
}
|
||||
|
||||
// zio: an exact (case-insensitive) name match, or else every player whose name contains it.
|
||||
public static List<CCSPlayerController> GetPlayerFromName(string name)
|
||||
{
|
||||
return GetValidPlayers().FindAll(x => x.PlayerName.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
var players = GetValidPlayers();
|
||||
var exact = players.FindAll(x => x.PlayerName.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
return exact.Count > 0 ? exact : players.FindAll(x => x.PlayerName.Contains(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static CCSPlayerController? GetPlayerFromSteamid64(ulong steamid)
|
||||
|
|
@ -144,19 +147,35 @@ internal static class Helper
|
|||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
// zio: staff rank groups, highest first, and their chat abbreviations.
|
||||
// zio: whether an admin_overrides.json override lets the player run the command, checked the way
|
||||
// CSS checks a typed command: "all" needs every flag, anything else ("any") needs one of them.
|
||||
internal static bool CanRunOverriddenCommand(CCSPlayerController player, string commandName)
|
||||
{
|
||||
var data = AdminManager.GetCommandOverrideData(commandName);
|
||||
if (data == null) return false;
|
||||
var flags = data.Flags.ToArray();
|
||||
BaseRequiresPermissions check = data.CheckType == "all"
|
||||
? new RequiresPermissions(flags)
|
||||
: new RequiresPermissionsOr(flags);
|
||||
check.Command = commandName;
|
||||
return check.CanExecuteCommand(player);
|
||||
}
|
||||
|
||||
// zio: rank groups, highest first, and their chat tags (the same as CS2-Tags' chat prefixes).
|
||||
// Guardian (VIP) is last, so a staff rank wins.
|
||||
private static readonly (string Group, string Tag)[] StaffRanks =
|
||||
[
|
||||
("#rank/owner", "O"),
|
||||
("#rank/senioradmin", "SA"),
|
||||
("#rank/admin", "A"),
|
||||
("#rank/trialadmin", "TA"),
|
||||
("#rank/mod", "M"),
|
||||
("#rank/trialmod", "TM"),
|
||||
("#rank/helper", "H"),
|
||||
("#rank/senioradmin", "S.Admin"),
|
||||
("#rank/admin", "Admin"),
|
||||
("#rank/trialadmin", "T.Admin"),
|
||||
("#rank/mod", "Mod"),
|
||||
("#rank/trialmod", "T.Mod"),
|
||||
("#rank/helper", "Helper"),
|
||||
("#rank/guardian", "Guardian"),
|
||||
];
|
||||
|
||||
// zio: the player's highest staff rank abbreviation, or null if they're in no rank group.
|
||||
// zio: the player's highest rank tag, or null if they're in no rank group.
|
||||
internal static string? GetStaffRankTag(CCSPlayerController player)
|
||||
{
|
||||
var groups = AdminManager.GetPlayerAdminData(new SteamID(player.SteamID))?.Groups;
|
||||
|
|
@ -987,74 +1006,6 @@ internal static class Helper
|
|||
|
||||
}
|
||||
|
||||
public static class PluginInfo
|
||||
{
|
||||
internal static async Task CheckVersion(string localVersion, ILogger logger)
|
||||
{
|
||||
ShowAd(localVersion);
|
||||
const string versionUrl = "https://raw.githubusercontent.com/daffyyyy/CS2-SimpleAdmin/main/CS2-SimpleAdmin/VERSION";
|
||||
var client = CS2_SimpleAdmin.HttpClient;
|
||||
client.Timeout = TimeSpan.FromSeconds(3);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync(versionUrl);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var remoteVersion = await response.Content.ReadAsStringAsync();
|
||||
remoteVersion = remoteVersion.Trim();
|
||||
|
||||
var comparisonResult = string.CompareOrdinal(localVersion, remoteVersion);
|
||||
|
||||
switch (comparisonResult)
|
||||
{
|
||||
case < 0:
|
||||
logger.LogWarning("Plugin is outdated! Check https://github.com/daffyyyy/CS2-SimpleAdmin");
|
||||
break;
|
||||
case > 0:
|
||||
logger.LogInformation("Probably dev version detected");
|
||||
break;
|
||||
default:
|
||||
logger.LogInformation("Plugin is up to date");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning($"Failed to check version. Status Code: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to connect to the version server.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "An error occurred while checking version.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowAd(string moduleVersion)
|
||||
{
|
||||
Console.WriteLine(" ");
|
||||
Console.WriteLine(" _______ ___ __ __ _______ ___ _______ _______ ______ __ __ ___ __ _ ");
|
||||
Console.WriteLine("| || | | |_| || || | | || _ || | | |_| || | | | | |");
|
||||
Console.WriteLine("| _____|| | | || _ || | | ___|| |_| || _ || || | | |_| |");
|
||||
Console.WriteLine("| |_____ | | | || |_| || | | |___ | || | | || || | | |");
|
||||
Console.WriteLine("|_____ || | | || ___|| |___ | ___|| || |_| || || | | _ |");
|
||||
Console.WriteLine(" _____| || | | ||_|| || | | || |___ | _ || || ||_|| || | | | | |");
|
||||
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(" ");
|
||||
}
|
||||
}
|
||||
|
||||
public class SchemaString<TSchemaClass>(TSchemaClass instance, string member)
|
||||
: NativeObject(Schema.GetSchemaValue<nint>(instance.Handle, typeof(TSchemaClass).Name, member))
|
||||
where TSchemaClass : NativeObject
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
||||
using CounterStrikeSharp.API.Modules.Entities;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -339,8 +340,34 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
|
|||
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ namespace CS2_SimpleAdmin.Managers;
|
|||
internal class CacheManager: IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
|
||||
private readonly ConcurrentDictionary<ulong, List<BanRecord>> _steamIdIndex = [];
|
||||
private readonly ConcurrentDictionary<uint, List<BanRecord>> _ipIndex = [];
|
||||
// Rebuilt into fresh instances and swapped, so unlocked readers on the game thread never see a half-built index
|
||||
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 HashSet<uint> _cachedIgnoredIps = [];
|
||||
|
|
@ -178,7 +180,7 @@ internal class CacheManager: IDisposable
|
|||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
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 }
|
||||
)).ToList();
|
||||
|
|
@ -221,7 +223,7 @@ internal class CacheManager: IDisposable
|
|||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
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 }
|
||||
)).ToList();
|
||||
|
|
@ -294,7 +296,7 @@ internal class CacheManager: IDisposable
|
|||
.GroupBy(x => x.Address)
|
||||
.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(
|
||||
g.Key,
|
||||
latest.Used_at,
|
||||
|
|
@ -353,8 +355,36 @@ internal class CacheManager: IDisposable
|
|||
/// </summary>
|
||||
private void RebuildIndexes()
|
||||
{
|
||||
_steamIdIndex.Clear();
|
||||
_ipIndex.Clear();
|
||||
// SetBanStatus (unban thread) and RefreshCacheAsync (timer thread) can both get here
|
||||
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
|
||||
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
|
||||
|
|
@ -369,10 +399,10 @@ internal class CacheManager: IDisposable
|
|||
if (ban.PlayerSteamId.HasValue)
|
||||
{
|
||||
var steamId = ban.PlayerSteamId.Value;
|
||||
if (!_steamIdIndex.TryGetValue(steamId, out var steamList))
|
||||
if (!steamIdIndex.TryGetValue(steamId, out var steamList))
|
||||
{
|
||||
steamList = new List<BanRecord>();
|
||||
_steamIdIndex[steamId] = steamList;
|
||||
steamIdIndex[steamId] = steamList;
|
||||
}
|
||||
steamList.Add(ban);
|
||||
}
|
||||
|
|
@ -381,14 +411,18 @@ internal class CacheManager: IDisposable
|
|||
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
|
||||
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>();
|
||||
_ipIndex[ipUInt] = ipList;
|
||||
ipIndex[ipUInt] = ipList;
|
||||
}
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -212,7 +212,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
|
|||
foreach (var muteRecord in muteRecords)
|
||||
{
|
||||
DateTime endDateTime = muteRecord.ends;
|
||||
PlayerPenaltyManager.RemovePenaltiesByDateTime(slot, endDateTime);
|
||||
// zio: match by type and approximate end time; see RemovePenaltiesByDateTime.
|
||||
string muteType = muteRecord.type;
|
||||
var penaltyType = muteType switch
|
||||
{
|
||||
"MUTE" => PenaltyType.Mute,
|
||||
"SILENCE" => PenaltyType.Silence,
|
||||
_ => PenaltyType.Gag
|
||||
};
|
||||
PlayerPenaltyManager.RemovePenaltiesByDateTime(slot, penaltyType, endDateTime);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <summary>
|
||||
/// Retrieves all players' flags and associated data asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>A list of tuples containing player SteamID, name, flags, immunity, and expiration time.</returns>
|
||||
private async Task<List<(ulong, string ,List<string>, int, DateTime?)>> GetAllPlayersFlags()
|
||||
/// <returns>A list of tuples containing player SteamID, name, flags, immunity, and expiration time, or null if the database query failed.</returns>
|
||||
private async Task<List<(ulong, string ,List<string>, int, DateTime?)>?> GetAllPlayersFlags()
|
||||
{
|
||||
if (databaseProvider == null)
|
||||
return new List<(ulong, string, List<string>, int, DateTime?)>();
|
||||
|
|
@ -68,7 +68,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
|
||||
return [];
|
||||
// zio: null rather than empty, so ReloadAdmins keeps the loaded admins instead of removing them all.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,96 +151,50 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <summary>
|
||||
/// Creates a JSON file containing admins data asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>zio: every admin row loaded, for ReloadAdmins to rebuild AdminCache from, or null if the database query failed (the file is then left alone).</returns>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
public async Task CreateAdminsJsonFile()
|
||||
public async Task<List<(SteamID SteamId, DateTime? Ends, List<string> Flags)>?> CreateAdminsJsonFile()
|
||||
{
|
||||
List<(ulong identity, string name, List<string> flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags();
|
||||
var allPlayers = await GetAllPlayersFlags();
|
||||
if (allPlayers == null) return null;
|
||||
|
||||
var validPlayers = allPlayers
|
||||
.Where(player => SteamID.TryParse(player.identity.ToString(), out _))
|
||||
.Select(player => (player, steamId: SteamID.TryParse(player.Item1.ToString(), out var steamId) ? steamId : null))
|
||||
.Where(p => p.steamId != null)
|
||||
.ToList();
|
||||
|
||||
var jsonData = validPlayers
|
||||
.GroupBy(player => player.name)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
object (group) =>
|
||||
{
|
||||
var consolidatedData = group.Aggregate(
|
||||
new
|
||||
{
|
||||
identity = string.Empty,
|
||||
immunity = 0,
|
||||
flags = new List<string>(),
|
||||
groups = new List<string>()
|
||||
},
|
||||
(acc, player) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
|
||||
{
|
||||
acc = acc with { identity = player.identity.ToString() };
|
||||
}
|
||||
// zio: keyed by SteamID, not name: CSS ignores the key, and two admins with the same name were
|
||||
// merged into one entry under the first one's SteamID. Upstream also cleared and refilled
|
||||
// AdminCache here once per admin, so it only ever kept the last admin and a reload never
|
||||
// removed anyone else; ReloadAdmins now does that once, from the list returned here.
|
||||
var jsonData = validPlayers
|
||||
.GroupBy(p => p.steamId!.SteamId64)
|
||||
.ToDictionary(
|
||||
group => group.Key.ToString(),
|
||||
object (group) =>
|
||||
{
|
||||
var rows = group.Select(p => p.player).ToList();
|
||||
var flags = rows.SelectMany(r => r.Item3).Distinct().ToList();
|
||||
return new
|
||||
{
|
||||
identity = group.Key.ToString(),
|
||||
immunity = rows.Max(r => r.Item4),
|
||||
flags = flags.Where(flag => flag.StartsWith($"@")).ToList(),
|
||||
groups = flags.Where(flag => flag.StartsWith($"#")).ToList()
|
||||
};
|
||||
});
|
||||
|
||||
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
acc = acc with
|
||||
{
|
||||
flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(),
|
||||
groups = acc.groups.Concat(player.flags.Where(flag => flag.StartsWith($"#"))).Distinct().ToList()
|
||||
};
|
||||
|
||||
return acc;
|
||||
});
|
||||
|
||||
Server.NextWorldUpdate(() =>
|
||||
{
|
||||
var keysToRemove = new List<SteamID>();
|
||||
|
||||
foreach (var steamId in AdminCache.Keys.ToList())
|
||||
{
|
||||
var data = AdminManager.GetPlayerAdminData(steamId);
|
||||
if (data != null)
|
||||
{
|
||||
var flagsArray = AdminCache[steamId].Flags.ToArray();
|
||||
AdminManager.RemovePlayerPermissions(steamId, flagsArray);
|
||||
AdminManager.RemovePlayerFromGroup(steamId, true, flagsArray);
|
||||
}
|
||||
|
||||
keysToRemove.Add(steamId);
|
||||
}
|
||||
|
||||
foreach (var steamId in keysToRemove)
|
||||
{
|
||||
if (!AdminCache.TryRemove(steamId, out _)) continue;
|
||||
|
||||
var data = AdminManager.GetPlayerAdminData(steamId);
|
||||
if (data == null) continue;
|
||||
if (data.Flags.Count != 0 && data.Groups.Count != 0) continue;
|
||||
|
||||
AdminManager.ClearPlayerPermissions(steamId);
|
||||
AdminManager.RemovePlayerAdminData(steamId);
|
||||
}
|
||||
|
||||
foreach (var player in group)
|
||||
{
|
||||
if (SteamID.TryParse(player.identity.ToString(), out var steamId) && steamId != null)
|
||||
{
|
||||
AdminCache.TryAdd(steamId, (player.ends, player.flags));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return consolidatedData;
|
||||
});
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(jsonData, options);
|
||||
var json = JsonSerializer.Serialize(jsonData, options);
|
||||
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
|
||||
return validPlayers.Select(p => (p.steamId!, p.player.Item5, p.player.Item3)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -247,7 +202,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// </summary>
|
||||
/// <param name="playerSteamId">The SteamID of the admin to delete.</param>
|
||||
/// <param name="globalDelete">Whether to delete the admin globally or only for the current server.</param>
|
||||
public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false)
|
||||
/// <param name="adminSteamId">zio: SteamID64 of who removed them, 0 for Console, for the rank log.</param>
|
||||
/// <param name="adminName">zio: name of who removed them, for the rank log.</param>
|
||||
public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false, ulong adminSteamId = 0, string adminName = "Console")
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
if (string.IsNullOrEmpty(playerSteamId)) return;
|
||||
|
|
@ -255,8 +212,14 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
// zio: note the rank and name before they're gone, for the rank log.
|
||||
var before = await GetRank(connection, playerSteamId);
|
||||
var playerName = await connection.ExecuteScalarAsync<string?>(
|
||||
"SELECT player_name FROM sa_admins WHERE player_steamid = @playerSteamId ORDER BY server_id IS NULL DESC, id LIMIT 1",
|
||||
new { playerSteamId }) ?? "";
|
||||
var sql = databaseProvider.GetDeleteAdminQuery(globalDelete);
|
||||
await connection.ExecuteAsync(sql, new { PlayerSteamID = playerSteamId, CS2_SimpleAdmin.ServerId });
|
||||
await LogRankChange(connection, playerSteamId, playerName, before, await GetRank(connection, playerSteamId), adminSteamId, adminName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -273,7 +236,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
/// <param name="immunity">Immunity level.</param>
|
||||
/// <param name="time">Duration in minutes for admin expiration; 0 means permanent.</param>
|
||||
/// <param name="globalAdmin">Whether the admin is global or server-specific.</param>
|
||||
public async Task AddAdminBySteamId(string playerSteamId, string playerName, List<string> flagsList, int immunity = 0, int time = 0, bool globalAdmin = false)
|
||||
/// <param name="adminSteamId">zio: SteamID64 of who added them, 0 for Console, for the rank log.</param>
|
||||
/// <param name="adminName">zio: name of who added them, for the rank log.</param>
|
||||
public async Task AddAdminBySteamId(string playerSteamId, string playerName, List<string> flagsList, int immunity = 0, int time = 0, bool globalAdmin = false, ulong adminSteamId = 0, string adminName = "Console")
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
|
|
@ -285,6 +250,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
try
|
||||
{
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var before = await GetRank(connection, playerSteamId); // zio: for the rank log
|
||||
|
||||
var insertAdminSql = databaseProvider.GetAddAdminQuery();
|
||||
var adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
|
||||
|
|
@ -307,6 +273,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
});
|
||||
}
|
||||
|
||||
await LogRankChange(connection, playerSteamId, playerName, before, await GetRank(connection, playerSteamId), adminSteamId, adminName);
|
||||
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.ReloadAdmins(null);
|
||||
|
|
@ -318,6 +286,69 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
|
|||
}
|
||||
}
|
||||
|
||||
// zio: rank history in sa_rank_log, a table owned by our web panel (simpleadmin-web's
|
||||
// sql/rank-log.sql), which logs its own changes there too. A player's rank is their
|
||||
// highest-immunity group; a change is only logged when that changes.
|
||||
private sealed class RankRow
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public int Immunity { get; set; }
|
||||
}
|
||||
|
||||
private static bool _rankLogMissingWarned;
|
||||
|
||||
// zio: the player's current rank, or null. Only unexpired admin rows and groups that apply to
|
||||
// this server count, the same set SimpleAdmin loads.
|
||||
private static async Task<RankRow?> GetRank(System.Data.Common.DbConnection connection, string playerSteamId)
|
||||
{
|
||||
return await connection.QueryFirstOrDefaultAsync<RankRow>("""
|
||||
SELECT g.name AS Name, g.immunity AS Immunity
|
||||
FROM sa_admins a
|
||||
JOIN sa_admins_flags f ON f.admin_id = a.id
|
||||
JOIN sa_groups g ON g.name = f.flag
|
||||
JOIN sa_groups_servers gs ON gs.group_id = g.id AND (gs.server_id IS NULL OR gs.server_id = @serverid)
|
||||
WHERE a.player_steamid = @playerSteamId AND (a.ends IS NULL OR a.ends > @now)
|
||||
AND (a.server_id IS NULL OR a.server_id = @serverid)
|
||||
ORDER BY g.immunity DESC, g.id LIMIT 1
|
||||
""", new { playerSteamId, now = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
|
||||
// zio: records a rank change in sa_rank_log. Never fails the admin change itself: if the table
|
||||
// is missing (the panel hasn't created it), it warns once and carries on.
|
||||
private static async Task LogRankChange(System.Data.Common.DbConnection connection, string playerSteamId, string playerName,
|
||||
RankRow? before, RankRow? after, ulong adminSteamId, string adminName)
|
||||
{
|
||||
if (before?.Name == after?.Name) return;
|
||||
var kind = before == null ? "added"
|
||||
: after == null ? "removed"
|
||||
: after.Immunity > before.Immunity ? "promoted"
|
||||
: after.Immunity < before.Immunity ? "demoted"
|
||||
: "changed";
|
||||
try
|
||||
{
|
||||
await connection.ExecuteAsync("""
|
||||
INSERT INTO sa_rank_log (player_steamid, player_name, old_rank, new_rank, kind, admin_steamid, admin_name, source, created)
|
||||
VALUES (@playerSteamId, @playerName, @oldRank, @newRank, @kind, @adminSteamId, @adminName, 'game', @created)
|
||||
""", new
|
||||
{
|
||||
playerSteamId = ulong.Parse(playerSteamId),
|
||||
playerName,
|
||||
oldRank = before?.Name,
|
||||
newRank = after?.Name,
|
||||
kind,
|
||||
adminSteamId,
|
||||
adminName,
|
||||
created = Time.ActualDateTime()
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_rankLogMissingWarned) return;
|
||||
_rankLogMissingWarned = true;
|
||||
CS2_SimpleAdmin._logger?.LogWarning($"Couldn't write sa_rank_log (the web panel creates it): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new group with flags and immunity asynchronously.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Globalization;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
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.
|
||||
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;
|
||||
|
||||
Task.Run(async () =>
|
||||
|
|
@ -297,10 +303,92 @@ internal class PlayerManager
|
|||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,9 +95,11 @@ public static class PlayerPenaltyManager
|
|||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
|
||||
{
|
||||
if (penaltiesList.Count == 0) return false;
|
||||
|
||||
endDateTime = penaltiesList.First().EndDateTime;
|
||||
// zio: a penalty marked passed is over, even before RemoveExpiredPenalties drops it.
|
||||
var active = penaltiesList.FirstOrDefault(p => !p.Passed || p.Duration == 0);
|
||||
if (active == default) return false;
|
||||
|
||||
endDateTime = active.EndDateTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -255,6 +257,26 @@ public static class PlayerPenaltyManager
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// zio: marks a penalty of one type as passed when its end is within a few seconds of the
|
||||
/// database's. A penalty added in game keeps the end time from the command, which has
|
||||
/// sub-second precision and is taken a moment before the one the database stores (to the
|
||||
/// second), so an exact match never finds it and the penalty outlives its database row.
|
||||
/// </summary>
|
||||
public static void RemovePenaltiesByDateTime(int slot, PenaltyType penaltyType, DateTime dateTime)
|
||||
{
|
||||
if (!Penalties.TryGetValue(slot, out var penaltyDict) ||
|
||||
!penaltyDict.TryGetValue(penaltyType, out var penaltiesList)) return;
|
||||
|
||||
for (var i = 0; i < penaltiesList.Count; i++)
|
||||
{
|
||||
if ((penaltiesList[i].EndDateTime - dateTime).Duration() > TimeSpan.FromSeconds(5)) continue;
|
||||
var penalty = penaltiesList[i];
|
||||
penalty.Passed = true;
|
||||
penaltiesList[i] = penalty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes or expires penalties automatically across all players based on their duration or "passed" flag.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ public class ServerManager
|
|||
if (_getIpTryCount > 32 && isInvalidIp)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Unable to load server data - can't fetch ip address!");
|
||||
_ = CS2_SimpleAdmin.PlayerManager.LoadRenamedPlayers();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +104,7 @@ public class ServerManager
|
|||
CS2_SimpleAdmin.ServerId = serverId;
|
||||
CS2_SimpleAdmin._logger?.LogInformation("Loaded server with ip {ip}", ipAddress);
|
||||
|
||||
CS2_SimpleAdmin.ServerLoaded = true;
|
||||
CS2_SimpleAdmin.ServerLoaded = true;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
var queryString = $"?address={address}&hostname={hostname}";
|
||||
|
|
|
|||
|
|
@ -20,20 +20,22 @@ public abstract class BasicMenu
|
|||
var manager = MenuManager.Instance;
|
||||
|
||||
// Players category menus - using translation keys
|
||||
manager.RegisterMenu("players", "slap", "sa_slap", CreateSlapMenu, "@css/slay");
|
||||
manager.RegisterMenu("players", "slay", "sa_slay", CreateSlayMenu, "@css/slay");
|
||||
manager.RegisterMenu("players", "kick", "sa_kick", CreateKickMenu, "@css/kick");
|
||||
manager.RegisterMenu("players", "warn", "sa_warn", CreateWarnMenu, "@css/kick");
|
||||
manager.RegisterMenu("players", "ban", "sa_ban", CreateBanMenu, "@css/ban");
|
||||
manager.RegisterMenu("players", "gag", "sa_gag", CreateGagMenu, "@css/chat");
|
||||
manager.RegisterMenu("players", "mute", "sa_mute", CreateMuteMenu, "@css/chat");
|
||||
manager.RegisterMenu("players", "silence", "sa_silence", CreateSilenceMenu, "@css/chat");
|
||||
manager.RegisterMenu("players", "team", "sa_team_force", CreateForceTeamMenu, "@css/kick");
|
||||
// zio: each entry names its command, so admin_overrides.json applies to the menu too.
|
||||
manager.RegisterMenu("players", "slap", "sa_slap", CreateSlapMenu, "@css/slay", "css_slap");
|
||||
manager.RegisterMenu("players", "slay", "sa_slay", CreateSlayMenu, "@css/slay", "css_slay");
|
||||
manager.RegisterMenu("players", "kick", "sa_kick", CreateKickMenu, "@css/kick", "css_kick");
|
||||
manager.RegisterMenu("players", "warn", "sa_warn", CreateWarnMenu, "@css/kick", "css_warn");
|
||||
manager.RegisterMenu("players", "ban", "sa_ban", CreateBanMenu, "@css/ban", "css_ban");
|
||||
manager.RegisterMenu("players", "gag", "sa_gag", CreateGagMenu, "@css/chat", "css_gag");
|
||||
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", "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
|
||||
manager.RegisterMenu("server", "plugins", "sa_menu_pluginsmanager_title", CreatePluginsMenu, "@css/root");
|
||||
manager.RegisterMenu("server", "changemap", "sa_changemap", CreateChangeMapMenu, "@css/changemap");
|
||||
manager.RegisterMenu("server", "restart", "sa_restart_game", CreateRestartGameMenu, "@css/generic");
|
||||
manager.RegisterMenu("server", "changemap", "sa_changemap", CreateChangeMapMenu, "@css/changemap", "css_map");
|
||||
manager.RegisterMenu("server", "restart", "sa_restart_game", CreateRestartGameMenu, "@css/generic", "css_rr");
|
||||
manager.RegisterMenu("server", "custom", "sa_menu_custom_commands", CreateCustomCommandsMenu, "@css/generic");
|
||||
|
||||
// Admin category menus - using translation keys
|
||||
|
|
@ -127,6 +129,62 @@ public abstract class BasicMenu
|
|||
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>
|
||||
/// Creates menu for kicking players with reason selection.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -219,8 +219,9 @@ public class MenuManager
|
|||
// Check if command has overridden permissions
|
||||
if (AdminManager.CommandIsOverriden(commandName))
|
||||
{
|
||||
var overriddenPermission = AdminManager.GetPermissionOverrides(commandName);
|
||||
hasPermission = AdminManager.PlayerHasPermissions(steamId, overriddenPermission);
|
||||
// zio: honour the override's check_type ("any" passes with one flag), as CSS does
|
||||
// for the typed command, instead of requiring every flag.
|
||||
hasPermission = Helper.CanRunOverriddenCommand(player, commandName);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(permission))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public partial class CS2_SimpleAdmin
|
|||
internal MuteManager MuteManager = new(DatabaseProvider);
|
||||
internal WarnManager WarnManager = new(DatabaseProvider);
|
||||
internal CacheManager? CacheManager = new();
|
||||
private static readonly PlayerManager PlayerManager = new();
|
||||
internal static readonly PlayerManager PlayerManager = new();
|
||||
|
||||
// Timers
|
||||
internal Timer? PlayersTimer = null;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{GREEN}[ CS2-SimpleAdmin HELP ]{DEFAULT}
|
||||
- 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_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
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@
|
|||
"sa_admin_team_message": "{lightred}{0}{default} نقل {lightred}{1}{default} إلى {lightred}{2}{default}!",
|
||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}تحذيرات",
|
||||
"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_message": "{lightred}{0}{default} بدأ التصويت لـ {lightred}{1}{default}",
|
||||
"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_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_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_message": "{lightred}{0}{default} hat eine Abstimmung für {lightred}{1}{default} gestartet",
|
||||
"sa_admin_vote_message_results": "{lime}ABSTIMMUNGSERGEBNISSE FÜR {gold}{0}",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"sa_title": "SimpleAdmin",
|
||||
"sa_prefix": "{lightred}[SA] {default}",
|
||||
"sa_prefix": "{default}[{red}zio{default}] ",
|
||||
|
||||
"sa_unknown": "Unknown",
|
||||
"sa_no_permission": "You do not have permissions to use this command.",
|
||||
"sa_ban_max_duration_exceeded": "Ban duration cannot exceed {lightred}{0}{default} minutes.",
|
||||
"sa_ban_max_duration_exceeded": "Ban duration cannot exceed {red}{0}{default} minutes.",
|
||||
"sa_ban_perm_restricted": "You do not have the right to permanently ban.",
|
||||
|
||||
"sa_admin_add": "Add Admin",
|
||||
|
|
@ -64,30 +64,30 @@
|
|||
"sa_discord_penalty_warn": "Warn registered",
|
||||
"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_chat_active": "{red}Your chat is blocked until: {grey}{0}",
|
||||
"sa_player_penalty_gag_active": "You are gagged for {red}{0}{default}.\nReason: {red}{1}",
|
||||
"sa_player_penalty_gag_active_perm": "You are gagged {red}permanently{default}.\nReason: {red}{0}",
|
||||
"sa_player_penalty_silence_active": "You are silenced for {red}{0}{default}.\nReason: {red}{1}",
|
||||
"sa_player_penalty_silence_active_perm": "You are silenced {red}permanently{default}.\nReason: {red}{0}",
|
||||
"sa_player_penalty_no_reason": "none given",
|
||||
"sa_player_ban_active": "{lightred}You are banned from this server for {grey}{0}{lightred}. Reason: {grey}{1}",
|
||||
"sa_player_ban_active_perm": "{lightred}You are banned from this server permanently. Reason: {grey}{0}",
|
||||
"sa_player_ban_active_other_account": "{grey}The ban is on another account that has played from your IP address.",
|
||||
"sa_player_ban_active_kick": "{grey}You will be disconnected in a few seconds.",
|
||||
"sa_player_ban_active": "You are banned from this server for {red}{0}{default}.\nReason: {red}{1}",
|
||||
"sa_player_ban_active_perm": "You are banned from this server {red}permanently{default}.\nReason: {red}{0}",
|
||||
"sa_player_ban_active_other_account": "The ban is on another account that has played from your IP address.",
|
||||
"sa_player_ban_active_kick": "You will be disconnected in a few seconds.",
|
||||
|
||||
"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}]",
|
||||
"sa_player_penalty_info_active_silence": "➔ Silence [{lightred}❌{default}] - Expire [{lightred}{0}{default}]",
|
||||
"sa_player_penalty_info_active_warn": "➔ Warn [{lightred}❌{default}] - Expire [{lightred}{0}{default}] - Reason [{lightred}{1}{default}]",
|
||||
"sa_player_penalty_info_active_mute": "➔ Mute [{lightred}❌{default}] - Expire [{red}{0}{default}]",
|
||||
"sa_player_penalty_info_active_gag": "➔ Gag [{lightred}❌{default}] - Expire [{red}{0}{default}]",
|
||||
"sa_player_penalty_info_active_silence": "➔ Silence [{lightred}❌{default}] - Expire [{red}{0}{default}]",
|
||||
"sa_player_penalty_info_active_warn": "➔ Warn [{lightred}❌{default}] - Expire [{red}{0}{default}] - Reason [{red}{1}{default}]",
|
||||
|
||||
"sa_player_penalty_info_no_active_mute": "➔ Mute [{lime}✔{default}]",
|
||||
"sa_player_penalty_info_no_active_gag": "➔ Gag [{lime}✔{default}]",
|
||||
"sa_player_penalty_info_no_active_silence": "➔ Silence [{lime}✔{default}]",
|
||||
"sa_player_penalty_info_no_active_warn": "➔ Warn [{lime}✔{default}]",
|
||||
|
||||
"sa_player_penalty_info": "===========================\nPlayer penalties for {lightred}{0}{default},\nNumber of bans: {lightred}{1}{default}, Number of gags: {lightred}{2}{default}, Number of mutes: {lightred}{3}{default}, Number of silences: {lightred}{4}{default}, Number of warnings: {lightred}{5}{default}\nActive penalties:\n{6}\nActive warnings:\n{7}\n===========================",
|
||||
"sa_admin_penalty_info": "{grey}Player penalties for {lightred}{0}{grey}, Bans: {lightred}{1}{grey}, Gags: {lightred}{2}{grey}, Mutes: {lightred}{3}{grey}, Silences: {lightred}{4}{grey}, Warns: {lightred}{5}",
|
||||
"sa_admin_associated_accounts": "{grey}Associated accounts of player {lightred}{0}{grey}: {1}",
|
||||
"sa_player_penalty_info": "===========================\nPlayer penalties for {yellow}{0}{default},\nNumber of bans: {red}{1}{default}, Number of gags: {red}{2}{default}, Number of mutes: {red}{3}{default}, Number of silences: {red}{4}{default}, Number of warnings: {red}{5}\nActive penalties:\n{6}\nActive warnings:\n{7}\n===========================",
|
||||
"sa_admin_penalty_info": "Player penalties for {yellow}{0}{default}, Bans: {red}{1}{default}, Gags: {red}{2}{default}, Mutes: {red}{3}{default}, Silences: {red}{4}{default}, Warns: {red}{5}",
|
||||
"sa_admin_associated_accounts": "Associated accounts of player {yellow}{0}{default}: {1}",
|
||||
"sa_player_ban_message_time": "You have been banned for {lightred}{0}{default} for {lightred}{1}{default} minutes by {lightred}{2}{default}!",
|
||||
"sa_player_ban_message_perm": "You have been banned permanently for {lightred}{0}{default} by {lightred}{1}{default}!",
|
||||
"sa_player_kick_message": "You have been kicked for {lightred}{0}{default} by {lightred}{1}{default}!",
|
||||
|
|
@ -99,55 +99,60 @@
|
|||
"sa_player_silence_message_perm": "You have been silenced permanently for {lightred}{0}{default} by {lightred}{1}{default}!",
|
||||
"sa_player_warn_message_time": "You have been warned for {lightred}{0}{default} for {lightred}{1}{default} minutes by {lightred}{2}{default}!",
|
||||
"sa_player_warn_message_perm": "You have been warned permanently for {lightred}{0}{default} by {lightred}{1}{default}!",
|
||||
"sa_admin_ban_message_time": "{lightred}{0}{default} banned {lightred}{1}{default} for {lightred}{2}{default} for {lightred}{3}{default} minutes!",
|
||||
"sa_admin_ban_message_perm": "{lightred}{0}{default} banned {lightred}{1}{default} permanently for {lightred}{2}{default}!",
|
||||
"sa_admin_kick_message": "{lightred}{0}{default} kicked {lightred}{1}{default} for {lightred}{2}{default}!",
|
||||
"sa_admin_gag_message_time": "{lightred}{0}{default} gagged {lightred}{1}{default} for {lightred}{2}{default} for {lightred}{3}{default} minutes!",
|
||||
"sa_admin_gag_message_perm": "{lightred}{0}{default} gagged {lightred}{1}{default} permanently for {lightred}{2}{default}!",
|
||||
"sa_admin_mute_message_time": "{lightred}{0}{default} muted {lightred}{1}{default} for {lightred}{2}{default} for {lightred}{3}{default} minutes!",
|
||||
"sa_admin_mute_message_perm": "{lightred}{0}{default} muted {lightred}{1}{default} permanently for {lightred}{2}{default}!",
|
||||
"sa_admin_silence_message_time": "{lightred}{0}{default} silenced {lightred}{1}{default} for {lightred}{2}{default} for {lightred}{3}{default} minutes!",
|
||||
"sa_admin_silence_message_perm": "{lightred}{0}{default} silenced {lightred}{1}{default} permanently for {lightred}{2}{default}!",
|
||||
"sa_admin_warn_message_time": "{lightred}{0}{default} warned {lightred}{1}{default} for {lightred}{2}{default} for {lightred}{3}{default} minutes!",
|
||||
"sa_admin_warn_message_perm": "{lightred}{0}{default} warned {lightred}{1}{default} permanently for {lightred}{2}{default}!",
|
||||
"sa_admin_give_message": "{lightred}{0}{default} gave {lightred}{1}{default} a {lightred}{2}{default}!",
|
||||
"sa_admin_strip_message": "{lightred}{0}{default} took all of player {lightred}{1}{default} weapons!",
|
||||
"sa_admin_hp_message": "{lightred}{0}{default} changed {lightred}{1}{default} hp amount{default}!",
|
||||
"sa_admin_speed_message": "{lightred}{0}{default} changed speed for {lightred}{1}{default}!",
|
||||
"sa_admin_gravity_message": "{lightred}{0}{default} changed gravity for {lightred}{1}{default}!",
|
||||
"sa_admin_money_message": "{lightred}{0}{default} changed money for {lightred}{1}{default}!",
|
||||
"sa_admin_god_message": "{lightred}{0}{default} changed god mode for {lightred}{1}{default}!",
|
||||
"sa_admin_resize_message": "{lightred}{0}{default} changed size for {lightred}{1}{default}!",
|
||||
"sa_admin_slay_message": "{lightred}{0}{default} slayed {lightred}{1}{default}!",
|
||||
"sa_admin_slap_message": "{lightred}{0}{default} slapped {lightred}{1}{default}!",
|
||||
"sa_admin_changemap_message": "{lightred}{0}{default} changed map to {lightred}{1}{default}!",
|
||||
"sa_admin_noclip_message": "{lightred}{0}{default} toggled noclip for {lightred}{1}{default}!",
|
||||
"sa_admin_freeze_message": "{lightred}{0}{default} froze {lightred}{1}{default}!",
|
||||
"sa_admin_unfreeze_message": "{lightred}{0}{default} unfroze {lightred}{1}{default}!",
|
||||
"sa_admin_rename_message": "{lightred}{0}{default} changed {lightred}{1}{default} nickname to {lightred}{2}{default}!",
|
||||
"sa_admin_respawn_message": "{lightred}{0}{default} respawned {lightred}{1}{default}!",
|
||||
"sa_admin_tp_message": "{lightred}{0}{default} teleported to {lightred}{1}{default}!",
|
||||
"sa_admin_bring_message": "{lightred}{0}{default} teleported to himself {lightred}{1}{default}!",
|
||||
"sa_admin_team_message": "{lightred}{0}{default} transfered {lightred}{1}{default} to {lightred}{2}{default}!",
|
||||
"sa_admin_ban_message_time": "{red}{0}{default} banned {yellow}{1}{default} for {red}{2}{default} for {red}{3}{default} minutes",
|
||||
"sa_admin_ban_message_perm": "{red}{0}{default} banned {yellow}{1}{default} permanently for {red}{2}",
|
||||
"sa_admin_kick_message": "{red}{0}{default} kicked {yellow}{1}{default} for {red}{2}",
|
||||
"sa_admin_gag_message_time": "{red}{0}{default} gagged {yellow}{1}{default} for {red}{2}{default} for {red}{3}{default} minutes",
|
||||
"sa_admin_gag_message_perm": "{red}{0}{default} gagged {yellow}{1}{default} permanently for {red}{2}",
|
||||
"sa_admin_mute_message_time": "{red}{0}{default} muted {yellow}{1}{default} for {red}{2}{default} for {red}{3}{default} minutes",
|
||||
"sa_admin_mute_message_perm": "{red}{0}{default} muted {yellow}{1}{default} permanently for {red}{2}",
|
||||
"sa_admin_silence_message_time": "{red}{0}{default} silenced {yellow}{1}{default} for {red}{2}{default} for {red}{3}{default} minutes",
|
||||
"sa_admin_silence_message_perm": "{red}{0}{default} silenced {yellow}{1}{default} permanently for {red}{2}",
|
||||
"sa_admin_warn_message_time": "{red}{0}{default} warned {yellow}{1}{default} for {red}{2}{default} for {red}{3}{default} minutes",
|
||||
"sa_admin_warn_message_perm": "{red}{0}{default} warned {yellow}{1}{default} permanently for {red}{2}{default}!",
|
||||
"sa_admin_give_message": "{red}{0}{default} gave {yellow}{1}{default} a {grey}{2}",
|
||||
"sa_admin_strip_message": "{red}{0}{default} took all of player {yellow}{1}{default} weapons",
|
||||
"sa_admin_hp_message": "{red}{0}{default} changed {yellow}{1}{default} hp amount",
|
||||
"sa_admin_speed_message": "{red}{0}{default} changed speed for {yellow}{1}",
|
||||
"sa_admin_gravity_message": "{red}{0}{default} changed gravity for {yellow}{1}",
|
||||
"sa_admin_money_message": "{red}{0}{default} changed money for {yellow}{1}",
|
||||
"sa_admin_god_message": "{red}{0}{default} changed god mode for {yellow}{1}",
|
||||
"sa_admin_resize_message": "{red}{0}{default} changed size for {yellow}{1}",
|
||||
"sa_admin_slay_message": "{red}{0}{default} slayed {yellow}{1}",
|
||||
"sa_admin_slap_message": "{red}{0}{default} slapped {yellow}{1}",
|
||||
"sa_admin_changemap_message": "{red}{0}{default} changed map to {green}{1}",
|
||||
"sa_admin_noclip_message": "{red}{0}{default} toggled noclip for {yellow}{1}",
|
||||
"sa_admin_freeze_message": "{red}{0}{default} {blue}froze{default} {yellow}{1}",
|
||||
"sa_admin_unfreeze_message": "{red}{0}{default} {blue}unfroze{default} {yellow}{1}",
|
||||
"sa_admin_rename_message": "{red}{0}{default} changed {yellow}{1}{default} nickname to {yellow}{2}",
|
||||
"sa_admin_respawn_message": "{red}{0}{default} respawned {yellow}{1}",
|
||||
"sa_admin_tp_message": "{red}{0}{default} teleported to {yellow}{1}",
|
||||
"sa_admin_bring_message": "{red}{0}{default} teleported {yellow}{1}{default} to themselves",
|
||||
"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_unwarn": "{lime}Successfully{default} unwarned {gold}{0} {default}for {gold}{1}{default}!",
|
||||
"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_message": "{lightred}{0}{default} started voting for {lightred}{1}{default}",
|
||||
"sa_admin_vote_message_results": "{lime}VOTING RESULTS FOR {gold}{0}",
|
||||
"sa_admin_vote_message_results_answer": "{lime}{0} {default}- {gold}{1}",
|
||||
"sa_admin_voice_mute_all": "{Lightred}You muted {default}all players during your speech",
|
||||
"sa_admin_voice_unmute_all": "{Lime}You unmuted {default}all players",
|
||||
"sa_admin_voice_listen_all": "{Default}You can now hear {lime}all {default}players",
|
||||
"sa_admin_voice_unlisten_all": "{Default}You no longer hear {lime}all {default}players",
|
||||
"sa_adminsay_prefix": "{RED}ADMIN: {lightred}{0}{default}",
|
||||
"sa_adminchat_template_admin": "{LIME}(ADMIN) {lightred}{0}{default}: {lightred}{1}{default}",
|
||||
"sa_staffchat_template": "{default}>[{yellow}STAFF{default}][{lime}{0}{default}] {yellow}{1}{default}: {grey}{2}{default}",
|
||||
"sa_staffchat_template_norank": "{default}>[{yellow}STAFF{default}] {yellow}{0}{default}: {grey}{1}{default}",
|
||||
"sa_staffchat_template_player": "{default}>[{purple}STAFF{default}] {purple}{0}{default}: {purple}{1}{default}",
|
||||
"sa_staffsay_template": "[{lime}{0}{default}] {darkred}{1}{default}: {red}{2}{default}",
|
||||
"sa_staffsay_template_norank": "{darkred}{0}{default}: {red}{1}{default}",
|
||||
"sa_adminchat_template_player": "{SILVER}(PLAYER) {lightred}{0}{default}: {lightred}{1}{default}",
|
||||
"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_answer": "{lime}{0} {default}- {orange}{1}",
|
||||
"sa_admin_voice_mute_all": "{red}You muted {default}all players",
|
||||
"sa_admin_voice_unmute_all": "{lime}You unmuted {default}all players",
|
||||
"sa_admin_voice_listen_all": "You can now hear {lime}all {default}players",
|
||||
"sa_admin_voice_unlisten_all": "You no longer hear {lime}all {default}players",
|
||||
"sa_adminsay_prefix": "{lightred}{0}",
|
||||
"sa_adminchat_template_admin": ">[{yellow}STAFF{default}][{lime}{0}{default}] {grey}{1}",
|
||||
"sa_staffchat_template": ">[{yellow}STAFF{default}][{lime}{0}{default}] {yellow}{1}{default}: {grey}{2}",
|
||||
"sa_staffchat_template_norank": ">[{yellow}STAFF{default}] {yellow}{0}{default}: {grey}{1}",
|
||||
"sa_staffchat_template_player": ">[{red}STAFF{default}] {silver}{0}{default}: {silver}{1}",
|
||||
"sa_staffsay_template": "{default}[{red}ALL{default}] [{lime}{0}{default}] {yellow}{1}{default}: {yellow}{2}{default}",
|
||||
"sa_staffsay_template_norank": "{yellow}{0}{default}: {red}{1}",
|
||||
"sa_psay_template": "[{red}PM{default}] [{lime}{0}{default}] {yellow}{1}{default}: {grey}{2}",
|
||||
"sa_psay_template_norank": "[{red}PM{default}] {yellow}{0}{default}: {grey}{1}",
|
||||
"sa_adminchat_template_player": "{SILVER}(PLAYER) {lightred}{0}{default}: {lightred}{1}",
|
||||
"sa_discord_log_command": "**{0}** issued command `{1}` on server `HOSTNAME`",
|
||||
"sa_menu_pluginsmanager_loaded": "{lime}Enabled {default}plugin {lime}{0}",
|
||||
"sa_menu_pluginsmanager_unloaded": "{lightred}Disabled {default}plugin {lightred}{0}"
|
||||
"sa_menu_pluginsmanager_unloaded": "{red}Disabled {default}plugin {yellow}{0}"
|
||||
}
|
||||
|
|
@ -122,6 +122,9 @@
|
|||
"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_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_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}",
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@
|
|||
"sa_admin_team_message": "{lightred}{0}{default} {lightred}{1}{default} را به {lightred}{2}{default} انتقال داد!",
|
||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}هشدارها",
|
||||
"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_message": "{lightred}{0}{default} رأیگیری برای {lightred}{1}{default} را شروع کرد",
|
||||
"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_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_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_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}",
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@
|
|||
"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_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_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}",
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@
|
|||
"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_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_message": "{lightred}{0}{default} rozpoczął głosowanie na {lightred}{1}{default}",
|
||||
"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_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_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_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}",
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@
|
|||
"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_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_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}",
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@
|
|||
"sa_admin_team_message": "{lightred}{0}{default} перевел {lightred}{1}{default} в команду {lightred}{2}{default}!",
|
||||
"sa_admin_warns_menu_title": "{gold}{0} {lime}ПРЕДУПРЕЖДЕНИЯ",
|
||||
"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_message": "{lightred}{0}{default} начал голосование за {lightred}{1}{default}",
|
||||
"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_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_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_message": "{lightred}{0}{default} {lightred}{1}{default} için oy kullanmaya başladı",
|
||||
"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_warns_menu_title": "{gold}{0} {lime}警告",
|
||||
"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_message": "{lightred}{0}{default} 发起了 [{lightred}{1}{default}] 的投票",
|
||||
"sa_admin_vote_message_results": "{lime}投票结果 {gold}{0}",
|
||||
|
|
|
|||
40
README.md
40
README.md
|
|
@ -14,13 +14,49 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code:
|
|||
- New permission `@css/bypassipban`, for players or groups: IP bans, and bans on other accounts
|
||||
that share their IP, don't apply to them; a ban on their own SteamID still does. `@css/root`
|
||||
has it, as it has every `@css` flag.
|
||||
- Staff chat shows the sender's rank, from their highest `#rank/...` group (O, SA, A, TA, M, TM,
|
||||
H for helper): `@message` in team chat and `css_asay` read `>[STAFF][RANK] name: message`, and
|
||||
- Staff chat shows the sender's rank, from their highest `#rank/...` group (O, S.Admin, Admin,
|
||||
T.Admin, Mod, T.Mod, Helper, Guardian, the same as CS2-Tags' chat prefixes): `@message` in team chat and `css_asay` read
|
||||
`>[STAFF][RANK] name: message`, and
|
||||
`@message` in all chat and `css_say` from a player read `[RANK] name: message` (`name: message`
|
||||
if they have no rank group). A non-staff player's `@message` in team chat reads
|
||||
`>[STAFF] name: message`, all in purple. Console's `css_say`/`css_asay` are unchanged. New lang keys
|
||||
`sa_staffchat_template`, `sa_staffsay_template`, their `_norank` variants and
|
||||
`sa_staffchat_template_player`, in `lang/en.json` only.
|
||||
- `css_psay` from a player reads `(PM) [RANK] name: message` (`(PM) name: message` without a rank
|
||||
group); console's is unchanged. Lang keys `sa_psay_template` and `sa_psay_template_norank`.
|
||||
- `admin_overrides.json` reaches the admin menu too: every player and server entry names its command,
|
||||
and an overridden entry is checked with the override's `check_type` (`any` passes with one flag),
|
||||
as CSS checks the typed command. Upstream required every flag in the override.
|
||||
- `css_penalties <player>` (someone else's penalties) needs `@zio/penalties` or `@css/root`
|
||||
instead of `@css/kick`.
|
||||
- `css_addadmin`, `css_deladmin` and the admin menu's add/remove log rank changes (the player's
|
||||
highest-immunity group, and who changed it) to `sa_rank_log`, the table simpleadmin-web creates
|
||||
and lists as the public promotions/demotions log. If the table is missing, it warns once in the
|
||||
log and the admin change goes ahead.
|
||||
- With `TimeMode` 0 (online time only), a gag, mute or silence given in game now ends when its
|
||||
database row expires. Upstream matched the two by exact end time, which never matched for one
|
||||
given in game (the database stores the time to the second), so it lasted until the player
|
||||
reconnected. A penalty marked passed also stops applying at once, not on the next 61-second tick.
|
||||
- Targeting by part of a name: when several names contain it, one that is exactly it (any case)
|
||||
is picked, and otherwise the reply lists the matches. `css_ungag`, `css_unmute` and
|
||||
`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.
|
||||
- No startup version check or banner: upstream's `PluginInfo` (`CheckVersion`, which asked upstream's
|
||||
GitHub for its `VERSION` on every load, and `ShowAd`, the console banner with two encoded lines
|
||||
of personal messages) is removed. `EnableUpdateCheck` stays in the config, so existing files
|
||||
still load, and does nothing.
|
||||
|
||||
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`).
|
||||
It rebuilds from the committed source and uploads `SimpleAdmin-<tag>.tar.gz` to git.zio.sh
|
||||
|
|
|
|||
11
release.sh
11
release.sh
|
|
@ -22,8 +22,13 @@ 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.
|
||||
# Needs write:repository. Only uploading needs it; downloading from the public repo doesn't. From the
|
||||
# environment, or else from .forgejo-token in the cs2/ folder above every plugin (outside their
|
||||
# repos, mode 600). Don't commit a real token.
|
||||
TOKEN_FILE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/../../.forgejo-token"
|
||||
if [[ -z "${FORGEJO_TOKEN:-}" && -r "$TOKEN_FILE" ]]; then
|
||||
FORGEJO_TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
|
||||
fi
|
||||
FORGEJO_TOKEN="${FORGEJO_TOKEN:-CHANGE_ME_FORGEJO_TOKEN}"
|
||||
|
||||
TAG="${1:-}"
|
||||
|
|
@ -32,7 +37,7 @@ if [[ -z "$TAG" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
if [[ "$FORGEJO_TOKEN" == "CHANGE_ME_FORGEJO_TOKEN" ]]; then
|
||||
echo "Set FORGEJO_TOKEN (edit release.sh or export it) first." >&2
|
||||
echo "Set FORGEJO_TOKEN (export it, or put it in $TOKEN_FILE) first." >&2
|
||||
exit 1
|
||||
fi
|
||||
for tool in podman jq curl git; do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue