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 {