From 3e652f9f6ffe63e4c9c15e6e1a35e754313af554 Mon Sep 17 00:00:00 2001 From: Astra Date: Mon, 28 Sep 2026 20:00:53 +0100 Subject: [PATCH 1/5] 1.0.4c-zio3: spell out ranks on the scoreboard, without brackets CS2 adds its own brackets around the clan tag, so "[O]" showed as "[[O]]". The default scoreboard tags are now Owner, Sen.Admin, Admin, T.Admin, Mod, T.Mod, Helper and Guardian. Chat keeps the short [O]/[M] prefixes. --- CS2-Tags.cs | 23 ++++++++++++----------- README.md | 16 +++++++++------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/CS2-Tags.cs b/CS2-Tags.cs index ec61549..b09b7cd 100644 --- a/CS2-Tags.cs +++ b/CS2-Tags.cs @@ -33,7 +33,7 @@ public class CS2_Tags : BasePlugin public override string ModuleName => "CS2-Tags"; public override string ModuleDescription => "Add player tags easily in cs2 game"; public override string ModuleAuthor => "daffyy"; - public override string ModuleVersion => "1.0.4c-zio2"; + public override string ModuleVersion => "1.0.4c-zio3"; private static string TagsPath => Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", "CS2-Tags", "tags.json"); @@ -260,19 +260,20 @@ public class CS2_Tags : BasePlugin // Written to configs/plugins/CS2-Tags/tags.json on first load. Our CS2-SimpleAdmin rank groups, // highest first; guardian (VIP) comes after staff so a staff member's rank tag wins. The - // abbreviations match CS2-SimpleAdmin's staff chat ("[M] name: message"). + // abbreviations match CS2-SimpleAdmin's staff chat ("[M] name: message"); the scoreboard spells + // the rank out. No brackets there: CS2 adds its own around the clan tag. private const string DefaultTagsJson = """ { "tags": { - "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[O]" }, - "#rank/senioradmin": { "prefix": "{Default}[{Lime}SA{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[SA]" }, - "#rank/admin": { "prefix": "{Default}[{Lime}A{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[A]" }, - "#rank/trialadmin": { "prefix": "{Default}[{Lime}TA{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[TA]" }, - "#rank/mod": { "prefix": "{Default}[{Lime}M{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[M]" }, - "#rank/trialmod": { "prefix": "{Default}[{Lime}TM{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[TM]" }, - "#rank/helper": { "prefix": "{Default}[{Lime}H{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[H]" }, - "#rank/guardian": { "prefix": "{Default}[{Lime}G{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[G]" }, - "everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" } + "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Owner" }, + "#rank/senioradmin": { "prefix": "{Default}[{Lime}SA{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Sen.Admin" }, + "#rank/admin": { "prefix": "{Default}[{Lime}A{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Admin" }, + "#rank/trialadmin": { "prefix": "{Default}[{Lime}TA{Default}]", "nick_color": "", "message_color": "", "scoreboard": "T.Admin" }, + "#rank/mod": { "prefix": "{Default}[{Lime}M{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Mod" }, + "#rank/trialmod": { "prefix": "{Default}[{Lime}TM{Default}]", "nick_color": "", "message_color": "", "scoreboard": "T.Mod" }, + "#rank/helper": { "prefix": "{Default}[{Lime}H{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Helper" }, + "#rank/guardian": { "prefix": "{Default}[{Lime}G{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Guardian" }, + "everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" } } } """; diff --git a/README.md b/README.md index a2e222b..0077048 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,16 @@ CounterStrikeSharp 1.0.375 (net10.0) and meant to run alongside CS2-SimpleAdmin. `addons/counterstrikesharp/configs/plugins/CS2-Tags/tags.json`, written on first load with a tag for each of our SimpleAdmin rank groups. They use the same abbreviations as SimpleAdmin's staff chat (O, SA, A, TA, M, TM, H, G): `[M] name: message` in chat, with a white `[ ]`, lime rank and -team-coloured name, and `[M]` on the scoreboard. An example with other kinds of key: +team-coloured name. The scoreboard spells the rank out: Owner, Sen.Admin, Admin, T.Admin, Mod, +T.Mod, Helper and Guardian (CS2 adds the brackets itself). An example with other kinds of key: ```jsonc { "tags": { - "76561198000000000": { "prefix": "{Magenta}[Dev]", "nick_color": "", "message_color": "", "scoreboard": "[Dev]" }, - "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[O]" }, - "#rank/mod": { "prefix": "{Default}[{Lime}M{Default}]", "nick_color": "", "message_color": "", "scoreboard": "[M]" }, - "@css/vip": { "prefix": "{Gold}[VIP]", "nick_color": "", "message_color": "", "scoreboard": "[VIP]" }, + "76561198000000000": { "prefix": "{Magenta}[Dev]", "nick_color": "", "message_color": "", "scoreboard": "Dev" }, + "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Owner" }, + "#rank/mod": { "prefix": "{Default}[{Lime}M{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Mod" }, + "@css/vip": { "prefix": "{Gold}[VIP]", "nick_color": "", "message_color": "", "scoreboard": "VIP" }, "everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" } } } @@ -53,7 +54,8 @@ Fields (all optional, empty means "not set"): - `prefix` - shown before the name in chat. - `nick_color` / `message_color` - colour tags; empty means team colour / default. -- `scoreboard` - clan tag on the scoreboard; empty leaves the player's own clan tag. +- `scoreboard` - clan tag on the scoreboard, without brackets (CS2 adds them); empty leaves the + player's own clan tag. Colours: any `ChatColors` field name in braces, case-insensitive, e.g. `{Red}`, `{LightBlue}`, `{Gold}`, `{Grey}`, plus `{TEAMCOLOR}`. @@ -75,4 +77,4 @@ FORGEJO_TOKEN=... ./release.sh v `release.sh` rebuilds from the committed source and publishes `CS2-Tags-.tar.gz`, laid out like `game/csgo/`, to https://git.zio.sh/cs2/CS2-Tags. The tag must equal `v` -(`v1.0.4c-zio2` now). +(`v1.0.4c-zio3` now). From 3ac24ac3feb99fda77477a13df71b85d1b4720e9 Mon Sep 17 00:00:00 2001 From: Astra Date: Mon, 28 Sep 2026 20:29:29 +0100 Subject: [PATCH 2/5] 1.0.4c-zio4: chat labels in tags.json The dead marker and the (T)/(CT)/(SPEC)/(NONE) team-chat labels come from a new optional "chat" section in tags.json, with colour tags; "" drops a label, and missing keys keep the old defaults. --- CS2-Tags.cs | 43 +++++++++++++++++++++++++++++++++++-------- README.md | 16 +++++++++++++++- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/CS2-Tags.cs b/CS2-Tags.cs index b09b7cd..d6fc30d 100644 --- a/CS2-Tags.cs +++ b/CS2-Tags.cs @@ -27,19 +27,31 @@ public class Tag public bool AffectsChat => Prefix != "" || NickColor != "" || MessageColor != ""; } +// Labels CS2-Tags adds to the chat lines it prints, from tags.json's "chat" section. Empty leaves +// the label out. +public class ChatLabels +{ + [JsonPropertyName("dead")] public string Dead { get; set; } = "{White}☠"; + [JsonPropertyName("team_t")] public string TeamT { get; set; } = "{Yellow}(T)"; + [JsonPropertyName("team_ct")] public string TeamCT { get; set; } = "{Blue}(CT)"; + [JsonPropertyName("team_spec")] public string TeamSpec { get; set; } = "(SPEC)"; + [JsonPropertyName("team_none")] public string TeamNone { get; set; } = "(NONE)"; +} + [MinimumApiVersion(375)] public class CS2_Tags : BasePlugin { public override string ModuleName => "CS2-Tags"; public override string ModuleDescription => "Add player tags easily in cs2 game"; public override string ModuleAuthor => "daffyy"; - public override string ModuleVersion => "1.0.4c-zio3"; + public override string ModuleVersion => "1.0.4c-zio4"; private static string TagsPath => Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", "CS2-Tags", "tags.json"); // Checked in file order after the SteamID64 entries, first match wins, so list higher ranks first. private List> _tags = new(); + private ChatLabels _labels = new(); public override void Load(bool hotReload) { @@ -68,6 +80,7 @@ public class CS2_Tags : BasePlugin var root = JsonNode.Parse(File.ReadAllText(TagsPath), documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); + var labels = root?["chat"]?.Deserialize() ?? new ChatLabels(); var tags = new List>(); if (root?["tags"] is JsonObject tagsObject) { @@ -80,6 +93,7 @@ public class CS2_Tags : BasePlugin } _tags = tags; + _labels = labels; Logger.LogInformation("Loaded {Count} tags from {Path}", _tags.Count, TagsPath); } catch (Exception ex) @@ -160,7 +174,7 @@ public class CS2_Tags : BasePlugin var tag = FindTag(player); if (tag == null || !tag.AffectsChat) return HookResult.Continue; - var line = $"{TeamName(player.TeamNum)} {ChatColors.Default}{FormatChat(player, tag, message, player.TeamNum)}"; + var line = $"{Label(TeamLabel(player.TeamNum), player.TeamNum)}{FormatChat(player, tag, message, player.TeamNum)}"; foreach (var p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false } && p.TeamNum == player.TeamNum)) { p.PrintToChat($" {line}"); @@ -171,7 +185,7 @@ public class CS2_Tags : BasePlugin private string FormatChat(CCSPlayerController player, Tag tag, string message, int teamNum) { - string deadIcon = player.TeamNum > (int)CsTeam.Spectator && !player.PawnIsAlive ? $"{ChatColors.White}☠ {ChatColors.Default}" : ""; + string deadIcon = player.TeamNum > (int)CsTeam.Spectator && !player.PawnIsAlive ? Label(_labels.Dead, teamNum) : ""; string prefix = tag.Prefix == "" ? "" : ReplaceTags(tag.Prefix, teamNum).TrimEnd() + " "; string nickColor = tag.NickColor == "" ? TeamColor(teamNum) : ReplaceTags(tag.NickColor, teamNum); string messageColor = tag.MessageColor == "" ? ChatColors.Default.ToString() : ReplaceTags(tag.MessageColor, teamNum); @@ -224,17 +238,23 @@ public class CS2_Tags : BasePlugin Utilities.SetStateChanged(player, "CCSPlayerController", "m_szClan"); } - private static string TeamName(int teamNum) + private string TeamLabel(int teamNum) { return teamNum switch { - (int)CsTeam.Spectator => "(SPEC)", - (int)CsTeam.Terrorist => $"{ChatColors.Yellow}(T)", - (int)CsTeam.CounterTerrorist => $"{ChatColors.Blue}(CT)", - _ => "(NONE)", + (int)CsTeam.Spectator => _labels.TeamSpec, + (int)CsTeam.Terrorist => _labels.TeamT, + (int)CsTeam.CounterTerrorist => _labels.TeamCT, + _ => _labels.TeamNone, }; } + // A label from the "chat" section with its colour tags replaced and a space after it, or nothing. + private static string Label(string label, int teamNum) + { + return label == "" ? "" : $"{ReplaceTags(label, teamNum).TrimEnd()} {ChatColors.Default}"; + } + private static string TeamColor(int teamNum) { return teamNum switch @@ -264,6 +284,13 @@ public class CS2_Tags : BasePlugin // the rank out. No brackets there: CS2 adds its own around the clan tag. private const string DefaultTagsJson = """ { + "chat": { + "dead": "{White}☠", + "team_t": "{Yellow}(T)", + "team_ct": "{Blue}(CT)", + "team_spec": "(SPEC)", + "team_none": "(NONE)" + }, "tags": { "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Owner" }, "#rank/senioradmin": { "prefix": "{Default}[{Lime}SA{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Sen.Admin" }, diff --git a/README.md b/README.md index 0077048..e7c658e 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,20 @@ Fields (all optional, empty means "not set"): - `scoreboard` - clan tag on the scoreboard, without brackets (CS2 adds them); empty leaves the player's own clan tag. +The optional `chat` section sets the labels CS2-Tags adds to the chat lines it prints (only for +players whose tag changes their chat). Leave a key out for its default, or set it to `""` to drop +the label: + +```jsonc +"chat": { + "dead": "{White}☠", // before a dead player's line + "team_t": "{Yellow}(T)", // start of team chat, per team + "team_ct": "{Blue}(CT)", + "team_spec": "(SPEC)", + "team_none": "(NONE)" +} +``` + Colours: any `ChatColors` field name in braces, case-insensitive, e.g. `{Red}`, `{LightBlue}`, `{Gold}`, `{Grey}`, plus `{TEAMCOLOR}`. @@ -77,4 +91,4 @@ FORGEJO_TOKEN=... ./release.sh v `release.sh` rebuilds from the committed source and publishes `CS2-Tags-.tar.gz`, laid out like `game/csgo/`, to https://git.zio.sh/cs2/CS2-Tags. The tag must equal `v` -(`v1.0.4c-zio3` now). +(`v1.0.4c-zio4` now). From b0e67b1ee070cae2dee9f02ebe470d030dc4197d Mon Sep 17 00:00:00 2001 From: Astra Date: Fri, 2 Oct 2026 18:44:36 +0100 Subject: [PATCH 3/5] 1.0.4c-zio6: GroupVerify clan tag for verified players, upper-case rank tags While GroupVerify has a clan tag set (css_gv_clantag or the web panel), players without a rank tag get it on the scoreboard if they're verified, and no tag at all if they aren't. Staff keep their rank tag either way. With no clan tag, or GroupVerify off or missing, nothing changes; clearing it gives players their own tags back. Tags update as soon as verification or the clan tag changes (OnVerifiedChanged, OnClanTagChanged). GroupVerifyApi is a compile-only reference (lib/), optional at runtime. Also (zio5, not released on its own): the scoreboard spells ranks in capitals (OWNER, S.ADMIN, ADMIN, ...) and chat prefixes use SimpleAdmin's staff chat rank names ([Mod], [S.Admin], ...). --- CS2-Tags.cs | 90 ++++++++++++++++++++++++++++++++++------- CS2-Tags.csproj | 6 +++ README.md | 35 ++++++++++++---- VerifyLink.cs | 51 +++++++++++++++++++++++ lib/GroupVerifyApi.dll | Bin 0 -> 9216 bytes 5 files changed, 160 insertions(+), 22 deletions(-) create mode 100644 VerifyLink.cs create mode 100644 lib/GroupVerifyApi.dll diff --git a/CS2-Tags.cs b/CS2-Tags.cs index d6fc30d..4b87067 100644 --- a/CS2-Tags.cs +++ b/CS2-Tags.cs @@ -44,7 +44,7 @@ public class CS2_Tags : BasePlugin public override string ModuleName => "CS2-Tags"; public override string ModuleDescription => "Add player tags easily in cs2 game"; public override string ModuleAuthor => "daffyy"; - public override string ModuleVersion => "1.0.4c-zio4"; + public override string ModuleVersion => "1.0.4c-zio6"; private static string TagsPath => Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", "CS2-Tags", "tags.json"); @@ -53,6 +53,12 @@ public class CS2_Tags : BasePlugin private List> _tags = new(); private ChatLabels _labels = new(); + // zio: GroupVerify, when it's installed (see ScoreboardTag). + private VerifyLink? _verify; + // zio: players whose own clan tag was replaced because they have no rank tag: slot -> their + // SteamID64, their own tag (to put back) and the tag set in its place. + private readonly Dictionary _replaced = new(); + public override void Load(bool hotReload) { LoadTags(); @@ -61,6 +67,7 @@ public class CS2_Tags : BasePlugin RegisterEventHandler(OnPlayerConnectFull); RegisterEventHandler(OnPlayerSpawn); RegisterEventHandler(OnPlayerDeath); + RegisterListener(slot => _replaced.Remove(slot)); AddCommandListener("say", OnPlayerChat); AddCommandListener("say_team", OnPlayerChatTeam); @@ -68,6 +75,30 @@ public class CS2_Tags : BasePlugin ApplyAllClanTags(); } + // zio: GroupVerify's API is only there after every plugin has loaded. + public override void OnAllPluginsLoaded(bool hotReload) + { + try + { + _verify = VerifyLink.TryCreate(player => SetPlayerClanTag(player), ApplyAllClanTags); + } + catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or TypeLoadException) + { + // GroupVerifyApi isn't in shared/: tags work as they always have. + } + Logger.LogInformation(_verify == null + ? "GroupVerify isn't loaded: players without a rank tag keep their own" + : "Using GroupVerify's clan tag for verified players without a rank tag"); + if (_verify != null) + ApplyAllClanTags(); + } + + public override void Unload(bool hotReload) + { + _verify?.Dispose(); + _verify = null; + } + private void LoadTags() { try @@ -229,15 +260,44 @@ public class CS2_Tags : BasePlugin private void SetPlayerClanTag(CCSPlayerController? player) { - if (player == null || !player.IsValid) return; + if (player == null || !player.IsValid || player.IsBot || player.IsHLTV || player.AuthorizedSteamID == null) return; + + var steamId = player.AuthorizedSteamID.SteamId64; + if (_replaced.TryGetValue(player.Slot, out var r) && r.SteamId != steamId) + { + _replaced.Remove(player.Slot); // left over from whoever had the slot before + r = default; + } var tag = FindTag(player); - if (tag == null || tag.Scoreboard == "" || player.Clan == tag.Scoreboard) return; + string? want = tag is { Scoreboard: not "" } ? tag.Scoreboard : ClanTagFor(player); + if (want == null) + { + // Nothing to show for them any more: put their own tag back if it was replaced. + if (!_replaced.Remove(player.Slot)) return; + want = r.Own ?? ""; + } + else if (tag is not { Scoreboard: not "" }) + { + // Replacing their own tag. Keep it, unless what's there is what was set last time. + var own = _replaced.ContainsKey(player.Slot) && player.Clan == r.Set ? r.Own ?? "" : player.Clan ?? ""; + _replaced[player.Slot] = (steamId, own, want); + } - player.Clan = tag.Scoreboard; + if (player.Clan == want) return; + player.Clan = want; Utilities.SetStateChanged(player, "CCSPlayerController", "m_szClan"); } + // zio: for a player with no rank tag, while GroupVerify has a clan tag set: that tag if they're + // verified, none at all if not. null when no clan tag is set, leaving their own tag alone. + private string? ClanTagFor(CCSPlayerController player) + { + var clan = _verify?.ClanTag ?? ""; + if (clan == "") return null; + return _verify!.IsVerified(player) ? clan : ""; + } + private string TeamLabel(int teamNum) { return teamNum switch @@ -280,8 +340,8 @@ public class CS2_Tags : BasePlugin // Written to configs/plugins/CS2-Tags/tags.json on first load. Our CS2-SimpleAdmin rank groups, // highest first; guardian (VIP) comes after staff so a staff member's rank tag wins. The - // abbreviations match CS2-SimpleAdmin's staff chat ("[M] name: message"); the scoreboard spells - // the rank out. No brackets there: CS2 adds its own around the clan tag. + // chat prefixes match CS2-SimpleAdmin's rank tags in staff chat ("[Mod] name: message"). No + // brackets on the scoreboard: CS2 adds its own around the clan tag. private const string DefaultTagsJson = """ { "chat": { @@ -292,15 +352,15 @@ public class CS2_Tags : BasePlugin "team_none": "(NONE)" }, "tags": { - "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Owner" }, - "#rank/senioradmin": { "prefix": "{Default}[{Lime}SA{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Sen.Admin" }, - "#rank/admin": { "prefix": "{Default}[{Lime}A{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Admin" }, - "#rank/trialadmin": { "prefix": "{Default}[{Lime}TA{Default}]", "nick_color": "", "message_color": "", "scoreboard": "T.Admin" }, - "#rank/mod": { "prefix": "{Default}[{Lime}M{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Mod" }, - "#rank/trialmod": { "prefix": "{Default}[{Lime}TM{Default}]", "nick_color": "", "message_color": "", "scoreboard": "T.Mod" }, - "#rank/helper": { "prefix": "{Default}[{Lime}H{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Helper" }, - "#rank/guardian": { "prefix": "{Default}[{Lime}G{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Guardian" }, - "everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" } + "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "OWNER" }, + "#rank/senioradmin": { "prefix": "{Default}[{Lime}S.Admin{Default}]", "nick_color": "", "message_color": "", "scoreboard": "S.ADMIN" }, + "#rank/admin": { "prefix": "{Default}[{Lime}Admin{Default}]", "nick_color": "", "message_color": "", "scoreboard": "ADMIN" }, + "#rank/trialadmin": { "prefix": "{Default}[{Lime}T.Admin{Default}]", "nick_color": "", "message_color": "", "scoreboard": "T.ADMIN" }, + "#rank/mod": { "prefix": "{Default}[{Lime}Mod{Default}]", "nick_color": "", "message_color": "", "scoreboard": "MOD" }, + "#rank/trialmod": { "prefix": "{Default}[{Lime}T.Mod{Default}]", "nick_color": "", "message_color": "", "scoreboard": "T.MOD" }, + "#rank/helper": { "prefix": "{Default}[{Lime}Helper{Default}]", "nick_color": "", "message_color": "", "scoreboard": "HELPER" }, + "#rank/guardian": { "prefix": "{Default}[{Lime}Guardian{Default}]", "nick_color": "", "message_color": "", "scoreboard": "GUARDIAN" }, + "everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" } } } """; diff --git a/CS2-Tags.csproj b/CS2-Tags.csproj index 2dca71c..f162b53 100644 --- a/CS2-Tags.csproj +++ b/CS2-Tags.csproj @@ -15,6 +15,12 @@ runtime compile; build; native; contentfiles; analyzers; buildtransitive + + + lib/GroupVerifyApi.dll + false + diff --git a/README.md b/README.md index e7c658e..e635ed1 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ CounterStrikeSharp 1.0.375 (net10.0) and meant to run alongside CS2-SimpleAdmin. (the game prints it), in both all and team chat. The old `team_chat` key is gone. - `css_tags_reload` works from the server console and for `@css/root` admins, and reapplies scoreboard tags straight away. Hot reload applies tags to players already connected. +- Verification tags from GroupVerify (see below). - Removed `css_tag_mute`/`css_tag_unmute` and the `@` team-chat admin chat. CS2-SimpleAdmin already does both, and its catch-all command listener runs first. CSS always runs those before per-command listeners, so gagged players' messages never reach this plugin. @@ -26,17 +27,18 @@ CounterStrikeSharp 1.0.375 (net10.0) and meant to run alongside CS2-SimpleAdmin. ## Configuration `addons/counterstrikesharp/configs/plugins/CS2-Tags/tags.json`, written on first load with a tag -for each of our SimpleAdmin rank groups. They use the same abbreviations as SimpleAdmin's staff chat -(O, SA, A, TA, M, TM, H, G): `[M] name: message` in chat, with a white `[ ]`, lime rank and -team-coloured name. The scoreboard spells the rank out: Owner, Sen.Admin, Admin, T.Admin, Mod, -T.Mod, Helper and Guardian (CS2 adds the brackets itself). An example with other kinds of key: +for each of our SimpleAdmin rank groups. The chat prefixes are the same rank tags SimpleAdmin uses in +staff chat (O, S.Admin, Admin, T.Admin, Mod, T.Mod, Helper, Guardian): `[Mod] name: message`, +with a white `[ ]`, lime rank and team-coloured name. The scoreboard shows OWNER, S.ADMIN, ADMIN, +T.ADMIN, MOD, T.MOD, HELPER and GUARDIAN (CS2 adds the brackets itself). An example with other +kinds of key: ```jsonc { "tags": { "76561198000000000": { "prefix": "{Magenta}[Dev]", "nick_color": "", "message_color": "", "scoreboard": "Dev" }, - "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Owner" }, - "#rank/mod": { "prefix": "{Default}[{Lime}M{Default}]", "nick_color": "", "message_color": "", "scoreboard": "Mod" }, + "#rank/owner": { "prefix": "{Default}[{Lime}O{Default}]", "nick_color": "", "message_color": "", "scoreboard": "OWNER" }, + "#rank/mod": { "prefix": "{Default}[{Lime}Mod{Default}]", "nick_color": "", "message_color": "", "scoreboard": "MOD" }, "@css/vip": { "prefix": "{Gold}[VIP]", "nick_color": "", "message_color": "", "scoreboard": "VIP" }, "everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" } } @@ -78,6 +80,25 @@ Group membership comes from CS2-SimpleAdmin (`css_addadmin ... -g`, `css_addgrou `css_reloadadmins`). After changing ranks or `tags.json`, run `css_tags_reload` to reapply scoreboard tags without waiting for a respawn. +## Verified players' clan tag (GroupVerify) + +When GroupVerify (`plugins/GroupVerify`) is installed and has a clan tag set (`css_gv_clantag`, +or the web panel's Verification page), players **without a rank tag** get it in place of their own +Steam group tag: + +| | verified | not verified | +|-----------------------------------|----------------|----------------| +| a scoreboard tag from `tags.json` | their rank tag | their rank tag | +| everyone else | the clan tag | no tag at all | + +Staff keep their rank tag either way. With no clan tag set, GroupVerify turned off or not +installed, everyone else keeps their own tag, as before; turning the clan tag off puts back the +tag each player had. Tags update as soon as someone's verification or the clan tag changes. + +GroupVerify's API is a compile-only reference (`lib/GroupVerifyApi.dll`, a copy of +`plugins/GroupVerify`'s build; GroupVerify installs the real one in `shared/GroupVerifyApi`). +CS2-Tags loads without it. + ## Commands - `css_tags_reload` (`@css/root` or server console) - reload `tags.json` and reapply scoreboard tags. @@ -91,4 +112,4 @@ FORGEJO_TOKEN=... ./release.sh v `release.sh` rebuilds from the committed source and publishes `CS2-Tags-.tar.gz`, laid out like `game/csgo/`, to https://git.zio.sh/cs2/CS2-Tags. The tag must equal `v` -(`v1.0.4c-zio4` now). +(`v1.0.4c-zio6` now). diff --git a/VerifyLink.cs b/VerifyLink.cs new file mode 100644 index 0000000..5935ba1 --- /dev/null +++ b/VerifyLink.cs @@ -0,0 +1,51 @@ +using CounterStrikeSharp.API.Core; +using GroupVerifyApi; + +namespace CS2_Tags; + +// zio: GroupVerify's API (shared/GroupVerifyApi). Kept in its own class, and only created inside a +// try in OnAllPluginsLoaded, so CS2-Tags still loads, and tags as before, when GroupVerify isn't +// installed and its assembly can't be found. +internal sealed class VerifyLink : IDisposable +{ + private readonly IGroupVerifyApi _api; + private readonly Action _playerChanged; + private readonly Action _tagChanged; + + private VerifyLink(IGroupVerifyApi api, Action playerChanged, Action tagChanged) + { + _api = api; + _playerChanged = playerChanged; + _tagChanged = tagChanged; + _api.OnVerifiedChanged += OnVerifiedChanged; + _api.OnClanTagChanged += _tagChanged; + } + + // The API, or null when GroupVerify isn't loaded. + public static VerifyLink? TryCreate(Action playerChanged, Action tagChanged) + { + IGroupVerifyApi? api; + try + { + api = IGroupVerifyApi.Capability.Get(); + } + catch (KeyNotFoundException) + { + return null; + } + return api == null ? null : new VerifyLink(api, playerChanged, tagChanged); + } + + // The tag verified players without a rank get, "" while none is set or GroupVerify is off. + public string ClanTag => _api.ClanTag; + + public bool IsVerified(CCSPlayerController player) => _api.IsVerified(player); + + private void OnVerifiedChanged(CCSPlayerController player, VerifyStatus status) => _playerChanged(player); + + public void Dispose() + { + _api.OnVerifiedChanged -= OnVerifiedChanged; + _api.OnClanTagChanged -= _tagChanged; + } +} diff --git a/lib/GroupVerifyApi.dll b/lib/GroupVerifyApi.dll new file mode 100644 index 0000000000000000000000000000000000000000..6f187f96e14906172282e84b0b3e42f95958e836 GIT binary patch literal 9216 zcmeHMeQX@pai8~gZ|`{g5byY*Z|Nw0_;jL9qD0Y_E!*Okq;n*ZA}L#z6N}5cC3)8I z_Pn>G#YnQL2sYv-4I&kA6B}`x8m<$#R@68S92bEdCs8ZFX^RGF;-ZLDv``BqMU3R1 z+BM|<=ItJNA|)sNBS8P?JMQevym>S8=DnG>yF768i=+{ej_ae3h+fB?+inFv9?YS; zX8jvA^sR~?HN7tO|EOtXGFyxl9DBksr($U{pSRuEm=$xT^Ra9`);BO5o3b-jvbs9l z;;SC)C)zJG>U{i#=StSD(uP>2*h;hp9K+*&a2M_|T)S}*)hb+ielvspS8Waud~Oc%tiYm9^dQKIvFF8-naBjpAZm8*r7i-9$ZQq(c9R z2gH?}Xq@GwO<58k;g6g(vv=M5+jmVP-upTxlgIc@}y)Kxm ztK=!>hJtZWo5DtL`;`RbZ#53C{Z8XBeb_^=%I(#|57=Wa>_KFyy~X2{y<3gDPo9Ll z-nv<9YfRYi-$>ML6|^4yV@|Noy=n50ZI_J+&0Yz#b#ql)R9tVs=+(hZ;rJ@>imL&_QHo<9h1+W+u(iPA z)oh{Bv%%Xx!2W3Q8n9U!tIWPiYC#`TY^kqdwuDhKm@qUO6EPASq;{HTlz85onU)j@ z8Lvc_tXCa2HfFf7BPin$Fxz+?SHlM9R}eOe2uwKatxD@=y{&#zz>a~{y4h%pV^W(c z8>==o1w+qPSGXKvIcR&7LjZhMI086t}YWQ&@um-`=vFp1oud@iFEwTu<~kuv91_uHiXw^~6yG z3xU<8ej0PvW#BZ*0Vl>u=BijK1Ptz1426K9Bstm&*(2i!oUs{UZNfdV+{+J1yWtZg zBE>dX4I2(ZRIqOY-ex#=&*RdFgJ-l?80%wpT=lL$?ISi)GpzQ(^UzfRU7FKBKT`7y zW4~N)gbJagJp-EaP`McUb#t-PT`o7?h&7jF%ag^+M9tMNj$7gW65NDwbWi@ z#M9b{xM-9@T)35)&^$v)jBVdiW<*m9X)FG6*5O;xJX>49Kt|5^Q618~W=1y<;bP5uhAk9~P?laiWX3e-_hg9(J8YZ;S!pduSjKrq=?m zNP`{@Fz4STTNL2eP&<7M79Ugm%aSFx(+>3Bt#rNsj)60j;aP=8f;{%ifqlU!WyBBl zFs+lX$S_@o{xCgG52N?jI?Im)8Qu@gHM9~|xJ@X?>-klz#D9u9gp?_PQgT$*1^R*O zsxKPgz5vxz1^TWSb%96W=Q`@3Q{s~XBjhOH`|v@~T_P{K$PGH8iCz_-27E@rcf@Hg z)O4x(Mg2T<0MPi-` zu=R54g20jJNvbF?r?NktIJr>AfzXJ7w zPwf}yX%)S$s7v%7BEO3C7IyRu{f+V8pe`utnb3Gp%GLCSPaOrdmVTh9OLS1aji(sB zmB(G83r1M3r!|VYCN3DuWQ-nF)Km0<(I9#81bs@uKhYUq3a?;ymt;IiDVgIz?v?aQ zR0BavC6{XRIOo);9_Z2Nc{O&aQY%zCrbVBOt*)2Pmlr4zV^3`5K@w z>fANduLz)ltiFbNu@SHqdC736g53(H6g;Hhg9@5}g0g@OGz+*2Yrr_e7=0bkpm!B~ zTEWZooIHg5&&i{7nZBtXqKGJ9J-FH=|W=hPvd2>u%?%qdjE7WzKDjOunuzK1%<{tLm9LWE^NFsu?! zt39)eV!%_yjl%o9HZ6YpqnIC&g{_5N*>EbdpwRNt^|D$|TiM zFW_n#18k<_fSc%L!0q%V;9c|%U@yH3_zA@y2JD8msMbqqY6d(`8?`>A^FGBl6>ciK zX(f|Z*3(Kdqk11va8AjbQKQZ%{H&7y1I2lPJ_pa=QvB~JcvZoFQIJF$K9Jzq2?b+{ z6H~Y@d5$d#lEyMI1xGa=HLCDAh0iJcs)D2|NdLk7W3~(E<37XptdklE{%Dpr<_qy*fA*fSf~YX9c8ckvA->LpFr{ z5CUfnpj6p~ti(^^+^Ys$Maux!Bi{tY03|hH9|_t3D5;qmfVTijYDML2qb=CkKc)Xd zKE%bnLZ9ROE^mCMhRYbk;`8v|lR7akijxNIM!DBGjEi_bL~SGF{{*g+*t@@A^5AqX zXO88p(GE)WKRRu?KEZe{>(2JtQw7tp9Q5zc7G2y1bJG*qe6LwB$FeK|R(IOX+WFCK zG-0{NhF!~?N@cn_sW^}AI$&p}bJjgdpnKY#w4Lmyt&AtyN4@rR-nATl4m@rRPnu34 z**%z2!l~lE{Ui6&@GO)~C422$&QivT$-P$Iasjm#F9bF*!NP2oU` zm9r+GiTX2H*LHdw`>`T#9JOb_$aK5dYh%;s>a)hCCni{Qfmce31w}lnhzp!{)f<&JH2tiVJ)%210{sR zyp?xR&8%Gc4u@belgs&n)MMK@%gpEcUIi+pXIR{&va@M#@PIh9_McwW{ zfpU$!OXah!@~qDqH>YzRo+|d(E*s$Dl~yUl$6_ghBm7mo+i}cUvJ1ybXDH*Tyyc&$ zG#{c_>YpmOvwRHW;9oz1=OUY?@q|1>7Vs>MzGtlNsY-EYWef@Mo*>xaSEJ?0r$pt^M=*yJ) zsOzn}epcajcE9r0doAP9eS@UOga~Od5&v{A3a;z%gnC ztzCz&6i z604j$gQsg5sgKmN?lo*CQeOiNwGuy1P>bVJTAXV15L^qzeN`cW2W)QZ_-oUMY<$2m z3kU7|eAz^)$N4TGCzag2f>tIE_K)zR03Kx8{fdA0OvlzF{-ZHcQ$K$c_hpM*6%XQM zuHYdt7O4|dQ})2Zb)TvP1#wVXsH+~)flrzE5p5TgRK%u^t=5ilYv=g(wAHoEynU>* z`>zt48J;9qoGZk+uVoO)aQ$#2P9F2`t*NV1R_qv3ts+jex3uQgZjwZilCAAjSU zPoDkc_~#qUHwu3X?@H}I7>|{x8}Ft6=6T#>Sk1m%?tq!i(^N5yrM1){>-B!r3N4GK zC=>(#OPRY-uNPme+!8*^?Zb6sH_;%!fckAv>jZ}P4mt+dPeZ^`sF4RjrvUMlR!M$W z|J8MGCA<~%aPJ#43n1nEEvm#7=mQkcYNAr_K_$+iigM*1NA>5{qDGCN*95nSey;R* z5&`A4i~2=u3IU&RY@r8_5jWAq?}~!9&i^`T3^>s}cuN&{|FdZd6`pO(Vn!w?Y;?4R z6J(*%&r-K{3ZZ|G8ej6fPn{2G<##QjxyUT&6_DN zV!x|hFYbfTXTzIme9>{sy#MWS{|vqt2)c3n;;1deY!h`LdKT@#zOwM8ZyaB)(!jcC z8$7=qd!Z8&cL3{DyiQ1rV?-BzJ0WXAdK>)g1Z84eM#b(9wPyUCs|el{wB!)E(lf!r e>Mi)e>-T$XP|p8n`mn6R{2s0Szv};E5%?brAtk#2 literal 0 HcmV?d00001 From 851397c7d90763ba9f5b9da3f51fc7f70945a5f8 Mon Sep 17 00:00:00 2001 From: Astra Date: Fri, 2 Oct 2026 18:47:40 +0100 Subject: [PATCH 4/5] 1.0.4c-zio7: don't reapply tags in OnAllPluginsLoaded at server start CSS runs OnAllPluginsLoaded before the engine's globals exist, so Utilities.GetPlayers() threw ("Global Variables not initialized yet"). Nobody is connected then anyway; tags are only reapplied there after a hot reload. --- CS2-Tags.cs | 6 ++++-- README.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CS2-Tags.cs b/CS2-Tags.cs index 4b87067..af56b24 100644 --- a/CS2-Tags.cs +++ b/CS2-Tags.cs @@ -44,7 +44,7 @@ public class CS2_Tags : BasePlugin public override string ModuleName => "CS2-Tags"; public override string ModuleDescription => "Add player tags easily in cs2 game"; public override string ModuleAuthor => "daffyy"; - public override string ModuleVersion => "1.0.4c-zio6"; + public override string ModuleVersion => "1.0.4c-zio7"; private static string TagsPath => Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", "CS2-Tags", "tags.json"); @@ -89,7 +89,9 @@ public class CS2_Tags : BasePlugin Logger.LogInformation(_verify == null ? "GroupVerify isn't loaded: players without a rank tag keep their own" : "Using GroupVerify's clan tag for verified players without a rank tag"); - if (_verify != null) + // Only after a hot reload: at server start nobody is connected, and the engine's globals + // (which GetPlayers needs) don't exist yet. + if (_verify != null && hotReload) ApplyAllClanTags(); } diff --git a/README.md b/README.md index e635ed1..79e6cad 100644 --- a/README.md +++ b/README.md @@ -112,4 +112,4 @@ FORGEJO_TOKEN=... ./release.sh v `release.sh` rebuilds from the committed source and publishes `CS2-Tags-.tar.gz`, laid out like `game/csgo/`, to https://git.zio.sh/cs2/CS2-Tags. The tag must equal `v` -(`v1.0.4c-zio6` now). +(`v1.0.4c-zio7` now). From 6ffdfbaf9aa6d6aa24894f3b8fc7f1a338aafe3e Mon Sep 17 00:00:00 2001 From: Astra Date: Sun, 4 Oct 2026 18:34:11 +0100 Subject: [PATCH 5/5] 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 06e9b35..c3f479f 100755 --- a/release.sh +++ b/release.sh @@ -19,8 +19,13 @@ set -euo pipefail FORGEJO_URL="https://git.zio.sh" OWNER="cs2" REPO="CS2-Tags" -# 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}" # Only these ship from the publish output. CounterStrikeSharp.API.dll is excluded by the csproj @@ -37,7 +42,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