From 73465aa508a9faf7dc8a8ed1581a6423e982c8d9 Mon Sep 17 00:00:00 2001 From: wanderingpix Date: Thu, 11 Jun 2026 23:31:40 +0100 Subject: [PATCH 1/5] Chats API --- .../CustomChats/ExampleCustomChat.cs | 19 ++ .../CustomChats/ExampleCustomChat2.cs | 26 ++ MiraAPI.Example/ExampleAssets.cs | 1 + MiraAPI.Example/Resources/BlueChatBubble.png | Bin 0 -> 2956 bytes MiraAPI/CustomChats/CustomChat.cs | 126 ++++++++ MiraAPI/CustomChats/CustomChatManager.cs | 30 ++ MiraAPI/CustomChats/DefaultChat.cs | 24 ++ MiraAPI/Networking/CustomSendChatRpc.cs | 36 +++ MiraAPI/Networking/MiraRpc.cs | 5 + MiraAPI/Patches/ChatControllerPatch.cs | 276 +++++++++++++++++- MiraAPI/Patches/PlayerControlPatches.cs | 10 + MiraAPI/PluginLoading/MiraPluginInfo.cs | 9 + MiraAPI/PluginLoading/MiraPluginManager.cs | 18 ++ MiraAPI/Resources/ChatNormalBubble.png | Bin 0 -> 3222 bytes MiraAPI/Resources/DefaultChatIcon.png | Bin 0 -> 7086 bytes MiraAPI/Resources/DefaultChatIcon.png~ | Bin 0 -> 10265 bytes MiraAPI/Resources/NextButtonChat.png | Bin 0 -> 8500 bytes MiraAPI/Resources/NextButtonChat.png~ | Bin 0 -> 8686 bytes MiraAPI/Resources/NormalChatHover.png | Bin 0 -> 6161 bytes MiraAPI/Resources/NormalChatIdle.png | Bin 0 -> 6201 bytes MiraAPI/Resources/NormalChatOpen.png | Bin 0 -> 6228 bytes MiraAPI/Resources/PreviousButtonChat.png | Bin 0 -> 8478 bytes MiraAPI/Resources/PreviousButtonChat.png~ | Bin 0 -> 8658 bytes MiraAPI/Utilities/Assets/MiraAssets.cs | 37 ++- 24 files changed, 614 insertions(+), 3 deletions(-) create mode 100644 MiraAPI.Example/CustomChats/ExampleCustomChat.cs create mode 100644 MiraAPI.Example/CustomChats/ExampleCustomChat2.cs create mode 100644 MiraAPI.Example/Resources/BlueChatBubble.png create mode 100644 MiraAPI/CustomChats/CustomChat.cs create mode 100644 MiraAPI/CustomChats/CustomChatManager.cs create mode 100644 MiraAPI/CustomChats/DefaultChat.cs create mode 100644 MiraAPI/Networking/CustomSendChatRpc.cs create mode 100644 MiraAPI/Resources/ChatNormalBubble.png create mode 100644 MiraAPI/Resources/DefaultChatIcon.png create mode 100644 MiraAPI/Resources/DefaultChatIcon.png~ create mode 100644 MiraAPI/Resources/NextButtonChat.png create mode 100644 MiraAPI/Resources/NextButtonChat.png~ create mode 100644 MiraAPI/Resources/NormalChatHover.png create mode 100644 MiraAPI/Resources/NormalChatIdle.png create mode 100644 MiraAPI/Resources/NormalChatOpen.png create mode 100644 MiraAPI/Resources/PreviousButtonChat.png create mode 100644 MiraAPI/Resources/PreviousButtonChat.png~ diff --git a/MiraAPI.Example/CustomChats/ExampleCustomChat.cs b/MiraAPI.Example/CustomChats/ExampleCustomChat.cs new file mode 100644 index 00000000..b1f36ec8 --- /dev/null +++ b/MiraAPI.Example/CustomChats/ExampleCustomChat.cs @@ -0,0 +1,19 @@ +using MiraAPI.CustomChats; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.CustomChats; + +public class ExampleCustomChat : CustomChat +{ + public override string Name => "uwu"; + + public override Color ChatBackgroundColor => Color.yellow; + + public override LoadableResourceAsset ChatIcon => ExampleAssets.TeleportButton; + + public override bool CanSee() + { + return true; + } +} \ No newline at end of file diff --git a/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs b/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs new file mode 100644 index 00000000..58706b73 --- /dev/null +++ b/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs @@ -0,0 +1,26 @@ +using MiraAPI.CustomChats; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.Example.CustomChats; + +public class ExampleCustomChat2 : CustomChat +{ + public override string Name => "The chat for the mute"; + + public override Color ChatBackgroundColor => Color.gray; + + public override LoadableResourceAsset ChatIcon => ExampleAssets.ExampleButton; + + public override ChatButtonVisualAppearance ChatButtonAppearance => new(ExampleAssets.CallMeetingButton, ExampleAssets.ExampleButton, ExampleAssets.TeleportButton, ExampleAssets.BlueChatBubble); + + public override bool CanSee() + { + return true; + } + + public override bool CanSendMessage() + { + return true; + } +} diff --git a/MiraAPI.Example/ExampleAssets.cs b/MiraAPI.Example/ExampleAssets.cs index 903afd83..c3f6e80c 100644 --- a/MiraAPI.Example/ExampleAssets.cs +++ b/MiraAPI.Example/ExampleAssets.cs @@ -10,4 +10,5 @@ public static class ExampleAssets // Credit to EpicHorrors for the teleport button asset. public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png"); public static LoadableResourceAsset Banner { get; } = new("MiraAPI.Example.Resources.FortniteBanner.jpeg"); + public static LoadableResourceAsset BlueChatBubble { get; } = new("MiraAPI.Example.Resources.BlueChatBubble.png"); } diff --git a/MiraAPI.Example/Resources/BlueChatBubble.png b/MiraAPI.Example/Resources/BlueChatBubble.png new file mode 100644 index 0000000000000000000000000000000000000000..b7cbd3ec08765a3336e634fa8ebe273924c93865 GIT binary patch literal 2956 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 +/// abstract Class for creating custom chats. +/// +public abstract class CustomChat +{ + /// + /// Gets the name of the custom chat. + /// + public abstract string Name { get; } + + /// + /// Gets the which the chat background will use. + /// + public abstract Color ChatBackgroundColor { get; } + + /// + /// Gets the which is used for the chat icon. + /// + public abstract LoadableResourceAsset ChatIcon { get; } + + /// + /// Gets the which is used for changing the chat button's visual appearance. + /// + public virtual ChatButtonVisualAppearance ChatButtonAppearance { get; } = + ChatButtonVisualAppearance.DefaultAppearance; + + /// + /// Gets the which is played when a message is sent, if null, it will fall back to the default sound. + /// + public virtual LoadableAudioResourceAsset? MessageSound { get; } = null!; + + /// + /// Determines if the local player can view this chat. + /// + /// True if they can view it, false otherwise. + public abstract bool CanSee(); + + /// + /// Determines if the local player can send messages in this chat. + /// + /// True if they can send messages, false otherwise. + public virtual bool CanSendMessage() + { + return CanSee(); + } + + /// + /// Callback method for when a message is sent in this custom chat. + /// + /// The who sent the message. + /// the instance. + public virtual void OnMessageSent(PlayerControl sourcePlayer, ChatBubble bubble) + { + } + + /// + /// Callback method for when this chat is being opened. + /// + /// The instance. + public virtual void OnChatOpen(ChatController chat) + { + } + + /// + /// Callback method for when this chat is being closed. + /// + /// The instance. + public virtual void OnChatClose(ChatController chat) + { + } +} + +/// +/// a Class for defining how the chat button looks. +/// +public class ChatButtonVisualAppearance +{ + /// + /// Gets the inactive for the Chat Button. + /// + public LoadableResourceAsset InactiveSprite; + + /// + /// Gets the active for the Chat Button. + /// + public LoadableResourceAsset ActiveSprite; + + /// + /// Gets the opened for the Chat Button. + /// + public LoadableResourceAsset OpenedSprite; + + /// + /// Gets the notification dot for the Chat Button. + /// + public LoadableResourceAsset NotificationSprite; + + /// + /// Initializes a new instance of the class. + /// + /// The inactive sprite . + /// The active sprite . + /// The opened sprite . + /// The notification dot sprite . + public ChatButtonVisualAppearance(LoadableResourceAsset inactiveSprite, LoadableResourceAsset activeSprite, LoadableResourceAsset openedSprite, LoadableResourceAsset notificationSprite) + { + InactiveSprite = inactiveSprite; + ActiveSprite = activeSprite; + OpenedSprite = openedSprite; + NotificationSprite = notificationSprite; + } + + /// + /// The default appearance for the Chat Button. + /// + public static readonly ChatButtonVisualAppearance DefaultAppearance = new( + MiraAssets.NormalChatIdle, + MiraAssets.NormalChatHover, + MiraAssets.NormalChatOpen, + MiraAssets.ChatNormalBubble); +} diff --git a/MiraAPI/CustomChats/CustomChatManager.cs b/MiraAPI/CustomChats/CustomChatManager.cs new file mode 100644 index 00000000..bba1d7e7 --- /dev/null +++ b/MiraAPI/CustomChats/CustomChatManager.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using MiraAPI.PluginLoading; + +namespace MiraAPI.CustomChats; + +/// +/// Manages custom chats. +/// +public static class CustomChatManager +{ + /// + /// Gets a list of all registered s. + /// + public static readonly List Chats = [new DefaultChat()]; + + internal static bool RegisterCustomChat(Type type, MiraPluginInfo info) + { + if (!typeof(CustomChat).IsAssignableFrom(type)) + { + return false; + } + + CustomChat? chat = Activator.CreateInstance(type) as CustomChat; + if (chat == null) return false; + Chats.Add(chat); + info.InternalChats.Add(chat); + return true; + } +} diff --git a/MiraAPI/CustomChats/DefaultChat.cs b/MiraAPI/CustomChats/DefaultChat.cs new file mode 100644 index 00000000..c7dfe26d --- /dev/null +++ b/MiraAPI/CustomChats/DefaultChat.cs @@ -0,0 +1,24 @@ +using MiraAPI.CustomChats; +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.CustomChats; + +/// +/// The default . +/// +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class DefaultChat : CustomChat +{ + public override string Name => "Default Chat"; + + public override Color ChatBackgroundColor => Color.white; + + public override LoadableResourceAsset ChatIcon => MiraAssets.DefaultChatIcon; + + public override bool CanSee() + { + return true; + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/MiraAPI/Networking/CustomSendChatRpc.cs b/MiraAPI/Networking/CustomSendChatRpc.cs new file mode 100644 index 00000000..d94247ba --- /dev/null +++ b/MiraAPI/Networking/CustomSendChatRpc.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Linq; +using AmongUs.Data; +using AmongUs.GameOptions; +using Assets.CoreScripts; +using BepInEx.Unity.IL2CPP.Utils; +using MiraAPI.CustomChats; +using MiraAPI.Events; +using MiraAPI.Events.Vanilla.Gameplay; +using MiraAPI.Patches; +using MiraAPI.Utilities; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace MiraAPI.Networking; + +/// +/// Custom SendChat RPC adapted for custom chats. +/// +public static class CustomSendChatRpc +{ + [MethodRpc((uint)MiraRpc.SendChat, LocalHandling = RpcLocalHandling.Before)] + public static void RpcCustomSendChat( + this PlayerControl source, + string chatText, + int id) + { + CustomChat chat = CustomChatManager.Chats[id]; + if (!chat.CanSee()) return; + + HudManager.Instance.Chat.CustomAddChat(source, chatText, chat); + } +} diff --git a/MiraAPI/Networking/MiraRpc.cs b/MiraAPI/Networking/MiraRpc.cs index edc3a94e..e9917762 100644 --- a/MiraAPI/Networking/MiraRpc.cs +++ b/MiraAPI/Networking/MiraRpc.cs @@ -54,4 +54,9 @@ public enum MiraRpc : uint /// Custom RPC to replace Among Us CastVote. /// CastVote, + + /// + /// Custom RPC to replace Among Us SendChat. + /// + SendChat, } diff --git a/MiraAPI/Patches/ChatControllerPatch.cs b/MiraAPI/Patches/ChatControllerPatch.cs index e65be7f8..f83cced6 100644 --- a/MiraAPI/Patches/ChatControllerPatch.cs +++ b/MiraAPI/Patches/ChatControllerPatch.cs @@ -1,5 +1,21 @@ -using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using AmongUs.Data; +using HarmonyLib; +using InnerNet; +using MiraAPI.CustomChats; +using MiraAPI.LocalSettings; +using MiraAPI.PluginLoading; +using MiraAPI.Utilities; +using MiraAPI.Utilities.Assets; +using Reactor.Utilities.Extensions; +using TMPro; using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; +using Action = Il2CppSystem.Action; +using Object = UnityEngine.Object; namespace MiraAPI.Patches; @@ -10,7 +26,7 @@ namespace MiraAPI.Patches; /// #pragma warning restore SA1629 [HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))] -public static class ChatControllerPatch +public static class ChatControllerPastePatch { public static void Prefix(ChatController __instance) { @@ -33,3 +49,259 @@ public static void Prefix(ChatController __instance) __instance.freeChatField.textArea.SetText(__instance.freeChatField.textArea.text + clipboard); } } +[HarmonyPatch(typeof(ChatController))] +public static class ChatControllerCustomChatsPatches +{ + private static PassiveButton _previousButton = null!; + private static PassiveButton _nextButton = null!; + private static SpriteRenderer _chatIcon = null!; + private static TextMeshPro _text = null!; + public static CustomChat CurrentChat = null!; + private static Dictionary pages = new(); + + [HarmonyPatch(nameof(ChatController.Awake))] + [HarmonyPostfix] + public static void ChatController_Awake_Postfix(ChatController __instance) + { + pages = new(); + CreatePaginationControls(__instance); + SetUpObjectPools(__instance); + SetPage(__instance, CustomChatManager.Chats[0]); + } + [HarmonyPatch(nameof(ChatController.AddChatNote))] + [HarmonyPrefix] + public static bool ChatController_AddChatNote_Prefix(ChatController __instance, ref NetworkedPlayerInfo srcPlayer, ref ChatNoteTypes noteType) + { + __instance.CustomAddChatNote(srcPlayer, CustomChatManager.Chats[0], noteType); + return false; + } + + [HarmonyPatch(nameof(ChatController.Update))] + [HarmonyPrefix] + public static void ChatController_Update_Prefix(ChatController __instance) + { + if (!CurrentChat.CanSee()) SetPage(__instance, CustomChatManager.Chats[0]); + + bool canSend = CurrentChat.CanSendMessage(); + __instance.freeChatField.gameObject.SetActive(DataManager.Settings.Multiplayer.ChatMode == QuickChatModes.FreeChatOrQuickChat && canSend); + __instance.quickChatField.gameObject.SetActive(DataManager.Settings.Multiplayer.ChatMode == QuickChatModes.QuickChatOnly && canSend); + } + + public static ChatBubble GetPooledBubble(ChatController __instance, CustomChat chat) + { + if (!pages.TryGetValue(chat, out var pool)) return null; + if (pool.NotInUse == 0) + pool.ReclaimOldest(); + return pool.Get(); + } + + [HarmonyPatch(nameof(ChatController.AlignAllBubbles))] + [HarmonyPrefix] + public static bool ChatController_AlignAllBubbles_Postfix(ChatController __instance) + { + foreach (var pool in pages.Values) + { + float num1 = 0.0f; + var activeChildren = pool.activeChildren; + for (int index = activeChildren.Count - 1; index >= 0; --index) + { + var chatBubble = activeChildren[index].TryCast(); + if (chatBubble == null) continue; + float num2 = num1 + chatBubble.Background.size.y; + Vector3 localPosition = chatBubble.transform.localPosition; + localPosition.y = num2 - 1.85f; + chatBubble.transform.localPosition = localPosition; + num1 = num2 + 0.15f; + } + + float num3 = -0.3f; + var scroller = pool.GetComponent(); + scroller.SetYBoundsMin(Mathf.Min(0.0f, -num1 + scroller.Hitbox.bounds.size.y + num3)); + } + + return false; + } + + private static void CreatePaginationControls(ChatController __instance) + { + _text = Object.Instantiate(HudManager.Instance.UseButton.buttonLabelText, __instance.chatScreen.transform); + _text.color = Color.white; + _text.alignment = TextAlignmentOptions.MidlineLeft; + _text.fontSizeMax = 4f; + _text.fontSizeMin = 2f; + _text.overflowMode = TextOverflowModes.Overflow; + _text.transform.localPosition = new Vector3(-6.1f, -0.3f, -490f); + _text.text = "Default Chat"; + + _nextButton = Object.Instantiate(__instance.quickChatButton, __instance.quickChatButton.transform.parent, true); + _nextButton.transform.localPosition += new Vector3(0, 3, 0); + _nextButton.name = "UpButton"; + _nextButton.transform.GetChild(0).gameObject.GetComponent().sprite = + MiraAssets.NextButtonChat.LoadAsset(); + _nextButton.GetComponent().size /= new Vector2(1, 2); + _nextButton.OnClick = new Button.ButtonClickedEvent(); + var customChats = CustomChatManager.Chats.Where(x => x.CanSee()).ToList(); + _nextButton.OnClick.AddListener( + (UnityAction)(() => + { + int id = customChats.IndexOf(CurrentChat); + id++; + if (id > customChats.Count - 1) + { + id = 0; + } + var chat = customChats[id]; + SetPage(__instance, chat); + })); + + _previousButton = Object.Instantiate(_nextButton, __instance.chatScreen.transform, true); + _previousButton.transform.localPosition -= new Vector3(0, 1, 0); + _previousButton.name = "LeftArrowButton"; + _previousButton.transform.GetChild(0).GetComponent().sprite = MiraAssets.PreviousButtonChat.LoadAsset(); + _previousButton.OnClick = new Button.ButtonClickedEvent(); + _previousButton.OnClick.AddListener( + (UnityAction)(() => + { + int id = customChats.IndexOf(CurrentChat); + id--; + if (id < 0) + { + id = customChats.Count - 1; + } + var chat = customChats[id]; + SetPage(__instance, chat); + })); + + _chatIcon = new GameObject("CurrentChatIcon").AddComponent(); + _chatIcon.gameObject.transform.SetParent(_nextButton.transform.parent); + _chatIcon.gameObject.layer = LayerMask.NameToLayer("UI"); + _chatIcon.transform.localPosition = _nextButton.transform.localPosition - new Vector3(0, 0.5f, 0); + _chatIcon.transform.localScale = Vector3.one / 2f; + } + + private static void SetUpObjectPools(ChatController __instance) + { + foreach (var chat in CustomChatManager.Chats) + { + var pool = Object.Instantiate(__instance.chatBubblePool, __instance.chatBubblePool.transform.parent, true); + pool.GetComponent().active = true; + pages.Add(chat, pool); + } + __instance.chatBubblePool.gameObject.SetActive(false); + } + + private static void SetPage(ChatController __instance, CustomChat chat) + { + if (CurrentChat != null!) CurrentChat.OnChatClose(__instance); + CurrentChat = chat; + _text.text = CurrentChat.Name; + _chatIcon.sprite = CurrentChat.ChatIcon.LoadAsset(); + + __instance.StartCoroutine(Effects.ColorFade(__instance.backgroundImage, __instance.backgroundImage.color, CurrentChat.ChatBackgroundColor, 0.4f)); + __instance.freeChatField.gameObject.SetActive(chat.CanSendMessage()); + __instance.quickChatField.gameObject.SetActive(chat.CanSendMessage()); + + __instance.chatButton.activeSprites.GetComponent().sprite = + CurrentChat.ChatButtonAppearance.ActiveSprite.LoadAsset(); + __instance.chatButton.inactiveSprites.GetComponent().sprite = + CurrentChat.ChatButtonAppearance.InactiveSprite.LoadAsset(); + __instance.chatButton.selectedSprites.GetComponent().sprite = + CurrentChat.ChatButtonAppearance.OpenedSprite.LoadAsset(); + foreach (var page in pages.Values) + { + page.gameObject.SetActive(false); + } + + if (!pages.TryGetValue(CurrentChat, out var pool)) return; + pool.gameObject.SetActive(true); + chat.OnChatOpen(__instance); + } + + public static void CustomAddChat(this ChatController __instance, PlayerControl sourcePlayer, string chatText, CustomChat chat, bool censor = true) + { + if (!sourcePlayer || !PlayerControl.LocalPlayer) + return; + if (!pages.TryGetValue(chat, out var page)) return; + NetworkedPlayerInfo data1 = PlayerControl.LocalPlayer.Data; + NetworkedPlayerInfo data2 = sourcePlayer.Data; + if (data2 == null || data1 == null || (data2.IsDead && !data1.IsDead)) + return; + ChatBubble pooledBubble = GetPooledBubble(__instance, chat); + try + { + pooledBubble.transform.SetParent(page.transform.GetChild(0)); + pooledBubble.transform.localScale = Vector3.one; + int num = sourcePlayer == PlayerControl.LocalPlayer ? 1 : 0; + if (num != 0) + pooledBubble.SetRight(); + else + pooledBubble.SetLeft(); + bool didVote = MeetingHud.Instance && MeetingHud.Instance.DidVote(sourcePlayer.PlayerId); + pooledBubble.SetCosmetics(data2); + __instance.SetChatBubbleName(pooledBubble, data2, data2.IsDead, didVote, PlayerNameColor.Get(data2)); + if (censor && DataManager.Settings.Multiplayer.CensorChat) + chatText = BlockedWords.CensorWords(chatText); + pooledBubble.SetText(chatText); + pooledBubble.AlignChildren(); + __instance.AlignAllBubbles(); + if (!__instance.IsOpenOrOpening && __instance.notificationRoutine == null && chat.CanSee()) + { + __instance.chatNotifyDot.sprite = chat.ChatButtonAppearance.NotificationSprite.LoadAsset(); + __instance.notificationRoutine = __instance.StartCoroutine(__instance.BounceDot()); + } + if (num != 0 || __instance.IsOpenOrOpening) + return; + if (chat.CanSee()) + { + var audio = chat.MessageSound != null! ? chat.MessageSound.LoadAsset() : __instance.messageSound; + SoundManager.Instance.PlaySound(audio, false).pitch = + (float)(0.5 + sourcePlayer.PlayerId / 15.0); + } + __instance.chatNotification.SetUp(sourcePlayer, chatText); + + page.activeChildren.Add(pooledBubble); + chat.OnMessageSent(sourcePlayer, pooledBubble); + } + catch (Exception ex) + { + ChatController.Logger.Error(ex.ToString()); + page.Reclaim(pooledBubble); + } + } + + public static void CustomAddChatNote(this ChatController __instance, NetworkedPlayerInfo srcPlayer, CustomChat chat, ChatNoteTypes noteType) + { + if (!pages.TryGetValue(chat, out var page)) return; + if (srcPlayer == null) + return; + ChatBubble pooledBubble = GetPooledBubble(__instance, chat); + pooledBubble.SetCosmetics(srcPlayer); + pooledBubble.transform.SetParent(page.transform.GetChild(0)); + pooledBubble.transform.localScale = Vector3.one; + pooledBubble.SetNotification(); + if (noteType == ChatNoteTypes.DidVote) + { + int rem = MeetingHud.Instance.GetVotesRemaining(); + pooledBubble.SetName(TranslationController.Instance.GetString(StringNames.MeetingHasVoted, srcPlayer.PlayerName, rem), false, true, Color.green); + } + pooledBubble.SetText(string.Empty); + pooledBubble.AlignChildren(); + __instance.AlignAllBubbles(); + if (!__instance.IsOpenOrOpening && __instance.notificationRoutine == null && chat.CanSee()) + { + __instance.chatNotifyDot.sprite = chat.ChatButtonAppearance.NotificationSprite.LoadAsset(); + __instance.notificationRoutine = __instance.StartCoroutine(__instance.BounceDot()); + } + if (srcPlayer.Object.AmOwner) + return; + if (chat.CanSee()) + { + var audio = chat.MessageSound != null! ? chat.MessageSound.LoadAsset() : __instance.messageSound; + SoundManager.Instance.PlaySound(audio, false).pitch = + (float)(0.5 + srcPlayer.PlayerId / 15.0); + } + page.activeChildren.Add(pooledBubble); + + chat.OnMessageSent(srcPlayer.Object, pooledBubble); + } +} diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index 946b6f26..b4bf3244 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -1,11 +1,13 @@ using System.Globalization; using System.Linq; using HarmonyLib; +using MiraAPI.CustomChats; using MiraAPI.Events; using MiraAPI.Events.Vanilla.Gameplay; using MiraAPI.Events.Vanilla.Player; using MiraAPI.Hud; using MiraAPI.Modifiers; +using MiraAPI.Networking; using MiraAPI.Utilities; using MiraAPI.Voting; using Reactor.Utilities; @@ -165,4 +167,12 @@ public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance) } } } + [HarmonyPrefix] + [HarmonyPatch(nameof(PlayerControl.RpcSendChat))] + // ReSharper disable once InconsistentNaming + public static bool RpcSendChatPrefix(PlayerControl __instance, ref string chatText) + { + __instance.RpcCustomSendChat(chatText, CustomChatManager.Chats.IndexOf(ChatControllerCustomChatsPatches.CurrentChat)); + return false; + } } diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 7168a706..838c5d75 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using BepInEx; using BepInEx.Configuration; +using MiraAPI.CustomChats; using MiraAPI.GameModes; using MiraAPI.GameOptions; using MiraAPI.Hud; @@ -40,6 +41,11 @@ internal MiraPluginInfo(IMiraPlugin miraPlugin, PluginInfo info) /// public IReadOnlyCollection Options { get; private set; } = null!; + /// + /// Gets a read only collection of this plugin's custom chats. + /// + public IReadOnlyCollection Chats { get; private set; } = null!; + /// /// Gets a read only dictionary of Role IDs and the RoleBehaviour object they are associated with. /// @@ -63,6 +69,7 @@ internal void SavePublicCollections() Options = [..InternalOptions]; Roles = new ReadOnlyDictionary(InternalRoles); Buttons = [..InternalButtons]; + Chats = [..InternalChats]; } internal List InternalPresets { get; } = []; @@ -79,6 +86,8 @@ internal void SavePublicCollections() internal List InternalButtons { get; } = []; + internal List InternalChats { get; } = []; + /// /// Gets the plugin's ID, as defined in the plugin's BepInEx metadata. /// diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 26a881f4..e41d2111 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -8,6 +8,7 @@ using BepInEx.Unity.IL2CPP; using HarmonyLib; using MiraAPI.Colors; +using MiraAPI.CustomChats; using MiraAPI.Events; using MiraAPI.GameEnd; using MiraAPI.GameOptions; @@ -116,6 +117,11 @@ internal void Initialize() continue; } + if (RegisterCustomChat(type, info)) + { + continue; + } + RegisterColorClasses(type); RegisterKeybinds(type, plugin); } @@ -438,4 +444,16 @@ private static void RegisterKeybinds(Type type, BasePlugin source) Error($"Failed to register keybind class {type.Name}: {e}"); } } + private static bool RegisterCustomChat(Type type, MiraPluginInfo info) + { + try + { + return CustomChatManager.RegisterCustomChat(type, info); + } + catch (Exception e) + { + Error($"Failed to register custom chat {type.Name}: {e}"); + return false; + } + } } diff --git a/MiraAPI/Resources/ChatNormalBubble.png b/MiraAPI/Resources/ChatNormalBubble.png new file mode 100644 index 0000000000000000000000000000000000000000..82502ad1292d4e4817f0d0977d956839b8d5e334 GIT binary patch literal 3222 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 IM6N<$f+eIX9smFU literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/DefaultChatIcon.png b/MiraAPI/Resources/DefaultChatIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..aedef098e935ea397b9dc5cafcd0686b3cf40bd9 GIT binary patch literal 7086 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 Yf0?+I)x`*gWdHyG07*qoM6N<$g4Ggnb^rhX literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/DefaultChatIcon.png~ b/MiraAPI/Resources/DefaultChatIcon.png~ new file mode 100644 index 0000000000000000000000000000000000000000..c2cd626ac8bf3072072b725cf586d9524bbd0cbf GIT binary patch literal 10265 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 Ls>)PKn}+@moHua< literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/NextButtonChat.png b/MiraAPI/Resources/NextButtonChat.png new file mode 100644 index 0000000000000000000000000000000000000000..507ccfc533151dcb44237eb0967fa2ea7e2e0686 GIT binary patch literal 8500 zcmV-4AL2 zwMAvbKG>eq+D_Zub`Q>+o;l(fb!?Z@t&YoRv$TpJF32MLPJlpI1A&A<2wBL!LbjUo z|Hy+%>Q#O3RlVAh?{`j}BIMP3sqfwYyWeuZd%eiBh$5olMC@H4a0N9)R6@kwJpwN` z{ww7K4S{F%2wfm>1rT^%kI*IDTKMiJa#LZ;30in^fvb@dw5jmjPVnv`a!I`K(vqhZ zf);oRroxs4FL`^Hck5j(^1k9W6}Uj)iAaKn$fcJ{$MCKaxe&LYzvDJycecodxCH|Z zmy6w*B8R|d5Yg;D<8FaqAYdx?%|x`zS<7%bC$+95@P$NlKM^GnQIM0aEfBZ^u=lv0 z&o z;0Pe_yu3X2E}^mT9WHdOMXss9O@%EdXyM5Pjs}8;BQf>lroz`q@D3Hamh0r230x8{ z8dvhvLeK(t!Bp6i;3aQwXSW_>0(99|X}ONBmB=*_cwcdw3S1y?M&1c>16}epk7vdK57c>^Trb4%;yKc9@XUv#EvuDrNtXqz*)77h2*{ig) zl*-D=*wfT#G?JH>7wL35>gwuZKX-O^wzapn*SEE`T{ro?v8$`gOZb{fxh|X8``vK6J5G=i3z*4wN=fH2_#}KfxwKwz(5*2 zcrc}>r_;=tGilzuc{F0g2*oW0o0jI7qNAhP=WGzVLucKsfeX(kqP0X+O+-dDuc)Xf zOUbg3~bD@)Zi)%<jd{ zTW065ci27b--b4W&Y(Bw&f$??P8WDbM+X!3m@#8)X&mtxAzFvnKw@H|@zqyfW%j8A zPZ7G)1^({4?-~sTgF|kTFn-4v|3GWkn3x#jn{U3UMCWcVbf*e@`SRt))YQ}gXD|az zApPn4rlh7ayXE1o9^GjI-@kvqF()UdU)}uvWXR&X@OwXZ-Z|a4Wy=<;T6Z_0J5Atk zzx}o_A_Ybyj^PW<9op!vx8Cvyp*v0BFTC)ATih6TFb?rIc!r3T+^1hZ`>b0C-P$@i z_HQETqUw}QSRcgtH*X!hs=9_2KK2-G-SVU2^+#*ye0(T0EQ0hAQ8Z}KAR03=krLt! zvcWKYgJZ@!QK_`-Rc8`H<0fTOTT3%lmR(lWXb8F9vu6)I`Q(%2 z@9%H+S!{4Y7_DBfr^d!cs;a6|vx80As#)GsZME0dH#w1~_6R&%O>T5-6@BE9N9g|h z@0X25ztP^&dAXv-`(GA4P6a3PRW%U0+-+)pRUBpB`U`@&o2b zLdT}6qM{-yFNbCogC_^N#zjO-?6>GEa+|%lv=bTLIlBUN>8QWFjI^O=)H_gvr*^ud9YmN=9eTUzN_T~n`XL=K9f z%$%7N78y+i`G*u=9~-M`YHR5y_oK#$ywCvzj^q<6_M=<$=6ywOvlo{VJ;)?^s4Xt5 z3i$8W-|$(z;uFP<0Z~8s+`mz3#uVAlWRD+B_fAbGo$mISTp-XI*jI8w9Gi?Qtu)Wr zYDh?;q?B~Jcbj<~PgZP@it1|2J$_%VTq&jagapdW$T0hzo!%S{7PH%! zgu4|CgRVNB0IC9CI9@#OMB(LWTfSSZsBtlo@#r%zT0Ay$%m|u1Rs^^x0c*Ucib~lS zQkog|_wyygpa`}KF?OVR7g3-b*kMC+D;+(1QT9WR{`y6Ff7we^SIrmt>#w~=Pe1jP z?0F=8Aq%X^RL`b*u8};s>IpZV7cZ--J@G$pzWGqzz8#8M@5$$vQj9^~_^?d2V3zy@ zoZvPP^!K}u(%$@YR9)Z1ytqVHS2xu)wb1#?6;ynsinVEIkjQ>zD@j9R==|mKUdw9| z{sDn(Lhji9oovF+8W68OES#oIo2sQZ@3ovJjmcGea?Y5c=0su1=xyJxQPhSDp85@q zNKUhOEV}6e>r_n8u4BiZx=_kJR$v)xb_si4u=qEMn(vJ_UPoe4 ztKM8?DX?C8wt4-@N<|Zl2TDGA|8Msp{+9>F;ghD{Yw@tmlo2c_L13Bk>U#R(#{*Q` z*sQpjFi!tr?=i|fbwTlG1igV>!agmLm2)=>%)1SY^fVW*b3 z`+%O*_9^GD{L*$xw)t7(1dO}SVzuP!|%AJ&yVtFz@ebg`t3J|gA);M_L zJYB16w0aDWZ+GTVMU8Eng$6pXPuQs?&~3+_3m)eS3t5ni9Gs!CC2r{&B3HdW&U>=# z-@P-F_ocwy4_Nl%AS*?zhx5oU#5Q94=O9f!}@cai}N&rWAOsLY#x{q?$Txn|_) z%>uV{ZIdVGaWent5Z*Cacg&&~gM36A7!fx}#H4e;{%0)ZXu;)*YTAADtQMylaA0dQ z?9~#mb8L_1v2oKTdny|BrI#nyl;bB(MwS#6a>x2((&hKnN~9%;wOng7w_T?LCv8u- zA;*Ceg>R#sN}c678`J*TgqKTDfzjX@E5-rqeS*>>m@b#=?G zvS<$So9#bdsQG#OGd#P+c2uMdx5o9hHZPT%QMhO-^1{Nx0NxctLit`=1i+Ys@Kd>j z1&(CTv9olsCz+vuj}p7(IXF;STB>PelBOcRbm@|L8p=Kd>-i#Y@3_5tN%*;g)q$;X*!>Y~KH0SYxXZp8 zf^9bJ&F5~J6ezRX&_3CQ)n_TcI45_2YUD6(5lrWnA>CZiTjMc&ob#SO5>jwc@F*EF46 z^eB!NT%>i|4=ZXXSAeCpRU7sx@>&B8MRsda+RGBA<-fE*OOe+!H#hTLu!1KUGMu62Hi-@|6G9YJVtAglQ5D1JO}f~}>ucgQCq94IKZ zR|P#;w;q#scXx9i8cLpI;Btj|-KqWtQa7#Hmdo~rI|`JPN=}G%Xjv&_r}@YF-7MYn zKt-{=D(K0&^OziPC*G7i$pG?}p&)#J1j}PciM;D*fkVIE)bvqI;9z=Y3dFe!>X#3%Tx3L9wsx;4@lAf$PkIADVA|m)J{R*CB5*M%XD^Pe6h~(Rvdx}cg+MDa!xeUr=MGN_w*HJSHC=8ym}Y6-u6D z5{TLEFQAaYN9(pzUO}Vh#jH$qJXQGtPh+)M*Xo@5eOqG@byBYg-r4~5$TgU4>Jf{SHN`5T8sh0|OaflCaE z2&D&SWHAvs8hmHG#te_6w4|XlGM<~?)8l}Bs#;1^(35rRF}b2yK?P4TQ}PQvcZuf6 znPU3%yPZxGcvM&j&Als&LW8V_K0|Cj$>CS_&6%>?&eX2o8Pu88s{9Kv=fdI^^t7o_QE^5JBVEScIOe->+bA0eA=Ob zEq$3Xb|n4ujtsk-B_TGNb@h4KMjZiS1u8u-gcG#RQWx(0O~)ob2&**ThuKE{8GwFX!Y?k_(DjV2tib zzz$U{o0-s}peLJ?sEBA1D%z+@zxgus7J9J=uB+j`0JK{QUgX&rEYr zEpjA&FDWTu?{c7K6OXgUkMIt$3ZlqXZ#n2zrF?v>)ONFnke~n-iaI@Wj6J8Z2?W=m z=Wxe_WSc&(2Fx?KD@dpbd$V`L)VY)Vn1sm4NZBcSp6;xnx@95oyu3X2F2|fXbGY~F z{ONqV$6#pil^garEYX5mAPB8Nz{)cV?zy#-%vIz}NT!AN=Wr{8*h6?IKf$LfKq3#= znY{w<>grM?^6c#FQX*Dcycay(X)1Ew6J_4KdE9$}arvnu`?<&Qz!C;hCLy_CX{BrT zpAR>17Zwx~Hz<};yuz2oWls$g5=I6)W1jN+46R;=P73|BFrLw-9 zIeL&fJNV1rFTL~<0%6R$!#v$tL({NUFmmKb%F4=O-rgDo`G+Ze@*SGoS0oq23lUtC z(h}Jw4QGQ8P0V~(*EiXGXjAknxfzJH zbf#F_82hs)_<8HY!^1CRWo5yOYdW(EHvt!jzPDzsP)V2l*nyo?S$0X&n~NLskMDQW znytAm+0Sgx*fnK@fu4HsF1HrAL{wNPJ#^0$dSH5%-Cg(yeXv7%2Wq)cTEXlF_M^4H zVdr^!w{icZQKLrjXXcc1OV>IgLLUOIFA0<5O3o1;9!^O~No?$0Gv``? zDxMi*hO@RsV#lUdb<4Y$Y{Rx#SB=M!R}Z17TXRpcjVAVvyDjnkKR>3*vdi3Nf8~`| zzRSqSz}jUm9ipp#;J^XeyLT^@mn+V(fW`dvM05ZmM^HGr2iSWEp>t_(Z@(qEeqgrKD3t^kD8aZQ=Tj4z}wF2^)~lysEB|TH9`rx0e^2wRQx< zhD1}4zaN#9S97=CN^#>z^5DtDM24~8oP;6KZ2YQZ$N~=zfup^g1;S^MhYrF<@=0Wx zg?+$I97R#VN!qabGw$gF0s_j`tXYGk1KC8>%X1bK6wvzh>zUBo+7$2MKpgl+B06Q$ zm|V#uz=8z}XxXx5+%LEN`_=Sx@_%xVvlrA8gJ+kzVt&3pZ0tTHI*ekYBbcGW@U$Q^ zWdsXcIZ{we=Pq6CwYkRI+lz+B4x;o?!&r)@{zUwcDE6Ad#%Gw4hudbtt>IOE}c11?n9qYeR^cC;B?>^L^%90z8E76-ZY!W#zy>L{N z`8>g2{_+?4?6c2Myv^z#faSEjecLH3=Wa!fp%u8{=&qTjLdL`wa-c;=gi>fwAnSfd zh6PhtNU);X)23j?`0hz*?A6?Qodr?obUKGDwYeL3cw)Sc%`=E(L!b+~=Ha11c8}pr zX}#V~4b3f7+t9*#4^6FYR9)A^iX3P$BW^E;cW9@cAEw`=K{@>pyGCGDvBs(rYxDsF(c>3ej zmyo~!3JVQl!p95w%p>)|Otidg9~c@qYIxwzpk0Q>*f$hG$ZJhQGu1Y>uqq`GGhPi% zt!^D9ck$e5TKnnW6y2V=bLVdN_xHzkLao-vdrhWG{Uw}y0TC_f=`tt+0$;LZ3G156 z2_2-RrO}x)XSnCXxcu3ruaQ37KBI62*ozGjM}!8mcT7aVlcQ^jo&kmi0(sD*g6G9v zeC*LiWl;2d>Y7^EJ0|oz4dhIC@0CAL?KS>ps-U2tqL!8xOua*Yy+jT#zIN?e`tZXK zxl7W0zM6?#2ZaGh`7@ju=fBy5I>4_(v;GVOtx{AK|;7wZcC;m9z-GCwDWochD zc6aGRg4mcJGw2XL9s+nE3YJEgAz}y`^CK8DH#WD@jeb~p<%|D) zQ<1>IqD6~#TN;bo5#`p)lbgC2)m?TxX%O^+7nalHy!qpgKTaQf@B!`Hw~uR+#ph1Z znor)Phn{|(d#on`L&gp!OS|Xb>4O6NnCOH2{crv3@8`>6;sgBg?#;&Zcwk_{%G=AE z{QZ2`3_iNWm`}jJaST=ujASHV-Oc~Qx*o>F-8k0E>(+Z0D$E*xiV&LXwXZS3k#GAkV`sHbcX)IFNLFYjX+K$t9!( zIIQd7$)czfrnGK{^>RzWG4b*76cG_Yn>KAy^!+iGIhA*S;)W$UYzuwYG`Png9i^PYKXSh78VK^78y;31Vu4A0)f-P^6$#Ozea5>&5Ey`k&&@+>(;Hv zY>WUjGiPdJ@C&#V<<=AA#^@_@hk0{=nD*S<-1Z9>F7&;wMGEo{QOWrNCicid+$u~0 zfm49@{`#}O)1J-Waw8i}Nk~XIQczF;k1N|v*V>EAiJUoRwXy8%L+$O<+1W{Yy&frL zWnENf+fLUi%4l$`VZe;l1?~qnn|!zGBiiugr&N1Q zU8&}Xh=@xC1qBxZ0s;{9Vzwp@f$!S2i`K1MN5_vJR};c&(TiIWxy|0Zs;Y__8yl?( z-QVAzrca;lzjEcu=8ldIRk@51>wa+iMyjj1M%`VV6g|Yi4MY(L>;)?fClBwVojBX1}$8; zkiA<9lF84{$Bg@h*49>=H&6Tc`cZOvCMA!}q?C-w6dD#`@BI`A7zk)VE<&U#L+CXU zo!E-OV5s~2^Uo{pyz@@%#IoqYZ4@}@75vio@3gi<0843zfHRB4;h|LMO-)V3Lx&D6 zX>4rNByio&KY)Tm^c1WQW07-Vkx>*77|gsqOZnTgW-NXd?O|#UDBT57H#OE%V_gl) zs@m15&HDb-sZ%R9Z{F;u*Xw0NIi)VT9Ri1_5!r?aH-2V{KH!Nnzvl!9p2v0~#jmKS zDC7M3^NnlQt~FLyR~xNR8W$J0Qq8%=vC8j*Xe-yX)#AmAD~(2@MdODqpbzK-da>bN zL6;CXbgF32i5$)ndgVR$-1C7AU(%L_5ub6P;SZE+zwyQ!&DK1dy9ivuSwc6y{r20R z64B0qVhG}U_P+;iMn*={?%liVJxSnB7rN1CEMB~L@%uz{Qi!1ciJ-sU)8ypj&J`Ysb=xhokN8E$c-h2Y#n zaL&39Sy@>|2s<}7SCOzK+`Wr#38ziQS)#bO*!b7K{?+*4gAW=L5)zC()l{8!!<}VV z;ySLmr$IqM#-yYq!y)~qOnc1^vvounc zl1`|huC9(QU%t$eMDWF334CWHcDQ-7y~tbV zeKRox5ep2`dF$A@kx&AleV{EVAqCEdKLUvTR3(KG*zMlOK8(HM4vj_z42*izj;8L;&sTcz6T17zw3C#qqg~*v!?Gd^VyTFst zSlmEUfdhNH>!#S%b?^u7pBE^sss5OiN0=D{Vj9Fv=A(4KX4fnWgOaDfARm(Z+X_pG}>&|RLOhX4Qo z+DSw~RF83rz#T1eAcQUunBz2oJ456`=mJ4+oGNfTHr6(3tJMr7+cT3N z&6_vbtEQ%gYHMrR)6{0Okxr*0gTX)+i-r9?Ffh>D*Vot9+uM8F?%!<|i$y2?9%`>K z)E;_$Xhc-H#d`PtGkee*NUyU|oXJGy)D%idNg=b@Op_)}B9qDF*7L`{8^2@64%)we zzboT)O}g%`&aP`B@HK1JFrmA$&Rt$!PJjOMpK1H{?NnV|&4k_4)1&6bgcGrsKro8% z@Nh~>N}}A{Tw1zxDXm<&lBP_VqPV4C)6yJMLP7%joC`wt=&ZXlaN$)%w3&#Sh{&eq z6(1jOTeWJHZS&^Mwx*^gn;MQEKWh3-{>zxUpIww#=t zac3~&O(0|G_h#qhFuUd9uO8iN0zY!(h^?roXiVMwv1G{NxA1#E_uaSHwsYrBr&{+n zp?gi>Z@lq_Fd_v;B#z+=%^%w6m%sdFKnUGy0)OeHm;B!c$&tT_qKm3aHer1b>)*U}@P?*lTJzLXv}@9*XSvDAEWN?i5dw8(WcxsOuH*e9!%VktwUr*f~?bOxTPW^p%+_uTL-h7kAaD1|9 z6GcWwjyj*D6A3rg(Xqr2gXoPhwtMV(Gk@ z^w5IY6doF)`n&>x1neg#V^B2(Ud4<&5-9`1a*HtwdLaq-U zJV?(x^9+TChK~9y5?l~QYcv|Eqoacw8XDAOut}+!b%VRD4oiKL6M4>{zzfyXMn|gX zcfRu-di2ppWn!XQY)& z=TY_)v#N6n1kNxg#nI%{M5=FYqwd~s#^IpLf?86dyL2n9YEkHKA|EX-Jv%hDRP&+xK!vt zrpQA}Wo<*)f4#OLXw&E4oSsRU*|~J>$_47}=~i_46DLkkRJ4%_3m3^AkM4Y1TbpLy++O6K@Z!x) zt(~=hc>g2wFW3LOq6PpNS+nT5fBJWtFfm>BGt1`BqD8sW6rWEZaF_Iy1PTiYrfap> z*=RU$NOKk}rkmF*6?flve*HD8rI41EHtKwmqtwcq+q*%mi%a{1E5=D#WR~!LhGW%7^kAC#e z4i9ys-1sOR+k5iaMD#5p_O1xbs5=>pmtLr-seSu*e^|6{+ZILVo0>C+p8oz%9Ui;% zo|!`6D(N4v(168CL1I{=2`LG&6cY5!%=<`k@)uC$Jlf-r9OKL`Vb81A zeqT}ZZP@S{3X3}R<|?-W8>VNM*Pop1XoB%T)$iW?&BKWQ<$-bdyu}YWJS;zZ3JXdQ zIHsRW}=38xHK+7Q_2-VCi4~t;55DgA7zOCrim`#x>gxmms3hr8d}i>JpV* zR~1<$umko9J9WeZk3He=-|X44QQ{8F_khz>uz8^5i_umzUOp0R>S^{}?9Y0T%GKmseP+iwV$Iff-O~yK~ zSJ*&<~u!2Qk)RE9L?=r zY)#x7>^*hS8Gpv?(Nm7Oh)0NsmRO9%EEY8jEd%a zX%PT38ib$96)f;12hUulYlFoM1$>m)EziM$nwlC-Ba<{0d3AO5=rWWeh&1v=-q-(4 z_LP9Ath$bcpZbCwN6u=t7C+WeVz=1%gGWV0#oaQo8lkDkYinz{A~(i5@2Ue==xs49<{Z#++nX8G!=PQR~L6?JuHI1yNOnz znt@DhLkoTU^-=2V9#$vcXI!qjNq^pZ(r4EX1eDlqL==Bgxv$UpxP-M4uTYEXee)}&i&{E{hU0q## z7p&k(h6*SC(oOXyUmrV1JCC3D<|SPMx##cyGEiNo=~AKJg7V4lZ#~FnusuO^WH{UL)l+vVJ`#w_2^-hlY|T8MuCH^uAML4U}&BboX(V8}2EvrBqgWibuDV zLUo!yZau)tJ&#uu+pB_}tUr&*0e9j}$&(DAZW%U&AB$jl3?-5GpDy+2&s&%~jR_nq z&d+AG!aYSxdl!AS=LGFJIsOt`V5ds!>?!HV`tz7P-efZISNat^$t14bZ5cgWO|tjVdgBtLuDDgSzLKE)ZMw7kqHP?GBH&vq-S|2|ewW2Z`bvi>|K zpPZ7C!gUo&o@5e;*&ZukBZJ><*+a#pl^*{|=A<}UdH?9#W$enia_Ky$C4{vAo^A8) z6D&$jsfP<>RN1Ky#^l@xH5J5%hlg`34Jdh%NnnYlqpO#UQQSHdehUm9*<7X9YQ6Fu zC&b6n3K@Y*B$}e>+e-?V2t5scGhQ<$r_#*Ki8M8hTi-L_fPJc3N>tF3_3JUYqFF%& zPcnN-$^v(Z=G3K1dgqIMUK4nHOcbrSzks46oQFO`Y~L#4@9d!!2#Li*1#9c5Y%`Wn zYoAJbvRaSHwe;l5o*2?>>1!T{q56uz}$d4=tj|aBdbdlT|1c94OqVZ=v`8a=@zs$DUkIEiY0maEY9ZiL~m0`N|&78iE*agzjP(6Ue|W z)uu(??%}T(MMXtv8g#&2*H(ksZ00Wbc6F$aWJ9+X-qGJdnX6;_;(&& zLM`Pp)LA(~+QC zgzm4uv-jjhj|R4kWWlVd^jG)hx!o-3DG98r&#N}-2@oq#AAljuut)p--<8tQ#^34J zY&L6JP(f3XXJlk>_ZaSI<3_&9f@O+h8t)jQCFp`;g2@{g^m<7SWvPj`xvsmC)cIdVJXer+aRD0z4d*Oi1|yus_(Lt8^=vlbSw#I(OKs z22DMALP7$Chle{Y95FD^@6xzj0`#0YJXks$I7@#{mv|u(pyeQY5 zQ>X$0zZn2E1YmctKUcv}weOB|{Td90JITq(u5C8&08K>>Qd3j8@#`0hkGk4UtXc}{ zp0#!IXDPjjC-xctYb$6#pi5579+u|f-KfgrR70W05I{oqh0nXf3Cn?-9LE#mGF z;tsLV`~si80EIkYXYLBzVzDR^d0}B;4H4U0d>B04X)1Ew6J_PfmE3!QaruQ)N4UrF zz!C;ZCZV`sO}%gP&nKI?3k!-#O*FCK7KEM#8B{-qJp$$d)~>jp3g=AM{QSsGKuouwDkvym-rgCdB_}C& z{=J&qR}>e-3lUuNW@fM?4R3=GP0V~ZwRO6D>EIv(^O{)y_E+x9n?X4lX4-r5l4dI% zvok08&CkUI>dA|hbg5F?82iiT_;u@JV`Iw;3JT!GHJw?7n}7>M-&-?RsHDq&=IB1E zudUYf=HkZu@yi4B>8|5G$!B(F?3z8rOwWD$e!mvDM0`v%J^tVVdTeol+g*5*G14Qw z1GSXb++ubE`_Wq9u=CnK|VQ>DoX<=tIEuWnyw%$vI+UV<|H;lTETk zM2s#H7!?&ozyJO3xu@#s>ZIB8inzzA1>+~AIO1ltb@s3>uP2Hbjs%_@`8SG70THnr z@8yManTPb&%(+%z7tg#IlUduMuw!SBy5(I=wjnLnSL1Qi)kA3NuH)xfqKUiXZby9i zr{7b3?R9Rmzw*i}U*zTGVeN954$)OVdh{qAI&_HY>J-;lz+(PZB036@BPbl*1LPh; z=v?~x`i6v_nmYQqU^biSi_@XIW1u*noQG4Xqv2 z(|d>XIvty}_5_nABv3?X2vyZJaZ_)lxN#JD2xOAuVpwoa`h)~FepNDLfd_}c(O%vH z;j^ei2VtZ5Br46qK42%FqN4OXeYNRb?&-tA!fHSL^ivcaC?uj`p0l*Hl(uf&%7otA zt2l=Pap2pC=z>dQawU@ht5>h4_3PJjf83rgH_`K1|G_=ZU9g`RJiF8t3keQlWA_OO zF_e;EVulLC(}Mi$DJ*d1RB0t$slGWZxkj(o(d3jw%AGcem1r7Eq)mutuLW#eTQL~P zHas@U7#U8pr%qyBbhL%1fLE?>X{WlTR=U;H&c@ols3^O4>nDnS;=>O=j6J9dq9ejt_d6~ol47DF741E34`z%Xm^YKXx_WN2APR%Q;E|;^e*+IsjMtGogGe?6 zx}a+w8y(^H7~Y11)C6-R0zgom75_zawpMaB%ac zO`Fi|y>H|--4!^HL~cL%rf6nQTDQYYx}3;^f`aJBKmIZO@P|L-w$ulCL!zmAf-cI6^%e&;ueZqJGpEB1tjh9aF%tM&0=lj%}_2`67oL@x|> z859A5zwp8ftZObObTD(~OuBUG68C%K*zU zCUSW3&6_vV+i$`oa+DX1>@H(e1RL{X1|7o3LjVti#Mo$;3qAw_sGQQ+*2x455kt_JAHkTp zqpOGRjKQ5(KKg&JD-t+(_St6-I2wz)5#=__liRx()m?TxX%O^+7nalHy!q2lKTU7F z^%fmIe3)yKl~*p%r+@f0J^uU;xW@((Fl1yfIm(`cr;i8=VWN)+4ITPBG$feC#D|6A zUC+k!cwk_{O0UyXXh;y7!AG|k^9lHG9D~&Z8`&t>aQFXVT@PboD~{FahTbjMVb*42 z<)8X37Shq(e?tf+`+ox#)?LPb-{~75tIbMQtBw6095Z0CvYBaYWehQ6j114*bLYH-GK5 z*QlsS*?Rb?L%XT1rHP*W-ak>4(fg5Wz5qhN3v2pc84(|Az8`)@2dV2b)($Z3RT-k2A)?Qprg%I{fdMiajVLKA8`BO}tXN^#xN&1=dwaXGT>yw!_x<$YJ=A!smXcD;<7TWb@IR1j z^2NsA(N`b8LoJQ!wrVz+Ox2~OrR8B^VF-E|wI&XM@87?lwrtr#XV0Eh6T<4yi#rmz z%ig@9p@BL&I-CkUG&GbJFJ2t_!3Q67_4oIys%3;&_hWmuQEPJ}SuF#UFu}|XL=gzw z1uG2aPadXyf7wi1-g{F~vdKuo!^1m2|NQgw+1c6ioe6y7#*IwqF3hNp8jDLf3LbVT zI0j%DZOM`)v}Vm3_U=N=IrQKz3LFdzei`{aon;7MDGd>Dqaty5 zC>45VXJ_Tai4&_jIyy87Tn`BiqsS;DMH*vRvy6ufO-gt+I_Kg=q5WjQmJ#h2#@;VP3IM5bI0{6PmZ8lrw+O=!nB%<>|1Y=JG zWA&b9Wn~S#|Ni^Vw9W805ALfmy2POsD^_gO>2z3(9J9tDep_JQ+mw_P%ddX*E3UW$ zLEygd=#2jU{-$T2eRgkncsNST2vLYO@CzH5K7IPYx^?SdpJPhkJ`=jci4!N<)~s1` zDk&)m**ku5v&HuqKlfqo+`0V__VMG#6$x9y-@E9J@Y-aYBg)ImTV8(o<(kEd7uP2x zC3Ob}2P0ZRh)%SDCN>Zn8aj}cmUer|k|li`Hf*q6y?RxZu>DEk&VpfhK8`7*>uhUlL!odOg=H{VhyA-Z|JLgbx`?Q#(1<9bDJUp7BrGB#DmXYaSZ^?d z2L%O%>GXPCa7bu4ku6B4(+3+22E5yJIvryF^*X&yZ!iQI28%lc>2x~1PNzqPQ?Siu zwc+!6J*ysJu-dFv6o5c5kj-k-TWwbSZi`N*V~1L;mj1h+*I5AC%Vx6<;Cxn##cI&g zKty1qgK6PedCB-(A`O#T}6Yw}lRIL*&v+1F_rVXm^-ViS>gx zI(`Tk=~%{Qv*Dhio`76%1^z!GA)Req>+JI*;kLIIQ1gAc*o#}L?BZ+}U?*rIaG-_7 zx+Qe>q==9AwAhszmm~2@Pt_`&PUq6o)LXD}PmWPy(9VRc4B`=iYwgL0w5LVaeI%05?FB7O+Svx8T zfqPoyKnPtR7>(Bi?hTO(p$i1V@v6YRCGwF%M_qS-*lW>!7mODmZtQS^(#c!giA7h7 zC&%O?k-F~Kqp6OF+Jx8z;{bbcV~;Yt&JHG@cm+?+t~*1Dyh5^r^m3FGArJ&2QiLSR zBug)ZMteQ?!B--e5MmdMHT*{0622C>Be6?fU3hmvAYh-@ASE)vd;7$5kHlX@K2q%V z9^O88StM8pe2rY9l6{7v-z75no5<~v6SR_4NrAwV$fX?nov`isU+~X+8#}=)-~a#s M07*qoM6N<$f*%DRmH+?% literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/NormalChatHover.png b/MiraAPI/Resources/NormalChatHover.png new file mode 100644 index 0000000000000000000000000000000000000000..e6d7a5045ffe94da940d7db0943cea574958a0f2 GIT binary patch literal 6161 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/NormalChatIdle.png b/MiraAPI/Resources/NormalChatIdle.png new file mode 100644 index 0000000000000000000000000000000000000000..42fc51b49570923c8bb3eef70e30b42b6b623cbe GIT binary patch literal 6201 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 X$5;;iW8h~000000NkvXXu0mjf^Tp#3 literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/NormalChatOpen.png b/MiraAPI/Resources/NormalChatOpen.png new file mode 100644 index 0000000000000000000000000000000000000000..62d7b26ef53272468aa269f93b7ec5bf7638677e GIT binary patch literal 6228 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Resources/PreviousButtonChat.png~ b/MiraAPI/Resources/PreviousButtonChat.png~ new file mode 100644 index 0000000000000000000000000000000000000000..5ec18b1900b4ea6a09cb6935d90ecec6d86a0c04 GIT binary patch literal 8658 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 literal 0 HcmV?d00001 diff --git a/MiraAPI/Utilities/Assets/MiraAssets.cs b/MiraAPI/Utilities/Assets/MiraAssets.cs index bb91416c..162e4e47 100644 --- a/MiraAPI/Utilities/Assets/MiraAssets.cs +++ b/MiraAPI/Utilities/Assets/MiraAssets.cs @@ -71,7 +71,7 @@ static MiraAssets() public static LoadableResourceAsset NextButton { get; } = new("MiraAPI.Resources.NextButton.png"); /// - /// Gets the Mira settings icon.. + /// Gets the Mira settings icon. /// public static LoadableResourceAsset SettingsIcon { get; } = new("MiraAPI.Resources.Settings.png"); @@ -79,6 +79,21 @@ static MiraAssets() /// Gets the highlighted Next Button sprite. /// public static LoadableResourceAsset NextButtonActive { get; } = new("MiraAPI.Resources.NextButtonActive.png"); + + /// + /// Gets the Next Button sprite used for switching custom chats. + /// + public static LoadableResourceAsset NextButtonChat { get; } = new("MiraAPI.Resources.NextButtonChat.png"); + + /// + /// Gets the Previous Button sprite used for switching custom chats. + /// + public static LoadableResourceAsset PreviousButtonChat { get; } = new("MiraAPI.Resources.PreviousButtonChat.png"); + + /// + /// Gets the sprite used for the default chat. + /// + public static LoadableResourceAsset DefaultChatIcon { get; } = new("MiraAPI.Resources.DefaultChatIcon.png"); /// /// Gets the Cog icon used in Role Settings Menu. @@ -134,4 +149,24 @@ static MiraAssets() /// Gets the sprite used for timed modifier file in TaskAdderGame. /// public static LoadableResourceAsset TimedModifierFile { get; } = new("MiraAPI.Resources.TimedModifierFile.png"); + + /// + /// Gets the sprite used for the chat button in its idle state. + /// + public static LoadableResourceAsset NormalChatIdle { get; } = new("MiraAPI.Resources.NormalChatIdle.png"); + + /// + /// Gets the sprite used for the chat button in its hover state. + /// + public static LoadableResourceAsset NormalChatHover { get; } = new("MiraAPI.Resources.NormalChatHover.png"); + + /// + /// Gets the sprite used for the chat button in its open state. + /// + public static LoadableResourceAsset NormalChatOpen { get; } = new("MiraAPI.Resources.NormalChatOpen.png"); + + /// + /// Gets the sprite used for the chat button notifications. + /// + public static LoadableResourceAsset ChatNormalBubble { get; } = new("MiraAPI.Resources.ChatNormalBubble.png"); } From 4369dea7dadf1a39b0ca3dc00732e5a49980d8bf Mon Sep 17 00:00:00 2001 From: wanderingpix Date: Fri, 12 Jun 2026 00:01:17 +0100 Subject: [PATCH 2/5] Update Mira example mod --- MiraAPI.Example/CustomChats/ExampleCustomChat.cs | 2 +- MiraAPI.Example/CustomChats/ExampleCustomChat2.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MiraAPI.Example/CustomChats/ExampleCustomChat.cs b/MiraAPI.Example/CustomChats/ExampleCustomChat.cs index b1f36ec8..912b3ed4 100644 --- a/MiraAPI.Example/CustomChats/ExampleCustomChat.cs +++ b/MiraAPI.Example/CustomChats/ExampleCustomChat.cs @@ -6,7 +6,7 @@ namespace MiraAPI.Example.CustomChats; public class ExampleCustomChat : CustomChat { - public override string Name => "uwu"; + public override string Name => "Example Chat"; public override Color ChatBackgroundColor => Color.yellow; diff --git a/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs b/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs index 58706b73..c628286f 100644 --- a/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs +++ b/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs @@ -21,6 +21,6 @@ public override bool CanSee() public override bool CanSendMessage() { - return true; + return false; } } From dfeb2a40e9bbf5b59893ca1eda91f5d72db184e1 Mon Sep 17 00:00:00 2001 From: wanderingpix Date: Fri, 12 Jun 2026 14:49:14 +0100 Subject: [PATCH 3/5] Adjustments --- .../CustomChats/ExampleCustomChat.cs | 4 +- .../CustomChats/ExampleCustomChat2.cs | 10 +- MiraAPI/CustomChats/AbstractCustomChat.cs | 101 ++++++++ MiraAPI/CustomChats/CustomChat.cs | 126 ---------- MiraAPI/CustomChats/CustomChatManager.cs | 8 +- MiraAPI/CustomChats/DefaultChat.cs | 6 +- MiraAPI/Networking/CustomSendChatRpc.cs | 2 +- MiraAPI/Patches/ChatControllerPatch.cs | 222 +++++------------- MiraAPI/PluginLoading/MiraPluginInfo.cs | 4 +- MiraAPI/Utilities/ChatControllerExtensions.cs | 193 +++++++++++++++ 10 files changed, 369 insertions(+), 307 deletions(-) create mode 100644 MiraAPI/CustomChats/AbstractCustomChat.cs delete mode 100644 MiraAPI/CustomChats/CustomChat.cs create mode 100644 MiraAPI/Utilities/ChatControllerExtensions.cs diff --git a/MiraAPI.Example/CustomChats/ExampleCustomChat.cs b/MiraAPI.Example/CustomChats/ExampleCustomChat.cs index 912b3ed4..5a5c1299 100644 --- a/MiraAPI.Example/CustomChats/ExampleCustomChat.cs +++ b/MiraAPI.Example/CustomChats/ExampleCustomChat.cs @@ -4,7 +4,7 @@ namespace MiraAPI.Example.CustomChats; -public class ExampleCustomChat : CustomChat +public class ExampleCustomChat : AbstractCustomChat { public override string Name => "Example Chat"; @@ -12,6 +12,8 @@ public class ExampleCustomChat : CustomChat public override LoadableResourceAsset ChatIcon => ExampleAssets.TeleportButton; + public override ChatButtonSprites Sprites => new(); + public override bool CanSee() { return true; diff --git a/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs b/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs index c628286f..2230df31 100644 --- a/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs +++ b/MiraAPI.Example/CustomChats/ExampleCustomChat2.cs @@ -4,7 +4,7 @@ namespace MiraAPI.Example.CustomChats; -public class ExampleCustomChat2 : CustomChat +public class ExampleCustomChat2 : AbstractCustomChat { public override string Name => "The chat for the mute"; @@ -12,7 +12,13 @@ public class ExampleCustomChat2 : CustomChat public override LoadableResourceAsset ChatIcon => ExampleAssets.ExampleButton; - public override ChatButtonVisualAppearance ChatButtonAppearance => new(ExampleAssets.CallMeetingButton, ExampleAssets.ExampleButton, ExampleAssets.TeleportButton, ExampleAssets.BlueChatBubble); + public override ChatButtonSprites Sprites => new() + { + ActiveSprite = ExampleAssets.CallMeetingButton, + InactiveSprite = ExampleAssets.ExampleButton, + OpenedSprite = ExampleAssets.TeleportButton, + NotificationSprite = ExampleAssets.BlueChatBubble, + }; public override bool CanSee() { diff --git a/MiraAPI/CustomChats/AbstractCustomChat.cs b/MiraAPI/CustomChats/AbstractCustomChat.cs new file mode 100644 index 00000000..6faeb652 --- /dev/null +++ b/MiraAPI/CustomChats/AbstractCustomChat.cs @@ -0,0 +1,101 @@ +using MiraAPI.Utilities.Assets; +using UnityEngine; + +namespace MiraAPI.CustomChats; + +/// +/// abstract Class for creating custom chats. +/// +public abstract class AbstractCustomChat +{ + /// + /// Gets the name of the custom chat. + /// + public abstract string Name { get; } + + /// + /// Gets the which the chat background will use. + /// + public abstract Color ChatBackgroundColor { get; } + + /// + /// Gets the which is used for the chat icon. + /// + public abstract LoadableAsset ChatIcon { get; } + + /// + /// Gets the which is used for changing the chat button's visual appearance. + /// + public abstract ChatButtonSprites Sprites { get; } + + /// + /// Gets the audio which is played when a message is sent, if null, it will fall back to the default sound. + /// + public virtual LoadableAsset? MessageSound { get; } = null!; + + /// + /// Determines if the local player can view this chat. + /// + /// True if they can view it, false otherwise. + public abstract bool CanSee(); + + /// + /// Determines if the local player can send messages in this chat. + /// + /// True if they can send messages, false otherwise. + public virtual bool CanSendMessage() + { + return CanSee(); + } + + /// + /// Callback method for when a message is sent in this custom chat. + /// + /// The who sent the message (can be null when a warning message is sent!). + /// the instance. + public virtual void OnMessageSent(PlayerControl? sourcePlayer, ChatBubble bubble) + { + } + + /// + /// Callback method for when this chat is being opened. + /// + /// The instance. + public virtual void OnChatOpen(ChatController chat) + { + } + + /// + /// Callback method for when this chat is being closed. + /// + /// The instance. + public virtual void OnChatClose(ChatController chat) + { + } +} + +/// +/// a Class for defining how the chat button looks. +/// +public class ChatButtonSprites +{ + /// + /// Gets the inactive for the Chat Button. + /// + public LoadableAsset InactiveSprite = MiraAssets.NormalChatIdle; + + /// + /// Gets the active for the Chat Button. + /// + public LoadableAsset ActiveSprite = MiraAssets.NormalChatHover; + + /// + /// Gets the opened for the Chat Button. + /// + public LoadableAsset OpenedSprite = MiraAssets.NormalChatOpen; + + /// + /// Gets the notification dot for the Chat Button. + /// + public LoadableAsset NotificationSprite = MiraAssets.ChatNormalBubble; +} diff --git a/MiraAPI/CustomChats/CustomChat.cs b/MiraAPI/CustomChats/CustomChat.cs deleted file mode 100644 index ddb12b64..00000000 --- a/MiraAPI/CustomChats/CustomChat.cs +++ /dev/null @@ -1,126 +0,0 @@ -using MiraAPI.Utilities.Assets; -using UnityEngine; - -namespace MiraAPI.CustomChats; - -/// -/// abstract Class for creating custom chats. -/// -public abstract class CustomChat -{ - /// - /// Gets the name of the custom chat. - /// - public abstract string Name { get; } - - /// - /// Gets the which the chat background will use. - /// - public abstract Color ChatBackgroundColor { get; } - - /// - /// Gets the which is used for the chat icon. - /// - public abstract LoadableResourceAsset ChatIcon { get; } - - /// - /// Gets the which is used for changing the chat button's visual appearance. - /// - public virtual ChatButtonVisualAppearance ChatButtonAppearance { get; } = - ChatButtonVisualAppearance.DefaultAppearance; - - /// - /// Gets the which is played when a message is sent, if null, it will fall back to the default sound. - /// - public virtual LoadableAudioResourceAsset? MessageSound { get; } = null!; - - /// - /// Determines if the local player can view this chat. - /// - /// True if they can view it, false otherwise. - public abstract bool CanSee(); - - /// - /// Determines if the local player can send messages in this chat. - /// - /// True if they can send messages, false otherwise. - public virtual bool CanSendMessage() - { - return CanSee(); - } - - /// - /// Callback method for when a message is sent in this custom chat. - /// - /// The who sent the message. - /// the instance. - public virtual void OnMessageSent(PlayerControl sourcePlayer, ChatBubble bubble) - { - } - - /// - /// Callback method for when this chat is being opened. - /// - /// The instance. - public virtual void OnChatOpen(ChatController chat) - { - } - - /// - /// Callback method for when this chat is being closed. - /// - /// The instance. - public virtual void OnChatClose(ChatController chat) - { - } -} - -/// -/// a Class for defining how the chat button looks. -/// -public class ChatButtonVisualAppearance -{ - /// - /// Gets the inactive for the Chat Button. - /// - public LoadableResourceAsset InactiveSprite; - - /// - /// Gets the active for the Chat Button. - /// - public LoadableResourceAsset ActiveSprite; - - /// - /// Gets the opened for the Chat Button. - /// - public LoadableResourceAsset OpenedSprite; - - /// - /// Gets the notification dot for the Chat Button. - /// - public LoadableResourceAsset NotificationSprite; - - /// - /// Initializes a new instance of the class. - /// - /// The inactive sprite . - /// The active sprite . - /// The opened sprite . - /// The notification dot sprite . - public ChatButtonVisualAppearance(LoadableResourceAsset inactiveSprite, LoadableResourceAsset activeSprite, LoadableResourceAsset openedSprite, LoadableResourceAsset notificationSprite) - { - InactiveSprite = inactiveSprite; - ActiveSprite = activeSprite; - OpenedSprite = openedSprite; - NotificationSprite = notificationSprite; - } - - /// - /// The default appearance for the Chat Button. - /// - public static readonly ChatButtonVisualAppearance DefaultAppearance = new( - MiraAssets.NormalChatIdle, - MiraAssets.NormalChatHover, - MiraAssets.NormalChatOpen, - MiraAssets.ChatNormalBubble); -} diff --git a/MiraAPI/CustomChats/CustomChatManager.cs b/MiraAPI/CustomChats/CustomChatManager.cs index bba1d7e7..504a7639 100644 --- a/MiraAPI/CustomChats/CustomChatManager.cs +++ b/MiraAPI/CustomChats/CustomChatManager.cs @@ -10,18 +10,18 @@ namespace MiraAPI.CustomChats; public static class CustomChatManager { /// - /// Gets a list of all registered s. + /// Gets a list of all registered s. /// - public static readonly List Chats = [new DefaultChat()]; + public static readonly List Chats = [new DefaultChat()]; internal static bool RegisterCustomChat(Type type, MiraPluginInfo info) { - if (!typeof(CustomChat).IsAssignableFrom(type)) + if (!typeof(AbstractCustomChat).IsAssignableFrom(type)) { return false; } - CustomChat? chat = Activator.CreateInstance(type) as CustomChat; + AbstractCustomChat? chat = Activator.CreateInstance(type) as AbstractCustomChat; if (chat == null) return false; Chats.Add(chat); info.InternalChats.Add(chat); diff --git a/MiraAPI/CustomChats/DefaultChat.cs b/MiraAPI/CustomChats/DefaultChat.cs index c7dfe26d..5c08ee1a 100644 --- a/MiraAPI/CustomChats/DefaultChat.cs +++ b/MiraAPI/CustomChats/DefaultChat.cs @@ -5,10 +5,10 @@ namespace MiraAPI.CustomChats; /// -/// The default . +/// The default . /// #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -public class DefaultChat : CustomChat +public class DefaultChat : AbstractCustomChat { public override string Name => "Default Chat"; @@ -16,6 +16,8 @@ public class DefaultChat : CustomChat public override LoadableResourceAsset ChatIcon => MiraAssets.DefaultChatIcon; + public override ChatButtonSprites Sprites => new(); + public override bool CanSee() { return true; diff --git a/MiraAPI/Networking/CustomSendChatRpc.cs b/MiraAPI/Networking/CustomSendChatRpc.cs index d94247ba..2eaf8f10 100644 --- a/MiraAPI/Networking/CustomSendChatRpc.cs +++ b/MiraAPI/Networking/CustomSendChatRpc.cs @@ -28,7 +28,7 @@ public static void RpcCustomSendChat( string chatText, int id) { - CustomChat chat = CustomChatManager.Chats[id]; + AbstractCustomChat chat = CustomChatManager.Chats[id]; if (!chat.CanSee()) return; HudManager.Instance.Chat.CustomAddChat(source, chatText, chat); diff --git a/MiraAPI/Patches/ChatControllerPatch.cs b/MiraAPI/Patches/ChatControllerPatch.cs index f83cced6..72ed565a 100644 --- a/MiraAPI/Patches/ChatControllerPatch.cs +++ b/MiraAPI/Patches/ChatControllerPatch.cs @@ -5,8 +5,6 @@ using HarmonyLib; using InnerNet; using MiraAPI.CustomChats; -using MiraAPI.LocalSettings; -using MiraAPI.PluginLoading; using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities.Extensions; @@ -14,7 +12,6 @@ using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; -using Action = Il2CppSystem.Action; using Object = UnityEngine.Object; namespace MiraAPI.Patches; @@ -52,54 +49,39 @@ public static void Prefix(ChatController __instance) [HarmonyPatch(typeof(ChatController))] public static class ChatControllerCustomChatsPatches { - private static PassiveButton _previousButton = null!; - private static PassiveButton _nextButton = null!; - private static SpriteRenderer _chatIcon = null!; - private static TextMeshPro _text = null!; - public static CustomChat CurrentChat = null!; - private static Dictionary pages = new(); + public static PassiveButton PreviousButton { get; private set; } = null!; + public static PassiveButton NextButton { get; private set; } = null!; + public static SpriteRenderer ChatIcon { get; private set; } = null!; + public static TextMeshPro Text { get; private set; } = null!; + public static AbstractCustomChat CurrentChat { get; set; } = null!; + public static Dictionary Pages { get; private set; } = new(); [HarmonyPatch(nameof(ChatController.Awake))] [HarmonyPostfix] public static void ChatController_Awake_Postfix(ChatController __instance) { - pages = new(); + Pages = new(); CreatePaginationControls(__instance); SetUpObjectPools(__instance); - SetPage(__instance, CustomChatManager.Chats[0]); - } - [HarmonyPatch(nameof(ChatController.AddChatNote))] - [HarmonyPrefix] - public static bool ChatController_AddChatNote_Prefix(ChatController __instance, ref NetworkedPlayerInfo srcPlayer, ref ChatNoteTypes noteType) - { - __instance.CustomAddChatNote(srcPlayer, CustomChatManager.Chats[0], noteType); - return false; + __instance.SetChat(CustomChatManager.Chats[0]); } [HarmonyPatch(nameof(ChatController.Update))] [HarmonyPrefix] public static void ChatController_Update_Prefix(ChatController __instance) { - if (!CurrentChat.CanSee()) SetPage(__instance, CustomChatManager.Chats[0]); + if (!CurrentChat.CanSee()) __instance.SetChat(CustomChatManager.Chats[0]); // Fallback to default chat bool canSend = CurrentChat.CanSendMessage(); __instance.freeChatField.gameObject.SetActive(DataManager.Settings.Multiplayer.ChatMode == QuickChatModes.FreeChatOrQuickChat && canSend); __instance.quickChatField.gameObject.SetActive(DataManager.Settings.Multiplayer.ChatMode == QuickChatModes.QuickChatOnly && canSend); } - public static ChatBubble GetPooledBubble(ChatController __instance, CustomChat chat) - { - if (!pages.TryGetValue(chat, out var pool)) return null; - if (pool.NotInUse == 0) - pool.ReclaimOldest(); - return pool.Get(); - } - [HarmonyPatch(nameof(ChatController.AlignAllBubbles))] [HarmonyPrefix] - public static bool ChatController_AlignAllBubbles_Postfix(ChatController __instance) + public static bool ChatController_AlignAllBubbles_Prefix(ChatController __instance) { - foreach (var pool in pages.Values) + foreach (var pool in Pages.Values) { float num1 = 0.0f; var activeChildren = pool.activeChildren; @@ -122,26 +104,27 @@ public static bool ChatController_AlignAllBubbles_Postfix(ChatController __insta return false; } - private static void CreatePaginationControls(ChatController __instance) + private static void CreatePaginationControls(ChatController instance) { - _text = Object.Instantiate(HudManager.Instance.UseButton.buttonLabelText, __instance.chatScreen.transform); - _text.color = Color.white; - _text.alignment = TextAlignmentOptions.MidlineLeft; - _text.fontSizeMax = 4f; - _text.fontSizeMin = 2f; - _text.overflowMode = TextOverflowModes.Overflow; - _text.transform.localPosition = new Vector3(-6.1f, -0.3f, -490f); - _text.text = "Default Chat"; - - _nextButton = Object.Instantiate(__instance.quickChatButton, __instance.quickChatButton.transform.parent, true); - _nextButton.transform.localPosition += new Vector3(0, 3, 0); - _nextButton.name = "UpButton"; - _nextButton.transform.GetChild(0).gameObject.GetComponent().sprite = + Text = Object.Instantiate(HudManager.Instance.UseButton.buttonLabelText, instance.chatScreen.transform); + Text.color = Color.white; + Text.alignment = TextAlignmentOptions.MidlineLeft; + Text.fontSizeMax = 4f; + Text.fontSizeMin = 2f; + Text.overflowMode = TextOverflowModes.Overflow; + Text.transform.localPosition = new Vector3(-6.1f, -0.3f, -490f); + Text.GetComponent().DestroyImmediate(); + Text.text = "Default Chat"; + + NextButton = Object.Instantiate(instance.quickChatButton, instance.quickChatButton.transform.parent, true); + NextButton.transform.localPosition += new Vector3(0, 3, 0); + NextButton.name = "UpButton"; + NextButton.transform.GetChild(0).gameObject.GetComponent().sprite = MiraAssets.NextButtonChat.LoadAsset(); - _nextButton.GetComponent().size /= new Vector2(1, 2); - _nextButton.OnClick = new Button.ButtonClickedEvent(); + NextButton.GetComponent().size /= new Vector2(1, 2); + NextButton.OnClick = new Button.ButtonClickedEvent(); var customChats = CustomChatManager.Chats.Where(x => x.CanSee()).ToList(); - _nextButton.OnClick.AddListener( + NextButton.OnClick.AddListener( (UnityAction)(() => { int id = customChats.IndexOf(CurrentChat); @@ -151,15 +134,15 @@ private static void CreatePaginationControls(ChatController __instance) id = 0; } var chat = customChats[id]; - SetPage(__instance, chat); + instance.SetChat(chat); })); - _previousButton = Object.Instantiate(_nextButton, __instance.chatScreen.transform, true); - _previousButton.transform.localPosition -= new Vector3(0, 1, 0); - _previousButton.name = "LeftArrowButton"; - _previousButton.transform.GetChild(0).GetComponent().sprite = MiraAssets.PreviousButtonChat.LoadAsset(); - _previousButton.OnClick = new Button.ButtonClickedEvent(); - _previousButton.OnClick.AddListener( + PreviousButton = Object.Instantiate(NextButton, instance.chatScreen.transform, true); + PreviousButton.transform.localPosition -= new Vector3(0, 1, 0); + PreviousButton.name = "LeftArrowButton"; + PreviousButton.transform.GetChild(0).GetComponent().sprite = MiraAssets.PreviousButtonChat.LoadAsset(); + PreviousButton.OnClick = new Button.ButtonClickedEvent(); + PreviousButton.OnClick.AddListener( (UnityAction)(() => { int id = customChats.IndexOf(CurrentChat); @@ -169,14 +152,14 @@ private static void CreatePaginationControls(ChatController __instance) id = customChats.Count - 1; } var chat = customChats[id]; - SetPage(__instance, chat); + instance.SetChat(chat); })); - _chatIcon = new GameObject("CurrentChatIcon").AddComponent(); - _chatIcon.gameObject.transform.SetParent(_nextButton.transform.parent); - _chatIcon.gameObject.layer = LayerMask.NameToLayer("UI"); - _chatIcon.transform.localPosition = _nextButton.transform.localPosition - new Vector3(0, 0.5f, 0); - _chatIcon.transform.localScale = Vector3.one / 2f; + ChatIcon = new GameObject("CurrentChatIcon").AddComponent(); + ChatIcon.gameObject.transform.SetParent(NextButton.transform.parent); + ChatIcon.gameObject.layer = LayerMask.NameToLayer("UI"); + ChatIcon.transform.localPosition = NextButton.transform.localPosition - new Vector3(0, 0.5f, 10); + ChatIcon.transform.localScale = Vector3.one / 2f; } private static void SetUpObjectPools(ChatController __instance) @@ -185,123 +168,24 @@ private static void SetUpObjectPools(ChatController __instance) { var pool = Object.Instantiate(__instance.chatBubblePool, __instance.chatBubblePool.transform.parent, true); pool.GetComponent().active = true; - pages.Add(chat, pool); + Pages.Add(chat, pool); } __instance.chatBubblePool.gameObject.SetActive(false); } - private static void SetPage(ChatController __instance, CustomChat chat) - { - if (CurrentChat != null!) CurrentChat.OnChatClose(__instance); - CurrentChat = chat; - _text.text = CurrentChat.Name; - _chatIcon.sprite = CurrentChat.ChatIcon.LoadAsset(); - - __instance.StartCoroutine(Effects.ColorFade(__instance.backgroundImage, __instance.backgroundImage.color, CurrentChat.ChatBackgroundColor, 0.4f)); - __instance.freeChatField.gameObject.SetActive(chat.CanSendMessage()); - __instance.quickChatField.gameObject.SetActive(chat.CanSendMessage()); - - __instance.chatButton.activeSprites.GetComponent().sprite = - CurrentChat.ChatButtonAppearance.ActiveSprite.LoadAsset(); - __instance.chatButton.inactiveSprites.GetComponent().sprite = - CurrentChat.ChatButtonAppearance.InactiveSprite.LoadAsset(); - __instance.chatButton.selectedSprites.GetComponent().sprite = - CurrentChat.ChatButtonAppearance.OpenedSprite.LoadAsset(); - foreach (var page in pages.Values) - { - page.gameObject.SetActive(false); - } - - if (!pages.TryGetValue(CurrentChat, out var pool)) return; - pool.gameObject.SetActive(true); - chat.OnChatOpen(__instance); - } - - public static void CustomAddChat(this ChatController __instance, PlayerControl sourcePlayer, string chatText, CustomChat chat, bool censor = true) + [HarmonyPatch(nameof(ChatController.AddChat))] + [HarmonyPrefix] + public static bool ChatController_AddChat_Prefix(ChatController __instance, ref PlayerControl sourcePlayer, ref string chatText, ref bool censor) { - if (!sourcePlayer || !PlayerControl.LocalPlayer) - return; - if (!pages.TryGetValue(chat, out var page)) return; - NetworkedPlayerInfo data1 = PlayerControl.LocalPlayer.Data; - NetworkedPlayerInfo data2 = sourcePlayer.Data; - if (data2 == null || data1 == null || (data2.IsDead && !data1.IsDead)) - return; - ChatBubble pooledBubble = GetPooledBubble(__instance, chat); - try - { - pooledBubble.transform.SetParent(page.transform.GetChild(0)); - pooledBubble.transform.localScale = Vector3.one; - int num = sourcePlayer == PlayerControl.LocalPlayer ? 1 : 0; - if (num != 0) - pooledBubble.SetRight(); - else - pooledBubble.SetLeft(); - bool didVote = MeetingHud.Instance && MeetingHud.Instance.DidVote(sourcePlayer.PlayerId); - pooledBubble.SetCosmetics(data2); - __instance.SetChatBubbleName(pooledBubble, data2, data2.IsDead, didVote, PlayerNameColor.Get(data2)); - if (censor && DataManager.Settings.Multiplayer.CensorChat) - chatText = BlockedWords.CensorWords(chatText); - pooledBubble.SetText(chatText); - pooledBubble.AlignChildren(); - __instance.AlignAllBubbles(); - if (!__instance.IsOpenOrOpening && __instance.notificationRoutine == null && chat.CanSee()) - { - __instance.chatNotifyDot.sprite = chat.ChatButtonAppearance.NotificationSprite.LoadAsset(); - __instance.notificationRoutine = __instance.StartCoroutine(__instance.BounceDot()); - } - if (num != 0 || __instance.IsOpenOrOpening) - return; - if (chat.CanSee()) - { - var audio = chat.MessageSound != null! ? chat.MessageSound.LoadAsset() : __instance.messageSound; - SoundManager.Instance.PlaySound(audio, false).pitch = - (float)(0.5 + sourcePlayer.PlayerId / 15.0); - } - __instance.chatNotification.SetUp(sourcePlayer, chatText); - - page.activeChildren.Add(pooledBubble); - chat.OnMessageSent(sourcePlayer, pooledBubble); - } - catch (Exception ex) - { - ChatController.Logger.Error(ex.ToString()); - page.Reclaim(pooledBubble); - } + __instance.CustomAddChat(sourcePlayer, chatText, CustomChatManager.Chats[0], censor); + return false; } - public static void CustomAddChatNote(this ChatController __instance, NetworkedPlayerInfo srcPlayer, CustomChat chat, ChatNoteTypes noteType) + [HarmonyPatch(nameof(ChatController.AddChatNote))] + [HarmonyPrefix] + public static bool ChatController_AddChatNote_Prefix(ChatController __instance, ref NetworkedPlayerInfo srcPlayer, ref ChatNoteTypes noteType) { - if (!pages.TryGetValue(chat, out var page)) return; - if (srcPlayer == null) - return; - ChatBubble pooledBubble = GetPooledBubble(__instance, chat); - pooledBubble.SetCosmetics(srcPlayer); - pooledBubble.transform.SetParent(page.transform.GetChild(0)); - pooledBubble.transform.localScale = Vector3.one; - pooledBubble.SetNotification(); - if (noteType == ChatNoteTypes.DidVote) - { - int rem = MeetingHud.Instance.GetVotesRemaining(); - pooledBubble.SetName(TranslationController.Instance.GetString(StringNames.MeetingHasVoted, srcPlayer.PlayerName, rem), false, true, Color.green); - } - pooledBubble.SetText(string.Empty); - pooledBubble.AlignChildren(); - __instance.AlignAllBubbles(); - if (!__instance.IsOpenOrOpening && __instance.notificationRoutine == null && chat.CanSee()) - { - __instance.chatNotifyDot.sprite = chat.ChatButtonAppearance.NotificationSprite.LoadAsset(); - __instance.notificationRoutine = __instance.StartCoroutine(__instance.BounceDot()); - } - if (srcPlayer.Object.AmOwner) - return; - if (chat.CanSee()) - { - var audio = chat.MessageSound != null! ? chat.MessageSound.LoadAsset() : __instance.messageSound; - SoundManager.Instance.PlaySound(audio, false).pitch = - (float)(0.5 + srcPlayer.PlayerId / 15.0); - } - page.activeChildren.Add(pooledBubble); - - chat.OnMessageSent(srcPlayer.Object, pooledBubble); + __instance.CustomAddChatNote(srcPlayer, CustomChatManager.Chats[0], noteType); + return false; } } diff --git a/MiraAPI/PluginLoading/MiraPluginInfo.cs b/MiraAPI/PluginLoading/MiraPluginInfo.cs index 838c5d75..b0636131 100644 --- a/MiraAPI/PluginLoading/MiraPluginInfo.cs +++ b/MiraAPI/PluginLoading/MiraPluginInfo.cs @@ -44,7 +44,7 @@ internal MiraPluginInfo(IMiraPlugin miraPlugin, PluginInfo info) /// /// Gets a read only collection of this plugin's custom chats. /// - public IReadOnlyCollection Chats { get; private set; } = null!; + public IReadOnlyCollection Chats { get; private set; } = null!; /// /// Gets a read only dictionary of Role IDs and the RoleBehaviour object they are associated with. @@ -86,7 +86,7 @@ internal void SavePublicCollections() internal List InternalButtons { get; } = []; - internal List InternalChats { get; } = []; + internal List InternalChats { get; } = []; /// /// Gets the plugin's ID, as defined in the plugin's BepInEx metadata. diff --git a/MiraAPI/Utilities/ChatControllerExtensions.cs b/MiraAPI/Utilities/ChatControllerExtensions.cs new file mode 100644 index 00000000..b3551374 --- /dev/null +++ b/MiraAPI/Utilities/ChatControllerExtensions.cs @@ -0,0 +1,193 @@ +using System; +using AmongUs.Data; +using MiraAPI.CustomChats; +using MiraAPI.Patches; +using UnityEngine; + +namespace MiraAPI.Utilities; + +/// +/// a Class with a bunch of extensions to replace methods related to adding chat messages. +/// +public static class ChatControllerExtensions +{ + /// + /// Adds a chat message to a specific . + /// + /// The Instance. + /// The who sent the message. + /// The message text. + /// The target . + /// Determines if the message should be censored. + public static void CustomAddChat(this ChatController instance, PlayerControl sourcePlayer, string chatText, AbstractCustomChat chat, bool censor = true) + { + if (!sourcePlayer || !PlayerControl.LocalPlayer) + return; + if (!ChatControllerCustomChatsPatches.Pages.TryGetValue(chat, out var page)) return; + NetworkedPlayerInfo data1 = PlayerControl.LocalPlayer.Data; + NetworkedPlayerInfo data2 = sourcePlayer.Data; + if (data2 == null || data1 == null || (data2.IsDead && !data1.IsDead)) + return; + ChatBubble pooledBubble = GetPooledBubble(instance, chat); + try + { + pooledBubble.transform.SetParent(page.transform.GetChild(0)); + pooledBubble.transform.localScale = Vector3.one; + int num = sourcePlayer == PlayerControl.LocalPlayer ? 1 : 0; + if (num != 0) + pooledBubble.SetRight(); + else + pooledBubble.SetLeft(); + bool didVote = MeetingHud.Instance && MeetingHud.Instance.DidVote(sourcePlayer.PlayerId); + pooledBubble.SetCosmetics(data2); + instance.SetChatBubbleName(pooledBubble, data2, data2.IsDead, didVote, PlayerNameColor.Get(data2)); + if (censor && DataManager.Settings.Multiplayer.CensorChat) + chatText = BlockedWords.CensorWords(chatText); + pooledBubble.SetText(chatText); + pooledBubble.AlignChildren(); + instance.AlignAllBubbles(); + if (!instance.IsOpenOrOpening && instance.notificationRoutine == null && chat.CanSee()) + { + instance.chatNotifyDot.sprite = chat.Sprites.NotificationSprite.LoadAsset(); + instance.notificationRoutine = instance.StartCoroutine(instance.BounceDot()); + } + if (num != 0 || instance.IsOpenOrOpening) + return; + if (chat.CanSee()) + { + var audio = chat.MessageSound != null! ? chat.MessageSound.LoadAsset() : instance.messageSound; + SoundManager.Instance.PlaySound(audio, false).pitch = + (float)(0.5 + sourcePlayer.PlayerId / 15.0); + instance.chatNotification.SetUp(sourcePlayer, chatText); + } + + page.activeChildren.Add(pooledBubble); + chat.OnMessageSent(sourcePlayer, pooledBubble); + } + catch (Exception ex) + { + ChatController.Logger.Error(ex.ToString()); + page.Reclaim(pooledBubble); + } + } + + /// + /// Adds a chat note to a specific . + /// + /// The Instance. + /// The who sent the message. + /// The target . + /// The of the message. + public static void CustomAddChatNote(this ChatController instance, NetworkedPlayerInfo srcPlayer, AbstractCustomChat chat, ChatNoteTypes noteType) + { + if (!ChatControllerCustomChatsPatches.Pages.TryGetValue(chat, out var page)) return; + if (srcPlayer == null) + return; + ChatBubble pooledBubble = GetPooledBubble(instance, chat); + pooledBubble.SetCosmetics(srcPlayer); + pooledBubble.transform.SetParent(page.transform.GetChild(0)); + pooledBubble.transform.localScale = Vector3.one; + pooledBubble.SetNotification(); + if (noteType == ChatNoteTypes.DidVote) + { + int rem = MeetingHud.Instance.GetVotesRemaining(); + pooledBubble.SetName(TranslationController.Instance.GetString(StringNames.MeetingHasVoted, srcPlayer.PlayerName, rem), false, true, Color.green); + } + pooledBubble.SetText(string.Empty); + pooledBubble.AlignChildren(); + instance.AlignAllBubbles(); + if (!instance.IsOpenOrOpening && instance.notificationRoutine == null && chat.CanSee()) + { + instance.chatNotifyDot.sprite = chat.Sprites.NotificationSprite.LoadAsset(); + instance.notificationRoutine = instance.StartCoroutine(instance.BounceDot()); + } + if (srcPlayer.Object.AmOwner) + return; + if (chat.CanSee()) + { + var audio = chat.MessageSound != null! ? chat.MessageSound.LoadAsset() : instance.messageSound; + SoundManager.Instance.PlaySound(audio, false).pitch = + (float)(0.5 + srcPlayer.PlayerId / 15.0); + } + page.activeChildren.Add(pooledBubble); + + chat.OnMessageSent(srcPlayer.Object, pooledBubble); + } + + /// + /// Adds a chat warning to a specific . + /// + /// The Instance. + /// The warning text. + /// The target . + public static void CustomAddChatWarning(this ChatController instance, string warningText, AbstractCustomChat chat) + { + if (!ChatControllerCustomChatsPatches.Pages.TryGetValue(chat, out var page)) return; + ChatBubble pooledBubble = GetPooledBubble(instance, chat); + pooledBubble.transform.SetParent(page.transform.GetChild(0)); + pooledBubble.transform.localScale = Vector3.one; + pooledBubble.SetRight(); + pooledBubble.SetWarning(warningText); + pooledBubble.AlignChildren(); + instance.AlignAllBubbles(); + if (!instance.IsOpenOrOpening && instance.notificationRoutine == null && chat.CanSee()) + { + instance.chatNotifyDot.sprite = chat.Sprites.NotificationSprite.LoadAsset(); + instance.notificationRoutine = instance.StartCoroutine(instance.BounceDot()); + } + if (chat.CanSee()) + { + var audio = chat.MessageSound != null! ? chat.MessageSound.LoadAsset() : instance.messageSound; + SoundManager.Instance.PlaySound(audio, false); + } + page.activeChildren.Add(pooledBubble); + + chat.OnMessageSent(null!, pooledBubble); + } + + /// + /// Gets a pooled from a specific . + /// + /// The Instance. + /// The target . + /// A pooled if the chat is initialized, null otherwise. + public static ChatBubble GetPooledBubble(this ChatController instance, AbstractCustomChat chat) + { + if (!ChatControllerCustomChatsPatches.Pages.TryGetValue(chat, out var pool)) return null!; + if (pool.NotInUse == 0) + pool.ReclaimOldest(); + return pool.Get(); + } + + /// + /// Switches the currently selected . + /// + /// The Instance. + /// The target . + public static void SetChat(this ChatController instance, AbstractCustomChat chat) + { + if (ChatControllerCustomChatsPatches.CurrentChat != null!) ChatControllerCustomChatsPatches.CurrentChat.OnChatClose(instance); + ChatControllerCustomChatsPatches.CurrentChat = chat; + ChatControllerCustomChatsPatches.Text.text = ChatControllerCustomChatsPatches.CurrentChat.Name; + ChatControllerCustomChatsPatches.ChatIcon.sprite = ChatControllerCustomChatsPatches.CurrentChat.ChatIcon.LoadAsset(); + + instance.StartCoroutine(Effects.ColorFade(instance.backgroundImage, instance.backgroundImage.color, ChatControllerCustomChatsPatches.CurrentChat.ChatBackgroundColor, 0.4f)); + instance.freeChatField.gameObject.SetActive(chat.CanSendMessage()); + instance.quickChatField.gameObject.SetActive(chat.CanSendMessage()); + + instance.chatButton.activeSprites.GetComponent().sprite = + ChatControllerCustomChatsPatches.CurrentChat.Sprites.ActiveSprite.LoadAsset(); + instance.chatButton.inactiveSprites.GetComponent().sprite = + ChatControllerCustomChatsPatches.CurrentChat.Sprites.InactiveSprite.LoadAsset(); + instance.chatButton.selectedSprites.GetComponent().sprite = + ChatControllerCustomChatsPatches.CurrentChat.Sprites.OpenedSprite.LoadAsset(); + foreach (var page in ChatControllerCustomChatsPatches.Pages.Values) + { + page.gameObject.SetActive(false); + } + + if (!ChatControllerCustomChatsPatches.Pages.TryGetValue(ChatControllerCustomChatsPatches.CurrentChat, out var pool)) return; + pool.gameObject.SetActive(true); + chat.OnChatOpen(instance); + } +} From 7adbd3643b2934bd1959ba39355c49531bba98c9 Mon Sep 17 00:00:00 2001 From: WanderingPixel <126269533+WanderingPix@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:12:06 +0100 Subject: [PATCH 4/5] Adjust ChatIcon scale for better visibility --- MiraAPI/Patches/ChatControllerPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiraAPI/Patches/ChatControllerPatch.cs b/MiraAPI/Patches/ChatControllerPatch.cs index 72ed565a..7070966c 100644 --- a/MiraAPI/Patches/ChatControllerPatch.cs +++ b/MiraAPI/Patches/ChatControllerPatch.cs @@ -159,7 +159,7 @@ private static void CreatePaginationControls(ChatController instance) ChatIcon.gameObject.transform.SetParent(NextButton.transform.parent); ChatIcon.gameObject.layer = LayerMask.NameToLayer("UI"); ChatIcon.transform.localPosition = NextButton.transform.localPosition - new Vector3(0, 0.5f, 10); - ChatIcon.transform.localScale = Vector3.one / 2f; + ChatIcon.transform.localScale = new Vector3(0.4f, 0.4f, 1f); } private static void SetUpObjectPools(ChatController __instance) From d5c29f2a260bd7b1c70d30a77782ecfad2eeeaf0 Mon Sep 17 00:00:00 2001 From: wanderingpix Date: Sat, 13 Jun 2026 11:25:20 +0100 Subject: [PATCH 5/5] Custom Chat Singleton, Custom chat message converter --- MiraAPI/CustomChats/CustomChatSingleton.cs | 19 +++++++++++ MiraAPI/Networking/CustomChatConverter.cs | 38 ++++++++++++++++++++++ MiraAPI/Networking/CustomSendChatRpc.cs | 17 ++-------- MiraAPI/Patches/PlayerControlPatches.cs | 2 +- 4 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 MiraAPI/CustomChats/CustomChatSingleton.cs create mode 100644 MiraAPI/Networking/CustomChatConverter.cs diff --git a/MiraAPI/CustomChats/CustomChatSingleton.cs b/MiraAPI/CustomChats/CustomChatSingleton.cs new file mode 100644 index 00000000..493c43a3 --- /dev/null +++ b/MiraAPI/CustomChats/CustomChatSingleton.cs @@ -0,0 +1,19 @@ +using System.Linq; + +namespace MiraAPI.CustomChats; + +/// +/// Singleton for accessing custom chats. +/// +/// The custom chat type. +public static class CustomChatSingleton where T : AbstractCustomChat +{ + private static T? _instance; + + /// + /// Gets the instance of the option group. + /// +#pragma warning disable CA1000 + public static T Instance => _instance ??= CustomChatManager.Chats.OfType().Single(); +#pragma warning restore CA1000 +} diff --git a/MiraAPI/Networking/CustomChatConverter.cs b/MiraAPI/Networking/CustomChatConverter.cs new file mode 100644 index 00000000..afd8c51c --- /dev/null +++ b/MiraAPI/Networking/CustomChatConverter.cs @@ -0,0 +1,38 @@ +using System; +using Hazel; +using InnerNet; +using MiraAPI.CustomChats; +using MiraAPI.Modifiers; +using Reactor.Networking.Attributes; +using Reactor.Networking.Serialization; + +namespace MiraAPI.Networking; + +/// +/// Converter for serializing and deserializing objects. +/// +[MessageConverter] +public class CustomChatConverter : MessageConverter +{ + /// + /// Writes a to the writer. + /// + /// The writer to write to. + /// The to write. + public override void Write(MessageWriter writer, AbstractCustomChat value) + { + writer.Write(CustomChatManager.Chats.IndexOf(value)); + } + + /// + /// Reads a from the reader. + /// + /// The reader to read from. + /// The type of the object to read. + /// The that was read. + /// Thrown if the custom chat is not found. + public override AbstractCustomChat Read(MessageReader reader, Type objectType) + { + return CustomChatManager.Chats[reader.ReadInt32()]; + } +} diff --git a/MiraAPI/Networking/CustomSendChatRpc.cs b/MiraAPI/Networking/CustomSendChatRpc.cs index 2eaf8f10..8d0b91a4 100644 --- a/MiraAPI/Networking/CustomSendChatRpc.cs +++ b/MiraAPI/Networking/CustomSendChatRpc.cs @@ -1,19 +1,7 @@ -using System; -using System.Collections; -using System.Linq; -using AmongUs.Data; -using AmongUs.GameOptions; -using Assets.CoreScripts; -using BepInEx.Unity.IL2CPP.Utils; -using MiraAPI.CustomChats; -using MiraAPI.Events; -using MiraAPI.Events.Vanilla.Gameplay; -using MiraAPI.Patches; +using MiraAPI.CustomChats; using MiraAPI.Utilities; using Reactor.Networking.Attributes; using Reactor.Networking.Rpc; -using UnityEngine; -using Object = UnityEngine.Object; namespace MiraAPI.Networking; @@ -26,9 +14,8 @@ public static class CustomSendChatRpc public static void RpcCustomSendChat( this PlayerControl source, string chatText, - int id) + AbstractCustomChat chat) { - AbstractCustomChat chat = CustomChatManager.Chats[id]; if (!chat.CanSee()) return; HudManager.Instance.Chat.CustomAddChat(source, chatText, chat); diff --git a/MiraAPI/Patches/PlayerControlPatches.cs b/MiraAPI/Patches/PlayerControlPatches.cs index b4bf3244..ff1d16e5 100644 --- a/MiraAPI/Patches/PlayerControlPatches.cs +++ b/MiraAPI/Patches/PlayerControlPatches.cs @@ -172,7 +172,7 @@ public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance) // ReSharper disable once InconsistentNaming public static bool RpcSendChatPrefix(PlayerControl __instance, ref string chatText) { - __instance.RpcCustomSendChat(chatText, CustomChatManager.Chats.IndexOf(ChatControllerCustomChatsPatches.CurrentChat)); + __instance.RpcCustomSendChat(chatText, ChatControllerCustomChatsPatches.CurrentChat); return false; } }