mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-21 15:27:43 +01:00
Merge remote-tracking branch 'upstream/master' into flytext
This commit is contained in:
commit
5812b5a9c0
110 changed files with 2605 additions and 1389 deletions
63
Dalamud/Game/Gui/Addons/Addon.cs
Normal file
63
Dalamud/Game/Gui/Addons/Addon.cs
Normal 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
463
Dalamud/Game/Gui/ChatGui.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Dalamud/Game/Gui/ChatGuiAddressResolver.cs
Normal file
120
Dalamud/Game/Gui/ChatGuiAddressResolver.cs
Normal 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+201↑p
|
||||
.text:00000001405CD210 ; sub_140141D10+220↑p ...
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
321
Dalamud/Game/Gui/FlyTextGui.cs
Normal file
321
Dalamud/Game/Gui/FlyTextGui.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Dalamud/Game/Gui/FlyTextGuiAddressResolver.cs
Normal file
32
Dalamud/Game/Gui/FlyTextGuiAddressResolver.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
214
Dalamud/Game/Gui/FlyTextKind.cs
Normal file
214
Dalamud/Game/Gui/FlyTextKind.cs
Normal 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
671
Dalamud/Game/Gui/GameGui.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Dalamud/Game/Gui/GameGuiAddressResolver.cs
Normal file
123
Dalamud/Game/Gui/GameGuiAddressResolver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Dalamud/Game/Gui/HoverActionKind.cs
Normal file
49
Dalamud/Game/Gui/HoverActionKind.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
23
Dalamud/Game/Gui/HoveredAction.cs
Normal file
23
Dalamud/Game/Gui/HoveredAction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
28
Dalamud/Game/Gui/PartyFinder/Internal/PartyFinderPacket.cs
Normal file
28
Dalamud/Game/Gui/PartyFinder/Internal/PartyFinderPacket.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs
Normal file
21
Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs
Normal file
133
Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Dalamud/Game/Gui/PartyFinder/Types/DutyFinderCategory.cs
Normal file
48
Dalamud/Game/Gui/PartyFinder/Types/DutyFinderCategory.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
23
Dalamud/Game/Gui/PartyFinder/Types/DutyType.cs
Normal file
23
Dalamud/Game/Gui/PartyFinder/Types/DutyType.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
146
Dalamud/Game/Gui/PartyFinder/Types/JobFlags.cs
Normal file
146
Dalamud/Game/Gui/PartyFinder/Types/JobFlags.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
27
Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs
Normal file
27
Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
231
Dalamud/Game/Gui/PartyFinder/Types/PartyFinderListing.cs
Normal file
231
Dalamud/Game/Gui/PartyFinder/Types/PartyFinderListing.cs
Normal 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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
51
Dalamud/Game/Gui/PartyFinder/Types/PartyFinderSlot.cs
Normal file
51
Dalamud/Game/Gui/PartyFinder/Types/PartyFinderSlot.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
32
Dalamud/Game/Gui/Toast/QuestToastOptions.cs
Normal file
32
Dalamud/Game/Gui/Toast/QuestToastOptions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
23
Dalamud/Game/Gui/Toast/QuestToastPosition.cs
Normal file
23
Dalamud/Game/Gui/Toast/QuestToastPosition.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
18
Dalamud/Game/Gui/Toast/ToastOptions.cs
Normal file
18
Dalamud/Game/Gui/Toast/ToastOptions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
18
Dalamud/Game/Gui/Toast/ToastPosition.cs
Normal file
18
Dalamud/Game/Gui/Toast/ToastPosition.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
18
Dalamud/Game/Gui/Toast/ToastSpeed.cs
Normal file
18
Dalamud/Game/Gui/Toast/ToastSpeed.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
431
Dalamud/Game/Gui/ToastGui.cs
Normal file
431
Dalamud/Game/Gui/ToastGui.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Dalamud/Game/Gui/ToastGuiAddressResolver.cs
Normal file
35
Dalamud/Game/Gui/ToastGuiAddressResolver.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue