diff --git a/src/MonoScripts/MeetingInfoDisplay.cs b/src/MonoScripts/MeetingInfoDisplay.cs index 0a3eb36..152ceeb 100644 --- a/src/MonoScripts/MeetingInfoDisplay.cs +++ b/src/MonoScripts/MeetingInfoDisplay.cs @@ -1,12 +1,11 @@ -using AmongUs.GameOptions; -using BetterAmongUs.Attributes; +using BetterAmongUs.Attributes; using BetterAmongUs.Data; using BetterAmongUs.Generated; using BetterAmongUs.Modules.Support; using BetterAmongUs.MonoScripts.Extended; +using BetterAmongUs.Structs; using BetterAmongUs.Utilities; using Il2CppInterop.Runtime.Attributes; -using System.Text; using TMPro; using UnityEngine; @@ -23,8 +22,8 @@ internal sealed class MeetingInfoDisplay : PlayerInfoDisplay private Vector3 _infoPos; private Vector3 _TopPos; - private readonly StringBuilder _sbTag = new(256); - private readonly StringBuilder _sbInfo = new(256); + private readonly SplitStringBuilder _ssTag = new(100, '-'); + private readonly SplitStringBuilder _ssInfo = new(100, '-'); private string _lastInfoText = "", _lastTopText = ""; private int _lastUpdateFrame; private const int UPDATE_COOLDOWN = 5; @@ -75,8 +74,8 @@ internal sealed class MeetingInfoDisplay : PlayerInfoDisplay if (_pva == null) return; - _sbTag.Clear(); - _sbInfo.Clear(); + _ssTag.Clear(); + _ssInfo.Clear(); if (_player != null) { @@ -142,22 +141,21 @@ internal sealed class MeetingInfoDisplay : PlayerInfoDisplay if (_player == null || _player.Data == null || _player.ExtendedData() == null) return; - SetPlayerTags(_sbTag); - FormatPlayerInfo(_sbTag, _sbInfo); + SetPlayerTags(_ssTag); + _ssInfo.Append(_player.GetRoleInfo(true)); - string roleText = GetRoleText(); - UpdateNameTextPosition(roleText, _sbInfo.ToString()); + UpdateNameTextPosition(_ssInfo.ToString(), _ssInfo.ToString()); - UpdateTextIfChanged(_infoText, _sbInfo.ToString(), ref _lastInfoText); - UpdateTextIfChanged(_topText, roleText, ref _lastTopText); + UpdateTextIfChanged(_infoText, _ssInfo.ToString(), ref _lastInfoText); + UpdateTextIfChanged(_topText, _ssTag.ToString(), ref _lastTopText); } /// /// Sets player tags based on data from BetterDataManager. /// - /// StringBuilder for tag text. + /// StringBuilder for tag text. [HideFromIl2Cpp] - private void SetPlayerTags(StringBuilder sbTag) + private void SetPlayerTags(SplitStringBuilder ssTag) { if (_player == null) return; @@ -167,70 +165,10 @@ internal sealed class MeetingInfoDisplay : PlayerInfoDisplay if (BetterDataManager.Files.BetterDataFile.TryGetCheatInfo(_player.Data, out var info)) { - sbTag.Append(info.title.ToColor(info.hexColor) + "+++"); + ssTag.Append(info.title.ToColor(info.hexColor)); } } - /// - /// Formats player information tags into a readable string. - /// - /// StringBuilder containing tags. - /// StringBuilder for formatted info. - [HideFromIl2Cpp] - private static void FormatPlayerInfo(StringBuilder sbTag, StringBuilder sbInfo) - { - if (sbTag.Length == 0) - return; - - string tagString = sbTag.ToString(); - string[] tags = tagString.Split(["+++"], StringSplitOptions.RemoveEmptyEntries); - - for (int i = 0; i < tags.Length; i++) - { - if (!string.IsNullOrEmpty(tags[i])) - { - sbInfo.Append(tags[i]); - if (i < tags.Length - 1) - { - sbInfo.Append(" - "); - } - } - } - } - - /// - /// Gets the role text for display. - /// - /// Formatted role text. - private string GetRoleText() - { - if (_player == null) return string.Empty; - - string roleHexColor = _player.IsImpostorTeam() ? "#ff1919" : "#8cffff"; - string role = $"{_player.GetRoleName()}"; - - if (!_player.IsImpostorTeam() && _player.myTasks.Count > 0) - { - int completedTasks = 0; - foreach (var task in _player.Data.Tasks) - { - if (task.Complete) - completedTasks++; - } - role += $" ({completedTasks}/{_player.Data.Tasks.Count})"; - } - - if (!_player.IsImpostorTeammate()) - { - if ((PlayerControl.LocalPlayer.IsAlive() || PlayerControl.LocalPlayer.Is(RoleTypes.GuardianAngel)) && !_player.IsLocalPlayer()) - { - return string.Empty; - } - } - - return role; - } - /// /// Updates name text position based on role and info text presence. /// diff --git a/src/MonoScripts/PlayerInfoDisplay.cs b/src/MonoScripts/PlayerInfoDisplay.cs index 8c70c2f..731aa24 100644 --- a/src/MonoScripts/PlayerInfoDisplay.cs +++ b/src/MonoScripts/PlayerInfoDisplay.cs @@ -1,5 +1,4 @@ using AmongUs.Data; -using AmongUs.GameOptions; using BetterAmongUs.Attributes; using BetterAmongUs.Data; using BetterAmongUs.Data.Config; @@ -8,10 +7,9 @@ using BetterAmongUs.Modules; using BetterAmongUs.Modules.Support; using BetterAmongUs.MonoScripts.Extended; using BetterAmongUs.Patches.Gameplay.UI.Settings; +using BetterAmongUs.Structs; using BetterAmongUs.Utilities; -using HarmonyLib; using Il2CppInterop.Runtime.Attributes; -using System.Text; using System.Text.RegularExpressions; using TMPro; using UnityEngine; @@ -30,9 +28,9 @@ internal class PlayerInfoDisplay : MonoBehaviour protected TextMeshPro? _topText; protected TextMeshPro? _bottomText; - private readonly StringBuilder _sbTag = new(256); - private readonly StringBuilder _sbTagTop = new(256); - private readonly StringBuilder _sbTagBottom = new(256); + private readonly SplitStringBuilder _ssTag = new(100, '-'); + private readonly SplitStringBuilder _ssTagTop = new(100, '-'); + private readonly SplitStringBuilder _ssTagBottom = new(100, '-'); private string _lastTopText = "", _lastBottomText = "", _lastInfoText = ""; private int _lastUpdateFrame; private const int UPDATE_COOLDOWN = 10; @@ -132,9 +130,9 @@ internal class PlayerInfoDisplay : MonoBehaviour return; } - _sbTag.Clear(); - _sbTagTop.Clear(); - _sbTagBottom.Clear(); + _ssTag.Clear(); + _ssTagTop.Clear(); + _ssTagBottom.Clear(); UpdatePlayerInfo(); UpdatePlayerHighlight(); @@ -158,9 +156,6 @@ internal class PlayerInfoDisplay : MonoBehaviour if (_nameText == null) return; - if (_sbTag == null || _sbTagTop == null || _sbTagBottom == null) - return; - if (_topText == null || _bottomText == null || _infoText == null) return; @@ -195,20 +190,20 @@ internal class PlayerInfoDisplay : MonoBehaviour platform = TranslationStrings.Player_PlatformHidden.LocalizedString; if (!_player.IsInShapeshift()) - SetPlayerOutline(_sbTag); + SetPlayerOutline(_ssTag); if (GameState.IsInGame && GameState.IsLobby && !GameState.IsFreePlay) { - SetLobbyInfo(ref newName, betterData, _sbTag); + SetLobbyInfo(ref newName, betterData, _ssTag); - _sbTagTop.Append($"{platform}+++") - .Append($"Lv: {_player.Data.PlayerLevel + 1}+++"); + _ssTagTop.Append($"{platform}") + .Append($"Lv: {_player.Data.PlayerLevel + 1}"); - _sbTagBottom.Append($"{friendCode}+++"); + _ssTagBottom.Append($"{friendCode}"); } else if ((GameState.IsInGame || GameState.IsFreePlay) && !GameState.IsHideNSeek) { - SetInGameInfo(_sbTagTop); + SetInGameInfo(_ssTagTop); } if (!BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_NameOverride)) @@ -236,18 +231,18 @@ internal class PlayerInfoDisplay : MonoBehaviour } } - UpdateTextIfChanged(_topText, _sbTagTop, ref _lastTopText); - UpdateTextIfChanged(_bottomText, _sbTagBottom, ref _lastBottomText); - UpdateTextIfChanged(_infoText, _sbTag, ref _lastInfoText); + UpdateTextIfChanged(_topText, _ssTagTop, ref _lastTopText); + UpdateTextIfChanged(_bottomText, _ssTagBottom, ref _lastBottomText); + UpdateTextIfChanged(_infoText, _ssTag, ref _lastInfoText); } /// /// Updates text if changed, optimizing performance. /// /// TextMeshPro component to update. - /// StringBuilder containing new text. + /// StringBuilder containing new text. /// Reference to last value for comparison. - private static void UpdateTextIfChanged(TextMeshPro textMesh, StringBuilder sb, ref string lastValue) + private static void UpdateTextIfChanged(TextMeshPro textMesh, SplitStringBuilder ss, ref string lastValue) { if (BAUModdedSupportFlags.HasFlag(BAUModdedSupportFlags.Disable_PlayerInfo)) { @@ -258,7 +253,7 @@ internal class PlayerInfoDisplay : MonoBehaviour if (textMesh == null) return; - string newText = Utils.FormatInfo(sb); + string newText = ss.ToString(); if (newText != lastValue) { textMesh.SetText(newText); @@ -341,9 +336,9 @@ internal class PlayerInfoDisplay : MonoBehaviour /// /// Sets player outline based on data from BetterDataManager. /// - /// StringBuilder for tag text. + /// StringBuilder for tag text. [HideFromIl2Cpp] - private void SetPlayerOutline(StringBuilder? sbTag) + private void SetPlayerOutline(SplitStringBuilder? ssTag) { if (_player == null) return; @@ -358,9 +353,9 @@ internal class PlayerInfoDisplay : MonoBehaviour if (BetterDataManager.Files.BetterDataFile.TryGetCheatInfo(_player.Data, out var info)) { - if (sbTag != null) + if (ssTag.HasValue) { - sbTag.Append(info.title.ToColor(info.hexColor) + "+++"); + ssTag.Value.Append(info.title.ToColor(info.hexColor)); } _player.SetOutlineByHex(true, info.hexColor); } @@ -374,7 +369,7 @@ internal class PlayerInfoDisplay : MonoBehaviour /// Sets lobby specific information. /// [HideFromIl2Cpp] - private void SetLobbyInfo(ref string newName, ExtendedPlayerInfo betterData, StringBuilder sbTag) + private void SetLobbyInfo(ref string newName, ExtendedPlayerInfo betterData, SplitStringBuilder ssTag) { if (betterData == null) return; @@ -386,37 +381,20 @@ internal class PlayerInfoDisplay : MonoBehaviour { string verificationSymbol = betterData.IsVerifiedBetterUser || _player.IsLocalPlayer() ? "✓ " : ""; - sbTag.AppendFormat("{1}{0}+++", + ssTag.AppendFormat("{1}{0}", TranslationStrings.Player_BetterUser.LocalizedString, verificationSymbol); } - sbTag.Append($"ID: {_player.PlayerId}+++"); + ssTag.Append($"ID: {_player.PlayerId}"); } /// /// Sets in-game specific information. /// - /// StringBuilder for top tag text. + /// StringBuilder for top tag text. [HideFromIl2Cpp] - private void SetInGameInfo(StringBuilder sbTagTop) + private void SetInGameInfo(SplitStringBuilder ssTagTop) { - if (_player.IsImpostorTeammate() || _player.IsLocalPlayer() || - !PlayerControl.LocalPlayer.IsAlive() && !PlayerControl.LocalPlayer.Is(RoleTypes.GuardianAngel)) - { - string roleInfo = _player.GetRoleName().ToColor(_player.Data.Role.TeamColor); - - if (!_player.IsImpostorTeam() && _player.myTasks.Count > 0) - { - int completedTasks = 0; - foreach (var task in _player.Data.Tasks) - { - if (task.Complete) - completedTasks++; - } - roleInfo += $" ({completedTasks}/{_player.Data.Tasks.Count})"; - } - - sbTagTop.Append(roleInfo + "+++"); - } + ssTagTop.Append(_player.GetRoleInfo(true)); } /// diff --git a/src/Patches/Gameplay/UI/Chat/ChatPatch.cs b/src/Patches/Gameplay/UI/Chat/ChatPatch.cs index 3df93f7..80685ff 100644 --- a/src/Patches/Gameplay/UI/Chat/ChatPatch.cs +++ b/src/Patches/Gameplay/UI/Chat/ChatPatch.cs @@ -1,14 +1,13 @@ -using AmongUs.GameOptions; -using BetterAmongUs.Data; +using BetterAmongUs.Data; using BetterAmongUs.Data.Config; using BetterAmongUs.Generated; using BetterAmongUs.Modules; using BetterAmongUs.Modules.Support; using BetterAmongUs.MonoScripts.Extended; +using BetterAmongUs.Structs; using BetterAmongUs.Utilities; using BetterAmongUs.Utilities.Extension; using HarmonyLib; -using System.Text; using TMPro; using UnityEngine; @@ -234,21 +233,15 @@ internal static class ChatPatch if (localPlayer == null) return; - StringBuilder sbTag = new(); - StringBuilder sbInfo = new(); + SplitStringBuilder ssTag = new(100, '-'); string hashPuid = Utils.GetHashPuid(sourcePlayer); string friendCode = playerInfo.FriendCode; string playerName = playerInfo.ExtendedData()?.RealName ?? "???"; - // Format role display with team color - string Role = $"{sourcePlayer.GetRoleName()}+++"; - // In lobby, show player tags instead of roles if (GameState.IsLobby && !GameState.IsFreePlay) { - Role = ""; - var betterData = sourcePlayer.ExtendedData(); if (betterData == null) return; @@ -256,50 +249,25 @@ internal static class ChatPatch // Show BAU user tag if (sourcePlayer.IsLocalPlayer() || betterData.IsBetterUser) { - sbTag.AppendFormat("{1}{0}+++", TranslationStrings.Player_BetterUser, betterData.IsVerifiedBetterUser || sourcePlayer.IsLocalPlayer() ? "✓ " : ""); + ssTag.AppendFormat("{1}{0}", TranslationStrings.Player_BetterUser.LocalizedString, betterData.IsVerifiedBetterUser || sourcePlayer.IsLocalPlayer() ? "✓ " : ""); } // Show mod-specific tags based on player data if (BetterDataManager.Files.BetterDataFile.TryGetCheatInfo(sourcePlayer.Data, out var info)) { - sbTag.Append(info.title.ToColor(info.hexColor) + "+++"); + ssTag.Append(info.title.ToColor(info.hexColor)); } } - // Hide roles from alive players (unless same team) - if (!sourcePlayer.IsImpostorTeammate()) - { - if (localPlayer.IsAlive() && !sourcePlayer.IsLocalPlayer()) - Role = ""; - } + ssTag.Append(sourcePlayer.GetRoleInfo(false).Size(75f)); - // Show role for dead players or if local player is Guardian Angel - if ((localPlayer.Is(RoleTypes.GuardianAngel) && !sourcePlayer.IsAlive()) || !localPlayer.Is(RoleTypes.GuardianAngel)) - { - sbTag.Append(Role); - } - - // Format tags with separators - sbInfo.Append(""); - var parts = sbTag.ToString().Split("+++"); - - for (int i = 0; i < parts.Length; i++) - { - if (string.IsNullOrEmpty(parts[i])) continue; - - sbInfo.Append(parts[i]); - - if (i != parts.Length - 2) - sbInfo.Append(" - "); - } - - sbInfo.Append(""); + string infoText = ssTag.ToString().Size(75f); // Position tags before local player name, after other players' names if (sourcePlayer.IsLocalPlayer()) - playerName = sbInfo + " " + playerName; + playerName = infoText + " " + playerName; else - playerName += " " + sbInfo; + playerName += " " + infoText; bubble.NameText.SetText(playerName); } diff --git a/src/Structs/SplitStringBuilder.cs b/src/Structs/SplitStringBuilder.cs new file mode 100644 index 0000000..b6c57df --- /dev/null +++ b/src/Structs/SplitStringBuilder.cs @@ -0,0 +1,70 @@ +using BetterAmongUs.Utilities; +using System.Text; + +namespace BetterAmongUs.Structs; + +/// +/// Represents a mutable string builder that concatenates text with a specified separator. +/// +internal readonly struct SplitStringBuilder(int capacity, char? separator = null) +{ + private readonly char? _separator = separator; + private readonly StringBuilder _value = new(capacity); + + /// + /// Appends a text segment to the current string with automatic separator insertion. + /// + /// The text segment to append. + /// The current instance for fluent method chaining. + internal SplitStringBuilder Append(string text) + { + if (Utils.RemoveHtmlText(text) == string.Empty) + return this; + + if (_value.Length > 0) + { + if (_separator != null) + { + _value.Append($" {_separator} "); + } + else + { + _value.Append(' '); + } + } + + _value.Append(text); + return this; + } + + /// + /// Appends a formatted string to the current builder with automatic separator insertion. + /// + /// A composite format string. + /// An array of objects to format. + /// The current instance for fluent method chaining. + internal SplitStringBuilder AppendFormat(string format, params string[] strings) + { + return Append(string.Format(format, strings)); + } + + /// + /// Clears all content from the string builder, resetting it to an empty state. + /// + internal void Clear() + { + _value.Clear(); + } + + /// + /// Returns the complete concatenated string with all appended segments. + /// + /// + /// The full string containing all appended text segments separated by the configured separator. + /// Returns if no text has been appended. + /// + public readonly override string ToString() + { + return _value.ToString(); + } +} \ No newline at end of file diff --git a/src/Utilities/RoleUtils.cs b/src/Utilities/RoleUtils.cs index a64cf6f..323ff69 100644 --- a/src/Utilities/RoleUtils.cs +++ b/src/Utilities/RoleUtils.cs @@ -1,4 +1,5 @@ using AmongUs.GameOptions; +using BetterAmongUs.Modules; namespace BetterAmongUs.Utilities; @@ -83,23 +84,48 @@ internal static class RoleUtils } /// - /// Determines whether a role type is cliant side and can be desynced. + /// Gets the role information for a specified player, optionally including task progress. /// - /// The role type to check. - /// True if the role can be desynced to other clients, false otherwise. - internal static bool CanDesyncRole(this RoleTypes role) + /// The player to get role information for. + /// Whether to display the player's task completion progress. + /// Returns role info or if the player's role information cannot be displayed. + internal static string GetRoleInfo(this PlayerControl player, bool displayTask) { - if (role is RoleTypes.Phantom or RoleTypes.Viper) + if (!GameState.IsInGamePlay) { - return false; + return string.Empty; } - if (role is RoleTypes.Noisemaker) + if (!player.IsLocalPlayer() && !player.IsImpostorTeammate()) { - return false; + if (PlayerControl.LocalPlayer.IsAlive()) + { + return string.Empty; + } + + if (PlayerControl.LocalPlayer.Is(RoleTypes.GuardianAngel)) + { + return string.Empty; + } } - return true; + string roleInfo = player.GetRoleName().ToColor(player.Data.Role.TeamColor); + + if (!player.IsImpostorTeam() && player.myTasks.Count > 0) + { + if (displayTask) + { + int completedTasks = 0; + foreach (var task in player.Data.Tasks) + { + if (task.Complete) + completedTasks++; + } + roleInfo += $" ({completedTasks}/{player.Data.Tasks.Count})"; + } + } + + return roleInfo; } /// diff --git a/src/Utilities/Utils.cs b/src/Utilities/Utils.cs index 90977f2..3198b21 100644 --- a/src/Utilities/Utils.cs +++ b/src/Utilities/Utils.cs @@ -43,26 +43,6 @@ internal static class Utils return text; } - /// - /// Formats information from a StringBuilder, cleaning HTML and formatting separators. - /// - /// The StringBuilder containing the text to format. - /// The formatted string. - internal static string FormatInfo(StringBuilder source) - { - if (source.Length == 0) return string.Empty; - - var sb = new StringBuilder(); - foreach (var part in source.ToString().Split("+++")) - { - if (!string.IsNullOrEmpty(RemoveHtmlText(part))) - { - sb.Append(part).Append(" - "); - } - } - return sb.ToString().TrimEnd(" - ".ToCharArray()); - } - /// /// Removes all HTML tags and formatting from a string. ///