Compare commits

...

No commits in common. "main" and "892dcb81eaba07508c22857663e7683319320e2c" have entirely different histories.

45 changed files with 3299 additions and 2264 deletions

View File

@ -1,183 +1,187 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions;
/// <summary>
/// The class containing BattleTalk functionality
/// </summary>
public class BattleTalk : IDisposable {
private bool HookEnabled { get; }
namespace XivCommon.Functions {
/// <summary>
/// The delegate for BattleTalk events.
/// The class containing BattleTalk functionality
/// </summary>
public delegate void BattleTalkEventDelegate(ref SeString sender, ref SeString message, ref BattleTalkOptions options, ref bool isHandled);
public class BattleTalk : IDisposable {
private static class Signatures {
internal const string AddBattleTalk = "48 89 5C 24 ?? 57 48 83 EC 50 48 8B 01 49 8B D8 0F 29 74 24 ?? 48 8B FA 0F 28 F3 FF 50 40 C7 44 24 ?? ?? ?? ?? ??";
}
/// <summary>
/// <para>
/// The event that is fired when a BattleTalk window is shown.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.BattleTalk"/> hook to be enabled.
/// </para>
/// </summary>
public event BattleTalkEventDelegate? OnBattleTalk;
private GameFunctions Functions { get; }
private bool HookEnabled { get; }
private delegate byte AddBattleTalkDelegate(IntPtr uiModule, IntPtr sender, IntPtr message, float duration, byte style);
/// <summary>
/// The delegate for BattleTalk events.
/// </summary>
public delegate void BattleTalkEventDelegate(ref SeString sender, ref SeString message, ref BattleTalkOptions options, ref bool isHandled);
private AddBattleTalkDelegate? AddBattleTalk { get; }
private Hook<AddBattleTalkDelegate>? AddBattleTalkHook { get; }
/// <summary>
/// <para>
/// The event that is fired when a BattleTalk window is shown.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.BattleTalk"/> hook to be enabled.
/// </para>
/// </summary>
public event BattleTalkEventDelegate? OnBattleTalk;
internal unsafe BattleTalk(IGameInteropProvider interop, bool hook) {
this.HookEnabled = hook;
private delegate byte AddBattleTalkDelegate(IntPtr uiModule, IntPtr sender, IntPtr message, float duration, byte style);
var addBattleTalkPtr = (IntPtr) Framework.Instance()->GetUiModule()->VTable->ShowBattleTalk;
if (addBattleTalkPtr != IntPtr.Zero) {
this.AddBattleTalk = Marshal.GetDelegateForFunctionPointer<AddBattleTalkDelegate>(addBattleTalkPtr);
private AddBattleTalkDelegate? AddBattleTalk { get; }
private Hook<AddBattleTalkDelegate>? AddBattleTalkHook { get; }
if (this.HookEnabled) {
this.AddBattleTalkHook = interop.HookFromAddress<AddBattleTalkDelegate>(addBattleTalkPtr, this.AddBattleTalkDetour);
this.AddBattleTalkHook.Enable();
internal BattleTalk(GameFunctions functions, SigScanner scanner,bool hook) {
this.Functions = functions;
this.HookEnabled = hook;
if (scanner.TryScanText(Signatures.AddBattleTalk, out var addBattleTalkPtr, "battle talk")) {
this.AddBattleTalk = Marshal.GetDelegateForFunctionPointer<AddBattleTalkDelegate>(addBattleTalkPtr);
if (this.HookEnabled) {
this.AddBattleTalkHook = new Hook<AddBattleTalkDelegate>(addBattleTalkPtr, this.AddBattleTalkDetour);
this.AddBattleTalkHook.Enable();
}
}
}
/// <inheritdoc />
public void Dispose() {
this.AddBattleTalkHook?.Dispose();
}
private byte AddBattleTalkDetour(IntPtr uiModule, IntPtr senderPtr, IntPtr messagePtr, float duration, byte style) {
if (this.OnBattleTalk == null) {
goto Return;
}
try {
return this.AddBattleTalkDetourInner(uiModule, senderPtr, messagePtr, duration, style);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in BattleTalk detour");
}
Return:
return this.AddBattleTalkHook!.Original(uiModule, senderPtr, messagePtr, duration, style);
}
private unsafe byte AddBattleTalkDetourInner(IntPtr uiModule, IntPtr senderPtr, IntPtr messagePtr, float duration, byte style) {
var rawSender = Util.ReadTerminated(senderPtr);
var rawMessage = Util.ReadTerminated(messagePtr);
var sender = SeString.Parse(rawSender);
var message = SeString.Parse(rawMessage);
var options = new BattleTalkOptions {
Duration = duration,
Style = (BattleTalkStyle) style,
};
var handled = false;
try {
this.OnBattleTalk?.Invoke(ref sender, ref message, ref options, ref handled);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in BattleTalk event");
}
if (handled) {
return 0;
}
var finalSender = sender.Encode().Terminate();
var finalMessage = message.Encode().Terminate();
fixed (byte* fSenderPtr = finalSender, fMessagePtr = finalMessage) {
return this.AddBattleTalkHook!.Original(uiModule, (IntPtr) fSenderPtr, (IntPtr) fMessagePtr, options.Duration, (byte) options.Style);
}
}
/// <summary>
/// Show a BattleTalk window with the given options.
/// </summary>
/// <param name="sender">The name to attribute to the message</param>
/// <param name="message">The message to show in the window</param>
/// <param name="options">Optional options for the window</param>
/// <exception cref="ArgumentException">If sender or message are empty</exception>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public void Show(SeString sender, SeString message, BattleTalkOptions? options = null) {
this.Show(sender.Encode(), message.Encode(), options);
}
private unsafe void Show(byte[] sender, byte[] message, BattleTalkOptions? options) {
if (sender.Length == 0) {
throw new ArgumentException("sender cannot be empty", nameof(sender));
}
if (message.Length == 0) {
throw new ArgumentException("message cannot be empty", nameof(message));
}
if (this.AddBattleTalk == null) {
throw new InvalidOperationException("Signature for battle talk could not be found");
}
options ??= new BattleTalkOptions();
var uiModule = (IntPtr) this.Functions.GetFramework()->GetUiModule();
fixed (byte* senderPtr = sender.Terminate(), messagePtr = message.Terminate()) {
if (this.HookEnabled) {
this.AddBattleTalkDetour(uiModule, (IntPtr) senderPtr, (IntPtr) messagePtr, options.Duration, (byte) options.Style);
} else {
this.AddBattleTalk(uiModule, (IntPtr) senderPtr, (IntPtr) messagePtr, options.Duration, (byte) options.Style);
}
}
}
}
/// <inheritdoc />
public void Dispose() {
this.AddBattleTalkHook?.Dispose();
}
/// <summary>
/// Options for displaying a BattleTalk window.
/// </summary>
public class BattleTalkOptions {
/// <summary>
/// Duration to display the window, in seconds.
/// </summary>
public float Duration { get; set; } = 5f;
private byte AddBattleTalkDetour(IntPtr uiModule, IntPtr senderPtr, IntPtr messagePtr, float duration, byte style) {
if (this.OnBattleTalk == null) {
goto Return;
}
try {
return this.AddBattleTalkDetourInner(uiModule, senderPtr, messagePtr, duration, style);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in BattleTalk detour");
}
Return:
return this.AddBattleTalkHook!.Original(uiModule, senderPtr, messagePtr, duration, style);
}
private unsafe byte AddBattleTalkDetourInner(IntPtr uiModule, IntPtr senderPtr, IntPtr messagePtr, float duration, byte style) {
var rawSender = Util.ReadTerminated(senderPtr);
var rawMessage = Util.ReadTerminated(messagePtr);
var sender = SeString.Parse(rawSender);
var message = SeString.Parse(rawMessage);
var options = new BattleTalkOptions {
Duration = duration,
Style = (BattleTalkStyle) style,
};
var handled = false;
try {
this.OnBattleTalk?.Invoke(ref sender, ref message, ref options, ref handled);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in BattleTalk event");
}
if (handled) {
return 0;
}
var finalSender = sender.Encode().Terminate();
var finalMessage = message.Encode().Terminate();
fixed (byte* fSenderPtr = finalSender, fMessagePtr = finalMessage) {
return this.AddBattleTalkHook!.Original(uiModule, (IntPtr) fSenderPtr, (IntPtr) fMessagePtr, options.Duration, (byte) options.Style);
}
/// <summary>
/// The style of the window.
/// </summary>
public BattleTalkStyle Style { get; set; } = BattleTalkStyle.Normal;
}
/// <summary>
/// Show a BattleTalk window with the given options.
/// BattleTalk window styles.
/// </summary>
/// <param name="sender">The name to attribute to the message</param>
/// <param name="message">The message to show in the window</param>
/// <param name="options">Optional options for the window</param>
/// <exception cref="ArgumentException">If sender or message are empty</exception>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public void Show(SeString sender, SeString message, BattleTalkOptions? options = null) {
this.Show(sender.Encode(), message.Encode(), options);
}
public enum BattleTalkStyle : byte {
/// <summary>
/// A normal battle talk window with a white background.
/// </summary>
Normal = 0,
private unsafe void Show(byte[] sender, byte[] message, BattleTalkOptions? options) {
if (sender.Length == 0) {
throw new ArgumentException("sender cannot be empty", nameof(sender));
}
/// <summary>
/// A battle talk window with a blue background and styled edges.
/// </summary>
Aetherial = 6,
if (message.Length == 0) {
throw new ArgumentException("message cannot be empty", nameof(message));
}
/// <summary>
/// A battle talk window styled similarly to a system text message (black background).
/// </summary>
System = 7,
if (this.AddBattleTalk == null) {
throw new InvalidOperationException("Signature for battle talk could not be found");
}
options ??= new BattleTalkOptions();
var uiModule = (IntPtr) Framework.Instance()->GetUiModule();
fixed (byte* senderPtr = sender.Terminate(), messagePtr = message.Terminate()) {
if (this.HookEnabled) {
this.AddBattleTalkDetour(uiModule, (IntPtr) senderPtr, (IntPtr) messagePtr, options.Duration, (byte) options.Style);
} else {
this.AddBattleTalk(uiModule, (IntPtr) senderPtr, (IntPtr) messagePtr, options.Duration, (byte) options.Style);
}
}
/// <summary>
/// <para>
/// A battle talk window with a blue, computer-y background.
/// </para>
/// <para>
/// Used by the Ultima Weapons (Ruby, Emerald, etc.).
/// </para>
/// </summary>
Blue = 9,
}
}
/// <summary>
/// Options for displaying a BattleTalk window.
/// </summary>
public class BattleTalkOptions {
/// <summary>
/// Duration to display the window, in seconds.
/// </summary>
public float Duration { get; set; } = 5f;
/// <summary>
/// The style of the window.
/// </summary>
public BattleTalkStyle Style { get; set; } = BattleTalkStyle.Normal;
}
/// <summary>
/// BattleTalk window styles.
/// </summary>
public enum BattleTalkStyle : byte {
/// <summary>
/// A normal battle talk window with a white background.
/// </summary>
Normal = 0,
/// <summary>
/// A battle talk window with a blue background and styled edges.
/// </summary>
Aetherial = 6,
/// <summary>
/// A battle talk window styled similarly to a system text message (black background).
/// </summary>
System = 7,
/// <summary>
/// <para>
/// A battle talk window with a blue, computer-y background.
/// </para>
/// <para>
/// Used by the Ultima Weapons (Ruby, Emerald, etc.).
/// </para>
/// </summary>
Blue = 9,
}

View File

@ -3,155 +3,81 @@ using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions;
/// <summary>
/// A class containing chat functionality
/// </summary>
public class Chat {
private static class Signatures {
internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9";
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D";
}
private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
private ProcessChatBoxDelegate? ProcessChatBox { get; }
private readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString = null!;
internal Chat(ISigScanner scanner) {
if (scanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr, "chat sending")) {
this.ProcessChatBox = Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(processChatBoxPtr);
namespace XivCommon.Functions {
/// <summary>
/// A class containing chat functionality
/// </summary>
public class Chat {
private static class Signatures {
internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9";
}
unsafe {
if (scanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr, "string sanitiser")) {
this._sanitiseString = (delegate* unmanaged<Utf8String*, int, IntPtr, void>) sanitisePtr;
private GameFunctions Functions { get; }
private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
private ProcessChatBoxDelegate? ProcessChatBox { get; }
internal Chat(GameFunctions functions, SigScanner scanner) {
this.Functions = functions;
if (scanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr, "chat sending")) {
this.ProcessChatBox = Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(processChatBoxPtr);
}
}
/// <summary>
/// Send a given message to the chat box. <b>This can send chat to the server.</b>
/// </summary>
/// <param name="message">Message to send</param>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public unsafe void SendMessage(string message) {
if (this.ProcessChatBox == null) {
throw new InvalidOperationException("Could not find signature for chat sending");
}
var uiModule = (IntPtr) this.Functions.GetFramework()->GetUiModule();
using var payload = new ChatPayload(message);
var mem1 = Marshal.AllocHGlobal(400);
Marshal.StructureToPtr(payload, mem1, false);
this.ProcessChatBox(uiModule, mem1, IntPtr.Zero, 0);
Marshal.FreeHGlobal(mem1);
}
[StructLayout(LayoutKind.Explicit)]
[SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")]
private readonly struct ChatPayload : IDisposable {
[FieldOffset(0)]
private readonly IntPtr textPtr;
[FieldOffset(16)]
private readonly ulong textLen;
[FieldOffset(8)]
private readonly ulong unk1;
[FieldOffset(24)]
private readonly ulong unk2;
internal ChatPayload(string text) {
var stringBytes = Encoding.UTF8.GetBytes(text);
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
this.textLen = (ulong) (stringBytes.Length + 1);
this.unk1 = 64;
this.unk2 = 0;
}
public void Dispose() {
Marshal.FreeHGlobal(this.textPtr);
}
}
}
/// <summary>
/// <para>
/// Send a given message to the chat box. <b>This can send chat to the server.</b>
/// </para>
/// <para>
/// <b>This method is unsafe.</b> This method does no checking on your input and
/// may send content to the server that the normal client could not. You must
/// verify what you're sending and handle content and length to properly use
/// this.
/// </para>
/// </summary>
/// <param name="message">Message to send</param>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public unsafe void SendMessageUnsafe(byte[] message) {
if (this.ProcessChatBox == null) {
throw new InvalidOperationException("Could not find signature for chat sending");
}
var uiModule = (IntPtr) Framework.Instance()->GetUiModule();
using var payload = new ChatPayload(message);
var mem1 = Marshal.AllocHGlobal(400);
Marshal.StructureToPtr(payload, mem1, false);
this.ProcessChatBox(uiModule, mem1, IntPtr.Zero, 0);
Marshal.FreeHGlobal(mem1);
}
/// <summary>
/// <para>
/// Send a given message to the chat box. <b>This can send chat to the server.</b>
/// </para>
/// <para>
/// This method is slightly less unsafe than <see cref="SendMessageUnsafe"/>. It
/// will throw exceptions for certain inputs that the client can't normally send,
/// but it is still possible to make mistakes. Use with caution.
/// </para>
/// </summary>
/// <param name="message">message to send</param>
/// <exception cref="ArgumentException">If <paramref name="message"/> is empty, longer than 500 bytes in UTF-8, or contains invalid characters.</exception>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public void SendMessage(string message) {
var bytes = Encoding.UTF8.GetBytes(message);
if (bytes.Length == 0) {
throw new ArgumentException("message is empty", nameof(message));
}
if (bytes.Length > 500) {
throw new ArgumentException("message is longer than 500 bytes", nameof(message));
}
if (message.Length != this.SanitiseText(message).Length) {
throw new ArgumentException("message contained invalid characters", nameof(message));
}
this.SendMessageUnsafe(bytes);
}
/// <summary>
/// <para>
/// Sanitises a string by removing any invalid input.
/// </para>
/// <para>
/// The result of this method is safe to use with
/// <see cref="SendMessage"/>, provided that it is not empty or too
/// long.
/// </para>
/// </summary>
/// <param name="text">text to sanitise</param>
/// <returns>sanitised text</returns>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public unsafe string SanitiseText(string text) {
if (this._sanitiseString == null) {
throw new InvalidOperationException("Could not find signature for chat sanitisation");
}
var uText = Utf8String.FromString(text);
this._sanitiseString(uText, 0x27F, IntPtr.Zero);
var sanitised = uText->ToString();
uText->Dtor();
IMemorySpace.Free(uText);
return sanitised;
}
[StructLayout(LayoutKind.Explicit)]
[SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")]
private readonly struct ChatPayload : IDisposable {
[FieldOffset(0)]
private readonly IntPtr textPtr;
[FieldOffset(16)]
private readonly ulong textLen;
[FieldOffset(8)]
private readonly ulong unk1;
[FieldOffset(24)]
private readonly ulong unk2;
internal ChatPayload(byte[] stringBytes) {
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
this.textLen = (ulong) (stringBytes.Length + 1);
this.unk1 = 64;
this.unk2 = 0;
}
public void Dispose() {
Marshal.FreeHGlobal(this.textPtr);
}
}
}

View File

@ -1,168 +1,168 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
namespace XivCommon.Functions;
/// <summary>
/// Class containing chat bubble events and functions
/// </summary>
public class ChatBubbles : IDisposable {
private static class Signatures {
internal const string ChatBubbleOpen = "E8 ?? ?? ?? ?? C7 43 ?? ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8";
internal const string ChatBubbleUpdate = "48 85 D2 0F 84 ?? ?? ?? ?? 48 89 5C 24 ?? 57 48 83 EC 20 8B 41 0C";
}
private IObjectTable ObjectTable { get; }
private delegate void OpenChatBubbleDelegate(IntPtr manager, IntPtr @object, IntPtr text, byte a4);
private delegate void UpdateChatBubbleDelegate(IntPtr bubblePtr, IntPtr @object);
private Hook<OpenChatBubbleDelegate>? OpenChatBubbleHook { get; }
private Hook<UpdateChatBubbleDelegate>? UpdateChatBubbleHook { get; }
namespace XivCommon.Functions {
/// <summary>
/// The delegate for chat bubble events.
/// Class containing chat bubble events and functions
/// </summary>
public delegate void OnChatBubbleDelegate(ref GameObject @object, ref SeString text);
/// <summary>
/// The delegate for chat bubble update events.
/// </summary>
public delegate void OnUpdateChatBubbleDelegate(ref GameObject @object);
/// <summary>
/// <para>
/// The event that is fired when a chat bubble is shown.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ChatBubbles"/> hook to be enabled.
/// </para>
/// </summary>
public event OnChatBubbleDelegate? OnChatBubble;
/// <summary>
/// <para>
/// The event that is fired when a chat bubble is updated.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ChatBubbles"/> hook to be enabled.
/// </para>
/// </summary>
public event OnUpdateChatBubbleDelegate? OnUpdateBubble;
internal ChatBubbles(IObjectTable objectTable, ISigScanner scanner, IGameInteropProvider interop, bool hookEnabled) {
this.ObjectTable = objectTable;
if (!hookEnabled) {
return;
public class ChatBubbles : IDisposable {
private static class Signatures {
internal const string ChatBubbleOpen = "E8 ?? ?? ?? ?? 80 BF ?? ?? ?? ?? ?? C7 07 ?? ?? ?? ??";
internal const string ChatBubbleUpdate = "48 85 D2 0F 84 ?? ?? ?? ?? 48 89 5C 24 ?? 57 48 83 EC 20 8B 41 0C";
}
if (scanner.TryScanText(Signatures.ChatBubbleOpen, out var openPtr, "chat bubbles open")) {
this.OpenChatBubbleHook = interop.HookFromAddress<OpenChatBubbleDelegate>(openPtr, this.OpenChatBubbleDetour);
this.OpenChatBubbleHook.Enable();
}
private ObjectTable ObjectTable { get; }
if (scanner.TryScanText(Signatures.ChatBubbleUpdate, out var updatePtr, "chat bubbles update")) {
this.UpdateChatBubbleHook = interop.HookFromAddress<UpdateChatBubbleDelegate>(updatePtr + 9, this.UpdateChatBubbleDetour);
this.UpdateChatBubbleHook.Enable();
}
}
private delegate void OpenChatBubbleDelegate(IntPtr manager, IntPtr @object, IntPtr text, byte a4);
/// <inheritdoc />
public void Dispose() {
this.OpenChatBubbleHook?.Dispose();
this.UpdateChatBubbleHook?.Dispose();
}
private delegate void UpdateChatBubbleDelegate(IntPtr bubblePtr, IntPtr @object);
private void OpenChatBubbleDetour(IntPtr manager, IntPtr @object, IntPtr text, byte a4) {
try {
this.OpenChatBubbleDetourInner(manager, @object, text, a4);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in chat bubble detour");
this.OpenChatBubbleHook!.Original(manager, @object, text, a4);
}
}
private Hook<OpenChatBubbleDelegate>? OpenChatBubbleHook { get; }
private void OpenChatBubbleDetourInner(IntPtr manager, IntPtr objectPtr, IntPtr textPtr, byte a4) {
var @object = this.ObjectTable.CreateObjectReference(objectPtr);
if (@object == null) {
return;
}
private Hook<UpdateChatBubbleDelegate>? UpdateChatBubbleHook { get; }
var text = Util.ReadSeString(textPtr);
/// <summary>
/// The delegate for chat bubble events.
/// </summary>
public delegate void OnChatBubbleDelegate(ref GameObject @object, ref SeString text);
try {
this.OnChatBubble?.Invoke(ref @object, ref text);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in chat bubble event");
}
/// <summary>
/// The delegate for chat bubble update events.
/// </summary>
public delegate void OnUpdateChatBubbleDelegate(ref GameObject @object);
var newText = text.Encode().Terminate();
/// <summary>
/// <para>
/// The event that is fired when a chat bubble is shown.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ChatBubbles"/> hook to be enabled.
/// </para>
/// </summary>
public event OnChatBubbleDelegate? OnChatBubble;
unsafe {
fixed (byte* newTextPtr = newText) {
this.OpenChatBubbleHook!.Original(manager, @object.Address, (IntPtr) newTextPtr, a4);
/// <summary>
/// <para>
/// The event that is fired when a chat bubble is updated.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ChatBubbles"/> hook to be enabled.
/// </para>
/// </summary>
public event OnUpdateChatBubbleDelegate? OnUpdateBubble;
internal ChatBubbles(ObjectTable objectTable, SigScanner scanner, bool hookEnabled) {
this.ObjectTable = objectTable;
if (!hookEnabled) {
return;
}
if (scanner.TryScanText(Signatures.ChatBubbleOpen, out var openPtr, "chat bubbles open")) {
this.OpenChatBubbleHook = new Hook<OpenChatBubbleDelegate>(openPtr, this.OpenChatBubbleDetour);
this.OpenChatBubbleHook.Enable();
}
if (scanner.TryScanText(Signatures.ChatBubbleUpdate, out var updatePtr, "chat bubbles update")) {
this.UpdateChatBubbleHook = new Hook<UpdateChatBubbleDelegate>(updatePtr + 9, this.UpdateChatBubbleDetour);
this.UpdateChatBubbleHook.Enable();
}
}
}
private void UpdateChatBubbleDetour(IntPtr bubblePtr, IntPtr @object) {
try {
this.UpdateChatBubbleDetourInner(bubblePtr, @object);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in update chat bubble detour");
this.UpdateChatBubbleHook!.Original(bubblePtr, @object);
/// <inheritdoc />
public void Dispose() {
this.OpenChatBubbleHook?.Dispose();
this.UpdateChatBubbleHook?.Dispose();
}
private void OpenChatBubbleDetour(IntPtr manager, IntPtr @object, IntPtr text, byte a4) {
try {
this.OpenChatBubbleDetourInner(manager, @object, text, a4);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in chat bubble detour");
this.OpenChatBubbleHook!.Original(manager, @object, text, a4);
}
}
private void OpenChatBubbleDetourInner(IntPtr manager, IntPtr objectPtr, IntPtr textPtr, byte a4) {
var @object = this.ObjectTable.CreateObjectReference(objectPtr);
if (@object == null) {
return;
}
var text = Util.ReadSeString(textPtr);
try {
this.OnChatBubble?.Invoke(ref @object, ref text);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in chat bubble event");
}
var newText = text.Encode().Terminate();
unsafe {
fixed (byte* newTextPtr = newText) {
this.OpenChatBubbleHook!.Original(manager, @object.Address, (IntPtr) newTextPtr, a4);
}
}
}
private void UpdateChatBubbleDetour(IntPtr bubblePtr, IntPtr @object) {
try {
this.UpdateChatBubbleDetourInner(bubblePtr, @object);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in update chat bubble detour");
this.UpdateChatBubbleHook!.Original(bubblePtr, @object);
}
}
private void UpdateChatBubbleDetourInner(IntPtr bubblePtr, IntPtr objectPtr) {
// var bubble = (ChatBubble*) bubblePtr;
var @object = this.ObjectTable.CreateObjectReference(objectPtr);
if (@object == null) {
return;
}
try {
this.OnUpdateBubble?.Invoke(ref @object);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in chat bubble update event");
}
this.UpdateChatBubbleHook!.Original(bubblePtr, @object.Address);
}
}
private void UpdateChatBubbleDetourInner(IntPtr bubblePtr, IntPtr objectPtr) {
// var bubble = (ChatBubble*) bubblePtr;
var @object = this.ObjectTable.CreateObjectReference(objectPtr);
if (@object == null) {
return;
}
[StructLayout(LayoutKind.Explicit, Size = 0x80)]
internal unsafe struct ChatBubble {
[FieldOffset(0x0)]
internal readonly uint Id;
try {
this.OnUpdateBubble?.Invoke(ref @object);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in chat bubble update event");
}
[FieldOffset(0x4)]
internal float Timer;
this.UpdateChatBubbleHook!.Original(bubblePtr, @object.Address);
[FieldOffset(0x8)]
internal readonly uint Unk_8; // enum probably
[FieldOffset(0xC)]
internal ChatBubbleStatus Status; // state of the bubble
[FieldOffset(0x10)]
internal readonly byte* Text;
[FieldOffset(0x78)]
internal readonly ulong Unk_78; // check whats in memory here
}
internal enum ChatBubbleStatus : uint {
GetData = 0,
On = 1,
Init = 2,
Off = 3,
}
}
[StructLayout(LayoutKind.Explicit, Size = 0x80)]
internal unsafe struct ChatBubble {
[FieldOffset(0x0)]
internal readonly uint Id;
[FieldOffset(0x4)]
internal float Timer;
[FieldOffset(0x8)]
internal readonly uint Unk_8; // enum probably
[FieldOffset(0xC)]
internal ChatBubbleStatus Status; // state of the bubble
[FieldOffset(0x10)]
internal readonly byte* Text;
[FieldOffset(0x78)]
internal readonly ulong Unk_78; // check whats in memory here
}
internal enum ChatBubbleStatus : uint {
GetData = 0,
On = 1,
Init = 2,
Off = 3,
}

View File

@ -0,0 +1,54 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// The base class for context menu arguments
/// </summary>
public abstract class BaseContextMenuArgs {
/// <summary>
/// Pointer to the context menu addon.
/// </summary>
public IntPtr Addon { get; }
/// <summary>
/// Pointer to the context menu agent.
/// </summary>
public IntPtr Agent { get; }
/// <summary>
/// The name of the addon containing this context menu, if any.
/// </summary>
public string? ParentAddonName { get; }
/// <summary>
/// The object ID for this context menu. May be invalid (0xE0000000).
/// </summary>
public uint ObjectId { get; }
/// <summary>
/// The lower half of the content ID of the object for this context menu. May be zero.
/// </summary>
public uint ContentIdLower { get; }
/// <summary>
/// The text related to this context menu, usually an object name.
/// </summary>
public SeString? Text { get; }
/// <summary>
/// The world of the object this context menu is for, if any.
/// </summary>
public ushort ObjectWorld { get; }
internal BaseContextMenuArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint objectId, uint contentIdLower, SeString? text, ushort objectWorld) {
this.Addon = addon;
this.Agent = agent;
this.ParentAddonName = parentAddonName;
this.ObjectId = objectId;
this.ContentIdLower = contentIdLower;
this.Text = text;
this.ObjectWorld = objectWorld;
}
}
}

View File

@ -0,0 +1,16 @@
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A base context menu item
/// </summary>
public abstract class BaseContextMenuItem {
/// <summary>
/// If this item should be enabled in the menu.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// If this item should have the submenu arrow in the menu.
/// </summary>
public bool IsSubMenu { get; set; }
}
}

View File

@ -0,0 +1,773 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud;
using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using XivCommon.Functions.ContextMenu.Inventory;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// Context menu functions
/// </summary>
public class ContextMenu : IDisposable {
private static class Signatures {
internal const string SomeOpenAddonThing = "E8 ?? ?? ?? ?? 0F B7 C0 48 83 C4 60";
internal const string ContextMenuOpen = "48 8B C4 57 41 56 41 57 48 81 EC ?? ?? ?? ??";
internal const string ContextMenuSelected = "48 89 5C 24 ?? 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 44 24 ?? 80 B9 ?? ?? ?? ?? ??";
internal const string ContextMenuEvent66 = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 84 ?? ?? ?? ??";
internal const string InventoryContextMenuEvent30 = "E8 ?? ?? ?? ?? 48 83 C4 30 5B C3 8B 83 ?? ?? ?? ??";
internal const string SetUpContextSubMenu = "E8 ?? ?? ?? ?? 44 39 A3 ?? ?? ?? ?? 0F 86 ?? ?? ?? ??";
internal const string SetUpInventoryContextSubMenu = "44 88 44 24 ?? 88 54 24 10 53";
internal const string TitleContextMenuOpen = "48 8B C4 57 41 55 41 56 48 81 EC ?? ?? ?? ??";
internal const string AtkValueChangeType = "E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??";
internal const string AtkValueSetString = "E8 ?? ?? ?? ?? 41 03 ED";
internal const string GetAddonByInternalId = "E8 ?? ?? ?? ?? 8B 6B 20";
}
#region Offsets and other constants
private const int MaxItems = 32;
/// <summary>
/// Offset from addon to menu type
/// </summary>
private const int ParentAddonIdOffset = 0x1D2;
private const int AddonArraySizeOffset = 0x1CA;
private const int AddonArrayOffset = 0x160;
private const int ContextMenuItemOffset = 7;
/// <summary>
/// Offset from agent to actions byte array pointer (have to add the actions offset after)
/// </summary>
private const int MenuActionsPointerOffset = 0xD18;
/// <summary>
/// SetUpContextSubMenu checks this
/// </summary>
private const int BooleanOffsetCheck = 0x690;
/// <summary>
/// Offset from [MenuActionsPointer] to actions byte array
/// </summary>
private const int MenuActionsOffset = 0x428;
/// <summary>
/// Offset from inventory context agent to actions byte array
/// </summary>
private const int InventoryMenuActionsOffset = 0x558;
private const int ObjectIdOffset = 0xEF0;
private const int ContentIdLowerOffset = 0xEE0;
private const int TextPointerOffset = 0xE08;
private const int WorldOffset = 0xF00;
private const int ItemIdOffset = 0x5F8;
private const int ItemAmountOffset = 0x5FC;
private const int ItemHqOffset = 0x604;
// Found in the first function in the agent's vtable
private const byte NoopContextId = 0x67;
private const byte InventoryNoopContextId = 0xFF;
private const byte ContextSubId = 0x66;
private const byte InventoryContextSubId = 0x30;
#endregion
// 82C570 called when you click on a title menu context item
/// <summary>
/// The delegate for context menu events.
/// </summary>
public delegate void ContextMenuOpenEventDelegate(ContextMenuOpenArgs args);
/// <summary>
/// <para>
/// The event that is fired when a context menu is being prepared for opening.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ContextMenu"/> hook to be enabled.
/// </para>
/// </summary>
public event ContextMenuOpenEventDelegate? OpenContextMenu;
/// <summary>
/// The delegate for inventory context menu events.
/// </summary>
public delegate void InventoryContextMenuOpenEventDelegate(InventoryContextMenuOpenArgs args);
/// <summary>
/// <para>
/// The event that is fired when an inventory context menu is being prepared for opening.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ContextMenu"/> hook to be enabled.
/// </para>
/// </summary>
public event InventoryContextMenuOpenEventDelegate? OpenInventoryContextMenu;
/// <summary>
/// The delegate that is run when a context menu item is selected.
/// </summary>
public delegate void ContextMenuItemSelectedDelegate(ContextMenuItemSelectedArgs args);
/// <summary>
/// The delegate that is run when an inventory context menu item is selected.
/// </summary>
public delegate void InventoryContextMenuItemSelectedDelegate(InventoryContextMenuItemSelectedArgs args);
private delegate IntPtr SomeOpenAddonThingDelegate(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, IntPtr a6, IntPtr a7, ushort a8);
private Hook<SomeOpenAddonThingDelegate>? SomeOpenAddonThingHook { get; }
private unsafe delegate byte ContextMenuOpenDelegate(IntPtr addon, int menuSize, AtkValue* atkValueArgs);
private delegate IntPtr GetAddonByInternalIdDelegate(IntPtr raptureAtkUnitManager, short id);
private readonly GetAddonByInternalIdDelegate _getAddonByInternalId = null!;
private Hook<ContextMenuOpenDelegate>? ContextMenuOpenHook { get; }
private Hook<ContextMenuOpenDelegate>? TitleContextMenuOpenHook { get; }
private delegate byte ContextMenuItemSelectedInternalDelegate(IntPtr addon, int index, byte a3);
private Hook<ContextMenuItemSelectedInternalDelegate>? ContextMenuItemSelectedHook { get; }
private delegate byte SetUpContextSubMenuDelegate(IntPtr agent);
private readonly SetUpContextSubMenuDelegate _setUpContextSubMenu = null!;
private delegate IntPtr SetUpInventoryContextSubMenuDelegate(IntPtr agent, byte hasTitle, byte zero);
private readonly SetUpInventoryContextSubMenuDelegate _setUpInventoryContextSubMenu = null!;
private delegate byte ContextMenuEvent66Delegate(IntPtr agent);
private Hook<ContextMenuEvent66Delegate>? ContextMenuEvent66Hook { get; }
private delegate void InventoryContextMenuEvent30Delegate(IntPtr agent, IntPtr a2, int a3, int a4, short a5);
private Hook<InventoryContextMenuEvent30Delegate>? InventoryContextMenuEvent30Hook { get; }
private unsafe delegate void AtkValueChangeTypeDelegate(AtkValue* thisPtr, ValueType type);
private readonly AtkValueChangeTypeDelegate _atkValueChangeType = null!;
private unsafe delegate void AtkValueSetStringDelegate(AtkValue* thisPtr, byte* bytes);
private readonly AtkValueSetStringDelegate _atkValueSetString = null!;
private GameFunctions Functions { get; }
private ClientLanguage Language { get; }
private IntPtr Agent { get; set; } = IntPtr.Zero;
private List<BaseContextMenuItem> Items { get; } = new();
private int NormalSize { get; set; }
internal ContextMenu(GameFunctions functions, SigScanner scanner, ClientLanguage language, Hooks hooks) {
this.Functions = functions;
this.Language = language;
if (!hooks.HasFlag(Hooks.ContextMenu)) {
return;
}
if (scanner.TryScanText(Signatures.AtkValueChangeType, out var changeTypePtr, "Context Menu (change type)")) {
this._atkValueChangeType = Marshal.GetDelegateForFunctionPointer<AtkValueChangeTypeDelegate>(changeTypePtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.AtkValueSetString, out var setStringPtr, "Context Menu (set string)")) {
this._atkValueSetString = Marshal.GetDelegateForFunctionPointer<AtkValueSetStringDelegate>(setStringPtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.GetAddonByInternalId, out var getAddonPtr, "Context Menu (get addon)")) {
this._getAddonByInternalId = Marshal.GetDelegateForFunctionPointer<GetAddonByInternalIdDelegate>(getAddonPtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.SetUpContextSubMenu, out var setUpSubPtr, "Context Menu (set up submenu)")) {
this._setUpContextSubMenu = Marshal.GetDelegateForFunctionPointer<SetUpContextSubMenuDelegate>(setUpSubPtr);
} else {
return;
}
// TODO: uncomment when inv submenus
// if (scanner.TryScanText(Signatures.SetUpInventoryContextSubMenu, out var setUpInvSubPtr, "Context Menu (set up inventory submenu)")) {
// this._setUpInventoryContextSubMenu = Marshal.GetDelegateForFunctionPointer<SetUpInventoryContextSubMenuDelegate>(setUpInvSubPtr);
// } else {
// return;
// }
if (scanner.TryScanText(Signatures.SomeOpenAddonThing, out var thingPtr, "Context Menu (some OpenAddon thing)")) {
this.SomeOpenAddonThingHook = new Hook<SomeOpenAddonThingDelegate>(thingPtr, this.SomeOpenAddonThingDetour);
this.SomeOpenAddonThingHook.Enable();
} else {
return;
}
if (scanner.TryScanText(Signatures.ContextMenuOpen, out var openPtr, "Context Menu open")) {
unsafe {
this.ContextMenuOpenHook = new Hook<ContextMenuOpenDelegate>(openPtr, this.OpenMenuDetour);
}
this.ContextMenuOpenHook.Enable();
} else {
return;
}
if (scanner.TryScanText(Signatures.ContextMenuSelected, out var selectedPtr, "Context Menu selected")) {
this.ContextMenuItemSelectedHook = new Hook<ContextMenuItemSelectedInternalDelegate>(selectedPtr, this.ItemSelectedDetour);
this.ContextMenuItemSelectedHook.Enable();
}
if (scanner.TryScanText(Signatures.TitleContextMenuOpen, out var titleOpenPtr, "Context Menu (title menu open)")) {
unsafe {
this.TitleContextMenuOpenHook = new Hook<ContextMenuOpenDelegate>(titleOpenPtr, this.TitleContextMenuOpenDetour);
}
this.TitleContextMenuOpenHook.Enable();
}
if (scanner.TryScanText(Signatures.ContextMenuEvent66, out var event66Ptr, "Context Menu (event 66)")) {
this.ContextMenuEvent66Hook = new Hook<ContextMenuEvent66Delegate>(event66Ptr, this.ContextMenuEvent66Detour);
this.ContextMenuEvent66Hook.Enable();
}
// TODO: uncomment when inv submenus
// if (scanner.TryScanText(Signatures.InventoryContextMenuEvent30, out var event30Ptr, "Context Menu (inv event 30)")) {
// this.InventoryContextMenuEvent30Hook = new Hook<InventoryContextMenuEvent30Delegate>(event30Ptr, new InventoryContextMenuEvent30Delegate(this.InventoryContextMenuEvent30Detour));
// this.InventoryContextMenuEvent30Hook.Enable();
// }
}
/// <inheritdoc />
public void Dispose() {
this.SomeOpenAddonThingHook?.Dispose();
this.ContextMenuOpenHook?.Dispose();
this.TitleContextMenuOpenHook?.Dispose();
this.ContextMenuItemSelectedHook?.Dispose();
this.ContextMenuEvent66Hook?.Dispose();
this.InventoryContextMenuEvent30Hook?.Dispose();
}
private IntPtr SomeOpenAddonThingDetour(IntPtr a1, IntPtr a2, IntPtr a3, uint a4, IntPtr a5, IntPtr a6, IntPtr a7, ushort a8) {
this.Agent = a6;
return this.SomeOpenAddonThingHook!.Original(a1, a2, a3, a4, a5, a6, a7, a8);
}
private unsafe byte TitleContextMenuOpenDetour(IntPtr addon, int menuSize, AtkValue* atkValueArgs) {
if (this.SubMenuTitle == IntPtr.Zero) {
this.Items.Clear();
}
return this.TitleContextMenuOpenHook!.Original(addon, menuSize, atkValueArgs);
}
private enum AgentType {
Normal,
Inventory,
Unknown,
}
private unsafe (AgentType agentType, IntPtr agent) GetContextMenuAgent(IntPtr? agent = null) {
agent ??= this.Agent;
IntPtr GetAgent(AgentId id) {
return (IntPtr) this.Functions.GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(id);
}
var agentType = AgentType.Unknown;
if (agent == GetAgent(AgentId.Context)) {
agentType = AgentType.Normal;
} else if (agent == GetAgent(AgentId.InventoryContext)) {
agentType = AgentType.Inventory;
}
return (agentType, agent.Value);
}
private unsafe string? GetParentAddonName(IntPtr addon) {
var parentAddonId = Marshal.ReadInt16(addon + ParentAddonIdOffset);
if (parentAddonId == 0) {
return null;
}
var stage = AtkStage.GetSingleton();
var parentAddon = this._getAddonByInternalId((IntPtr) stage->RaptureAtkUnitManager, parentAddonId);
return Encoding.UTF8.GetString(Util.ReadTerminated(parentAddon + 8));
}
private unsafe IntPtr GetAddonFromAgent(IntPtr agent) {
var addonId = *(byte*) (agent + 0x20);
if (addonId == 0) {
return IntPtr.Zero;
}
var stage = AtkStage.GetSingleton();
return this._getAddonByInternalId((IntPtr) stage->RaptureAtkUnitManager, addonId);
}
private unsafe (uint objectId, uint contentIdLower, SeString? text, ushort objectWorld) GetAgentInfo(IntPtr agent) {
var objectId = *(uint*) (agent + ObjectIdOffset);
var contentIdLower = *(uint*) (agent + ContentIdLowerOffset);
var textBytes = Util.ReadTerminated(Marshal.ReadIntPtr(agent + TextPointerOffset));
var text = textBytes.Length == 0 ? null : SeString.Parse(textBytes);
var objectWorld = *(ushort*) (agent + WorldOffset);
return (objectId, contentIdLower, text, objectWorld);
}
private static unsafe (uint itemId, uint itemAmount, bool itemHq) GetInventoryAgentInfo(IntPtr agent) {
var itemId = *(uint*) (agent + ItemIdOffset);
var itemAmount = *(uint*) (agent + ItemAmountOffset);
var itemHq = *(byte*) (agent + ItemHqOffset) == 1;
return (itemId, itemAmount, itemHq);
}
[HandleProcessCorruptedStateExceptions]
private unsafe byte OpenMenuDetour(IntPtr addon, int menuSize, AtkValue* atkValueArgs) {
try {
this.OpenMenuDetourInner(addon, ref menuSize, ref atkValueArgs);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OpenMenuDetour");
}
return this.ContextMenuOpenHook!.Original(addon, menuSize, atkValueArgs);
}
private unsafe AtkValue* ExpandContextMenuArray(IntPtr addon) {
const ulong newItemCount = MaxItems * 2 + ContextMenuItemOffset;
var oldArray = *(AtkValue**) (addon + AddonArrayOffset);
var oldArrayItemCount = *(ushort*) (addon + AddonArraySizeOffset);
// if the array has enough room, don't reallocate
if (oldArrayItemCount >= newItemCount) {
return oldArray;
}
// reallocate
var size = (ulong) sizeof(AtkValue) * newItemCount + 8;
var newArray = this.Functions.UiAlloc.Alloc(size);
// zero new memory
Marshal.Copy(new byte[size], 0, newArray, (int) size);
// update size and pointer
*(ulong*) newArray = newItemCount;
*(void**) (addon + AddonArrayOffset) = (void*) (newArray + 8);
*(ushort*) (addon + AddonArraySizeOffset) = (ushort) newItemCount;
// copy old memory if existing
if (oldArray != null) {
Buffer.MemoryCopy(oldArray, (void*) (newArray + 8), size, (ulong) sizeof(AtkValue) * oldArrayItemCount);
this.Functions.UiAlloc.Free((IntPtr) oldArray - 8);
}
return (AtkValue*) (newArray + 8);
}
private unsafe void OpenMenuDetourInner(IntPtr addon, ref int menuSize, ref AtkValue* atkValueArgs) {
this.Items.Clear();
this.FreeSubMenuTitle();
var (agentType, agent) = this.GetContextMenuAgent();
if (agent == IntPtr.Zero) {
return;
}
if (agentType == AgentType.Unknown) {
return;
}
atkValueArgs = this.ExpandContextMenuArray(addon);
var inventory = agentType == AgentType.Inventory;
var offset = ContextMenuItemOffset + (inventory ? 0 : *(long*) (agent + BooleanOffsetCheck) != 0 ? 1 : 0);
this.NormalSize = (int) (&atkValueArgs[0])->UInt;
// idx 3 is bitmask of indices that are submenus
var submenuArg = &atkValueArgs[3];
var submenus = (int) submenuArg->UInt;
var hasGameDisabled = menuSize - offset - this.NormalSize > 0;
var menuActions = inventory
? (byte*) (agent + InventoryMenuActionsOffset)
: (byte*) (Marshal.ReadIntPtr(agent + MenuActionsPointerOffset) + MenuActionsOffset);
var nativeItems = new List<NativeContextMenuItem>();
for (var i = 0; i < this.NormalSize; i++) {
var atkItem = &atkValueArgs[offset + i];
var name = Util.ReadSeString((IntPtr) atkItem->String);
var enabled = true;
if (hasGameDisabled) {
var disabledItem = &atkValueArgs[offset + this.NormalSize + i];
enabled = disabledItem->Int == 0;
}
var action = *(menuActions + offset + i);
var isSubMenu = (submenus & (1 << i)) > 0;
nativeItems.Add(new NativeContextMenuItem(action, name, enabled, isSubMenu));
}
if (this.PopulateItems(addon, agent, this.OpenContextMenu, this.OpenInventoryContextMenu, nativeItems)) {
return;
}
var hasCustomDisabled = this.Items.Any(item => !item.Enabled);
var hasAnyDisabled = hasGameDisabled || hasCustomDisabled;
// clear all submenu flags
submenuArg->UInt = 0;
for (var i = 0; i < this.Items.Count; i++) {
var item = this.Items[i];
if (hasAnyDisabled) {
var disabledArg = &atkValueArgs[offset + this.Items.Count + i];
this._atkValueChangeType(disabledArg, ValueType.Int);
disabledArg->Int = item.Enabled ? 0 : 1;
}
// set up the agent to take the appropriate action for this item
*(menuActions + offset + i) = item switch {
NativeContextMenuItem nativeItem => nativeItem.InternalAction,
NormalContextSubMenuItem => ContextSubId,
// TODO: uncomment when inv submenus
// InventoryContextSubMenuItem => InventoryContextSubId,
_ => inventory ? InventoryNoopContextId : NoopContextId,
};
// set submenu flag
if (item.IsSubMenu) {
submenuArg->UInt |= (uint) (1 << i);
}
// set up the menu item
var newItem = &atkValueArgs[offset + i];
this._atkValueChangeType(newItem, ValueType.String);
var name = this.GetItemName(item);
fixed (byte* nameBytesPtr = name.Encode().Terminate()) {
this._atkValueSetString(newItem, nameBytesPtr);
}
}
(&atkValueArgs[0])->UInt = (uint) this.Items.Count;
menuSize = (int) (&atkValueArgs[0])->UInt;
if (hasAnyDisabled) {
menuSize *= 2;
}
menuSize += offset;
}
/// <returns>true on error</returns>
private bool PopulateItems(IntPtr addon, IntPtr agent, ContextMenuOpenEventDelegate? normalAction, InventoryContextMenuOpenEventDelegate? inventoryAction, IReadOnlyCollection<NativeContextMenuItem>? nativeItems = null) {
var (agentType, _) = this.GetContextMenuAgent(agent);
if (agentType == AgentType.Unknown) {
return true;
}
var inventory = agentType == AgentType.Inventory;
var parentAddonName = this.GetParentAddonName(addon);
if (inventory) {
var info = GetInventoryAgentInfo(agent);
var args = new InventoryContextMenuOpenArgs(
addon,
agent,
parentAddonName,
info.itemId,
info.itemAmount,
info.itemHq
);
if (nativeItems != null) {
args.Items.AddRange(nativeItems);
}
try {
inventoryAction?.Invoke(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OpenMenuDetour");
return true;
}
// remove any NormalContextMenuItems that may have been added - these will crash the game
args.Items.RemoveAll(item => item is NormalContextMenuItem);
// set the agent of any remaining custom items
foreach (var item in args.Items) {
switch (item) {
case InventoryContextMenuItem custom:
custom.Agent = agent;
break;
case InventoryContextSubMenuItem custom:
custom.Agent = agent;
break;
}
}
this.Items.AddRange(args.Items);
} else {
var info = this.GetAgentInfo(agent);
var args = new ContextMenuOpenArgs(
addon,
agent,
parentAddonName,
info.objectId,
info.contentIdLower,
info.text,
info.objectWorld
);
if (nativeItems != null) {
args.Items.AddRange(nativeItems);
}
try {
normalAction?.Invoke(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OpenMenuDetour");
return true;
}
// remove any InventoryContextMenuItems that may have been added - these will crash the game
args.Items.RemoveAll(item => item is InventoryContextMenuItem);
// set the agent of any remaining custom items
foreach (var item in args.Items) {
switch (item) {
case NormalContextMenuItem custom:
custom.Agent = agent;
break;
case NormalContextSubMenuItem custom:
custom.Agent = agent;
break;
}
}
this.Items.AddRange(args.Items);
}
if (this.Items.Count > MaxItems) {
var toRemove = this.Items.Count - MaxItems;
this.Items.RemoveRange(MaxItems, toRemove);
Logger.LogWarning($"Context menu item limit ({MaxItems}) exceeded. Removing {toRemove} item(s).");
}
return false;
}
private SeString GetItemName(BaseContextMenuItem item) {
return item switch {
NormalContextMenuItem custom => this.Language switch {
ClientLanguage.Japanese => custom.NameJapanese,
ClientLanguage.English => custom.NameEnglish,
ClientLanguage.German => custom.NameGerman,
ClientLanguage.French => custom.NameFrench,
_ => custom.NameEnglish,
},
InventoryContextMenuItem custom => this.Language switch {
ClientLanguage.Japanese => custom.NameJapanese,
ClientLanguage.English => custom.NameEnglish,
ClientLanguage.German => custom.NameGerman,
ClientLanguage.French => custom.NameFrench,
_ => custom.NameEnglish,
},
NormalContextSubMenuItem custom => this.Language switch {
ClientLanguage.Japanese => custom.NameJapanese,
ClientLanguage.English => custom.NameEnglish,
ClientLanguage.German => custom.NameGerman,
ClientLanguage.French => custom.NameFrench,
_ => custom.NameEnglish,
},
InventoryContextSubMenuItem custom => this.Language switch {
ClientLanguage.Japanese => custom.NameJapanese,
ClientLanguage.English => custom.NameEnglish,
ClientLanguage.German => custom.NameGerman,
ClientLanguage.French => custom.NameFrench,
_ => custom.NameEnglish,
},
NativeContextMenuItem native => native.Name,
_ => "Invalid context menu item",
};
}
private BaseContextMenuItem? SubMenuItem { get; set; }
private byte ItemSelectedDetour(IntPtr addon, int index, byte a3) {
this.FreeSubMenuTitle();
if (index < 0 || index >= this.Items.Count) {
goto Original;
}
var item = this.Items[index];
switch (item) {
case NormalContextSubMenuItem sub: {
this.SubMenuItem = sub;
break;
}
case InventoryContextSubMenuItem sub: {
this.SubMenuItem = sub;
break;
}
case NormalContextMenuItem custom: {
var addonName = this.GetParentAddonName(addon);
var info = this.GetAgentInfo(custom.Agent);
var args = new ContextMenuItemSelectedArgs(
addon,
custom.Agent,
addonName,
info.objectId,
info.contentIdLower,
info.text,
info.objectWorld
);
try {
custom.Action(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in custom context menu item");
}
break;
}
case InventoryContextMenuItem custom: {
var addonName = this.GetParentAddonName(addon);
var info = GetInventoryAgentInfo(custom.Agent);
var args = new InventoryContextMenuItemSelectedArgs(
addon,
custom.Agent,
addonName,
info.itemId,
info.itemAmount,
info.itemHq
);
try {
custom.Action(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in custom context menu item");
}
break;
}
}
Original:
return this.ContextMenuItemSelectedHook!.Original(addon, index, a3);
}
private IntPtr SubMenuTitle { get; set; } = IntPtr.Zero;
private void FreeSubMenuTitle() {
if (this.SubMenuTitle == IntPtr.Zero) {
return;
}
this.Functions.UiAlloc.Free(this.SubMenuTitle);
this.SubMenuTitle = IntPtr.Zero;
}
/// <returns>false if original should be called</returns>
private unsafe bool SubMenuInner(IntPtr agent) {
if (this.SubMenuItem == null) {
return false;
}
var subMenuItem = this.SubMenuItem;
this.SubMenuItem = null;
// free our workaround pointer
this.FreeSubMenuTitle();
this.Items.Clear();
var name = this.GetItemName(subMenuItem);
// Since the game checks the agent's AtkValue array for the submenu title, and since we
// don't update that array, we need to work around this check.
// First, we will convince the game to make the submenu title pointer null by telling it
// that an invalid index was selected.
// Second, we will replace the null pointer with our own pointer.
// Third, we will restore the original selected index.
// step 1
var selectedIdx = (byte*) (agent + 0x670);
var wasSelected = *selectedIdx;
*selectedIdx = 0xFF;
this._setUpContextSubMenu(agent);
// step 2 (see SetUpContextSubMenu)
var nameBytes = name.Encode().Terminate();
this.SubMenuTitle = this.Functions.UiAlloc.Alloc((ulong) nameBytes.Length);
Marshal.Copy(nameBytes, 0, this.SubMenuTitle, nameBytes.Length);
var v10 = agent + 0x678 * *(byte*) (agent + 0x1740) + 0x28;
*(byte**) (v10 + 0x668) = (byte*) this.SubMenuTitle;
// step 3
*selectedIdx = wasSelected;
var secondaryArgsPtr = Marshal.ReadIntPtr(agent + MenuActionsPointerOffset);
var submenuArgs = (AtkValue*) (secondaryArgsPtr + 8);
var addon = this.GetAddonFromAgent(agent);
var normalAction = (subMenuItem as NormalContextSubMenuItem)?.Action;
var inventoryAction = (subMenuItem as InventoryContextSubMenuItem)?.Action;
if (this.PopulateItems(addon, agent, normalAction, inventoryAction)) {
return true;
}
var booleanOffset = *(long*) (agent + *(byte*) (agent + 0x1740) * 0x678 + 0x690) != 0 ? 1 : 0;
for (var i = 0; i < this.Items.Count; i++) {
var item = this.Items[i];
*(ushort*) secondaryArgsPtr += 1;
var arg = &submenuArgs[ContextMenuItemOffset + i + 1];
this._atkValueChangeType(arg, ValueType.String);
var itemName = this.GetItemName(item);
fixed (byte* namePtr = itemName.Encode().Terminate()) {
this._atkValueSetString(arg, namePtr);
}
// set action to no-op
*(byte*) (secondaryArgsPtr + booleanOffset + i + ContextMenuItemOffset + 0x428) = NoopContextId;
}
return true;
}
private byte ContextMenuEvent66Detour(IntPtr agent) {
return this.SubMenuInner(agent) ? (byte) 0 : this.ContextMenuEvent66Hook!.Original(agent);
}
private void InventoryContextMenuEvent30Detour(IntPtr agent, IntPtr a2, int a3, int a4, short a5) {
if (!this.SubMenuInner(agent)) {
this.InventoryContextMenuEvent30Hook!.Original(agent, a2, a3, a4, a5);
}
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// Arguments for the context menu item selected delegate.
/// </summary>
public class ContextMenuItemSelectedArgs : BaseContextMenuArgs {
internal ContextMenuItemSelectedArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint objectId, uint contentIdLower, SeString? text, ushort objectWorld) : base(addon, agent, parentAddonName, objectId, contentIdLower, text, objectWorld) {
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// Arguments for the context menu event.
/// </summary>
public class ContextMenuOpenArgs : BaseContextMenuArgs {
/// <summary>
/// Context menu items in this menu.
/// </summary>
public List<BaseContextMenuItem> Items { get; } = new();
internal ContextMenuOpenArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint objectId, uint contentIdLower, SeString? text, ushort objectWorld) : base(addon, agent, parentAddonName, objectId, contentIdLower, text, objectWorld) {
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A custom context menu item
/// </summary>
public abstract class CustomContextMenuItem<T> : BaseContextMenuItem
where T : Delegate {
internal IntPtr Agent { get; set; }
/// <summary>
/// The name of the context item to be shown for English clients.
/// </summary>
public SeString NameEnglish { get; set; }
/// <summary>
/// The name of the context item to be shown for Japanese clients.
/// </summary>
public SeString NameJapanese { get; set; }
/// <summary>
/// The name of the context item to be shown for French clients.
/// </summary>
public SeString NameFrench { get; set; }
/// <summary>
/// The name of the context item to be shown for German clients.
/// </summary>
public SeString NameGerman { get; set; }
/// <summary>
/// The action to perform when this item is clicked.
/// </summary>
public T Action { get; set; }
/// <summary>
/// Create a new context menu item.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
internal CustomContextMenuItem(SeString name, T action) {
this.NameEnglish = name;
this.NameJapanese = name;
this.NameFrench = name;
this.NameGerman = name;
this.Action = action;
}
}
}

View File

@ -0,0 +1,47 @@
using System;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// The base class for inventory context menu arguments
/// </summary>
public abstract class BaseInventoryContextMenuArgs {
/// <summary>
/// Pointer to the context menu addon.
/// </summary>
public IntPtr Addon { get; }
/// <summary>
/// Pointer to the context menu agent.
/// </summary>
public IntPtr Agent { get; }
/// <summary>
/// The name of the addon containing this context menu, if any.
/// </summary>
public string? ParentAddonName { get; }
/// <summary>
/// The ID of the item this context menu is for.
/// </summary>
public uint ItemId { get; }
/// <summary>
/// The amount of the item this context menu is for.
/// </summary>
public uint ItemAmount { get; }
/// <summary>
/// If the item this context menu is for is high-quality.
/// </summary>
public bool ItemHq { get; }
internal BaseInventoryContextMenuArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint itemId, uint itemAmount, bool itemHq) {
this.Addon = addon;
this.Agent = agent;
this.ParentAddonName = parentAddonName;
this.ItemId = itemId;
this.ItemAmount = itemAmount;
this.ItemHq = itemHq;
}
}
}

View File

@ -0,0 +1,16 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// A custom context menu item for inventory items.
/// </summary>
public class InventoryContextMenuItem : CustomContextMenuItem<ContextMenu.InventoryContextMenuItemSelectedDelegate> {
/// <summary>
/// Create a new context menu item for inventory items.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
public InventoryContextMenuItem(SeString name, ContextMenu.InventoryContextMenuItemSelectedDelegate action) : base(name, action) {
}
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// The arguments for when an inventory context menu item is selected
/// </summary>
public class InventoryContextMenuItemSelectedArgs : BaseInventoryContextMenuArgs {
internal InventoryContextMenuItemSelectedArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint itemId, uint itemAmount, bool itemHq) : base(addon, agent, parentAddonName, itemId, itemAmount, itemHq) {
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// The arguments for when an inventory context menu is opened
/// </summary>
public class InventoryContextMenuOpenArgs : BaseInventoryContextMenuArgs {
/// <summary>
/// Context menu items in this menu.
/// </summary>
public List<BaseContextMenuItem> Items { get; } = new();
internal InventoryContextMenuOpenArgs(IntPtr addon, IntPtr agent, string? parentAddonName, uint itemId, uint itemAmount, bool itemHq) : base(addon, agent, parentAddonName, itemId, itemAmount, itemHq) {
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu.Inventory {
/// <summary>
/// A custom inventory context menu item that will open a submenu
/// </summary>
public class InventoryContextSubMenuItem : CustomContextMenuItem<ContextMenu.InventoryContextMenuOpenEventDelegate> {
/// <summary>
/// Create a new context menu item for inventory items that will open a submenu.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
[Obsolete("Inventory context submenus do not work yet", true)]
public InventoryContextSubMenuItem(SeString name, ContextMenu.InventoryContextMenuOpenEventDelegate action) : base(name, action) {
}
}
}

View File

@ -0,0 +1,25 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A native context menu item
/// </summary>
public sealed class NativeContextMenuItem : BaseContextMenuItem {
/// <summary>
/// The action code to be used in the context menu agent for this item.
/// </summary>
public byte InternalAction { get; }
/// <summary>
/// The name of the context item.
/// </summary>
public SeString Name { get; set; }
internal NativeContextMenuItem(byte action, SeString name, bool enabled, bool isSubMenu) {
this.Name = name;
this.InternalAction = action;
this.Enabled = enabled;
this.IsSubMenu = isSubMenu;
}
}
}

View File

@ -0,0 +1,16 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A custom normal context menu item
/// </summary>
public class NormalContextMenuItem : CustomContextMenuItem<ContextMenu.ContextMenuItemSelectedDelegate> {
/// <summary>
/// Create a new custom context menu item.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
public NormalContextMenuItem(SeString name, ContextMenu.ContextMenuItemSelectedDelegate action) : base(name, action) {
}
}
}

View File

@ -0,0 +1,17 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.ContextMenu {
/// <summary>
/// A custom context menu item that will open a submenu
/// </summary>
public class NormalContextSubMenuItem : CustomContextMenuItem<ContextMenu.ContextMenuOpenEventDelegate> {
/// <summary>
/// Create a new custom context menu item that will open a submenu.
/// </summary>
/// <param name="name">the English name of the item, copied to other languages</param>
/// <param name="action">the action to perform on click</param>
public NormalContextSubMenuItem(SeString name, ContextMenu.ContextMenuOpenEventDelegate action) : base(name, action) {
this.IsSubMenu = true;
}
}
}

View File

@ -3,79 +3,81 @@ using System.Runtime.InteropServices;
using Dalamud.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions;
/// <summary>
/// Duty Finder functions
/// </summary>
public class DutyFinder {
private static class Signatures {
internal const string OpenRegularDuty = "48 89 6C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B F9 41 0F B6 E8";
internal const string OpenRoulette = "E9 ?? ?? ?? ?? 8B 93 ?? ?? ?? ?? 48 83 C4 20";
}
private delegate IntPtr OpenDutyDelegate(IntPtr agent, uint contentFinderCondition, byte a3);
private delegate IntPtr OpenRouletteDelegate(IntPtr agent, byte roulette, byte a3);
private readonly OpenDutyDelegate? _openDuty;
private readonly OpenRouletteDelegate? _openRoulette;
internal DutyFinder(ISigScanner scanner) {
if (scanner.TryScanText(Signatures.OpenRegularDuty, out var openDutyPtr, "Duty Finder (open duty)")) {
this._openDuty = Marshal.GetDelegateForFunctionPointer<OpenDutyDelegate>(openDutyPtr);
namespace XivCommon.Functions {
/// <summary>
/// Duty Finder functions
/// </summary>
public class DutyFinder {
private static class Signatures {
internal const string OpenRegularDuty = "48 89 6C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B F9 41 0F B6 E8";
internal const string OpenRoulette = "E9 ?? ?? ?? ?? 8B 93 ?? ?? ?? ?? 48 83 C4 20";
}
if (scanner.TryScanText(Signatures.OpenRoulette, out var openRoulettePtr, "Duty Finder (open roulette)")) {
this._openRoulette = Marshal.GetDelegateForFunctionPointer<OpenRouletteDelegate>(openRoulettePtr);
}
}
private delegate IntPtr OpenDutyDelegate(IntPtr agent, uint contentFinderCondition, byte a3);
/// <summary>
/// Opens the Duty Finder to the given duty.
/// </summary>
/// <param name="condition">duty to show</param>
/// <exception cref="InvalidOperationException">if the open duty function could not be found in memory</exception>
public void OpenDuty(ContentFinderCondition condition) {
this.OpenDuty(condition.RowId);
}
private delegate IntPtr OpenRouletteDelegate(IntPtr agent, byte roulette, byte a3);
/// <summary>
/// Opens the Duty Finder to the given duty ID.
/// </summary>
/// <param name="contentFinderCondition">ID of duty to show</param>
/// <exception cref="InvalidOperationException">if the open duty function could not be found in memory</exception>
public unsafe void OpenDuty(uint contentFinderCondition) {
if (this._openDuty == null) {
throw new InvalidOperationException("Could not find signature for open duty function");
private GameFunctions Functions { get; }
private readonly OpenDutyDelegate? _openDuty;
private readonly OpenRouletteDelegate? _openRoulette;
internal DutyFinder(GameFunctions functions, SigScanner scanner) {
this.Functions = functions;
if (scanner.TryScanText(Signatures.OpenRegularDuty, out var openDutyPtr, "Duty Finder (open duty)")) {
this._openDuty = Marshal.GetDelegateForFunctionPointer<OpenDutyDelegate>(openDutyPtr);
}
if (scanner.TryScanText(Signatures.OpenRoulette, out var openRoulettePtr, "Duty Finder (open roulette)")) {
this._openRoulette = Marshal.GetDelegateForFunctionPointer<OpenRouletteDelegate>(openRoulettePtr);
}
}
var agent = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ContentsFinder);
this._openDuty(agent, contentFinderCondition, 0);
}
/// <summary>
/// Opens the Duty Finder to the given roulette.
/// </summary>
/// <param name="roulette">roulette to show</param>
public void OpenRoulette(ContentRoulette roulette) {
this.OpenRoulette((byte) roulette.RowId);
}
/// <summary>
/// Opens the Duty Finder to the given roulette ID.
/// </summary>
/// <param name="roulette">ID of roulette to show</param>
public unsafe void OpenRoulette(byte roulette) {
if (this._openRoulette == null) {
throw new InvalidOperationException("Could not find signature for open roulette function");
/// <summary>
/// Opens the Duty Finder to the given duty.
/// </summary>
/// <param name="condition">duty to show</param>
/// <exception cref="InvalidOperationException">if the open duty function could not be found in memory</exception>
public void OpenDuty(ContentFinderCondition condition) {
this.OpenDuty(condition.RowId);
}
var agent = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ContentsFinder);
/// <summary>
/// Opens the Duty Finder to the given duty ID.
/// </summary>
/// <param name="contentFinderCondition">ID of duty to show</param>
/// <exception cref="InvalidOperationException">if the open duty function could not be found in memory</exception>
public unsafe void OpenDuty(uint contentFinderCondition) {
if (this._openDuty == null) {
throw new InvalidOperationException("Could not find signature for open duty function");
}
this._openRoulette(agent, roulette, 0);
var agent = (IntPtr) this.Functions.GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ContentsFinder);
this._openDuty(agent, contentFinderCondition, 0);
}
/// <summary>
/// Opens the Duty Finder to the given roulette.
/// </summary>
/// <param name="roulette">roulette to show</param>
public void OpenRoulette(ContentRoulette roulette) {
this.OpenRoulette((byte) roulette.RowId);
}
/// <summary>
/// Opens the Duty Finder to the given roulette ID.
/// </summary>
/// <param name="roulette">ID of roulette to show</param>
public unsafe void OpenRoulette(byte roulette) {
if (this._openRoulette == null) {
throw new InvalidOperationException("Could not find signature for open roulette function");
}
var agent = (IntPtr) this.Functions.GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ContentsFinder);
this._openRoulette(agent, roulette, 0);
}
}
}

View File

@ -2,66 +2,68 @@
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects.Types;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions;
/// <summary>
/// Class containing examine functions
/// </summary>
public class Examine {
private static class Signatures {
internal const string RequestCharacterInfo = "40 53 48 83 EC 40 48 8B D9 48 8B 49 10 48 8B 01 FF 90 ?? ?? ?? ?? BA";
}
private delegate long RequestCharInfoDelegate(IntPtr ptr);
private RequestCharInfoDelegate? RequestCharacterInfo { get; }
internal Examine(ISigScanner scanner) {
// got this by checking what accesses rciData below
if (scanner.TryScanText(Signatures.RequestCharacterInfo, out var rciPtr, "Examine")) {
this.RequestCharacterInfo = Marshal.GetDelegateForFunctionPointer<RequestCharInfoDelegate>(rciPtr);
}
}
namespace XivCommon.Functions {
/// <summary>
/// Opens the Examine window for the specified object.
/// Class containing examine functions
/// </summary>
/// <param name="object">Object to open window for</param>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public void OpenExamineWindow(GameObject @object) {
this.OpenExamineWindow(@object.ObjectId);
}
/// <summary>
/// Opens the Examine window for the object with the specified ID.
/// </summary>
/// <param name="objectId">Object ID to open window for</param>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public unsafe void OpenExamineWindow(uint objectId) {
if (this.RequestCharacterInfo == null) {
throw new InvalidOperationException("Could not find signature for Examine function");
public class Examine {
private static class Signatures {
internal const string RequestCharacterInfo = "48 89 5C 24 ?? 57 48 83 EC 40 BA ?? ?? ?? ?? 48 8B D9 E8 ?? ?? ?? ?? 48 8B F8 48 85 C0 74 16";
}
// NOTES LAST UPDATED: 6.0
private GameFunctions Functions { get; }
// offsets and stuff come from the beginning of case 0x2c (around line 621 in IDA)
// if 29f8 ever changes, I'd just scan for it in old binary and find what it is in the new binary at the same spot
// 40 55 53 57 41 54 41 55 41 56 48 8D 6C 24
// offset below is 4C 8B B0 ?? ?? ?? ?? 4D 85 F6 0F 84 ?? ?? ?? ?? 0F B6 83
var agentModule = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule();
var rciData = Marshal.ReadIntPtr(agentModule + 0x1B0);
private delegate long RequestCharInfoDelegate(IntPtr ptr);
// offsets at sig E8 ?? ?? ?? ?? 33 C0 EB 4C
// this is called at the end of the 2c case
var raw = (uint*) rciData;
*(raw + 10) = objectId;
*(raw + 11) = objectId;
*(raw + 12) = objectId;
*(raw + 13) = 0xE0000000;
*(raw + 301) = 0;
private RequestCharInfoDelegate? RequestCharacterInfo { get; }
this.RequestCharacterInfo(rciData);
internal Examine(GameFunctions functions, SigScanner scanner) {
this.Functions = functions;
// got this by checking what accesses rciData below
if (scanner.TryScanText(Signatures.RequestCharacterInfo, out var rciPtr, "Examine")) {
this.RequestCharacterInfo = Marshal.GetDelegateForFunctionPointer<RequestCharInfoDelegate>(rciPtr);
}
}
/// <summary>
/// Opens the Examine window for the specified object.
/// </summary>
/// <param name="object">Object to open window for</param>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public void OpenExamineWindow(GameObject @object) {
this.OpenExamineWindow(@object.ObjectId);
}
/// <summary>
/// Opens the Examine window for the object with the specified ID.
/// </summary>
/// <param name="objectId">Object ID to open window for</param>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public unsafe void OpenExamineWindow(uint objectId) {
if (this.RequestCharacterInfo == null) {
throw new InvalidOperationException("Could not find signature for Examine function");
}
// NOTES LAST UPDATED: 5.55
// offsets and stuff come from the beginning of case 0x2c (around line 621 in IDA)
// if 29f8 ever changes, I'd just scan for it in old binary and find what it is in the new binary at the same spot
// 40 55 53 57 41 54 41 55 41 56 48 8D 6C 24 ??
var agentModule = (IntPtr) this.Functions.GetFramework()->GetUiModule()->GetAgentModule();
var rciData = Marshal.ReadIntPtr(agentModule + 0x1A0);
// offsets at sig E8 ?? ?? ?? ?? 33 C0 EB 4C
// this is called at the end of the 2c case
var raw = (uint*) rciData;
*(raw + 10) = objectId;
*(raw + 11) = objectId;
*(raw + 12) = objectId;
*(raw + 13) = 0xE0000000;
*(raw + 311) = 0;
this.RequestCharacterInfo(rciData);
}
}
}

View File

@ -1,62 +1,65 @@
using System;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace XivCommon.Functions.FriendList;
/// <summary>
/// The class containing friend list functionality
/// </summary>
public class FriendList {
// Updated: 5.58-HF1
private const int InfoOffset = 0x28;
private const int LengthOffset = 0x10;
private const int ListOffset = 0x98;
namespace XivCommon.Functions.FriendList {
/// <summary>
/// <para>
/// A live list of the currently-logged-in player's friends.
/// </para>
/// <para>
/// The list is empty if not logged in.
/// </para>
/// The class containing friend list functionality
/// </summary>
public unsafe IList<FriendListEntry> List {
get {
var friendListAgent = (IntPtr) Framework.Instance()
->GetUiModule()
public class FriendList {
// Updated: 5.58-HF1
private const int InfoOffset = 0x28;
private const int LengthOffset = 0x10;
private const int ListOffset = 0x98;
private GameFunctions Functions { get; }
/// <summary>
/// <para>
/// A live list of the currently-logged-in player's friends.
/// </para>
/// <para>
/// The list is empty if not logged in.
/// </para>
/// </summary>
public unsafe IList<FriendListEntry> List {
get {
var friendListAgent = (IntPtr) this.Functions
.GetFramework()
->GetUiModule()
->GetAgentModule()
->GetAgentByInternalId(AgentId.SocialFriendList);
if (friendListAgent == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
->GetAgentByInternalId(AgentId.FriendList);
if (friendListAgent == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
var info = *(IntPtr*) (friendListAgent + InfoOffset);
if (info == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
var info = *(IntPtr*) (friendListAgent + InfoOffset);
if (info == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
var length = *(ushort*) (info + LengthOffset);
if (length == 0) {
return Array.Empty<FriendListEntry>();
}
var length = *(ushort*) (info + LengthOffset);
if (length == 0) {
return Array.Empty<FriendListEntry>();
}
var list = *(IntPtr*) (info + ListOffset);
if (list == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
var list = *(IntPtr*) (info + ListOffset);
if (list == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
var entries = new List<FriendListEntry>(length);
for (var i = 0; i < length; i++) {
var entry = *(FriendListEntry*) (list + i * FriendListEntry.Size);
entries.Add(entry);
}
var entries = new List<FriendListEntry>(length);
for (var i = 0; i < length; i++) {
var entry = *(FriendListEntry*) (list + i * FriendListEntry.Size);
entries.Add(entry);
}
return entries;
return entries;
}
}
internal FriendList(GameFunctions functions) {
this.Functions = functions;
}
}
internal FriendList() {
}
}

View File

@ -3,69 +3,69 @@ using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
namespace XivCommon.Functions.FriendList;
/// <summary>
/// An entry in a player's friend list.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = Size)]
public unsafe struct FriendListEntry {
internal const int Size = 104;
namespace XivCommon.Functions.FriendList {
/// <summary>
/// The content ID of the friend.
/// An entry in a player's friend list.
/// </summary>
[FieldOffset(0x00)]
public readonly ulong ContentId;
[StructLayout(LayoutKind.Explicit, Size = Size)]
public unsafe struct FriendListEntry {
internal const int Size = 96;
/// <summary>
/// The current world of the friend.
/// </summary>
[FieldOffset(0x1E)]
public readonly ushort CurrentWorld;
/// <summary>
/// The content ID of the friend.
/// </summary>
[FieldOffset(0x00)]
public readonly ulong ContentId;
/// <summary>
/// The home world of the friend.
/// </summary>
[FieldOffset(0x20)]
public readonly ushort HomeWorld;
/// <summary>
/// The home world of the friend.
/// </summary>
[FieldOffset(0x16)]
public readonly ushort HomeWorld;
/// <summary>
/// The job the friend is currently on.
/// </summary>
[FieldOffset(0x29)]
public readonly byte Job;
/// <summary>
/// The current world of the friend.
/// </summary>
[FieldOffset(0x18)]
public readonly ushort CurrentWorld;
/// <summary>
/// The friend's raw SeString name. See <see cref="Name"/>.
/// </summary>
[FieldOffset(0x2A)]
public fixed byte RawName[32];
/// <summary>
/// The job the friend is currently on.
/// </summary>
[FieldOffset(0x21)]
public readonly byte Job;
/// <summary>
/// The friend's raw SeString free company tag. See <see cref="FreeCompany"/>.
/// </summary>
[FieldOffset(0x4A)]
public fixed byte RawFreeCompany[5];
/// <summary>
/// The friend's raw SeString name. See <see cref="Name"/>.
/// </summary>
[FieldOffset(0x22)]
public fixed byte RawName[32];
/// <summary>
/// The friend's name.
/// </summary>
public SeString Name {
get {
fixed (byte* ptr = this.RawName) {
return MemoryHelper.ReadSeStringNullTerminated((IntPtr) ptr);
/// <summary>
/// The friend's raw SeString free company tag. See <see cref="FreeCompany"/>.
/// </summary>
[FieldOffset(0x42)]
public fixed byte RawFreeCompany[5];
/// <summary>
/// The friend's name.
/// </summary>
public SeString Name {
get {
fixed (byte* ptr = this.RawName) {
return MemoryHelper.ReadSeStringNullTerminated((IntPtr) ptr);
}
}
}
}
/// <summary>
/// The friend's free company tag.
/// </summary>
public SeString FreeCompany {
get {
fixed (byte* ptr = this.RawFreeCompany) {
return MemoryHelper.ReadSeStringNullTerminated((IntPtr) ptr);
/// <summary>
/// The friend's free company tag.
/// </summary>
public SeString FreeCompany {
get {
fixed (byte* ptr = this.RawFreeCompany) {
return MemoryHelper.ReadSeStringNullTerminated((IntPtr) ptr);
}
}
}
}

View File

@ -1,56 +1,56 @@
using System;
using Dalamud.Game;
namespace XivCommon.Functions.Housing;
/// <summary>
/// The class containing housing functionality
/// </summary>
public class Housing {
private static class Signatures {
internal const string HousingPointer = "48 8B 05 ?? ?? ?? ?? 48 83 78 ?? ?? 74 16 48 8D 8F ?? ?? ?? ?? 66 89 5C 24 ?? 48 8D 54 24 ?? E8 ?? ?? ?? ?? 48 8B 7C 24";
}
private IntPtr HousingPointer { get; }
namespace XivCommon.Functions.Housing {
/// <summary>
/// Gets the raw struct containing information about the player's current location in a housing ward.
///
/// <returns>struct if player is in a housing ward, null otherwise</returns>
/// The class containing housing functionality
/// </summary>
// Updated: 6.0
// 48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC 20 49 8B 00 (ward?)
public unsafe RawHousingLocation? RawLocation {
get {
if (this.HousingPointer == IntPtr.Zero) {
return null;
}
var loc = Util.FollowPointerChain(this.HousingPointer, new[] { 0, 0 });
if (loc == IntPtr.Zero) {
return null;
}
var locPtr = (RawHousingLocation*) (loc + 0x96a0);
return *locPtr;
public class Housing {
private static class Signatures {
internal const string HousingPointer = "48 8B 05 ?? ?? ?? ?? 48 83 78 ?? ?? 74 16 48 8D 8F ?? ?? ?? ?? 66 89 5C 24 ?? 48 8D 54 24 ?? E8 ?? ?? ?? ?? 48 8B 7C 24";
}
}
/// <summary>
/// Gets process information about the player's current location in a housing ward.
///
/// <returns>information class if player is in a housing ward, null otherwise</returns>
/// </summary>
public HousingLocation? Location {
get {
var loc = this.RawLocation;
return loc == null ? null : new HousingLocation(loc.Value);
private IntPtr HousingPointer { get; }
/// <summary>
/// Gets the raw struct containing information about the player's current location in a housing ward.
///
/// <returns>struct if player is in a housing ward, null otherwise</returns>
/// </summary>
// Updated: 5.55
// 48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC 20 49 8B 00 (ward?)
public unsafe RawHousingLocation? RawLocation {
get {
if (this.HousingPointer == IntPtr.Zero) {
return null;
}
var loc = Util.FollowPointerChain(this.HousingPointer, new[] { 0, 0 });
if (loc == IntPtr.Zero) {
return null;
}
var locPtr = (RawHousingLocation*) (loc + 0x96a0);
return *locPtr;
}
}
}
internal Housing(ISigScanner scanner) {
if (scanner.TryGetStaticAddressFromSig(Signatures.HousingPointer, out var ptr)) {
this.HousingPointer = ptr;
/// <summary>
/// Gets process information about the player's current location in a housing ward.
///
/// <returns>information class if player is in a housing ward, null otherwise</returns>
/// </summary>
public HousingLocation? Location {
get {
var loc = this.RawLocation;
return loc == null ? null : new HousingLocation(loc.Value);
}
}
internal Housing(SigScanner scanner) {
if (scanner.TryGetStaticAddressFromSig(Signatures.HousingPointer, out var ptr)) {
this.HousingPointer = ptr;
}
}
}
}

View File

@ -1,59 +1,59 @@
namespace XivCommon.Functions.Housing;
namespace XivCommon.Functions.Housing {
/// <summary>
/// Information about a player's current location in a housing ward.
/// </summary>
public class HousingLocation {
/// <summary>
/// The housing ward that the player is in.
/// </summary>
public ushort Ward;
/// <summary>
/// <para>
/// The yard that the player is in.
/// </para>
/// <para>
/// This is the same as plot number but indicates that the player is in
/// the exterior area (the yard) of that plot.
/// </para>
/// </summary>
public ushort? Yard;
/// <summary>
/// The plot that the player is in.
/// </summary>
public ushort? Plot;
/// <summary>
/// The apartment wing (1 or 2 for normal or subdivision) that the
/// player is in.
/// </summary>
public ushort? ApartmentWing;
/// <summary>
/// The apartment that the player is in.
/// </summary>
public ushort? Apartment;
/// <summary>
/// Information about a player's current location in a housing ward.
/// </summary>
public class HousingLocation {
/// <summary>
/// The housing ward that the player is in.
/// </summary>
public ushort Ward;
/// <summary>
/// <para>
/// The yard that the player is in.
/// </para>
/// <para>
/// This is the same as plot number but indicates that the player is in
/// the exterior area (the yard) of that plot.
/// </para>
/// </summary>
public ushort? Yard;
/// <summary>
/// The plot that the player is in.
/// </summary>
public ushort? Plot;
/// <summary>
/// The apartment wing (1 or 2 for normal or subdivision) that the
/// player is in.
/// </summary>
public ushort? ApartmentWing;
/// <summary>
/// The apartment that the player is in.
/// </summary>
public ushort? Apartment;
internal HousingLocation(RawHousingLocation loc) {
var ward = loc.CurrentWard;
internal HousingLocation(RawHousingLocation loc) {
var ward = loc.CurrentWard;
if ((loc.CurrentPlot & 0x80) > 0) {
// the struct is in apartment mode
this.ApartmentWing = (ushort?) ((loc.CurrentPlot & ~0x80) + 1);
this.Apartment = (ushort?) (ward >> 6);
this.Ward = (ushort) ((ward & 0x3F) + 1);
if (this.Apartment == 0) {
this.Apartment = null;
if ((loc.CurrentPlot & 0x80) > 0) {
// the struct is in apartment mode
this.ApartmentWing = (ushort?) ((loc.CurrentPlot & ~0x80) + 1);
this.Apartment = (ushort?) (ward >> 6);
this.Ward = (ushort) ((ward & 0x3F) + 1);
if (this.Apartment == 0) {
this.Apartment = null;
}
} else if (loc.InsideIndicator == 0) {
// inside a plot
this.Plot = (ushort?) (loc.CurrentPlot + 1);
} else if (loc.CurrentYard != 0xFF) {
// not inside a plot
// yard is 0xFF when not in one
this.Yard = (ushort?) (loc.CurrentYard + 1);
}
} else if (loc.InsideIndicator == 0) {
// inside a plot
this.Plot = (ushort?) (loc.CurrentPlot + 1);
} else if (loc.CurrentYard != 0xFF) {
// not inside a plot
// yard is 0xFF when not in one
this.Yard = (ushort?) (loc.CurrentYard + 1);
}
if (this.Ward == 0) {
this.Ward = (ushort) (ward + 1);
if (this.Ward == 0) {
this.Ward = (ushort) (ward + 1);
}
}
}
}
}

View File

@ -1,41 +1,41 @@
using System.Runtime.InteropServices;
namespace XivCommon.Functions.Housing;
/// <summary>
/// Information about the player's current location in a housing ward as
/// kept by the game's internal structures.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct RawHousingLocation {
namespace XivCommon.Functions.Housing {
/// <summary>
/// The zero-indexed plot number that the player is in.
///
/// <para>
/// Contains apartment data when inside an apartment building.
/// </para>
/// Information about the player's current location in a housing ward as
/// kept by the game's internal structures.
/// </summary>
public readonly ushort CurrentPlot; // a0 -> a2
/// <summary>
/// The zero-indexed ward number that the player is in.
///
/// <para>
/// Contains apartment data when inside an apartment building.
/// </para>
/// </summary>
public readonly ushort CurrentWard; // a2 -> a4
private readonly uint unknownBytes1; // a4 -> a8
/// <summary>
/// The zero-indexed yard number that the player is in.
///
/// <para>
/// Is <c>0xFF</c> when not in a yard.
/// </para>
/// </summary>
public readonly byte CurrentYard; // a8 -> a9
private readonly byte unknownBytes2; // a9 -> aa
/// <summary>
/// A byte that is zero when the player is inside a plot.
/// </summary>
public readonly byte InsideIndicator; // aa -> ab
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct RawHousingLocation {
/// <summary>
/// The zero-indexed plot number that the player is in.
///
/// <para>
/// Contains apartment data when inside an apartment building.
/// </para>
/// </summary>
public readonly ushort CurrentPlot; // a0 -> a2
/// <summary>
/// The zero-indexed ward number that the player is in.
///
/// <para>
/// Contains apartment data when inside an apartment building.
/// </para>
/// </summary>
public readonly ushort CurrentWard; // a2 -> a4
private readonly uint unknownBytes1; // a4 -> a8
/// <summary>
/// The zero-indexed yard number that the player is in.
///
/// <para>
/// Is <c>0xFF</c> when not in a yard.
/// </para>
/// </summary>
public readonly byte CurrentYard; // a8 -> a9
private readonly byte unknownBytes2; // a9 -> aa
/// <summary>
/// A byte that is zero when the player is inside a plot.
/// </summary>
public readonly byte InsideIndicator; // aa -> ab
}
}

View File

@ -3,81 +3,86 @@ using System.Runtime.InteropServices;
using Dalamud.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions;
/// <summary>
/// Journal functions
/// </summary>
public class Journal {
private static class Signatures {
internal const string OpenQuest = "E8 ?? ?? ?? ?? 48 8B 74 24 ?? 48 8B 7C 24 ?? 48 83 C4 30 5B C3 48 8B CB";
internal const string IsQuestCompleted = "E8 ?? ?? ?? ?? 41 88 84 2C";
}
private delegate IntPtr OpenQuestDelegate(IntPtr agent, int questId, int a3, ushort a4, byte a5);
private delegate byte IsQuestCompletedDelegate(ushort questId);
private readonly OpenQuestDelegate? _openQuest;
private readonly IsQuestCompletedDelegate? _isQuestCompleted;
internal Journal(ISigScanner scanner) {
if (scanner.TryScanText(Signatures.OpenQuest, out var openQuestPtr, "Journal (open quest)")) {
this._openQuest = Marshal.GetDelegateForFunctionPointer<OpenQuestDelegate>(openQuestPtr);
namespace XivCommon.Functions {
/// <summary>
/// Journal functions
/// </summary>
public class Journal {
private static class Signatures {
internal const string OpenQuest = "E8 ?? ?? ?? ?? 48 8B 74 24 ?? 48 8B 7C 24 ?? 48 83 C4 30 5B C3 48 8B CB";
internal const string IsQuestCompleted = "E8 ?? ?? ?? ?? 41 88 84 2C ?? ?? ?? ??";
}
if (scanner.TryScanText(Signatures.IsQuestCompleted, out var questCompletedPtr, "Journal (quest completed)")) {
this._isQuestCompleted = Marshal.GetDelegateForFunctionPointer<IsQuestCompletedDelegate>(questCompletedPtr);
}
}
private const int JournalAgentId = 31;
/// <summary>
/// Opens the quest journal to the given quest.
/// </summary>
/// <param name="quest">quest to show</param>
/// <exception cref="InvalidOperationException">if the open quest function could not be found in memory</exception>
public void OpenQuest(Quest quest) {
this.OpenQuest(quest.RowId);
}
private delegate IntPtr OpenQuestDelegate(IntPtr agent, int questId, int a3, ushort a4, byte a5);
/// <summary>
/// Opens the quest journal to the given quest ID.
/// </summary>
/// <param name="questId">ID of quest to show</param>
/// <exception cref="InvalidOperationException">if the open quest function could not be found in memory</exception>
public unsafe void OpenQuest(uint questId) {
if (this._openQuest == null) {
throw new InvalidOperationException("Could not find signature for open quest function");
private delegate byte IsQuestCompletedDelegate(ushort questId);
private GameFunctions Functions { get; }
private readonly OpenQuestDelegate? _openQuest;
private readonly IsQuestCompletedDelegate? _isQuestCompleted;
internal Journal(GameFunctions functions, SigScanner scanner) {
this.Functions = functions;
if (scanner.TryScanText(Signatures.OpenQuest, out var openQuestPtr, "Journal (open quest)")) {
this._openQuest = Marshal.GetDelegateForFunctionPointer<OpenQuestDelegate>(openQuestPtr);
}
if (scanner.TryScanText(Signatures.IsQuestCompleted, out var questCompletedPtr, "Journal (quest completed)")) {
this._isQuestCompleted = Marshal.GetDelegateForFunctionPointer<IsQuestCompletedDelegate>(questCompletedPtr);
}
}
var agent = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Journal);
this._openQuest(agent, (int) (questId & 0xFFFF), 1, 0, 1);
}
/// <summary>
/// Checks if the given quest is completed.
/// </summary>
/// <param name="quest">quest to check</param>
/// <returns>true if the quest is completed</returns>
/// <exception cref="InvalidOperationException">if the function for checking quest completion could not be found in memory</exception>
public bool IsQuestCompleted(Quest quest) {
return this.IsQuestCompleted(quest.RowId);
}
/// <summary>
/// Checks if the given quest ID is completed.
/// </summary>
/// <param name="questId">ID of quest to check</param>
/// <returns>true if the quest is completed</returns>
/// <exception cref="InvalidOperationException">if the function for checking quest completion could not be found in memory</exception>
public bool IsQuestCompleted(uint questId) {
if (this._isQuestCompleted == null) {
throw new InvalidOperationException("Could not find signature for quest completed function");
/// <summary>
/// Opens the quest journal to the given quest.
/// </summary>
/// <param name="quest">quest to show</param>
/// <exception cref="InvalidOperationException">if the open quest function could not be found in memory</exception>
public void OpenQuest(Quest quest) {
this.OpenQuest(quest.RowId);
}
return this._isQuestCompleted((ushort) (questId & 0xFFFF)) != 0;
/// <summary>
/// Opens the quest journal to the given quest ID.
/// </summary>
/// <param name="questId">ID of quest to show</param>
/// <exception cref="InvalidOperationException">if the open quest function could not be found in memory</exception>
public unsafe void OpenQuest(uint questId) {
if (this._openQuest == null) {
throw new InvalidOperationException("Could not find signature for open quest function");
}
var agent = (IntPtr) this.Functions.GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Journal);
this._openQuest(agent, (int) (questId & 0xFFFF), 1, 0, 1);
}
/// <summary>
/// Checks if the given quest is completed.
/// </summary>
/// <param name="quest">quest to check</param>
/// <returns>true if the quest is completed</returns>
/// <exception cref="InvalidOperationException">if the function for checking quest completion could not be found in memory</exception>
public bool IsQuestCompleted(Quest quest) {
return this.IsQuestCompleted(quest.RowId);
}
/// <summary>
/// Checks if the given quest ID is completed.
/// </summary>
/// <param name="questId">ID of quest to check</param>
/// <returns>true if the quest is completed</returns>
/// <exception cref="InvalidOperationException">if the function for checking quest completion could not be found in memory</exception>
public bool IsQuestCompleted(uint questId) {
if (this._isQuestCompleted == null) {
throw new InvalidOperationException("Could not find signature for quest completed function");
}
return this._isQuestCompleted((ushort) (questId & 0xFFFF)) != 0;
}
}
}

View File

@ -1,78 +1,78 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.NamePlates;
/// <summary>
/// Arguments for the name plate update event
/// </summary>
public class NamePlateUpdateEventArgs {
namespace XivCommon.Functions.NamePlates {
/// <summary>
/// The object ID associated with this name plate.
/// Arguments for the name plate update event
/// </summary>
public uint ObjectId { get; }
public class NamePlateUpdateEventArgs {
/// <summary>
/// The object ID associated with this name plate.
/// </summary>
public uint ObjectId { get; }
/// <summary>
/// The name string.
/// </summary>
public SeString Name { get; set; } = null!;
/// <summary>
/// The name string.
/// </summary>
public SeString Name { get; set; } = null!;
/// <summary>
/// The FC tag string for name plates that use it. Set to the empty string to disable.
/// </summary>
public SeString FreeCompany { get; set; } = null!;
/// <summary>
/// The FC tag string for name plates that use it. Set to the empty string to disable.
/// </summary>
public SeString FreeCompany { get; set; } = null!;
/// <summary>
/// The title string for name plates that use it. Set to the empty string to disable.
/// </summary>
public SeString Title { get; set; } = null!;
/// <summary>
/// The title string for name plates that use it. Set to the empty string to disable.
/// </summary>
public SeString Title { get; set; } = null!;
/// <summary>
/// The level string for name plates that use it. Set to the empty string to disable.
/// </summary>
///
public SeString Level { get; set; } = null!;
/// <summary>
/// The level string for name plates that use it. Set to the empty string to disable.
/// </summary>
///
public SeString Level { get; set; } = null!;
/// <summary>
/// <para>
/// The letter that appears after enemy names, such as A, B, etc.
/// </para>
/// <para>
/// <b>Setting this property will always cause a memory leak.</b>
/// </para>
/// </summary>
public SeString EnemyLetter {
get;
[Obsolete("Setting this property will always cause a memory leak.")]
set;
} = null!;
/// <summary>
/// <para>
/// The letter that appears after enemy names, such as A, B, etc.
/// </para>
/// <para>
/// <b>Setting this property will always cause a memory leak.</b>
/// </para>
/// </summary>
public SeString EnemyLetter {
get;
[Obsolete("Setting this property will always cause a memory leak.")]
set;
} = null!;
/// <summary>
/// The icon to be shown on this name plate. Use <see cref="uint.MaxValue"/> for no icon.
/// </summary>
public uint Icon { get; set; }
/// <summary>
/// The icon to be shown on this name plate. Use <see cref="uint.MaxValue"/> for no icon.
/// </summary>
public uint Icon { get; set; }
/// <summary>
/// The colour of this name plate.
/// </summary>
public RgbaColour Colour { get; set; } = new();
/// <summary>
/// The colour of this name plate.
/// </summary>
public RgbaColour Colour { get; set; } = new();
/// <summary>
/// <para>
/// The type of this name plate.
/// </para>
/// <para>
/// Changing this without setting the appropriate fields can cause the game to crash.
/// </para>
/// </summary>
public PlateType Type { get; set; }
/// <summary>
/// <para>
/// The type of this name plate.
/// </para>
/// <para>
/// Changing this without setting the appropriate fields can cause the game to crash.
/// </para>
/// </summary>
public PlateType Type { get; set; }
/// <summary>
/// A bitmask of flags for the name plate.
/// </summary>
public int Flags { get; set; }
/// <summary>
/// A bitmask of flags for the name plate.
/// </summary>
public int Flags { get; set; }
internal NamePlateUpdateEventArgs(uint objectId) {
this.ObjectId = objectId;
internal NamePlateUpdateEventArgs(uint objectId) {
this.ObjectId = objectId;
}
}
}
}

View File

@ -3,225 +3,223 @@ using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics;
using FFXIVClientStructs.FFXIV.Client.UI;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions.NamePlates;
/// <summary>
/// The class containing name plate functionality
/// </summary>
public class NamePlates : IDisposable {
private static class Signatures {
internal const string NamePlateUpdate = "48 8B C4 41 56 48 81 EC ?? ?? ?? ?? 48 89 58 F0";
}
private unsafe delegate IntPtr NamePlateUpdateDelegate(AddonNamePlate* addon, NumberArrayData** numberData, StringArrayData** stringData);
namespace XivCommon.Functions.NamePlates {
/// <summary>
/// The delegate for name plate update events.
/// The class containing name plate functionality
/// </summary>
public delegate void NamePlateUpdateEvent(NamePlateUpdateEventArgs args);
/// <summary>
/// <para>
/// The event that is fired when a name plate is due to update.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.NamePlates"/> hook to be enabled.
/// </para>
/// </summary>
public event NamePlateUpdateEvent? OnUpdate;
private GameFunctions Functions { get; }
private readonly Hook<NamePlateUpdateDelegate>? _namePlateUpdateHook;
/// <summary>
/// <para>
/// If all name plates should be forced to redraw.
/// </para>
/// <para>
/// This is useful for forcing your changes to apply to existing name plates when the plugin is hot-loaded.
/// </para>
/// </summary>
public bool ForceRedraw { get; set; }
internal NamePlates(GameFunctions functions, ISigScanner scanner, IGameInteropProvider interop, bool hookEnabled) {
this.Functions = functions;
if (!hookEnabled) {
return;
public class NamePlates : IDisposable {
private static class Signatures {
internal const string NamePlateUpdate = "48 8B C4 41 56 48 81 EC ?? ?? ?? ?? 48 89 58 F0";
}
if (scanner.TryScanText(Signatures.NamePlateUpdate, out var updatePtr)) {
unsafe {
this._namePlateUpdateHook = interop.HookFromAddress<NamePlateUpdateDelegate>(updatePtr, this.NamePlateUpdateDetour);
private unsafe delegate IntPtr NamePlateUpdateDelegate(AddonNamePlate* addon, NumberArrayData** numberData, StringArrayData** stringData);
/// <summary>
/// The delegate for name plate update events.
/// </summary>
public delegate void NamePlateUpdateEvent(NamePlateUpdateEventArgs args);
/// <summary>
/// <para>
/// The event that is fired when a name plate is due to update.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.NamePlates"/> hook to be enabled.
/// </para>
/// </summary>
public event NamePlateUpdateEvent? OnUpdate;
private GameFunctions Functions { get; }
private readonly Hook<NamePlateUpdateDelegate>? _namePlateUpdateHook;
/// <summary>
/// <para>
/// If all name plates should be forced to redraw.
/// </para>
/// <para>
/// This is useful for forcing your changes to apply to existing name plates when the plugin is hot-loaded.
/// </para>
/// </summary>
public bool ForceRedraw { get; set; }
internal NamePlates(GameFunctions functions, SigScanner scanner, bool hookEnabled) {
this.Functions = functions;
if (!hookEnabled) {
return;
}
this._namePlateUpdateHook.Enable();
}
}
/// <inheritdoc />
public void Dispose() {
this._namePlateUpdateHook?.Dispose();
}
private const int PlateTypeIndex = 1;
private const int UpdateIndex = 2;
private const int ColourIndex = 8;
private const int IconIndex = 13;
private const int NamePlateObjectIndex = 15;
private const int FlagsIndex = 17;
private const int NameIndex = 0;
private const int TitleIndex = 50;
private const int FreeCompanyIndex = 100;
private const int EnemyLetterIndex = 150;
private const int LevelIndex = 200;
private unsafe IntPtr NamePlateUpdateDetour(AddonNamePlate* addon, NumberArrayData** numberData, StringArrayData** stringData) {
try {
this.NamePlateUpdateDetourInner(numberData, stringData);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in NamePlateUpdateDetour");
}
return this._namePlateUpdateHook!.Original(addon, numberData, stringData);
}
private unsafe void NamePlateUpdateDetourInner(NumberArrayData** numberData, StringArrayData** stringData) {
// don't skip to original if no subscribers because of ForceRedraw
var numbers = numberData[5];
var strings = stringData[4];
if (numbers == null || strings == null) {
return;
}
var atkModule = Framework.Instance()->GetUiModule()->GetRaptureAtkModule();
var active = numbers->IntArray[0];
var force = this.ForceRedraw;
if (force) {
this.ForceRedraw = false;
}
for (var i = 0; i < active; i++) {
var numbersIndex = i * 19 + 5;
if (force) {
numbers->SetValue(numbersIndex + UpdateIndex, numbers->IntArray[numbersIndex + UpdateIndex] | 1 | 2);
}
if (this.OnUpdate == null) {
continue;
}
if (numbers->IntArray[numbersIndex + UpdateIndex] == 0) {
continue;
}
var npObjIndex = numbers->IntArray[numbersIndex + NamePlateObjectIndex];
var info = (&atkModule->NamePlateInfoArray)[npObjIndex];
var icon = numbers->IntArray[numbersIndex + IconIndex];
var nameColour = *(ByteColor*) &numbers->IntArray[numbersIndex + ColourIndex];
var plateType = numbers->IntArray[numbersIndex + PlateTypeIndex];
var flags = numbers->IntArray[numbersIndex + FlagsIndex];
var nameRaw = strings->StringArray[NameIndex + i];
var name = Util.ReadSeString((IntPtr) nameRaw);
var titleRaw = strings->StringArray[TitleIndex + i];
var title = Util.ReadSeString((IntPtr) titleRaw);
var fcRaw = strings->StringArray[FreeCompanyIndex + i];
var fc = Util.ReadSeString((IntPtr) fcRaw);
var levelRaw = strings->StringArray[LevelIndex + i];
var level = Util.ReadSeString((IntPtr) levelRaw);
var letterRaw = strings->StringArray[EnemyLetterIndex + i];
var letter = Util.ReadSeString((IntPtr) letterRaw);
var args = new NamePlateUpdateEventArgs(info.ObjectID.ObjectID) {
Name = new SeString(name.Payloads),
FreeCompany = new SeString(fc.Payloads),
Title = new SeString(title.Payloads),
Level = new SeString(level.Payloads),
#pragma warning disable 0618
EnemyLetter = new SeString(letter.Payloads),
#pragma warning restore 0618
Colour = nameColour,
Icon = (uint) icon,
Type = (PlateType) plateType,
Flags = flags,
};
try {
this.OnUpdate?.Invoke(args);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in name plate update event");
}
void Replace(byte[] bytes, int i, bool free = true) {
// allocate new memory with the game for the new string
var mem = this.Functions.UiAlloc.Alloc((ulong) bytes.Length + 1);
// copy the new string over to the game's memory
Marshal.Copy(bytes, 0, mem, bytes.Length);
// terminate the new string
*(byte*) (mem + bytes.Length) = 0;
// replace the pointer with our new one
var old = strings->StringArray[i];
strings->StringArray[i] = (byte*) mem;
// free the old pointer
if (free && old != null) {
this.Functions.UiAlloc.Free((IntPtr) old);
if (scanner.TryScanText(Signatures.NamePlateUpdate, out var updatePtr)) {
unsafe {
this._namePlateUpdateHook = new Hook<NamePlateUpdateDelegate>(updatePtr, this.NamePlateUpdateDetour);
}
this._namePlateUpdateHook.Enable();
}
}
/// <inheritdoc />
public void Dispose() {
this._namePlateUpdateHook?.Dispose();
}
private const int PlateTypeIndex = 1;
private const int UpdateIndex = 2;
private const int ColourIndex = 8;
private const int IconIndex = 13;
private const int NamePlateObjectIndex = 15;
private const int FlagsIndex = 17;
private const int NameIndex = 0;
private const int TitleIndex = 50;
private const int FreeCompanyIndex = 100;
private const int EnemyLetterIndex = 150;
private const int LevelIndex = 200;
private unsafe IntPtr NamePlateUpdateDetour(AddonNamePlate* addon, NumberArrayData** numberData, StringArrayData** stringData) {
try {
this.NamePlateUpdateDetourInner(numberData, stringData);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in NamePlateUpdateDetour");
}
if (name != args.Name) {
Replace(args.Name.Encode(), NameIndex + i);
return this._namePlateUpdateHook!.Original(addon, numberData, stringData);
}
private unsafe void NamePlateUpdateDetourInner(NumberArrayData** numberData, StringArrayData** stringData) {
// don't skip to original if no subscribers because of ForceRedraw
var numbers = numberData[5];
var strings = stringData[4];
if (numbers == null || strings == null) {
return;
}
if (title != args.Title) {
Replace(args.Title.Encode(), TitleIndex + i);
var atkModule = this.Functions.GetFramework()->GetUiModule()->GetRaptureAtkModule();
var active = numbers->IntArray[0];
var force = this.ForceRedraw;
if (force) {
this.ForceRedraw = false;
}
if (fc != args.FreeCompany) {
Replace(args.FreeCompany.Encode(), FreeCompanyIndex + i);
}
for (var i = 0; i < active; i++) {
var numbersIndex = i * 19 + 5;
if (level != args.Level) {
Replace(args.Level.Encode(), LevelIndex + i);
}
if (force) {
numbers->SetValue(numbersIndex + UpdateIndex, numbers->IntArray[numbersIndex + UpdateIndex] | 1 | 2);
}
if (letter != args.EnemyLetter) {
// FIXME: sometimes the pointer here in the game is garbage, so freeing is a heap corruption
// figure out how to free this properly
Replace(args.EnemyLetter.Encode(), EnemyLetterIndex + i, false);
}
if (this.OnUpdate == null) {
continue;
}
if (icon != args.Icon) {
numbers->SetValue(numbersIndex + IconIndex, (int) args.Icon);
}
if (numbers->IntArray[numbersIndex + UpdateIndex] == 0) {
continue;
}
var colour = (ByteColor) args.Colour;
var colourInt = *(int*) &colour;
if (colourInt != numbers->IntArray[numbersIndex + ColourIndex]) {
numbers->SetValue(numbersIndex + ColourIndex, colourInt);
}
var npObjIndex = numbers->IntArray[numbersIndex + NamePlateObjectIndex];
var info = (&atkModule->NamePlateInfoArray)[npObjIndex];
if (plateType != (int) args.Type) {
numbers->SetValue(numbersIndex + PlateTypeIndex, (int) args.Type);
}
var icon = numbers->IntArray[numbersIndex + IconIndex];
var nameColour = *(ByteColor*) &numbers->IntArray[numbersIndex + ColourIndex];
var plateType = numbers->IntArray[numbersIndex + PlateTypeIndex];
var flags = numbers->IntArray[numbersIndex + FlagsIndex];
if (flags != args.Flags) {
numbers->SetValue(numbersIndex + FlagsIndex, args.Flags);
var nameRaw = strings->StringArray[NameIndex + i];
var name = Util.ReadSeString((IntPtr) nameRaw);
var titleRaw = strings->StringArray[TitleIndex + i];
var title = Util.ReadSeString((IntPtr) titleRaw);
var fcRaw = strings->StringArray[FreeCompanyIndex + i];
var fc = Util.ReadSeString((IntPtr) fcRaw);
var levelRaw = strings->StringArray[LevelIndex + i];
var level = Util.ReadSeString((IntPtr) levelRaw);
var letterRaw = strings->StringArray[EnemyLetterIndex + i];
var letter = Util.ReadSeString((IntPtr) letterRaw);
var args = new NamePlateUpdateEventArgs(info.ObjectID.ObjectID) {
Name = new SeString(name.Payloads),
FreeCompany = new SeString(fc.Payloads),
Title = new SeString(title.Payloads),
Level = new SeString(level.Payloads),
#pragma warning disable 0618
EnemyLetter = new SeString(letter.Payloads),
#pragma warning restore 0618
Colour = nameColour,
Icon = (uint) icon,
Type = (PlateType) plateType,
Flags = flags,
};
try {
this.OnUpdate?.Invoke(args);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in name plate update event");
}
void Replace(byte[] bytes, int i, bool free = true) {
// allocate new memory with the game for the new string
var mem = this.Functions.UiAlloc.Alloc((ulong) bytes.Length + 1);
// copy the new string over to the game's memory
Marshal.Copy(bytes, 0, mem, bytes.Length);
// terminate the new string
*(byte*) (mem + bytes.Length) = 0;
// replace the pointer with our new one
var old = strings->StringArray[i];
strings->StringArray[i] = (byte*) mem;
// free the old pointer
if (free && old != null) {
this.Functions.UiAlloc.Free((IntPtr) old);
}
}
if (name != args.Name) {
Replace(args.Name.Encode(), NameIndex + i);
}
if (title != args.Title) {
Replace(args.Title.Encode(), TitleIndex + i);
}
if (fc != args.FreeCompany) {
Replace(args.FreeCompany.Encode(), FreeCompanyIndex + i);
}
if (level != args.Level) {
Replace(args.Level.Encode(), LevelIndex + i);
}
if (letter != args.EnemyLetter) {
// FIXME: sometimes the pointer here in the game is garbage, so freeing is a heap corruption
// figure out how to free this properly
Replace(args.EnemyLetter.Encode(), EnemyLetterIndex + i, false);
}
if (icon != args.Icon) {
numbers->SetValue(numbersIndex + IconIndex, (int) args.Icon);
}
var colour = (ByteColor) args.Colour;
var colourInt = *(int*) &colour;
if (colourInt != numbers->IntArray[numbersIndex + ColourIndex]) {
numbers->SetValue(numbersIndex + ColourIndex, colourInt);
}
if (plateType != (int) args.Type) {
numbers->SetValue(numbersIndex + PlateTypeIndex, (int) args.Type);
}
if (flags != args.Flags) {
numbers->SetValue(numbersIndex + FlagsIndex, args.Flags);
}
}
}
}

View File

@ -1,176 +1,176 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics;
namespace XivCommon.Functions.NamePlates;
namespace XivCommon.Functions.NamePlates {
[StructLayout(LayoutKind.Explicit, Size = 0x28)]
internal unsafe struct NumberArrayData {
[FieldOffset(0x0)]
public AtkArrayData AtkArrayData;
[StructLayout(LayoutKind.Explicit, Size = 0x28)]
internal unsafe struct NumberArrayData {
[FieldOffset(0x0)]
public AtkArrayData AtkArrayData;
[FieldOffset(0x20)]
public int* IntArray;
[FieldOffset(0x20)]
public int* IntArray;
public void SetValue(int index, int value) {
if (index >= this.AtkArrayData.Size) {
return;
}
public void SetValue(int index, int value) {
if (index >= this.AtkArrayData.Size) {
return;
if (this.IntArray[index] == value) {
return;
}
this.IntArray[index] = value;
this.AtkArrayData.HasModifiedData = 1;
}
}
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
internal unsafe struct AtkArrayData {
[FieldOffset(0x0)]
public void* vtbl;
[FieldOffset(0x8)]
public int Size;
[FieldOffset(0x1C)]
public byte Unk1C;
[FieldOffset(0x1D)]
public byte Unk1D;
[FieldOffset(0x1E)]
public byte HasModifiedData;
[FieldOffset(0x1F)]
public byte Unk1F; // initialized to -1
}
[StructLayout(LayoutKind.Explicit, Size = 0x30)]
internal unsafe struct StringArrayData {
[FieldOffset(0x0)]
public AtkArrayData AtkArrayData;
[FieldOffset(0x20)]
public byte** StringArray; // char * *
[FieldOffset(0x28)]
public byte* UnkString; // char *
}
/// <summary>
/// The various different name plate types
/// </summary>
public enum PlateType {
/// <summary>
/// A normal player name plate
/// </summary>
Player = 0,
/// <summary>
/// A name plate with the icon and FC tag removed
/// </summary>
NoIconOrFc = 1, // 2, 5
/// <summary>
/// A name plate with a level string visible, title always below the name, and FC tag removed
/// </summary>
LevelNoFc = 3, // 4
/// <summary>
/// A name plate with only the name visible
/// </summary>
NameOnly = 6,
/// <summary>
/// A name plate with only the level string and name visible
/// </summary>
LevelAndName = 7,
/// <summary>
/// A name plate where the title always appears below the name and the FC tag is removed
/// </summary>
LowTitleNoFc = 8,
}
/// <summary>
/// A colour, represented in the RGBA format.
/// </summary>
public class RgbaColour {
/// <summary>
/// The red component of the colour.
/// </summary>
public byte R { get; set; }
/// <summary>
/// The green component of the colour.
/// </summary>
public byte G { get; set; }
/// <summary>
/// The blue component of the colour.
/// </summary>
public byte B { get; set; }
/// <summary>
/// The alpha component of the colour.
/// </summary>
public byte A { get; set; } = byte.MaxValue;
/// <summary>
/// Converts an unsigned integer into an RgbaColour.
/// </summary>
/// <param name="rgba">32-bit integer representing an RGBA colour</param>
/// <returns>an RgbaColour equivalent to the integer representation</returns>
public static implicit operator RgbaColour(uint rgba) {
var r = (byte) ((rgba >> 24) & 0xFF);
var g = (byte) ((rgba >> 16) & 0xFF);
var b = (byte) ((rgba >> 8) & 0xFF);
var a = (byte) (rgba & 0xFF);
return new RgbaColour {
R = r,
G = g,
B = b,
A = a,
};
}
if (this.IntArray[index] == value) {
return;
/// <summary>
/// Converts an RgbaColour into an unsigned integer representation.
/// </summary>
/// <param name="rgba">an RgbaColour to convert</param>
/// <returns>32-bit integer representing an RGBA colour</returns>
public static implicit operator uint(RgbaColour rgba) {
return (uint) ((rgba.R << 24)
| (rgba.G << 16)
| (rgba.B << 8)
| rgba.A);
}
this.IntArray[index] = value;
this.AtkArrayData.HasModifiedData = 1;
/// <summary>
/// Converts a ByteColor into an RgbaColour.
/// </summary>
/// <param name="rgba">ByteColor</param>
/// <returns>equivalent RgbaColour</returns>
public static implicit operator RgbaColour(ByteColor rgba) {
return (uint) ((rgba.R << 24)
| (rgba.G << 16)
| (rgba.B << 8)
| rgba.A);
}
/// <summary>
/// Converts an RgbaColour into a ByteColor.
/// </summary>
/// <param name="rgba">RgbaColour</param>
/// <returns>equivalent ByteColour</returns>
public static implicit operator ByteColor(RgbaColour rgba) {
return new() {
R = rgba.R,
G = rgba.G,
B = rgba.B,
A = rgba.A,
};
}
}
}
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
internal unsafe struct AtkArrayData {
[FieldOffset(0x0)]
public void* vtbl;
[FieldOffset(0x8)]
public int Size;
[FieldOffset(0x1C)]
public byte Unk1C;
[FieldOffset(0x1D)]
public byte Unk1D;
[FieldOffset(0x1E)]
public byte HasModifiedData;
[FieldOffset(0x1F)]
public byte Unk1F; // initialized to -1
}
[StructLayout(LayoutKind.Explicit, Size = 0x30)]
internal unsafe struct StringArrayData {
[FieldOffset(0x0)]
public AtkArrayData AtkArrayData;
[FieldOffset(0x20)]
public byte** StringArray; // char * *
[FieldOffset(0x28)]
public byte* UnkString; // char *
}
/// <summary>
/// The various different name plate types
/// </summary>
public enum PlateType {
/// <summary>
/// A normal player name plate
/// </summary>
Player = 0,
/// <summary>
/// A name plate with the icon and FC tag removed
/// </summary>
NoIconOrFc = 1, // 2, 5
/// <summary>
/// A name plate with a level string visible, title always below the name, and FC tag removed
/// </summary>
LevelNoFc = 3, // 4
/// <summary>
/// A name plate with only the name visible
/// </summary>
NameOnly = 6,
/// <summary>
/// A name plate with only the level string and name visible
/// </summary>
LevelAndName = 7,
/// <summary>
/// A name plate where the title always appears below the name and the FC tag is removed
/// </summary>
LowTitleNoFc = 8,
}
/// <summary>
/// A colour, represented in the RGBA format.
/// </summary>
public class RgbaColour {
/// <summary>
/// The red component of the colour.
/// </summary>
public byte R { get; set; }
/// <summary>
/// The green component of the colour.
/// </summary>
public byte G { get; set; }
/// <summary>
/// The blue component of the colour.
/// </summary>
public byte B { get; set; }
/// <summary>
/// The alpha component of the colour.
/// </summary>
public byte A { get; set; } = byte.MaxValue;
/// <summary>
/// Converts an unsigned integer into an RgbaColour.
/// </summary>
/// <param name="rgba">32-bit integer representing an RGBA colour</param>
/// <returns>an RgbaColour equivalent to the integer representation</returns>
public static implicit operator RgbaColour(uint rgba) {
var r = (byte) ((rgba >> 24) & 0xFF);
var g = (byte) ((rgba >> 16) & 0xFF);
var b = (byte) ((rgba >> 8) & 0xFF);
var a = (byte) (rgba & 0xFF);
return new RgbaColour {
R = r,
G = g,
B = b,
A = a,
};
}
/// <summary>
/// Converts an RgbaColour into an unsigned integer representation.
/// </summary>
/// <param name="rgba">an RgbaColour to convert</param>
/// <returns>32-bit integer representing an RGBA colour</returns>
public static implicit operator uint(RgbaColour rgba) {
return (uint) ((rgba.R << 24)
| (rgba.G << 16)
| (rgba.B << 8)
| rgba.A);
}
/// <summary>
/// Converts a ByteColor into an RgbaColour.
/// </summary>
/// <param name="rgba">ByteColor</param>
/// <returns>equivalent RgbaColour</returns>
public static implicit operator RgbaColour(ByteColor rgba) {
return (uint) ((rgba.R << 24)
| (rgba.G << 16)
| (rgba.B << 8)
| rgba.A);
}
/// <summary>
/// Converts an RgbaColour into a ByteColor.
/// </summary>
/// <param name="rgba">RgbaColour</param>
/// <returns>equivalent ByteColour</returns>
public static implicit operator ByteColor(RgbaColour rgba) {
return new() {
R = rgba.R,
G = rgba.G,
B = rgba.B,
A = rgba.A,
};
}
}

View File

@ -2,180 +2,180 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.Gui.PartyFinder;
using Dalamud.Game.Gui.PartyFinder.Types;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
namespace XivCommon.Functions;
/// <summary>
/// A class containing Party Finder functionality
/// </summary>
public class PartyFinder : IDisposable {
private static class Signatures {
internal const string RequestListings = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 40 0F 10 81";
internal const string JoinCrossParty = "E8 ?? ?? ?? ?? 41 0F B7 07 49 8B CC";
}
private delegate byte RequestPartyFinderListingsDelegate(IntPtr agent, byte categoryIdx);
private delegate IntPtr JoinPfDelegate(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5);
private RequestPartyFinderListingsDelegate? RequestPartyFinderListings { get; }
private Hook<RequestPartyFinderListingsDelegate>? RequestPfListingsHook { get; }
private Hook<JoinPfDelegate>? JoinPfHook { get; }
namespace XivCommon.Functions {
/// <summary>
/// The delegate for party join events.
/// A class containing Party Finder functionality
/// </summary>
public delegate void JoinPfEventDelegate(PartyFinderListing listing);
/// <summary>
/// <para>
/// The event that is fired when the player joins a <b>cross-world</b> party via Party Finder.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.PartyFinderJoins"/> hook to be enabled.
/// </para>
/// </summary>
public event JoinPfEventDelegate? JoinParty;
private IPartyFinderGui PartyFinderGui { get; }
private bool JoinsEnabled { get; }
private bool ListingsEnabled { get; }
private IntPtr PartyFinderAgent { get; set; } = IntPtr.Zero;
private Dictionary<uint, PartyFinderListing> Listings { get; } = new();
private int LastBatch { get; set; } = -1;
/// <summary>
/// <para>
/// The current Party Finder listings that have been displayed.
/// </para>
/// <para>
/// This dictionary is cleared and updated each time the Party Finder is requested, and it only contains the category selected in the Party Finder addon.
/// </para>
/// <para>
/// Keys are the listing ID for fast lookup by ID. Values are the listing itself.
/// </para>
/// </summary>
public IReadOnlyDictionary<uint, PartyFinderListing> CurrentListings => this.Listings;
internal PartyFinder(ISigScanner scanner, IPartyFinderGui partyFinderGui, IGameInteropProvider interop, Hooks hooks) {
this.PartyFinderGui = partyFinderGui;
this.ListingsEnabled = hooks.HasFlag(Hooks.PartyFinderListings);
this.JoinsEnabled = hooks.HasFlag(Hooks.PartyFinderJoins);
if (this.ListingsEnabled || this.JoinsEnabled) {
this.PartyFinderGui.ReceiveListing += this.ReceiveListing;
public class PartyFinder : IDisposable {
private static class Signatures {
internal const string RequestListings = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 40 0F 10 81 ?? ?? ?? ??";
internal const string JoinCrossParty = "E8 ?? ?? ?? ?? 0F B7 47 28";
}
if (scanner.TryScanText(Signatures.RequestListings, out var requestPfPtr, "Party Finder listings")) {
this.RequestPartyFinderListings = Marshal.GetDelegateForFunctionPointer<RequestPartyFinderListingsDelegate>(requestPfPtr);
private delegate byte RequestPartyFinderListingsDelegate(IntPtr agent, byte categoryIdx);
if (this.ListingsEnabled) {
this.RequestPfListingsHook = interop.HookFromAddress<RequestPartyFinderListingsDelegate>(requestPfPtr, this.OnRequestPartyFinderListings);
this.RequestPfListingsHook.Enable();
private delegate IntPtr JoinPfDelegate(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5);
private RequestPartyFinderListingsDelegate? RequestPartyFinderListings { get; }
private Hook<RequestPartyFinderListingsDelegate>? RequestPfListingsHook { get; }
private Hook<JoinPfDelegate>? JoinPfHook { get; }
/// <summary>
/// The delegate for party join events.
/// </summary>
public delegate void JoinPfEventDelegate(PartyFinderListing listing);
/// <summary>
/// <para>
/// The event that is fired when the player joins a <b>cross-world</b> party via Party Finder.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.PartyFinderJoins"/> hook to be enabled.
/// </para>
/// </summary>
public event JoinPfEventDelegate? JoinParty;
private PartyFinderGui PartyFinderGui { get; }
private bool JoinsEnabled { get; }
private bool ListingsEnabled { get; }
private IntPtr PartyFinderAgent { get; set; } = IntPtr.Zero;
private Dictionary<uint, PartyFinderListing> Listings { get; } = new();
private int LastBatch { get; set; } = -1;
/// <summary>
/// <para>
/// The current Party Finder listings that have been displayed.
/// </para>
/// <para>
/// This dictionary is cleared and updated each time the Party Finder is requested, and it only contains the category selected in the Party Finder addon.
/// </para>
/// <para>
/// Keys are the listing ID for fast lookup by ID. Values are the listing itself.
/// </para>
/// </summary>
public IReadOnlyDictionary<uint, PartyFinderListing> CurrentListings => this.Listings;
internal PartyFinder(SigScanner scanner, PartyFinderGui partyFinderGui, Hooks hooks) {
this.PartyFinderGui = partyFinderGui;
this.ListingsEnabled = hooks.HasFlag(Hooks.PartyFinderListings);
this.JoinsEnabled = hooks.HasFlag(Hooks.PartyFinderJoins);
if (this.ListingsEnabled || this.JoinsEnabled) {
this.PartyFinderGui.ReceiveListing += this.ReceiveListing;
}
if (scanner.TryScanText(Signatures.RequestListings, out var requestPfPtr, "Party Finder listings")) {
this.RequestPartyFinderListings = Marshal.GetDelegateForFunctionPointer<RequestPartyFinderListingsDelegate>(requestPfPtr);
if (this.ListingsEnabled) {
this.RequestPfListingsHook = new Hook<RequestPartyFinderListingsDelegate>(requestPfPtr, this.OnRequestPartyFinderListings);
this.RequestPfListingsHook.Enable();
}
}
if (this.JoinsEnabled && scanner.TryScanText(Signatures.JoinCrossParty, out var joinPtr, "Party Finder joins")) {
this.JoinPfHook = new Hook<JoinPfDelegate>(joinPtr, this.JoinPfDetour);
this.JoinPfHook.Enable();
}
}
if (this.JoinsEnabled && scanner.TryScanText(Signatures.JoinCrossParty, out var joinPtr, "Party Finder joins")) {
this.JoinPfHook = interop.HookFromAddress<JoinPfDelegate>(joinPtr, this.JoinPfDetour);
this.JoinPfHook.Enable();
}
}
/// <inheritdoc />
public void Dispose() {
this.PartyFinderGui.ReceiveListing -= this.ReceiveListing;
this.JoinPfHook?.Dispose();
this.RequestPfListingsHook?.Dispose();
}
private void ReceiveListing(PartyFinderListing listing, PartyFinderListingEventArgs args) {
if (args.BatchNumber != this.LastBatch) {
this.Listings.Clear();
/// <inheritdoc />
public void Dispose() {
this.PartyFinderGui.ReceiveListing -= this.ReceiveListing;
this.JoinPfHook?.Dispose();
this.RequestPfListingsHook?.Dispose();
}
this.LastBatch = args.BatchNumber;
private void ReceiveListing(PartyFinderListing listing, PartyFinderListingEventArgs args) {
if (args.BatchNumber != this.LastBatch) {
this.Listings.Clear();
}
this.Listings[listing.Id] = listing;
}
this.LastBatch = args.BatchNumber;
private byte OnRequestPartyFinderListings(IntPtr agent, byte categoryIdx) {
this.PartyFinderAgent = agent;
return this.RequestPfListingsHook!.Original(agent, categoryIdx);
}
this.Listings[listing.Id] = listing;
}
private IntPtr JoinPfDetour(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5) {
// Updated: 5.5
const int idOffset = -0x20;
private byte OnRequestPartyFinderListings(IntPtr agent, byte categoryIdx) {
this.PartyFinderAgent = agent;
return this.RequestPfListingsHook!.Original(agent, categoryIdx);
}
var ret = this.JoinPfHook!.Original(manager, a2, type, packetData, a5);
private IntPtr JoinPfDetour(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5) {
// Updated: 5.5
const int idOffset = -0x20;
var ret = this.JoinPfHook!.Original(manager, a2, type, packetData, a5);
if (this.JoinParty == null || (JoinType) type != JoinType.PartyFinder || packetData == IntPtr.Zero) {
return ret;
}
try {
var id = (uint) Marshal.ReadInt32(packetData + idOffset);
if (this.Listings.TryGetValue(id, out var listing)) {
this.JoinParty?.Invoke(listing);
}
} catch (Exception ex) {
Logger.LogError(ex, "Exception in PF join detour");
}
if (this.JoinParty == null || (JoinType) type != JoinType.PartyFinder || packetData == IntPtr.Zero) {
return ret;
}
try {
var id = (uint) Marshal.ReadInt32(packetData + idOffset);
if (this.Listings.TryGetValue(id, out var listing)) {
this.JoinParty?.Invoke(listing);
/// <summary>
/// <para>
/// Refresh the Party Finder listings. This does not open the Party Finder.
/// </para>
/// <para>
/// This maintains the currently selected category.
/// </para>
/// </summary>
/// <exception cref="InvalidOperationException">If the <see cref="Hooks.PartyFinderListings"/> hook is not enabled or if the signature for this function could not be found</exception>
public void RefreshListings() {
if (this.RequestPartyFinderListings == null) {
throw new InvalidOperationException("Could not find signature for Party Finder listings");
}
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in PF join detour");
}
return ret;
if (!this.ListingsEnabled) {
throw new InvalidOperationException("PartyFinder hooks are not enabled");
}
// Updated 5.5
const int categoryOffset = 10_655;
if (this.PartyFinderAgent == IntPtr.Zero) {
return;
}
var categoryIdx = Marshal.ReadByte(this.PartyFinderAgent + categoryOffset);
this.RequestPartyFinderListings(this.PartyFinderAgent, categoryIdx);
}
}
/// <summary>
/// <para>
/// Refresh the Party Finder listings. This does not open the Party Finder.
/// </para>
/// <para>
/// This maintains the currently selected category.
/// </para>
/// </summary>
/// <exception cref="InvalidOperationException">If the <see cref="Hooks.PartyFinderListings"/> hook is not enabled or if the signature for this function could not be found</exception>
public void RefreshListings() {
if (this.RequestPartyFinderListings == null) {
throw new InvalidOperationException("Could not find signature for Party Finder listings");
}
internal enum JoinType : byte {
/// <summary>
/// Join via invite or party conversion.
/// </summary>
Normal = 0,
if (!this.ListingsEnabled) {
throw new InvalidOperationException("PartyFinder hooks are not enabled");
}
/// <summary>
/// Join via Party Finder.
/// </summary>
PartyFinder = 1,
// Updated 6.0
const int categoryOffset = 11_031;
Unknown2 = 2,
if (this.PartyFinderAgent == IntPtr.Zero) {
return;
}
/// <summary>
/// Remain in cross-world party after leaving a duty.
/// </summary>
LeaveDuty = 3,
var categoryIdx = Marshal.ReadByte(this.PartyFinderAgent + categoryOffset);
this.RequestPartyFinderListings(this.PartyFinderAgent, categoryIdx);
Unknown4 = 4,
}
}
internal enum JoinType : byte {
/// <summary>
/// Join via invite or party conversion.
/// </summary>
Normal = 0,
/// <summary>
/// Join via Party Finder.
/// </summary>
PartyFinder = 1,
Unknown2 = 2,
/// <summary>
/// Remain in cross-world party after leaving a duty.
/// </summary>
LeaveDuty = 3,
Unknown4 = 4,
}

View File

@ -3,158 +3,157 @@ using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
namespace XivCommon.Functions;
/// <summary>
/// Class containing Talk events
/// </summary>
public class Talk : IDisposable {
private static class Signatures {
internal const string SetAtkValue = "E8 ?? ?? ?? ?? 41 03 ED";
internal const string ShowMessageBox = "4C 8B DC 55 57 41 55 49 8D 6B 98";
}
// Updated: 5.5
private const int TextOffset = 0;
private const int NameOffset = 0x10;
private const int StyleOffset = 0x38;
private delegate void AddonTalkV45Delegate(IntPtr addon, IntPtr a2, IntPtr data);
private Hook<AddonTalkV45Delegate>? AddonTalkV45Hook { get; }
private delegate IntPtr SetAtkValueStringDelegate(IntPtr atkValue, IntPtr text);
private SetAtkValueStringDelegate SetAtkValueString { get; } = null!;
namespace XivCommon.Functions {
/// <summary>
/// The delegate for Talk events.
/// Class containing Talk events
/// </summary>
public delegate void TalkEventDelegate(ref SeString name, ref SeString text, ref TalkStyle style);
/// <summary>
/// <para>
/// The event that is fired when NPCs talk.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Talk"/> hook to be enabled.
/// </para>
/// </summary>
public event TalkEventDelegate? OnTalk;
internal Talk(ISigScanner scanner, IGameInteropProvider interop, bool hooksEnabled) {
if (scanner.TryScanText(Signatures.SetAtkValue, out var setAtkPtr, "Talk - set atk value")) {
this.SetAtkValueString = Marshal.GetDelegateForFunctionPointer<SetAtkValueStringDelegate>(setAtkPtr);
} else {
return;
public class Talk : IDisposable {
private static class Signatures {
internal const string SetAtkValue = "E8 ?? ?? ?? ?? 41 03 ED";
internal const string ShowMessageBox = "4C 8B DC 55 57 41 55 49 8D 6B 98";
}
if (!hooksEnabled) {
return;
// Updated: 5.5
private const int TextOffset = 0;
private const int NameOffset = 0x10;
private const int StyleOffset = 0x38;
private delegate void AddonTalkV45Delegate(IntPtr addon, IntPtr a2, IntPtr data);
private Hook<AddonTalkV45Delegate>? AddonTalkV45Hook { get; }
private delegate IntPtr SetAtkValueStringDelegate(IntPtr atkValue, IntPtr text);
private SetAtkValueStringDelegate SetAtkValueString { get; } = null!;
/// <summary>
/// The delegate for Talk events.
/// </summary>
public delegate void TalkEventDelegate(ref SeString name, ref SeString text, ref TalkStyle style);
/// <summary>
/// <para>
/// The event that is fired when NPCs talk.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Talk"/> hook to be enabled.
/// </para>
/// </summary>
public event TalkEventDelegate? OnTalk;
internal Talk(SigScanner scanner, bool hooksEnabled) {
if (scanner.TryScanText(Signatures.SetAtkValue, out var setAtkPtr, "Talk - set atk value")) {
this.SetAtkValueString = Marshal.GetDelegateForFunctionPointer<SetAtkValueStringDelegate>(setAtkPtr);
} else {
return;
}
if (!hooksEnabled) {
return;
}
if (scanner.TryScanText(Signatures.ShowMessageBox, out var showMessageBoxPtr, "Talk")) {
this.AddonTalkV45Hook = new Hook<AddonTalkV45Delegate>(showMessageBoxPtr, this.AddonTalkV45Detour);
this.AddonTalkV45Hook.Enable();
}
}
if (scanner.TryScanText(Signatures.ShowMessageBox, out var showMessageBoxPtr, "Talk")) {
this.AddonTalkV45Hook = interop.HookFromAddress<AddonTalkV45Delegate>(showMessageBoxPtr, this.AddonTalkV45Detour);
this.AddonTalkV45Hook.Enable();
}
}
/// <inheritdoc />
public void Dispose() {
this.AddonTalkV45Hook?.Dispose();
}
private void AddonTalkV45Detour(IntPtr addon, IntPtr a2, IntPtr data) {
if (this.OnTalk == null) {
goto Return;
/// <inheritdoc />
public void Dispose() {
this.AddonTalkV45Hook?.Dispose();
}
try {
this.AddonTalkV45DetourInner(data);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in Talk detour");
private void AddonTalkV45Detour(IntPtr addon, IntPtr a2, IntPtr data) {
if (this.OnTalk == null) {
goto Return;
}
try {
this.AddonTalkV45DetourInner(data);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in Talk detour");
}
Return:
this.AddonTalkV45Hook!.Original(addon, a2, data);
}
Return:
this.AddonTalkV45Hook!.Original(addon, a2, data);
}
private void AddonTalkV45DetourInner(IntPtr data) {
var rawName = Util.ReadTerminated(Marshal.ReadIntPtr(data + NameOffset + 8));
var rawText = Util.ReadTerminated(Marshal.ReadIntPtr(data + TextOffset + 8));
var style = (TalkStyle) Marshal.ReadByte(data + StyleOffset);
private void AddonTalkV45DetourInner(IntPtr data) {
var rawName = Util.ReadTerminated(Marshal.ReadIntPtr(data + NameOffset + 8));
var rawText = Util.ReadTerminated(Marshal.ReadIntPtr(data + TextOffset + 8));
var style = (TalkStyle) Marshal.ReadByte(data + StyleOffset);
var name = SeString.Parse(rawName);
var text = SeString.Parse(rawText);
var name = SeString.Parse(rawName);
var text = SeString.Parse(rawText);
try {
this.OnTalk?.Invoke(ref name, ref text, ref style);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in Talk event");
}
try {
this.OnTalk?.Invoke(ref name, ref text, ref style);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in Talk event");
}
var newName = name.Encode().Terminate();
var newText = text.Encode().Terminate();
var newName = name.Encode().Terminate();
var newText = text.Encode().Terminate();
Marshal.WriteByte(data + StyleOffset, (byte) style);
Marshal.WriteByte(data + StyleOffset, (byte) style);
unsafe {
fixed (byte* namePtr = newName, textPtr = newText) {
this.SetAtkValueString(data + NameOffset, (IntPtr) namePtr);
this.SetAtkValueString(data + TextOffset, (IntPtr) textPtr);
unsafe {
fixed (byte* namePtr = newName, textPtr = newText) {
this.SetAtkValueString(data + NameOffset, (IntPtr) namePtr);
this.SetAtkValueString(data + TextOffset, (IntPtr) textPtr);
}
}
}
}
}
/// <summary>
/// Talk window styles.
/// </summary>
public enum TalkStyle : byte {
/// <summary>
/// The normal style with a white background.
/// </summary>
Normal = 0,
/// <summary>
/// A style with lights on the top and bottom border.
/// </summary>
Lights = 2,
/// <summary>
/// A style used for when characters are shouting.
/// </summary>
Shout = 3,
/// <summary>
/// Like <see cref="Shout"/> but with flatter edges.
/// </summary>
FlatShout = 4,
/// <summary>
/// The style used when dragons (and some other NPCs) talk.
/// </summary>
Dragon = 5,
/// <summary>
/// The style used for Allagan machinery.
/// </summary>
Allagan = 6,
/// <summary>
/// The style used for system messages.
/// </summary>
System = 7,
/// <summary>
/// A mixture of the system message style and the dragon style.
/// </summary>
DragonSystem = 8,
/// <summary>
/// The system message style with a purple background.
/// </summary>
PurpleSystem = 9,
/// <summary>
/// Talk window styles.
/// </summary>
public enum TalkStyle : byte {
/// <summary>
/// The normal style with a white background.
/// </summary>
Normal = 0,
/// <summary>
/// A style with lights on the top and bottom border.
/// </summary>
Lights = 2,
/// <summary>
/// A style used for when characters are shouting.
/// </summary>
Shout = 3,
/// <summary>
/// Like <see cref="Shout"/> but with flatter edges.
/// </summary>
FlatShout = 4,
/// <summary>
/// The style used when dragons (and some other NPCs) talk.
/// </summary>
Dragon = 5,
/// <summary>
/// The style used for Allagan machinery.
/// </summary>
Allagan = 6,
/// <summary>
/// The style used for system messages.
/// </summary>
System = 7,
/// <summary>
/// A mixture of the system message style and the dragon style.
/// </summary>
DragonSystem = 8,
/// <summary>
/// The system message style with a purple background.
/// </summary>
PurpleSystem = 9,
}
}

View File

@ -1,28 +1,28 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.Tooltips;
/// <summary>
/// The class allowing for action tooltip manipulation
/// </summary>
public unsafe class ActionTooltip : BaseTooltip {
internal ActionTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) : base(sadSetString, stringArrayData, numberArrayData) {
}
namespace XivCommon.Functions.Tooltips {
/// <summary>
/// Gets or sets the SeString for the given string enum.
/// The class allowing for action tooltip manipulation
/// </summary>
/// <param name="ats">the string to retrieve/update</param>
public SeString this[ActionTooltipString ats] {
get => this[(int) ats];
set => this[(int) ats] = value;
}
public unsafe class ActionTooltip : BaseTooltip {
internal ActionTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) : base(sadSetString, stringArrayData, numberArrayData) {
}
/// <summary>
/// Gets or sets which fields are visible on the tooltip.
/// </summary>
public ActionTooltipFields Fields {
get => (ActionTooltipFields) (**(this.NumberArrayData + 4));
set => **(this.NumberArrayData + 4) = (int) value;
/// <summary>
/// Gets or sets the SeString for the given string enum.
/// </summary>
/// <param name="ats">the string to retrieve/update</param>
public SeString this[ActionTooltipString ats] {
get => this[(int) ats];
set => this[(int) ats] = value;
}
/// <summary>
/// Gets or sets which fields are visible on the tooltip.
/// </summary>
public ActionTooltipFields Fields {
get => (ActionTooltipFields) (**(this.NumberArrayData + 4));
set => **(this.NumberArrayData + 4) = (int) value;
}
}
}
}

View File

@ -1,44 +1,44 @@
using System;
namespace XivCommon.Functions.Tooltips;
namespace XivCommon.Functions.Tooltips {
/// <summary>
/// An enum containing the strings used in action tooltips.
/// </summary>
public enum ActionTooltipString {
#pragma warning disable 1591
Name = 0,
Type = 1,
RangeLabel = 3,
Range = 4,
RadiusLabel = 5,
Radius = 6,
CostLabel = 7,
Cost = 8,
RecastLabel = 9,
Recast = 10,
CastLabel = 11,
Cast = 12,
Description = 13,
Acquired = 14,
Affinity = 15,
#pragma warning restore 1591
}
/// <summary>
/// An enum containing the strings used in action tooltips.
/// </summary>
public enum ActionTooltipString {
#pragma warning disable 1591
Name = 0,
Type = 1,
RangeLabel = 3,
Range = 4,
RadiusLabel = 5,
Radius = 6,
CostLabel = 7,
Cost = 8,
RecastLabel = 9,
Recast = 10,
CastLabel = 11,
Cast = 12,
Description = 13,
Acquired = 14,
Affinity = 15,
#pragma warning restore 1591
/// <summary>
/// An enum containing the fields that can be displayed in action tooltips.
/// </summary>
[Flags]
public enum ActionTooltipFields {
#pragma warning disable 1591
Range = 1 << 0,
Radius = 1 << 1,
Cost = 1 << 2,
Recast = 1 << 3,
Cast = 1 << 4,
Description = 1 << 5,
Acquired = 1 << 6,
Affinity = 1 << 7,
Unknown8 = 1 << 8,
#pragma warning restore 1591
}
}
/// <summary>
/// An enum containing the fields that can be displayed in action tooltips.
/// </summary>
[Flags]
public enum ActionTooltipFields {
#pragma warning disable 1591
Range = 1 << 0,
Radius = 1 << 1,
Cost = 1 << 2,
Recast = 1 << 3,
Cast = 1 << 4,
Description = 1 << 5,
Acquired = 1 << 6,
Affinity = 1 << 7,
Unknown8 = 1 << 8,
#pragma warning restore 1591
}

View File

@ -1,50 +1,50 @@
using System;
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.Tooltips;
/// <summary>
/// The base class for tooltips
/// </summary>
public abstract unsafe class BaseTooltip {
private Tooltips.StringArrayDataSetStringDelegate SadSetString { get; }
namespace XivCommon.Functions.Tooltips {
/// <summary>
/// A pointer to the StringArrayData class for this tooltip.
/// The base class for tooltips
/// </summary>
private readonly byte*** _stringArrayData; // this is StringArrayData* when ClientStructs is updated
public abstract unsafe class BaseTooltip {
private Tooltips.StringArrayDataSetStringDelegate SadSetString { get; }
/// <summary>
/// A pointer to the NumberArrayData class for this tooltip.
/// </summary>
protected readonly int** NumberArrayData;
/// <summary>
/// A pointer to the StringArrayData class for this tooltip.
/// </summary>
private readonly byte*** _stringArrayData; // this is StringArrayData* when ClientStructs is updated
internal BaseTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) {
this.SadSetString = sadSetString;
this._stringArrayData = stringArrayData;
this.NumberArrayData = numberArrayData;
}
/// <summary>
/// A pointer to the NumberArrayData class for this tooltip.
/// </summary>
protected readonly int** NumberArrayData;
/// <summary>
/// <para>
/// Gets the SeString at the given index for this tooltip.
/// </para>
/// <para>
/// Implementors should provide an enum accessor for this.
/// </para>
/// </summary>
/// <param name="index">string index to retrieve</param>
protected SeString this[int index] {
get {
var ptr = *(this._stringArrayData + 4) + index;
return Util.ReadSeString((IntPtr) (*ptr));
internal BaseTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) {
this.SadSetString = sadSetString;
this._stringArrayData = stringArrayData;
this.NumberArrayData = numberArrayData;
}
set {
var encoded = value.Encode().Terminate();
fixed (byte* encodedPtr = encoded) {
this.SadSetString((IntPtr) this._stringArrayData, index, encodedPtr, 0, 1, 1);
/// <summary>
/// <para>
/// Gets the SeString at the given index for this tooltip.
/// </para>
/// <para>
/// Implementors should provide an enum accessor for this.
/// </para>
/// </summary>
/// <param name="index">string index to retrieve</param>
protected SeString this[int index] {
get {
var ptr = *(this._stringArrayData + 4) + index;
return Util.ReadSeString((IntPtr) (*ptr));
}
set {
var encoded = value.Encode().Terminate();
fixed (byte* encodedPtr = encoded) {
this.SadSetString((IntPtr) this._stringArrayData, index, encodedPtr, 0, 1, 1);
}
}
}
}
}
}

View File

@ -1,28 +1,28 @@
using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.Tooltips;
/// <summary>
/// The class allowing for item tooltip manipulation
/// </summary>
public unsafe class ItemTooltip : BaseTooltip {
internal ItemTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) : base(sadSetString, stringArrayData, numberArrayData) {
}
namespace XivCommon.Functions.Tooltips {
/// <summary>
/// Gets or sets the SeString for the given string enum.
/// The class allowing for item tooltip manipulation
/// </summary>
/// <param name="its">the string to retrieve/update</param>
public SeString this[ItemTooltipString its] {
get => this[(int) its];
set => this[(int) its] = value;
}
public unsafe class ItemTooltip : BaseTooltip {
internal ItemTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) : base(sadSetString, stringArrayData, numberArrayData) {
}
/// <summary>
/// Gets or sets which fields are visible on the tooltip.
/// </summary>
public ItemTooltipFields Fields {
get => (ItemTooltipFields) (*(*(this.NumberArrayData + 4) + 4));
set => *(*(this.NumberArrayData + 4) + 4) = (int) value;
/// <summary>
/// Gets or sets the SeString for the given string enum.
/// </summary>
/// <param name="its">the string to retrieve/update</param>
public SeString this[ItemTooltipString its] {
get => this[(int) its];
set => this[(int) its] = value;
}
/// <summary>
/// Gets or sets which fields are visible on the tooltip.
/// </summary>
public ItemTooltipFields Fields {
get => (ItemTooltipFields) (*(*(this.NumberArrayData + 4) + 4));
set => *(*(this.NumberArrayData + 4) + 4) = (int) value;
}
}
}
}

View File

@ -1,94 +1,94 @@
using System;
namespace XivCommon.Functions.Tooltips;
/// <summary>
/// An enum containing the strings used in item tooltips.
/// </summary>
public enum ItemTooltipString {
#pragma warning disable 1591
Name = 0,
GlamourName = 1,
Type = 2,
Stat1Label = 4,
Stat2Label = 5,
Stat3Label = 6,
Stat1 = 7,
Stat2 = 8,
Stat3 = 9,
Stat1Delta = 10,
Stat2Delta = 11,
Stat3Delta = 12,
Description = 13,
Quantity = 14,
EffectsLabel = 15,
Effects = 16,
EquipJobs = 22,
EquipLevel = 23,
VendorSellPrice = 25,
Crafter = 26,
Level = 27,
Condition = 28,
SpiritbondLabel = 29,
Spiritbond = 30,
RepairLevel = 31,
Materials = 32,
QuickRepairs = 33,
MateriaMelding = 34,
Capabilities = 35,
BonusesLabel = 36,
Bonus1 = 37,
Bonus2 = 38,
Bonus3 = 39,
Bonus4 = 40,
MateriaLabel = 52,
Materia1 = 53,
Materia2 = 54,
Materia3 = 55,
Materia4 = 56,
Materia5 = 57,
Materia1Effect = 58,
Materia2Effect = 59,
Materia3Effect = 60,
Materia4Effect = 61,
Materia5Effect = 62,
ShopSellingPrice = 63,
ControllerControls = 64,
#pragma warning restore 1591
}
/// <summary>
/// An enum containing the fields that can be displayed in item tooltips.
/// </summary>
[Flags]
public enum ItemTooltipFields {
#pragma warning disable 1591
Crafter = 1 << 0,
Description = 1 << 1,
VendorSellPrice = 1 << 2,
// makes the tooltip much smaller when hovered over gear and unset
// something to do with EquipLevel maybe?
Unknown3 = 1 << 3,
Bonuses = 1 << 4,
Materia = 1 << 5,
CraftingAndRepairs = 1 << 6,
Effects = 1 << 8,
DyeableIndicator = 1 << 10,
Stat1 = 1 << 11,
Stat2 = 1 << 12,
Stat3 = 1 << 13,
namespace XivCommon.Functions.Tooltips {
/// <summary>
/// An enum containing the strings used in item tooltips.
/// </summary>
public enum ItemTooltipString {
#pragma warning disable 1591
Name = 0,
GlamourName = 1,
Type = 2,
Stat1Label = 4,
Stat2Label = 5,
Stat3Label = 6,
Stat1 = 7,
Stat2 = 8,
Stat3 = 9,
Stat1Delta = 10,
Stat2Delta = 11,
Stat3Delta = 12,
Description = 13,
Quantity = 14,
EffectsLabel = 15,
Effects = 16,
EquipJobs = 22,
EquipLevel = 23,
VendorSellPrice = 25,
Crafter = 26,
Level = 27,
Condition = 28,
SpiritbondLabel = 29,
Spiritbond = 30,
RepairLevel = 31,
Materials = 32,
QuickRepairs = 33,
MateriaMelding = 34,
Capabilities = 35,
BonusesLabel = 36,
Bonus1 = 37,
Bonus2 = 38,
Bonus3 = 39,
Bonus4 = 40,
MateriaLabel = 52,
Materia1 = 53,
Materia2 = 54,
Materia3 = 55,
Materia4 = 56,
Materia5 = 57,
Materia1Effect = 58,
Materia2Effect = 59,
Materia3Effect = 60,
Materia4Effect = 61,
Materia5Effect = 62,
ShopSellingPrice = 63,
ControllerControls = 64,
#pragma warning restore 1591
}
/// <summary>
/// <para>
/// Shows item level and equip level.
/// </para>
/// <para>
/// Item level is always visible, but if equip level is set to an empty string, it will be hidden.
/// </para>
/// An enum containing the fields that can be displayed in item tooltips.
/// </summary>
Levels = 1 << 15,
GlamourIndicator = 1 << 16,
Unknown19 = 1 << 19,
#pragma warning restore 1591
}
[Flags]
public enum ItemTooltipFields {
#pragma warning disable 1591
Crafter = 1 << 0,
Description = 1 << 1,
VendorSellPrice = 1 << 2,
// makes the tooltip much smaller when hovered over gear and unset
// something to do with EquipLevel maybe?
Unknown3 = 1 << 3,
Bonuses = 1 << 4,
Materia = 1 << 5,
CraftingAndRepairs = 1 << 6,
Effects = 1 << 8,
DyeableIndicator = 1 << 10,
Stat1 = 1 << 11,
Stat2 = 1 << 12,
Stat3 = 1 << 13,
/// <summary>
/// <para>
/// Shows item level and equip level.
/// </para>
/// <para>
/// Item level is always visible, but if equip level is set to an empty string, it will be hidden.
/// </para>
/// </summary>
Levels = 1 << 15,
GlamourIndicator = 1 << 16,
Unknown19 = 1 << 19,
#pragma warning restore 1591
}
}

View File

@ -3,150 +3,149 @@ using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.Gui;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
namespace XivCommon.Functions.Tooltips;
/// <summary>
/// The class containing tooltip functionality
/// </summary>
public class Tooltips : IDisposable {
private static class Signatures {
internal const string AgentItemDetailUpdateTooltip = "E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ?? 48 89 AE";
internal const string AgentActionDetailUpdateTooltip = "E8 ?? ?? ?? ?? EB 68 FF 50 40";
internal const string SadSetString = "E8 ?? ?? ?? ?? F6 47 14 08";
}
// Last checked: 6.0
// E8 ?? ?? ?? ?? EB 68 FF 50 40
private const int AgentActionDetailUpdateFlagOffset = 0x58;
internal unsafe delegate void StringArrayDataSetStringDelegate(IntPtr self, int index, byte* str, byte updatePtr, byte copyToUi, byte dontSetModified);
private unsafe delegate ulong ItemUpdateTooltipDelegate(IntPtr agent, int** numberArrayData, byte*** stringArrayData, float a4);
private unsafe delegate void ActionUpdateTooltipDelegate(IntPtr agent, int** numberArrayData, byte*** stringArrayData);
private StringArrayDataSetStringDelegate? SadSetString { get; }
private Hook<ItemUpdateTooltipDelegate>? ItemUpdateTooltipHook { get; }
private Hook<ActionUpdateTooltipDelegate>? ActionGenerateTooltipHook { get; }
namespace XivCommon.Functions.Tooltips {
/// <summary>
/// The delegate for item tooltip events.
/// The class containing tooltip functionality
/// </summary>
public delegate void ItemTooltipEventDelegate(ItemTooltip itemTooltip, ulong itemId);
/// <summary>
/// The tooltip for action tooltip events.
/// </summary>
public delegate void ActionTooltipEventDelegate(ActionTooltip actionTooltip, HoveredAction action);
/// <summary>
/// <para>
/// The event that is fired when an item tooltip is being generated for display.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Tooltips"/> hook to be enabled.
/// </para>
/// </summary>
public event ItemTooltipEventDelegate? OnItemTooltip;
/// <summary>
/// <para>
/// The event that is fired when an action tooltip is being generated for display.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Tooltips"/> hook to be enabled.
/// </para>
/// </summary>
public event ActionTooltipEventDelegate? OnActionTooltip;
private IGameGui GameGui { get; }
private ItemTooltip? ItemTooltip { get; set; }
private ActionTooltip? ActionTooltip { get; set; }
internal Tooltips(ISigScanner scanner, IGameGui gui, IGameInteropProvider interop, bool enabled) {
this.GameGui = gui;
if (scanner.TryScanText(Signatures.SadSetString, out var setStringPtr, "Tooltips - StringArrayData::SetString")) {
this.SadSetString = Marshal.GetDelegateForFunctionPointer<StringArrayDataSetStringDelegate>(setStringPtr);
} else {
return;
public class Tooltips : IDisposable {
private static class Signatures {
internal const string AgentItemDetailUpdateTooltip = "E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ??";
internal const string AgentActionDetailUpdateTooltip = "E8 ?? ?? ?? ?? EB 68 FF 50 40";
internal const string SadSetString = "E8 ?? ?? ?? ?? F6 47 14 08";
}
if (!enabled) {
return;
}
// Updated: 5.55
// E8 ?? ?? ?? ?? EB 68 FF 50 40
private const int AgentActionDetailUpdateFlagOffset = 0x58;
if (scanner.TryScanText(Signatures.AgentItemDetailUpdateTooltip, out var updateItemPtr, "Tooltips - Items")) {
unsafe {
this.ItemUpdateTooltipHook = interop.HookFromAddress<ItemUpdateTooltipDelegate>(updateItemPtr, this.ItemUpdateTooltipDetour);
internal unsafe delegate void StringArrayDataSetStringDelegate(IntPtr self, int index, byte* str, byte updatePtr, byte copyToUi, byte dontSetModified);
private unsafe delegate byte ItemUpdateTooltipDelegate(IntPtr agent, int** numberArrayData, byte*** stringArrayData, float a4);
private unsafe delegate void ActionUpdateTooltipDelegate(IntPtr agent, int** numberArrayData, byte*** stringArrayData);
private StringArrayDataSetStringDelegate? SadSetString { get; }
private Hook<ItemUpdateTooltipDelegate>? ItemUpdateTooltipHook { get; }
private Hook<ActionUpdateTooltipDelegate>? ActionGenerateTooltipHook { get; }
/// <summary>
/// The delegate for item tooltip events.
/// </summary>
public delegate void ItemTooltipEventDelegate(ItemTooltip itemTooltip, ulong itemId);
/// <summary>
/// The tooltip for action tooltip events.
/// </summary>
public delegate void ActionTooltipEventDelegate(ActionTooltip actionTooltip, HoveredAction action);
/// <summary>
/// <para>
/// The event that is fired when an item tooltip is being generated for display.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Tooltips"/> hook to be enabled.
/// </para>
/// </summary>
public event ItemTooltipEventDelegate? OnItemTooltip;
/// <summary>
/// <para>
/// The event that is fired when an action tooltip is being generated for display.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Tooltips"/> hook to be enabled.
/// </para>
/// </summary>
public event ActionTooltipEventDelegate? OnActionTooltip;
private GameGui GameGui { get; }
private ItemTooltip? ItemTooltip { get; set; }
private ActionTooltip? ActionTooltip { get; set; }
internal Tooltips(SigScanner scanner, GameGui gui, bool enabled) {
this.GameGui = gui;
if (scanner.TryScanText(Signatures.SadSetString, out var setStringPtr, "Tooltips - StringArrayData::SetString")) {
this.SadSetString = Marshal.GetDelegateForFunctionPointer<StringArrayDataSetStringDelegate>(setStringPtr);
} else {
return;
}
this.ItemUpdateTooltipHook.Enable();
}
if (scanner.TryScanText(Signatures.AgentActionDetailUpdateTooltip, out var updateActionPtr, "Tooltips - Actions")) {
unsafe {
this.ActionGenerateTooltipHook = interop.HookFromAddress<ActionUpdateTooltipDelegate>(updateActionPtr, this.ActionUpdateTooltipDetour);
if (!enabled) {
return;
}
this.ActionGenerateTooltipHook.Enable();
if (scanner.TryScanText(Signatures.AgentItemDetailUpdateTooltip, out var updateItemPtr, "Tooltips - Items")) {
unsafe {
this.ItemUpdateTooltipHook = new Hook<ItemUpdateTooltipDelegate>(updateItemPtr, this.ItemUpdateTooltipDetour);
}
this.ItemUpdateTooltipHook.Enable();
}
if (scanner.TryScanText(Signatures.AgentActionDetailUpdateTooltip, out var updateActionPtr, "Tooltips - Actions")) {
unsafe {
this.ActionGenerateTooltipHook = new Hook<ActionUpdateTooltipDelegate>(updateActionPtr, this.ActionUpdateTooltipDetour);
}
this.ActionGenerateTooltipHook.Enable();
}
}
}
/// <inheritdoc />
public void Dispose() {
this.ActionGenerateTooltipHook?.Dispose();
this.ItemUpdateTooltipHook?.Dispose();
}
/// <inheritdoc />
public void Dispose() {
this.ActionGenerateTooltipHook?.Dispose();
this.ItemUpdateTooltipHook?.Dispose();
}
private unsafe ulong ItemUpdateTooltipDetour(IntPtr agent, int** numberArrayData, byte*** stringArrayData, float a4) {
var ret = this.ItemUpdateTooltipHook!.Original(agent, numberArrayData, stringArrayData, a4);
private unsafe byte ItemUpdateTooltipDetour(IntPtr agent, int** numberArrayData, byte*** stringArrayData, float a4) {
var ret = this.ItemUpdateTooltipHook!.Original(agent, numberArrayData, stringArrayData, a4);
if (ret > 0) {
try {
this.ItemUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in item tooltip detour");
}
}
return ret;
}
private unsafe void ItemUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.ItemTooltip = new ItemTooltip(this.SadSetString!, stringArrayData, numberArrayData);
if (ret > 0) {
try {
this.ItemUpdateTooltipDetourInner(numberArrayData, stringArrayData);
this.OnItemTooltip?.Invoke(this.ItemTooltip, this.GameGui.HoveredItem);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in item tooltip detour");
Logger.LogError(ex, "Exception in OnItemTooltip event");
}
}
return ret;
}
private unsafe void ActionUpdateTooltipDetour(IntPtr agent, int** numberArrayData, byte*** stringArrayData) {
var flag = *(byte*) (agent + AgentActionDetailUpdateFlagOffset);
this.ActionGenerateTooltipHook!.Original(agent, numberArrayData, stringArrayData);
private unsafe void ItemUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.ItemTooltip = new ItemTooltip(this.SadSetString!, stringArrayData, numberArrayData);
if (flag == 0) {
return;
}
try {
this.OnItemTooltip?.Invoke(this.ItemTooltip, this.GameGui.HoveredItem);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in OnItemTooltip event");
}
}
private unsafe void ActionUpdateTooltipDetour(IntPtr agent, int** numberArrayData, byte*** stringArrayData) {
var flag = *(byte*) (agent + AgentActionDetailUpdateFlagOffset);
this.ActionGenerateTooltipHook!.Original(agent, numberArrayData, stringArrayData);
if (flag == 0) {
return;
try {
this.ActionUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in action tooltip detour");
}
}
try {
this.ActionUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in action tooltip detour");
}
}
private unsafe void ActionUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.ActionTooltip = new ActionTooltip(this.SadSetString!, stringArrayData, numberArrayData);
private unsafe void ActionUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.ActionTooltip = new ActionTooltip(this.SadSetString!, stringArrayData, numberArrayData);
try {
this.OnActionTooltip?.Invoke(this.ActionTooltip, this.GameGui.HoveredAction);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in OnActionTooltip event");
try {
this.OnActionTooltip?.Invoke(this.ActionTooltip, this.GameGui.HoveredAction);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OnActionTooltip event");
}
}
}
}

View File

@ -2,58 +2,58 @@
using System.Runtime.InteropServices;
using Dalamud.Game;
namespace XivCommon.Functions;
internal class UiAlloc {
private static class Signatures {
internal const string GameAlloc = "E8 ?? ?? ?? ?? 49 83 CC FF 4C 8B F0";
internal const string GameFree = "E8 ?? ?? ?? ?? 4C 89 7B 60";
internal const string GetGameAllocator = "E8 ?? ?? ?? ?? 8B 75 08";
}
private delegate IntPtr GameAllocDelegate(ulong size, IntPtr unk, IntPtr allocator, IntPtr alignment);
private readonly GameAllocDelegate? _gameAlloc;
private delegate IntPtr GameFreeDelegate(IntPtr a1);
private readonly GameFreeDelegate? _gameFree;
private delegate IntPtr GetGameAllocatorDelegate();
private readonly GetGameAllocatorDelegate? _getGameAllocator;
internal UiAlloc(ISigScanner scanner) {
if (scanner.TryScanText(Signatures.GameAlloc, out var gameAllocPtr, "UiAlloc (GameAlloc)")) {
this._gameAlloc = Marshal.GetDelegateForFunctionPointer<GameAllocDelegate>(gameAllocPtr);
} else {
return;
namespace XivCommon.Functions {
internal class UiAlloc {
private static class Signatures {
internal const string GameAlloc = "E8 ?? ?? ?? ?? 45 8D 67 23";
internal const string GameFree = "E8 ?? ?? ?? ?? 4C 89 7B 60";
internal const string GetGameAllocator = "E8 ?? ?? ?? ?? 8B 75 08";
}
if (scanner.TryScanText(Signatures.GameFree, out var gameFreePtr, "UiAlloc (GameFree)")) {
this._gameFree = Marshal.GetDelegateForFunctionPointer<GameFreeDelegate>(gameFreePtr);
} else {
return;
private delegate IntPtr GameAllocDelegate(ulong size, IntPtr unk, IntPtr allocator, IntPtr alignment);
private readonly GameAllocDelegate? _gameAlloc;
private delegate IntPtr GameFreeDelegate(IntPtr a1);
private readonly GameFreeDelegate? _gameFree;
private delegate IntPtr GetGameAllocatorDelegate();
private readonly GetGameAllocatorDelegate? _getGameAllocator;
internal UiAlloc(SigScanner scanner) {
if (scanner.TryScanText(Signatures.GameAlloc, out var gameAllocPtr, "UiAlloc (GameAlloc)")) {
this._gameAlloc = Marshal.GetDelegateForFunctionPointer<GameAllocDelegate>(gameAllocPtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.GameFree, out var gameFreePtr, "UiAlloc (GameFree)")) {
this._gameFree = Marshal.GetDelegateForFunctionPointer<GameFreeDelegate>(gameFreePtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.GetGameAllocator, out var getAllocatorPtr, "UiAlloc (GetGameAllocator)")) {
this._getGameAllocator = Marshal.GetDelegateForFunctionPointer<GetGameAllocatorDelegate>(getAllocatorPtr);
}
}
if (scanner.TryScanText(Signatures.GetGameAllocator, out var getAllocatorPtr, "UiAlloc (GetGameAllocator)")) {
this._getGameAllocator = Marshal.GetDelegateForFunctionPointer<GetGameAllocatorDelegate>(getAllocatorPtr);
}
}
internal IntPtr Alloc(ulong size) {
if (this._getGameAllocator == null || this._gameAlloc == null) {
throw new InvalidOperationException();
}
internal IntPtr Alloc(ulong size) {
if (this._getGameAllocator == null || this._gameAlloc == null) {
throw new InvalidOperationException();
return this._gameAlloc(size, IntPtr.Zero, this._getGameAllocator(), IntPtr.Zero);
}
return this._gameAlloc(size, IntPtr.Zero, this._getGameAllocator(), IntPtr.Zero);
}
internal void Free(IntPtr ptr) {
if (this._gameFree == null) {
throw new InvalidOperationException();
}
internal void Free(IntPtr ptr) {
if (this._gameFree == null) {
throw new InvalidOperationException();
this._gameFree(ptr);
}
this._gameFree(ptr);
}
}

View File

@ -1,182 +1,185 @@
using System;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.PartyFinder;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using XivCommon.Functions;
using XivCommon.Functions.ContextMenu;
using XivCommon.Functions.FriendList;
using XivCommon.Functions.Housing;
using XivCommon.Functions.NamePlates;
using XivCommon.Functions.Tooltips;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon;
/// <summary>
/// A class containing game functions
/// </summary>
public class GameFunctions : IDisposable {
private IGameGui GameGui { get; }
private IFramework Framework { get; }
internal UiAlloc UiAlloc { get; }
namespace XivCommon {
/// <summary>
/// Chat functions
/// A class containing game functions
/// </summary>
public Chat Chat { get; }
public class GameFunctions : IDisposable {
private GameGui GameGui { get; }
/// <summary>
/// Party Finder functions and events
/// </summary>
public PartyFinder PartyFinder { get; }
private Dalamud.Game.Framework Framework { get; }
/// <summary>
/// BattleTalk functions and events
/// </summary>
public BattleTalk BattleTalk { get; }
internal UiAlloc UiAlloc { get; }
/// <summary>
/// Examine functions
/// </summary>
public Examine Examine { get; }
/// <summary>
/// Chat functions
/// </summary>
public Chat Chat { get; }
/// <summary>
/// Talk events
/// </summary>
public Talk Talk { get; }
/// <summary>
/// Party Finder functions and events
/// </summary>
public PartyFinder PartyFinder { get; }
/// <summary>
/// Chat bubble functions and events
/// </summary>
public ChatBubbles ChatBubbles { get; }
/// <summary>
/// BattleTalk functions and events
/// </summary>
public BattleTalk BattleTalk { get; }
/// <summary>
/// Tooltip events
/// </summary>
public Tooltips Tooltips { get; }
/// <summary>
/// Examine functions
/// </summary>
public Examine Examine { get; }
/// <summary>
/// Name plate tools and events
/// </summary>
public NamePlates NamePlates { get; }
/// <summary>
/// Talk events
/// </summary>
public Talk Talk { get; }
/// <summary>
/// Duty Finder functions
/// </summary>
public DutyFinder DutyFinder { get; }
/// <summary>
/// Chat bubble functions and events
/// </summary>
public ChatBubbles ChatBubbles { get; }
/// <summary>
/// Friend list functions
/// </summary>
public FriendList FriendList { get; }
/// <summary>
/// Context menu functions
/// </summary>
public ContextMenu ContextMenu { get; }
/// <summary>
/// Journal functions
/// </summary>
public Journal Journal { get; }
/// <summary>
/// Tooltip events
/// </summary>
public Tooltips Tooltips { get; }
/// <summary>
/// Housing functions
/// </summary>
public Housing Housing { get; }
/// <summary>
/// Name plate tools and events
/// </summary>
public NamePlates NamePlates { get; }
internal GameFunctions(DalamudPluginInterface @interface, Hooks hooks) {
var services = @interface.Create<Services>();
if (services == null) {
throw new Exception("could not create services");
/// <summary>
/// Duty Finder functions
/// </summary>
public DutyFinder DutyFinder { get; }
/// <summary>
/// Friend list functions
/// </summary>
public FriendList FriendList { get; }
/// <summary>
/// Journal functions
/// </summary>
public Journal Journal { get; }
/// <summary>
/// Housing functions
/// </summary>
public Housing Housing { get; }
internal GameFunctions(Hooks hooks) {
this.Framework = Util.GetService<Dalamud.Game.Framework>();
this.GameGui = Util.GetService<GameGui>();
var clientState = Util.GetService<ClientState>();
var objectTable = Util.GetService<ObjectTable>();
var partyFinderGui = Util.GetService<PartyFinderGui>();
var scanner = Util.GetService<SigScanner>();
this.UiAlloc = new UiAlloc(scanner);
this.Chat = new Chat(this, scanner);
this.PartyFinder = new PartyFinder(scanner, partyFinderGui, hooks);
this.BattleTalk = new BattleTalk(this, scanner, hooks.HasFlag(Hooks.BattleTalk));
this.Examine = new Examine(this, scanner);
this.Talk = new Talk(scanner, hooks.HasFlag(Hooks.Talk));
this.ChatBubbles = new ChatBubbles(objectTable, scanner, hooks.HasFlag(Hooks.ChatBubbles));
this.ContextMenu = new ContextMenu(this, scanner, clientState.ClientLanguage, hooks);
this.Tooltips = new Tooltips(scanner, this.GameGui, hooks.HasFlag(Hooks.Tooltips));
this.NamePlates = new NamePlates(this, scanner, hooks.HasFlag(Hooks.NamePlates));
this.DutyFinder = new DutyFinder(this, scanner);
this.Journal = new Journal(this, scanner);
this.FriendList = new FriendList(this);
this.Housing = new Housing(scanner);
}
Logger.Log = services.Log;
/// <inheritdoc />
public void Dispose() {
this.NamePlates.Dispose();
this.Tooltips.Dispose();
this.ContextMenu.Dispose();
this.ChatBubbles.Dispose();
this.Talk.Dispose();
this.BattleTalk.Dispose();
this.PartyFinder.Dispose();
}
this.Framework = services.Framework;
this.GameGui = services.GameGui;
/// <summary>
/// Convenience method to get a pointer to <see cref="Framework"/>.
/// </summary>
/// <returns>pointer to struct</returns>
public unsafe Framework* GetFramework() {
return (Framework*) this.Framework.Address.BaseAddress;
}
var interop = services.GameInteropProvider;
var objectTable = services.ObjectTable;
var partyFinderGui = services.PartyFinderGui;
var scanner = services.SigScanner;
/// <summary>
/// Gets the pointer to the UI module
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use GetFramework()->GetUiModule()")]
public unsafe IntPtr GetUiModule() {
return (IntPtr) this.GetFramework()->GetUiModule();
}
this.UiAlloc = new UiAlloc(scanner);
this.Chat = new Chat(scanner);
this.PartyFinder = new PartyFinder(scanner, partyFinderGui, interop, hooks);
this.BattleTalk = new BattleTalk(interop, hooks.HasFlag(Hooks.BattleTalk));
this.Examine = new Examine(scanner);
this.Talk = new Talk(scanner, interop, hooks.HasFlag(Hooks.Talk));
this.ChatBubbles = new ChatBubbles(objectTable, scanner, interop, hooks.HasFlag(Hooks.ChatBubbles));
this.Tooltips = new Tooltips(scanner, this.GameGui, interop, hooks.HasFlag(Hooks.Tooltips));
this.NamePlates = new NamePlates(this, scanner, interop, hooks.HasFlag(Hooks.NamePlates));
this.DutyFinder = new DutyFinder(scanner);
this.Journal = new Journal(scanner);
this.FriendList = new FriendList();
this.Housing = new Housing(scanner);
}
/// <summary>
/// Gets the pointer to the RaptureAtkModule
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use GetFramework()->GetUiModule()->GetRaptureAtkModule()")]
public unsafe IntPtr GetAtkModule() {
return (IntPtr) this.GetFramework()->GetUiModule()->GetRaptureAtkModule();
}
/// <inheritdoc />
public void Dispose() {
this.NamePlates.Dispose();
this.Tooltips.Dispose();
this.ChatBubbles.Dispose();
this.Talk.Dispose();
this.BattleTalk.Dispose();
this.PartyFinder.Dispose();
}
/// <summary>
/// Gets the pointer to the agent module
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use GetFramework()->GetUiModule()->GetAgentModule()")]
public unsafe IntPtr GetAgentModule() {
return (IntPtr) this.GetFramework()->GetUiModule()->GetAgentModule();
}
/// <summary>
/// Convenience method to get a pointer to <see cref="Framework"/>.
/// </summary>
/// <returns>pointer to struct</returns>
[Obsolete("Use Framework.Instance()")]
public unsafe Framework* GetFramework() {
return FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
}
/// <summary>
/// Gets the pointer to an agent from its internal ID.
/// </summary>
/// <param name="id">internal id of agent</param>
/// <returns>Pointer</returns>
/// <exception cref="InvalidOperationException">if the signature for the function could not be found</exception>
[Obsolete("Use GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId)")]
public unsafe IntPtr GetAgentByInternalId(uint id) {
return (IntPtr) this.GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId((AgentId) id);
}
/// <summary>
/// Gets the pointer to the UI module
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()")]
public unsafe IntPtr GetUiModule() {
return (IntPtr) this.GetFramework()->GetUiModule();
}
/// <summary>
/// Gets the pointer to the RaptureAtkModule
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetRaptureAtkModule()")]
public unsafe IntPtr GetAtkModule() {
return (IntPtr) this.GetFramework()->GetUiModule()->GetRaptureAtkModule();
}
/// <summary>
/// Gets the pointer to the agent module
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetAgentModule()")]
public unsafe IntPtr GetAgentModule() {
return (IntPtr) this.GetFramework()->GetUiModule()->GetAgentModule();
}
/// <summary>
/// Gets the pointer to an agent from its internal ID.
/// </summary>
/// <param name="id">internal id of agent</param>
/// <returns>Pointer</returns>
/// <exception cref="InvalidOperationException">if the signature for the function could not be found</exception>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId)")]
public unsafe IntPtr GetAgentByInternalId(uint id) {
return (IntPtr) this.GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId((AgentId) id);
}
/// <summary>
/// Gets the pointer to the AtkStage singleton
/// </summary>
/// <returns>Pointer</returns>
/// <exception cref="InvalidOperationException">if the signature for the function could not be found</exception>
[Obsolete("Use AtkStage.GetSingleton()")]
public unsafe IntPtr GetAtkStageSingleton() {
return (IntPtr) AtkStage.GetSingleton();
/// <summary>
/// Gets the pointer to the AtkStage singleton
/// </summary>
/// <returns>Pointer</returns>
/// <exception cref="InvalidOperationException">if the signature for the function could not be found</exception>
[Obsolete("Use AtkStage.GetSingleton()")]
public unsafe IntPtr GetAtkStageSingleton() {
return (IntPtr) AtkStage.GetSingleton();
}
}
}

View File

@ -1,74 +1,79 @@
using System;
namespace XivCommon;
/// <summary>
/// Flags for which hooks to use
/// </summary>
[Flags]
public enum Hooks {
namespace XivCommon {
/// <summary>
/// No hook.
///
/// This flag is used to disable all hooking.
/// Flags for which hooks to use
/// </summary>
None = 0,
[Flags]
public enum Hooks {
/// <summary>
/// No hook.
///
/// This flag is used to disable all hooking.
/// </summary>
None = 0,
/// <summary>
/// The Tooltips hooks.
///
/// This hook is used in order to enable the tooltip events.
/// </summary>
Tooltips = 1 << 0,
/// <summary>
/// The Tooltips hooks.
///
/// This hook is used in order to enable the tooltip events.
/// </summary>
Tooltips = 1 << 0,
/// <summary>
/// The BattleTalk hook.
///
/// This hook is used in order to enable the BattleTalk events.
/// </summary>
BattleTalk = 1 << 1,
/// <summary>
/// The BattleTalk hook.
///
/// This hook is used in order to enable the BattleTalk events.
/// </summary>
BattleTalk = 1 << 1,
/// <summary>
/// Hooks used for refreshing Party Finder listings.
/// </summary>
PartyFinderListings = 1 << 2,
/// <summary>
/// Hooks used for refreshing Party Finder listings.
/// </summary>
PartyFinderListings = 1 << 2,
/// <summary>
/// Hooks used for Party Finder join events.
/// </summary>
PartyFinderJoins = 1 << 3,
/// <summary>
/// Hooks used for Party Finder join events.
/// </summary>
PartyFinderJoins = 1 << 3,
/// <summary>
/// All Party Finder hooks.
///
/// This hook is used in order to enable all Party Finder functions.
/// </summary>
PartyFinder = PartyFinderListings | PartyFinderJoins,
/// <summary>
/// All Party Finder hooks.
///
/// This hook is used in order to enable all Party Finder functions.
/// </summary>
PartyFinder = PartyFinderListings | PartyFinderJoins,
/// <summary>
/// The Talk hooks.
///
/// This hook is used in order to enable the Talk events.
/// </summary>
Talk = 1 << 4,
/// <summary>
/// The Talk hooks.
///
/// This hook is used in order to enable the Talk events.
/// </summary>
Talk = 1 << 4,
/// <summary>
/// The chat bubbles hooks.
///
/// This hook is used in order to enable the chat bubbles events.
/// </summary>
ChatBubbles = 1 << 5,
/// <summary>
/// The chat bubbles hooks.
///
/// This hook is used in order to enable the chat bubbles events.
/// </summary>
ChatBubbles = 1 << 5,
// 1 << 6 used to be ContextMenu
/// <summary>
/// The context menu hooks.
///
/// This hook is used in order to enable context menu functions.
/// </summary>
ContextMenu = 1 << 6,
/// <summary>
/// The name plate hooks.
///
/// This hook is used in order to enable name plate functions.
/// </summary>
NamePlates = 1 << 7,
/// <summary>
/// The name plate hooks.
///
/// This hook is used in order to enable name plate functions.
/// </summary>
NamePlates = 1 << 7,
}
internal static class HooksExt {
internal const Hooks DefaultHooks = Hooks.None;
}
}
internal static class HooksExt {
internal const Hooks DefaultHooks = Hooks.None;
}

27
XivCommon/Logger.cs Normal file → Executable file
View File

@ -1,7 +1,26 @@
using Dalamud.Plugin.Services;
using System;
using Dalamud.Logging;
namespace XivCommon;
namespace XivCommon {
internal static class Logger {
private static string Format(string msg) {
return $"[XIVCommon] {msg}";
}
internal static class Logger {
internal static IPluginLog Log { get; set; } = null!;
internal static void Log(string msg) {
PluginLog.Log(Format(msg));
}
internal static void LogWarning(string msg) {
PluginLog.LogWarning(Format(msg));
}
internal static void LogError(string msg) {
PluginLog.LogError(Format(msg));
}
internal static void LogError(Exception ex, string msg) {
PluginLog.LogError(ex, Format(msg));
}
}
}

View File

@ -1,28 +0,0 @@
using Dalamud.Game;
using Dalamud.IoC;
using Dalamud.Plugin.Services;
namespace XivCommon;
internal class Services {
[PluginService]
internal IPluginLog Log { get; private set; }
[PluginService]
internal IFramework Framework { get; private set; }
[PluginService]
internal IGameGui GameGui { get; private set; }
[PluginService]
internal IGameInteropProvider GameInteropProvider { get; private set; }
[PluginService]
internal IObjectTable ObjectTable { get; private set; }
[PluginService]
internal IPartyFinderGui PartyFinderGui { get; private set; }
[PluginService]
internal ISigScanner SigScanner { get; private set; }
}

View File

@ -2,28 +2,28 @@
using System.Collections.Generic;
using Dalamud.Game;
namespace XivCommon;
namespace XivCommon {
internal static class SigScannerExt {
/// <summary>
/// Scan for a signature in memory.
/// </summary>
/// <param name="scanner">SigScanner to use for scanning</param>
/// <param name="sig">signature to search for</param>
/// <param name="result">pointer where signature was found or <see cref="IntPtr.Zero"/> if not found</param>
/// <param name="name">name of this signature - if specified, a warning will be printed if the signature could not be found</param>
/// <returns>true if signature was found</returns>
internal static bool TryScanText(this SigScanner scanner, string sig, out IntPtr result, string? name = null) {
result = IntPtr.Zero;
try {
result = scanner.ScanText(sig);
return true;
} catch (KeyNotFoundException) {
if (name != null) {
Util.PrintMissingSig(name);
}
internal static class SigScannerExt {
/// <summary>
/// Scan for a signature in memory.
/// </summary>
/// <param name="scanner">SigScanner to use for scanning</param>
/// <param name="sig">signature to search for</param>
/// <param name="result">pointer where signature was found or <see cref="IntPtr.Zero"/> if not found</param>
/// <param name="name">name of this signature - if specified, a warning will be printed if the signature could not be found</param>
/// <returns>true if signature was found</returns>
internal static bool TryScanText(this ISigScanner scanner, string sig, out IntPtr result, string? name = null) {
result = IntPtr.Zero;
try {
result = scanner.ScanText(sig);
return true;
} catch (KeyNotFoundException) {
if (name != null) {
Util.PrintMissingSig(name);
return false;
}
return false;
}
}
}

View File

@ -1,55 +1,63 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin;
namespace XivCommon;
namespace XivCommon {
internal static class Util {
internal static byte[] Terminate(this byte[] array) {
var terminated = new byte[array.Length + 1];
Array.Copy(array, terminated, array.Length);
terminated[^1] = 0;
internal static class Util {
internal static byte[] Terminate(this byte[] array) {
var terminated = new byte[array.Length + 1];
Array.Copy(array, terminated, array.Length);
terminated[^1] = 0;
return terminated;
}
internal static unsafe byte[] ReadTerminated(IntPtr memory) {
if (memory == IntPtr.Zero) {
return Array.Empty<byte>();
return terminated;
}
var buf = new List<byte>();
internal static unsafe byte[] ReadTerminated(IntPtr memory) {
if (memory == IntPtr.Zero) {
return Array.Empty<byte>();
}
var ptr = (byte*) memory;
while (*ptr != 0) {
buf.Add(*ptr);
ptr += 1;
var buf = new List<byte>();
var ptr = (byte*) memory;
while (*ptr != 0) {
buf.Add(*ptr);
ptr += 1;
}
return buf.ToArray();
}
return buf.ToArray();
}
internal static SeString ReadSeString(IntPtr memory) {
var terminated = ReadTerminated(memory);
return SeString.Parse(terminated);
}
internal static void PrintMissingSig(string name) {
Logger.Log.Warning($"Could not find signature for {name}. This functionality will be disabled.");
}
internal static unsafe IntPtr FollowPointerChain(IntPtr start, IEnumerable<int> offsets) {
if (start == IntPtr.Zero) {
return IntPtr.Zero;
internal static SeString ReadSeString(IntPtr memory) {
var terminated = ReadTerminated(memory);
return SeString.Parse(terminated);
}
foreach (var offset in offsets) {
start = *(IntPtr*) (start + offset);
internal static void PrintMissingSig(string name) {
Logger.LogWarning($"Could not find signature for {name}. This functionality will be disabled.");
}
internal static T GetService<T>() {
var service = typeof(IDalamudPlugin).Assembly.GetType("Dalamud.Service`1")!.MakeGenericType(typeof(T));
var get = service.GetMethod("Get", BindingFlags.Public | BindingFlags.Static)!;
return (T) get.Invoke(null, null)!;
}
internal static unsafe IntPtr FollowPointerChain(IntPtr start, IEnumerable<int> offsets) {
if (start == IntPtr.Zero) {
return IntPtr.Zero;
}
}
return start;
foreach (var offset in offsets) {
start = *(IntPtr*) (start + offset);
if (start == IntPtr.Zero) {
return IntPtr.Zero;
}
}
return start;
}
}
}
}

View File

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7-windows</TargetFramework>
<TargetFramework>net5-windows</TargetFramework>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<Version>9.0.0</Version>
<Version>3.2.0</Version>
<DebugType>full</DebugType>
</PropertyGroup>
@ -13,8 +13,8 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Title>XivCommon</Title>
<Authors>anna</Authors>
<RepositoryUrl>https://git.anna.lgbt/anna/XivCommon</RepositoryUrl>
<Authors>ascclemens</Authors>
<RepositoryUrl>https://git.sr.ht/~jkcclemens/XivCommon</RepositoryUrl>
<Description>A set of common functions, hooks, and events not included in Dalamud.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
@ -23,8 +23,8 @@
<Dalamud>$(AppData)\XIVLauncher\addon\Hooks\dev</Dalamud>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<Dalamud>$(DALAMUD_HOME)</Dalamud>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
<Dalamud>$(HOME)/games/final-fantasy-xiv-online/drive_c/users/$(USER)/AppData/Roaming/XIVLauncher/addon/Hooks/dev</Dalamud>
</PropertyGroup>
<PropertyGroup Condition="'$(IsCI)' == 'true'">

View File

@ -1,32 +1,31 @@
using System;
using Dalamud.Plugin;
namespace XivCommon;
/// <summary>
/// A base class for accessing XivCommon functionality.
/// </summary>
public class XivCommonBase : IDisposable {
namespace XivCommon {
/// <summary>
/// Game functions and events
/// A base class for accessing XivCommon functionality.
/// </summary>
public GameFunctions Functions { get; }
public class XivCommonBase : IDisposable {
/// <summary>
/// Game functions and events
/// </summary>
public GameFunctions Functions { get; }
/// <summary>
/// <para>
/// Construct a new XivCommon base.
/// </para>
/// <para>
/// This will automatically enable hooks based on the hooks parameter.
/// </para>
/// </summary>
/// <param name="hooks">Flags indicating which hooks to enable</param>
public XivCommonBase(DalamudPluginInterface @interface, Hooks hooks = HooksExt.DefaultHooks) {
this.Functions = new GameFunctions(@interface, hooks);
}
/// <summary>
/// <para>
/// Construct a new XivCommon base.
/// </para>
/// <para>
/// This will automatically enable hooks based on the hooks parameter.
/// </para>
/// </summary>
/// <param name="hooks">Flags indicating which hooks to enable</param>
public XivCommonBase(Hooks hooks = HooksExt.DefaultHooks) {
this.Functions = new GameFunctions(hooks);
}
/// <inheritdoc />
public void Dispose() {
this.Functions.Dispose();
/// <inheritdoc />
public void Dispose() {
this.Functions.Dispose();
}
}
}
}