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;