From 57ab45e9f787081c0bee708e45a0ae49fd04a396 Mon Sep 17 00:00:00 2001 From: Cara Date: Thu, 17 Dec 2020 20:25:20 +1030 Subject: [PATCH] Dalamud Link System --- Dalamud/Game/Chat/SeStringHandling/Payload.cs | 8 +- .../Game/Chat/SeStringHandling/PayloadType.cs | 4 + .../Payloads/DalamudLinkPayload.cs | 48 +++++++++++ Dalamud/Game/Internal/Gui/ChatGui.cs | 82 +++++++++++++++++++ .../Internal/Gui/ChatGuiAddressResolver.cs | 3 + Dalamud/Plugin/DalamudPluginInterface.cs | 30 +++++++ 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/DalamudLinkPayload.cs diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs index b7a900b2c..9d3d441f3 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -155,6 +155,10 @@ namespace Dalamud.Game.Chat.SeStringHandling payload = new QuestPayload(); break; + case EmbeddedInfoType.DalamudLink: + payload = new DalamudLinkPayload(); + break; + case EmbeddedInfoType.LinkTerminator: // this has no custom handling and so needs to fallthrough to ensure it is captured default: @@ -224,7 +228,7 @@ namespace Dalamud.Game.Chat.SeStringHandling UIGlow = 0x49 } - protected enum EmbeddedInfoType + public enum EmbeddedInfoType { PlayerName = 0x01, ItemLink = 0x03, @@ -232,6 +236,8 @@ namespace Dalamud.Game.Chat.SeStringHandling QuestLink = 0x05, Status = 0x09, + DalamudLink = 0x0F, // Dalamud Custom + LinkTerminator = 0xCF // not clear but seems to always follow a link } diff --git a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs index 060d14093..6d8a3de1b 100644 --- a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs +++ b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs @@ -51,6 +51,10 @@ namespace Dalamud.Game.Chat.SeStringHandling /// Quest, /// + /// A SeString payload representing a custom clickable link for dalamud plugins + /// + DalamudLink, + /// /// An SeString payload representing any data we don't handle. /// Unknown diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/DalamudLinkPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/DalamudLinkPayload.cs new file mode 100644 index 000000000..88bd15695 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/DalamudLinkPayload.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads { + + /// + /// + /// + public class DalamudLinkPayload : Payload { + public override PayloadType Type => PayloadType.DalamudLink; + + public uint CommandId { get; internal set; } = 0; + + [NotNull] + public string Plugin { get; internal set; } = string.Empty; + + protected override byte[] EncodeImpl() { + var pluginBytes = Encoding.UTF8.GetBytes(Plugin); + var commandBytes = MakeInteger(CommandId); + var chunkLen = 3 + pluginBytes.Length + commandBytes.Length; + + if (chunkLen > 255) { + throw new Exception("Chunk is too long. Plugin name exceeds limits for DalamudLinkPayload"); + } + + var bytes = new List {START_BYTE, (byte) SeStringChunkType.Interactable, (byte) chunkLen, (byte) EmbeddedInfoType.DalamudLink}; + bytes.Add((byte) pluginBytes.Length); + bytes.AddRange(pluginBytes); + bytes.AddRange(commandBytes); + bytes.Add(END_BYTE); + return bytes.ToArray(); + } + + protected override void DecodeImpl(BinaryReader reader, long endOfStream) { + Plugin = Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadByte())); + CommandId = GetInteger(reader); + } + + public override string ToString() { + return $"{Type} - Plugin: {Plugin}, Command: {CommandId}"; + } + } +} diff --git a/Dalamud/Game/Internal/Gui/ChatGui.cs b/Dalamud/Game/Internal/Gui/ChatGui.cs index b55c4a040..ff23a8312 100644 --- a/Dalamud/Game/Internal/Gui/ChatGui.cs +++ b/Dalamud/Game/Internal/Gui/ChatGui.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using Dalamud.Game.Chat; using Dalamud.Game.Chat.SeStringHandling; +using Dalamud.Game.Chat.SeStringHandling.Payloads; using Dalamud.Game.Internal.Libc; using Dalamud.Hooking; using Serilog; @@ -42,6 +44,8 @@ namespace Dalamud.Game.Internal.Gui { private readonly Hook populateItemLinkHook; + private readonly Hook interactableLinkClickedHook; + #endregion #region Delegates @@ -55,6 +59,10 @@ namespace Dalamud.Game.Internal.Gui { [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr); + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr); + #endregion public int LastLinkedItemId { get; private set; } @@ -81,16 +89,22 @@ namespace Dalamud.Game.Internal.Gui { new Hook(Address.PopulateItemLinkObject, new PopulateItemLinkDelegate(HandlePopulateItemLinkDetour), this); + this.interactableLinkClickedHook = + new Hook(Address.InteractableLinkClicked, + new InteractableLinkClickedDelegate(InteractableLinkClickedDetour)); + } public void Enable() { this.printMessageHook.Enable(); this.populateItemLinkHook.Enable(); + this.interactableLinkClickedHook.Enable(); } public void Dispose() { this.printMessageHook.Dispose(); this.populateItemLinkHook.Dispose(); + this.interactableLinkClickedHook.Dispose(); } private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr) { @@ -168,6 +182,74 @@ namespace Dalamud.Game.Internal.Gui { return retVal; } + private readonly Dictionary<(string pluginName, uint commandId), Action> dalamudLinkHandlers = new Dictionary<(string, uint), Action>(); + + /// + /// Create a link handler + /// + /// + /// + /// + /// + internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action commandAction) { + var payload = new DalamudLinkPayload() {Plugin = pluginName, CommandId = commandId}; + this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction); + return payload; + } + + /// + /// Remove a registered link handler + /// + /// + /// + internal void RemoveChatLinkHandler(string pluginName, uint commandId) { + if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId))) { + this.dalamudLinkHandlers.Remove((pluginName, commandId)); + } + } + + /// + /// Remove all handlers owned by a plugin. + /// + /// + internal void RemoveChatLinkHandler(string pluginName) { + foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.pluginName == pluginName)) { + this.dalamudLinkHandlers.Remove(handler); + } + } + + private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr) { + try { + var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1); + + if (interactableType != Payload.EmbeddedInfoType.DalamudLink) { + this.interactableLinkClickedHook.Original(managerPtr, messagePtr); + return; + } + + Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); + + var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10); + var payloadSize = 3 + Marshal.ReadByte(payloadPtr, 2); + var payloadBytes = new byte[payloadSize]; + Marshal.Copy(payloadPtr, payloadBytes, 0, payloadSize); + var seStr = this.dalamud.SeStringManager.Parse(payloadBytes); + if (seStr.Payloads.Count == 0) return; + var payload = seStr.Payloads[0]; + + if (payload is DalamudLinkPayload link) { + if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId))) { + Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}"); + this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId); + } else { + Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}"); + } + } + } catch (Exception ex) { + Log.Error(ex, "Exception on InteractableLinkClicked hook"); + } + } + // Copyright (c) 2008-2013 Hafthor Stefansson // Distributed under the MIT/X11 software license // Ref: http://www.opensource.org/licenses/mit-license.php. diff --git a/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs index bc4d7b0f9..0620f93b5 100644 --- a/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs @@ -6,6 +6,7 @@ namespace Dalamud.Game.Internal.Gui { public IntPtr PrintMessage { get; private set; } public IntPtr PopulateItemLinkObject { get; private set; } + public IntPtr InteractableLinkClicked { get; private set; } public ChatGuiAddressResolver(IntPtr baseAddres) { BaseAddress = baseAddres; @@ -89,6 +90,8 @@ namespace Dalamud.Game.Internal.Gui { //PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0 PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); + + InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9; } } } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 1892a16f0..11c1d09d1 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -10,6 +10,7 @@ using Dalamud.Configuration; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.Chat.SeStringHandling; +using Dalamud.Game.Chat.SeStringHandling.Payloads; using Dalamud.Game.ClientState; using Dalamud.Game.Command; using Dalamud.Game.Internal; @@ -90,6 +91,7 @@ namespace Dalamud.Plugin /// public void Dispose() { this.UiBuilder.Dispose(); + this.Framework.Gui.Chat.RemoveChatLinkHandler(this.pluginName); } /// @@ -129,6 +131,34 @@ namespace Dalamud.Plugin return this.configs.Load(this.pluginName); } + #region Chat Links + + /// + /// Register a chat link handler. + /// + /// + /// + /// Returns an SeString payload for the link. + public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action commandAction) { + return this.Framework.Gui.Chat.AddChatLinkHandler(this.pluginName, commandId, commandAction); + } + + /// + /// Remove a chat link handler. + /// + /// + public void RemoveChatLinkHandler(uint commandId) { + this.Framework.Gui.Chat.RemoveChatLinkHandler(this.pluginName, commandId); + } + + /// + /// Removes all chat link handlers registered by the plugin. + /// + public void RemoveChatLinkHandler() { + this.Framework.Gui.Chat.RemoveChatLinkHandler(this.pluginName); + } + #endregion + #region IPC internal Action anyPluginIpcAction;