From 703893a908480ba2d67cbe58697dcc1b3ddb1cb3 Mon Sep 17 00:00:00 2001 From: Astra Date: Mon, 28 Sep 2026 19:50:02 +0100 Subject: [PATCH 01/13] zio fork 1.8.2b-zio7: guardian rank tag, ranks on private messages #rank/guardian shows as G, after the staff ranks so a staff rank wins. css_psay from a player reads "(PM) [RANK] name: message" ("(PM) name: message" without a rank group); console's is unchanged. New lang keys sa_psay_template and sa_psay_template_norank. --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/Commands/basechat.cs | 9 ++++++++- CS2-SimpleAdmin/Helper.cs | 6 ++++-- CS2-SimpleAdmin/lang/en.json | 2 ++ README.md | 5 ++++- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index dbf769a..bea9180 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio7"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/Commands/basechat.cs b/CS2-SimpleAdmin/Commands/basechat.cs index a301a9b..8df9685 100644 --- a/CS2-SimpleAdmin/Commands/basechat.cs +++ b/CS2-SimpleAdmin/Commands/basechat.cs @@ -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}".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!"); diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 226ff47..8227425 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -144,7 +144,8 @@ internal static class Helper return string.Join(" ", parts); } - // zio: staff rank groups, highest first, and their chat abbreviations. + // zio: rank groups, highest first, and their chat abbreviations. Guardian (VIP) is last, so a + // staff rank wins. private static readonly (string Group, string Tag)[] StaffRanks = [ ("#rank/owner", "O"), @@ -154,9 +155,10 @@ internal static class Helper ("#rank/mod", "M"), ("#rank/trialmod", "TM"), ("#rank/helper", "H"), + ("#rank/guardian", "G"), ]; - // zio: the player's highest staff rank abbreviation, or null if they're in no rank group. + // zio: the player's highest rank abbreviation, or null if they're in no rank group. internal static string? GetStaffRankTag(CCSPlayerController player) { var groups = AdminManager.GetPlayerAdminData(new SteamID(player.SteamID))?.Groups; diff --git a/CS2-SimpleAdmin/lang/en.json b/CS2-SimpleAdmin/lang/en.json index 7f60c78..2dd7b7e 100644 --- a/CS2-SimpleAdmin/lang/en.json +++ b/CS2-SimpleAdmin/lang/en.json @@ -146,6 +146,8 @@ "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_psay_template": "{default}(PM) [{lime}{0}{default}] {yellow}{1}{default}: {grey}{2}{default}", + "sa_psay_template_norank": "{default}(PM) {yellow}{0}{default}: {grey}{1}{default}", "sa_adminchat_template_player": "{SILVER}(PLAYER) {lightred}{0}{default}: {lightred}{1}{default}", "sa_discord_log_command": "**{0}** issued command `{1}` on server `HOSTNAME`", "sa_menu_pluginsmanager_loaded": "{lime}Enabled {default}plugin {lime}{0}", diff --git a/README.md b/README.md index cabad66..5425f65 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,15 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: 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 + H for helper, G for guardian): `@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`. Publish with `FORGEJO_TOKEN=... ./release.sh v` (ModuleVersion is `1.8.2b-zioN`). It rebuilds from the committed source and uploads `SimpleAdmin-.tar.gz` to git.zio.sh From 65a413fa52a4dcacf9280faf730310d760178823 Mon Sep 17 00:00:00 2001 From: Astra Date: Mon, 28 Sep 2026 19:56:25 +0100 Subject: [PATCH 02/13] zio fork 1.8.2b-zio8: staff all-chat line with a lime name and gold message --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/lang/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index bea9180..39b6984 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio7"; + public override string ModuleVersion => "1.8.2b-zio8"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/lang/en.json b/CS2-SimpleAdmin/lang/en.json index 2dd7b7e..b679d1a 100644 --- a/CS2-SimpleAdmin/lang/en.json +++ b/CS2-SimpleAdmin/lang/en.json @@ -144,7 +144,7 @@ "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": "{default}[{lime}{0}{default}] {lime}{1}{default}: {gold}{2}{default}", "sa_staffsay_template_norank": "{darkred}{0}{default}: {red}{1}{default}", "sa_psay_template": "{default}(PM) [{lime}{0}{default}] {yellow}{1}{default}: {grey}{2}{default}", "sa_psay_template_norank": "{default}(PM) {yellow}{0}{default}: {grey}{1}{default}", From cf1090cbd2d2889fc69fed591e057e9c0a2e155c Mon Sep 17 00:00:00 2001 From: Astra Date: Tue, 29 Sep 2026 12:44:15 +0100 Subject: [PATCH 03/13] zio fork 1.8.2b-zio9: admin menu honours command overrides, @zio/penalties Every player and server entry in the admin menu names its command, and an overridden entry is checked with the override's check_type ("any" passes with one flag), the way CSS checks the typed command; upstream required every flag. css_penalties for someone else needs @zio/penalties or @css/root instead of @css/kick. --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/Commands/basecommands.cs | 4 +++- CS2-SimpleAdmin/Helper.cs | 14 ++++++++++++++ CS2-SimpleAdmin/Menus/BasicMenu.cs | 23 ++++++++++++----------- CS2-SimpleAdmin/Menus/MenuManager.cs | 5 +++-- README.md | 5 +++++ 6 files changed, 38 insertions(+), 15 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 39b6984..2c85a41 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio8"; + public override string ModuleVersion => "1.8.2b-zio9"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index 9513cbc..122ca85 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -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); diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 8227425..5851218 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -144,6 +144,20 @@ internal static class Helper return string.Join(" ", parts); } + // 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 abbreviations. Guardian (VIP) is last, so a // staff rank wins. private static readonly (string Group, string Tag)[] StaffRanks = diff --git a/CS2-SimpleAdmin/Menus/BasicMenu.cs b/CS2-SimpleAdmin/Menus/BasicMenu.cs index 1234079..ae9ef3f 100644 --- a/CS2-SimpleAdmin/Menus/BasicMenu.cs +++ b/CS2-SimpleAdmin/Menus/BasicMenu.cs @@ -20,20 +20,21 @@ 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"); // 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 diff --git a/CS2-SimpleAdmin/Menus/MenuManager.cs b/CS2-SimpleAdmin/Menus/MenuManager.cs index 6abf3ea..f850a33 100644 --- a/CS2-SimpleAdmin/Menus/MenuManager.cs +++ b/CS2-SimpleAdmin/Menus/MenuManager.cs @@ -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)) { diff --git a/README.md b/README.md index 5425f65..a1bd016 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,11 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: `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 ` (someone else's penalties) needs `@zio/penalties` or `@css/root` + instead of `@css/kick`. Publish with `FORGEJO_TOKEN=... ./release.sh v` (ModuleVersion is `1.8.2b-zioN`). It rebuilds from the committed source and uploads `SimpleAdmin-.tar.gz` to git.zio.sh From f46ccf18ce3cb2668dc3dc99dde88d5b941ab25b Mon Sep 17 00:00:00 2001 From: Astra Date: Tue, 29 Sep 2026 16:56:38 +0100 Subject: [PATCH 04/13] zio fork 1.8.2b-zio10: rank tags match CS2-Tags, log rank changes to sa_rank_log --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/Commands/basecommands.cs | 7 +- CS2-SimpleAdmin/Helper.cs | 20 ++--- CS2-SimpleAdmin/Managers/PermissionManager.cs | 80 ++++++++++++++++++- README.md | 8 +- 5 files changed, 100 insertions(+), 17 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 2c85a41..f14ed8d 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio9"; + public override string ModuleVersion => "1.8.2b-zio10"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index 122ca85..e2a2b30 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -286,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}"); @@ -331,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, () => { diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 5851218..105be3b 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -158,21 +158,21 @@ internal static class Helper return check.CanExecuteCommand(player); } - // zio: rank groups, highest first, and their chat abbreviations. Guardian (VIP) is last, so a - // staff rank wins. + // 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/guardian", "G"), + ("#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 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; diff --git a/CS2-SimpleAdmin/Managers/PermissionManager.cs b/CS2-SimpleAdmin/Managers/PermissionManager.cs index 5826e74..73d191c 100644 --- a/CS2-SimpleAdmin/Managers/PermissionManager.cs +++ b/CS2-SimpleAdmin/Managers/PermissionManager.cs @@ -247,7 +247,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) /// /// The SteamID of the admin to delete. /// Whether to delete the admin globally or only for the current server. - public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false) + /// zio: SteamID64 of who removed them, 0 for Console, for the rank log. + /// zio: name of who removed them, for the rank log. + 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 +257,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( + "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 +281,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) /// Immunity level. /// Duration in minutes for admin expiration; 0 means permanent. /// Whether the admin is global or server-specific. - public async Task AddAdminBySteamId(string playerSteamId, string playerName, List flagsList, int immunity = 0, int time = 0, bool globalAdmin = false) + /// zio: SteamID64 of who added them, 0 for Console, for the rank log. + /// zio: name of who added them, for the rank log. + public async Task AddAdminBySteamId(string playerSteamId, string playerName, List flagsList, int immunity = 0, int time = 0, bool globalAdmin = false, ulong adminSteamId = 0, string adminName = "Console") { if (databaseProvider == null) return; @@ -285,6 +295,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(insertAdminSql, new @@ -307,6 +318,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 +331,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 GetRank(System.Data.Common.DbConnection connection, string playerSteamId) + { + return await connection.QueryFirstOrDefaultAsync(""" + 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}"); + } + } + /// /// Adds a new group with flags and immunity asynchronously. /// diff --git a/README.md b/README.md index a1bd016..4a2bdb0 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ 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, G for guardian): `@message` in team chat and `css_asay` read +- 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 @@ -29,6 +29,10 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: as CSS checks the typed command. Upstream required every flag in the override. - `css_penalties ` (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. Publish with `FORGEJO_TOKEN=... ./release.sh v` (ModuleVersion is `1.8.2b-zioN`). It rebuilds from the committed source and uploads `SimpleAdmin-.tar.gz` to git.zio.sh From d042aab1c0c5affba2a64006e11fec4526b952c7 Mon Sep 17 00:00:00 2001 From: Astra Date: Fri, 2 Oct 2026 21:21:09 +0100 Subject: [PATCH 05/13] zio fork 1.8.2b-zio11: reloading admins removes deleted and demoted admins CSS's admin loaders only add, and the per-admin cleanup in CreateAdminsJsonFile reset AdminCache once per admin, so it only ever tracked the last one and a reload never took anyone else out of memory. ReloadAdmins now removes every admin it loaded last time and loads the new set in one tick. admins.json is keyed by SteamID instead of name, so admins sharing a name aren't merged, and a failed admins query keeps the loaded admins instead of removing them. --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/Commands/basecommands.cs | 41 +++--- CS2-SimpleAdmin/Managers/PermissionManager.cs | 123 ++++++------------ 3 files changed, 63 insertions(+), 103 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index f14ed8d..7192b94 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio10"; + public override string ModuleVersion => "1.8.2b-zio11"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index e2a2b30..4b2302e 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -507,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(); diff --git a/CS2-SimpleAdmin/Managers/PermissionManager.cs b/CS2-SimpleAdmin/Managers/PermissionManager.cs index 73d191c..4104a8a 100644 --- a/CS2-SimpleAdmin/Managers/PermissionManager.cs +++ b/CS2-SimpleAdmin/Managers/PermissionManager.cs @@ -17,8 +17,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) /// /// Retrieves all players' flags and associated data asynchronously. /// - /// A list of tuples containing player SteamID, name, flags, immunity, and expiration time. - private async Task, int, DateTime?)>> GetAllPlayersFlags() + /// A list of tuples containing player SteamID, name, flags, immunity, and expiration time, or null if the database query failed. + private async Task, int, DateTime?)>?> GetAllPlayersFlags() { if (databaseProvider == null) return new List<(ulong, string, List, 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) /// /// Creates a JSON file containing admins data asynchronously. /// + /// zio: every admin row loaded, for ReloadAdmins to rebuild AdminCache from, or null if the database query failed (the file is then left alone). [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - public async Task CreateAdminsJsonFile() + public async Task Flags)>?> CreateAdminsJsonFile() { - List<(ulong identity, string name, List 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(), - groups = new List() - }, - (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(); - - 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(); } /// From 395e513f4402fb37328a6bb3fb03255645312e6f Mon Sep 17 00:00:00 2001 From: Astra Date: Fri, 2 Oct 2026 21:44:01 +0100 Subject: [PATCH 06/13] zio fork 1.8.2b-zio12: restyle lang/en.json [zio] prefix, red admin and yellow player names in announcements, two-line gag/silence/ban notices with the reason on its own line, [PM] for private messages, and no {default} at the start or end of messages (sa_prefix keeps it). The bring message says "to themselves". --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/lang/en.json | 134 ++++++++++++++--------------- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 7192b94..477ad2b 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio11"; + public override string ModuleVersion => "1.8.2b-zio12"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/lang/en.json b/CS2-SimpleAdmin/lang/en.json index b679d1a..f39210e 100644 --- a/CS2-SimpleAdmin/lang/en.json +++ b/CS2-SimpleAdmin/lang/en.json @@ -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,57 +99,57 @@ "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_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": "{default}[{lime}{0}{default}] {lime}{1}{default}: {gold}{2}{default}", - "sa_staffsay_template_norank": "{darkred}{0}{default}: {red}{1}{default}", - "sa_psay_template": "{default}(PM) [{lime}{0}{default}] {yellow}{1}{default}: {grey}{2}{default}", - "sa_psay_template_norank": "{default}(PM) {yellow}{0}{default}: {grey}{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": "[{lime}{0}{default}] {yellow}{1}{default}: {red}{2}", + "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}" } \ No newline at end of file From bbb72757e74930f2a0eb156b27b247001b1c072d Mon Sep 17 00:00:00 2001 From: Astra Date: Fri, 2 Oct 2026 21:47:48 +0100 Subject: [PATCH 07/13] zio fork 1.8.2b-zio13: no double space in console css_say/css_asay/css_psay The message is taken from the first space in the command string, so it starts with a space; the player paths trimmed it, the console paths didn't. --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/Commands/basechat.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 477ad2b..0fd0cf4 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio12"; + public override string ModuleVersion => "1.8.2b-zio13"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/Commands/basechat.cs b/CS2-SimpleAdmin/Commands/basechat.cs index 8df9685..b4cae68 100644 --- a/CS2-SimpleAdmin/Commands/basechat.cs +++ b/CS2-SimpleAdmin/Commands/basechat.cs @@ -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 } } @@ -131,7 +131,7 @@ public partial class CS2_SimpleAdmin // zio: from a player, "(PM) [RANK] name: message". var rank = caller == null ? null : Helper.GetStaffRankTag(caller); var line = caller == null || _localizer == null - ? $"({callerName}) {utf8String}".ReplaceColorTags() + ? $"({callerName}) {utf8String.Trim()}".ReplaceColorTags() : rank == null ? _localizer["sa_psay_template_norank", callerName, utf8String.Trim()] : _localizer["sa_psay_template", rank, callerName, utf8String.Trim()]; From 7b3d0ed452e4d9a48aa16bc7c31844b8e0af9b36 Mon Sep 17 00:00:00 2001 From: Astra Date: Sat, 3 Oct 2026 00:07:20 +0100 Subject: [PATCH 08/13] zio fork 1.8.2b-zio14: [ALL] tag on staff all-chat (sa_staffsay_template) --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/lang/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 0fd0cf4..a16ab9c 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio13"; + public override string ModuleVersion => "1.8.2b-zio14"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/lang/en.json b/CS2-SimpleAdmin/lang/en.json index f39210e..36b31c6 100644 --- a/CS2-SimpleAdmin/lang/en.json +++ b/CS2-SimpleAdmin/lang/en.json @@ -144,7 +144,7 @@ "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": "[{lime}{0}{default}] {yellow}{1}{default}: {red}{2}", + "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}", From dc5ad30952f2db36a529a5bc2e63cd5856c98a78 Mon Sep 17 00:00:00 2001 From: Astra Date: Sat, 3 Oct 2026 00:15:03 +0100 Subject: [PATCH 09/13] zio fork 1.8.2b-zio15: in-game gags/mutes/silences expire with TimeMode 0 The in-memory penalty was only cleared when its end time exactly matched the database row's, which never happens for one given in game (sub-second vs to-the-second), so it lasted until reconnect. Match by type within 5 seconds, and stop applying a penalty as soon as it's marked passed. --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/Managers/MuteManager.cs | 10 ++++++- .../Managers/PlayerPenaltyManager.cs | 28 +++++++++++++++++-- README.md | 4 +++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index a16ab9c..23a403c 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio14"; + public override string ModuleVersion => "1.8.2b-zio15"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/Managers/MuteManager.cs b/CS2-SimpleAdmin/Managers/MuteManager.cs index 01d83df..6f9b65b 100644 --- a/CS2-SimpleAdmin/Managers/MuteManager.cs +++ b/CS2-SimpleAdmin/Managers/MuteManager.cs @@ -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); } } diff --git a/CS2-SimpleAdmin/Managers/PlayerPenaltyManager.cs b/CS2-SimpleAdmin/Managers/PlayerPenaltyManager.cs index 7931fe3..9acdc04 100644 --- a/CS2-SimpleAdmin/Managers/PlayerPenaltyManager.cs +++ b/CS2-SimpleAdmin/Managers/PlayerPenaltyManager.cs @@ -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 } } + /// + /// 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. + /// + 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; + } + } + /// /// Removes or expires penalties automatically across all players based on their duration or "passed" flag. /// diff --git a/README.md b/README.md index 4a2bdb0..ee39a72 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: 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. Publish with `FORGEJO_TOKEN=... ./release.sh v` (ModuleVersion is `1.8.2b-zioN`). It rebuilds from the committed source and uploads `SimpleAdmin-.tar.gz` to git.zio.sh From 8bec658db4313d39f9a4bbaa1dd1431841241fce Mon Sep 17 00:00:00 2001 From: Astra Date: Sat, 3 Oct 2026 00:18:04 +0100 Subject: [PATCH 10/13] Targets: prefer an exact name, partial names for ungag/unmute/unsilence Part of 1.8.2b-zio15. --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 8 +++++++- CS2-SimpleAdmin/Commands/basecomms.cs | 21 +++++++++++++++++++++ CS2-SimpleAdmin/Helper.cs | 5 ++++- README.md | 4 ++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 23a403c..c4ea959 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -242,7 +242,13 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig 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; } diff --git a/CS2-SimpleAdmin/Commands/basecomms.cs b/CS2-SimpleAdmin/Commands/basecomms.cs index 6305f40..e3b5656 100644 --- a/CS2-SimpleAdmin/Commands/basecomms.cs +++ b/CS2-SimpleAdmin/Commands/basecomms.cs @@ -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); diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 105be3b..1a04a9c 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -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 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) diff --git a/README.md b/README.md index ee39a72..b0669d1 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,10 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: 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. Publish with `FORGEJO_TOKEN=... ./release.sh v` (ModuleVersion is `1.8.2b-zioN`). It rebuilds from the committed source and uploads `SimpleAdmin-.tar.gz` to git.zio.sh From 71d51d7b92858b32229a367f7286e55b13236b2e Mon Sep 17 00:00:00 2001 From: Astra Date: Sat, 3 Oct 2026 12:08:45 +0100 Subject: [PATCH 11/13] zio fork 1.8.2b-zio16: backport unban, rename and css_history from upstream 1.9.0 From upstream 530f603: - css_unban updates the ban cache at once (and clears the engine banid with UnlockedCommands) instead of waiting up to a minute for the cache refresh. - css_prename is saved in sa_renames (migration 017) and re-applied on connect and name change, so it survives restarts. css_rename from a hidden admin renames again. - New css_history command and Players > History menu entry. Commands missing from an existing Commands.json are registered under their default name. - Migrations are copied with a glob, so 017 reaches the build output. Upstream's admin-loading changes, css_say change and css_penalties fix are left out. --- .gitignore | 1 + .../docs/user/commands/basecommands.md | 30 ++++++ .../docs/user/commands/playercommands.md | 6 +- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 2 +- CS2-SimpleAdmin/CS2-SimpleAdmin.csproj | 98 +------------------ CS2-SimpleAdmin/Commands/RegisterCommands.cs | 9 ++ CS2-SimpleAdmin/Commands/basecommands.cs | 93 ++++++++++++++++++ CS2-SimpleAdmin/Commands/playercommands.cs | 12 ++- CS2-SimpleAdmin/Database/IDatabaseProvider.cs | 10 +- .../Mysql/017_CreateRenamesTable.sql | 5 + .../Sqlite/017_CreateRenamesTable.sql | 4 + .../Database/MysqlDatabaseProvider.cs | 36 ++++++- .../Database/SqliteDatabaseProvider.cs | 45 +++++++-- CS2-SimpleAdmin/Events.cs | 37 ++++--- CS2-SimpleAdmin/Managers/BanManager.cs | 29 +++++- CS2-SimpleAdmin/Managers/CacheManager.cs | 56 ++++++++--- CS2-SimpleAdmin/Managers/PlayerManager.cs | 92 ++++++++++++++++- CS2-SimpleAdmin/Managers/ServerManager.cs | 6 +- CS2-SimpleAdmin/Menus/BasicMenu.cs | 57 +++++++++++ CS2-SimpleAdmin/Variables.cs | 2 +- CS2-SimpleAdmin/admin_help.txt | 1 + CS2-SimpleAdmin/lang/ar.json | 3 + CS2-SimpleAdmin/lang/de.json | 3 + CS2-SimpleAdmin/lang/en.json | 3 + CS2-SimpleAdmin/lang/es.json | 3 + CS2-SimpleAdmin/lang/fa.json | 3 + CS2-SimpleAdmin/lang/fr.json | 3 + CS2-SimpleAdmin/lang/lv.json | 3 + CS2-SimpleAdmin/lang/pl.json | 3 + CS2-SimpleAdmin/lang/pt-BR.json | 3 + CS2-SimpleAdmin/lang/pt-PT.json | 3 + CS2-SimpleAdmin/lang/ru.json | 3 + CS2-SimpleAdmin/lang/tr.json | 3 + CS2-SimpleAdmin/lang/zh-Hans.json | 3 + README.md | 12 +++ 35 files changed, 539 insertions(+), 143 deletions(-) create mode 100644 CS2-SimpleAdmin/Database/Migrations/Mysql/017_CreateRenamesTable.sql create mode 100644 CS2-SimpleAdmin/Database/Migrations/Sqlite/017_CreateRenamesTable.sql diff --git a/.gitignore b/.gitignore index 55358e7..dad4536 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ CLAUDE.md /Modules/CS2-SimpleAdmin_StealthModule/METAMOD PLUGIN release-out/ +.claude/ diff --git a/CS2-SimpleAdmin-docs/docs/user/commands/basecommands.md b/CS2-SimpleAdmin-docs/docs/user/commands/basecommands.md index 5414380..b56f80f 100644 --- a/CS2-SimpleAdmin-docs/docs/user/commands/basecommands.md +++ b/CS2-SimpleAdmin-docs/docs/user/commands/basecommands.md @@ -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. diff --git a/CS2-SimpleAdmin-docs/docs/user/commands/playercommands.md b/CS2-SimpleAdmin-docs/docs/user/commands/playercommands.md index 1db3d93..e51c959 100644 --- a/CS2-SimpleAdmin-docs/docs/user/commands/playercommands.md +++ b/CS2-SimpleAdmin-docs/docs/user/commands/playercommands.md @@ -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 ` with no name removes the rename - Useful for offensive names --- diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index c4ea959..338a41f 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio15"; + public override string ModuleVersion => "1.8.2b-zio16"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.csproj b/CS2-SimpleAdmin/CS2-SimpleAdmin.csproj index a3ad25d..98ecdcd 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.csproj +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.csproj @@ -36,106 +36,10 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + diff --git a/CS2-SimpleAdmin/Commands/RegisterCommands.cs b/CS2-SimpleAdmin/Commands/RegisterCommands.cs index 584f3ec..2edfa43 100644 --- a/CS2-SimpleAdmin/Commands/RegisterCommands.cs +++ b/CS2-SimpleAdmin/Commands/RegisterCommands.cs @@ -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) diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index 4b2302e..e0c362b 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -846,6 +846,99 @@ public partial class CS2_SimpleAdmin }); } + /// + /// 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. + /// + /// The player issuing the command or null for console. + /// The command containing the target and an optional penalty type filter. + [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 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); + }); + }); + } + + /// + /// Writes history rows from as one console line each. + /// + internal void PrintHistory(Action print, string name, ulong steamId, List 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") ?? "?"; + } + /// /// Lists players currently connected to the server with options to output JSON or filter duplicate IPs. /// diff --git a/CS2-SimpleAdmin/Commands/playercommands.cs b/CS2-SimpleAdmin/Commands/playercommands.cs index 2330ccf..209f122 100644 --- a/CS2-SimpleAdmin/Commands/playercommands.cs +++ b/CS2-SimpleAdmin/Commands/playercommands.cs @@ -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)); }); } diff --git a/CS2-SimpleAdmin/Database/IDatabaseProvider.cs b/CS2-SimpleAdmin/Database/IDatabaseProvider.cs index e2a4ae4..cdfea20 100644 --- a/CS2-SimpleAdmin/Database/IDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/IDatabaseProvider.cs @@ -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); } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Database/Migrations/Mysql/017_CreateRenamesTable.sql b/CS2-SimpleAdmin/Database/Migrations/Mysql/017_CreateRenamesTable.sql new file mode 100644 index 0000000..634bd07 --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Mysql/017_CreateRenamesTable.sql @@ -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; diff --git a/CS2-SimpleAdmin/Database/Migrations/Sqlite/017_CreateRenamesTable.sql b/CS2-SimpleAdmin/Database/Migrations/Sqlite/017_CreateRenamesTable.sql new file mode 100644 index 0000000..ec893be --- /dev/null +++ b/CS2-SimpleAdmin/Database/Migrations/Sqlite/017_CreateRenamesTable.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS `sa_renames` ( + `player_steamid` INTEGER NOT NULL PRIMARY KEY, + `name` TEXT NOT NULL +); diff --git a/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs index fa9a27e..baf4cac 100644 --- a/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/MysqlDatabaseProvider.cs @@ -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; } diff --git a/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs b/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs index d6186fb..53b2191 100644 --- a/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs +++ b/CS2-SimpleAdmin/Database/SqliteDatabaseProvider.cs @@ -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 + """; } diff --git a/CS2-SimpleAdmin/Events.cs b/CS2-SimpleAdmin/Events.cs index bd0a404..7559a72 100644 --- a/CS2-SimpleAdmin/Events.cs +++ b/CS2-SimpleAdmin/Events.cs @@ -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; + } + + /// + /// 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). + /// + 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); + }); + } } \ No newline at end of file diff --git a/CS2-SimpleAdmin/Managers/BanManager.cs b/CS2-SimpleAdmin/Managers/BanManager.cs index 5863b59..f8a04b2 100644 --- a/CS2-SimpleAdmin/Managers/BanManager.cs +++ b/CS2-SimpleAdmin/Managers/BanManager.cs @@ -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) diff --git a/CS2-SimpleAdmin/Managers/CacheManager.cs b/CS2-SimpleAdmin/Managers/CacheManager.cs index db74f2c..165efac 100644 --- a/CS2-SimpleAdmin/Managers/CacheManager.cs +++ b/CS2-SimpleAdmin/Managers/CacheManager.cs @@ -10,8 +10,10 @@ namespace CS2_SimpleAdmin.Managers; internal class CacheManager: IDisposable { private readonly ConcurrentDictionary _banCache = []; - private readonly ConcurrentDictionary> _steamIdIndex = []; - private readonly ConcurrentDictionary> _ipIndex = []; + // Rebuilt into fresh instances and swapped, so unlocked readers on the game thread never see a half-built index + private ConcurrentDictionary> _steamIdIndex = []; + private ConcurrentDictionary> _ipIndex = []; + private readonly object _indexLock = new(); private readonly ConcurrentDictionary> _playerIpsCache = []; private HashSet _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 /// private void RebuildIndexes() { - _steamIdIndex.Clear(); - _ipIndex.Clear(); + // SetBanStatus (unban thread) and RefreshCacheAsync (timer thread) can both get here + lock (_indexLock) + { + RebuildIndexesCore(); + } + } + + /// + /// 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. + /// + /// Ban ids to update. + /// New status. + public void SetBanStatus(IEnumerable 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>(); + var ipIndex = new ConcurrentDictionary>(); // 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(); - _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(); - _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); } /// diff --git a/CS2-SimpleAdmin/Managers/PlayerManager.cs b/CS2-SimpleAdmin/Managers/PlayerManager.cs index 44debaa..a7422ef 100644 --- a/CS2-SimpleAdmin/Managers/PlayerManager.cs +++ b/CS2-SimpleAdmin/Managers/PlayerManager.cs @@ -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)) + /// + /// 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. + /// + /// SteamID64 of the player, online or not. + /// Optional filter: bans, gags, mutes, silences or warns. + public async Task> 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 []; + } + } + + /// + /// Dapper gives DateTime on MySQL but can give a string on SQLite; accept both. + /// + 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 + }; + + /// + /// Loads permanent renames from the database into memory. + /// + 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); + } + } + + /// + /// Persists or removes a permanent rename (css_prename) for a player. + /// + /// SteamID64 of the player. + /// Forced name, or null/empty to remove the rename. + 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); } } diff --git a/CS2-SimpleAdmin/Managers/ServerManager.cs b/CS2-SimpleAdmin/Managers/ServerManager.cs index ec80aaa..10dcc16 100644 --- a/CS2-SimpleAdmin/Managers/ServerManager.cs +++ b/CS2-SimpleAdmin/Managers/ServerManager.cs @@ -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}"; diff --git a/CS2-SimpleAdmin/Menus/BasicMenu.cs b/CS2-SimpleAdmin/Menus/BasicMenu.cs index ae9ef3f..41c8ea1 100644 --- a/CS2-SimpleAdmin/Menus/BasicMenu.cs +++ b/CS2-SimpleAdmin/Menus/BasicMenu.cs @@ -30,6 +30,7 @@ public abstract class BasicMenu 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"); @@ -128,6 +129,62 @@ public abstract class BasicMenu return slayMenu.WithBackButton(); } + /// + /// 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. + /// + /// The admin player opening the menu. + /// A MenuBuilder instance for the history menu. + 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(); + } + /// /// Creates menu for kicking players with reason selection. /// diff --git a/CS2-SimpleAdmin/Variables.cs b/CS2-SimpleAdmin/Variables.cs index d3566d6..ab65b36 100644 --- a/CS2-SimpleAdmin/Variables.cs +++ b/CS2-SimpleAdmin/Variables.cs @@ -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; diff --git a/CS2-SimpleAdmin/admin_help.txt b/CS2-SimpleAdmin/admin_help.txt index e8ce7af..8a01ab5 100644 --- a/CS2-SimpleAdmin/admin_help.txt +++ b/CS2-SimpleAdmin/admin_help.txt @@ -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 [time in minutes/0 perm] [reason] - Ban player via steamid64 diff --git a/CS2-SimpleAdmin/lang/ar.json b/CS2-SimpleAdmin/lang/ar.json index 4533510..1236e7f 100644 --- a/CS2-SimpleAdmin/lang/ar.json +++ b/CS2-SimpleAdmin/lang/ar.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/de.json b/CS2-SimpleAdmin/lang/de.json index f32db52..dfdfc85 100644 --- a/CS2-SimpleAdmin/lang/de.json +++ b/CS2-SimpleAdmin/lang/de.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/en.json b/CS2-SimpleAdmin/lang/en.json index 36b31c6..d808e55 100644 --- a/CS2-SimpleAdmin/lang/en.json +++ b/CS2-SimpleAdmin/lang/en.json @@ -131,6 +131,9 @@ "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 {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": "{red}{0}{default} started voting for {orange}{1}", "sa_admin_vote_message_results": "{lime}VOTING RESULTS FOR {orange}{0}", diff --git a/CS2-SimpleAdmin/lang/es.json b/CS2-SimpleAdmin/lang/es.json index e5b0edd..a966057 100644 --- a/CS2-SimpleAdmin/lang/es.json +++ b/CS2-SimpleAdmin/lang/es.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/fa.json b/CS2-SimpleAdmin/lang/fa.json index 77d8273..a15fbd8 100644 --- a/CS2-SimpleAdmin/lang/fa.json +++ b/CS2-SimpleAdmin/lang/fa.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/fr.json b/CS2-SimpleAdmin/lang/fr.json index 27a4731..391a3a5 100644 --- a/CS2-SimpleAdmin/lang/fr.json +++ b/CS2-SimpleAdmin/lang/fr.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/lv.json b/CS2-SimpleAdmin/lang/lv.json index e27aed8..1128ff3 100644 --- a/CS2-SimpleAdmin/lang/lv.json +++ b/CS2-SimpleAdmin/lang/lv.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/pl.json b/CS2-SimpleAdmin/lang/pl.json index 3b6b3a7..ae6be91 100644 --- a/CS2-SimpleAdmin/lang/pl.json +++ b/CS2-SimpleAdmin/lang/pl.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/pt-BR.json b/CS2-SimpleAdmin/lang/pt-BR.json index 299ebc2..95f08cb 100644 --- a/CS2-SimpleAdmin/lang/pt-BR.json +++ b/CS2-SimpleAdmin/lang/pt-BR.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/pt-PT.json b/CS2-SimpleAdmin/lang/pt-PT.json index 94cce8c..93fc675 100644 --- a/CS2-SimpleAdmin/lang/pt-PT.json +++ b/CS2-SimpleAdmin/lang/pt-PT.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/ru.json b/CS2-SimpleAdmin/lang/ru.json index f92fb5a..de16a5f 100644 --- a/CS2-SimpleAdmin/lang/ru.json +++ b/CS2-SimpleAdmin/lang/ru.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/tr.json b/CS2-SimpleAdmin/lang/tr.json index c520397..12c9cbd 100644 --- a/CS2-SimpleAdmin/lang/tr.json +++ b/CS2-SimpleAdmin/lang/tr.json @@ -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}", diff --git a/CS2-SimpleAdmin/lang/zh-Hans.json b/CS2-SimpleAdmin/lang/zh-Hans.json index c3d2331..d8597da 100644 --- a/CS2-SimpleAdmin/lang/zh-Hans.json +++ b/CS2-SimpleAdmin/lang/zh-Hans.json @@ -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}", diff --git a/README.md b/README.md index b0669d1..47b3a1b 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,18 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: `css_unsilence` match online players by part of the name too (upstream needed the full name), falling back to the offline search only when no one online matches. +Backported from upstream 1.9.0 (commit `530f603`), not marked `zio:`: + +- `css_unban` applies at once: the ban cache is updated straight away instead of on the next + 61-second refresh, and `removeid` clears the engine ban when `UnlockedCommands` is on. +- `css_prename` survives restarts: renames are stored in the new `sa_renames` table (migration + 017) and re-applied on connect and name change. `css_rename` from a hidden admin renames again. +- New `css_history <#userid|name|steamid64> [bans|gags|mutes|silences|warns]` (`@css/kick`), and + Players > History in the admin menu. Commands missing from an existing `Commands.json` are + registered under their default name. +- Left out: upstream's admin-loading changes (ours differ, see `1.8.2b-zio11`), its `css_say` + change and its `css_penalties` fix. + Publish with `FORGEJO_TOKEN=... ./release.sh v` (ModuleVersion is `1.8.2b-zioN`). It rebuilds from the committed source and uploads `SimpleAdmin-.tar.gz` to git.zio.sh (`cs2/SimpleAdmin`), laid out like `game/csgo/`. It holds the whole plugin (its libraries, all lang From 1a1253b8653e0f7bd205bca0b42d3d62bb095a49 Mon Sep 17 00:00:00 2001 From: Astra Date: Sun, 4 Oct 2026 18:34:10 +0100 Subject: [PATCH 12/13] release.sh: read the Forgejo token from ../../.forgejo-token when FORGEJO_TOKEN isn't set --- release.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/release.sh b/release.sh index 0ccb29b..6913b4e 100755 --- a/release.sh +++ b/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 From 4420d41048bb9d953dfa349b0fc3195dc15faef1 Mon Sep 17 00:00:00 2001 From: Astra Date: Wed, 7 Oct 2026 09:47:58 +0100 Subject: [PATCH 13/13] zio fork 1.8.2b-zio17: no startup version check or banner Removes upstream's PluginInfo: CheckVersion, which fetched upstream's VERSION from GitHub on every load, and ShowAd, the console banner with two encoded lines of personal messages. EnableUpdateCheck stays in the config so existing files load, and does nothing. --- CS2-SimpleAdmin/CS2-SimpleAdmin.cs | 6 +-- CS2-SimpleAdmin/Helper.cs | 68 ------------------------------ README.md | 4 ++ 3 files changed, 7 insertions(+), 71 deletions(-) diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 338a41f..63c23d1 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "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-zio16"; + 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 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); diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 1a04a9c..98d07a9 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -1006,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 { 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 { 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 instance, string member) : NativeObject(Schema.GetSchemaValue(instance.Handle, typeof(TSchemaClass).Name, member)) where TSchemaClass : NativeObject diff --git a/README.md b/README.md index 47b3a1b..6a3254e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: 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:`: