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 0000000..6f187f9 Binary files /dev/null and b/lib/GroupVerifyApi.dll differ