diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 232867c..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -ko_fi: daffyy diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..54a7a16 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,67 @@ +name: Build + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + BUILD_NUMBER: ${{ github.run_number }} + PROJECT_PATH: "CS2-Tags.csproj" + PROJECT_NAME: "CS2-Tags" + OUTPUT_PATH: "./CS2-Tags" + +jobs: + build: + permissions: write-all + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Restore + run: dotnet restore + - name: Build + run: dotnet build ${{ env.PROJECT_PATH }} -c WeaponPaints -o ${{ env.OUTPUT_PATH }} + + publish: + if: github.event_name == 'push' + permissions: write-all + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Restore + run: dotnet restore + - name: Build + run: dotnet build ${{ env.PROJECT_PATH }} -c WeaponPaints -o ${{ env.OUTPUT_PATH }} + - name: Clean files + run: | + rm -f \ + ${{ env.OUTPUT_PATH }}/CounterStrikeSharp.API.dll \ + ${{ env.OUTPUT_PATH }}/McMaster.NETCore.Plugins.dll \ + ${{ env.OUTPUT_PATH }}/Microsoft.DotNet.PlatformAbstractions.dll \ + ${{ env.OUTPUT_PATH }}/Microsoft.Extensions.DependencyModel.dll \ + - name: Zip + uses: thedoctor0/zip-release@0.7.5 + with: + type: 'zip' + filename: '${{ env.PROJECT_NAME }}.zip' + path: ${{ env.OUTPUT_PATH }} + - name: CS2-Tags + uses: ncipollo/release-action@v1.12.0 + with: + artifacts: "${{ env.PROJECT_NAME }}.zip" + name: "Build ${{ env.BUILD_NUMBER }}" + tag: "build-${{ env.BUILD_NUMBER }}" + body: | + Place the plugin in game/csgo/addons/counterstrikesharp/plugins/CS2-Tags + After first server start, tags.json be created diff --git a/.gitignore b/.gitignore index 4c1a8ba..b639bb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ +.git .vs -bin/ obj/ -release-out/ +bin/ \ No newline at end of file diff --git a/CS2-Tags.cs b/CS2-Tags.cs index 5de35e1..c4cda10 100644 --- a/CS2-Tags.cs +++ b/CS2-Tags.cs @@ -1,278 +1,533 @@ -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using CounterStrikeSharp.API; +using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Entities; -using CounterStrikeSharp.API.Modules.Timers; using CounterStrikeSharp.API.Modules.Utils; -using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using System.Reflection; namespace CS2_Tags; -public class Tag -{ - [JsonPropertyName("prefix")] public string Prefix { get; set; } = ""; - [JsonPropertyName("nick_color")] public string NickColor { get; set; } = ""; - [JsonPropertyName("message_color")] public string MessageColor { get; set; } = ""; - [JsonPropertyName("scoreboard")] public string Scoreboard { get; set; } = ""; - - // A tag with nothing to show in chat leaves the player's messages to the game. - [JsonIgnore] - public bool AffectsChat => Prefix != "" || NickColor != "" || MessageColor != ""; -} - -[MinimumApiVersion(375)] +[MinimumApiVersion(98)] public class CS2_Tags : BasePlugin { + List GaggedIds = new List(); + public static JObject? JsonTags { get; private set; } 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-zio1"; - - 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(); + public override string ModuleVersion => "1.0.3a"; public override void Load(bool hotReload) { - LoadTags(); + CreateOrLoadJsonFile(ModuleDirectory + "/tags.json"); RegisterListener(OnClientAuthorized); + //RegisterListener(OnMapStart); RegisterEventHandler(OnPlayerConnectFull); + //RegisterListener(OnClientPutInServer); RegisterEventHandler(OnPlayerSpawn); RegisterEventHandler(OnPlayerDeath); AddCommandListener("say", OnPlayerChat); AddCommandListener("say_team", OnPlayerChatTeam); if (hotReload) - ApplyAllClanTags(); + { + OnMapStart(string.Empty); + } } - private void LoadTags() + private void OnMapStart(string mapname) { - try - { - if (!File.Exists(TagsPath)) - { - Directory.CreateDirectory(Path.GetDirectoryName(TagsPath)!); - File.WriteAllText(TagsPath, DefaultTagsJson); - } + //AddTimer(30.0f, () => CS2_SimpleAdmin(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.REPEAT | CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + } - var root = JsonNode.Parse(File.ReadAllText(TagsPath), - documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); - var tags = new List>(); - if (root?["tags"] is JsonObject tagsObject) + /* + private async void CS2_SimpleAdmin() + { + string? path = Path.GetDirectoryName(ModuleDirectory); + if (Directory.Exists(path + "/CS2-SimpleAdmin")) + { + string _configPath = Path.Combine(path, "../configs/plugins/CS2-SimpleAdmin/CS2-SimpleAdmin.json"); + if (!File.Exists(_configPath)) { - foreach (var (key, value) in tagsObject) + return; + } + GaggedSteamids.Clear(); + try + { + string configJson = await File.ReadAllTextAsync(_configPath); + + dynamic config = JObject.Parse(configJson); + + string connectionString = $"Server={config.DatabaseHost};Database={config.DatabaseName};Uid={config.DatabaseUser};Pwd={config.DatabasePassword};"; + + using var connection = new MySqlConnection(connectionString); + await connection.OpenAsync(); + + string query = "SELECT DISTINCT player_steamid FROM sa_mutes WHERE status = 'ACTIVE' AND type = 'GAG' AND (duration = 0 OR ends > NOW())"; + + using var cmd = new MySqlCommand(query, connection); + using MySqlDataReader reader = await cmd.ExecuteReaderAsync(); + + while (await reader.ReadAsync()) { - var tag = value?.Deserialize(); - if (tag != null) - tags.Add(new(key, tag)); + string steamID = reader.GetString("player_steamid"); + GaggedSteamids.Add(steamID); } } - - _tags = tags; - Logger.LogInformation("Loaded {Count} tags from {Path}", _tags.Count, TagsPath); + catch (Exception) + { + } } - catch (Exception ex) + } + */ + + private static void CreateOrLoadJsonFile(string filepath) + { + if (!File.Exists(filepath)) { - // Keep the previous tags, so a typo in the file doesn't strip everyone's tags. - Logger.LogError(ex, "Failed to load {Path}", TagsPath); + var templateData = new JObject + { + ["tags"] = new JObject + { + ["#css/admin"] = new JObject + { + ["prefix"] = "{RED}[ADMIN]", + ["nick_color"] = "{RED}", + ["message_color"] = "{GOLD}", + ["scoreboard"] = "[ADMIN]" + }, + ["@css/chat"] = new JObject + { + ["prefix"] = "{GREEN}[CHAT]", + ["nick_color"] = "{RED}", + ["message_color"] = "{GOLD}", + ["scoreboard"] = "[CHAT]" + }, + ["76561197961430531"] = new JObject + { + ["prefix"] = "{RED}[ADMIN]", + ["nick_color"] = "{RED}", + ["message_color"] = "{GOLD}", + ["scoreboard"] = "[ADMIN]" + }, + ["everyone"] = new JObject + { + ["team_chat"] = false, + ["prefix"] = "{Grey}[Player]", + ["nick_color"] = "", + ["message_color"] = "", + ["scoreboard"] = "[Player]" + }, + } + }; + + File.WriteAllText(filepath, templateData.ToString()); + var jsonData = File.ReadAllText(filepath); + JsonTags = JObject.Parse(jsonData); + } + else + { + var jsonData = File.ReadAllText(filepath); + JsonTags = JObject.Parse(jsonData); } } - [ConsoleCommand("css_tags_reload", "Reload tags.json and reapply scoreboard tags")] - [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] - [RequiresPermissions("@css/root")] + [ConsoleCommand("css_tags_reload")] public void OnReloadConfig(CCSPlayerController? player, CommandInfo info) { - LoadTags(); - ApplyAllClanTags(); - info.ReplyToCommand($"[CS2-Tags] Reloaded {_tags.Count} tags."); + if (player != null) return; + CreateOrLoadJsonFile(ModuleDirectory + "/tags.json"); + + Server.PrintToConsole("[CS2-Tags] Config reloaded!"); + } + + [ConsoleCommand("css_tag_mute")] + [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.SERVER_ONLY)] + public void OnTagMuteCommand(CCSPlayerController? caller, CommandInfo command) + { + int index = 0; + int.TryParse(command.GetArg(1), out index); + + if (index == 0) + return; + + if (!GaggedIds.Contains(index)) + GaggedIds.Add(index); + } + + [ConsoleCommand("css_tag_unmute")] + [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.SERVER_ONLY)] + public void OnTagUnMuteCommand(CCSPlayerController? caller, CommandInfo command) + { + int index = 0; + int.TryParse(command.GetArg(1), out index); + + if (index == 0) + return; + + if (GaggedIds.Contains(index)) + GaggedIds.Remove(index); } private void OnClientAuthorized(int playerSlot, SteamID steamId) { - var player = Utilities.GetPlayerFromSlot(playerSlot); - if (player == null || !player.IsValid || player.IsBot || player.IsHLTV) return; + CCSPlayerController? player = Utilities.GetPlayerFromSlot(playerSlot); - AddTimer(2.0f, () => SetPlayerClanTag(player), TimerFlags.STOP_ON_MAPCHANGE); + if (player == null || !player.IsValid || player.IsBot) return; + + AddTimer(2.5f, () => SetPlayerClanTag(player)); } private HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info) { - var player = @event.Userid; - if (player == null || !player.IsValid || player.IsBot || player.IsHLTV) return HookResult.Continue; + CCSPlayerController? player = @event.Userid; + + if (player == null || !player.IsValid || player.IsBot) return HookResult.Continue; + + AddTimer(4.0f, () => SetPlayerClanTag(player)); - AddTimer(2.0f, () => SetPlayerClanTag(player), TimerFlags.STOP_ON_MAPCHANGE); return HookResult.Continue; } private HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) { - var player = @event.Userid; + CCSPlayerController? player = @event.Userid; if (player == null || !player.IsValid || player.IsBot) return HookResult.Continue; - AddTimer(1.5f, () => SetPlayerClanTag(player), TimerFlags.STOP_ON_MAPCHANGE); + AddTimer(1.0f, () => SetPlayerClanTag(player)); + return HookResult.Continue; } private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info) { - var player = @event.Userid; + CCSPlayerController? player = @event.Userid; if (player == null || !player.IsValid || player.IsBot) return HookResult.Continue; - AddTimer(1.5f, () => SetPlayerClanTag(player), TimerFlags.STOP_ON_MAPCHANGE); + AddTimer(1.0f, () => SetPlayerClanTag(player)); + return HookResult.Continue; } - // Gags and "@" admin chat are CS2-SimpleAdmin's job: its catch-all command listener runs before - // these and stops the command, so a gagged player's message never reaches here. - private static bool IsCommandOrTrigger(string message) => - message.StartsWith('!') || message.StartsWith('@') || message.StartsWith('/') || message.StartsWith('.') || message == "rtv"; - private HookResult OnPlayerChat(CCSPlayerController? player, CommandInfo info) { - var message = info.GetArg(1); - if (player == null || !player.IsValid || message.Length == 0 || IsCommandOrTrigger(message)) return HookResult.Continue; + if (player == null || !player.IsValid || info.GetArg(1).Length == 0) return HookResult.Continue; + string steamid = player.AuthorizedSteamID!.SteamId64.ToString(); - var tag = FindTag(player); - if (tag == null || !tag.AffectsChat) return HookResult.Continue; + if (GaggedIds.Contains((int)player.Index)) return HookResult.Handled; - var line = FormatChat(player, tag, message, player.TeamNum); - Server.PrintToChatAll($" {line}"); - return HookResult.Handled; + if (info.GetArg(1).StartsWith("!") || info.GetArg(1).StartsWith("@") || info.GetArg(1).StartsWith("/") || info.GetArg(1).StartsWith(".") || info.GetArg(1) == "rtv") return HookResult.Continue; + + if (JsonTags != null && JsonTags.TryGetValue("tags", out var tags) && tags is JObject tagsObject) + { + string deadIcon = !player.PawnIsAlive ? $"{ChatColors.White}☠ {ChatColors.Default}" : ""; + + if (tagsObject.TryGetValue(steamid, out var playerTag) && playerTag is JObject) + { + string prefix = playerTag["prefix"]?.ToString() ?? ""; + string nickColor = playerTag?["nick_color"]?.ToString() ?? ChatColors.Default.ToString(); + string messageColor = playerTag?["message_color"]?.ToString() ?? ChatColors.Default.ToString(); + + Server.PrintToChatAll(ReplaceTags($" {deadIcon}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}", player.TeamNum)); + + return HookResult.Handled; + } + + foreach (var tagKey in tagsObject.Properties()) + { + if (tagKey.Name.StartsWith("#")) + { + string group = tagKey.Name; + bool inGroup = AdminManager.PlayerInGroup(player, group); + + if (inGroup) + { + if (tagsObject.TryGetValue(group, out var groupTag) && groupTag is JObject) + { + string prefix = groupTag["prefix"]?.ToString() ?? ""; + string nickColor = groupTag?["nick_color"]?.ToString() ?? ChatColors.Default.ToString(); + string messageColor = groupTag?["message_color"]?.ToString() ?? ChatColors.Default.ToString(); + + Server.PrintToChatAll(ReplaceTags($" {deadIcon}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}", player.TeamNum)); + + return HookResult.Handled; + } + } + } + + if (tagKey.Name.StartsWith("@")) + { + string permission = tagKey.Name; + bool hasPermission = AdminManager.PlayerHasPermissions(player, permission); + + if (hasPermission) + { + if (tagsObject.TryGetValue(permission, out var permissionTag) && permissionTag is JObject) + { + string prefix = permissionTag["prefix"]?.ToString() ?? ""; + string nickColor = permissionTag?["nick_color"]?.ToString() ?? ChatColors.Default.ToString(); + string messageColor = permissionTag?["message_color"]?.ToString() ?? ChatColors.Default.ToString(); + + Server.PrintToChatAll(ReplaceTags($" {deadIcon}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}", player.TeamNum)); + + return HookResult.Handled; + } + } + } + } + + if (tagsObject.TryGetValue("everyone", out var everyoneTag) && everyoneTag is JObject && everyoneTag?["team_chat"]?.Value() == true) + { + string prefix = everyoneTag["prefix"]?.ToString() ?? ""; + string nickColor = everyoneTag?["nick_color"]?.ToString() ?? ChatColors.Default.ToString(); + string messageColor = everyoneTag?["message_color"]?.ToString() ?? ChatColors.Default.ToString(); + + Server.PrintToChatAll(ReplaceTags($" {deadIcon}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}", player.TeamNum)); + + return HookResult.Handled; + } + } + + return HookResult.Continue; } private HookResult OnPlayerChatTeam(CCSPlayerController? player, CommandInfo info) { - var message = info.GetArg(1); - if (player == null || !player.IsValid || message.Length == 0 || IsCommandOrTrigger(message)) return HookResult.Continue; + if (player == null || !player.IsValid || info.GetArg(1).Length == 0) return HookResult.Continue; + string steamid = player.AuthorizedSteamID!.SteamId64.ToString(); - var tag = FindTag(player); - if (tag == null || !tag.AffectsChat) return HookResult.Continue; + if (GaggedIds.Contains((int)player.Index)) return HookResult.Handled; - 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)) + if (info.GetArg(1).StartsWith("@") && AdminManager.PlayerHasPermissions(player, "@css/chat")) { - p.PrintToChat($" {line}"); + foreach (var p in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsBot && !p.IsHLTV && AdminManager.PlayerHasPermissions(p, "@css/chat"))) + { + p.PrintToChat($" {ChatColors.Lime}(ADMIN) {ChatColors.Default}{player.PlayerName}: {info.GetArg(1).Remove(0, 1)}"); + } + + return HookResult.Handled; } - return HookResult.Handled; - } + if (info.GetArg(1).StartsWith("!") || info.GetArg(1).StartsWith("@") || info.GetArg(1).StartsWith("/") || info.GetArg(1).StartsWith(".") || info.GetArg(1) == "rtv") return HookResult.Continue; - 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 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); - - // Colour tags are only replaced in the config's strings, never in the player's name or message. - return $"{deadIcon}{prefix}{ChatColors.Default}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{message}"; - } - - // SteamID64 entries win, then groups ("#...") and permissions ("@...") in file order, then "everyone". - private Tag? FindTag(CCSPlayerController player) - { - if (player.IsBot || player.IsHLTV || player.AuthorizedSteamID == null) return null; - - string steamid = player.AuthorizedSteamID.SteamId64.ToString(); - foreach (var (key, tag) in _tags) + if (JsonTags != null && JsonTags.TryGetValue("tags", out var tags) && tags is JObject tagsObject) { - if (key == steamid) return tag; - } + string deadIcon = !player.PawnIsAlive ? $"{ChatColors.White}☠ {ChatColors.Default}" : ""; + if (tagsObject.TryGetValue(steamid, out var playerTag) && playerTag is JObject) + { + string prefix = playerTag["prefix"]?.ToString() ?? ""; + string nickColor = playerTag?["nick_color"]?.ToString() ?? ChatColors.Default.ToString(); + string messageColor = playerTag?["message_color"]?.ToString() ?? ChatColors.Default.ToString(); - 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 p in Utilities.GetPlayers().Where(p => p.TeamNum == player.TeamNum && p.IsValid && !p.IsBot)) + { + string messageToSend = $"{deadIcon}{TeamName(player.TeamNum)} {ChatColors.Default}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}"; + p.PrintToChat($" {ReplaceTags(messageToSend, p.TeamNum)}"); + } - foreach (var (key, tag) in _tags) - { - if (key == "everyone") return tag; - } + return HookResult.Handled; + } - return null; - } + foreach (var tagKey in tagsObject.Properties()) + { + if (tagKey.Name.StartsWith("#")) + { + string group = tagKey.Name; + bool inGroup = AdminManager.PlayerInGroup(player, group); + + if (inGroup && tagsObject.TryGetValue(group, out var groupTag) && groupTag is JObject) + { + string prefix = groupTag["prefix"]?.ToString() ?? ""; + string nickColor = groupTag["nick_color"]?.ToString() ?? ChatColors.Default.ToString(); + string messageColor = groupTag["message_color"]?.ToString() ?? ChatColors.Default.ToString(); + + foreach (var p in Utilities.GetPlayers().Where(p => p.TeamNum == player.TeamNum && p.IsValid && !p.IsBot)) + { + string messageToSend = $"{deadIcon}{TeamName(player.TeamNum)} {ChatColors.Default}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}"; + p.PrintToChat($" {ReplaceTags(messageToSend, p.TeamNum)}"); + } + + return HookResult.Handled; + } + } + + if (tagKey.Name.StartsWith("@")) + { + string permission = tagKey.Name; + bool hasPermission = AdminManager.PlayerHasPermissions(player, permission); + + if (hasPermission && tagsObject.TryGetValue(permission, out var permissionTag) && permissionTag is JObject) + { + string prefix = permissionTag["prefix"]?.ToString() ?? ""; + string nickColor = permissionTag["nick_color"]?.ToString() ?? ChatColors.Default.ToString(); + string messageColor = permissionTag["message_color"]?.ToString() ?? ChatColors.Default.ToString(); + + foreach (var p in Utilities.GetPlayers().Where(p => p.TeamNum == player.TeamNum && p.IsValid && !p.IsBot)) + { + string messageToSend = $"{deadIcon}{TeamName(player.TeamNum)} {ChatColors.Default}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}"; + p.PrintToChat($" {ReplaceTags(messageToSend, p.TeamNum)}"); + } + + return HookResult.Handled; + } + } + } + + if (tagsObject.TryGetValue("everyone", out var everyoneTag) && everyoneTag is JObject) + { + string prefix = everyoneTag["prefix"]?.ToString() ?? ""; + string nickColor = everyoneTag["nick_color"]?.ToString() ?? ChatColors.Default.ToString(); + string messageColor = everyoneTag["message_color"]?.ToString() ?? ChatColors.Default.ToString(); + + foreach (var p in Utilities.GetPlayers().Where(p => p.TeamNum == player.TeamNum && p.IsValid && !p.IsBot)) + { + string messageToSend = $"{deadIcon}{TeamName(player.TeamNum)} {ChatColors.Default}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}"; + p.PrintToChat($" {ReplaceTags(messageToSend, p.TeamNum)}"); + } + //p.PrintToChat(ReplaceTags($" {TeamName(player.TeamNum)} {ChatColors.Default}{prefix}{nickColor}{player.PlayerName}{ChatColors.Default}: {messageColor}{info.GetArg(1)}", p.TeamNum)); + + return HookResult.Handled; + } - private void ApplyAllClanTags() - { - foreach (var player in Utilities.GetPlayers()) - { - SetPlayerClanTag(player); } + return HookResult.Continue; } 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 tag = FindTag(player); - if (tag == null || tag.Scoreboard == "" || player.Clan == tag.Scoreboard) return; + string steamid = player.AuthorizedSteamID!.SteamId64.ToString(); - player.Clan = tag.Scoreboard; - Utilities.SetStateChanged(player, "CCSPlayerController", "m_szClan"); + if (JsonTags != null && JsonTags.TryGetValue("tags", out var tags) && tags is JObject tagsObject) + { + if (tagsObject.TryGetValue(steamid, out var playerTag) && playerTag is JObject) + { + var scoreboardValue = playerTag["scoreboard"]?.ToString(); + if (!string.IsNullOrEmpty(scoreboardValue)) + { + player.Clan = scoreboardValue; + return; + } + } + + foreach (var tagKey in tagsObject.Properties()) + { + if (tagKey.Name.StartsWith("#")) + { + string group = tagKey.Name; + bool inGroup = AdminManager.PlayerInGroup(player, group); + + if (inGroup) + { + if (tagsObject.TryGetValue(group, out var groupTag) && groupTag is JObject) + { + var scoreboardValue = groupTag["scoreboard"]?.ToString(); + if (!string.IsNullOrEmpty(scoreboardValue)) + { + player.Clan = scoreboardValue; + return; + } + } + } + } + + if (tagKey.Name.StartsWith("@")) + { + string permission = tagKey.Name; + bool hasPermission = AdminManager.PlayerHasPermissions(player, permission); + + if (hasPermission) + { + if (tagsObject.TryGetValue(permission, out var permissionTag) && permissionTag is JObject) + { + var scoreboardValue = permissionTag["scoreboard"]?.ToString(); + if (!string.IsNullOrEmpty(scoreboardValue)) + { + player.Clan = scoreboardValue; + return; + } + } + } + } + } + + if (tagsObject.TryGetValue("everyone", out var everyone) && everyone is JObject everyoneObject) + { + var scoreboardValue = everyoneObject["scoreboard"]?.ToString(); + if (!string.IsNullOrEmpty(scoreboardValue)) + { + player.Clan = scoreboardValue; + } + } + } } - private static string TeamName(int teamNum) + private string TeamName(int teamNum) { - return teamNum switch - { - (int)CsTeam.Spectator => "(SPEC)", - (int)CsTeam.Terrorist => $"{ChatColors.Yellow}(T)", - (int)CsTeam.CounterTerrorist => $"{ChatColors.Blue}(CT)", - _ => "(NONE)", - }; - } + string teamName = ""; - private static string TeamColor(int teamNum) - { - return teamNum switch + switch (teamNum) { - (int)CsTeam.Terrorist => ChatColors.Gold.ToString(), - (int)CsTeam.CounterTerrorist => ChatColors.Blue.ToString(), - _ => "", - }; - } - - private static string ReplaceTags(string text, int teamNum = 0) - { - if (!text.Contains('{')) return text; - - foreach (FieldInfo 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); + case 0: + teamName = $"(NONE)"; + break; + case 1: + teamName = $"(SPEC)"; + break; + case 2: + teamName = $"{ChatColors.Yellow}(T)"; + break; + case 3: + teamName = $"{ChatColors.Blue}(CT)"; + break; } - return text.Replace("{TEAMCOLOR}", TeamColor(teamNum), StringComparison.OrdinalIgnoreCase); + return teamName; } - // 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. - private const string DefaultTagsJson = """ + private string TeamColor(int teamNum) + { + string teamColor; + + switch (teamNum) { - "tags": { - "#rank/owner": { "prefix": "{DarkRed}[Owner]", "nick_color": "{LightRed}", "message_color": "", "scoreboard": "[Owner]" }, - "#rank/senioradmin": { "prefix": "{Red}[Senior Admin]", "nick_color": "{LightRed}", "message_color": "", "scoreboard": "[Sr. Admin]" }, - "#rank/admin": { "prefix": "{Red}[Admin]", "nick_color": "{LightRed}", "message_color": "", "scoreboard": "[Admin]" }, - "#rank/trialadmin": { "prefix": "{LightRed}[Trial Admin]", "nick_color": "", "message_color": "", "scoreboard": "[T. Admin]" }, - "#rank/mod": { "prefix": "{Blue}[Mod]", "nick_color": "", "message_color": "", "scoreboard": "[Mod]" }, - "#rank/trialmod": { "prefix": "{LightBlue}[Trial Mod]", "nick_color": "", "message_color": "", "scoreboard": "[T. Mod]" }, - "#rank/helper": { "prefix": "{Green}[Helper]", "nick_color": "", "message_color": "", "scoreboard": "[Helper]" }, - "#rank/guardian": { "prefix": "{Gold}[Guardian]", "nick_color": "", "message_color": "", "scoreboard": "[Guardian]" }, - "everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" } - } + case 2: + teamColor = $"{ChatColors.Gold}"; + break; + case 3: + teamColor = $"{ChatColors.Blue}"; + break; + default: + teamColor = ""; + break; } - """; + + return teamColor; + } + + private string ReplaceTags(string message, int teamNum = 0) + { + if (message.Contains('{')) + { + string modifiedValue = message; + foreach (FieldInfo field in typeof(ChatColors).GetFields()) + { + string pattern = $"{{{field.Name}}}"; + if (message.Contains(pattern, StringComparison.OrdinalIgnoreCase)) + { + modifiedValue = modifiedValue.Replace(pattern, field.GetValue(null)!.ToString(), StringComparison.OrdinalIgnoreCase); + } + } + return modifiedValue.Replace("{TEAMCOLOR}", TeamColor(teamNum)); + } + + return message; + } + } diff --git a/CS2-Tags.csproj b/CS2-Tags.csproj index 945a5b5..e71e1cd 100644 --- a/CS2-Tags.csproj +++ b/CS2-Tags.csproj @@ -1,20 +1,16 @@ - + - net10.0 + net7.0 CS2_Tags - CS2-Tags enable enable + true - - - none - runtime - compile; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/CS2-Tags.sln b/CS2-Tags.sln new file mode 100644 index 0000000..28a4de2 --- /dev/null +++ b/CS2-Tags.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CS2-Tags", "CS2-Tags.csproj", "{06A7649D-5CC9-4A37-9AB8-FEFB5DE69668}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {06A7649D-5CC9-4A37-9AB8-FEFB5DE69668}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06A7649D-5CC9-4A37-9AB8-FEFB5DE69668}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06A7649D-5CC9-4A37-9AB8-FEFB5DE69668}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06A7649D-5CC9-4A37-9AB8-FEFB5DE69668}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 2931f45..1a67768 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,75 @@ # CS2-Tags -Our fork of [daffyyyy/CS2-Tags](https://github.com/daffyyyy/CS2-Tags): chat prefixes, name/message -colours and scoreboard (clan) tags, assigned by SteamID64, admin group or permission. Built for -CounterStrikeSharp 1.0.375 (net10.0) and meant to run alongside CS2-SimpleAdmin. +![image](https://github.com/daffyyyy/CS2-Tags/assets/41084667/25dd3f2b-0604-41a2-b2bd-9be230db71e1) +![image](https://github.com/daffyyyy/CS2-Tags/assets/41084667/663a0de1-b875-48fc-bda5-56add5a4833b) -## Changes from upstream 1.0.4c +### Description +Adds tags to the server that can be easily edited, tags can be assigned via permission or steamid64 -- Targets net10.0 / CSS 1.0.375. Uses System.Text.Json, so Newtonsoft.Json no longer ships with it. -- Config moved to `configs/plugins/CS2-Tags/tags.json`. The default file uses our SimpleAdmin rank - groups. Comments and trailing commas are allowed. A broken file is logged and the previous - tags stay in use. -- Scoreboard tags call `SetStateChanged` on `m_szClan` so clients are told about the change. -- Colour tags like `{RED}` are only expanded in the config's strings. Players can no longer colour - their own chat by typing them. -- An empty `nick_color` means the team colour (like normal chat), not whatever colour the prefix - ended on. -- A tag with an empty `prefix`, `nick_color` and `message_color` leaves that player's chat alone - (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. -- 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. +### Commands +- css_tags_reload - Reload tags config -## Configuration - -`addons/counterstrikesharp/configs/plugins/CS2-Tags/tags.json`, written with defaults on first load: - -```jsonc +### Configuration +``` { "tags": { - "76561198000000000": { "prefix": "{Magenta}[Dev]", "nick_color": "", "message_color": "", "scoreboard": "[Dev]" }, - "#rank/owner": { "prefix": "{DarkRed}[Owner]", "nick_color": "{LightRed}", "message_color": "", "scoreboard": "[Owner]" }, - "#rank/admin": { "prefix": "{Red}[Admin]", "nick_color": "{LightRed}", "message_color": "", "scoreboard": "[Admin]" }, - "@css/vip": { "prefix": "{Gold}[VIP]", "nick_color": "", "message_color": "", "scoreboard": "[VIP]" }, - "everyone": { "prefix": "", "nick_color": "", "message_color": "", "scoreboard": "" } + "#css/admin": { // Group + "prefix": "{GREEN}[ADMIN]", // Chat prefix + "nick_color": "{RED}", // Nick color + "message_color": "{GOLD}", // Message nick color + "scoreboard": "[ADMIN]" // Scoreboard tag + }, + "@css/chat": { // Permission + "prefix": "{GREEN}[ADMIN]", // Chat prefix + "nick_color": "{RED}", // Nick color + "message_color": "{GOLD}", // Message nick color + "scoreboard": "[ADMIN]" // Scoreboard tag + }, + "76561198202892670": { // SteamID64 + "prefix": "{GREEN}[ADMIN]", + "nick_color": "{RED}", + "message_color": "{GOLD}", + "scoreboard": "[ADMIN]" + }, + "everyone": { // Tag for everyone, bots excluded + "prefix": "", + "nick_color": "", + "message_color": "", + "scoreboard": "[Player]" + } } } ``` +In addons/counterstrikesharp/plugins/CS2-Tags/tags.json -Each player gets exactly one tag, picked in this order: - -1. A SteamID64 key matching the player. -2. The first `#group` or `@permission` key the player matches, **in file order**. Put higher - ranks first. `@css/root` passes every `@css/...` check, so owners match any permission key. -3. `everyone`, if present. - -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. - -Colours: any `ChatColors` field name in braces, case-insensitive, e.g. `{Red}`, `{LightBlue}`, -`{Gold}`, `{Grey}`, plus `{TEAMCOLOR}`. - -Group membership comes from CS2-SimpleAdmin (`css_addadmin ... -g`, `css_addgroup`, then -`css_reloadadmins`). After changing ranks or `tags.json`, run `css_tags_reload` to reapply -scoreboard tags without waiting for a respawn. - -## Commands - -- `css_tags_reload` (`@css/root` or server console) - reload `tags.json` and reapply scoreboard tags. - -## Building and releasing +### Requirments +[CounterStrikeSharp](https://github.com/roflmuffin/CounterStrikeSharp/) **tested on v61** +### Colors ``` -cd plugins/CS2-Tags && ../../build.sh # output in compiled/CS2-Tags/ -FORGEJO_TOKEN=... ./release.sh v + public static char Default = '\x01'; + public static char White = '\x01'; + public static char Darkred = '\x02'; + public static char Green = '\x04'; + public static char LightYellow = '\x03'; + public static char LightBlue = '\x03'; + public static char Olive = '\x05'; + public static char Lime = '\x06'; + public static char Red = '\x07'; + public static char Purple = '\x03'; + public static char Grey = '\x08'; + public static char Yellow = '\x09'; + public static char Gold = '\x10'; + public static char Silver = '\x0A'; + public static char Blue = '\x0B'; + public static char DarkBlue = '\x0C'; + public static char BlueGrey = '\x0D'; + public static char Magenta = '\x0E'; + public static char LightRed = '\x0F'; ``` -`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-zio1` now). +```{TEAMCOLOR} - Team color``` +Use color name for e.g. {LightRed} + +### TODO +- Thinking about better fix for commands handling diff --git a/release.sh b/release.sh deleted file mode 100755 index 06e9b35..0000000 --- a/release.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash -# Build CS2-Tags, package it as one tarball and publish it as a Forgejo release. -# -# ./release.sh e.g. ./release.sh v0.14.0 -# -# Commit first: the plugin is rebuilt here from the committed source, so the release always matches -# the tag. The tag is created on HEAD and pushed to origin if it doesn't exist yet. Re-running with -# the same tag replaces that release's attachment. -# -# Asset: CS2-Tags-.tar.gz, laid out like the server's game/csgo/ - extract it there: -# tar -xzf CS2-Tags-.tar.gz -C /srv/cs2/game/csgo -# -# addons/counterstrikesharp/plugins/CS2-Tags/ CS2-Tags.dll, .pdb, .deps.json -# -# No configs/: the plugin writes configs/plugins/CS2-Tags/tags.json itself when it is missing, so an -# upgrade never touches the server's tags. -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. -FORGEJO_TOKEN="${FORGEJO_TOKEN:-CHANGE_ME_FORGEJO_TOKEN}" - -# Only these ship from the publish output. CounterStrikeSharp.API.dll is excluded by the csproj -# (ExcludeAssets=runtime); the server already provides it. -PLUGIN_FILES=( - CS2-Tags.dll - CS2-Tags.pdb - CS2-Tags.deps.json -) - -TAG="${1:-}" -if [[ -z "$TAG" ]]; then - echo "usage: $0 (e.g. v1.0.4c-zio1)" >&2 - exit 1 -fi -if [[ "$FORGEJO_TOKEN" == "CHANGE_ME_FORGEJO_TOKEN" ]]; then - echo "Set FORGEJO_TOKEN (edit release.sh or export it) first." >&2 - exit 1 -fi -for tool in podman jq curl git; do - command -v "$tool" >/dev/null || { echo "'$tool' is required." >&2; exit 1; } -done - -cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" - -if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then - echo "Tracked files have uncommitted changes; commit them so the tag matches the release." >&2 - exit 1 -fi - -# The tag should name the version the plugin reports in `css_plugins list`. -version="$(sed -n 's/.*ModuleVersion => "\(.*\)";.*/\1/p' CS2-Tags.cs)" -if [[ "${TAG#v}" != "$version" ]]; then - echo "Tag $TAG doesn't match ModuleVersion $version in CS2-Tags.cs." >&2 - exit 1 -fi - -# --- Build ----------------------------------------------------------------------- -OUT="release-out" -PUBLISH="$OUT/publish" -rm -rf bin obj "$OUT" -podman run --rm -v "$(pwd)":/src:Z -w /src \ - mcr.microsoft.com/dotnet/sdk:10.0 dotnet publish -c Release -o "/src/$PUBLISH" - -# --- Stage (layout of game/csgo/) and package ---------------------------------------- -STAGE="$OUT/stage" -PLUGIN_DIR="$STAGE/addons/counterstrikesharp/plugins/CS2-Tags" -mkdir -p "$PLUGIN_DIR" -for file in "${PLUGIN_FILES[@]}"; do - cp "$PUBLISH/$file" "$PLUGIN_DIR/" -done - -TARBALL="CS2-Tags-${TAG}.tar.gz" -tar -czf "$OUT/$TARBALL" -C "$STAGE" addons -rm -rf "$STAGE" "$PUBLISH" - -echo "Packaged:" -tar -tzf "$OUT/$TARBALL" - -# --- Tag --------------------------------------------------------------------------- -if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then - git tag -a "$TAG" -m "$TAG" -fi -git push origin "refs/tags/$TAG" - -# --- Release ----------------------------------------------------------------------- -API="$FORGEJO_URL/api/v1/repos/$OWNER/$REPO" -AUTH=(-H "Authorization: token $FORGEJO_TOKEN") - -release_json="$(curl -fsS "${AUTH[@]}" "$API/releases/tags/$TAG" 2>/dev/null || true)" -if [[ -z "$release_json" ]]; then - body="$(git log -1 --format=%B "$TAG")" - release_json="$(jq -n --arg tag "$TAG" --arg body "$body" \ - '{tag_name: $tag, name: $tag, body: $body, draft: false, prerelease: false}' | - curl -fsS "${AUTH[@]}" -H "Content-Type: application/json" -X POST --data @- "$API/releases")" - echo "Created release $TAG" -else - echo "Release $TAG already exists, replacing its attachment" -fi -release_id="$(jq -r '.id' <<<"$release_json")" - -existing_id="$(jq -r --arg n "$TARBALL" '.assets[]? | select(.name == $n) | .id' <<<"$release_json")" -if [[ -n "$existing_id" ]]; then - curl -fsS "${AUTH[@]}" -X DELETE "$API/releases/$release_id/assets/$existing_id" >/dev/null -fi -curl -fsS "${AUTH[@]}" -X POST -F "attachment=@$OUT/$TARBALL" \ - "$API/releases/$release_id/assets?name=$TARBALL" >/dev/null -echo "Uploaded $TARBALL" - -echo -echo "Download URL:" -echo " $FORGEJO_URL/$OWNER/$REPO/releases/download/$TAG/$TARBALL"