diff --git a/XivCommon/Functions/ChatBubbles.cs b/XivCommon/Functions/ChatBubbles.cs new file mode 100755 index 0000000..55c7f37 --- /dev/null +++ b/XivCommon/Functions/ChatBubbles.cs @@ -0,0 +1,162 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Game; +using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; +using Dalamud.Plugin; + +namespace XivCommon.Functions { + /// + /// Class containing chat bubble events and functions + /// + public class ChatBubbles : IDisposable { + private Dalamud.Dalamud Dalamud { get; } + private SeStringManager SeStringManager { get; } + + private delegate void OpenChatBubbleDelegate(IntPtr manager, IntPtr actor, IntPtr text, byte a4); + + private unsafe delegate void UpdateChatBubbleDelegate(ChatBubble* bubble, IntPtr actor, IntPtr a3); + + private Hook? OpenChatBubbleHook { get; } + + private Hook? UpdateChatBubbleHook { get; } + + /// + /// The delegate for chat bubble events. + /// + public delegate void OnChatBubbleDelegate(ref Actor actor, ref SeString text); + + /// + /// The delegate for chat bubble update events. + /// + public delegate void OnUpdateChatBubbleDelegate(ref Actor actor); + + /// + /// + /// The event that is fired when a chat bubble is shown. + /// + /// + /// Requires the hook to be enabled. + /// + /// + public event OnChatBubbleDelegate? OnChatBubble; + + /// + /// + /// The event that is fired when a chat bubble is updated. + /// + /// + /// Requires the hook to be enabled. + /// + /// + public event OnUpdateChatBubbleDelegate? OnUpdateBubble; + + internal ChatBubbles(Dalamud.Dalamud dalamud, SigScanner scanner, SeStringManager manager, bool hookEnabled) { + this.Dalamud = dalamud; + this.SeStringManager = manager; + + if (!hookEnabled) { + return; + } + + var openPtr = scanner.ScanText("E8 ?? ?? ?? ?? 80 BF ?? ?? ?? ?? ?? C7 07 ?? ?? ?? ??"); + this.OpenChatBubbleHook = new Hook(openPtr, new OpenChatBubbleDelegate(this.OpenChatBubbleDetour)); + this.OpenChatBubbleHook.Enable(); + + var updatePtr = scanner.ScanText("48 85 D2 0F 84 ?? ?? ?? ?? 48 89 5C 24 ?? 57 48 83 EC 20 8B 41 0C"); + unsafe { + this.UpdateChatBubbleHook = new Hook(updatePtr + 9, new UpdateChatBubbleDelegate(this.UpdateChatBubbleDetour)); + } + + this.UpdateChatBubbleHook.Enable(); + } + + /// + public void Dispose() { + this.OpenChatBubbleHook?.Dispose(); + this.UpdateChatBubbleHook?.Dispose(); + } + + private void OpenChatBubbleDetour(IntPtr manager, IntPtr actor, IntPtr text, byte a4) { + try { + this.OpenChatBubbleDetourInner(manager, actor, text, a4); + } catch (Exception ex) { + PluginLog.LogError(ex, "Exception in chat bubble detour"); + this.OpenChatBubbleHook!.Original(manager, actor, text, a4); + } + } + + private void OpenChatBubbleDetourInner(IntPtr manager, IntPtr actorPtr, IntPtr textPtr, byte a4) { + var actorStruct = Marshal.PtrToStructure(actorPtr); + var actor = new Actor(actorPtr, actorStruct, this.Dalamud); + + var rawText = Util.ReadTerminated(textPtr); + var text = this.SeStringManager.Parse(rawText); + + try { + this.OnChatBubble?.Invoke(ref actor, ref text); + } catch (Exception ex) { + PluginLog.LogError(ex, "Exception in chat bubble event"); + } + + var newText = text.Encode().Terminate(); + + unsafe { + fixed (byte* newTextPtr = newText) { + this.OpenChatBubbleHook!.Original(manager, actor.Address, (IntPtr) newTextPtr, a4); + } + } + } + + private unsafe void UpdateChatBubbleDetour(ChatBubble* bubble, IntPtr actor, IntPtr a3) { + try { + this.UpdateChatBubbleDetourInner(bubble, actor, a3); + } catch (Exception ex) { + PluginLog.LogError(ex, "Exception in update chat bubble detour"); + this.UpdateChatBubbleHook!.Original(bubble, actor, a3); + } + } + + private unsafe void UpdateChatBubbleDetourInner(ChatBubble* bubble, IntPtr actorPtr, IntPtr a3) { + var actorStruct = Marshal.PtrToStructure(actorPtr); + var actor = new Actor(actorPtr, actorStruct, this.Dalamud); + + try { + this.OnUpdateBubble?.Invoke(ref actor); + } catch (Exception ex) { + PluginLog.LogError(ex, "Exception in chat bubble update event"); + } + + this.UpdateChatBubbleHook!.Original(bubble, actor.Address, a3); + } + } + + [StructLayout(LayoutKind.Explicit, Size = 0x80)] + internal unsafe struct ChatBubble { + [FieldOffset(0x0)] + internal readonly uint Id; + + [FieldOffset(0x4)] + internal float Timer; + + [FieldOffset(0x8)] + internal readonly uint Unk_8; // enum probably + + [FieldOffset(0xC)] + internal ChatBubbleStatus Status; // state of the bubble + + [FieldOffset(0x10)] + internal readonly byte* Text; + + [FieldOffset(0x78)] + internal readonly ulong Unk_78; // check whats in memory here + } + + internal enum ChatBubbleStatus : uint { + GetData = 0, + On = 1, + Init = 2, + Off = 3, + } +} diff --git a/XivCommon/GameFunctions.cs b/XivCommon/GameFunctions.cs index 92c2cdc..3a04ddd 100755 --- a/XivCommon/GameFunctions.cs +++ b/XivCommon/GameFunctions.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using Dalamud.Plugin; using XivCommon.Functions; @@ -33,21 +34,31 @@ namespace XivCommon { /// public Talk Talk { get; } + /// + /// Chat bubble functions and events + /// + public ChatBubbles ChatBubbles { get; } + internal GameFunctions(Hooks hooks, DalamudPluginInterface @interface) { this.Interface = @interface; var scanner = @interface.TargetModuleScanner; var seStringManager = @interface.SeStringManager; + var dalamudField = @interface.GetType().GetField("dalamud", BindingFlags.Instance | BindingFlags.NonPublic); + var dalamud = (Dalamud.Dalamud) dalamudField!.GetValue(@interface); + this.Chat = new Chat(this, scanner); this.PartyFinder = new PartyFinder(scanner, hooks.HasFlag(Hooks.PartyFinder)); this.BattleTalk = new BattleTalk(this, scanner, seStringManager, hooks.HasFlag(Hooks.BattleTalk)); this.Examine = new Examine(this, scanner); this.Talk = new Talk(scanner, seStringManager, hooks.HasFlag(Hooks.Talk)); + this.ChatBubbles = new ChatBubbles(dalamud, scanner, seStringManager, hooks.HasFlag(Hooks.ChatBubbles)); } /// public void Dispose() { + this.ChatBubbles.Dispose(); this.Talk.Dispose(); this.BattleTalk.Dispose(); this.PartyFinder.Dispose(); diff --git a/XivCommon/Hooks.cs b/XivCommon/Hooks.cs index 4701a94..f019c83 100755 --- a/XivCommon/Hooks.cs +++ b/XivCommon/Hooks.cs @@ -33,6 +33,13 @@ namespace XivCommon { /// This hook is used in order to enable the Talk events. /// Talk, + + /// + /// The chat bubbles hooks. + /// + /// This hook is used in order to enable the chat bubbles events. + /// + ChatBubbles, } internal static class HooksExt {