From a162297997675013858f44118fcfa1cd401707c2 Mon Sep 17 00:00:00 2001 From: DeveloperNyx Date: Tue, 17 Mar 2026 09:57:38 +0000 Subject: [PATCH] Added linux comaptibility! Added null safety and cross-platform behaviour, improved BetterUser handshake. --- src/Data/SharedSecretExchange.cs | 67 +++++++++++-- src/Modules/HandshakeHandler.cs | 74 +++----------- src/Mono/ExtendedPlayerInfo.cs | 7 +- src/Mono/PlayerInfoDisplay.cs | 20 +++- src/Patches/Client/PrivateLobbyPatch.cs | 2 +- src/Patches/Gameplay/UI/Chat/ChatPatch.cs | 112 ++++++++-------------- 6 files changed, 131 insertions(+), 151 deletions(-) diff --git a/src/Data/SharedSecretExchange.cs b/src/Data/SharedSecretExchange.cs index 2ce7a60..87115dd 100644 --- a/src/Data/SharedSecretExchange.cs +++ b/src/Data/SharedSecretExchange.cs @@ -11,23 +11,40 @@ internal sealed class SharedSecretExchange private byte[] sharedSecret = []; private bool cryptoDisabled; + internal bool CryptoAvailable => !cryptoDisabled; + internal bool UseFallback { get; set; } + + private int? remoteTempKey; + internal SharedSecretExchange() { try { - dh = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256); + dh = ECDiffieHellman.Create(); + dh.GenerateKey(ECCurve.NamedCurves.nistP256); publicKey = dh.ExportSubjectPublicKeyInfo(); } catch (Exception ex) { - Logger_.Error("ECDH unavailable, disabling secure handshake: " + ex.Message); + Logger_.Error("ECDH unavailable, using fallback handshake: " + ex.Message); cryptoDisabled = true; dh = null; publicKey = []; } - var random = new Random(); - tempKey = random.Next(); + tempKey = GenerateSecureTempKey(); + } + + private static int GenerateSecureTempKey() + { + byte[] buffer = new byte[4]; + RandomNumberGenerator.Fill(buffer); + return Math.Abs(BitConverter.ToInt32(buffer, 0)); + } + + internal void SetRemoteTempKey(int key) + { + remoteTempKey = key; } internal byte[] GetPublicKey() @@ -43,21 +60,32 @@ internal sealed class SharedSecretExchange internal byte[] GetSharedSecret() { - if (cryptoDisabled) return []; return sharedSecret; } internal byte[] GenerateSharedSecret(byte[] otherPartyPublicKey) { - if (cryptoDisabled) return []; if (sharedSecret.Length > 0) return sharedSecret; + if (cryptoDisabled || UseFallback) + { + if (!remoteTempKey.HasValue) + return []; + + sharedSecret = DeriveFallbackSecret(tempKey, remoteTempKey.Value); + return sharedSecret; + } + try { - using ECDiffieHellman otherPartyDH = ECDiffieHellman.Create(); - otherPartyDH.ImportSubjectPublicKeyInfo(otherPartyPublicKey, out _); + using var otherPartyDH = ECDiffieHellman.Create(); + if (otherPartyPublicKey == null || otherPartyPublicKey.Length == 0) + return []; + + otherPartyDH.ImportSubjectPublicKeyInfo(otherPartyPublicKey, out _); sharedSecret = dh.DeriveKeyMaterial(otherPartyDH.PublicKey); + dh.Dispose(); return sharedSecret; } @@ -70,15 +98,34 @@ internal sealed class SharedSecretExchange internal int GetSharedSecretHash() { - if (cryptoDisabled) return 0; if (sharedSecret.Length == 0) return 0; using SHA256 sha256 = SHA256.Create(); byte[] hashBytes = sha256.ComputeHash(sharedSecret); - int numericHash = BitConverter.ToInt32(hashBytes, 0); + + int numericHash = (hashBytes[0] << 24) | + (hashBytes[1] << 16) | + (hashBytes[2] << 8) | + hashBytes[3]; + return Math.Abs(numericHash); } + private static byte[] DeriveFallbackSecret(int local, int remote) + { + using SHA256 sha256 = SHA256.Create(); + + int a = local < remote ? local : remote; + int b = local < remote ? remote : local; + + byte[] buffer = new byte[8]; + + Buffer.BlockCopy(BitConverter.GetBytes(a), 0, buffer, 0, 4); + Buffer.BlockCopy(BitConverter.GetBytes(b), 0, buffer, 4, 4); + + return sha256.ComputeHash(buffer); + } + internal bool HasBeenCleared { get; private set; } internal void ClearData() diff --git a/src/Modules/HandshakeHandler.cs b/src/Modules/HandshakeHandler.cs index 275a9bf..2c47867 100644 --- a/src/Modules/HandshakeHandler.cs +++ b/src/Modules/HandshakeHandler.cs @@ -11,9 +11,6 @@ using UnityEngine; namespace BetterAmongUs.Modules; -/// -/// Handles the secure handshake process between BetterAmongUs clients using Diffie-Hellman key exchange. -/// internal sealed class HandshakeHandler { [HideFromIl2Cpp] @@ -25,17 +22,11 @@ internal sealed class HandshakeHandler [HideFromIl2Cpp] private ExtendedPlayerInfo extendedData { get; } - /// - /// Initiates the wait period before sending the secret to another player. - /// internal void WaitSendSecretToPlayer() { extendedData.StartCoroutine(CoWaitSendSecretToPlayer()); } - /// - /// Coroutine that waits for player initialization before sending the secret. - /// private IEnumerator CoWaitSendSecretToPlayer() { if (!BAUPlugin.SendBetterRpc.Value) yield break; @@ -45,14 +36,11 @@ internal sealed class HandshakeHandler if (GameState.IsFreePlay) yield break; yield return null; } - yield return new WaitForSeconds(1f); + yield return new WaitForSeconds(1f); SendSecretToPlayer(); } - /// - /// Resends the secret to the player if not already verified. - /// internal void ResendSecretToPlayer() { if (!BAUPlugin.SendBetterRpc.Value) return; @@ -62,61 +50,47 @@ internal sealed class HandshakeHandler SendSecretToPlayer(); } - /// - /// Sends the local client's public key and temporary key to another player. - /// - // Local client sends to client private void SendSecretToPlayer() { if (extendedData._Data.Object.IsLocalPlayer()) return; if (HasSendSharedSecret) return; HasSendSharedSecret = true; + RPC.SendCustomRpcPacked(CustomRPC.SendSecretToPlayer, writer => { + writer.Write(SharedSecret.CryptoAvailable); writer.WriteBytes(SharedSecret.GetPublicKey()); writer.Write(SharedSecret.GetTempKey()); }, extendedData._Data.ClientId); } - /// - /// Handles receiving a secret from another player and generates a shared secret. - /// - /// MessageReader containing the sender's public key and temporary key. - // Client receives from local client internal void HandleSecretFromSender(MessageReader reader) { if (extendedData._Data?.Object?.IsLocalPlayer() == true) return; + bool senderSupportsCrypto = reader.ReadBoolean(); byte[] sendersPublicKey = reader.ReadBytes(); int tempKey = reader.ReadInt32(); - // Logger.Log($"Received public key ({sendersPublicKey.Length} bytes) from {_Data.PlayerName}"); + SharedSecret.UseFallback = !senderSupportsCrypto; + SharedSecret.SetRemoteTempKey(tempKey); byte[] secret = SharedSecret.GenerateSharedSecret(sendersPublicKey); - if (secret.Length == 0) - { - // Logger.Error("Failed to generate shared secret!"); - return; - } + if (secret.Length == 0) return; + extendedData.IsBetterUser = true; + TryHandlePendingVerificationData(); SendSecretHashToSender(tempKey, extendedData._Data.ClientId); ResendSecretToPlayer(); } - /// - /// Sends the hash of the generated shared secret back to the original sender for verification. - /// - /// The temporary key received from the sender. - /// The client ID of the sender. - // Client sends back to local client private void SendSecretHashToSender(int tempKey, int senderClientId) { if (!BAUPlugin.SendBetterRpc.Value) return; int hash = SharedSecret.GetSharedSecretHash(); - // Logger.Log($"Sending secret hash: {hash} (tempKey: {tempKey})"); RPC.SendCustomRpcPacked(CustomRPC.CheckSecretHashFromPlayer, writer => { @@ -125,47 +99,30 @@ internal sealed class HandshakeHandler }, senderClientId); } - /// - /// Handles receiving a secret hash from another player for verification. - /// - /// MessageReader containing the temporary key and hash. internal void HandleSecretHashFromPlayer(MessageReader reader) { int tempKey = reader.ReadInt32(); int receivedHash = reader.ReadInt32(); + _pendingVerificationData = (tempKey, receivedHash); TryHandlePendingVerificationData(); } - /// - /// Attempts to verify pending handshake data if all required information is available. - /// internal void TryHandlePendingVerificationData() { - if (_pendingVerificationData?.tempKey == null || _pendingVerificationData?.receivedHash == null) return; + if (!_pendingVerificationData.HasValue) return; if (SharedSecret.GetSharedSecret().Length == 0) return; - var tempKey = _pendingVerificationData?.tempKey; - var receivedHash = _pendingVerificationData?.receivedHash; + var data = _pendingVerificationData.Value; - // Logger.Log($"Received hash check: TempKey={tempKey} (ours={SharedSecret.GetTempKey()}), Hash={receivedHash} (ours={SharedSecret.GetSharedSecretHash()})"); - - if (tempKey != SharedSecret.GetTempKey()) - { - // Logger.Warning($"Invalid tempKey from {_Data.PlayerName}"); + if (data.tempKey != SharedSecret.GetTempKey()) return; - } extendedData.IsBetterUser = true; - if (receivedHash == SharedSecret.GetSharedSecretHash()) + if (data.receivedHash == SharedSecret.GetSharedSecretHash()) { extendedData.IsVerifiedBetterUser = true; - // Logger.Log($"Verified player: {_Data.PlayerName}"); - } - else - { - // Logger.Warning($"Hash mismatch from {_Data.PlayerName}"); } _pendingVerificationData = null; @@ -174,8 +131,5 @@ internal sealed class HandshakeHandler private (int tempKey, int receivedHash)? _pendingVerificationData = null; private bool HasSendSharedSecret { get; set; } - /// - /// Gets the SharedSecretExchange instance for secure key exchange. - /// internal SharedSecretExchange SharedSecret { get; set; } = new(); } \ No newline at end of file diff --git a/src/Mono/ExtendedPlayerInfo.cs b/src/Mono/ExtendedPlayerInfo.cs index e306bd5..ac638d1 100644 --- a/src/Mono/ExtendedPlayerInfo.cs +++ b/src/Mono/ExtendedPlayerInfo.cs @@ -17,7 +17,7 @@ internal sealed class ExtendedPlayerInfo : MonoBehaviour, IMonoExtension= ExtendedAntiCheatInfo.MAX_RPC_SENT && !flag) { - BetterNotificationManager.NotifyCheat(_Data.Object, + BetterNotificationManager.NotifyCheat( + _Data.Object, Translator.GetString("AntiCheat.Reason.RPCSentPS"), Translator.GetString("AntiCheat.UnauthorizedAction") ); + Logger_.LogCheat($"{_Data.Object.BetterData().RealName} {AntiCheatInfo.RPCSentPS} Sent."); } timeAccumulator += time; + if (timeAccumulator >= 0.25f - 0.005 * AntiCheatInfo.RPCSentPS) { AntiCheatInfo.RPCSentPS -= 1; diff --git a/src/Mono/PlayerInfoDisplay.cs b/src/Mono/PlayerInfoDisplay.cs index 4fa73e6..9878b44 100644 --- a/src/Mono/PlayerInfoDisplay.cs +++ b/src/Mono/PlayerInfoDisplay.cs @@ -374,11 +374,8 @@ internal class PlayerInfoDisplay : MonoBehaviour } /// - /// Sets lobby-specific information. + /// Sets in-game specific information. /// - /// Reference to the player's name. - /// Extended player data. - /// StringBuilder for tag text. [HideFromIl2Cpp] private void SetLobbyInfo(ref string newName, ExtendedPlayerInfo betterData, StringBuilder sbTag) { @@ -389,10 +386,23 @@ internal class PlayerInfoDisplay : MonoBehaviour if ((_player.IsLocalPlayer() || betterData.IsBetterUser) && !GameState.IsInGamePlay) { - string verificationSymbol = betterData.IsVerifiedBetterUser || _player.IsLocalPlayer() ? "✓ " : ""; + string verificationSymbol; + + if (betterData.HandshakeHandler.SharedSecret.UseFallback) + { + // Fallback path (Linux or crypto unavailable) + verificationSymbol = betterData.IsVerifiedBetterUser ? "(Linux) ✓ " : "(Linux) "; + } + else + { + // Normal ECDH path + verificationSymbol = betterData.IsVerifiedBetterUser || _player.IsLocalPlayer() ? "✓ " : ""; + } + sbTag.AppendFormat("{1}{0}+++", _cachedTranslations.BetterUser, verificationSymbol); } + sbTag.Append($"ID: {_player.PlayerId}+++"); } diff --git a/src/Patches/Client/PrivateLobbyPatch.cs b/src/Patches/Client/PrivateLobbyPatch.cs index bdef56c..102050e 100644 --- a/src/Patches/Client/PrivateLobbyPatch.cs +++ b/src/Patches/Client/PrivateLobbyPatch.cs @@ -129,7 +129,7 @@ internal static class PrivateLobbyPatch var sprite = __instance.HostPrivateButton.transform.Find("Inactive")?.GetComponent(); if (sprite != null) { - sprite.color = new Color(0.35f, 1f, 1f, 1f); // Corrected color assignment + sprite.color = new Color(0.35f, 1f, 1f, 1f); } } } diff --git a/src/Patches/Gameplay/UI/Chat/ChatPatch.cs b/src/Patches/Gameplay/UI/Chat/ChatPatch.cs index f2942bf..f307558 100644 --- a/src/Patches/Gameplay/UI/Chat/ChatPatch.cs +++ b/src/Patches/Gameplay/UI/Chat/ChatPatch.cs @@ -21,8 +21,6 @@ internal static class ChatPatch internal static void ClearChat() { if (!HudManager.InstanceExists) return; - - // Clear all chat bubbles HudManager.Instance.Chat.chatBubblePool.ReclaimAll(); } @@ -30,7 +28,6 @@ internal static class ChatPatch { if (!HudManager.InstanceExists) return; - // Clear only player chat bubbles (keep command bubbles) foreach (var obj in HudManager.Instance.Chat.chatBubblePool.activeChildren.ToArray()) { var chatBubble = obj.GetComponent(); @@ -40,6 +37,7 @@ internal static class ChatPatch HudManager.Instance.Chat.chatBubblePool.Reclaim(chatBubble); } } + HudManager.Instance.Chat.AlignAllBubbles(); } @@ -47,7 +45,6 @@ internal static class ChatPatch { if (!HudManager.InstanceExists) return; - // Clear only command chat bubbles (keep player chat) foreach (var obj in HudManager.Instance.Chat.chatBubblePool.activeChildren.ToArray()) { var chatBubble = obj.GetComponent(); @@ -57,14 +54,14 @@ internal static class ChatPatch HudManager.Instance.Chat.chatBubblePool.Reclaim(chatBubble); } } + HudManager.Instance.Chat.AlignAllBubbles(); } [HarmonyPatch(typeof(ChatController), nameof(ChatController.Toggle))] [HarmonyPostfix] - private static void ChatController_Toggle_Postfix(/*ChatController __instance*/) + private static void ChatController_Toggle_Postfix() { - // Apply chat theme when chat is opened/closed SetChatTheme(); } @@ -73,52 +70,45 @@ internal static class ChatPatch [HarmonyPriority(Priority.First)] private static void ChatController_Update_Prefix(ChatController __instance) { - // Apply dark/light theme to chat input field if (BAUPlugin.ChatDarkMode.Value) { - // Free chat color __instance.freeChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); __instance.freeChatField.textArea.compoText.Color(Color.white); __instance.freeChatField.textArea.outputText.color = Color.white; } else { - // Free chat color __instance.freeChatField.background.color = new Color32(255, 255, 255, byte.MaxValue); __instance.freeChatField.textArea.compoText.Color(Color.black); __instance.freeChatField.textArea.outputText.color = Color.black; } - // Ctrl+X to cut text to clipboard if ((Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && Input.GetKeyDown(KeyCode.X)) { ClipboardHelper.PutClipboardString(__instance.freeChatField.textArea.text); __instance.freeChatField.textArea.SetText(""); } - // Up arrow for chat history navigation if (Input.GetKeyDown(KeyCode.UpArrow) && ChatHistory.Any()) { CurrentHistorySelection = Mathf.Clamp(--CurrentHistorySelection, 0, ChatHistory.Count - 1); __instance.freeChatField.textArea.SetText(ChatHistory[CurrentHistorySelection]); } - // Down arrow for chat history navigation if (Input.GetKeyDown(KeyCode.DownArrow) && ChatHistory.Any()) { CurrentHistorySelection++; if (CurrentHistorySelection < ChatHistory.Count) __instance.freeChatField.textArea.SetText(ChatHistory[CurrentHistorySelection]); - else __instance.freeChatField.textArea.SetText(""); + else + __instance.freeChatField.textArea.SetText(""); } } - // Log chat messages to console [HarmonyPatch(typeof(ChatController), nameof(ChatController.AddChat))] [HarmonyPostfix] private static void ChatController_AddChat_Postfix(ChatController __instance, PlayerControl sourcePlayer, string chatText) { - // Log chat publicly if player is alive, privately if dead if (sourcePlayer.IsAlive() || !PlayerControl.LocalPlayer.IsAlive()) { Logger_.Log($"{sourcePlayer.Data.PlayerName} -> {chatText}", "ChatLog"); @@ -134,13 +124,11 @@ internal static class ChatPatch private static void ChatController_SetChatBubbleName_Postfix(ChatController __instance, ChatBubble bubble, NetworkedPlayerInfo playerInfo, bool isDead, bool didVote) { if (didVote) return; - if (bubble == null) return; - if (bubble.NameText == null) return; - if (playerInfo == null) return; + if (bubble == null || bubble.NameText == null || playerInfo == null) return; var sourcePlayer = playerInfo.Object; - if (sourcePlayer == null) return; - if (PlayerControl.LocalPlayer == null) return; + var localPlayer = PlayerControl.LocalPlayer; + if (sourcePlayer == null || localPlayer == null) return; StringBuilder sbTag = new(); StringBuilder sbInfo = new(); @@ -159,7 +147,13 @@ internal static class ChatPatch if (betterData == null) return; if (sourcePlayer.IsLocalPlayer() || betterData.IsBetterUser) - sbTag.AppendFormat("{1}{0}+++", Translator.GetString("Player.BetterUser"), betterData.IsVerifiedBetterUser || sourcePlayer.IsLocalPlayer() ? "✓ " : ""); + { + sbTag.AppendFormat( + "{1}{0}+++", + Translator.GetString("Player.BetterUser"), + betterData.IsVerifiedBetterUser || sourcePlayer.IsLocalPlayer() ? "✓ " : "" + ); + } if (BetterDataManager.BetterDataFile.SickoData.Any(info => info.CheckPlayerData(sourcePlayer.Data))) sbTag.Append($"{Translator.GetString("Player.SickoUser")}+++"); @@ -173,41 +167,34 @@ internal static class ChatPatch if (!sourcePlayer.IsImpostorTeammate()) { - if (PlayerControl.LocalPlayer.IsAlive() && !sourcePlayer.IsLocalPlayer()) - { + if (localPlayer.IsAlive() && !sourcePlayer.IsLocalPlayer()) Role = ""; - } } - if (PlayerControl.LocalPlayer.Is(RoleTypes.GuardianAngel) && !sourcePlayer.IsAlive() || !PlayerControl.LocalPlayer.Is(RoleTypes.GuardianAngel)) + if ((localPlayer.Is(RoleTypes.GuardianAngel) && !sourcePlayer.IsAlive()) || !localPlayer.Is(RoleTypes.GuardianAngel)) { sbTag.Append(Role); } sbInfo.Append(""); var parts = sbTag.ToString().Split("+++"); + for (int i = 0; i < parts.Length; i++) { - if (!string.IsNullOrEmpty(parts[i])) - { - sbInfo.Append(parts[i]); - if (i != parts.Length - 2) - { - sbInfo.Append(" - "); - } - } + if (string.IsNullOrEmpty(parts[i])) continue; + + sbInfo.Append(parts[i]); + + if (i != parts.Length - 2) + sbInfo.Append(" - "); } + sbInfo.Append(""); - bool flag = sourcePlayer.IsLocalPlayer(); - if (flag) - { - playerName = $"{sbInfo} " + playerName; - } + if (sourcePlayer.IsLocalPlayer()) + playerName = sbInfo + " " + playerName; else - { - playerName += $" {sbInfo}"; - } + playerName += " " + sbInfo; bubble.NameText.SetText(playerName); } @@ -225,63 +212,51 @@ internal static class ChatPatch if (BAUPlugin.ChatDarkMode.Value) { - // Quick chat color chat.quickChatField.background.color = new Color32(40, 40, 40, byte.MaxValue); chat.quickChatField.text.color = Color.white; - // Icons chat.quickChatButton.transform.Find("QuickChatIcon").GetComponent().color = new Color(0.5f, 0.5f, 0.5f, 1f); chat.openKeyboardButton.transform.Find("OpenKeyboardIcon").GetComponent().color = new Color(0.5f, 0.5f, 0.5f, 1f); } else { - // Quick chat color chat.quickChatField.background.color = new Color32(255, 255, 255, byte.MaxValue); chat.quickChatField.text.color = Color.black; - // Icons chat.quickChatButton.transform.Find("QuickChatIcon").GetComponent().color = new Color(1f, 1f, 1f, 1f); chat.openKeyboardButton.transform.Find("OpenKeyboardIcon").GetComponent().color = new Color(1f, 1f, 1f, 1f); } - // Apply theme to all existing chat bubbles foreach (var item in HudManager.Instance.Chat.chatBubblePool.activeChildren.SelectIl2Cpp(c => c.GetComponent())) { SetChatPoolTheme(item); } } - // Apply theme to individual chat bubble internal static ChatBubble SetChatPoolTheme(ChatBubble asChatBubble) { - ChatBubble chatBubble = asChatBubble; + var chatBubble = asChatBubble; if (BAUPlugin.ChatDarkMode.Value) { - chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren(true).color = new Color(1f, 1f, 1f, 1f); + chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren(true).color = Color.white; chatBubble.transform.Find("Background").GetComponentInChildren(true).color = new Color(0.15f, 0.15f, 0.15f, 1f); - // Make dead player chat bubbles more transparent - if (chatBubble.transform.Find("PoolablePlayer/xMark") != null) + var mark = chatBubble.transform.Find("PoolablePlayer/xMark"); + if (mark != null && mark.GetComponentInChildren(true).enabled) { - if (chatBubble.transform.Find("PoolablePlayer/xMark").GetComponentInChildren(true).enabled == true) - { - chatBubble.transform.Find("Background").GetComponentInChildren(true).color = new Color(0.15f, 0.15f, 0.15f, 0.5f); - } + chatBubble.transform.Find("Background").GetComponentInChildren(true).color = new Color(0.15f, 0.15f, 0.15f, 0.5f); } } else { - chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren(true).color = new Color(0f, 0f, 0f, 1f); - chatBubble.transform.Find("Background").GetComponentInChildren(true).color = new Color(1f, 1f, 1f, 1f); + chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren(true).color = Color.black; + chatBubble.transform.Find("Background").GetComponentInChildren(true).color = Color.white; - // Make dead player chat bubbles more transparent - if (chatBubble.transform.Find("PoolablePlayer/xMark") != null) + var mark = chatBubble.transform.Find("PoolablePlayer/xMark"); + if (mark != null && mark.GetComponentInChildren(true).enabled) { - if (chatBubble.transform.Find("PoolablePlayer/xMark").GetComponentInChildren(true).enabled == true) - { - chatBubble.transform.Find("Background").GetComponentInChildren(true).color = new Color(1f, 1f, 1f, 0.5f); - } + chatBubble.transform.Find("Background").GetComponentInChildren(true).color = new Color(1f, 1f, 1f, 0.5f); } } @@ -292,7 +267,6 @@ internal static class ChatPatch [HarmonyPostfix] private static void FreeChatInputField_Awake_Postfix(FreeChatInputField __instance) { - // Enable extended character support for chat __instance.textArea.allowAllCharacters = true; __instance.textArea.AllowSymbols = true; __instance.textArea.AllowPaste = true; @@ -305,7 +279,6 @@ internal static class ChatPatch [HarmonyPostfix] private static void FreeChatInputField_UpdateCharCount_Postfix(FreeChatInputField __instance) { - // Update character counter with color coding int length = __instance.textArea.text.Length; __instance.charCountText.text = string.Format("{0}/118", length); __instance.charCountText.color = GetCharColor(length); @@ -313,14 +286,7 @@ internal static class ChatPatch private static Color GetCharColor(int length) { - // Color gradient: green -> yellow -> red as text length increases - Color[] colorGradient = - [ - Color.green, - Color.yellow, - Color.red - ]; - + Color[] colorGradient = [Color.green, Color.yellow, Color.red]; (float min, float max) lerpRange = (0f, 117f); return colorGradient.LerpColor(lerpRange, length); }