diff --git a/XivCommon/Functions/Talk.cs b/XivCommon/Functions/Talk.cs new file mode 100755 index 0000000..a3a97b6 --- /dev/null +++ b/XivCommon/Functions/Talk.cs @@ -0,0 +1,146 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Game; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; +using Dalamud.Plugin; + +namespace XivCommon.Functions { + /// + /// Class containing Talk events + /// + public class Talk : IDisposable { + // Updated: 5.5 + private const int TextOffset = 0; + private const int NameOffset = 0x10; + private const int StyleOffset = 0x38; + + private SeStringManager SeStringManager { get; } + + private delegate void AddonTalkV45Delegate(IntPtr addon, IntPtr a2, IntPtr data); + + private Hook? AddonTalkV45Hook { get; } + + private delegate IntPtr SetAtkValueStringDelegate(IntPtr atkValue, IntPtr text); + + private SetAtkValueStringDelegate SetAtkValueString { get; } + + /// + /// The delegate for Talk events. + /// + public delegate void TalkEventDelegate(ref SeString name, ref SeString text, ref TalkStyle style); + + /// + /// + /// The event that is fired when NPCs talk. + /// + /// + /// Requires the hook to be enabled. + /// + /// + public event TalkEventDelegate? OnTalk; + + internal Talk(SigScanner scanner, SeStringManager manager, bool hooksEnabled) { + this.SeStringManager = manager; + + var setAtkPtr = scanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED"); + this.SetAtkValueString = Marshal.GetDelegateForFunctionPointer(setAtkPtr); + + if (!hooksEnabled) { + return; + } + + var showMessageBoxPtr = scanner.ScanText("4C 8B DC 55 57 41 55 49 8D 6B 98"); + this.AddonTalkV45Hook = new Hook(showMessageBoxPtr, new AddonTalkV45Delegate(this.AddonTalkV45Detour)); + this.AddonTalkV45Hook.Enable(); + } + + /// + public void Dispose() { + this.AddonTalkV45Hook?.Dispose(); + } + + private void AddonTalkV45Detour(IntPtr addon, IntPtr a2, IntPtr data) { + if (this.OnTalk == null) { + this.AddonTalkV45Hook!.Original(addon, a2, data); + return; + } + + var rawName = Util.ReadTerminated(Marshal.ReadIntPtr(data + NameOffset + 8)); + var rawText = Util.ReadTerminated(Marshal.ReadIntPtr(data + TextOffset + 8)); + var style = (TalkStyle) Marshal.ReadByte(data + StyleOffset); + + var name = this.SeStringManager.Parse(rawName); + var text = this.SeStringManager.Parse(rawText); + + try { + this.OnTalk?.Invoke(ref name, ref text, ref style); + } catch (Exception ex) { + PluginLog.LogError(ex, "Exception in Talk detour"); + } + + var newName = name.Encode().Terminate(); + var newText = text.Encode().Terminate(); + + Marshal.WriteByte(data + StyleOffset, (byte) style); + + unsafe { + fixed (byte* namePtr = newName, textPtr = newText) { + this.SetAtkValueString(data + NameOffset, (IntPtr) namePtr); + this.SetAtkValueString(data + TextOffset, (IntPtr) textPtr); + this.AddonTalkV45Hook!.Original(addon, a2, data); + } + } + } + } + + /// + /// Talk window styles. + /// + public enum TalkStyle : byte { + /// + /// The normal style with a white background. + /// + Normal = 0, + + /// + /// A style with lights on the top and bottom border. + /// + Lights = 2, + + /// + /// A style used for when characters are shouting. + /// + Shout = 3, + + /// + /// Like but with flatter edges. + /// + FlatShout = 4, + + /// + /// The style used when dragons (and some other NPCs) talk. + /// + Dragon = 5, + + /// + /// The style used for Allagan machinery. + /// + Allagan = 6, + + /// + /// The style used for system messages. + /// + System = 7, + + /// + /// A mixture of the system message style and the dragon style. + /// + DragonSystem = 8, + + /// + /// The system message style with a purple background. + /// + PurpleSystem = 9, + } +} diff --git a/XivCommon/GameFunctions.cs b/XivCommon/GameFunctions.cs index b0d39ed..92c2cdc 100755 --- a/XivCommon/GameFunctions.cs +++ b/XivCommon/GameFunctions.cs @@ -28,17 +28,27 @@ namespace XivCommon { /// public Examine Examine { get; } + /// + /// Talk events + /// + public Talk Talk { get; } + internal GameFunctions(Hooks hooks, DalamudPluginInterface @interface) { this.Interface = @interface; - this.Chat = new Chat(this, @interface.TargetModuleScanner); - this.PartyFinder = new PartyFinder(@interface.TargetModuleScanner, hooks.HasFlag(Hooks.PartyFinder)); - this.BattleTalk = new BattleTalk(this, @interface.TargetModuleScanner, @interface.SeStringManager, hooks.HasFlag(Hooks.BattleTalk)); - this.Examine = new Examine(this, @interface.TargetModuleScanner); + var scanner = @interface.TargetModuleScanner; + var seStringManager = @interface.SeStringManager; + + 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)); } /// public void Dispose() { + this.Talk.Dispose(); this.BattleTalk.Dispose(); this.PartyFinder.Dispose(); } diff --git a/XivCommon/Hooks.cs b/XivCommon/Hooks.cs index 8711b01..4701a94 100755 --- a/XivCommon/Hooks.cs +++ b/XivCommon/Hooks.cs @@ -26,6 +26,13 @@ namespace XivCommon { /// This hook is used in order to enable all Party Finder functions. /// PartyFinder, + + /// + /// The Talk hooks. + /// + /// This hook is used in order to enable the Talk events. + /// + Talk, } internal static class HooksExt {