Compare commits

..

No commits in common. "main" and "v1.0.4c-zio2" have entirely different histories.

6 changed files with 33 additions and 222 deletions

View file

@ -27,37 +27,19 @@ 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-zio7";
public override string ModuleVersion => "1.0.4c-zio2";
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<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)
{
@ -67,7 +49,6 @@ 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);
@ -75,32 +56,6 @@ 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
@ -113,7 +68,6 @@ 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<ChatLabels>() ?? new ChatLabels();
var tags = new List<KeyValuePair<string, Tag>>();
if (root?["tags"] is JsonObject tagsObject)
{
@ -126,7 +80,6 @@ public class CS2_Tags : BasePlugin
}
_tags = tags;
_labels = labels;
Logger.LogInformation("Loaded {Count} tags from {Path}", _tags.Count, TagsPath);
}
catch (Exception ex)
@ -207,7 +160,7 @@ public class CS2_Tags : BasePlugin
var tag = FindTag(player);
if (tag == null || !tag.AffectsChat) return HookResult.Continue;
var line = $"{Label(TeamLabel(player.TeamNum), player.TeamNum)}{FormatChat(player, tag, message, player.TeamNum)}";
var line = $"{TeamName(player.TeamNum)} {ChatColors.Default}{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}");
@ -218,7 +171,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 ? Label(_labels.Dead, teamNum) : "";
string deadIcon = player.TeamNum > (int)CsTeam.Spectator && !player.PawnIsAlive ? $"{ChatColors.White}☠ {ChatColors.Default}" : "";
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);
@ -262,61 +215,26 @@ public class CS2_Tags : BasePlugin
private void SetPlayerClanTag(CCSPlayerController? player)
{
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;
}
if (player == null || !player.IsValid) return;
var tag = FindTag(player);
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);
}
if (tag == null || tag.Scoreboard == "" || player.Clan == tag.Scoreboard) return;
if (player.Clan == want) return;
player.Clan = want;
player.Clan = tag.Scoreboard;
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)
private static string TeamName(int teamNum)
{
return teamNum switch
{
(int)CsTeam.Spectator => _labels.TeamSpec,
(int)CsTeam.Terrorist => _labels.TeamT,
(int)CsTeam.CounterTerrorist => _labels.TeamCT,
_ => _labels.TeamNone,
(int)CsTeam.Spectator => "(SPEC)",
(int)CsTeam.Terrorist => $"{ChatColors.Yellow}(T)",
(int)CsTeam.CounterTerrorist => $"{ChatColors.Blue}(CT)",
_ => "(NONE)",
};
}
// 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
@ -342,27 +260,19 @@ 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
// 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.
// abbreviations match CS2-SimpleAdmin's staff chat ("[M] name: message").
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}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": "" }
"#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": "" }
}
}
""";

View file

@ -15,12 +15,6 @@
<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,7 +19,6 @@ 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.
@ -27,19 +26,17 @@ 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. 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:
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:
```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}Mod{Default}]", "nick_color": "", "message_color": "", "scoreboard": "MOD" },
"@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": "[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]" },
"everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" }
}
}
@ -56,22 +53,7 @@ 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, 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)"
}
```
- `scoreboard` - clan tag on the scoreboard; 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}`.
@ -80,25 +62,6 @@ 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.
@ -112,4 +75,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-zio7` now).
(`v1.0.4c-zio2` now).

View file

@ -1,51 +0,0 @@
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;
}
}

Binary file not shown.

View file

@ -19,13 +19,8 @@ set -euo pipefail
FORGEJO_URL="https://git.zio.sh"
OWNER="cs2"
REPO="CS2-Tags"
# 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
# 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.
FORGEJO_TOKEN="${FORGEJO_TOKEN:-CHANGE_ME_FORGEJO_TOKEN}"
# Only these ship from the publish output. CounterStrikeSharp.API.dll is excluded by the csproj
@ -42,7 +37,7 @@ if [[ -z "$TAG" ]]; then
exit 1
fi
if [[ "$FORGEJO_TOKEN" == "CHANGE_ME_FORGEJO_TOKEN" ]]; then
echo "Set FORGEJO_TOKEN (export it, or put it in $TOKEN_FILE) first." >&2
echo "Set FORGEJO_TOKEN (edit release.sh or export it) first." >&2
exit 1
fi
for tool in podman jq curl git; do