Merge remote-tracking branch 'upstream/master' into flytext

This commit is contained in:
liam 2021-08-10 15:16:44 -04:00
commit 5812b5a9c0
110 changed files with 2605 additions and 1389 deletions

View file

@ -0,0 +1,63 @@
using System;
using Dalamud.Memory;
namespace Dalamud.Game.Gui.Addons
{
/// <summary>
/// This class represents an in-game UI "Addon".
/// </summary>
public unsafe class Addon
{
/// <summary>
/// Initializes a new instance of the <see cref="Addon"/> class.
/// </summary>
/// <param name="address">The address of the addon.</param>
public Addon(IntPtr address)
{
this.Address = address;
}
/// <summary>
/// Gets the address of the addon.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the name of the addon.
/// </summary>
public string Name => MemoryHelper.ReadString((IntPtr)this.Struct->Name, 0x20);
/// <summary>
/// Gets the X position of the addon on screen.
/// </summary>
public short X => this.Struct->X;
/// <summary>
/// Gets the Y position of the addon on screen.
/// </summary>
public short Y => this.Struct->Y;
/// <summary>
/// Gets the scale of the addon.
/// </summary>
public float Scale => this.Struct->Scale;
/// <summary>
/// Gets the width of the addon. This may include non-visible parts.
/// </summary>
public unsafe float Width => this.Struct->RootNode->Width * this.Scale;
/// <summary>
/// Gets the height of the addon. This may include non-visible parts.
/// </summary>
public unsafe float Height => this.Struct->RootNode->Height * this.Scale;
/// <summary>
/// Gets a value indicating whether the addon is visible.
/// </summary>
public bool Visible => this.Struct->IsVisible;
private FFXIVClientStructs.FFXIV.Component.GUI.AtkUnitBase* Struct => (FFXIVClientStructs.FFXIV.Component.GUI.AtkUnitBase*)this.Address;
}
}

463
Dalamud/Game/Gui/ChatGui.cs Normal file
View file

@ -0,0 +1,463 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.Libc;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Serilog;
namespace Dalamud.Game.Gui
{
/// <summary>
/// This class handles interacting with the native chat UI.
/// </summary>
public sealed class ChatGui : IDisposable
{
private readonly Dalamud dalamud;
private readonly ChatGuiAddressResolver address;
private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
private IntPtr baseAddress = IntPtr.Zero;
/// <summary>
/// Initializes a new instance of the <see cref="ChatGui"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the ChatManager.</param>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
internal ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new ChatGuiAddressResolver(baseAddress);
this.address.Setup(scanner);
Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}");
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
}
/// <summary>
/// A delegate type used with the <see cref="OnChatMessage"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
/// <param name="isHandled">A value indicating whether the message was handled or should be propagated.</param>
public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
/// <summary>
/// A delegate type used with the <see cref="OnCheckMessageHandled"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
/// <param name="isHandled">A value indicating whether the message was handled or should be propagated.</param>
public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
/// <summary>
/// A delegate type used with the <see cref="OnChatMessageHandled"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
/// <summary>
/// A delegate type used with the <see cref="OnChatMessageUnhandled"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr);
/// <summary>
/// Event that will be fired when a chat message is sent to chat by the game.
/// </summary>
public event OnMessageDelegate OnChatMessage;
/// <summary>
/// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true.
/// </summary>
public event OnCheckMessageHandledDelegate OnCheckMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageHandledDelegate OnChatMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is not handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageUnhandledDelegate OnChatMessageUnhandled;
/// <summary>
/// Gets the ID of the last linked item.
/// </summary>
public int LastLinkedItemId { get; private set; }
/// <summary>
/// Gets the flags of the last linked item.
/// </summary>
public byte LastLinkedItemFlags { get; private set; }
/// <summary>
/// Enables this module.
/// </summary>
public void Enable()
{
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose();
}
/// <summary>
/// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="chat">A message to send.</param>
public void PrintChat(XivChatEntry chat)
{
this.chatQueue.Enqueue(chat);
}
/// <summary>
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void Print(string message)
{
Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = this.dalamud.Configuration.GeneralChatType,
});
}
/// <summary>
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void Print(SeString message)
{
Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = this.dalamud.Configuration.GeneralChatType,
});
}
/// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// the queue, later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void PrintError(string message)
{
Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// the queue, later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void PrintError(SeString message)
{
Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Process a chat queue.
/// </summary>
/// <param name="framework">The Framework instance.</param>
public void UpdateQueue(Framework framework)
{
while (this.chatQueue.Count > 0)
{
var chat = this.chatQueue.Dequeue();
if (this.baseAddress == IntPtr.Zero)
{
continue;
}
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = framework.Libc.NewString(senderRaw);
var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = framework.Libc.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
}
}
/// <summary>
/// Create a link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to run.</param>
/// <param name="commandAction">The command action itself.</param>
/// <returns>A payload for handling.</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 all handlers owned by a plugin.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the links.</param>
internal void RemoveChatLinkHandler(string pluginName)
{
foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName))
{
this.dalamudLinkHandlers.Remove(handler);
}
}
/// <summary>
/// Remove a registered link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to be removed.</param>
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
{
if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId)))
{
this.dalamudLinkHandlers.Remove((pluginName, commandId));
}
}
private static unsafe bool FastByteArrayCompare(byte[] a1, byte[] a2)
{
// Copyright (c) 2008-2013 Hafthor Stefansson
// Distributed under the MIT/X11 software license
// Ref: http://www.opensource.org/licenses/mit-license.php.
// https://stackoverflow.com/a/8808245
if (a1 == a2) return true;
if (a1 == null || a2 == null || a1.Length != a2.Length)
return false;
fixed (byte* p1 = a1, p2 = a2)
{
byte* x1 = p1, x2 = p2;
var l = a1.Length;
for (var i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
{
if (*((long*)x1) != *((long*)x2))
return false;
}
if ((l & 4) != 0)
{
if (*((int*)x1) != *((int*)x2))
return false;
x1 += 4;
x2 += 4;
}
if ((l & 2) != 0)
{
if (*((short*)x1) != *((short*)x2))
return false;
x1 += 2;
x2 += 2;
}
if ((l & 1) != 0)
{
if (*((byte*)x1) != *((byte*)x2))
return false;
}
return true;
}
}
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr)
{
try
{
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8);
this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
Log.Verbose($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
}
catch (Exception ex)
{
Log.Error(ex, "Exception onPopulateItemLink hook.");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
}
}
private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chattype, IntPtr pSenderName, IntPtr pMessage, uint senderid, IntPtr parameter)
{
var retVal = IntPtr.Zero;
try
{
var sender = StdString.ReadFromPointer(pSenderName);
var message = StdString.ReadFromPointer(pMessage);
var parsedSender = this.dalamud.SeStringManager.Parse(sender.RawData);
var parsedMessage = this.dalamud.SeStringManager.Parse(message.RawData);
Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue);
// Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}");
var originalMessageData = (byte[])message.RawData.Clone();
var oldEdited = parsedMessage.Encode();
// Call events
var isHandled = false;
this.OnCheckMessageHandled?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
if (!isHandled)
{
this.OnChatMessage?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
}
var newEdited = parsedMessage.Encode();
if (!FastByteArrayCompare(oldEdited, newEdited))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
message.RawData = newEdited;
Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
var messagePtr = pMessage;
OwnedStdString allocatedString = null;
if (!FastByteArrayCompare(originalMessageData, message.RawData))
{
allocatedString = this.dalamud.Framework.Libc.NewString(message.RawData);
Log.Debug(
$"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
messagePtr = allocatedString.Address;
}
// Print the original chat if it's handled.
if (isHandled)
{
this.OnChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
}
else
{
retVal = this.printMessageHook.Original(manager, chattype, pSenderName, messagePtr, senderid, parameter);
this.OnChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
}
if (this.baseAddress == IntPtr.Zero)
this.baseAddress = manager;
allocatedString?.Dispose();
}
catch (Exception ex)
{
Log.Error(ex, "Exception on OnChatMessage hook.");
retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter);
}
return retVal;
}
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");
}
}
}
}

View file

@ -0,0 +1,120 @@
using System;
using Dalamud.Game.Internal;
namespace Dalamud.Game.Gui
{
/// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
/// </summary>
public sealed class ChatGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Initializes a new instance of the <see cref="ChatGuiAddressResolver"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the native ChatManager class.</param>
public ChatGuiAddressResolver(IntPtr baseAddress)
{
this.BaseAddress = baseAddress;
}
/// <summary>
/// Gets the base address of the native ChatManager class.
/// </summary>
public IntPtr BaseAddress { get; }
/// <summary>
/// Gets the address of the native PrintMessage method.
/// </summary>
public IntPtr PrintMessage { get; private set; }
/// <summary>
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
public IntPtr PopulateItemLinkObject { get; private set; }
/// <summary>
/// Gets the address of the native InteractableLinkClicked method.
/// </summary>
public IntPtr InteractableLinkClicked { get; private set; }
/*
--- for reference: 4.57 ---
.text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal)
.text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near
.text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p
.text:00000001405CD210 ; sub_140141D10+220p ...
.text:00000001405CD210
.text:00000001405CD210 var_220 = qword ptr -220h
.text:00000001405CD210 var_218 = byte ptr -218h
.text:00000001405CD210 var_210 = word ptr -210h
.text:00000001405CD210 var_208 = byte ptr -208h
.text:00000001405CD210 var_200 = word ptr -200h
.text:00000001405CD210 var_1FC = dword ptr -1FCh
.text:00000001405CD210 var_1F8 = qword ptr -1F8h
.text:00000001405CD210 var_1F0 = qword ptr -1F0h
.text:00000001405CD210 var_1E8 = qword ptr -1E8h
.text:00000001405CD210 var_1E0 = dword ptr -1E0h
.text:00000001405CD210 var_1DC = word ptr -1DCh
.text:00000001405CD210 var_1DA = word ptr -1DAh
.text:00000001405CD210 var_1D8 = qword ptr -1D8h
.text:00000001405CD210 var_1D0 = byte ptr -1D0h
.text:00000001405CD210 var_1C8 = qword ptr -1C8h
.text:00000001405CD210 var_1B0 = dword ptr -1B0h
.text:00000001405CD210 var_1AC = dword ptr -1ACh
.text:00000001405CD210 var_1A8 = dword ptr -1A8h
.text:00000001405CD210 var_1A4 = dword ptr -1A4h
.text:00000001405CD210 var_1A0 = dword ptr -1A0h
.text:00000001405CD210 var_160 = dword ptr -160h
.text:00000001405CD210 var_15C = dword ptr -15Ch
.text:00000001405CD210 var_140 = dword ptr -140h
.text:00000001405CD210 var_138 = dword ptr -138h
.text:00000001405CD210 var_130 = byte ptr -130h
.text:00000001405CD210 var_C0 = byte ptr -0C0h
.text:00000001405CD210 var_50 = qword ptr -50h
.text:00000001405CD210 var_38 = qword ptr -38h
.text:00000001405CD210 var_30 = qword ptr -30h
.text:00000001405CD210 var_28 = qword ptr -28h
.text:00000001405CD210 var_20 = qword ptr -20h
.text:00000001405CD210 senderActorId = dword ptr 30h
.text:00000001405CD210 isLocal = byte ptr 38h
.text:00000001405CD210
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck
.text:00000001405CD210 push rbp
.text:00000001405CD212 push rdi
.text:00000001405CD213 push r14
.text:00000001405CD215 push r15
.text:00000001405CD217 lea rbp, [rsp-128h]
.text:00000001405CD21F sub rsp, 228h
.text:00000001405CD226 mov rax, cs:__security_cookie
.text:00000001405CD22D xor rax, rsp
.text:00000001405CD230 mov [rbp+140h+var_50], rax
.text:00000001405CD237 xor r10b, r10b
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
.text:00000001405CD23F xor eax, eax
.text:00000001405CD241 mov r11, r9
.text:00000001405CD244 mov r14, r8
.text:00000001405CD247 mov r9d, eax
.text:00000001405CD24A movzx r15d, dx
.text:00000001405CD24E lea r8, [rcx+0C10h]
.text:00000001405CD255 mov rdi, rcx
*/
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1???
this.PrintMessage = sig.ScanText("40 55 53 56 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC 20 02 00 00 48 8B 05");
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old
// PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33");
// 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 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
// 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
this.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");
this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9;
}
}
}

View file

@ -0,0 +1,321 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.UI;
using Serilog;
namespace Dalamud.Game.Internal.Gui
{
/// <summary>
/// This class facilitates interacting with and creating native in-game "fly text".
/// </summary>
public sealed class FlyTextGui : IDisposable
{
/// <summary>
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
/// </summary>
private readonly AddFlyTextDelegate addFlyTextNative;
/// <summary>
/// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
private readonly Stopwatch hookTimer;
/// <summary>
/// Initializes a new instance of the <see cref="FlyTextGui"/> class.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
internal FlyTextGui(SigScanner scanner, Dalamud dalamud)
{
this.Dalamud = dalamud;
this.hookTimer = new Stopwatch();
this.Address = new FlyTextGuiAddressResolver();
this.Address.Setup(scanner);
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = new Hook<CreateFlyTextDelegate>(this.Address.CreateFlyText, this.CreateFlyTextDetour);
}
/// <summary>
/// The delegate defining the type for the FlyText event.
/// </summary>
/// <param name="kind">The FlyTextKind. See <see cref="FlyTextKind"/>.</param>
/// <param name="val1">Value1 passed to the native flytext function.</param>
/// <param name="val2">Value2 passed to the native flytext function. Seems unused.</param>
/// <param name="text1">Text1 passed to the native flytext function.</param>
/// <param name="text2">Text2 passed to the native flytext function.</param>
/// <param name="color">Color passed to the native flytext function. Changes flytext color.</param>
/// <param name="icon">Icon ID passed to the native flytext function. Only displays with select FlyTextKind.</param>
/// <param name="yOffset">The vertical offset to place the flytext at. 0 is default. Negative values result
/// in text appearing higher on the screen. This does not change where the element begins to fade.</param>
/// <param name="handled">Whether this flytext has been handled. If a subscriber sets this to true, the FlyText will not appear.</param>
public delegate void FlyTextDelegate(
ref FlyTextKind kind,
ref int val1,
ref int val2,
ref SeString text1,
ref SeString text2,
ref uint color,
ref uint icon,
ref float yOffset,
ref bool handled);
/// <summary>
/// Private delegate for the native CreateFlyText function's hook.
/// </summary>
private delegate IntPtr CreateFlyTextDelegate(
IntPtr addonFlyText,
int kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float unk3);
/// <summary>
/// Private delegate for the native AddFlyText function pointer.
/// </summary>
private delegate void AddFlyTextDelegate(
IntPtr addonFlyText,
uint actorIndex,
uint messageMax,
IntPtr numbers,
uint offsetNum,
uint offsetNumMax,
IntPtr strings,
uint offsetStr,
uint offsetStrMax,
int unknown);
/// <summary>
/// The FlyText event that can be subscribed to.
/// </summary>
public event FlyTextDelegate? OnFlyText;
private Dalamud Dalamud { get; }
private FlyTextGuiAddressResolver Address { get; }
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.createFlyTextHook.Disable();
this.createFlyTextHook.Dispose();
}
/// <summary>
/// Displays a fly text in-game on the local player.
/// </summary>
/// <param name="kind">The FlyTextKind. See <see cref="FlyTextKind"/>.</param>
/// <param name="actorIndex">The index of the actor to place flytext on. Indexing unknown. 1 places flytext on local player.</param>
/// <param name="val1">Value1 passed to the native flytext function.</param>
/// <param name="val2">Value2 passed to the native flytext function. Seems unused.</param>
/// <param name="text1">Text1 passed to the native flytext function.</param>
/// <param name="text2">Text2 passed to the native flytext function.</param>
/// <param name="color">Color passed to the native flytext function. Changes flytext color.</param>
/// <param name="icon">Icon ID passed to the native flytext function. Only displays with select FlyTextKind.</param>
public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon)
{
// Known valid flytext region within the atk arrays
int numIndex = 28;
int strIndex = 25;
uint numOffset = 147;
uint strOffset = 28;
// Get the UI module and flytext addon pointers
var ui = (UIModule*)this.Dalamud.Framework.Gui.GetUIModule();
var flytext = this.Dalamud.Framework.Gui.GetUiObjectByName("_FlyText", 1);
if (ui == null || flytext == IntPtr.Zero)
return;
// Get the number and string arrays we need
var atkArrayDataHolder = ui->RaptureAtkModule.AtkModule.AtkArrayDataHolder;
var numArray = atkArrayDataHolder._NumberArrays[numIndex];
var strArray = atkArrayDataHolder._StringArrays[strIndex];
// Write the values to the arrays using a known valid flytext region
// Whether or not to enable this set of values for displaying flytext
numArray->IntArray[numOffset + 0] = 1; // Some kind of "Enabled" flag for this section
numArray->IntArray[numOffset + 1] = (int)kind;
numArray->IntArray[numOffset + 2] = unchecked((int)val1);
numArray->IntArray[numOffset + 3] = unchecked((int)val2);
numArray->IntArray[numOffset + 4] = 5; // Unknown
numArray->IntArray[numOffset + 5] = unchecked((int)color);
numArray->IntArray[numOffset + 6] = unchecked((int)icon);
numArray->IntArray[numOffset + 7] = 0; // Unknown
numArray->IntArray[numOffset + 8] = 0; // Unknown, has something to do with yOffset
fixed (byte* pText1 = text1.Encode())
{
fixed (byte* pText2 = text2.Encode())
{
strArray->StringArray[strOffset + 0] = pText1;
strArray->StringArray[strOffset + 1] = pText2;
this.addFlyTextNative(
flytext,
actorIndex,
1,
(IntPtr)numArray,
numOffset,
9,
(IntPtr)strArray,
strOffset,
2,
0);
}
}
}
/// <summary>
/// Enables this module.
/// </summary>
internal void Enable()
{
this.createFlyTextHook.Enable();
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText,
int kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float yOffset)
{
var retVal = IntPtr.Zero;
try
{
this.hookTimer.Restart();
Log.Verbose("[FlyText] Enter CreateFlyText detour!");
var handled = false;
var tmpKind = (FlyTextKind)kind;
var tmpVal1 = val1;
var tmpVal2 = val2;
var tmpText1 = this.Dalamud.SeStringManager.Parse(text1);
var tmpText2 = this.Dalamud.SeStringManager.Parse(text2);
var tmpColor = color;
var tmpIcon = icon;
var tmpYOffset = yOffset;
var cmpText1 = tmpText1.ToString();
var cmpText2 = tmpText2.ToString();
Log.Verbose($"[FlyText] Called with addonFlyText({addonFlyText.ToInt64():X}) " +
$"kind({((FlyTextKind)kind).ToString()}) val1({val1}) val2({val2}) " +
$"text1({text1.ToInt64():X}, \"{tmpText1}\") text2({text2.ToInt64():X}, \"{tmpText2}\") " +
$"color({color:X}) icon({icon}) yOffset({yOffset})");
Log.Verbose("[FlyText] Calling flytext events!");
this.OnFlyText?.Invoke(
ref tmpKind,
ref tmpVal1,
ref tmpVal2,
ref tmpText1,
ref tmpText2,
ref tmpColor,
ref tmpIcon,
ref tmpYOffset,
ref handled);
// If handled, ignore the original call
if (handled)
{
Log.Verbose("[FlyText] FlyText was handled.");
// Returning null to AddFlyText from CreateFlyText will result
// in the operation being dropped entirely.
return IntPtr.Zero;
}
// Check if any values have changed
var dirty = tmpKind != (FlyTextKind)kind ||
tmpVal1 != val1 ||
tmpVal2 != val2 ||
tmpText1.ToString() != cmpText1 ||
tmpText2.ToString() != cmpText2 ||
tmpColor != color ||
tmpIcon != icon ||
Math.Abs(tmpYOffset - yOffset) > float.Epsilon;
// If not dirty, make the original call
if (!dirty)
{
Log.Verbose("[FlyText] Calling flytext with original args.");
return this.createFlyTextHook.Original(addonFlyText, kind, val1, val2, text2, color, icon, text1, yOffset);
}
var terminated1 = Terminate(tmpText1.Encode());
var terminated2 = Terminate(tmpText2.Encode());
var pText1 = Marshal.AllocHGlobal(terminated1.Length);
var pText2 = Marshal.AllocHGlobal(terminated2.Length);
Marshal.Copy(terminated1, 0, pText1, terminated1.Length);
Marshal.Copy(terminated2, 0, pText2, terminated2.Length);
Log.Verbose("[FlyText] Allocated and set strings.");
retVal = this.createFlyTextHook.Original(
addonFlyText,
(int)tmpKind,
tmpVal1,
tmpVal2,
pText2,
tmpColor,
tmpIcon,
pText1,
tmpYOffset);
Log.Verbose("[FlyText] Returned from original. Delaying free task.");
Task.Delay(2000).ContinueWith(_ =>
{
try
{
Marshal.FreeHGlobal(pText1);
Marshal.FreeHGlobal(pText2);
Log.Verbose("[FlyText] Freed strings.");
}
catch (Exception e)
{
Log.Verbose(e, "[FlyText] Exception occurred freeing strings in task.");
}
});
this.hookTimer.Stop();
Log.Verbose($"[FlyText] Hook took {this.hookTimer.ElapsedTicks} ticks, {this.hookTimer.ElapsedMilliseconds}ms.");
}
catch (Exception e)
{
Log.Error(e, "Exception occurred in CreateFlyTextDetour!");
}
return retVal;
}
private static byte[] Terminate(byte[] source)
{
var terminated = new byte[source.Length + 1];
Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
}
}
}

View file

@ -0,0 +1,32 @@
using System;
namespace Dalamud.Game.Internal.Gui
{
/// <summary>
/// An address resolver for the <see cref="FlyTextGui"/> class.
/// </summary>
public class FlyTextGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native AddFlyText method, which occurs
/// when the game adds fly text elements to the UI. Multiple fly text
/// elements can be added in a single AddFlyText call.
/// </summary>
public IntPtr AddFlyText { get; private set; }
/// <summary>
/// Gets the address of the native CreateFlyText method, which occurs
/// when the game creates a new fly text element. This method is called
/// once per fly text element, and can be called multiple times per
/// AddFlyText call.
/// </summary>
public IntPtr CreateFlyText { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.AddFlyText = sig.ScanText("E8 ?? ?? ?? ?? FF C7 41 D1 C7");
this.CreateFlyText = sig.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 48 63 FA");
}
}
}

View file

@ -0,0 +1,214 @@
namespace Dalamud.Game.Internal.Gui
{
/// <summary>
/// Enum of FlyTextKind values. Members suffixed with
/// a number seem to be a duplicate, or perform duplicate behavior.
/// </summary>
public enum FlyTextKind
{
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Used for autos and incoming DoTs.
/// </summary>
AutoAttack = 0,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Does a bounce effect on appearance.
/// </summary>
DirectHit = 1,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2
/// in sans-serif as subtitle. Does a bigger bounce effect on appearance.
/// </summary>
CriticalHit = 2,
/// <summary>
/// Val1 in even larger serif font with 2 exclamations, Text2 in
/// sans-serif as subtitle. Does a large bounce effect on appearance.
/// Does not scroll up or down the screen.
/// </summary>
CriticalDirectHit = 3,
/// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedAttack = 4,
/// <summary>
/// DirectHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedDirectHit = 5,
/// <summary>
/// CriticalHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedCriticalHit = 6,
/// <summary>
/// CriticalDirectHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedCriticalDirectHit = 7,
/// <summary>
/// All caps, serif MISS.
/// </summary>
Miss = 8,
/// <summary>
/// Sans-serif Text1 next to all caps serif MISS.
/// </summary>
NamedMiss = 9,
/// <summary>
/// All caps serif DODGE.
/// </summary>
Dodge = 10,
/// <summary>
/// Sans-serif Text1 next to all caps serif DODGE.
/// </summary>
NamedDodge = 11,
/// <summary>
/// Icon next to sans-serif Text1.
/// </summary>
NamedIcon = 12,
NamedIcon2 = 13,
/// <summary>
/// Serif Val1 with all caps condensed font EXP with Text2 in sans-serif as subtitle.
/// </summary>
Exp = 14,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle.
/// </summary>
NamedMp = 15,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle.
/// </summary>
NamedTp = 16,
NamedAttack2 = 17,
NamedMp2 = 18,
NamedTp2 = 19,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle.
/// </summary>
NamedEp = 20,
/// <summary>
/// Displays nothing.
/// </summary>
None = 21,
/// <summary>
/// All caps serif INVULNERABLE.
/// </summary>
Invulnerable = 22,
/// <summary>
/// All caps sans-serif condensed font INTERRUPTED!
/// Does a large bounce effect on appearance.
/// Does not scroll up or down the screen.
/// </summary>
Interrupted = 23,
/// <summary>
/// AutoAttack with no Text2.
/// </summary>
AutoAttackNoText = 24,
AutoAttackNoText2 = 25,
CriticalHit2 = 26,
AutoAttackNoText3 = 27,
NamedCriticalHit2 = 28,
/// <summary>
/// Same as NamedCriticalHit with a green (cannot change) MP in condensed font to the right of Val1.
/// Does a jiggle effect to the right on appearance.
/// </summary>
NamedCriticalHitWithMp = 29,
/// <summary>
/// Same as NamedCriticalHit with a yellow (cannot change) TP in condensed font to the right of Val1.
/// Does a jiggle effect to the right on appearance.
/// </summary>
NamedCriticalHitWithTp = 30,
/// <summary>
/// Same as NamedIcon with sans-serif "has no effect!" to the right.
/// </summary>
NamedIconHasNoEffect = 31,
/// <summary>
/// Same as NamedIcon but Text1 is slightly faded. Used for buff expiration.
/// </summary>
NamedIconFaded = 32,
NamedIconFaded2 = 33,
/// <summary>
/// Text1 in sans-serif font.
/// </summary>
Named = 34,
/// <summary>
/// Same as NamedIcon with sans-serif "(fully resisted)" to the right.
/// </summary>
NamedIconFullyResisted = 35,
/// <summary>
/// All caps serif 'INCAPACITATED!'.
/// </summary>
Incapacitated = 36,
/// <summary>
/// Text1 with sans-serif "(fully resisted)" to the right.
/// </summary>
NamedFullyResisted = 37,
/// <summary>
/// Text1 with sans-serif "has no effect!" to the right.
/// </summary>
NamedHasNoEffect = 38,
NamedAttack3 = 39,
NamedMp3 = 40,
NamedTp3 = 41,
/// <summary>
/// Same as NamedIcon with serif "INVULNERABLE!" beneath the Text1.
/// </summary>
NamedIconInvulnerable = 42,
/// <summary>
/// All caps serif RESIST.
/// </summary>
Resist = 43,
/// <summary>
/// Same as NamedIcon but places the given icon in the item icon outline.
/// </summary>
NamedIconWithItemOutline = 44,
AutoAttackNoText4 = 45,
CriticalHit3 = 46,
/// <summary>
/// All caps serif REFLECT.
/// </summary>
Reflect = 47,
/// <summary>
/// All caps serif REFLECTED.
/// </summary>
Reflected = 48,
DirectHit2 = 49,
CriticalHit5 = 50,
CriticalDirectHit2 = 51,
}
}

671
Dalamud/Game/Gui/GameGui.cs Normal file
View file

@ -0,0 +1,671 @@
using System;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Game.Gui.Addons;
using Dalamud.Game.Gui.PartyFinder;
using Dalamud.Game.Internal.Gui;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Interface;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Game.Gui
{
/// <summary>
/// A class handling many aspects of the in-game UI.
/// </summary>
public sealed class GameGui : IDisposable
{
private readonly Dalamud dalamud;
private readonly GameGuiAddressResolver address;
private readonly GetMatrixSingletonDelegate getMatrixSingleton;
private readonly GetUIObjectDelegate getUIObject;
private readonly ScreenToWorldNativeDelegate screenToWorldNative;
private readonly GetUIObjectByNameDelegate getUIObjectByName;
private readonly GetUiModuleDelegate getUiModule;
private readonly GetAgentModuleDelegate getAgentModule;
private readonly Hook<SetGlobalBgmDelegate> setGlobalBgmHook;
private readonly Hook<HandleItemHoverDelegate> handleItemHoverHook;
private readonly Hook<HandleItemOutDelegate> handleItemOutHook;
private readonly Hook<HandleActionHoverDelegate> handleActionHoverHook;
private readonly Hook<HandleActionOutDelegate> handleActionOutHook;
private readonly Hook<ToggleUiHideDelegate> toggleUiHideHook;
private GetUIMapObjectDelegate getUIMapObject;
private OpenMapWithFlagDelegate openMapWithFlag;
/// <summary>
/// Initializes a new instance of the <see cref="GameGui"/> class.
/// This class is responsible for many aspects of interacting with the native game UI.
/// </summary>
/// <param name="baseAddress">The base address of the native GuiManager class.</param>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
internal GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new GameGuiAddressResolver(baseAddress);
this.address.Setup(scanner);
Log.Verbose("===== G A M E G U I =====");
Log.Verbose($"GameGuiManager address 0x{this.address.BaseAddress.ToInt64():X}");
Log.Verbose($"SetGlobalBgm address 0x{this.address.SetGlobalBgm.ToInt64():X}");
Log.Verbose($"HandleItemHover address 0x{this.address.HandleItemHover.ToInt64():X}");
Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}");
Log.Verbose($"GetUIObject address 0x{this.address.GetUIObject.ToInt64():X}");
Log.Verbose($"GetAgentModule address 0x{this.address.GetAgentModule.ToInt64():X}");
this.Chat = new ChatGui(this.address.ChatManager, scanner, dalamud);
this.PartyFinder = new PartyFinderGui(scanner, dalamud);
this.Toast = new ToastGui(scanner, dalamud);
this.FlyText = new FlyTextGui(scanner, dalamud);
this.setGlobalBgmHook = new Hook<SetGlobalBgmDelegate>(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour);
this.handleItemHoverHook = new Hook<HandleItemHoverDelegate>(this.address.HandleItemHover, this.HandleItemHoverDetour);
this.handleItemOutHook = new Hook<HandleItemOutDelegate>(this.address.HandleItemOut, this.HandleItemOutDetour);
this.handleActionHoverHook = new Hook<HandleActionHoverDelegate>(this.address.HandleActionHover, this.HandleActionHoverDetour);
this.handleActionOutHook = new Hook<HandleActionOutDelegate>(this.address.HandleActionOut, this.HandleActionOutDetour);
this.getUIObject = Marshal.GetDelegateForFunctionPointer<GetUIObjectDelegate>(this.address.GetUIObject);
this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer<GetMatrixSingletonDelegate>(this.address.GetMatrixSingleton);
this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer<ScreenToWorldNativeDelegate>(this.address.ScreenToWorld);
this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(this.address.ToggleUiHide, this.ToggleUiHideDetour);
this.GetBaseUIObject = Marshal.GetDelegateForFunctionPointer<GetBaseUIObjectDelegate>(this.address.GetBaseUIObject);
this.getUIObjectByName = Marshal.GetDelegateForFunctionPointer<GetUIObjectByNameDelegate>(this.address.GetUIObjectByName);
this.getUiModule = Marshal.GetDelegateForFunctionPointer<GetUiModuleDelegate>(this.address.GetUIModule);
this.getAgentModule = Marshal.GetDelegateForFunctionPointer<GetAgentModuleDelegate>(this.address.GetAgentModule);
}
// Marshaled delegates
/// <summary>
/// The delegate type of the native method that gets the Client::UI::UIModule address.
/// </summary>
/// <returns>The Client::UI::UIModule address.</returns>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate IntPtr GetBaseUIObjectDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr GetMatrixSingletonDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr GetUIObjectDelegate();
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private unsafe delegate bool ScreenToWorldNativeDelegate(float* camPos, float* clipPos, float rayDistance, float* worldPos, int* unknown);
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
private delegate IntPtr GetUIObjectByNameDelegate(IntPtr thisPtr, string uiName, int index);
private delegate IntPtr GetUiModuleDelegate(IntPtr basePtr);
private delegate IntPtr GetAgentModuleDelegate(IntPtr uiModule);
// Hooked delegates
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetUIMapObjectDelegate(IntPtr uiObject);
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
private delegate bool OpenMapWithFlagDelegate(IntPtr uiMapObject, string flag);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetGlobalBgmDelegate(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemHoverDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemOutDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void HandleActionHoverDelegate(IntPtr hoverState, HoverActionKind a2, uint a3, int a4, byte a5);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleActionOutDelegate(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte);
/// <summary>
/// Event which is fired when the game UI hiding is toggled.
/// </summary>
public event EventHandler<bool> OnUiHideToggled;
/// <summary>
/// Gets a callable delegate for the GetBaseUIObject game method.
/// </summary>
/// <returns>The Client::UI::UIModule address.</returns>
public GetBaseUIObjectDelegate GetBaseUIObject { get; }
/// <summary>
/// Gets the <see cref="Chat"/> instance.
/// </summary>
public ChatGui Chat { get; private set; }
/// <summary>
/// Gets the <see cref="PartyFinder"/> instance.
/// </summary>
public PartyFinderGui PartyFinder { get; private set; }
/// <summary>
/// Gets the <see cref="Toast"/> instance.
/// </summary>
public ToastGui Toast { get; private set; }
/// <summary>
/// Gets the <see cref="FlyText"/> instance.
/// </summary>
public FlyTextGui FlyText { get; private set; }
/// <summary>
/// Gets a value indicating whether the game UI is hidden.
/// </summary>
public bool GameUiHidden { get; private set; }
/// <summary>
/// Gets or sets the item ID that is currently hovered by the player. 0 when no item is hovered.
/// If > 1.000.000, subtract 1.000.000 and treat it as HQ.
/// </summary>
public ulong HoveredItem { get; set; }
/// <summary>
/// Gets the action ID that is current hovered by the player. 0 when no action is hovered.
/// </summary>
public HoveredAction HoveredAction { get; } = new HoveredAction();
/// <summary>
/// Gets or sets the event that is fired when the currently hovered item changes.
/// </summary>
public EventHandler<ulong> HoveredItemChanged { get; set; }
/// <summary>
/// Gets or sets the event that is fired when the currently hovered action changes.
/// </summary>
public EventHandler<HoveredAction> HoveredActionChanged { get; set; }
/// <summary>
/// Opens the in-game map with a flag on the location of the parameter.
/// </summary>
/// <param name="mapLink">Link to the map to be opened.</param>
/// <returns>True if there were no errors and it could open the map.</returns>
public bool OpenMapWithMapLink(MapLinkPayload mapLink)
{
var uiObjectPtr = this.getUIObject();
if (uiObjectPtr.Equals(IntPtr.Zero))
{
Log.Error("OpenMapWithMapLink: Null pointer returned from getUIObject()");
return false;
}
this.getUIMapObject = this.address.GetVirtualFunction<GetUIMapObjectDelegate>(uiObjectPtr, 0, 8);
var uiMapObjectPtr = this.getUIMapObject(uiObjectPtr);
if (uiMapObjectPtr.Equals(IntPtr.Zero))
{
Log.Error("OpenMapWithMapLink: Null pointer returned from GetUIMapObject()");
return false;
}
this.openMapWithFlag = this.address.GetVirtualFunction<OpenMapWithFlagDelegate>(uiMapObjectPtr, 0, 63);
var mapLinkString = mapLink.DataString;
Log.Debug($"OpenMapWithMapLink: Opening Map Link: {mapLinkString}");
return this.openMapWithFlag(uiMapObjectPtr, mapLinkString);
}
/// <summary>
/// Converts in-world coordinates to screen coordinates (upper left corner origin).
/// </summary>
/// <param name="worldPos">Coordinates in the world.</param>
/// <param name="screenPos">Converted coordinates.</param>
/// <returns>True if worldPos corresponds to a position in front of the camera.</returns>
public bool WorldToScreen(SharpDX.Vector3 worldPos, out SharpDX.Vector2 screenPos)
{
// Get base object with matrices
var matrixSingleton = this.getMatrixSingleton();
// Read current ViewProjectionMatrix plus game window size
var viewProjectionMatrix = default(SharpDX.Matrix);
float width, height;
var windowPos = ImGuiHelpers.MainViewport.Pos;
unsafe
{
var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer();
for (var i = 0; i < 16; i++, rawMatrix++)
viewProjectionMatrix[i] = *rawMatrix;
width = *rawMatrix;
height = *(rawMatrix + 1);
}
SharpDX.Vector3.Transform(ref worldPos, ref viewProjectionMatrix, out SharpDX.Vector3 pCoords);
screenPos = new SharpDX.Vector2(pCoords.X / pCoords.Z, pCoords.Y / pCoords.Z);
screenPos.X = (0.5f * width * (screenPos.X + 1f)) + windowPos.X;
screenPos.Y = (0.5f * height * (1f - screenPos.Y)) + windowPos.Y;
return pCoords.Z > 0 &&
screenPos.X > windowPos.X && screenPos.X < windowPos.X + width &&
screenPos.Y > windowPos.Y && screenPos.Y < windowPos.Y + height;
}
/// <summary>
/// Converts in-world coordinates to screen coordinates (upper left corner origin).
/// </summary>
/// <param name="worldPos">Coordinates in the world.</param>
/// <param name="screenPos">Converted coordinates.</param>
/// <returns>True if worldPos corresponds to a position in front of the camera.</returns>
/// <remarks>
/// This overload requires a conversion to SharpDX vectors, however the penalty should be negligible.
/// </remarks>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos)
{
var result = this.WorldToScreen(worldPos.ToSharpDX(), out var sharpScreenPos);
screenPos = sharpScreenPos.ToSystem();
return result;
}
/// <summary>
/// Converts in-world coordinates to screen coordinates (upper left corner origin).
/// </summary>
/// <param name="worldPos">Coordinates in the world.</param>
/// <param name="screenPos">Converted coordinates.</param>
/// <returns>True if worldPos corresponds to a position in front of the camera.</returns>
/// <remarks>
/// This overload requires a conversion to SharpDX vectors, however the penalty should be negligible.
/// </remarks>
public bool WorldToScreen(Position3 worldPos, out Vector2 screenPos)
{
// This overload is necessary due to Positon3 implicit operators.
var result = this.WorldToScreen((SharpDX.Vector3)worldPos, out var sharpScreenPos);
screenPos = sharpScreenPos.ToSystem();
return result;
}
/// <summary>
/// Converts screen coordinates to in-world coordinates via raycasting.
/// </summary>
/// <param name="screenPos">Screen coordinates.</param>
/// <param name="worldPos">Converted coordinates.</param>
/// <param name="rayDistance">How far to search for a collision.</param>
/// <returns>True if successful. On false, worldPos's contents are undefined.</returns>
public bool ScreenToWorld(SharpDX.Vector2 screenPos, out SharpDX.Vector3 worldPos, float rayDistance = 100000.0f)
{
// The game is only visible in the main viewport, so if the cursor is outside
// of the game window, do not bother calculating anything
var windowPos = ImGuiHelpers.MainViewport.Pos;
var windowSize = ImGuiHelpers.MainViewport.Size;
if (screenPos.X < windowPos.X || screenPos.X > windowPos.X + windowSize.X ||
screenPos.Y < windowPos.Y || screenPos.Y > windowPos.Y + windowSize.Y)
{
worldPos = default;
return false;
}
// Get base object with matrices
var matrixSingleton = this.getMatrixSingleton();
// Read current ViewProjectionMatrix plus game window size
var viewProjectionMatrix = default(SharpDX.Matrix);
float width, height;
unsafe
{
var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer();
for (var i = 0; i < 16; i++, rawMatrix++)
viewProjectionMatrix[i] = *rawMatrix;
width = *rawMatrix;
height = *(rawMatrix + 1);
}
viewProjectionMatrix.Invert();
var localScreenPos = new SharpDX.Vector2(screenPos.X - windowPos.X, screenPos.Y - windowPos.Y);
var screenPos3D = new SharpDX.Vector3
{
X = (localScreenPos.X / width * 2.0f) - 1.0f,
Y = -((localScreenPos.Y / height * 2.0f) - 1.0f),
Z = 0,
};
SharpDX.Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPos);
screenPos3D.Z = 1;
SharpDX.Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPosOne);
var clipPos = camPosOne - camPos;
clipPos.Normalize();
bool isSuccess;
unsafe
{
var camPosArray = camPos.ToArray();
var clipPosArray = clipPos.ToArray();
// This array is larger than necessary because it contains more info than we currently use
var worldPosArray = stackalloc float[32];
// Theory: this is some kind of flag on what type of things the ray collides with
var unknown = stackalloc int[3]
{
0x4000,
0x4000,
0x0,
};
fixed (float* pCamPos = camPosArray)
{
fixed (float* pClipPos = clipPosArray)
{
isSuccess = this.screenToWorldNative(pCamPos, pClipPos, rayDistance, worldPosArray, unknown);
}
}
worldPos = new SharpDX.Vector3
{
X = worldPosArray[0],
Y = worldPosArray[1],
Z = worldPosArray[2],
};
}
return isSuccess;
}
/// <summary>
/// Converts screen coordinates to in-world coordinates via raycasting.
/// </summary>
/// <param name="screenPos">Screen coordinates.</param>
/// <param name="worldPos">Converted coordinates.</param>
/// <param name="rayDistance">How far to search for a collision.</param>
/// <returns>True if successful. On false, worldPos's contents are undefined.</returns>
/// <remarks>
/// This overload requires a conversion to SharpDX vectors, however the penalty should be negligible.
/// </remarks>
public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f)
{
var result = this.ScreenToWorld(screenPos.ToSharpDX(), out var sharpworldPos);
worldPos = sharpworldPos.ToSystem();
return result;
}
/// <summary>
/// Gets a pointer to the game's UI module.
/// </summary>
/// <returns>IntPtr pointing to UI module.</returns>
public IntPtr GetUIModule() => this.getUiModule(this.dalamud.Framework.Address.BaseAddress);
/// <summary>
/// Gets the pointer to the UI Object with the given name and index.
/// </summary>
/// <param name="name">Name of UI to find.</param>
/// <param name="index">Index of UI to find (1-indexed).</param>
/// <returns>IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the UI Object.</returns>
public IntPtr GetUiObjectByName(string name, int index)
{
var baseUi = this.GetBaseUIObject();
if (baseUi == IntPtr.Zero) return IntPtr.Zero;
var baseUiProperties = Marshal.ReadIntPtr(baseUi, 0x20);
if (baseUiProperties == IntPtr.Zero) return IntPtr.Zero;
return this.getUIObjectByName(baseUiProperties, name, index);
}
/// <summary>
/// Gets an Addon by it's internal name.
/// </summary>
/// <param name="name">The addon name.</param>
/// <param name="index">The index of the addon, starting at 1.</param>
/// <returns>The native memory representation of the addon, if it exists.</returns>
public Addon GetAddonByName(string name, int index)
{
var address = this.GetUiObjectByName(name, index);
if (address == IntPtr.Zero)
return null;
return new Addon(address);
}
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addonName">The addon name.</param>
/// <returns>A pointer to the agent interface.</returns>
public IntPtr FindAgentInterface(string addonName)
{
var addon = this.dalamud.Framework.Gui.GetUiObjectByName(addonName, 1);
return this.FindAgentInterface(addon);
}
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addon">The addon address.</param>
/// <returns>A pointer to the agent interface.</returns>
public IntPtr FindAgentInterface(IntPtr addon)
{
if (addon == IntPtr.Zero)
return IntPtr.Zero;
var uiModule = this.dalamud.Framework.Gui.GetUIModule();
if (uiModule == IntPtr.Zero)
{
return IntPtr.Zero;
}
var agentModule = this.getAgentModule(uiModule);
if (agentModule == IntPtr.Zero)
{
return IntPtr.Zero;
}
var id = Marshal.ReadInt16(addon, 0x1CE);
if (id == 0)
id = Marshal.ReadInt16(addon, 0x1CC);
if (id == 0)
return IntPtr.Zero;
for (var i = 0; i < 380; i++)
{
var agent = Marshal.ReadIntPtr(agentModule, 0x20 + (i * 8));
if (agent == IntPtr.Zero)
continue;
if (Marshal.ReadInt32(agent, 0x20) == id)
return agent;
}
return IntPtr.Zero;
}
/// <summary>
/// Set the current background music.
/// </summary>
/// <param name="bgmKey">The background music key.</param>
public void SetBgm(ushort bgmKey) => this.setGlobalBgmHook.Original(bgmKey, 0, 0, 0, 0, 0);
/// <summary>
/// Enables the hooks and submodules of this module.
/// </summary>
public void Enable()
{
this.Chat.Enable();
this.Toast.Enable();
this.FlyText.Enable();
this.PartyFinder.Enable();
this.setGlobalBgmHook.Enable();
this.handleItemHoverHook.Enable();
this.handleItemOutHook.Enable();
this.toggleUiHideHook.Enable();
this.handleActionHoverHook.Enable();
this.handleActionOutHook.Enable();
}
/// <summary>
/// Disables the hooks and submodules of this module.
/// </summary>
public void Dispose()
{
this.Chat.Dispose();
this.Toast.Dispose();
this.FlyText.Dispose();
this.PartyFinder.Dispose();
this.setGlobalBgmHook.Dispose();
this.handleItemHoverHook.Dispose();
this.handleItemOutHook.Dispose();
this.toggleUiHideHook.Dispose();
this.handleActionHoverHook.Dispose();
this.handleActionOutHook.Dispose();
}
private IntPtr HandleSetGlobalBgmDetour(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6)
{
var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6);
Log.Verbose("SetGlobalBgm: {0} {1} {2} {3} {4} {5} -> {6}", bgmKey, a2, a3, a4, a5, a6, retVal);
return retVal;
}
private IntPtr HandleItemHoverDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4)
{
var retVal = this.handleItemHoverHook.Original(hoverState, a2, a3, a4);
if (retVal.ToInt64() == 22)
{
var itemId = (ulong)Marshal.ReadInt32(hoverState, 0x138);
this.HoveredItem = itemId;
try
{
this.HoveredItemChanged?.Invoke(this, itemId);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverItemId:{0} this:{1}", itemId, hoverState.ToInt64().ToString("X"));
}
return retVal;
}
private IntPtr HandleItemOutDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4)
{
var retVal = this.handleItemOutHook.Original(hoverState, a2, a3, a4);
if (a3 != IntPtr.Zero && a4 == 1)
{
var a3Val = Marshal.ReadByte(a3, 0x8);
if (a3Val == 255)
{
this.HoveredItem = 0ul;
try
{
this.HoveredItemChanged?.Invoke(this, 0ul);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverItemId: 0");
}
}
return retVal;
}
private void HandleActionHoverDetour(IntPtr hoverState, HoverActionKind actionKind, uint actionId, int a4, byte a5)
{
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
this.HoveredAction.ActionKind = actionKind;
this.HoveredAction.BaseActionID = actionId;
this.HoveredAction.ActionID = (uint)Marshal.ReadInt32(hoverState, 0x3C);
try
{
this.HoveredActionChanged?.Invoke(this, this.HoveredAction);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverActionId: {0}/{1} this:{2}", actionKind, actionId, hoverState.ToInt64().ToString("X"));
}
private IntPtr HandleActionOutDetour(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4)
{
var retVal = this.handleActionOutHook.Original(agentActionDetail, a2, a3, a4);
if (a3 != IntPtr.Zero && a4 == 1)
{
var a3Val = Marshal.ReadByte(a3, 0x8);
if (a3Val == 255)
{
this.HoveredAction.ActionKind = HoverActionKind.None;
this.HoveredAction.BaseActionID = 0;
this.HoveredAction.ActionID = 0;
try
{
this.HoveredActionChanged?.Invoke(this, this.HoveredAction);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredActionChanged event.");
}
Log.Verbose("HoverActionId: 0");
}
}
return retVal;
}
private IntPtr ToggleUiHideDetour(IntPtr thisPtr, byte unknownByte)
{
this.GameUiHidden = !this.GameUiHidden;
try
{
this.OnUiHideToggled?.Invoke(this, this.GameUiHidden);
}
catch (Exception ex)
{
Log.Error(ex, "Error on OnUiHideToggled event dispatch");
}
Log.Debug("UiHide toggled: {0}", this.GameUiHidden);
return this.toggleUiHideHook.Original(thisPtr, unknownByte);
}
}
}

View file

@ -0,0 +1,123 @@
using System;
using System.Runtime.InteropServices;
namespace Dalamud.Game.Gui
{
/// <summary>
/// The address resolver for the <see cref="GameGui"/> class.
/// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Initializes a new instance of the <see cref="GameGuiAddressResolver"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the native GuiManager class.</param>
public GameGuiAddressResolver(IntPtr baseAddress)
{
this.BaseAddress = baseAddress;
}
/// <summary>
/// Gets the base address of the native GuiManager class.
/// </summary>
public IntPtr BaseAddress { get; private set; }
/// <summary>
/// Gets the address of the native ChatManager class.
/// </summary>
public IntPtr ChatManager { get; private set; }
/// <summary>
/// Gets the address of the native SetGlobalBgm method.
/// </summary>
public IntPtr SetGlobalBgm { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemHover method.
/// </summary>
public IntPtr HandleItemHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemOut method.
/// </summary>
public IntPtr HandleItemOut { get; private set; }
/// <summary>
/// Gets the address of the native HandleActionHover method.
/// </summary>
public IntPtr HandleActionHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleActionOut method.
/// </summary>
public IntPtr HandleActionOut { get; private set; }
/// <summary>
/// Gets the address of the native GetUIObject method.
/// </summary>
public IntPtr GetUIObject { get; private set; }
/// <summary>
/// Gets the address of the native GetMatrixSingleton method.
/// </summary>
public IntPtr GetMatrixSingleton { get; private set; }
/// <summary>
/// Gets the address of the native ScreenToWorld method.
/// </summary>
public IntPtr ScreenToWorld { get; private set; }
/// <summary>
/// Gets the address of the native ToggleUiHide method.
/// </summary>
public IntPtr ToggleUiHide { get; private set; }
/// <summary>
/// Gets the address of the native Client::UI::UIModule getter method.
/// </summary>
public IntPtr GetBaseUIObject { get; private set; }
/// <summary>
/// Gets the address of the native GetUIObjectByName method.
/// </summary>
public IntPtr GetUIObjectByName { get; private set; }
/// <summary>
/// Gets the address of the native GetUIModule method.
/// </summary>
public IntPtr GetUIModule { get; private set; }
/// <summary>
/// Gets the address of the native GetAgentModule method.
/// </summary>
public IntPtr GetAgentModule { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.SetGlobalBgm = sig.ScanText("4C 8B 15 ?? ?? ?? ?? 4D 85 D2 74 58");
this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ??");
this.HandleItemOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 4D");
this.HandleActionHover = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 83 F8 0F");
this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F");
this.GetUIObject = sig.ScanText("E8 ?? ?? ?? ?? 48 8B C8 48 8B 10 FF 52 40 80 88 ?? ?? ?? ?? 01 E9");
this.GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??");
this.ScreenToWorld = sig.ScanText("48 83 EC 48 48 8B 05 ?? ?? ?? ?? 4D 8B D1");
this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??");
this.GetBaseUIObject = sig.ScanText("E8 ?? ?? ?? ?? 41 B8 01 00 00 00 48 8D 15 ?? ?? ?? ?? 48 8B 48 20 E8 ?? ?? ?? ?? 48 8B CF");
this.GetUIObjectByName = sig.ScanText("E8 ?? ?? ?? ?? 48 8B CF 48 89 87 ?? ?? 00 00 E8 ?? ?? ?? ?? 41 B8 01 00 00 00");
this.GetUIModule = sig.ScanText("E8 ?? ?? ?? ?? 48 8B C8 48 85 C0 75 2D");
var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28");
this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size);
}
/// <inheritdoc/>
protected override void SetupInternal(SigScanner scanner)
{
// Xiv__UiManager__GetChatManager 000 lea rax, [rcx+13E0h]
// Xiv__UiManager__GetChatManager+7 000 retn
this.ChatManager = this.BaseAddress + 0x13E0;
}
}
}

View file

@ -0,0 +1,49 @@
namespace Dalamud.Game.Gui
{
/// <summary>
/// ActionKinds used in AgentActionDetail.
/// These describe the possible kinds of actions being hovered.
/// </summary>
public enum HoverActionKind
{
/// <summary>
/// No action is hovered.
/// </summary>
None = 0,
/// <summary>
/// A regular action is hovered.
/// </summary>
Action = 21,
/// <summary>
/// A general action is hovered.
/// </summary>
GeneralAction = 23,
/// <summary>
/// A companion order type of action is hovered.
/// </summary>
CompanionOrder = 24,
/// <summary>
/// A main command type of action is hovered.
/// </summary>
MainCommand = 25,
/// <summary>
/// An extras command type of action is hovered.
/// </summary>
ExtraCommand = 26,
/// <summary>
/// A pet order type of action is hovered.
/// </summary>
PetOrder = 28,
/// <summary>
/// A trait is hovered.
/// </summary>
Trait = 29,
}
}

View file

@ -0,0 +1,23 @@
namespace Dalamud.Game.Gui
{
/// <summary>
/// This class represents the hotbar action currently hovered over by the cursor.
/// </summary>
public class HoveredAction
{
/// <summary>
/// Gets or sets the base action ID.
/// </summary>
public uint BaseActionID { get; set; } = 0;
/// <summary>
/// Gets or sets the action ID accounting for automatic upgrades.
/// </summary>
public uint ActionID { get; set; } = 0;
/// <summary>
/// Gets or sets the type of action.
/// </summary>
public HoverActionKind ActionKind { get; set; } = HoverActionKind.None;
}
}

View file

@ -0,0 +1,28 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace Dalamud.Game.Gui.PartyFinder.Internal
{
/// <summary>
/// The structure of the PartyFinder packet.
/// </summary>
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Sequential struct marshaling.")]
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Sequential struct marshaling.")]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the field usage.")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct PartyFinderPacket
{
/// <summary>
/// Gets the size of this packet.
/// </summary>
internal static int PacketSize { get; } = Marshal.SizeOf<PartyFinderPacket>();
internal readonly int BatchNumber;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] padding1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
internal readonly PartyFinderPacketListing[] Listings;
}
}

View file

@ -0,0 +1,99 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
namespace Dalamud.Game.Gui.PartyFinder.Internal
{
/// <summary>
/// The structure of an individual listing within a packet.
/// </summary>
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Sequential struct marshaling.")]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the field usage.")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct PartyFinderPacketListing
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
private readonly byte[] header1;
internal readonly uint Id;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
private readonly byte[] header2;
internal readonly uint ContentIdLower;
private readonly ushort unknownShort1;
private readonly ushort unknownShort2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
private readonly byte[] header3;
internal readonly byte Category;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header4;
internal readonly ushort Duty;
internal readonly byte DutyType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)]
private readonly byte[] header5;
internal readonly ushort World;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] header6;
internal readonly byte Objective;
internal readonly byte BeginnersWelcome;
internal readonly byte Conditions;
internal readonly byte DutyFinderSettings;
internal readonly byte LootRules;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header7; // all zero in every pf I've examined
private readonly uint lastPatchHotfixTimestamp; // last time the servers were restarted?
internal readonly ushort SecondsRemaining;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
private readonly byte[] header8; // 00 00 01 00 00 00 in every pf I've examined
internal readonly ushort MinimumItemLevel;
internal readonly ushort HomeWorld;
internal readonly ushort CurrentWorld;
private readonly byte header9;
internal readonly byte NumSlots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header10;
internal readonly byte SearchArea;
private readonly byte header11;
internal readonly byte NumParties;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header12; // 00 00 00 always. maybe numParties is a u32?
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly uint[] Slots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly byte[] JobsPresent;
// Note that ByValTStr will not work here because the strings are UTF-8 and there's only a CharSet for UTF-16 in C#.
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
internal readonly byte[] Name;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 192)]
internal readonly byte[] Description;
internal bool IsNull()
{
// a valid party finder must have at least one slot set
return this.Slots.All(slot => slot == 0);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
namespace Dalamud.Game.Gui.PartyFinder
{
/// <summary>
/// The address resolver for the <see cref="PartyFinderGui"/> class.
/// </summary>
public class PartyFinderAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native ReceiveListing method.
/// </summary>
public IntPtr ReceiveListing { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.ReceiveListing = sig.ScanText("40 53 41 57 48 83 EC 28 48 8B D9");
}
}
}

View file

@ -0,0 +1,133 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game.Gui.PartyFinder.Internal;
using Dalamud.Game.Gui.PartyFinder.Types;
using Dalamud.Hooking;
using Serilog;
namespace Dalamud.Game.Gui.PartyFinder
{
/// <summary>
/// This class handles interacting with the native PartyFinder window.
/// </summary>
public sealed class PartyFinderGui : IDisposable
{
private readonly Dalamud dalamud;
private readonly PartyFinderAddressResolver address;
private readonly IntPtr memory;
private readonly Hook<ReceiveListingDelegate> receiveListingHook;
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
internal PartyFinderGui(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new PartyFinderAddressResolver();
this.address.Setup(scanner);
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
this.receiveListingHook = new Hook<ReceiveListingDelegate>(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
}
/// <summary>
/// Event type fired each time the game receives an individual Party Finder listing.
/// Cannot modify listings but can hide them.
/// </summary>
/// <param name="listing">The listings received.</param>
/// <param name="args">Additional arguments passed by the game.</param>
public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data);
/// <summary>
/// Event fired each time the game receives an individual Party Finder listing.
/// Cannot modify listings but can hide them.
/// </summary>
public event PartyFinderListingEventDelegate ReceiveListing;
/// <summary>
/// Enables this module.
/// </summary>
public void Enable()
{
this.receiveListingHook.Enable();
}
/// <summary>
/// Dispose of m anaged and unmanaged resources.
/// </summary>
public void Dispose()
{
this.receiveListingHook.Dispose();
Marshal.FreeHGlobal(this.memory);
}
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
{
try
{
this.HandleListingEvents(data);
}
catch (Exception ex)
{
Log.Error(ex, "Exception on ReceiveListing hook.");
}
this.receiveListingHook.Original(managerPtr, data);
}
private void HandleListingEvents(IntPtr data)
{
var dataPtr = data + 0x10;
var packet = Marshal.PtrToStructure<PartyFinderPacket>(dataPtr);
// rewriting is an expensive operation, so only do it if necessary
var needToRewrite = false;
for (var i = 0; i < packet.Listings.Length; i++)
{
// these are empty slots that are not shown to the player
if (packet.Listings[i].IsNull())
{
continue;
}
var listing = new PartyFinderListing(packet.Listings[i], this.dalamud.Data, this.dalamud.SeStringManager);
var args = new PartyFinderListingEventArgs(packet.BatchNumber);
this.ReceiveListing?.Invoke(listing, args);
if (args.Visible)
{
continue;
}
// hide the listing from the player by setting it to a null listing
packet.Listings[i] = default;
needToRewrite = true;
}
if (!needToRewrite)
{
return;
}
// write our struct into the memory (doing this directly crashes the game)
Marshal.StructureToPtr(packet, this.memory, false);
// copy our new memory over the game's
unsafe
{
Buffer.MemoryCopy((void*)this.memory, (void*)dataPtr, PartyFinderPacket.PacketSize, PartyFinderPacket.PacketSize);
}
}
}
}

View file

@ -0,0 +1,48 @@
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Category flags for the <see cref="PartyFinder"/> class.
/// </summary>
public enum DutyFinderCategory
{
/// <summary>
/// The duty category.
/// </summary>
Duty = 0,
/// <summary>
/// The quest battle category.
/// </summary>
QuestBattles = 1 << 0,
/// <summary>
/// The fate category.
/// </summary>
Fates = 1 << 1,
/// <summary>
/// The treasure hunt category.
/// </summary>
TreasureHunt = 1 << 2,
/// <summary>
/// The hunt category.
/// </summary>
TheHunt = 1 << 3,
/// <summary>
/// The gathering forays category.
/// </summary>
GatheringForays = 1 << 4,
/// <summary>
/// The deep dungeons category.
/// </summary>
DeepDungeons = 1 << 5,
/// <summary>
/// The adventuring forays category.
/// </summary>
AdventuringForays = 1 << 6,
}
}

View file

@ -0,0 +1,26 @@
using System;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Condition flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum DutyFinderConditionFlags : uint
{
/// <summary>
/// No duty condition.
/// </summary>
None = 1,
/// <summary>
/// The duty complete condition.
/// </summary>
DutyComplete = 2,
/// <summary>
/// The duty incomplete condition.
/// </summary>
DutyIncomplete = 4,
}
}

View file

@ -0,0 +1,26 @@
using System;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Loot rule flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum DutyFinderLootRuleFlags : uint
{
/// <summary>
/// No loot rules.
/// </summary>
None = 0,
/// <summary>
/// The greed only rule.
/// </summary>
GreedOnly = 1,
/// <summary>
/// The lootmaster rule.
/// </summary>
Lootmaster = 2,
}
}

View file

@ -0,0 +1,31 @@
using System;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Objective flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum DutyFinderObjectiveFlags : uint
{
/// <summary>
/// No objective.
/// </summary>
None = 0,
/// <summary>
/// The duty completion objective.
/// </summary>
DutyCompletion = 1,
/// <summary>
/// The practice objective.
/// </summary>
Practice = 2,
/// <summary>
/// The loot objective.
/// </summary>
Loot = 4,
}
}

View file

@ -0,0 +1,36 @@
using System;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Search area flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum DutyFinderSearchAreaFlags : uint
{
/// <summary>
/// Datacenter.
/// </summary>
DataCentre = 1 << 0,
/// <summary>
/// Private.
/// </summary>
Private = 1 << 1,
/// <summary>
/// Alliance raid.
/// </summary>
AllianceRaid = 1 << 2,
/// <summary>
/// World.
/// </summary>
World = 1 << 3,
/// <summary>
/// One player per job.
/// </summary>
OnePlayerPerJob = 1 << 5,
}
}

View file

@ -0,0 +1,31 @@
using System;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Duty finder settings flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum DutyFinderSettingsFlags : uint
{
/// <summary>
/// No duty finder settings.
/// </summary>
None = 0,
/// <summary>
/// The undersized party setting.
/// </summary>
UndersizedParty = 1 << 0,
/// <summary>
/// The minimum item level setting.
/// </summary>
MinimumItemLevel = 1 << 1,
/// <summary>
/// The silence echo setting.
/// </summary>
SilenceEcho = 1 << 2,
}
}

View file

@ -0,0 +1,23 @@
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Duty type flags for the <see cref="PartyFinder"/> class.
/// </summary>
public enum DutyType
{
/// <summary>
/// No duty type.
/// </summary>
Other = 0,
/// <summary>
/// The roulette duty type.
/// </summary>
Roulette = 1 << 0,
/// <summary>
/// The normal duty type.
/// </summary>
Normal = 1 << 1,
}
}

View file

@ -0,0 +1,146 @@
using System;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Job flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum JobFlags
{
/// <summary>
/// Gladiator (GLD).
/// </summary>
Gladiator = 1 << 1,
/// <summary>
/// Pugilist (PGL).
/// </summary>
Pugilist = 1 << 2,
/// <summary>
/// Marauder (MRD).
/// </summary>
Marauder = 1 << 3,
/// <summary>
/// Lancer (LNC).
/// </summary>
Lancer = 1 << 4,
/// <summary>
/// Archer (ARC).
/// </summary>
Archer = 1 << 5,
/// <summary>
/// Conjurer (CNJ).
/// </summary>
Conjurer = 1 << 6,
/// <summary>
/// Thaumaturge (THM).
/// </summary>
Thaumaturge = 1 << 7,
/// <summary>
/// Paladin (PLD).
/// </summary>
Paladin = 1 << 8,
/// <summary>
/// Monk (MNK).
/// </summary>
Monk = 1 << 9,
/// <summary>
/// Warrior (WAR).
/// </summary>
Warrior = 1 << 10,
/// <summary>
/// Dragoon (DRG).
/// </summary>
Dragoon = 1 << 11,
/// <summary>
/// Bard (BRD).
/// </summary>
Bard = 1 << 12,
/// <summary>
/// White mage (WHM).
/// </summary>
WhiteMage = 1 << 13,
/// <summary>
/// Black mage (BLM).
/// </summary>
BlackMage = 1 << 14,
/// <summary>
/// Arcanist (ACN).
/// </summary>
Arcanist = 1 << 15,
/// <summary>
/// Summoner (SMN).
/// </summary>
Summoner = 1 << 16,
/// <summary>
/// Scholar (SCH).
/// </summary>
Scholar = 1 << 17,
/// <summary>
/// Rogue (ROG).
/// </summary>
Rogue = 1 << 18,
/// <summary>
/// Ninja (NIN).
/// </summary>
Ninja = 1 << 19,
/// <summary>
/// Machinist (MCH).
/// </summary>
Machinist = 1 << 20,
/// <summary>
/// Dark Knight (DRK).
/// </summary>
DarkKnight = 1 << 21,
/// <summary>
/// Astrologian (AST).
/// </summary>
Astrologian = 1 << 22,
/// <summary>
/// Samurai (SAM).
/// </summary>
Samurai = 1 << 23,
/// <summary>
/// Red mage (RDM).
/// </summary>
RedMage = 1 << 24,
/// <summary>
/// Blue mage (BLM).
/// </summary>
BlueMage = 1 << 25,
/// <summary>
/// Gunbreaker (GNB).
/// </summary>
Gunbreaker = 1 << 26,
/// <summary>
/// Dancer (DNC).
/// </summary>
Dancer = 1 << 27,
}
}

View file

@ -0,0 +1,27 @@
using System;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// Extensions for the <see cref="JobFlags"/> enum.
/// </summary>
public static class JobFlagsExtensions
{
/// <summary>
/// Get the actual ClassJob from the in-game sheets for this JobFlags.
/// </summary>
/// <param name="job">A JobFlags enum member.</param>
/// <param name="data">A DataManager to get the ClassJob from.</param>
/// <returns>A ClassJob if found or null if not.</returns>
public static ClassJob ClassJob(this JobFlags job, DataManager data)
{
var result = Math.Log2((double)job);
return result % 1 == 0
? data.GetExcelSheet<ClassJob>().GetRow((uint)result)
: null;
}
}
}

View file

@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.Gui.PartyFinder.Internal;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// A single listing in party finder.
/// </summary>
public class PartyFinderListing
{
#region Backing fields
private readonly byte objective;
private readonly byte conditions;
private readonly byte dutyFinderSettings;
private readonly byte lootRules;
private readonly byte searchArea;
private readonly PartyFinderSlot[] slots;
private readonly byte[] jobsPresent;
#endregion
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderListing"/> class.
/// </summary>
/// <param name="listing">The interop listing data.</param>
/// <param name="dataManager">The DataManager instance.</param>
/// <param name="seStringManager">The SeStringManager instance.</param>
internal PartyFinderListing(PartyFinderPacketListing listing, DataManager dataManager, SeStringManager seStringManager)
{
this.objective = listing.Objective;
this.conditions = listing.Conditions;
this.dutyFinderSettings = listing.DutyFinderSettings;
this.lootRules = listing.LootRules;
this.searchArea = listing.SearchArea;
this.slots = listing.Slots.Select(accepting => new PartyFinderSlot(accepting)).ToArray();
this.jobsPresent = listing.JobsPresent;
this.Id = listing.Id;
this.ContentIdLower = listing.ContentIdLower;
this.Name = seStringManager.Parse(listing.Name.TakeWhile(b => b != 0).ToArray());
this.Description = seStringManager.Parse(listing.Description.TakeWhile(b => b != 0).ToArray());
this.World = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.World));
this.HomeWorld = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.HomeWorld));
this.CurrentWorld = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.CurrentWorld));
this.Category = (DutyFinderCategory)listing.Category;
this.RawDuty = listing.Duty;
this.Duty = new Lazy<ContentFinderCondition>(() => dataManager.GetExcelSheet<ContentFinderCondition>().GetRow(listing.Duty));
this.DutyType = (DutyType)listing.DutyType;
this.BeginnersWelcome = listing.BeginnersWelcome == 1;
this.SecondsRemaining = listing.SecondsRemaining;
this.MinimumItemLevel = listing.MinimumItemLevel;
this.Parties = listing.NumParties;
this.SlotsAvailable = listing.NumSlots;
this.JobsPresent = listing.JobsPresent
.Select(id => new Lazy<ClassJob>(
() => id == 0
? null
: dataManager.GetExcelSheet<ClassJob>().GetRow(id)))
.ToArray();
}
/// <summary>
/// Gets the ID assigned to this listing by the game's server.
/// </summary>
public uint Id { get; }
/// <summary>
/// Gets the lower bits of the player's content ID.
/// </summary>
public uint ContentIdLower { get; }
/// <summary>
/// Gets the name of the player hosting this listing.
/// </summary>
public SeString Name { get; }
/// <summary>
/// Gets the description of this listing as set by the host. May be multiple lines.
/// </summary>
public SeString Description { get; }
/// <summary>
/// Gets the world that this listing was created on.
/// </summary>
public Lazy<World> World { get; }
/// <summary>
/// Gets the home world of the listing's host.
/// </summary>
public Lazy<World> HomeWorld { get; }
/// <summary>
/// Gets the current world of the listing's host.
/// </summary>
public Lazy<World> CurrentWorld { get; }
/// <summary>
/// Gets the Party Finder category this listing is listed under.
/// </summary>
public DutyFinderCategory Category { get; }
/// <summary>
/// Gets the row ID of the duty this listing is for. May be 0 for non-duty listings.
/// </summary>
public ushort RawDuty { get; }
/// <summary>
/// Gets the duty this listing is for. May be null for non-duty listings.
/// </summary>
public Lazy<ContentFinderCondition> Duty { get; }
/// <summary>
/// Gets the type of duty this listing is for.
/// </summary>
public DutyType DutyType { get; }
/// <summary>
/// Gets a value indicating whether if this listing is beginner-friendly. Shown with a sprout icon in-game.
/// </summary>
public bool BeginnersWelcome { get; }
/// <summary>
/// Gets how many seconds this listing will continue to be available for. It may end before this time if the party
/// fills or the host ends it early.
/// </summary>
public ushort SecondsRemaining { get; }
/// <summary>
/// Gets the minimum item level required to join this listing.
/// </summary>
public ushort MinimumItemLevel { get; }
/// <summary>
/// Gets the number of parties this listing is recruiting for.
/// </summary>
public byte Parties { get; }
/// <summary>
/// Gets the number of player slots this listing is recruiting for.
/// </summary>
public byte SlotsAvailable { get; }
/// <summary>
/// Gets a list of player slots that the Party Finder is accepting.
/// </summary>
public IReadOnlyCollection<PartyFinderSlot> Slots => this.slots;
/// <summary>
/// Gets the objective of this listing.
/// </summary>
public DutyFinderObjectiveFlags Objective => (DutyFinderObjectiveFlags)this.objective;
/// <summary>
/// Gets the conditions of this listing.
/// </summary>
public DutyFinderConditionFlags Conditions => (DutyFinderConditionFlags)this.conditions;
/// <summary>
/// Gets the Duty Finder settings that will be used for this listing.
/// </summary>
public DutyFinderSettingsFlags DutyFinderSettings => (DutyFinderSettingsFlags)this.dutyFinderSettings;
/// <summary>
/// Gets the loot rules that will be used for this listing.
/// </summary>
public DutyFinderLootRuleFlags LootRules => (DutyFinderLootRuleFlags)this.lootRules;
/// <summary>
/// Gets where this listing is searching. Note that this is also used for denoting alliance raid listings and one
/// player per job.
/// </summary>
public DutyFinderSearchAreaFlags SearchArea => (DutyFinderSearchAreaFlags)this.searchArea;
/// <summary>
/// Gets a list of the class/job IDs that are currently present in the party.
/// </summary>
public IReadOnlyCollection<byte> RawJobsPresent => this.jobsPresent;
/// <summary>
/// Gets a list of the classes/jobs that are currently present in the party.
/// </summary>
public IReadOnlyCollection<Lazy<ClassJob>> JobsPresent { get; }
#region Indexers
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[DutyFinderObjectiveFlags flag] => this.objective == 0 || (this.objective & (uint)flag) > 0;
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[DutyFinderConditionFlags flag] => this.conditions == 0 || (this.conditions & (uint)flag) > 0;
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[DutyFinderSettingsFlags flag] => this.dutyFinderSettings == 0 || (this.dutyFinderSettings & (uint)flag) > 0;
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[DutyFinderLootRuleFlags flag] => this.lootRules == 0 || (this.lootRules & (uint)flag) > 0;
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[DutyFinderSearchAreaFlags flag] => this.searchArea == 0 || (this.searchArea & (uint)flag) > 0;
#endregion
}
}

View file

@ -0,0 +1,27 @@
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// This class represents additional arguments passed by the game.
/// </summary>
public class PartyFinderListingEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderListingEventArgs"/> class.
/// </summary>
/// <param name="batchNumber">The batch number.</param>
internal PartyFinderListingEventArgs(int batchNumber)
{
this.BatchNumber = batchNumber;
}
/// <summary>
/// Gets the batch number.
/// </summary>
public int BatchNumber { get; }
/// <summary>
/// Gets or sets a value indicating whether the listing is visible.
/// </summary>
public bool Visible { get; set; } = true;
}
}

View file

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Dalamud.Game.Gui.PartyFinder.Types
{
/// <summary>
/// A player slot in a Party Finder listing.
/// </summary>
public class PartyFinderSlot
{
private readonly uint accepting;
private JobFlags[] listAccepting;
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderSlot"/> class.
/// </summary>
/// <param name="accepting">The flag value of accepted jobs.</param>
internal PartyFinderSlot(uint accepting)
{
this.accepting = accepting;
}
/// <summary>
/// Gets a list of jobs that this slot is accepting.
/// </summary>
public IReadOnlyCollection<JobFlags> Accepting
{
get
{
if (this.listAccepting != null)
{
return this.listAccepting;
}
this.listAccepting = Enum.GetValues(typeof(JobFlags))
.Cast<JobFlags>()
.Where(flag => this[flag])
.ToArray();
return this.listAccepting;
}
}
/// <summary>
/// Tests if this slot is accepting a job.
/// </summary>
/// <param name="flag">Job to test.</param>
public bool this[JobFlags flag] => (this.accepting & (uint)flag) > 0;
}
}

View file

@ -0,0 +1,32 @@
namespace Dalamud.Game.Gui.Toast
{
/// <summary>
/// This class represents options that can be used with the <see cref="ToastGui"/> class for the quest toast variant.
/// </summary>
public sealed class QuestToastOptions
{
/// <summary>
/// Gets or sets the position of the toast on the screen.
/// </summary>
public QuestToastPosition Position { get; set; } = QuestToastPosition.Centre;
/// <summary>
/// Gets or sets the ID of the icon that will appear in the toast.
///
/// This may be 0 for no icon.
/// </summary>
public uint IconId { get; set; } = 0;
/// <summary>
/// Gets or sets a value indicating whether the toast will show a checkmark after appearing.
/// </summary>
public bool DisplayCheckmark { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether the toast will play a completion sound.
///
/// This only works if <see cref="IconId"/> is non-zero or <see cref="DisplayCheckmark"/> is true.
/// </summary>
public bool PlaySound { get; set; } = false;
}
}

View file

@ -0,0 +1,23 @@
namespace Dalamud.Game.Gui.Toast
{
/// <summary>
/// The alignment of native quest toast windows.
/// </summary>
public enum QuestToastPosition
{
/// <summary>
/// The toast will be aligned screen centre.
/// </summary>
Centre = 0,
/// <summary>
/// The toast will be aligned screen right.
/// </summary>
Right = 1,
/// <summary>
/// The toast will be aligned screen left.
/// </summary>
Left = 2,
}
}

View file

@ -0,0 +1,18 @@
namespace Dalamud.Game.Gui.Toast
{
/// <summary>
/// This class represents options that can be used with the <see cref="ToastGui"/> class.
/// </summary>
public sealed class ToastOptions
{
/// <summary>
/// Gets or sets the position of the toast on the screen.
/// </summary>
public ToastPosition Position { get; set; } = ToastPosition.Bottom;
/// <summary>
/// Gets or sets the speed of the toast.
/// </summary>
public ToastSpeed Speed { get; set; } = ToastSpeed.Slow;
}
}

View file

@ -0,0 +1,18 @@
namespace Dalamud.Game.Gui.Toast
{
/// <summary>
/// The positioning of native toast windows.
/// </summary>
public enum ToastPosition : byte
{
/// <summary>
/// The toast will be towards the bottom.
/// </summary>
Bottom = 0,
/// <summary>
/// The toast will be towards the top.
/// </summary>
Top = 1,
}
}

View file

@ -0,0 +1,18 @@
namespace Dalamud.Game.Gui.Toast
{
/// <summary>
/// The speed at which native toast windows will persist.
/// </summary>
public enum ToastSpeed : byte
{
/// <summary>
/// The toast will take longer to disappear (around four seconds).
/// </summary>
Slow = 0,
/// <summary>
/// The toast will disappear more quickly (around two seconds).
/// </summary>
Fast = 1,
}
}

View file

@ -0,0 +1,431 @@
using System;
using System.Collections.Generic;
using System.Text;
using Dalamud.Game.Gui.Toast;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
namespace Dalamud.Game.Gui
{
/// <summary>
/// This class facilitates interacting with and creating native toast windows.
/// </summary>
public sealed partial class ToastGui : IDisposable
{
private const uint QuestToastCheckmarkMagic = 60081;
private readonly Dalamud dalamud;
private readonly ToastGuiAddressResolver address;
private readonly Queue<(byte[] Message, ToastOptions Options)> normalQueue = new();
private readonly Queue<(byte[] Message, QuestToastOptions Options)> questQueue = new();
private readonly Queue<byte[]> errorQueue = new();
private readonly Hook<ShowNormalToastDelegate> showNormalToastHook;
private readonly Hook<ShowQuestToastDelegate> showQuestToastHook;
private readonly Hook<ShowErrorToastDelegate> showErrorToastHook;
/// <summary>
/// Initializes a new instance of the <see cref="ToastGui"/> class.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
internal ToastGui(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new ToastGuiAddressResolver();
this.address.Setup(scanner);
this.showNormalToastHook = new Hook<ShowNormalToastDelegate>(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour));
this.showQuestToastHook = new Hook<ShowQuestToastDelegate>(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour));
this.showErrorToastHook = new Hook<ShowErrorToastDelegate>(this.address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour));
}
#region Event delegates
/// <summary>
/// A delegate type used when a normal toast window appears.
/// </summary>
/// <param name="message">The message displayed.</param>
/// <param name="options">Assorted toast options.</param>
/// <param name="isHandled">Whether the toast has been handled or should be propagated.</param>
public delegate void OnNormalToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled);
/// <summary>
/// A delegate type used when a quest toast window appears.
/// </summary>
/// <param name="message">The message displayed.</param>
/// <param name="options">Assorted toast options.</param>
/// <param name="isHandled">Whether the toast has been handled or should be propagated.</param>
public delegate void OnQuestToastDelegate(ref SeString message, ref QuestToastOptions options, ref bool isHandled);
/// <summary>
/// A delegate type used when an error toast window appears.
/// </summary>
/// <param name="message">The message displayed.</param>
/// <param name="isHandled">Whether the toast has been handled or should be propagated.</param>
public delegate void OnErrorToastDelegate(ref SeString message, ref bool isHandled);
#endregion
#region Marshal delegates
private delegate IntPtr ShowNormalToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId);
private delegate byte ShowQuestToastDelegate(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound);
private delegate byte ShowErrorToastDelegate(IntPtr manager, IntPtr text, byte respectsHidingMaybe);
#endregion
#region Events
/// <summary>
/// Event that will be fired when a toast is sent by the game or a plugin.
/// </summary>
public event OnNormalToastDelegate OnToast;
/// <summary>
/// Event that will be fired when a quest toast is sent by the game or a plugin.
/// </summary>
public event OnQuestToastDelegate OnQuestToast;
/// <summary>
/// Event that will be fired when an error toast is sent by the game or a plugin.
/// </summary>
public event OnErrorToastDelegate OnErrorToast;
#endregion
/// <summary>
/// Enables this module.
/// </summary>
public void Enable()
{
this.showNormalToastHook.Enable();
this.showQuestToastHook.Enable();
this.showErrorToastHook.Enable();
}
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.showNormalToastHook.Dispose();
this.showQuestToastHook.Dispose();
this.showErrorToastHook.Dispose();
}
/// <summary>
/// Process the toast queue.
/// </summary>
internal void UpdateQueue()
{
while (this.normalQueue.Count > 0)
{
var (message, options) = this.normalQueue.Dequeue();
this.ShowNormal(message, options);
}
while (this.questQueue.Count > 0)
{
var (message, options) = this.questQueue.Dequeue();
this.ShowQuest(message, options);
}
while (this.errorQueue.Count > 0)
{
var message = this.errorQueue.Dequeue();
this.ShowError(message);
}
}
private static byte[] Terminate(byte[] source)
{
var terminated = new byte[source.Length + 1];
Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
}
private SeString ParseString(IntPtr text)
{
var bytes = new List<byte>();
unsafe
{
var ptr = (byte*)text;
while (*ptr != 0)
{
bytes.Add(*ptr);
ptr += 1;
}
}
// call events
return this.dalamud.SeStringManager.Parse(bytes.ToArray());
}
}
/// <summary>
/// Handles normal toasts.
/// </summary>
public sealed partial class ToastGui
{
/// <summary>
/// Show a toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
/// <param name="options">Options for the toast.</param>
public void ShowNormal(string message, ToastOptions options = null)
{
options ??= new ToastOptions();
this.normalQueue.Enqueue((Encoding.UTF8.GetBytes(message), options));
}
/// <summary>
/// Show a toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
/// <param name="options">Options for the toast.</param>
public void ShowNormal(SeString message, ToastOptions options = null)
{
options ??= new ToastOptions();
this.normalQueue.Enqueue((message.Encode(), options));
}
private void ShowNormal(byte[] bytes, ToastOptions options = null)
{
options ??= new ToastOptions();
var manager = this.dalamud.Framework.Gui.GetUIModule();
// terminate the string
var terminated = Terminate(bytes);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleNormalToastDetour(manager, (IntPtr)ptr, 5, (byte)options.Position, (byte)options.Speed, 0);
}
}
}
private IntPtr HandleNormalToastDetour(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId)
{
if (text == IntPtr.Zero)
{
return IntPtr.Zero;
}
// call events
var isHandled = false;
var str = this.ParseString(text);
var options = new ToastOptions
{
Position = (ToastPosition)isTop,
Speed = (ToastSpeed)isFast,
};
this.OnToast?.Invoke(ref str, ref options, ref isHandled);
// do nothing if handled
if (isHandled)
{
return IntPtr.Zero;
}
var terminated = Terminate(str.Encode());
unsafe
{
fixed (byte* message = terminated)
{
return this.showNormalToastHook.Original(manager, (IntPtr)message, layer, (byte)options.Position, (byte)options.Speed, logMessageId);
}
}
}
}
/// <summary>
/// Handles quest toasts.
/// </summary>
public sealed partial class ToastGui
{
/// <summary>
/// Show a quest toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
/// <param name="options">Options for the toast.</param>
public void ShowQuest(string message, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
this.questQueue.Enqueue((Encoding.UTF8.GetBytes(message), options));
}
/// <summary>
/// Show a quest toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
/// <param name="options">Options for the toast.</param>
public void ShowQuest(SeString message, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
this.questQueue.Enqueue((message.Encode(), options));
}
private void ShowQuest(byte[] bytes, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
var manager = this.dalamud.Framework.Gui.GetUIModule();
// terminate the string
var terminated = Terminate(bytes);
var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleQuestToastDetour(
manager,
(int)options.Position,
(IntPtr)ptr,
ioc1,
options.PlaySound ? (byte)1 : (byte)0,
ioc2,
0);
}
}
}
private byte HandleQuestToastDetour(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound)
{
if (text == IntPtr.Zero)
{
return 0;
}
// call events
var isHandled = false;
var str = this.ParseString(text);
var options = new QuestToastOptions
{
Position = (QuestToastPosition)position,
DisplayCheckmark = iconOrCheck1 == QuestToastCheckmarkMagic,
IconId = iconOrCheck1 == QuestToastCheckmarkMagic ? iconOrCheck2 : iconOrCheck1,
PlaySound = playSound == 1,
};
this.OnQuestToast?.Invoke(ref str, ref options, ref isHandled);
// do nothing if handled
if (isHandled)
{
return 0;
}
var terminated = Terminate(str.Encode());
var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe
{
fixed (byte* message = terminated)
{
return this.showQuestToastHook.Original(
manager,
(int)options.Position,
(IntPtr)message,
ioc1,
options.PlaySound ? (byte)1 : (byte)0,
ioc2,
0);
}
}
}
private (uint IconOrCheck1, uint IconOrCheck2) DetermineParameterOrder(QuestToastOptions options)
{
return options.DisplayCheckmark
? (QuestToastCheckmarkMagic, options.IconId)
: (options.IconId, 0);
}
}
/// <summary>
/// Handles error toasts.
/// </summary>
public sealed partial class ToastGui
{
/// <summary>
/// Show an error toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
public void ShowError(string message)
{
this.errorQueue.Enqueue(Encoding.UTF8.GetBytes(message));
}
/// <summary>
/// Show an error toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
public void ShowError(SeString message)
{
this.errorQueue.Enqueue(message.Encode());
}
private void ShowError(byte[] bytes)
{
var manager = this.dalamud.Framework.Gui.GetUIModule();
// terminate the string
var terminated = Terminate(bytes);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleErrorToastDetour(manager, (IntPtr)ptr, 0);
}
}
}
private byte HandleErrorToastDetour(IntPtr manager, IntPtr text, byte respectsHidingMaybe)
{
if (text == IntPtr.Zero)
{
return 0;
}
// call events
var isHandled = false;
var str = this.ParseString(text);
this.OnErrorToast?.Invoke(ref str, ref isHandled);
// do nothing if handled
if (isHandled)
{
return 0;
}
var terminated = Terminate(str.Encode());
unsafe
{
fixed (byte* message = terminated)
{
return this.showErrorToastHook.Original(manager, (IntPtr)message, respectsHidingMaybe);
}
}
}
}
}

View file

@ -0,0 +1,35 @@
using System;
using Dalamud.Game.Internal;
namespace Dalamud.Game.Gui
{
/// <summary>
/// An address resolver for the <see cref="ToastGui"/> class.
/// </summary>
public class ToastGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native ShowNormalToast method.
/// </summary>
public IntPtr ShowNormalToast { get; private set; }
/// <summary>
/// Gets the address of the native ShowQuestToast method.
/// </summary>
public IntPtr ShowQuestToast { get; private set; }
/// <summary>
/// Gets the address of the ShowErrorToast method.
/// </summary>
public IntPtr ShowErrorToast { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.ShowNormalToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 83 3D ?? ?? ?? ?? ??");
this.ShowQuestToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 83 3D ?? ?? ?? ?? ??");
this.ShowErrorToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 83 3D ?? ?? ?? ?? ?? 41 0F B6 F0");
}
}
}