Merge pull request #225 from Caraxi/dalamud-link

This commit is contained in:
goaaats 2020-12-19 18:11:53 +01:00 committed by GitHub
commit 8d969255b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 2 deletions

View file

@ -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
}

View file

@ -51,6 +51,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
/// </summary>
Quest,
/// <summary>
/// A SeString payload representing a custom clickable link for dalamud plugins
/// </summary>
DalamudLink,
/// <summary>
/// An SeString payload representing any data we don't handle.
/// </summary>
Unknown

View file

@ -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 {
/// <summary>
///
/// </summary>
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<byte> {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}";
}
}
}

View file

@ -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;

View file

@ -24,6 +24,8 @@ namespace Dalamud.Game {
private readonly Dalamud dalamud;
private DalamudLinkPayload openInstallerWindowLink;
private readonly Dictionary<XivChatType, Color> HandledChatTypeColors = new Dictionary<XivChatType, Color> {
{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<Payload>() {
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
});
}

View file

@ -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<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> 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<PopulateItemLinkDelegate>(Address.PopulateItemLinkObject,
new PopulateItemLinkDelegate(HandlePopulateItemLinkDetour),
this);
this.interactableLinkClickedHook =
new Hook<InteractableLinkClickedDelegate>(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<uint, SeString>> dalamudLinkHandlers = new Dictionary<(string, uint), Action<uint, SeString>>();
/// <summary>
/// Create a link handler
/// </summary>
/// <param name="pluginName"></param>
/// <param name="commandId"></param>
/// <param name="commandAction"></param>
/// <returns></returns>
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction) {
var payload = new DalamudLinkPayload() {Plugin = pluginName, CommandId = commandId};
this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
return payload;
}
/// <summary>
/// Remove a registered link handler
/// </summary>
/// <param name="pluginName"></param>
/// <param name="commandId"></param>
internal void RemoveChatLinkHandler(string pluginName, uint commandId) {
if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId))) {
this.dalamudLinkHandlers.Remove((pluginName, commandId));
}
}
/// <summary>
/// Remove all handlers owned by a plugin.
/// </summary>
/// <param name="pluginName"></param>
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.

View file

@ -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;
}
}
}

View file

@ -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
/// </summary>
public void Dispose() {
this.UiBuilder.Dispose();
this.Framework.Gui.Chat.RemoveChatLinkHandler(this.pluginName);
}
/// <summary>
@ -129,6 +131,34 @@ namespace Dalamud.Plugin
return this.configs.Load(this.pluginName);
}
#region Chat Links
/// <summary>
/// Register a chat link handler.
/// </summary>
/// <param name="commandId"></param>
/// <param name="commandAction"></param>
/// <returns>Returns an SeString payload for the link.</returns>
public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action<uint, SeString> commandAction) {
return this.Framework.Gui.Chat.AddChatLinkHandler(this.pluginName, commandId, commandAction);
}
/// <summary>
/// Remove a chat link handler.
/// </summary>
/// <param name="commandId"></param>
public void RemoveChatLinkHandler(uint commandId) {
this.Framework.Gui.Chat.RemoveChatLinkHandler(this.pluginName, commandId);
}
/// <summary>
/// Removes all chat link handlers registered by the plugin.
/// </summary>
public void RemoveChatLinkHandler() {
this.Framework.Gui.Chat.RemoveChatLinkHandler(this.pluginName);
}
#endregion
#region IPC
internal Action<string, ExpandoObject> anyPluginIpcAction;