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/Chat/SeStringHandling/Payloads/RawPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/RawPayload.cs
index a2438d32a..8184dd477 100644
--- a/Dalamud/Game/Chat/SeStringHandling/Payloads/RawPayload.cs
+++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/RawPayload.cs
@@ -64,6 +64,14 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return $"{Type} - Data: {BitConverter.ToString(Data).Replace("-", " ")}";
}
+ public override bool Equals(object obj) {
+ if (obj is RawPayload rp) {
+ if (rp.Data.Length != this.Data.Length) return false;
+ return !Data.Where((t, i) => rp.Data[i] != t).Any();
+ }
+ return false;
+ }
+
protected override byte[] EncodeImpl()
{
var chunkLen = this.data.Length + 1;
diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs
index 78df4ae1f..2e4c00060 100644
--- a/Dalamud/Game/ChatHandlers.cs
+++ b/Dalamud/Game/ChatHandlers.cs
@@ -24,6 +24,8 @@ namespace Dalamud.Game {
private readonly Dalamud dalamud;
+ private DalamudLinkPayload openInstallerWindowLink;
+
private readonly Dictionary HandledChatTypeColors = new Dictionary {
{XivChatType.CrossParty, Color.DodgerBlue},
{XivChatType.Party, Color.DodgerBlue},
@@ -87,6 +89,10 @@ namespace Dalamud.Game {
dalamud.Framework.Gui.Chat.OnCheckMessageHandled += OnCheckMessageHandled;
dalamud.Framework.Gui.Chat.OnChatMessage += OnChatMessage;
+
+ this.openInstallerWindowLink = this.dalamud.Framework.Gui.Chat.AddChatLinkHandler("Dalamud", 1001, (i, m) => {
+ this.dalamud.OpenPluginInstaller();
+ });
}
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) {
@@ -233,7 +239,16 @@ namespace Dalamud.Game {
}
} else {
this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry {
- MessageBytes = Encoding.UTF8.GetBytes(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")),
+ MessageBytes = new SeString(new List() {
+ new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")),
+ new TextPayload(" ["),
+ new UIForegroundPayload(this.dalamud.Data, 500),
+ this.openInstallerWindowLink,
+ new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")),
+ RawPayload.LinkTerminator,
+ new UIForegroundPayload(this.dalamud.Data, 0),
+ new TextPayload("]"),
+ }).Encode(),
Type = XivChatType.Urgent
});
}
diff --git a/Dalamud/Game/Internal/Gui/ChatGui.cs b/Dalamud/Game/Internal/Gui/ChatGui.cs
index b55c4a040..87e11d8e5 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,76 @@ 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 messageSize = 0;
+ while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++;
+ var payloadBytes = new byte[messageSize];
+ Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize);
+ var seStr = this.dalamud.SeStringManager.Parse(payloadBytes);
+ var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
+ var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
+ if (payloads.Count == 0) return;
+ var linkPayload = payloads[0];
+ if (linkPayload 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, new SeString(payloads));
+ } 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..4a2ae9f41 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;