Compare commits

...

2 Commits

Author SHA1 Message Date
Anna 6df71875cc
chore: bump version to 8.0.0 2023-10-03 02:31:46 -04:00
Anna f22702f251
refactor: update for api 9 2023-10-03 02:30:28 -04:00
30 changed files with 2244 additions and 2258 deletions

View File

@ -2,181 +2,182 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions { namespace XivCommon.Functions;
/// <summary>
/// The class containing BattleTalk functionality
/// </summary>
public class BattleTalk : IDisposable {
private bool HookEnabled { get; }
/// <summary> /// <summary>
/// The class containing BattleTalk functionality /// The delegate for BattleTalk events.
/// </summary> /// </summary>
public class BattleTalk : IDisposable { public delegate void BattleTalkEventDelegate(ref SeString sender, ref SeString message, ref BattleTalkOptions options, ref bool isHandled);
private bool HookEnabled { get; }
/// <summary> /// <summary>
/// The delegate for BattleTalk events. /// <para>
/// </summary> /// The event that is fired when a BattleTalk window is shown.
public delegate void BattleTalkEventDelegate(ref SeString sender, ref SeString message, ref BattleTalkOptions options, ref bool isHandled); /// </para>
/// <para>
/// Requires the <see cref="Hooks.BattleTalk"/> hook to be enabled.
/// </para>
/// </summary>
public event BattleTalkEventDelegate? OnBattleTalk;
/// <summary> private delegate byte AddBattleTalkDelegate(IntPtr uiModule, IntPtr sender, IntPtr message, float duration, byte style);
/// <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 delegate byte AddBattleTalkDelegate(IntPtr uiModule, IntPtr sender, IntPtr message, float duration, byte style); private AddBattleTalkDelegate? AddBattleTalk { get; }
private Hook<AddBattleTalkDelegate>? AddBattleTalkHook { get; }
private AddBattleTalkDelegate? AddBattleTalk { get; } internal unsafe BattleTalk(IGameInteropProvider interop, bool hook) {
private Hook<AddBattleTalkDelegate>? AddBattleTalkHook { get; } this.HookEnabled = hook;
internal unsafe BattleTalk(bool hook) { var addBattleTalkPtr = (IntPtr) Framework.Instance()->GetUiModule()->VTable->ShowBattleTalk;
this.HookEnabled = hook; if (addBattleTalkPtr != IntPtr.Zero) {
this.AddBattleTalk = Marshal.GetDelegateForFunctionPointer<AddBattleTalkDelegate>(addBattleTalkPtr);
var addBattleTalkPtr = (IntPtr) Framework.Instance()->GetUiModule()->VTable->ShowBattleTalk; if (this.HookEnabled) {
if (addBattleTalkPtr != IntPtr.Zero) { this.AddBattleTalkHook = interop.HookFromAddress<AddBattleTalkDelegate>(addBattleTalkPtr, this.AddBattleTalkDetour);
this.AddBattleTalk = Marshal.GetDelegateForFunctionPointer<AddBattleTalkDelegate>(addBattleTalkPtr); this.AddBattleTalkHook.Enable();
if (this.HookEnabled) {
this.AddBattleTalkHook = Hook<AddBattleTalkDelegate>.FromAddress(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) 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> /// <inheritdoc />
/// Options for displaying a BattleTalk window. public void Dispose() {
/// </summary> this.AddBattleTalkHook?.Dispose();
public class BattleTalkOptions { }
/// <summary>
/// Duration to display the window, in seconds.
/// </summary>
public float Duration { get; set; } = 5f;
/// <summary> private byte AddBattleTalkDetour(IntPtr uiModule, IntPtr senderPtr, IntPtr messagePtr, float duration, byte style) {
/// The style of the window. if (this.OnBattleTalk == null) {
/// </summary> goto Return;
public BattleTalkStyle Style { get; set; } = BattleTalkStyle.Normal; }
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> /// <summary>
/// BattleTalk window styles. /// Show a BattleTalk window with the given options.
/// </summary> /// </summary>
public enum BattleTalkStyle : byte { /// <param name="sender">The name to attribute to the message</param>
/// <summary> /// <param name="message">The message to show in the window</param>
/// A normal battle talk window with a white background. /// <param name="options">Optional options for the window</param>
/// </summary> /// <exception cref="ArgumentException">If sender or message are empty</exception>
Normal = 0, /// <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);
}
/// <summary> private unsafe void Show(byte[] sender, byte[] message, BattleTalkOptions? options) {
/// A battle talk window with a blue background and styled edges. if (sender.Length == 0) {
/// </summary> throw new ArgumentException("sender cannot be empty", nameof(sender));
Aetherial = 6, }
/// <summary> if (message.Length == 0) {
/// A battle talk window styled similarly to a system text message (black background). throw new ArgumentException("message cannot be empty", nameof(message));
/// </summary> }
System = 7,
/// <summary> if (this.AddBattleTalk == null) {
/// <para> throw new InvalidOperationException("Signature for battle talk could not be found");
/// A battle talk window with a blue, computer-y background. }
/// </para>
/// <para> options ??= new BattleTalkOptions();
/// Used by the Ultima Weapons (Ruby, Emerald, etc.).
/// </para> var uiModule = (IntPtr) Framework.Instance()->GetUiModule();
/// </summary>
Blue = 9, 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>
/// 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

@ -7,151 +7,151 @@ using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions { namespace XivCommon.Functions;
/// <summary>
/// A class containing chat functionality /// <summary>
/// </summary> /// A class containing chat functionality
public class Chat { /// </summary>
private static class Signatures { public class Chat {
internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9"; private static class Signatures {
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D"; 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);
} }
private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4); unsafe {
if (scanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr, "string sanitiser")) {
private ProcessChatBoxDelegate? ProcessChatBox { get; } this._sanitiseString = (delegate* unmanaged<Utf8String*, int, IntPtr, void>) sanitisePtr;
private readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString = null!;
internal Chat(SigScanner scanner) {
if (scanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr, "chat sending")) {
this.ProcessChatBox = Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(processChatBoxPtr);
}
unsafe {
if (scanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr, "string sanitiser")) {
this._sanitiseString = (delegate* unmanaged<Utf8String*, int, IntPtr, void>) sanitisePtr;
}
}
}
/// <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);
} }
} }
} }
/// <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;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking; 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> /// <summary>
/// Class containing chat bubble events and functions /// The delegate for chat bubble events.
/// </summary> /// </summary>
public class ChatBubbles : IDisposable { public delegate void OnChatBubbleDelegate(ref GameObject @object, ref SeString text);
private static class Signatures {
internal const string ChatBubbleOpen = "E8 ?? ?? ?? ?? C7 43 ?? ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8"; /// <summary>
internal const string ChatBubbleUpdate = "48 85 D2 0F 84 ?? ?? ?? ?? 48 89 5C 24 ?? 57 48 83 EC 20 8B 41 0C"; /// 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;
} }
private ObjectTable ObjectTable { get; } if (scanner.TryScanText(Signatures.ChatBubbleOpen, out var openPtr, "chat bubbles open")) {
this.OpenChatBubbleHook = interop.HookFromAddress<OpenChatBubbleDelegate>(openPtr, this.OpenChatBubbleDetour);
private delegate void OpenChatBubbleDelegate(IntPtr manager, IntPtr @object, IntPtr text, byte a4); this.OpenChatBubbleHook.Enable();
private delegate void UpdateChatBubbleDelegate(IntPtr bubblePtr, IntPtr @object);
private Hook<OpenChatBubbleDelegate>? OpenChatBubbleHook { get; }
private Hook<UpdateChatBubbleDelegate>? UpdateChatBubbleHook { get; }
/// <summary>
/// The delegate for chat bubble events.
/// </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(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 = Hook<OpenChatBubbleDelegate>.FromAddress(openPtr, this.OpenChatBubbleDetour);
this.OpenChatBubbleHook.Enable();
}
if (scanner.TryScanText(Signatures.ChatBubbleUpdate, out var updatePtr, "chat bubbles update")) {
this.UpdateChatBubbleHook = Hook<UpdateChatBubbleDelegate>.FromAddress(updatePtr + 9, this.UpdateChatBubbleDetour);
this.UpdateChatBubbleHook.Enable();
}
} }
/// <inheritdoc /> if (scanner.TryScanText(Signatures.ChatBubbleUpdate, out var updatePtr, "chat bubbles update")) {
public void Dispose() { this.UpdateChatBubbleHook = interop.HookFromAddress<UpdateChatBubbleDelegate>(updatePtr + 9, this.UpdateChatBubbleDetour);
this.OpenChatBubbleHook?.Dispose(); this.UpdateChatBubbleHook.Enable();
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);
} }
} }
[StructLayout(LayoutKind.Explicit, Size = 0x80)] /// <inheritdoc />
internal unsafe struct ChatBubble { public void Dispose() {
[FieldOffset(0x0)] this.OpenChatBubbleHook?.Dispose();
internal readonly uint Id; this.UpdateChatBubbleHook?.Dispose();
[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 { private void OpenChatBubbleDetour(IntPtr manager, IntPtr @object, IntPtr text, byte a4) {
GetData = 0, try {
On = 1, this.OpenChatBubbleDetourInner(manager, @object, text, a4);
Init = 2, } catch (Exception ex) {
Off = 3, Logger.Log.Error(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.Log.Error(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.Log.Error(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.Log.Error(ex, "Exception in chat bubble update event");
}
this.UpdateChatBubbleHook!.Original(bubblePtr, @object.Address);
} }
} }
[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

@ -5,77 +5,77 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions { namespace XivCommon.Functions;
/// <summary>
/// Duty Finder functions /// <summary>
/// </summary> /// Duty Finder functions
public class DutyFinder { /// </summary>
private static class Signatures { public class DutyFinder {
internal const string OpenRegularDuty = "48 89 6C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B F9 41 0F B6 E8"; private static class Signatures {
internal const string OpenRoulette = "E9 ?? ?? ?? ?? 8B 93 ?? ?? ?? ?? 48 83 C4 20"; 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);
} }
private delegate IntPtr OpenDutyDelegate(IntPtr agent, uint contentFinderCondition, byte a3); if (scanner.TryScanText(Signatures.OpenRoulette, out var openRoulettePtr, "Duty Finder (open roulette)")) {
this._openRoulette = Marshal.GetDelegateForFunctionPointer<OpenRouletteDelegate>(openRoulettePtr);
private delegate IntPtr OpenRouletteDelegate(IntPtr agent, byte roulette, byte a3);
private readonly OpenDutyDelegate? _openDuty;
private readonly OpenRouletteDelegate? _openRoulette;
internal DutyFinder(SigScanner scanner) {
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);
}
}
/// <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);
}
/// <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");
}
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");
}
var agent = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ContentsFinder);
this._openRoulette(agent, roulette, 0);
} }
} }
/// <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);
}
/// <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");
}
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");
}
var agent = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ContentsFinder);
this._openRoulette(agent, roulette, 0);
}
} }

View File

@ -4,64 +4,64 @@ using Dalamud.Game;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions { 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); /// <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 RequestCharInfoDelegate? RequestCharacterInfo { get; } private delegate long RequestCharInfoDelegate(IntPtr ptr);
internal Examine(SigScanner scanner) { private RequestCharInfoDelegate? RequestCharacterInfo { get; }
// got this by checking what accesses rciData below
if (scanner.TryScanText(Signatures.RequestCharacterInfo, out var rciPtr, "Examine")) {
this.RequestCharacterInfo = Marshal.GetDelegateForFunctionPointer<RequestCharInfoDelegate>(rciPtr);
}
}
/// <summary> internal Examine(ISigScanner scanner) {
/// Opens the Examine window for the specified object. // got this by checking what accesses rciData below
/// </summary> if (scanner.TryScanText(Signatures.RequestCharacterInfo, out var rciPtr, "Examine")) {
/// <param name="object">Object to open window for</param> this.RequestCharacterInfo = Marshal.GetDelegateForFunctionPointer<RequestCharInfoDelegate>(rciPtr);
/// <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: 6.0
// 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);
// 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;
this.RequestCharacterInfo(rciData);
} }
} }
/// <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: 6.0
// 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);
// 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;
this.RequestCharacterInfo(rciData);
}
} }

View File

@ -3,60 +3,60 @@ using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace XivCommon.Functions.FriendList { 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;
/// <summary> /// <summary>
/// The class containing friend list functionality /// <para>
/// A live list of the currently-logged-in player's friends.
/// </para>
/// <para>
/// The list is empty if not logged in.
/// </para>
/// </summary> /// </summary>
public class FriendList { public unsafe IList<FriendListEntry> List {
// Updated: 5.58-HF1 get {
private const int InfoOffset = 0x28; var friendListAgent = (IntPtr) Framework.Instance()
private const int LengthOffset = 0x10; ->GetUiModule()
private const int ListOffset = 0x98;
/// <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) Framework.Instance()
->GetUiModule()
->GetAgentModule() ->GetAgentModule()
->GetAgentByInternalId(AgentId.SocialFriendList); ->GetAgentByInternalId(AgentId.SocialFriendList);
if (friendListAgent == IntPtr.Zero) { if (friendListAgent == IntPtr.Zero) {
return Array.Empty<FriendListEntry>(); 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 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);
}
return entries;
} }
}
internal FriendList() { 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 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);
}
return entries;
} }
} }
internal FriendList() {
}
} }

View File

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

View File

@ -1,56 +1,56 @@
using System; using System;
using Dalamud.Game; using Dalamud.Game;
namespace XivCommon.Functions.Housing { 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; }
/// <summary> /// <summary>
/// The class containing housing functionality /// 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> /// </summary>
public class Housing { // Updated: 6.0
private static class Signatures { // 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?)
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"; public unsafe RawHousingLocation? RawLocation {
} get {
if (this.HousingPointer == IntPtr.Zero) {
private IntPtr HousingPointer { get; } return null;
/// <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: 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;
} }
}
/// <summary> var loc = Util.FollowPointerChain(this.HousingPointer, new[] { 0, 0 });
/// Gets process information about the player's current location in a housing ward. if (loc == IntPtr.Zero) {
/// return null;
/// <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) { var locPtr = (RawHousingLocation*) (loc + 0x96a0);
if (scanner.TryGetStaticAddressFromSig(Signatures.HousingPointer, out var ptr)) { return *locPtr;
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(ISigScanner 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> /// <summary>
/// Information about a player's current location in a housing ward. /// The housing ward that the player is in.
/// </summary> /// </summary>
public class HousingLocation { public ushort Ward;
/// <summary> /// <summary>
/// The housing ward that the player is in. /// <para>
/// </summary> /// The yard that the player is in.
public ushort Ward; /// </para>
/// <summary> /// <para>
/// <para> /// This is the same as plot number but indicates that the player is in
/// The yard that the player is in. /// the exterior area (the yard) of that plot.
/// </para> /// </para>
/// <para> /// </summary>
/// This is the same as plot number but indicates that the player is in public ushort? Yard;
/// the exterior area (the yard) of that plot. /// <summary>
/// </para> /// The plot that the player is in.
/// </summary> /// </summary>
public ushort? Yard; public ushort? Plot;
/// <summary> /// <summary>
/// The plot that the player is in. /// The apartment wing (1 or 2 for normal or subdivision) that the
/// </summary> /// player is in.
public ushort? Plot; /// </summary>
/// <summary> public ushort? ApartmentWing;
/// The apartment wing (1 or 2 for normal or subdivision) that the /// <summary>
/// player is in. /// The apartment that the player is in.
/// </summary> /// </summary>
public ushort? ApartmentWing; public ushort? Apartment;
/// <summary>
/// The apartment that the player is in.
/// </summary>
public ushort? Apartment;
internal HousingLocation(RawHousingLocation loc) { internal HousingLocation(RawHousingLocation loc) {
var ward = loc.CurrentWard; var ward = loc.CurrentWard;
if ((loc.CurrentPlot & 0x80) > 0) { if ((loc.CurrentPlot & 0x80) > 0) {
// the struct is in apartment mode // the struct is in apartment mode
this.ApartmentWing = (ushort?) ((loc.CurrentPlot & ~0x80) + 1); this.ApartmentWing = (ushort?) ((loc.CurrentPlot & ~0x80) + 1);
this.Apartment = (ushort?) (ward >> 6); this.Apartment = (ushort?) (ward >> 6);
this.Ward = (ushort) ((ward & 0x3F) + 1); this.Ward = (ushort) ((ward & 0x3F) + 1);
if (this.Apartment == 0) { if (this.Apartment == 0) {
this.Apartment = null; 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) { if (this.Ward == 0) {
this.Ward = (ushort) (ward + 1); this.Ward = (ushort) (ward + 1);
}
} }
} }
} }

View File

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

@ -5,79 +5,79 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions { namespace XivCommon.Functions;
/// <summary>
/// Journal functions /// <summary>
/// </summary> /// Journal functions
public class Journal { /// </summary>
private static class Signatures { public class Journal {
internal const string OpenQuest = "E8 ?? ?? ?? ?? 48 8B 74 24 ?? 48 8B 7C 24 ?? 48 83 C4 30 5B C3 48 8B CB"; private static class Signatures {
internal const string IsQuestCompleted = "E8 ?? ?? ?? ?? 41 88 84 2C"; 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);
} }
private delegate IntPtr OpenQuestDelegate(IntPtr agent, int questId, int a3, ushort a4, byte a5); if (scanner.TryScanText(Signatures.IsQuestCompleted, out var questCompletedPtr, "Journal (quest completed)")) {
this._isQuestCompleted = Marshal.GetDelegateForFunctionPointer<IsQuestCompletedDelegate>(questCompletedPtr);
private delegate byte IsQuestCompletedDelegate(ushort questId);
private readonly OpenQuestDelegate? _openQuest;
private readonly IsQuestCompletedDelegate? _isQuestCompleted;
internal Journal(SigScanner scanner) {
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);
}
}
/// <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);
}
/// <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) 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");
}
return this._isQuestCompleted((ushort) (questId & 0xFFFF)) != 0;
} }
} }
/// <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);
}
/// <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) 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");
}
return this._isQuestCompleted((ushort) (questId & 0xFFFF)) != 0;
}
} }

View File

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

View File

@ -3,224 +3,225 @@ using System.Runtime.InteropServices;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.Graphics;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions.NamePlates { 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);
/// <summary> /// <summary>
/// The class containing name plate functionality /// The delegate for name plate update events.
/// </summary> /// </summary>
public class NamePlates : IDisposable { public delegate void NamePlateUpdateEvent(NamePlateUpdateEventArgs args);
private static class Signatures {
internal const string NamePlateUpdate = "48 8B C4 41 56 48 81 EC ?? ?? ?? ?? 48 89 58 F0"; /// <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;
} }
private unsafe delegate IntPtr NamePlateUpdateDelegate(AddonNamePlate* addon, NumberArrayData** numberData, StringArrayData** stringData); if (scanner.TryScanText(Signatures.NamePlateUpdate, out var updatePtr)) {
unsafe {
/// <summary> this._namePlateUpdateHook = interop.HookFromAddress<NamePlateUpdateDelegate>(updatePtr, this.NamePlateUpdateDetour);
/// 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;
} }
if (scanner.TryScanText(Signatures.NamePlateUpdate, out var updatePtr)) { this._namePlateUpdateHook.Enable();
unsafe { }
this._namePlateUpdateHook = Hook<NamePlateUpdateDelegate>.FromAddress(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.Log.Error(ex, "Exception in NamePlateUpdateDetour");
} }
/// <inheritdoc /> return this._namePlateUpdateHook!.Original(addon, numberData, stringData);
public void Dispose() { }
this._namePlateUpdateHook?.Dispose();
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;
} }
private const int PlateTypeIndex = 1; var atkModule = Framework.Instance()->GetUiModule()->GetRaptureAtkModule();
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) { var active = numbers->IntArray[0];
try {
this.NamePlateUpdateDetourInner(numberData, stringData);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in NamePlateUpdateDetour");
}
return this._namePlateUpdateHook!.Original(addon, numberData, stringData); var force = this.ForceRedraw;
if (force) {
this.ForceRedraw = false;
} }
private unsafe void NamePlateUpdateDetourInner(NumberArrayData** numberData, StringArrayData** stringData) { for (var i = 0; i < active; i++) {
// don't skip to original if no subscribers because of ForceRedraw var numbersIndex = i * 19 + 5;
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) { if (force) {
this.ForceRedraw = false; numbers->SetValue(numbersIndex + UpdateIndex, numbers->IntArray[numbersIndex + UpdateIndex] | 1 | 2);
} }
for (var i = 0; i < active; i++) { if (this.OnUpdate == null) {
var numbersIndex = i * 19 + 5; continue;
}
if (force) { if (numbers->IntArray[numbersIndex + UpdateIndex] == 0) {
numbers->SetValue(numbersIndex + UpdateIndex, numbers->IntArray[numbersIndex + UpdateIndex] | 1 | 2); 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 (this.OnUpdate == null) { if (name != args.Name) {
continue; Replace(args.Name.Encode(), NameIndex + i);
} }
if (numbers->IntArray[numbersIndex + UpdateIndex] == 0) { if (title != args.Title) {
continue; Replace(args.Title.Encode(), TitleIndex + i);
} }
var npObjIndex = numbers->IntArray[numbersIndex + NamePlateObjectIndex]; if (fc != args.FreeCompany) {
var info = (&atkModule->NamePlateInfoArray)[npObjIndex]; Replace(args.FreeCompany.Encode(), FreeCompanyIndex + i);
}
var icon = numbers->IntArray[numbersIndex + IconIndex]; if (level != args.Level) {
var nameColour = *(ByteColor*) &numbers->IntArray[numbersIndex + ColourIndex]; Replace(args.Level.Encode(), LevelIndex + i);
var plateType = numbers->IntArray[numbersIndex + PlateTypeIndex]; }
var flags = numbers->IntArray[numbersIndex + FlagsIndex];
var nameRaw = strings->StringArray[NameIndex + i]; if (letter != args.EnemyLetter) {
var name = Util.ReadSeString((IntPtr) nameRaw); // 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);
}
var titleRaw = strings->StringArray[TitleIndex + i]; if (icon != args.Icon) {
var title = Util.ReadSeString((IntPtr) titleRaw); numbers->SetValue(numbersIndex + IconIndex, (int) args.Icon);
}
var fcRaw = strings->StringArray[FreeCompanyIndex + i]; var colour = (ByteColor) args.Colour;
var fc = Util.ReadSeString((IntPtr) fcRaw); var colourInt = *(int*) &colour;
if (colourInt != numbers->IntArray[numbersIndex + ColourIndex]) {
numbers->SetValue(numbersIndex + ColourIndex, colourInt);
}
var levelRaw = strings->StringArray[LevelIndex + i]; if (plateType != (int) args.Type) {
var level = Util.ReadSeString((IntPtr) levelRaw); numbers->SetValue(numbersIndex + PlateTypeIndex, (int) args.Type);
}
var letterRaw = strings->StringArray[EnemyLetterIndex + i]; if (flags != args.Flags) {
var letter = Util.ReadSeString((IntPtr) letterRaw); numbers->SetValue(numbersIndex + FlagsIndex, args.Flags);
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 System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics; 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;
[FieldOffset(0x20)] [StructLayout(LayoutKind.Explicit, Size = 0x28)]
public int* IntArray; internal unsafe struct NumberArrayData {
[FieldOffset(0x0)]
public AtkArrayData AtkArrayData;
public void SetValue(int index, int value) { [FieldOffset(0x20)]
if (index >= this.AtkArrayData.Size) { public int* IntArray;
return;
}
if (this.IntArray[index] == value) { public void SetValue(int index, int value) {
return; if (index >= this.AtkArrayData.Size) {
} 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,
};
} }
/// <summary> if (this.IntArray[index] == value) {
/// Converts an RgbaColour into an unsigned integer representation. return;
/// </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> this.IntArray[index] = value;
/// Converts a ByteColor into an RgbaColour. this.AtkArrayData.HasModifiedData = 1;
/// </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.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Gui.PartyFinder;
using Dalamud.Game.Gui.PartyFinder.Types; using Dalamud.Game.Gui.PartyFinder.Types;
using Dalamud.Hooking; 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> /// <summary>
/// A class containing Party Finder functionality /// The delegate for party join events.
/// </summary> /// </summary>
public class PartyFinder : IDisposable { public delegate void JoinPfEventDelegate(PartyFinderListing listing);
private static class Signatures {
internal const string RequestListings = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 40 0F 10 81"; /// <summary>
internal const string JoinCrossParty = "E8 ?? ?? ?? ?? 0F B7 47 28"; /// <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;
} }
private delegate byte RequestPartyFinderListingsDelegate(IntPtr agent, byte categoryIdx); if (scanner.TryScanText(Signatures.RequestListings, out var requestPfPtr, "Party Finder listings")) {
this.RequestPartyFinderListings = Marshal.GetDelegateForFunctionPointer<RequestPartyFinderListingsDelegate>(requestPfPtr);
private delegate IntPtr JoinPfDelegate(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5); if (this.ListingsEnabled) {
this.RequestPfListingsHook = interop.HookFromAddress<RequestPartyFinderListingsDelegate>(requestPfPtr, this.OnRequestPartyFinderListings);
private RequestPartyFinderListingsDelegate? RequestPartyFinderListings { get; } this.RequestPfListingsHook.Enable();
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 = Hook<RequestPartyFinderListingsDelegate>.FromAddress(requestPfPtr, this.OnRequestPartyFinderListings);
this.RequestPfListingsHook.Enable();
}
}
if (this.JoinsEnabled && scanner.TryScanText(Signatures.JoinCrossParty, out var joinPtr, "Party Finder joins")) {
this.JoinPfHook = Hook<JoinPfDelegate>.FromAddress(joinPtr, this.JoinPfDetour);
this.JoinPfHook.Enable();
} }
} }
/// <inheritdoc /> if (this.JoinsEnabled && scanner.TryScanText(Signatures.JoinCrossParty, out var joinPtr, "Party Finder joins")) {
public void Dispose() { this.JoinPfHook = interop.HookFromAddress<JoinPfDelegate>(joinPtr, this.JoinPfDetour);
this.PartyFinderGui.ReceiveListing -= this.ReceiveListing; this.JoinPfHook.Enable();
this.JoinPfHook?.Dispose(); }
this.RequestPfListingsHook?.Dispose(); }
/// <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();
} }
private void ReceiveListing(PartyFinderListing listing, PartyFinderListingEventArgs args) { this.LastBatch = args.BatchNumber;
if (args.BatchNumber != this.LastBatch) {
this.Listings.Clear();
}
this.LastBatch = args.BatchNumber; this.Listings[listing.Id] = listing;
}
this.Listings[listing.Id] = listing; private byte OnRequestPartyFinderListings(IntPtr agent, byte categoryIdx) {
} this.PartyFinderAgent = agent;
return this.RequestPfListingsHook!.Original(agent, categoryIdx);
}
private byte OnRequestPartyFinderListings(IntPtr agent, byte categoryIdx) { private IntPtr JoinPfDetour(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5) {
this.PartyFinderAgent = agent; // Updated: 5.5
return this.RequestPfListingsHook!.Original(agent, categoryIdx); const int idOffset = -0x20;
}
private IntPtr JoinPfDetour(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5) { var ret = this.JoinPfHook!.Original(manager, a2, type, packetData, 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; return ret;
} }
/// <summary> try {
/// <para> var id = (uint) Marshal.ReadInt32(packetData + idOffset);
/// Refresh the Party Finder listings. This does not open the Party Finder. if (this.Listings.TryGetValue(id, out var listing)) {
/// </para> this.JoinParty?.Invoke(listing);
/// <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) {
if (!this.ListingsEnabled) { Logger.Log.Error(ex, "Exception in PF join detour");
throw new InvalidOperationException("PartyFinder hooks are not enabled");
}
// Updated 6.0
const int categoryOffset = 11_031;
if (this.PartyFinderAgent == IntPtr.Zero) {
return;
}
var categoryIdx = Marshal.ReadByte(this.PartyFinderAgent + categoryOffset);
this.RequestPartyFinderListings(this.PartyFinderAgent, categoryIdx);
} }
return ret;
} }
internal enum JoinType : byte { /// <summary>
/// <summary> /// <para>
/// Join via invite or party conversion. /// Refresh the Party Finder listings. This does not open the Party Finder.
/// </summary> /// </para>
Normal = 0, /// <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");
}
/// <summary> if (!this.ListingsEnabled) {
/// Join via Party Finder. throw new InvalidOperationException("PartyFinder hooks are not enabled");
/// </summary> }
PartyFinder = 1,
Unknown2 = 2, // Updated 6.0
const int categoryOffset = 11_031;
/// <summary> if (this.PartyFinderAgent == IntPtr.Zero) {
/// Remain in cross-world party after leaving a duty. return;
/// </summary> }
LeaveDuty = 3,
Unknown4 = 4, var categoryIdx = Marshal.ReadByte(this.PartyFinderAgent + categoryOffset);
this.RequestPartyFinderListings(this.PartyFinderAgent, categoryIdx);
} }
} }
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,157 +3,158 @@ using System.Runtime.InteropServices;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking; 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> /// <summary>
/// Class containing Talk events /// The delegate for Talk events.
/// </summary> /// </summary>
public class Talk : IDisposable { public delegate void TalkEventDelegate(ref SeString name, ref SeString text, ref TalkStyle style);
private static class Signatures {
internal const string SetAtkValue = "E8 ?? ?? ?? ?? 41 03 ED"; /// <summary>
internal const string ShowMessageBox = "4C 8B DC 55 57 41 55 49 8D 6B 98"; /// <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;
} }
// Updated: 5.5 if (!hooksEnabled) {
private const int TextOffset = 0; return;
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 = Hook<AddonTalkV45Delegate>.FromAddress(showMessageBoxPtr, this.AddonTalkV45Detour);
this.AddonTalkV45Hook.Enable();
}
} }
/// <inheritdoc /> if (scanner.TryScanText(Signatures.ShowMessageBox, out var showMessageBoxPtr, "Talk")) {
public void Dispose() { this.AddonTalkV45Hook = interop.HookFromAddress<AddonTalkV45Delegate>(showMessageBoxPtr, this.AddonTalkV45Detour);
this.AddonTalkV45Hook?.Dispose(); this.AddonTalkV45Hook.Enable();
}
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);
}
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);
try {
this.OnTalk?.Invoke(ref name, ref text, ref style);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in Talk event");
}
var newName = name.Encode().Terminate();
var newText = text.Encode().Terminate();
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);
}
}
} }
} }
/// <summary> /// <inheritdoc />
/// Talk window styles. public void Dispose() {
/// </summary> this.AddonTalkV45Hook?.Dispose();
public enum TalkStyle : byte { }
/// <summary>
/// The normal style with a white background.
/// </summary>
Normal = 0,
/// <summary> private void AddonTalkV45Detour(IntPtr addon, IntPtr a2, IntPtr data) {
/// A style with lights on the top and bottom border. if (this.OnTalk == null) {
/// </summary> goto Return;
Lights = 2, }
/// <summary> try {
/// A style used for when characters are shouting. this.AddonTalkV45DetourInner(data);
/// </summary> } catch (Exception ex) {
Shout = 3, Logger.Log.Error(ex, "Exception in Talk detour");
}
/// <summary> Return:
/// Like <see cref="Shout"/> but with flatter edges. this.AddonTalkV45Hook!.Original(addon, a2, data);
/// </summary> }
FlatShout = 4,
/// <summary> private void AddonTalkV45DetourInner(IntPtr data) {
/// The style used when dragons (and some other NPCs) talk. var rawName = Util.ReadTerminated(Marshal.ReadIntPtr(data + NameOffset + 8));
/// </summary> var rawText = Util.ReadTerminated(Marshal.ReadIntPtr(data + TextOffset + 8));
Dragon = 5, var style = (TalkStyle) Marshal.ReadByte(data + StyleOffset);
/// <summary> var name = SeString.Parse(rawName);
/// The style used for Allagan machinery. var text = SeString.Parse(rawText);
/// </summary>
Allagan = 6,
/// <summary> try {
/// The style used for system messages. this.OnTalk?.Invoke(ref name, ref text, ref style);
/// </summary> } catch (Exception ex) {
System = 7, Logger.Log.Error(ex, "Exception in Talk event");
}
/// <summary> var newName = name.Encode().Terminate();
/// A mixture of the system message style and the dragon style. var newText = text.Encode().Terminate();
/// </summary>
DragonSystem = 8,
/// <summary> Marshal.WriteByte(data + StyleOffset, (byte) style);
/// The system message style with a purple background.
/// </summary> unsafe {
PurpleSystem = 9, 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,
}

View File

@ -1,28 +1,28 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.Tooltips { 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) {
}
/// <summary> /// <summary>
/// Gets or sets the SeString for the given string enum. /// The class allowing for action tooltip manipulation
/// </summary> /// </summary>
/// <param name="ats">the string to retrieve/update</param> public unsafe class ActionTooltip : BaseTooltip {
public SeString this[ActionTooltipString ats] { internal ActionTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) : base(sadSetString, stringArrayData, numberArrayData) {
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;
}
} }
}
/// <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; 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> /// <summary>
/// An enum containing the fields that can be displayed in action tooltips. /// An enum containing the strings used in action tooltips.
/// </summary> /// </summary>
[Flags] public enum ActionTooltipString {
public enum ActionTooltipFields { #pragma warning disable 1591
#pragma warning disable 1591 Name = 0,
Range = 1 << 0, Type = 1,
Radius = 1 << 1, RangeLabel = 3,
Cost = 1 << 2, Range = 4,
Recast = 1 << 3, RadiusLabel = 5,
Cast = 1 << 4, Radius = 6,
Description = 1 << 5, CostLabel = 7,
Acquired = 1 << 6, Cost = 8,
Affinity = 1 << 7, RecastLabel = 9,
Unknown8 = 1 << 8, Recast = 10,
#pragma warning restore 1591 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
}

View File

@ -1,50 +1,50 @@
using System; using System;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.Tooltips { namespace XivCommon.Functions.Tooltips;
/// <summary>
/// The base class for tooltips
/// </summary>
public abstract unsafe class BaseTooltip {
private Tooltips.StringArrayDataSetStringDelegate SadSetString { get; }
/// <summary> /// <summary>
/// The base class for tooltips /// A pointer to the StringArrayData class for this tooltip.
/// </summary> /// </summary>
public abstract unsafe class BaseTooltip { private readonly byte*** _stringArrayData; // this is StringArrayData* when ClientStructs is updated
private Tooltips.StringArrayDataSetStringDelegate SadSetString { get; }
/// <summary> /// <summary>
/// A pointer to the StringArrayData class for this tooltip. /// A pointer to the NumberArrayData class for this tooltip.
/// </summary> /// </summary>
private readonly byte*** _stringArrayData; // this is StringArrayData* when ClientStructs is updated protected readonly int** NumberArrayData;
/// <summary> internal BaseTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) {
/// A pointer to the NumberArrayData class for this tooltip. this.SadSetString = sadSetString;
/// </summary> this._stringArrayData = stringArrayData;
protected readonly int** NumberArrayData; this.NumberArrayData = numberArrayData;
}
internal BaseTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) { /// <summary>
this.SadSetString = sadSetString; /// <para>
this._stringArrayData = stringArrayData; /// Gets the SeString at the given index for this tooltip.
this.NumberArrayData = numberArrayData; /// </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();
/// <summary> fixed (byte* encodedPtr = encoded) {
/// <para> this.SadSetString((IntPtr) this._stringArrayData, index, encodedPtr, 0, 1, 1);
/// 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; using Dalamud.Game.Text.SeStringHandling;
namespace XivCommon.Functions.Tooltips { 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) {
}
/// <summary> /// <summary>
/// Gets or sets the SeString for the given string enum. /// The class allowing for item tooltip manipulation
/// </summary> /// </summary>
/// <param name="its">the string to retrieve/update</param> public unsafe class ItemTooltip : BaseTooltip {
public SeString this[ItemTooltipString its] { internal ItemTooltip(Tooltips.StringArrayDataSetStringDelegate sadSetString, byte*** stringArrayData, int** numberArrayData) : base(sadSetString, stringArrayData, numberArrayData) {
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;
}
} }
}
/// <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; using System;
namespace XivCommon.Functions.Tooltips { 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> /// <summary>
/// An enum containing the fields that can be displayed in item tooltips. /// An enum containing the strings used in item tooltips.
/// </summary> /// </summary>
[Flags] public enum ItemTooltipString {
public enum ItemTooltipFields { #pragma warning disable 1591
#pragma warning disable 1591 Name = 0,
Crafter = 1 << 0, GlamourName = 1,
Description = 1 << 1, Type = 2,
VendorSellPrice = 1 << 2, Stat1Label = 4,
Stat2Label = 5,
// makes the tooltip much smaller when hovered over gear and unset Stat3Label = 6,
// something to do with EquipLevel maybe? Stat1 = 7,
Unknown3 = 1 << 3, Stat2 = 8,
Bonuses = 1 << 4, Stat3 = 9,
Materia = 1 << 5, Stat1Delta = 10,
CraftingAndRepairs = 1 << 6, Stat2Delta = 11,
Effects = 1 << 8, Stat3Delta = 12,
DyeableIndicator = 1 << 10, Description = 13,
Stat1 = 1 << 11, Quantity = 14,
Stat2 = 1 << 12, EffectsLabel = 15,
Stat3 = 1 << 13, Effects = 16,
EquipJobs = 22,
/// <summary> EquipLevel = 23,
/// <para> VendorSellPrice = 25,
/// Shows item level and equip level. Crafter = 26,
/// </para> Level = 27,
/// <para> Condition = 28,
/// Item level is always visible, but if equip level is set to an empty string, it will be hidden. SpiritbondLabel = 29,
/// </para> Spiritbond = 30,
/// </summary> RepairLevel = 31,
Levels = 1 << 15, Materials = 32,
GlamourIndicator = 1 << 16, QuickRepairs = 33,
Unknown19 = 1 << 19, MateriaMelding = 34,
#pragma warning restore 1591 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,
/// <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,149 +3,150 @@ using System.Runtime.InteropServices;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Hooking; 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> /// <summary>
/// The class containing tooltip functionality /// The delegate for item tooltip events.
/// </summary> /// </summary>
public class Tooltips : IDisposable { public delegate void ItemTooltipEventDelegate(ItemTooltip itemTooltip, ulong itemId);
private static class Signatures {
internal const string AgentItemDetailUpdateTooltip = "E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ?? 48 89 AE"; /// <summary>
internal const string AgentActionDetailUpdateTooltip = "E8 ?? ?? ?? ?? EB 68 FF 50 40"; /// The tooltip for action tooltip events.
internal const string SadSetString = "E8 ?? ?? ?? ?? F6 47 14 08"; /// </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;
} }
// Last checked: 6.0 if (!enabled) {
// E8 ?? ?? ?? ?? EB 68 FF 50 40 return;
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; }
/// <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;
}
if (!enabled) {
return;
}
if (scanner.TryScanText(Signatures.AgentItemDetailUpdateTooltip, out var updateItemPtr, "Tooltips - Items")) {
unsafe {
this.ItemUpdateTooltipHook = Hook<ItemUpdateTooltipDelegate>.FromAddress(updateItemPtr, this.ItemUpdateTooltipDetour);
}
this.ItemUpdateTooltipHook.Enable();
}
if (scanner.TryScanText(Signatures.AgentActionDetailUpdateTooltip, out var updateActionPtr, "Tooltips - Actions")) {
unsafe {
this.ActionGenerateTooltipHook = Hook<ActionUpdateTooltipDelegate>.FromAddress(updateActionPtr, this.ActionUpdateTooltipDetour);
}
this.ActionGenerateTooltipHook.Enable();
}
} }
/// <inheritdoc /> if (scanner.TryScanText(Signatures.AgentItemDetailUpdateTooltip, out var updateItemPtr, "Tooltips - Items")) {
public void Dispose() { unsafe {
this.ActionGenerateTooltipHook?.Dispose(); this.ItemUpdateTooltipHook = interop.HookFromAddress<ItemUpdateTooltipDelegate>(updateItemPtr, this.ItemUpdateTooltipDetour);
this.ItemUpdateTooltipHook?.Dispose();
}
private unsafe ulong 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; this.ItemUpdateTooltipHook.Enable();
} }
private unsafe void ItemUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) { if (scanner.TryScanText(Signatures.AgentActionDetailUpdateTooltip, out var updateActionPtr, "Tooltips - Actions")) {
this.ItemTooltip = new ItemTooltip(this.SadSetString!, stringArrayData, numberArrayData); unsafe {
this.ActionGenerateTooltipHook = interop.HookFromAddress<ActionUpdateTooltipDelegate>(updateActionPtr, this.ActionUpdateTooltipDetour);
}
this.ActionGenerateTooltipHook.Enable();
}
}
/// <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);
if (ret > 0) {
try { try {
this.OnItemTooltip?.Invoke(this.ItemTooltip, this.GameGui.HoveredItem); this.ItemUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} catch (Exception ex) { } catch (Exception ex) {
Logger.LogError(ex, "Exception in OnItemTooltip event"); Logger.Log.Error(ex, "Exception in item tooltip detour");
} }
} }
private unsafe void ActionUpdateTooltipDetour(IntPtr agent, int** numberArrayData, byte*** stringArrayData) { return ret;
var flag = *(byte*) (agent + AgentActionDetailUpdateFlagOffset); }
this.ActionGenerateTooltipHook!.Original(agent, numberArrayData, stringArrayData);
if (flag == 0) { private unsafe void ItemUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
return; this.ItemTooltip = new ItemTooltip(this.SadSetString!, stringArrayData, numberArrayData);
}
try { try {
this.ActionUpdateTooltipDetourInner(numberArrayData, stringArrayData); this.OnItemTooltip?.Invoke(this.ItemTooltip, this.GameGui.HoveredItem);
} catch (Exception ex) { } catch (Exception ex) {
Logger.LogError(ex, "Exception in action tooltip detour"); 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;
} }
private unsafe void ActionUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) { try {
this.ActionTooltip = new ActionTooltip(this.SadSetString!, stringArrayData, numberArrayData); this.ActionUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in action tooltip detour");
}
}
try { private unsafe void ActionUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.OnActionTooltip?.Invoke(this.ActionTooltip, this.GameGui.HoveredAction); this.ActionTooltip = new ActionTooltip(this.SadSetString!, stringArrayData, numberArrayData);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OnActionTooltip event"); try {
} this.OnActionTooltip?.Invoke(this.ActionTooltip, this.GameGui.HoveredAction);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in OnActionTooltip event");
} }
} }
} }

View File

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

View File

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

View File

@ -1,74 +1,74 @@
using System; using System;
namespace XivCommon { namespace XivCommon;
/// <summary>
/// Flags for which hooks to use
/// </summary>
[Flags]
public enum Hooks {
/// <summary> /// <summary>
/// Flags for which hooks to use /// No hook.
///
/// This flag is used to disable all hooking.
/// </summary> /// </summary>
[Flags] None = 0,
public enum Hooks {
/// <summary>
/// No hook.
///
/// This flag is used to disable all hooking.
/// </summary>
None = 0,
/// <summary> /// <summary>
/// The Tooltips hooks. /// The Tooltips hooks.
/// ///
/// This hook is used in order to enable the tooltip events. /// This hook is used in order to enable the tooltip events.
/// </summary> /// </summary>
Tooltips = 1 << 0, Tooltips = 1 << 0,
/// <summary> /// <summary>
/// The BattleTalk hook. /// The BattleTalk hook.
/// ///
/// This hook is used in order to enable the BattleTalk events. /// This hook is used in order to enable the BattleTalk events.
/// </summary> /// </summary>
BattleTalk = 1 << 1, BattleTalk = 1 << 1,
/// <summary> /// <summary>
/// Hooks used for refreshing Party Finder listings. /// Hooks used for refreshing Party Finder listings.
/// </summary> /// </summary>
PartyFinderListings = 1 << 2, PartyFinderListings = 1 << 2,
/// <summary> /// <summary>
/// Hooks used for Party Finder join events. /// Hooks used for Party Finder join events.
/// </summary> /// </summary>
PartyFinderJoins = 1 << 3, PartyFinderJoins = 1 << 3,
/// <summary> /// <summary>
/// All Party Finder hooks. /// All Party Finder hooks.
/// ///
/// This hook is used in order to enable all Party Finder functions. /// This hook is used in order to enable all Party Finder functions.
/// </summary> /// </summary>
PartyFinder = PartyFinderListings | PartyFinderJoins, PartyFinder = PartyFinderListings | PartyFinderJoins,
/// <summary> /// <summary>
/// The Talk hooks. /// The Talk hooks.
/// ///
/// This hook is used in order to enable the Talk events. /// This hook is used in order to enable the Talk events.
/// </summary> /// </summary>
Talk = 1 << 4, Talk = 1 << 4,
/// <summary> /// <summary>
/// The chat bubbles hooks. /// The chat bubbles hooks.
/// ///
/// This hook is used in order to enable the chat bubbles events. /// This hook is used in order to enable the chat bubbles events.
/// </summary> /// </summary>
ChatBubbles = 1 << 5, ChatBubbles = 1 << 5,
// 1 << 6 used to be ContextMenu // 1 << 6 used to be ContextMenu
/// <summary> /// <summary>
/// The name plate hooks. /// The name plate hooks.
/// ///
/// This hook is used in order to enable name plate functions. /// This hook is used in order to enable name plate functions.
/// </summary> /// </summary>
NamePlates = 1 << 7, 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 Executable file → Normal file
View File

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

@ -2,28 +2,28 @@
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Game; 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);
}
return false; 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;
} }
} }
} }

View File

@ -4,60 +4,60 @@ using System.Reflection;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin; 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;
return terminated; 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>();
} }
internal static unsafe byte[] ReadTerminated(IntPtr memory) { var buf = new List<byte>();
if (memory == IntPtr.Zero) {
return Array.Empty<byte>();
}
var buf = new List<byte>(); var ptr = (byte*) memory;
while (*ptr != 0) {
var ptr = (byte*) memory; buf.Add(*ptr);
while (*ptr != 0) { ptr += 1;
buf.Add(*ptr);
ptr += 1;
}
return buf.ToArray();
} }
internal static SeString ReadSeString(IntPtr memory) { return buf.ToArray();
var terminated = ReadTerminated(memory); }
return SeString.Parse(terminated);
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 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;
} }
internal static void PrintMissingSig(string name) { foreach (var offset in offsets) {
Logger.LogWarning($"Could not find signature for {name}. This functionality will be disabled."); start = *(IntPtr*) (start + offset);
}
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) { if (start == IntPtr.Zero) {
return IntPtr.Zero; return IntPtr.Zero;
} }
foreach (var offset in offsets) {
start = *(IntPtr*) (start + offset);
if (start == IntPtr.Zero) {
return IntPtr.Zero;
}
}
return start;
} }
return start;
} }
} }

View File

@ -5,7 +5,7 @@
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>7.0.2</Version> <Version>8.0.0</Version>
<DebugType>full</DebugType> <DebugType>full</DebugType>
</PropertyGroup> </PropertyGroup>
@ -14,7 +14,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Title>XivCommon</Title> <Title>XivCommon</Title>
<Authors>ascclemens</Authors> <Authors>ascclemens</Authors>
<RepositoryUrl>https://git.annaclemens.io/ascclemens/XivCommon</RepositoryUrl> <RepositoryUrl>https://git.anna.lgbt/anna/XivCommon</RepositoryUrl>
<Description>A set of common functions, hooks, and events not included in Dalamud.</Description> <Description>A set of common functions, hooks, and events not included in Dalamud.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

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