Compare commits

..

3 commits

Author SHA1 Message Date
6ffdfbaf9a release.sh: read the Forgejo token from ../../.forgejo-token when FORGEJO_TOKEN isn't set 2026-10-04 18:34:11 +01:00
851397c7d9 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.
2026-10-02 18:47:40 +01:00
b0e67b1ee0 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], ...).
2026-10-02 18:44:36 +01:00
6 changed files with 170 additions and 25 deletions

View file

@ -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-zio7";
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<KeyValuePair<string, Tag>> _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<int, (ulong SteamId, string Own, string Set)> _replaced = new();
public override void Load(bool hotReload)
{
LoadTags();
@ -61,6 +67,7 @@ public class CS2_Tags : BasePlugin
RegisterEventHandler<EventPlayerConnectFull>(OnPlayerConnectFull);
RegisterEventHandler<EventPlayerSpawn>(OnPlayerSpawn);
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
RegisterListener<Listeners.OnClientDisconnect>(slot => _replaced.Remove(slot));
AddCommandListener("say", OnPlayerChat);
AddCommandListener("say_team", OnPlayerChatTeam);
@ -68,6 +75,32 @@ 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");
// 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();
}
public override void Unload(bool hotReload)
{
_verify?.Dispose();
_verify = null;
}
private void LoadTags()
{
try
@ -229,15 +262,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 +342,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 +354,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": "" }
}
}
""";

View file

@ -15,6 +15,12 @@
<ExcludeAssets>runtime</ExcludeAssets>
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Compile-time only: GroupVerify installs it in shared/GroupVerifyApi, copied from
plugins/GroupVerify's build. Optional at runtime: without it, tags work as before. -->
<Reference Include="GroupVerifyApi">
<HintPath>lib/GroupVerifyApi.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>

View file

@ -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<ModuleVersion>
`release.sh` rebuilds from the committed source and publishes `CS2-Tags-<tag>.tar.gz`, laid out
like `game/csgo/`, to https://git.zio.sh/cs2/CS2-Tags. The tag must equal `v<ModuleVersion>`
(`v1.0.4c-zio4` now).
(`v1.0.4c-zio7` now).

51
VerifyLink.cs Normal file
View file

@ -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<CCSPlayerController> _playerChanged;
private readonly Action _tagChanged;
private VerifyLink(IGroupVerifyApi api, Action<CCSPlayerController> 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<CCSPlayerController> 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;
}
}

BIN
lib/GroupVerifyApi.dll Normal file

Binary file not shown.

View file

@ -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