Rework string building

This commit is contained in:
D1GQ committed 2026-06-28 15:09:04 -05:00
1 parent 7460720178
commit 69f4e3bef3
6 files changed
+157 -197

No files matched your search

+14 -76
View File
@@ -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);
}
/// <summary>
/// Sets player tags based on data from BetterDataManager.
/// </summary>
/// <param name="sbTag">StringBuilder for tag text.</param>
/// <param name="ssTag">StringBuilder for tag text.</param>
[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));
}
}
/// <summary>
/// Formats player information tags into a readable string.
/// </summary>
/// <param name="sbTag">StringBuilder containing tags.</param>
/// <param name="sbInfo">StringBuilder for formatted info.</param>
[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(" - ");
}
}
}
}
/// <summary>
/// Gets the role text for display.
/// </summary>
/// <returns>Formatted role text.</returns>
private string GetRoleText()
{
if (_player == null) return string.Empty;
string roleHexColor = _player.IsImpostorTeam() ? "#ff1919" : "#8cffff";
string role = $"<color={roleHexColor}>{_player.GetRoleName()}</color>";
if (!_player.IsImpostorTeam() && _player.myTasks.Count > 0)
{
int completedTasks = 0;
foreach (var task in _player.Data.Tasks)
{
if (task.Complete)
completedTasks++;
}
role += $" <color=#cbcbcb>({completedTasks}/{_player.Data.Tasks.Count})</color>";
}
if (!_player.IsImpostorTeammate())
{
if ((PlayerControl.LocalPlayer.IsAlive() || PlayerControl.LocalPlayer.Is(RoleTypes.GuardianAngel)) && !_player.IsLocalPlayer())
{
return string.Empty;
}
}
return role;
}
/// <summary>
/// Updates name text position based on role and info text presence.
/// </summary>
+29 -51
View File
@@ -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($"<color=#9e9e9e>{platform}</color>+++")
.Append($"<color=#ffd829>Lv: {_player.Data.PlayerLevel + 1}</color>+++");
_ssTagTop.Append($"<color=#9e9e9e>{platform}</color>")
.Append($"<color=#ffd829>Lv: {_player.Data.PlayerLevel + 1}</color>");
_sbTagBottom.Append($"<color={friendCodeColor}>{friendCode}</color>+++");
_ssTagBottom.Append($"<color={friendCodeColor}>{friendCode}</color>");
}
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);
}
/// <summary>
/// Updates text if changed, optimizing performance.
/// </summary>
/// <param name="textMesh">TextMeshPro component to update.</param>
/// <param name="sb">StringBuilder containing new text.</param>
/// <param name="ss">StringBuilder containing new text.</param>
/// <param name="lastValue">Reference to last value for comparison.</param>
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
/// <summary>
/// Sets player outline based on data from BetterDataManager.
/// </summary>
/// <param name="sbTag">StringBuilder for tag text.</param>
/// <param name="ssTag">StringBuilder for tag text.</param>
[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.
/// </summary>
[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("<color=#0dff00>{1}{0}</color>+++",
ssTag.AppendFormat("<color=#0dff00>{1}{0}</color>",
TranslationStrings.Player_BetterUser.LocalizedString, verificationSymbol);
}
sbTag.Append($"<color=#b554ff>ID: {_player.PlayerId}</color>+++");
ssTag.Append($"<color=#b554ff>ID: {_player.PlayerId}</color>");
}
/// <summary>
/// Sets in-game specific information.
/// </summary>
/// <param name="sbTagTop">StringBuilder for top tag text.</param>
/// <param name="ssTagTop">StringBuilder for top tag text.</param>
[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 += $" <color=#cbcbcb>({completedTasks}/{_player.Data.Tasks.Count})</color>";
}
sbTagTop.Append(roleInfo + "+++");
}
ssTagTop.Append(_player.GetRoleInfo(true));
}
/// <summary>
+9 -41
View File
@@ -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 = $"<size=75%><color={sourcePlayer.GetTeamHexColor()}>{sourcePlayer.GetRoleName()}</color></size>+++";
// 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("<color=#0dff00>{1}{0}</color>+++", TranslationStrings.Player_BetterUser, betterData.IsVerifiedBetterUser || sourcePlayer.IsLocalPlayer() ? "✓ " : "");
ssTag.AppendFormat("<color=#0dff00>{1}{0}</color>", 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("<size=75%>");
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("</size>");
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);
}
+70
View File
@@ -0,0 +1,70 @@
using BetterAmongUs.Utilities;
using System.Text;
namespace BetterAmongUs.Structs;
/// <summary>
/// Represents a mutable string builder that concatenates text with a specified separator.
/// </summary>
internal readonly struct SplitStringBuilder(int capacity, char? separator = null)
{
private readonly char? _separator = separator;
private readonly StringBuilder _value = new(capacity);
/// <summary>
/// Appends a text segment to the current string with automatic separator insertion.
/// </summary>
/// <param name="text">The text segment to append.</param>
/// <returns>The current <see cref="SplitStringBuilder"/> instance for fluent method chaining.</returns>
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;
}
/// <summary>
/// Appends a formatted string to the current builder with automatic separator insertion.
/// </summary>
/// <param name="format">A composite format string.</param>
/// <param name="strings">An array of objects to format.</param>
/// <returns>The current <see cref="SplitStringBuilder"/> instance for fluent method chaining.</returns>
internal SplitStringBuilder AppendFormat(string format, params string[] strings)
{
return Append(string.Format(format, strings));
}
/// <summary>
/// Clears all content from the string builder, resetting it to an empty state.
/// </summary>
internal void Clear()
{
_value.Clear();
}
/// <summary>
/// Returns the complete concatenated string with all appended segments.
/// </summary>
/// <returns>
/// The full string containing all appended text segments separated by the configured separator.
/// Returns <see cref="string.Empty"/> if no text has been appended.
/// </returns>
public readonly override string ToString()
{
return _value.ToString();
}
}
+35 -9
View File
@@ -1,4 +1,5 @@
using AmongUs.GameOptions;
using BetterAmongUs.Modules;
namespace BetterAmongUs.Utilities;
@@ -83,23 +84,48 @@ internal static class RoleUtils
}
/// <summary>
/// Determines whether a role type is cliant side and can be desynced.
/// Gets the role information for a specified player, optionally including task progress.
/// </summary>
/// <param name="role">The role type to check.</param>
/// <returns>True if the role can be desynced to other clients, false otherwise.</returns>
internal static bool CanDesyncRole(this RoleTypes role)
/// <param name="player">The player to get role information for.</param>
/// <param name="displayTask">Whether to display the player's task completion progress.</param>
/// <returns>Returns role info or <see cref="string.Empty"/> if the player's role information cannot be displayed.</returns>
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 += $" <color=#cbcbcb>({completedTasks}/{player.Data.Tasks.Count})</color>";
}
}
return roleInfo;
}
/// <summary>
-20
View File
@@ -43,26 +43,6 @@ internal static class Utils
return text;
}
/// <summary>
/// Formats information from a StringBuilder, cleaning HTML and formatting separators.
/// </summary>
/// <param name="source">The StringBuilder containing the text to format.</param>
/// <returns>The formatted string.</returns>
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());
}
/// <summary>
/// Removes all HTML tags and formatting from a string.
/// </summary>