mirror of
https://github.com/D1GQ/BetterAmongUs.git
synced 2026-10-20 01:46:19 +00:00
Added linux comaptibility! Added null safety and cross-platform behaviour, improved BetterUser handshake.
This commit is contained in:
1 parent
c4baa42a23
commit
a162297997
6 files changed
+131
-151
No files matched your search
@@ -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()
|
||||
|
||||
@@ -11,9 +11,6 @@ using UnityEngine;
|
||||
|
||||
namespace BetterAmongUs.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the secure handshake process between BetterAmongUs clients using Diffie-Hellman key exchange.
|
||||
/// </summary>
|
||||
internal sealed class HandshakeHandler
|
||||
{
|
||||
[HideFromIl2Cpp]
|
||||
@@ -25,17 +22,11 @@ internal sealed class HandshakeHandler
|
||||
[HideFromIl2Cpp]
|
||||
private ExtendedPlayerInfo extendedData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initiates the wait period before sending the secret to another player.
|
||||
/// </summary>
|
||||
internal void WaitSendSecretToPlayer()
|
||||
{
|
||||
extendedData.StartCoroutine(CoWaitSendSecretToPlayer());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine that waits for player initialization before sending the secret.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resends the secret to the player if not already verified.
|
||||
/// </summary>
|
||||
internal void ResendSecretToPlayer()
|
||||
{
|
||||
if (!BAUPlugin.SendBetterRpc.Value) return;
|
||||
@@ -62,61 +50,47 @@ internal sealed class HandshakeHandler
|
||||
SendSecretToPlayer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the local client's public key and temporary key to another player.
|
||||
/// </summary>
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles receiving a secret from another player and generates a shared secret.
|
||||
/// </summary>
|
||||
/// <param name="reader">MessageReader containing the sender's public key and temporary key.</param>
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the hash of the generated shared secret back to the original sender for verification.
|
||||
/// </summary>
|
||||
/// <param name="tempKey">The temporary key received from the sender.</param>
|
||||
/// <param name="senderClientId">The client ID of the sender.</param>
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles receiving a secret hash from another player for verification.
|
||||
/// </summary>
|
||||
/// <param name="reader">MessageReader containing the temporary key and hash.</param>
|
||||
internal void HandleSecretHashFromPlayer(MessageReader reader)
|
||||
{
|
||||
int tempKey = reader.ReadInt32();
|
||||
int receivedHash = reader.ReadInt32();
|
||||
|
||||
_pendingVerificationData = (tempKey, receivedHash);
|
||||
TryHandlePendingVerificationData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to verify pending handshake data if all required information is available.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SharedSecretExchange instance for secure key exchange.
|
||||
/// </summary>
|
||||
internal SharedSecretExchange SharedSecret { get; set; } = new();
|
||||
}
|
||||
@@ -17,7 +17,7 @@ internal sealed class ExtendedPlayerInfo : MonoBehaviour, IMonoExtension<Network
|
||||
{
|
||||
try
|
||||
{
|
||||
HandshakeHandler = new(this);
|
||||
HandshakeHandler = new HandshakeHandler(this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -76,14 +76,17 @@ internal sealed class ExtendedPlayerInfo : MonoBehaviour, IMonoExtension<Network
|
||||
|
||||
if (AntiCheatInfo.RPCSentPS >= 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;
|
||||
|
||||
@@ -374,11 +374,8 @@ internal class PlayerInfoDisplay : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets lobby-specific information.
|
||||
/// Sets in-game specific information.
|
||||
/// </summary>
|
||||
/// <param name="newName">Reference to the player's name.</param>
|
||||
/// <param name="betterData">Extended player data.</param>
|
||||
/// <param name="sbTag">StringBuilder for tag text.</param>
|
||||
[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("<color=#0dff00>{1}{0}</color>+++",
|
||||
_cachedTranslations.BetterUser, verificationSymbol);
|
||||
}
|
||||
|
||||
sbTag.Append($"<color=#b554ff>ID: {_player.PlayerId}</color>+++");
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ internal static class PrivateLobbyPatch
|
||||
var sprite = __instance.HostPrivateButton.transform.Find("Inactive")?.GetComponent<SpriteRenderer>();
|
||||
if (sprite != null)
|
||||
{
|
||||
sprite.color = new Color(0.35f, 1f, 1f, 1f); // Corrected color assignment
|
||||
sprite.color = new Color(0.35f, 1f, 1f, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ChatBubble>();
|
||||
@@ -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<ChatBubble>();
|
||||
@@ -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("<color=#0dff00>{1}{0}</color>+++", Translator.GetString("Player.BetterUser"), betterData.IsVerifiedBetterUser || sourcePlayer.IsLocalPlayer() ? "✓ " : "");
|
||||
{
|
||||
sbTag.AppendFormat(
|
||||
"<color=#0dff00>{1}{0}</color>+++",
|
||||
Translator.GetString("Player.BetterUser"),
|
||||
betterData.IsVerifiedBetterUser || sourcePlayer.IsLocalPlayer() ? "✓ " : ""
|
||||
);
|
||||
}
|
||||
|
||||
if (BetterDataManager.BetterDataFile.SickoData.Any(info => info.CheckPlayerData(sourcePlayer.Data)))
|
||||
sbTag.Append($"<color=#00f583>{Translator.GetString("Player.SickoUser")}</color>+++");
|
||||
@@ -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("<size=75%>");
|
||||
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("</size>");
|
||||
|
||||
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<SpriteRenderer>().color = new Color(0.5f, 0.5f, 0.5f, 1f);
|
||||
chat.openKeyboardButton.transform.Find("OpenKeyboardIcon").GetComponent<SpriteRenderer>().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<SpriteRenderer>().color = new Color(1f, 1f, 1f, 1f);
|
||||
chat.openKeyboardButton.transform.Find("OpenKeyboardIcon").GetComponent<SpriteRenderer>().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<ChatBubble>()))
|
||||
{
|
||||
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<TextMeshPro>(true).color = new Color(1f, 1f, 1f, 1f);
|
||||
chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren<TextMeshPro>(true).color = Color.white;
|
||||
chatBubble.transform.Find("Background").GetComponentInChildren<SpriteRenderer>(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<SpriteRenderer>(true).enabled)
|
||||
{
|
||||
if (chatBubble.transform.Find("PoolablePlayer/xMark").GetComponentInChildren<SpriteRenderer>(true).enabled == true)
|
||||
{
|
||||
chatBubble.transform.Find("Background").GetComponentInChildren<SpriteRenderer>(true).color = new Color(0.15f, 0.15f, 0.15f, 0.5f);
|
||||
}
|
||||
chatBubble.transform.Find("Background").GetComponentInChildren<SpriteRenderer>(true).color = new Color(0.15f, 0.15f, 0.15f, 0.5f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren<TextMeshPro>(true).color = new Color(0f, 0f, 0f, 1f);
|
||||
chatBubble.transform.Find("Background").GetComponentInChildren<SpriteRenderer>(true).color = new Color(1f, 1f, 1f, 1f);
|
||||
chatBubble.transform.Find("ChatText (TMP)").GetComponentInChildren<TextMeshPro>(true).color = Color.black;
|
||||
chatBubble.transform.Find("Background").GetComponentInChildren<SpriteRenderer>(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<SpriteRenderer>(true).enabled)
|
||||
{
|
||||
if (chatBubble.transform.Find("PoolablePlayer/xMark").GetComponentInChildren<SpriteRenderer>(true).enabled == true)
|
||||
{
|
||||
chatBubble.transform.Find("Background").GetComponentInChildren<SpriteRenderer>(true).color = new Color(1f, 1f, 1f, 0.5f);
|
||||
}
|
||||
chatBubble.transform.Find("Background").GetComponentInChildren<SpriteRenderer>(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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user