chore: convert Dalamud to file-scoped namespaces

This commit is contained in:
goat 2022-10-29 15:23:22 +02:00
parent b093323acc
commit 987ff8dc8f
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
368 changed files with 55081 additions and 55450 deletions

View file

@ -14,446 +14,445 @@ using Dalamud.IoC.Internal;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Game.Gui
namespace Dalamud.Game.Gui;
/// <summary>
/// This class handles interacting with the native chat UI.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class ChatGui : IDisposable, IServiceType
{
/// <summary>
/// This class handles interacting with the native chat UI.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class ChatGui : IDisposable, IServiceType
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;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly LibcFunction libcFunction = Service<LibcFunction>.Get();
private IntPtr baseAddress = IntPtr.Zero;
[ServiceManager.ServiceConstructor]
private ChatGui(SigScanner sigScanner)
{
private readonly ChatGuiAddressResolver address;
this.address = new ChatGuiAddressResolver();
this.address.Setup(sigScanner);
private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
}
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
/// <summary>
/// A delegate type used with the <see cref="ChatGui.ChatMessage"/> 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);
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
/// <summary>
/// A delegate type used with the <see cref="ChatGui.CheckMessageHandled"/> 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);
[ServiceManager.ServiceDependency]
private readonly LibcFunction libcFunction = Service<LibcFunction>.Get();
/// <summary>
/// A delegate type used with the <see cref="ChatGui.ChatMessageHandled"/> 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);
private IntPtr baseAddress = IntPtr.Zero;
/// <summary>
/// A delegate type used with the <see cref="ChatGui.ChatMessageUnhandled"/> 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);
[ServiceManager.ServiceConstructor]
private ChatGui(SigScanner sigScanner)
[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 ChatMessage;
/// <summary>
/// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true.
/// </summary>
public event OnCheckMessageHandledDelegate CheckMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageHandledDelegate ChatMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is not handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageUnhandledDelegate ChatMessageUnhandled;
/// <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>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.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
{
this.address = new ChatGuiAddressResolver();
this.address.Setup(sigScanner);
Message = message,
Type = this.configuration.GeneralChatType,
});
}
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
}
/// <summary>
/// A delegate type used with the <see cref="ChatGui.ChatMessage"/> 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="ChatGui.CheckMessageHandled"/> 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="ChatGui.ChatMessageHandled"/> 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="ChatGui.ChatMessageUnhandled"/> 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 ChatMessage;
/// <summary>
/// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true.
/// </summary>
public event OnCheckMessageHandledDelegate CheckMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageHandledDelegate ChatMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is not handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageUnhandledDelegate ChatMessageUnhandled;
/// <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>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
/// <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
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose();
}
Message = message,
Type = this.configuration.GeneralChatType,
});
}
/// <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)
/// <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
{
this.chatQueue.Enqueue(chat);
}
Message = message,
Type = XivChatType.Urgent,
});
}
/// <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)
/// <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
{
// Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry
Message = message,
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Process a chat queue.
/// </summary>
public void UpdateQueue()
{
while (this.chatQueue.Count > 0)
{
var chat = this.chatQueue.Dequeue();
if (this.baseAddress == IntPtr.Zero)
{
Message = message,
Type = this.configuration.GeneralChatType,
});
continue;
}
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = this.libcFunction.NewString(senderRaw);
var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = this.libcFunction.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
}
}
/// <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)
/// <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))
{
// Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = this.configuration.GeneralChatType,
});
this.dalamudLinkHandlers.Remove(handler);
}
}
/// <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)
/// <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)))
{
// Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
this.dalamudLinkHandlers.Remove((pluginName, commandId));
}
}
/// <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)
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui, LibcFunction libcFunction)
{
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
}
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr)
{
try
{
// Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
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}");
}
/// <summary>
/// Process a chat queue.
/// </summary>
public void UpdateQueue()
catch (Exception ex)
{
while (this.chatQueue.Count > 0)
{
var chat = this.chatQueue.Dequeue();
Log.Error(ex, "Exception onPopulateItemLink hook.");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
}
}
if (this.baseAddress == IntPtr.Zero)
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 parsedSender = SeString.Parse(sender.RawData);
var originalSenderData = (byte[])sender.RawData.Clone();
var oldEditedSender = parsedSender.Encode();
var senderPtr = pSenderName;
OwnedStdString allocatedString = null;
var message = StdString.ReadFromPointer(pMessage);
var parsedMessage = SeString.Parse(message.RawData);
var originalMessageData = (byte[])message.RawData.Clone();
var oldEdited = parsedMessage.Encode();
var messagePtr = pMessage;
OwnedStdString allocatedStringSender = null;
// Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue);
// Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}");
// Call events
var isHandled = false;
var invocationList = this.CheckMessageHandled.GetInvocationList();
foreach (var @delegate in invocationList)
{
try
{
continue;
var messageHandledDelegate = @delegate as OnCheckMessageHandledDelegate;
messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", @delegate.Method.Name);
}
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = this.libcFunction.NewString(senderRaw);
var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = this.libcFunction.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))
if (!isHandled)
{
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));
}
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui, LibcFunction libcFunction)
{
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
}
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 parsedSender = SeString.Parse(sender.RawData);
var originalSenderData = (byte[])sender.RawData.Clone();
var oldEditedSender = parsedSender.Encode();
var senderPtr = pSenderName;
OwnedStdString allocatedString = null;
var message = StdString.ReadFromPointer(pMessage);
var parsedMessage = SeString.Parse(message.RawData);
var originalMessageData = (byte[])message.RawData.Clone();
var oldEdited = parsedMessage.Encode();
var messagePtr = pMessage;
OwnedStdString allocatedStringSender = null;
// Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue);
// Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}");
// Call events
var isHandled = false;
var invocationList = this.CheckMessageHandled.GetInvocationList();
invocationList = this.ChatMessage.GetInvocationList();
foreach (var @delegate in invocationList)
{
try
{
var messageHandledDelegate = @delegate as OnCheckMessageHandledDelegate;
var messageHandledDelegate = @delegate as OnMessageDelegate;
messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", @delegate.Method.Name);
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", @delegate.Method.Name);
}
}
}
if (!isHandled)
{
invocationList = this.ChatMessage.GetInvocationList();
foreach (var @delegate in invocationList)
{
try
{
var messageHandledDelegate = @delegate as OnMessageDelegate;
messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", @delegate.Method.Name);
}
}
}
var newEdited = parsedMessage.Encode();
if (!Util.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 newEdited = parsedMessage.Encode();
if (!Util.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)}");
}
if (!Util.FastByteArrayCompare(originalMessageData, message.RawData))
{
allocatedString = this.libcFunction.NewString(message.RawData);
Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
messagePtr = allocatedString.Address;
}
if (!Util.FastByteArrayCompare(originalMessageData, message.RawData))
{
allocatedString = this.libcFunction.NewString(message.RawData);
Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
messagePtr = allocatedString.Address;
}
var newEditedSender = parsedSender.Encode();
if (!Util.FastByteArrayCompare(oldEditedSender, newEditedSender))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
sender.RawData = newEditedSender;
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
var newEditedSender = parsedSender.Encode();
if (!Util.FastByteArrayCompare(oldEditedSender, newEditedSender))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
sender.RawData = newEditedSender;
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData))
{
allocatedStringSender = this.libcFunction.NewString(sender.RawData);
Log.Debug(
$"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})");
senderPtr = allocatedStringSender.Address;
}
if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData))
{
allocatedStringSender = this.libcFunction.NewString(sender.RawData);
Log.Debug(
$"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})");
senderPtr = allocatedStringSender.Address;
}
// Print the original chat if it's handled.
if (isHandled)
{
this.ChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
}
else
{
retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter);
this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
}
// Print the original chat if it's handled.
if (isHandled)
if (this.baseAddress == IntPtr.Zero)
this.baseAddress = manager;
allocatedString?.Dispose();
allocatedStringSender?.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 = SeString.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)))
{
this.ChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId, new SeString(payloads));
}
else
{
retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter);
this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
if (this.baseAddress == IntPtr.Zero)
this.baseAddress = manager;
allocatedString?.Dispose();
allocatedStringSender?.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)
catch (Exception ex)
{
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 = SeString.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");
}
Log.Error(ex, "Exception on InteractableLinkClicked hook");
}
}
}

View file

@ -1,104 +1,103 @@
using System;
namespace Dalamud.Game.Gui
namespace Dalamud.Game.Gui;
/// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
/// </summary>
public sealed class ChatGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
/// Gets the address of the native PrintMessage method.
/// </summary>
public sealed class ChatGuiAddressResolver : BaseAddressResolver
public IntPtr PrintMessage { get; private set; }
/// <summary>
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
public IntPtr PopulateItemLinkObject { get; private set; }
/// <summary>
/// Gets the address of the native InteractableLinkClicked method.
/// </summary>
public IntPtr InteractableLinkClicked { get; private set; }
/*
--- for reference: 4.57 ---
.text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal)
.text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near
.text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p
.text:00000001405CD210 ; sub_140141D10+220p ...
.text:00000001405CD210
.text:00000001405CD210 var_220 = qword ptr -220h
.text:00000001405CD210 var_218 = byte ptr -218h
.text:00000001405CD210 var_210 = word ptr -210h
.text:00000001405CD210 var_208 = byte ptr -208h
.text:00000001405CD210 var_200 = word ptr -200h
.text:00000001405CD210 var_1FC = dword ptr -1FCh
.text:00000001405CD210 var_1F8 = qword ptr -1F8h
.text:00000001405CD210 var_1F0 = qword ptr -1F0h
.text:00000001405CD210 var_1E8 = qword ptr -1E8h
.text:00000001405CD210 var_1E0 = dword ptr -1E0h
.text:00000001405CD210 var_1DC = word ptr -1DCh
.text:00000001405CD210 var_1DA = word ptr -1DAh
.text:00000001405CD210 var_1D8 = qword ptr -1D8h
.text:00000001405CD210 var_1D0 = byte ptr -1D0h
.text:00000001405CD210 var_1C8 = qword ptr -1C8h
.text:00000001405CD210 var_1B0 = dword ptr -1B0h
.text:00000001405CD210 var_1AC = dword ptr -1ACh
.text:00000001405CD210 var_1A8 = dword ptr -1A8h
.text:00000001405CD210 var_1A4 = dword ptr -1A4h
.text:00000001405CD210 var_1A0 = dword ptr -1A0h
.text:00000001405CD210 var_160 = dword ptr -160h
.text:00000001405CD210 var_15C = dword ptr -15Ch
.text:00000001405CD210 var_140 = dword ptr -140h
.text:00000001405CD210 var_138 = dword ptr -138h
.text:00000001405CD210 var_130 = byte ptr -130h
.text:00000001405CD210 var_C0 = byte ptr -0C0h
.text:00000001405CD210 var_50 = qword ptr -50h
.text:00000001405CD210 var_38 = qword ptr -38h
.text:00000001405CD210 var_30 = qword ptr -30h
.text:00000001405CD210 var_28 = qword ptr -28h
.text:00000001405CD210 var_20 = qword ptr -20h
.text:00000001405CD210 senderActorId = dword ptr 30h
.text:00000001405CD210 isLocal = byte ptr 38h
.text:00000001405CD210
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck
.text:00000001405CD210 push rbp
.text:00000001405CD212 push rdi
.text:00000001405CD213 push r14
.text:00000001405CD215 push r15
.text:00000001405CD217 lea rbp, [rsp-128h]
.text:00000001405CD21F sub rsp, 228h
.text:00000001405CD226 mov rax, cs:__security_cookie
.text:00000001405CD22D xor rax, rsp
.text:00000001405CD230 mov [rbp+140h+var_50], rax
.text:00000001405CD237 xor r10b, r10b
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
.text:00000001405CD23F xor eax, eax
.text:00000001405CD241 mov r11, r9
.text:00000001405CD244 mov r14, r8
.text:00000001405CD247 mov r9d, eax
.text:00000001405CD24A movzx r15d, dx
.text:00000001405CD24E lea r8, [rcx+0C10h]
.text:00000001405CD255 mov rdi, rcx
*/
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
/// <summary>
/// Gets the address of the native PrintMessage method.
/// </summary>
public IntPtr PrintMessage { get; private set; }
// 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
/// <summary>
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
public IntPtr PopulateItemLinkObject { get; private set; }
// 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");
/// <summary>
/// Gets the address of the native InteractableLinkClicked method.
/// </summary>
public IntPtr InteractableLinkClicked { get; private set; }
// 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");
/*
--- for reference: 4.57 ---
.text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal)
.text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near
.text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p
.text:00000001405CD210 ; sub_140141D10+220p ...
.text:00000001405CD210
.text:00000001405CD210 var_220 = qword ptr -220h
.text:00000001405CD210 var_218 = byte ptr -218h
.text:00000001405CD210 var_210 = word ptr -210h
.text:00000001405CD210 var_208 = byte ptr -208h
.text:00000001405CD210 var_200 = word ptr -200h
.text:00000001405CD210 var_1FC = dword ptr -1FCh
.text:00000001405CD210 var_1F8 = qword ptr -1F8h
.text:00000001405CD210 var_1F0 = qword ptr -1F0h
.text:00000001405CD210 var_1E8 = qword ptr -1E8h
.text:00000001405CD210 var_1E0 = dword ptr -1E0h
.text:00000001405CD210 var_1DC = word ptr -1DCh
.text:00000001405CD210 var_1DA = word ptr -1DAh
.text:00000001405CD210 var_1D8 = qword ptr -1D8h
.text:00000001405CD210 var_1D0 = byte ptr -1D0h
.text:00000001405CD210 var_1C8 = qword ptr -1C8h
.text:00000001405CD210 var_1B0 = dword ptr -1B0h
.text:00000001405CD210 var_1AC = dword ptr -1ACh
.text:00000001405CD210 var_1A8 = dword ptr -1A8h
.text:00000001405CD210 var_1A4 = dword ptr -1A4h
.text:00000001405CD210 var_1A0 = dword ptr -1A0h
.text:00000001405CD210 var_160 = dword ptr -160h
.text:00000001405CD210 var_15C = dword ptr -15Ch
.text:00000001405CD210 var_140 = dword ptr -140h
.text:00000001405CD210 var_138 = dword ptr -138h
.text:00000001405CD210 var_130 = byte ptr -130h
.text:00000001405CD210 var_C0 = byte ptr -0C0h
.text:00000001405CD210 var_50 = qword ptr -50h
.text:00000001405CD210 var_38 = qword ptr -38h
.text:00000001405CD210 var_30 = qword ptr -30h
.text:00000001405CD210 var_28 = qword ptr -28h
.text:00000001405CD210 var_20 = qword ptr -20h
.text:00000001405CD210 senderActorId = dword ptr 30h
.text:00000001405CD210 isLocal = byte ptr 38h
.text:00000001405CD210
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck
.text:00000001405CD210 push rbp
.text:00000001405CD212 push rdi
.text:00000001405CD213 push r14
.text:00000001405CD215 push r15
.text:00000001405CD217 lea rbp, [rsp-128h]
.text:00000001405CD21F sub rsp, 228h
.text:00000001405CD226 mov rax, cs:__security_cookie
.text:00000001405CD22D xor rax, rsp
.text:00000001405CD230 mov [rbp+140h+var_50], rax
.text:00000001405CD237 xor r10b, r10b
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
.text:00000001405CD23F xor eax, eax
.text:00000001405CD241 mov r11, r9
.text:00000001405CD244 mov r14, r8
.text:00000001405CD247 mov r9d, eax
.text:00000001405CD24A movzx r15d, dx
.text:00000001405CD24E lea r8, [rcx+0C10h]
.text:00000001405CD255 mov rdi, rcx
*/
// 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");
/// <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;
}
this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9;
}
}

View file

@ -10,314 +10,313 @@ using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
namespace Dalamud.Game.Gui.Dtr
namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// Class used to interface with the server info bar.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class DtrBar : IDisposable, IServiceType
{
/// <summary>
/// Class used to interface with the server info bar.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class DtrBar : IDisposable, IServiceType
private const uint BaseNodeId = 1000;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private List<DtrBarEntry> entries = new();
private uint runningNodeIds = BaseNodeId;
[ServiceManager.ServiceConstructor]
private DtrBar()
{
private const uint BaseNodeId = 1000;
this.framework.Update += this.Update;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
this.configuration.DtrOrder ??= new List<string>();
this.configuration.DtrIgnore ??= new List<string>();
this.configuration.Save();
}
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
/// <summary>
/// Get a DTR bar entry.
/// This allows you to add your own text, and users to sort it.
/// </summary>
/// <param name="title">A user-friendly name for sorting.</param>
/// <param name="text">The text the entry shows.</param>
/// <returns>The entry object used to update, hide and remove the entry.</returns>
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
public DtrBarEntry Get(string title, SeString? text = null)
{
if (this.entries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists.");
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
var node = this.MakeNode(++this.runningNodeIds);
var entry = new DtrBarEntry(title, node);
entry.Text = text;
private List<DtrBarEntry> entries = new();
private uint runningNodeIds = BaseNodeId;
// Add the entry to the end of the order list, if it's not there already.
if (!this.configuration.DtrOrder!.Contains(title))
this.configuration.DtrOrder!.Add(title);
this.entries.Add(entry);
this.ApplySort();
[ServiceManager.ServiceConstructor]
private DtrBar()
return entry;
}
/// <inheritdoc/>
void IDisposable.Dispose()
{
foreach (var entry in this.entries)
this.RemoveNode(entry.TextNode);
this.entries.Clear();
this.framework.Update -= this.Update;
}
/// <summary>
/// Remove nodes marked as "should be removed" from the bar.
/// </summary>
internal void HandleRemovedNodes()
{
foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
{
this.framework.Update += this.Update;
this.configuration.DtrOrder ??= new List<string>();
this.configuration.DtrIgnore ??= new List<string>();
this.configuration.Save();
this.RemoveNode(data.TextNode);
}
/// <summary>
/// Get a DTR bar entry.
/// This allows you to add your own text, and users to sort it.
/// </summary>
/// <param name="title">A user-friendly name for sorting.</param>
/// <param name="text">The text the entry shows.</param>
/// <returns>The entry object used to update, hide and remove the entry.</returns>
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
public DtrBarEntry Get(string title, SeString? text = null)
{
if (this.entries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists.");
this.entries.RemoveAll(d => d.ShouldBeRemoved);
}
var node = this.MakeNode(++this.runningNodeIds);
var entry = new DtrBarEntry(title, node);
entry.Text = text;
// Add the entry to the end of the order list, if it's not there already.
if (!this.configuration.DtrOrder!.Contains(title))
this.configuration.DtrOrder!.Add(title);
this.entries.Add(entry);
this.ApplySort();
return entry;
}
/// <inheritdoc/>
void IDisposable.Dispose()
{
foreach (var entry in this.entries)
this.RemoveNode(entry.TextNode);
this.entries.Clear();
this.framework.Update -= this.Update;
}
/// <summary>
/// Remove nodes marked as "should be removed" from the bar.
/// </summary>
internal void HandleRemovedNodes()
{
foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
{
this.RemoveNode(data.TextNode);
}
this.entries.RemoveAll(d => d.ShouldBeRemoved);
}
/// <summary>
/// Check whether an entry with the specified title exists.
/// </summary>
/// <param name="title">The title to check for.</param>
/// <returns>Whether or not an entry with that title is registered.</returns>
internal bool HasEntry(string title) => this.entries.Any(x => x.Title == title);
/// <summary>
/// Dirty the DTR bar entry with the specified title.
/// </summary>
/// <param name="title">Title of the entry to dirty.</param>
/// <returns>Whether the entry was found.</returns>
internal bool MakeDirty(string title)
{
var entry = this.entries.FirstOrDefault(x => x.Title == title);
if (entry == null)
return false;
entry.Dirty = true;
return true;
}
/// <summary>
/// Reapply the DTR entry ordering from <see cref="DalamudConfiguration"/>.
/// </summary>
internal void ApplySort()
{
// Sort the current entry list, based on the order in the configuration.
var positions = this.configuration
.DtrOrder!
.Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
this.entries.Sort((x, y) =>
{
var xPos = positions.TryGetValue(x.Title, out var xIndex) ? xIndex : int.MaxValue;
var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue;
return xPos.CompareTo(yPos);
});
}
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR", 1).ToPointer();
private void Update(Framework unused)
{
this.HandleRemovedNodes();
var dtr = this.GetDtr();
if (dtr == null) return;
// The collision node on the DTR element is always the width of its content
if (dtr->UldManager.NodeList == null) return;
// If we have an unmodified DTR but still have entries, we need to
// work to reset our state.
if (!this.CheckForDalamudNodes())
this.RecreateNodes();
var collisionNode = dtr->UldManager.NodeList[1];
if (collisionNode == null) return;
// If we are drawing backwards, we should start from the right side of the collision node. That is,
// collisionNode->X + collisionNode->Width.
var runningXPos = this.configuration.DtrSwapDirection
? collisionNode->X + collisionNode->Width
: collisionNode->X;
for (var i = 0; i < this.entries.Count; i++)
{
var data = this.entries[i];
var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown;
if (data.Dirty && data.Added && data.Text != null && data.TextNode != null)
{
var node = data.TextNode;
node->SetText(data.Text?.Encode());
ushort w = 0, h = 0;
if (isHide)
{
node->AtkResNode.ToggleVisibility(false);
}
else
{
node->AtkResNode.ToggleVisibility(true);
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
node->AtkResNode.SetWidth(w);
}
data.Dirty = false;
}
if (!data.Added)
{
data.Added = this.AddNode(data.TextNode);
}
if (!isHide)
{
var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
if (this.configuration.DtrSwapDirection)
{
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
runningXPos += elementWidth;
}
else
{
runningXPos -= elementWidth;
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
}
}
this.entries[i] = data;
}
}
/// <summary>
/// Checks if there are any Dalamud nodes in the DTR.
/// </summary>
/// <returns>True if there are nodes with an ID > 1000.</returns>
private bool CheckForDalamudNodes()
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null) return false;
for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
{
if (dtr->UldManager.NodeList[i]->NodeID > 1000)
return true;
}
/// <summary>
/// Check whether an entry with the specified title exists.
/// </summary>
/// <param name="title">The title to check for.</param>
/// <returns>Whether or not an entry with that title is registered.</returns>
internal bool HasEntry(string title) => this.entries.Any(x => x.Title == title);
/// <summary>
/// Dirty the DTR bar entry with the specified title.
/// </summary>
/// <param name="title">Title of the entry to dirty.</param>
/// <returns>Whether the entry was found.</returns>
internal bool MakeDirty(string title)
{
var entry = this.entries.FirstOrDefault(x => x.Title == title);
if (entry == null)
return false;
}
private void RecreateNodes()
entry.Dirty = true;
return true;
}
/// <summary>
/// Reapply the DTR entry ordering from <see cref="DalamudConfiguration"/>.
/// </summary>
internal void ApplySort()
{
// Sort the current entry list, based on the order in the configuration.
var positions = this.configuration
.DtrOrder!
.Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
this.entries.Sort((x, y) =>
{
this.runningNodeIds = BaseNodeId;
foreach (var entry in this.entries)
var xPos = positions.TryGetValue(x.Title, out var xIndex) ? xIndex : int.MaxValue;
var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue;
return xPos.CompareTo(yPos);
});
}
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR", 1).ToPointer();
private void Update(Framework unused)
{
this.HandleRemovedNodes();
var dtr = this.GetDtr();
if (dtr == null) return;
// The collision node on the DTR element is always the width of its content
if (dtr->UldManager.NodeList == null) return;
// If we have an unmodified DTR but still have entries, we need to
// work to reset our state.
if (!this.CheckForDalamudNodes())
this.RecreateNodes();
var collisionNode = dtr->UldManager.NodeList[1];
if (collisionNode == null) return;
// If we are drawing backwards, we should start from the right side of the collision node. That is,
// collisionNode->X + collisionNode->Width.
var runningXPos = this.configuration.DtrSwapDirection
? collisionNode->X + collisionNode->Width
: collisionNode->X;
for (var i = 0; i < this.entries.Count; i++)
{
var data = this.entries[i];
var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown;
if (data.Dirty && data.Added && data.Text != null && data.TextNode != null)
{
entry.TextNode = this.MakeNode(++this.runningNodeIds);
entry.Added = false;
}
}
var node = data.TextNode;
node->SetText(data.Text?.Encode());
ushort w = 0, h = 0;
private bool AddNode(AtkTextNode* node)
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
if (isHide)
{
node->AtkResNode.ToggleVisibility(false);
}
else
{
node->AtkResNode.ToggleVisibility(true);
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
node->AtkResNode.SetWidth(w);
}
var lastChild = dtr->RootNode->ChildNode;
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
Log.Debug($"Found last sibling: {(ulong)lastChild:X}");
lastChild->PrevSiblingNode = (AtkResNode*)node;
node->AtkResNode.ParentNode = lastChild->ParentNode;
node->AtkResNode.NextSiblingNode = lastChild;
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount + 1);
Log.Debug("Set last sibling of DTR and updated child count");
dtr->UldManager.UpdateDrawNodeList();
Log.Debug("Updated node draw list");
return true;
}
private bool RemoveNode(AtkTextNode* node)
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var tmpPrevNode = node->AtkResNode.PrevSiblingNode;
var tmpNextNode = node->AtkResNode.NextSiblingNode;
// if (tmpNextNode != null)
tmpNextNode->PrevSiblingNode = tmpPrevNode;
if (tmpPrevNode != null)
tmpPrevNode->NextSiblingNode = tmpNextNode;
node->AtkResNode.Destroy(true);
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1);
Log.Debug("Set last sibling of DTR and updated child count");
dtr->UldManager.UpdateDrawNodeList();
Log.Debug("Updated node draw list");
return true;
}
private AtkTextNode* MakeNode(uint nodeId)
{
var newTextNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8);
if (newTextNode == null)
{
Log.Debug("Failed to allocate memory for text node");
return null;
data.Dirty = false;
}
IMemorySpace.Memset(newTextNode, 0, (ulong)sizeof(AtkTextNode));
newTextNode->Ctor();
if (!data.Added)
{
data.Added = this.AddNode(data.TextNode);
}
newTextNode->AtkResNode.NodeID = nodeId;
newTextNode->AtkResNode.Type = NodeType.Text;
newTextNode->AtkResNode.Flags = (short)(NodeFlags.AnchorLeft | NodeFlags.AnchorTop);
newTextNode->AtkResNode.DrawFlags = 12;
newTextNode->AtkResNode.SetWidth(22);
newTextNode->AtkResNode.SetHeight(22);
newTextNode->AtkResNode.SetPositionFloat(-200, 2);
if (!isHide)
{
var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
newTextNode->LineSpacing = 12;
newTextNode->AlignmentFontType = 5;
newTextNode->FontSize = 14;
newTextNode->TextFlags = (byte)TextFlags.Edge;
newTextNode->TextFlags2 = 0;
if (this.configuration.DtrSwapDirection)
{
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
runningXPos += elementWidth;
}
else
{
runningXPos -= elementWidth;
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
}
}
newTextNode->SetText(" ");
newTextNode->TextColor.R = 255;
newTextNode->TextColor.G = 255;
newTextNode->TextColor.B = 255;
newTextNode->TextColor.A = 255;
newTextNode->EdgeColor.R = 142;
newTextNode->EdgeColor.G = 106;
newTextNode->EdgeColor.B = 12;
newTextNode->EdgeColor.A = 255;
return newTextNode;
this.entries[i] = data;
}
}
/// <summary>
/// Checks if there are any Dalamud nodes in the DTR.
/// </summary>
/// <returns>True if there are nodes with an ID > 1000.</returns>
private bool CheckForDalamudNodes()
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null) return false;
for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
{
if (dtr->UldManager.NodeList[i]->NodeID > 1000)
return true;
}
return false;
}
private void RecreateNodes()
{
this.runningNodeIds = BaseNodeId;
foreach (var entry in this.entries)
{
entry.TextNode = this.MakeNode(++this.runningNodeIds);
entry.Added = false;
}
}
private bool AddNode(AtkTextNode* node)
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var lastChild = dtr->RootNode->ChildNode;
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
Log.Debug($"Found last sibling: {(ulong)lastChild:X}");
lastChild->PrevSiblingNode = (AtkResNode*)node;
node->AtkResNode.ParentNode = lastChild->ParentNode;
node->AtkResNode.NextSiblingNode = lastChild;
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount + 1);
Log.Debug("Set last sibling of DTR and updated child count");
dtr->UldManager.UpdateDrawNodeList();
Log.Debug("Updated node draw list");
return true;
}
private bool RemoveNode(AtkTextNode* node)
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var tmpPrevNode = node->AtkResNode.PrevSiblingNode;
var tmpNextNode = node->AtkResNode.NextSiblingNode;
// if (tmpNextNode != null)
tmpNextNode->PrevSiblingNode = tmpPrevNode;
if (tmpPrevNode != null)
tmpPrevNode->NextSiblingNode = tmpNextNode;
node->AtkResNode.Destroy(true);
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1);
Log.Debug("Set last sibling of DTR and updated child count");
dtr->UldManager.UpdateDrawNodeList();
Log.Debug("Updated node draw list");
return true;
}
private AtkTextNode* MakeNode(uint nodeId)
{
var newTextNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8);
if (newTextNode == null)
{
Log.Debug("Failed to allocate memory for text node");
return null;
}
IMemorySpace.Memset(newTextNode, 0, (ulong)sizeof(AtkTextNode));
newTextNode->Ctor();
newTextNode->AtkResNode.NodeID = nodeId;
newTextNode->AtkResNode.Type = NodeType.Text;
newTextNode->AtkResNode.Flags = (short)(NodeFlags.AnchorLeft | NodeFlags.AnchorTop);
newTextNode->AtkResNode.DrawFlags = 12;
newTextNode->AtkResNode.SetWidth(22);
newTextNode->AtkResNode.SetHeight(22);
newTextNode->AtkResNode.SetPositionFloat(-200, 2);
newTextNode->LineSpacing = 12;
newTextNode->AlignmentFontType = 5;
newTextNode->FontSize = 14;
newTextNode->TextFlags = (byte)TextFlags.Edge;
newTextNode->TextFlags2 = 0;
newTextNode->SetText(" ");
newTextNode->TextColor.R = 255;
newTextNode->TextColor.G = 255;
newTextNode->TextColor.B = 255;
newTextNode->TextColor.A = 255;
newTextNode->EdgeColor.R = 142;
newTextNode->EdgeColor.G = 106;
newTextNode->EdgeColor.B = 12;
newTextNode->EdgeColor.A = 255;
return newTextNode;
}
}

View file

@ -3,91 +3,90 @@
using Dalamud.Game.Text.SeStringHandling;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Gui.Dtr
namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// Class representing an entry in the server info bar.
/// </summary>
public sealed unsafe class DtrBarEntry : IDisposable
{
private bool shownBacking = true;
private SeString? textBacking = null;
/// <summary>
/// Class representing an entry in the server info bar.
/// Initializes a new instance of the <see cref="DtrBarEntry"/> class.
/// </summary>
public sealed unsafe class DtrBarEntry : IDisposable
/// <param name="title">The title of the bar entry.</param>
/// <param name="textNode">The corresponding text node.</param>
internal DtrBarEntry(string title, AtkTextNode* textNode)
{
private bool shownBacking = true;
private SeString? textBacking = null;
this.Title = title;
this.TextNode = textNode;
}
/// <summary>
/// Initializes a new instance of the <see cref="DtrBarEntry"/> class.
/// </summary>
/// <param name="title">The title of the bar entry.</param>
/// <param name="textNode">The corresponding text node.</param>
internal DtrBarEntry(string title, AtkTextNode* textNode)
/// <summary>
/// Gets the title of this entry.
/// </summary>
public string Title { get; init; }
/// <summary>
/// Gets or sets the text of this entry.
/// </summary>
public SeString? Text
{
get => this.textBacking;
set
{
this.Title = title;
this.TextNode = textNode;
}
/// <summary>
/// Gets the title of this entry.
/// </summary>
public string Title { get; init; }
/// <summary>
/// Gets or sets the text of this entry.
/// </summary>
public SeString? Text
{
get => this.textBacking;
set
{
this.textBacking = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets a value indicating whether this entry is visible.
/// </summary>
public bool Shown
{
get => this.shownBacking;
set
{
this.shownBacking = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets the internal text node of this entry.
/// </summary>
internal AtkTextNode* TextNode { get; set; }
/// <summary>
/// Gets a value indicating whether this entry should be removed.
/// </summary>
internal bool ShouldBeRemoved { get; private set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this entry is dirty.
/// </summary>
internal bool Dirty { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this entry has just been added.
/// </summary>
internal bool Added { get; set; } = false;
/// <summary>
/// Remove this entry from the bar.
/// You will need to re-acquire it from DtrBar to reuse it.
/// </summary>
public void Remove()
{
this.ShouldBeRemoved = true;
}
/// <inheritdoc/>
public void Dispose()
{
this.Remove();
this.textBacking = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets a value indicating whether this entry is visible.
/// </summary>
public bool Shown
{
get => this.shownBacking;
set
{
this.shownBacking = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets the internal text node of this entry.
/// </summary>
internal AtkTextNode* TextNode { get; set; }
/// <summary>
/// Gets a value indicating whether this entry should be removed.
/// </summary>
internal bool ShouldBeRemoved { get; private set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this entry is dirty.
/// </summary>
internal bool Dirty { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this entry has just been added.
/// </summary>
internal bool Added { get; set; } = false;
/// <summary>
/// Remove this entry from the bar.
/// You will need to re-acquire it from DtrBar to reuse it.
/// </summary>
public void Remove()
{
this.ShouldBeRemoved = true;
}
/// <inheritdoc/>
public void Dispose()
{
this.Remove();
}
}

View file

@ -9,303 +9,302 @@ using Dalamud.IoC.Internal;
using Dalamud.Memory;
using Serilog;
namespace Dalamud.Game.Gui.FlyText
namespace Dalamud.Game.Gui.FlyText;
/// <summary>
/// This class facilitates interacting with and creating native in-game "fly text".
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class FlyTextGui : IDisposable, IServiceType
{
/// <summary>
/// This class facilitates interacting with and creating native in-game "fly text".
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class FlyTextGui : IDisposable, IServiceType
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;
[ServiceManager.ServiceConstructor]
private FlyTextGui(SigScanner sigScanner)
{
/// <summary>
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
/// </summary>
private readonly AddFlyTextDelegate addFlyTextNative;
this.Address = new FlyTextGuiAddressResolver();
this.Address.Setup(sigScanner);
/// <summary>
/// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
}
[ServiceManager.ServiceConstructor]
private FlyTextGui(SigScanner sigScanner)
/// <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 OnFlyTextCreatedDelegate(
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,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float yOffset);
/// <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 OnFlyTextCreatedDelegate? FlyTextCreated;
private Dalamud Dalamud { get; }
private FlyTextGuiAddressResolver Address { get; }
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
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
var numIndex = 28;
var strIndex = 25;
var numOffset = 147u;
var strOffset = 28u;
// Get the UI module and flytext addon pointers
var gameGui = Service<GameGui>.GetNullable();
if (gameGui == null)
return;
var ui = (FFXIVClientStructs.FFXIV.Client.UI.UIModule*)gameGui.GetUIModule();
var flytext = gameGui.GetAddonByName("_FlyText", 1);
if (ui == null || flytext == IntPtr.Zero)
return;
// Get the number and string arrays we need
var atkArrayDataHolder = ui->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder;
var numArray = atkArrayDataHolder._NumberArrays[numIndex];
var strArray = atkArrayDataHolder._StringArrays[strIndex];
// Write the values to the arrays using a known valid flytext region
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())
{
this.Address = new FlyTextGuiAddressResolver();
this.Address.Setup(sigScanner);
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(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 OnFlyTextCreatedDelegate(
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,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float yOffset);
/// <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 OnFlyTextCreatedDelegate? FlyTextCreated;
private Dalamud Dalamud { get; }
private FlyTextGuiAddressResolver Address { get; }
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
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
var numIndex = 28;
var strIndex = 25;
var numOffset = 147u;
var strOffset = 28u;
// Get the UI module and flytext addon pointers
var gameGui = Service<GameGui>.GetNullable();
if (gameGui == null)
return;
var ui = (FFXIVClientStructs.FFXIV.Client.UI.UIModule*)gameGui.GetUIModule();
var flytext = gameGui.GetAddonByName("_FlyText", 1);
if (ui == null || flytext == IntPtr.Zero)
return;
// Get the number and string arrays we need
var atkArrayDataHolder = ui->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder;
var numArray = atkArrayDataHolder._NumberArrays[numIndex];
var strArray = atkArrayDataHolder._StringArrays[strIndex];
// Write the values to the arrays using a known valid flytext region
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())
{
fixed (byte* pText2 = text2.Encode())
{
strArray->StringArray[strOffset + 0] = pText1;
strArray->StringArray[strOffset + 1] = pText2;
strArray->StringArray[strOffset + 0] = pText1;
strArray->StringArray[strOffset + 1] = pText2;
this.addFlyTextNative(
flytext,
actorIndex,
1,
(IntPtr)numArray,
numOffset,
9,
(IntPtr)strArray,
strOffset,
2,
0);
}
this.addFlyTextNative(
flytext,
actorIndex,
1,
(IntPtr)numArray,
numOffset,
9,
(IntPtr)strArray,
strOffset,
2,
0);
}
}
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;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui)
{
this.createFlyTextHook.Enable();
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float yOffset)
{
var retVal = IntPtr.Zero;
try
{
Log.Verbose("[FlyText] Enter CreateFlyText detour!");
var handled = false;
var tmpKind = kind;
var tmpVal1 = val1;
var tmpVal2 = val2;
var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1);
var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(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({kind}) 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.FlyTextCreated?.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 != 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,
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.");
}
});
}
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;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui)
{
this.createFlyTextHook.Enable();
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float yOffset)
{
var retVal = IntPtr.Zero;
try
{
Log.Verbose("[FlyText] Enter CreateFlyText detour!");
var handled = false;
var tmpKind = kind;
var tmpVal1 = val1;
var tmpVal2 = val2;
var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1);
var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(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({kind}) 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.FlyTextCreated?.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 != 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,
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.");
}
});
}
catch (Exception e)
{
Log.Error(e, "Exception occurred in CreateFlyTextDetour!");
}
return retVal;
}
}

View file

@ -1,32 +1,31 @@
using System;
namespace Dalamud.Game.Gui.FlyText
namespace Dalamud.Game.Gui.FlyText;
/// <summary>
/// An address resolver for the <see cref="FlyTextGui"/> class.
/// </summary>
public class FlyTextGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// An address resolver for the <see cref="FlyTextGui"/> class.
/// 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 class FlyTextGuiAddressResolver : BaseAddressResolver
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)
{
/// <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");
}
this.AddFlyText = sig.ScanText("E8 ?? ?? ?? ?? FF C7 41 D1 C7");
this.CreateFlyText = sig.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 48 63 FA");
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,80 +1,79 @@
using System;
namespace Dalamud.Game.Gui
namespace Dalamud.Game.Gui;
/// <summary>
/// The address resolver for the <see cref="GameGui"/> class.
/// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// The address resolver for the <see cref="GameGui"/> class.
/// Gets the base address of the native GuiManager class.
/// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver
public IntPtr BaseAddress { 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 HandleImm method.
/// </summary>
public IntPtr HandleImm { 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 Utf8StringFromSequence method.
/// </summary>
public IntPtr Utf8StringFromSequence { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
/// <summary>
/// Gets the base address of the native GuiManager class.
/// </summary>
public IntPtr BaseAddress { 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 HandleImm method.
/// </summary>
public IntPtr HandleImm { 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 Utf8StringFromSequence method.
/// </summary>
public IntPtr Utf8StringFromSequence { 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.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09");
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.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8");
}
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.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09");
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.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8");
}
}

View file

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

View file

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

View file

@ -14,261 +14,261 @@ using PInvoke;
using static Dalamud.NativeFunctions;
namespace Dalamud.Game.Gui.Internal
namespace Dalamud.Game.Gui.Internal;
/// <summary>
/// This class handles IME for non-English users.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class DalamudIME : IDisposable, IServiceType
{
/// <summary>
/// This class handles IME for non-English users.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class DalamudIME : IDisposable, IServiceType
private static readonly ModuleLog Log = new("IME");
private AsmHook imguiTextInputCursorHook;
private Vector2* cursorPos;
[ServiceManager.ServiceConstructor]
private DalamudIME()
{
private static readonly ModuleLog Log = new("IME");
}
private AsmHook imguiTextInputCursorHook;
private Vector2* cursorPos;
/// <summary>
/// Gets a value indicating whether the module is enabled.
/// </summary>
internal bool IsEnabled { get; private set; }
[ServiceManager.ServiceConstructor]
private DalamudIME()
/// <summary>
/// Gets the index of the first imm candidate in relation to the full list.
/// </summary>
internal CandidateList ImmCandNative { get; private set; } = default;
/// <summary>
/// Gets the imm candidates.
/// </summary>
internal List<string> ImmCand { get; private set; } = new();
/// <summary>
/// Gets the selected imm component.
/// </summary>
internal string ImmComp { get; private set; } = string.Empty;
/// <inheritdoc/>
public void Dispose()
{
this.imguiTextInputCursorHook?.Dispose();
Marshal.FreeHGlobal((IntPtr)this.cursorPos);
}
/// <summary>
/// Processes window messages.
/// </summary>
/// <param name="hWnd">Handle of the window.</param>
/// <param name="msg">Type of window message.</param>
/// <param name="wParamPtr">wParam or the pointer to it.</param>
/// <param name="lParamPtr">lParam or the pointer to it.</param>
/// <returns>Return value, if not doing further processing.</returns>
public unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParamPtr, void* lParamPtr)
{
try
{
}
/// <summary>
/// Gets a value indicating whether the module is enabled.
/// </summary>
internal bool IsEnabled { get; private set; }
/// <summary>
/// Gets the index of the first imm candidate in relation to the full list.
/// </summary>
internal CandidateList ImmCandNative { get; private set; } = default;
/// <summary>
/// Gets the imm candidates.
/// </summary>
internal List<string> ImmCand { get; private set; } = new();
/// <summary>
/// Gets the selected imm component.
/// </summary>
internal string ImmComp { get; private set; } = string.Empty;
/// <inheritdoc/>
public void Dispose()
{
this.imguiTextInputCursorHook?.Dispose();
Marshal.FreeHGlobal((IntPtr)this.cursorPos);
}
/// <summary>
/// Processes window messages.
/// </summary>
/// <param name="hWnd">Handle of the window.</param>
/// <param name="msg">Type of window message.</param>
/// <param name="wParamPtr">wParam or the pointer to it.</param>
/// <param name="lParamPtr">lParam or the pointer to it.</param>
/// <returns>Return value, if not doing further processing.</returns>
public unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParamPtr, void* lParamPtr)
{
try
if (ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput)
{
if (ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput)
var io = ImGui.GetIO();
var wmsg = (WindowsMessage)msg;
long wParam = (long)wParamPtr, lParam = (long)lParamPtr;
try
{
var io = ImGui.GetIO();
var wmsg = (WindowsMessage)msg;
long wParam = (long)wParamPtr, lParam = (long)lParamPtr;
try
{
wParam = Marshal.ReadInt32((IntPtr)wParamPtr);
}
catch
{
// ignored
}
wParam = Marshal.ReadInt32((IntPtr)wParamPtr);
}
catch
{
// ignored
}
try
{
lParam = Marshal.ReadInt32((IntPtr)lParamPtr);
}
catch
{
// ignored
}
try
{
lParam = Marshal.ReadInt32((IntPtr)lParamPtr);
}
catch
{
// ignored
}
switch (wmsg)
{
case WindowsMessage.WM_IME_NOTIFY:
switch ((IMECommand)(IntPtr)wParam)
{
case IMECommand.ChangeCandidate:
this.ToggleWindow(true);
this.LoadCand(hWnd);
break;
case IMECommand.OpenCandidate:
this.ToggleWindow(true);
this.ImmCandNative = default;
// this.ImmCand.Clear();
break;
case IMECommand.CloseCandidate:
this.ToggleWindow(false);
this.ImmCandNative = default;
// this.ImmCand.Clear();
break;
default:
break;
}
break;
case WindowsMessage.WM_IME_COMPOSITION:
if (((long)(IMEComposition.CompStr | IMEComposition.CompAttr | IMEComposition.CompClause |
IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & (long)(IntPtr)lParam) > 0)
{
var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero)
return IntPtr.Zero;
var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, IntPtr.Zero, 0);
var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, unmanagedPointer, (uint)dwSize);
var bytes = new byte[dwSize];
Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
Marshal.FreeHGlobal(unmanagedPointer);
var lpstr = Encoding.Unicode.GetString(bytes);
this.ImmComp = lpstr;
if (lpstr == string.Empty)
{
this.ToggleWindow(false);
}
else
{
this.LoadCand(hWnd);
}
}
if (((long)(IntPtr)lParam & (long)IMEComposition.ResultStr) > 0)
{
var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero)
return IntPtr.Zero;
var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0);
var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize);
var bytes = new byte[dwSize];
Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
Marshal.FreeHGlobal(unmanagedPointer);
var lpstr = Encoding.Unicode.GetString(bytes);
io.AddInputCharactersUTF8(lpstr);
this.ImmComp = string.Empty;
switch (wmsg)
{
case WindowsMessage.WM_IME_NOTIFY:
switch ((IMECommand)(IntPtr)wParam)
{
case IMECommand.ChangeCandidate:
this.ToggleWindow(true);
this.LoadCand(hWnd);
break;
case IMECommand.OpenCandidate:
this.ToggleWindow(true);
this.ImmCandNative = default;
this.ImmCand.Clear();
// this.ImmCand.Clear();
break;
case IMECommand.CloseCandidate:
this.ToggleWindow(false);
this.ImmCandNative = default;
// this.ImmCand.Clear();
break;
default:
break;
}
break;
case WindowsMessage.WM_IME_COMPOSITION:
if (((long)(IMEComposition.CompStr | IMEComposition.CompAttr | IMEComposition.CompClause |
IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & (long)(IntPtr)lParam) > 0)
{
var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero)
return IntPtr.Zero;
var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, IntPtr.Zero, 0);
var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, unmanagedPointer, (uint)dwSize);
var bytes = new byte[dwSize];
Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
Marshal.FreeHGlobal(unmanagedPointer);
var lpstr = Encoding.Unicode.GetString(bytes);
this.ImmComp = lpstr;
if (lpstr == string.Empty)
{
this.ToggleWindow(false);
}
else
{
this.LoadCand(hWnd);
}
}
break;
if (((long)(IntPtr)lParam & (long)IMEComposition.ResultStr) > 0)
{
var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero)
return IntPtr.Zero;
default:
break;
}
var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0);
var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize);
var bytes = new byte[dwSize];
Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
Marshal.FreeHGlobal(unmanagedPointer);
var lpstr = Encoding.Unicode.GetString(bytes);
io.AddInputCharactersUTF8(lpstr);
this.ImmComp = string.Empty;
this.ImmCandNative = default;
this.ImmCand.Clear();
this.ToggleWindow(false);
}
break;
default:
break;
}
}
catch (Exception ex)
}
catch (Exception ex)
{
Log.Error(ex, "Prevented a crash in an IME hook");
}
return null;
}
/// <summary>
/// Get the position of the cursor.
/// </summary>
/// <returns>The position of the cursor.</returns>
internal Vector2 GetCursorPos()
{
return new Vector2(this.cursorPos->X, this.cursorPos->Y);
}
private unsafe void LoadCand(IntPtr hWnd)
{
if (hWnd == IntPtr.Zero)
return;
var hImc = ImmGetContext(hWnd);
if (hImc == IntPtr.Zero)
return;
var size = ImmGetCandidateListW(hImc, 0, IntPtr.Zero, 0);
if (size == 0)
return;
var candlistPtr = Marshal.AllocHGlobal((int)size);
size = ImmGetCandidateListW(hImc, 0, candlistPtr, (uint)size);
var candlist = this.ImmCandNative = Marshal.PtrToStructure<CandidateList>(candlistPtr);
var pageSize = candlist.PageSize;
var candCount = candlist.Count;
if (pageSize > 0 && candCount > 1)
{
var dwOffsets = new int[candCount];
for (var i = 0; i < candCount; i++)
{
Log.Error(ex, "Prevented a crash in an IME hook");
dwOffsets[i] = Marshal.ReadInt32(candlistPtr + ((i + 6) * sizeof(int)));
}
return null;
}
var pageStart = candlist.PageStart;
/// <summary>
/// Get the position of the cursor.
/// </summary>
/// <returns>The position of the cursor.</returns>
internal Vector2 GetCursorPos()
{
return new Vector2(this.cursorPos->X, this.cursorPos->Y);
}
var cand = new string[pageSize];
this.ImmCand.Clear();
private unsafe void LoadCand(IntPtr hWnd)
{
if (hWnd == IntPtr.Zero)
return;
var hImc = ImmGetContext(hWnd);
if (hImc == IntPtr.Zero)
return;
var size = ImmGetCandidateListW(hImc, 0, IntPtr.Zero, 0);
if (size == 0)
return;
var candlistPtr = Marshal.AllocHGlobal((int)size);
size = ImmGetCandidateListW(hImc, 0, candlistPtr, (uint)size);
var candlist = this.ImmCandNative = Marshal.PtrToStructure<CandidateList>(candlistPtr);
var pageSize = candlist.PageSize;
var candCount = candlist.Count;
if (pageSize > 0 && candCount > 1)
for (var i = 0; i < pageSize; i++)
{
var dwOffsets = new int[candCount];
for (var i = 0; i < candCount; i++)
var offStart = dwOffsets[i + pageStart];
var offEnd = i + pageStart + 1 < candCount ? dwOffsets[i + pageStart + 1] : size;
var pStrStart = candlistPtr + (int)offStart;
var pStrEnd = candlistPtr + (int)offEnd;
var len = (int)(pStrEnd.ToInt64() - pStrStart.ToInt64());
if (len > 0)
{
dwOffsets[i] = Marshal.ReadInt32(candlistPtr + ((i + 6) * sizeof(int)));
var candBytes = new byte[len];
Marshal.Copy(pStrStart, candBytes, 0, len);
var candStr = Encoding.Unicode.GetString(candBytes);
cand[i] = candStr;
this.ImmCand.Add(candStr);
}
var pageStart = candlist.PageStart;
var cand = new string[pageSize];
this.ImmCand.Clear();
for (var i = 0; i < pageSize; i++)
{
var offStart = dwOffsets[i + pageStart];
var offEnd = i + pageStart + 1 < candCount ? dwOffsets[i + pageStart + 1] : size;
var pStrStart = candlistPtr + (int)offStart;
var pStrEnd = candlistPtr + (int)offEnd;
var len = (int)(pStrEnd.ToInt64() - pStrStart.ToInt64());
if (len > 0)
{
var candBytes = new byte[len];
Marshal.Copy(pStrStart, candBytes, 0, len);
var candStr = Encoding.Unicode.GetString(candBytes);
cand[i] = candStr;
this.ImmCand.Add(candStr);
}
}
Marshal.FreeHGlobal(candlistPtr);
}
Marshal.FreeHGlobal(candlistPtr);
}
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene)
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene)
{
try
{
try
var module = Process.GetCurrentProcess().Modules.Cast<ProcessModule>().First(m => m.ModuleName == "cimgui.dll");
var scanner = new SigScanner(module);
var cursorDrawingPtr = scanner.ScanModule("F3 0F 11 75 ?? 0F 28 CF");
Log.Debug($"Found cursorDrawingPtr at {cursorDrawingPtr:X}");
this.cursorPos = (Vector2*)Marshal.AllocHGlobal(sizeof(Vector2));
this.cursorPos->X = 0f;
this.cursorPos->Y = 0f;
var asm = new[]
{
var module = Process.GetCurrentProcess().Modules.Cast<ProcessModule>().First(m => m.ModuleName == "cimgui.dll");
var scanner = new SigScanner(module);
var cursorDrawingPtr = scanner.ScanModule("F3 0F 11 75 ?? 0F 28 CF");
Log.Debug($"Found cursorDrawingPtr at {cursorDrawingPtr:X}");
this.cursorPos = (Vector2*)Marshal.AllocHGlobal(sizeof(Vector2));
this.cursorPos->X = 0f;
this.cursorPos->Y = 0f;
var asm = new[]
{
"use64",
$"push rax",
$"mov rax, {(IntPtr)this.cursorPos + sizeof(float)}",
@ -276,27 +276,26 @@ namespace Dalamud.Game.Gui.Internal
$"mov rax, {(IntPtr)this.cursorPos}",
$"movss [rax],xmm6",
$"pop rax",
};
};
Log.Debug($"Asm Code:\n{string.Join("\n", asm)}");
this.imguiTextInputCursorHook = new AsmHook(cursorDrawingPtr, asm, "ImguiTextInputCursorHook");
this.imguiTextInputCursorHook?.Enable();
Log.Debug($"Asm Code:\n{string.Join("\n", asm)}");
this.imguiTextInputCursorHook = new AsmHook(cursorDrawingPtr, asm, "ImguiTextInputCursorHook");
this.imguiTextInputCursorHook?.Enable();
this.IsEnabled = true;
Log.Information("Enabled!");
}
catch (Exception ex)
{
Log.Information(ex, "Enable failed");
}
this.IsEnabled = true;
Log.Information("Enabled!");
}
private void ToggleWindow(bool visible)
catch (Exception ex)
{
if (visible)
Service<DalamudInterface>.GetNullable()?.OpenImeWindow();
else
Service<DalamudInterface>.GetNullable()?.CloseImeWindow();
Log.Information(ex, "Enable failed");
}
}
private void ToggleWindow(bool visible)
{
if (visible)
Service<DalamudInterface>.GetNullable()?.OpenImeWindow();
else
Service<DalamudInterface>.GetNullable()?.CloseImeWindow();
}
}

View file

@ -1,28 +1,27 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace Dalamud.Game.Gui.PartyFinder.Internal
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>
/// The structure of the PartyFinder packet.
/// Gets the size of this 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 static int PacketSize { get; } = Marshal.SizeOf<PartyFinderPacket>();
internal readonly int BatchNumber;
internal readonly int BatchNumber;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] padding1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] padding1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
internal readonly PartyFinderPacketListing[] Listings;
}
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
internal readonly PartyFinderPacketListing[] Listings;
}

View file

@ -2,98 +2,97 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
namespace Dalamud.Game.Gui.PartyFinder.Internal
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
{
/// <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
internal 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()
{
[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
internal 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);
}
// a valid party finder must have at least one slot set
return this.Slots.All(slot => slot == 0);
}
}

View file

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

View file

@ -8,134 +8,133 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Serilog;
namespace Dalamud.Game.Gui.PartyFinder
namespace Dalamud.Game.Gui.PartyFinder;
/// <summary>
/// This class handles interacting with the native PartyFinder window.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class PartyFinderGui : IDisposable, IServiceType
{
private readonly PartyFinderAddressResolver address;
private readonly IntPtr memory;
private readonly Hook<ReceiveListingDelegate> receiveListingHook;
/// <summary>
/// This class handles interacting with the native PartyFinder window.
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class PartyFinderGui : IDisposable, IServiceType
/// <param name="sigScanner">Sig scanner to use.</param>
[ServiceManager.ServiceConstructor]
private PartyFinderGui(SigScanner sigScanner)
{
private readonly PartyFinderAddressResolver address;
private readonly IntPtr memory;
this.address = new PartyFinderAddressResolver();
this.address.Setup(sigScanner);
private readonly Hook<ReceiveListingDelegate> receiveListingHook;
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
/// </summary>
/// <param name="sigScanner">Sig scanner to use.</param>
[ServiceManager.ServiceConstructor]
private PartyFinderGui(SigScanner sigScanner)
this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(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>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
{
this.receiveListingHook.Dispose();
try
{
this.address = new PartyFinderAddressResolver();
this.address.Setup(sigScanner);
Marshal.FreeHGlobal(this.memory);
}
catch (BadImageFormatException)
{
Log.Warning("Could not free PartyFinderGui memory.");
}
}
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui)
{
this.receiveListingHook.Enable();
}
this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
{
try
{
this.HandleListingEvents(data);
}
catch (Exception ex)
{
Log.Error(ex, "Exception on ReceiveListing hook.");
}
/// <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);
this.receiveListingHook.Original(managerPtr, data);
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data);
private void HandleListingEvents(IntPtr data)
{
var dataPtr = data + 0x10;
/// <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;
var packet = Marshal.PtrToStructure<PartyFinderPacket>(dataPtr);
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
// rewriting is an expensive operation, so only do it if necessary
var needToRewrite = false;
for (var i = 0; i < packet.Listings.Length; i++)
{
this.receiveListingHook.Dispose();
// these are empty slots that are not shown to the player
if (packet.Listings[i].IsNull())
{
continue;
}
try
var listing = new PartyFinderListing(packet.Listings[i]);
var args = new PartyFinderListingEventArgs(packet.BatchNumber);
this.ReceiveListing?.Invoke(listing, args);
if (args.Visible)
{
Marshal.FreeHGlobal(this.memory);
}
catch (BadImageFormatException)
{
Log.Warning("Could not free PartyFinderGui memory.");
continue;
}
// hide the listing from the player by setting it to a null listing
packet.Listings[i] = default;
needToRewrite = true;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui)
if (!needToRewrite)
{
this.receiveListingHook.Enable();
return;
}
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
// 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
{
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]);
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);
}
Buffer.MemoryCopy((void*)this.memory, (void*)dataPtr, PartyFinderPacket.PacketSize, PartyFinderPacket.PacketSize);
}
}
}

View file

@ -1,32 +1,31 @@
using System;
namespace Dalamud.Game.Gui.PartyFinder.Types
namespace Dalamud.Game.Gui.PartyFinder.Types;
/// <summary>
/// Condition flags for the <see cref="PartyFinderGui"/> class.
/// </summary>
[Flags]
public enum ConditionFlags : uint
{
/// <summary>
/// Condition flags for the <see cref="PartyFinderGui"/> class.
/// No duty condition.
/// </summary>
[Flags]
public enum ConditionFlags : uint
{
/// <summary>
/// No duty condition.
/// </summary>
None = 1 << 0,
None = 1 << 0,
/// <summary>
/// The duty complete condition.
/// </summary>
DutyComplete = 1 << 1,
/// <summary>
/// The duty complete condition.
/// </summary>
DutyComplete = 1 << 1,
/// <summary>
/// The duty complete (weekly reward unclaimed) condition. This condition is
/// only available for savage fights prior to echo release.
/// </summary>
DutyCompleteWeeklyRewardUnclaimed = 1 << 3,
/// <summary>
/// The duty complete (weekly reward unclaimed) condition. This condition is
/// only available for savage fights prior to echo release.
/// </summary>
DutyCompleteWeeklyRewardUnclaimed = 1 << 3,
/// <summary>
/// The duty incomplete condition.
/// </summary>
DutyIncomplete = 1 << 2,
}
/// <summary>
/// The duty incomplete condition.
/// </summary>
DutyIncomplete = 1 << 2,
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,58 +1,57 @@
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Gui.PartyFinder.Types
namespace Dalamud.Game.Gui.PartyFinder.Types;
/// <summary>
/// Extensions for the <see cref="JobFlags"/> enum.
/// </summary>
public static class JobFlagsExtensions
{
/// <summary>
/// Extensions for the <see cref="JobFlags"/> enum.
/// Get the actual ClassJob from the in-game sheets for this JobFlags.
/// </summary>
public static class JobFlagsExtensions
/// <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)
{
/// <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 jobs = data.GetExcelSheet<ClassJob>();
uint? row = job switch
{
var jobs = data.GetExcelSheet<ClassJob>();
JobFlags.Gladiator => 1,
JobFlags.Pugilist => 2,
JobFlags.Marauder => 3,
JobFlags.Lancer => 4,
JobFlags.Archer => 5,
JobFlags.Conjurer => 6,
JobFlags.Thaumaturge => 7,
JobFlags.Paladin => 19,
JobFlags.Monk => 20,
JobFlags.Warrior => 21,
JobFlags.Dragoon => 22,
JobFlags.Bard => 23,
JobFlags.WhiteMage => 24,
JobFlags.BlackMage => 25,
JobFlags.Arcanist => 26,
JobFlags.Summoner => 27,
JobFlags.Scholar => 28,
JobFlags.Rogue => 29,
JobFlags.Ninja => 30,
JobFlags.Machinist => 31,
JobFlags.DarkKnight => 32,
JobFlags.Astrologian => 33,
JobFlags.Samurai => 34,
JobFlags.RedMage => 35,
JobFlags.BlueMage => 36,
JobFlags.Gunbreaker => 37,
JobFlags.Dancer => 38,
JobFlags.Reaper => 39,
JobFlags.Sage => 40,
_ => null,
};
uint? row = job switch
{
JobFlags.Gladiator => 1,
JobFlags.Pugilist => 2,
JobFlags.Marauder => 3,
JobFlags.Lancer => 4,
JobFlags.Archer => 5,
JobFlags.Conjurer => 6,
JobFlags.Thaumaturge => 7,
JobFlags.Paladin => 19,
JobFlags.Monk => 20,
JobFlags.Warrior => 21,
JobFlags.Dragoon => 22,
JobFlags.Bard => 23,
JobFlags.WhiteMage => 24,
JobFlags.BlackMage => 25,
JobFlags.Arcanist => 26,
JobFlags.Summoner => 27,
JobFlags.Scholar => 28,
JobFlags.Rogue => 29,
JobFlags.Ninja => 30,
JobFlags.Machinist => 31,
JobFlags.DarkKnight => 32,
JobFlags.Astrologian => 33,
JobFlags.Samurai => 34,
JobFlags.RedMage => 35,
JobFlags.BlueMage => 36,
JobFlags.Gunbreaker => 37,
JobFlags.Dancer => 38,
JobFlags.Reaper => 39,
JobFlags.Sage => 40,
_ => null,
};
return row == null ? null : jobs.GetRow((uint)row);
}
return row == null ? null : jobs.GetRow((uint)row);
}
}

View file

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

View file

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

View file

@ -7,228 +7,227 @@ using Dalamud.Game.Gui.PartyFinder.Internal;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Gui.PartyFinder.Types
namespace Dalamud.Game.Gui.PartyFinder.Types;
/// <summary>
/// A single listing in party finder.
/// </summary>
public class PartyFinderListing
{
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;
/// <summary>
/// A single listing in party finder.
/// Initializes a new instance of the <see cref="PartyFinderListing"/> class.
/// </summary>
public class PartyFinderListing
/// <param name="listing">The interop listing data.</param>
internal PartyFinderListing(PartyFinderPacketListing listing)
{
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;
var dataManager = Service<DataManager>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderListing"/> class.
/// </summary>
/// <param name="listing">The interop listing data.</param>
internal PartyFinderListing(PartyFinderPacketListing listing)
{
var dataManager = Service<DataManager>.Get();
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 = SeString.Parse(listing.Name.TakeWhile(b => b != 0).ToArray());
this.Description = SeString.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 = (DutyCategory)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.LastPatchHotfixTimestamp = listing.LastPatchHotfixTimestamp;
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 DutyCategory 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 the time at which the server this listings is on last restarted for a patch/hotfix.
/// Probably.
/// </summary>
public uint LastPatchHotfixTimestamp { 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 ObjectiveFlags Objective => (ObjectiveFlags)this.objective;
/// <summary>
/// Gets the conditions of this listing.
/// </summary>
public ConditionFlags Conditions => (ConditionFlags)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 LootRuleFlags LootRules => (LootRuleFlags)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 SearchAreaFlags SearchArea => (SearchAreaFlags)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[ObjectiveFlags 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[ConditionFlags 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[LootRuleFlags 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[SearchAreaFlags flag] => this.searchArea == 0 || (this.searchArea & (uint)flag) > 0;
#endregion
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 = SeString.Parse(listing.Name.TakeWhile(b => b != 0).ToArray());
this.Description = SeString.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 = (DutyCategory)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.LastPatchHotfixTimestamp = listing.LastPatchHotfixTimestamp;
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 DutyCategory 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 the time at which the server this listings is on last restarted for a patch/hotfix.
/// Probably.
/// </summary>
public uint LastPatchHotfixTimestamp { 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 ObjectiveFlags Objective => (ObjectiveFlags)this.objective;
/// <summary>
/// Gets the conditions of this listing.
/// </summary>
public ConditionFlags Conditions => (ConditionFlags)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 LootRuleFlags LootRules => (LootRuleFlags)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 SearchAreaFlags SearchArea => (SearchAreaFlags)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[ObjectiveFlags 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[ConditionFlags 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[LootRuleFlags 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[SearchAreaFlags flag] => this.searchArea == 0 || (this.searchArea & (uint)flag) > 0;
#endregion
}

View file

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

View file

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

View file

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

View file

@ -1,32 +1,31 @@
namespace Dalamud.Game.Gui.Toast
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>
/// This class represents options that can be used with the <see cref="ToastGui"/> class for the quest toast variant.
/// Gets or sets the position of the toast on the screen.
/// </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;
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 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 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;
}
/// <summary>
/// Gets or sets a value indicating whether the toast will play a completion sound.
///
/// This only works if <see cref="IconId"/> is non-zero or <see cref="DisplayCheckmark"/> is true.
/// </summary>
public bool PlaySound { get; set; } = false;
}

View file

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

View file

@ -7,429 +7,428 @@ using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
namespace Dalamud.Game.Gui.Toast
namespace Dalamud.Game.Gui.Toast;
/// <summary>
/// This class facilitates interacting with and creating native toast windows.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class ToastGui : IDisposable, IServiceType
{
private const uint QuestToastCheckmarkMagic = 60081;
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="sigScanner">Sig scanner to use.</param>
[ServiceManager.ServiceConstructor]
private ToastGui(SigScanner sigScanner)
{
this.address = new ToastGuiAddressResolver();
this.address.Setup(sigScanner);
this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour));
this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour));
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(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 Toast;
/// <summary>
/// Event that will be fired when a quest toast is sent by the game or a plugin.
/// </summary>
public event OnQuestToastDelegate QuestToast;
/// <summary>
/// Event that will be fired when an error toast is sent by the game or a plugin.
/// </summary>
public event OnErrorToastDelegate ErrorToast;
#endregion
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
void IDisposable.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;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui)
{
this.showNormalToastHook.Enable();
this.showQuestToastHook.Enable();
this.showErrorToastHook.Enable();
}
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 SeString.Parse(bytes.ToArray());
}
}
/// <summary>
/// Handles normal toasts.
/// </summary>
public sealed partial class ToastGui
{
/// <summary>
/// This class facilitates interacting with and creating native toast windows.
/// Show a toast message with the given content.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class ToastGui : IDisposable, IServiceType
/// <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)
{
private const uint QuestToastCheckmarkMagic = 60081;
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="sigScanner">Sig scanner to use.</param>
[ServiceManager.ServiceConstructor]
private ToastGui(SigScanner sigScanner)
{
this.address = new ToastGuiAddressResolver();
this.address.Setup(sigScanner);
this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour));
this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour));
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(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 Toast;
/// <summary>
/// Event that will be fired when a quest toast is sent by the game or a plugin.
/// </summary>
public event OnQuestToastDelegate QuestToast;
/// <summary>
/// Event that will be fired when an error toast is sent by the game or a plugin.
/// </summary>
public event OnErrorToastDelegate ErrorToast;
#endregion
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
void IDisposable.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;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui)
{
this.showNormalToastHook.Enable();
this.showQuestToastHook.Enable();
this.showErrorToastHook.Enable();
}
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 SeString.Parse(bytes.ToArray());
}
options ??= new ToastOptions();
this.normalQueue.Enqueue((Encoding.UTF8.GetBytes(message), options));
}
/// <summary>
/// Handles normal toasts.
/// Show a toast message with the given content.
/// </summary>
public sealed partial class ToastGui
/// <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)
{
/// <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((message.Encode(), options));
}
private void ShowNormal(byte[] bytes, ToastOptions options = null)
{
options ??= new ToastOptions();
var manager = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
unsafe
{
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 = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
unsafe
fixed (byte* ptr = terminated)
{
fixed (byte* ptr = terminated)
{
this.HandleNormalToastDetour(manager!.Value, (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.Toast?.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);
}
this.HandleNormalToastDetour(manager!.Value, (IntPtr)ptr, 5, (byte)options.Position, (byte)options.Speed, 0);
}
}
}
/// <summary>
/// Handles quest toasts.
/// </summary>
public sealed partial class ToastGui
private IntPtr HandleNormalToastDetour(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId)
{
/// <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)
if (text == IntPtr.Zero)
{
options ??= new QuestToastOptions();
this.questQueue.Enqueue((Encoding.UTF8.GetBytes(message), options));
return IntPtr.Zero;
}
/// <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)
// call events
var isHandled = false;
var str = this.ParseString(text);
var options = new ToastOptions
{
options ??= new QuestToastOptions();
this.questQueue.Enqueue((message.Encode(), options));
Position = (ToastPosition)isTop,
Speed = (ToastSpeed)isFast,
};
this.Toast?.Invoke(ref str, ref options, ref isHandled);
// do nothing if handled
if (isHandled)
{
return IntPtr.Zero;
}
private void ShowQuest(byte[] bytes, QuestToastOptions options = null)
var terminated = Terminate(str.Encode());
unsafe
{
options ??= new QuestToastOptions();
var manager = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe
fixed (byte* message = terminated)
{
fixed (byte* ptr = terminated)
{
this.HandleQuestToastDetour(
manager!.Value,
(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.QuestToast?.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 = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleErrorToastDetour(manager!.Value, (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.ErrorToast?.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);
}
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 = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleQuestToastDetour(
manager!.Value,
(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.QuestToast?.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 = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleErrorToastDetour(manager!.Value, (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.ErrorToast?.Invoke(ref str, ref isHandled);
// do nothing if handled
if (isHandled)
{
return 0;
}
var terminated = Terminate(str.Encode());
unsafe
{
fixed (byte* message = terminated)
{
return this.showErrorToastHook.Original(manager, (IntPtr)message, respectsHidingMaybe);
}
}
}

View file

@ -1,33 +1,32 @@
using System;
namespace Dalamud.Game.Gui.Toast
namespace Dalamud.Game.Gui.Toast;
/// <summary>
/// An address resolver for the <see cref="ToastGui"/> class.
/// </summary>
public class ToastGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// An address resolver for the <see cref="ToastGui"/> class.
/// Gets the address of the native ShowNormalToast method.
/// </summary>
public class ToastGuiAddressResolver : BaseAddressResolver
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)
{
/// <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");
}
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");
}
}

View file

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

View file

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

View file

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