using System.Reflection; using System.Text.Json; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Utils; namespace Whisper; // Reads CS2-Tags' tags.json so private messages show the same rank prefix and colours as chat, and // picks a player's tag the way CS2-Tags does: their SteamID64 first, then the first "#group" or // "@permission" key they match in file order, then "everyone". Reread when the file changes. public sealed class Tags(string path, Action warn) { public sealed record Tag(string Prefix, string NickColor, string MessageColor); private List> _tags = []; private DateTime _loadedWrite; public Tag? Find(CCSPlayerController player) { Refresh(); if (player.IsBot || player.IsHLTV || player.AuthorizedSteamID == null) return null; var steamid = player.AuthorizedSteamID.SteamId64.ToString(); foreach (var (key, tag) in _tags) if (key == steamid) return tag; foreach (var (key, tag) in _tags) { if (key.StartsWith('#') && AdminManager.PlayerInGroup(player, key)) return tag; if (key.StartsWith('@') && AdminManager.PlayerHasPermissions(player, key)) return tag; } foreach (var (key, tag) in _tags) if (key == "everyone") return tag; return null; } private void Refresh() { DateTime written; try { written = File.GetLastWriteTimeUtc(path); } catch (Exception) { return; } // GetLastWriteTimeUtc gives 1601 for a missing file. if (written == _loadedWrite || written.Year < 1700) { if (written.Year < 1700 && _tags.Count > 0) _tags = []; _loadedWrite = written; return; } _loadedWrite = written; try { using var doc = JsonDocument.Parse(File.ReadAllText(path), new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); var tags = new List>(); if (doc.RootElement.TryGetProperty("tags", out var section)) foreach (var entry in section.EnumerateObject()) tags.Add(new(entry.Name, new Tag(Str(entry.Value, "prefix"), Str(entry.Value, "nick_color"), Str(entry.Value, "message_color")))); _tags = tags; } catch (Exception ex) { // Keep the tags from before, as CS2-Tags does with a broken file. warn($"Couldn't read {path}: {ex.Message}; keeping the previous tags"); } } private static string Str(JsonElement e, string name) => e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : ""; public static string TeamColor(int team) => team switch { (int)CsTeam.Terrorist => ChatColors.Gold.ToString(), (int)CsTeam.CounterTerrorist => ChatColors.Blue.ToString(), _ => "", }; // Colour tags in config strings only, never in names or messages: {Red}, {Default}, {TEAMCOLOR}. public static string Colors(string text, int team = 0) { if (!text.Contains('{')) return text; foreach (var field in typeof(ChatColors).GetFields(BindingFlags.Public | BindingFlags.Static)) if (field.FieldType == typeof(char)) text = text.Replace($"{{{field.Name}}}", field.GetValue(null)!.ToString(), StringComparison.OrdinalIgnoreCase); return text.Replace("{TEAMCOLOR}", TeamColor(team), StringComparison.OrdinalIgnoreCase); } }