refactor: update for api 9

This commit is contained in:
Anna 2023-10-03 02:25:45 -04:00
parent e0fff565bd
commit f22702f251
Signed by: anna
GPG Key ID: D0943384CD9F87D1
30 changed files with 2243 additions and 2257 deletions

View File

@ -2,181 +2,182 @@
using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
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>
/// The class containing BattleTalk functionality
/// The delegate for BattleTalk events.
/// </summary>
public class BattleTalk : IDisposable {
private bool HookEnabled { get; }
public delegate void BattleTalkEventDelegate(ref SeString sender, ref SeString message, ref BattleTalkOptions options, ref bool isHandled);
/// <summary>
/// The delegate for BattleTalk events.
/// </summary>
public delegate void BattleTalkEventDelegate(ref SeString sender, ref SeString message, ref BattleTalkOptions options, ref bool isHandled);
/// <summary>
/// <para>
/// The event that is fired when a BattleTalk window is shown.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.BattleTalk"/> hook to be enabled.
/// </para>
/// </summary>
public event BattleTalkEventDelegate? OnBattleTalk;
/// <summary>
/// <para>
/// The event that is fired when a BattleTalk window is shown.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.BattleTalk"/> hook to be enabled.
/// </para>
/// </summary>
public event BattleTalkEventDelegate? OnBattleTalk;
private delegate byte AddBattleTalkDelegate(IntPtr uiModule, IntPtr sender, IntPtr message, float duration, byte style);
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; }
private Hook<AddBattleTalkDelegate>? AddBattleTalkHook { get; }
internal unsafe BattleTalk(IGameInteropProvider interop, bool hook) {
this.HookEnabled = hook;
internal unsafe BattleTalk(bool hook) {
this.HookEnabled = hook;
var addBattleTalkPtr = (IntPtr) Framework.Instance()->GetUiModule()->VTable->ShowBattleTalk;
if (addBattleTalkPtr != IntPtr.Zero) {
this.AddBattleTalk = Marshal.GetDelegateForFunctionPointer<AddBattleTalkDelegate>(addBattleTalkPtr);
var addBattleTalkPtr = (IntPtr) Framework.Instance()->GetUiModule()->VTable->ShowBattleTalk;
if (addBattleTalkPtr != IntPtr.Zero) {
this.AddBattleTalk = Marshal.GetDelegateForFunctionPointer<AddBattleTalkDelegate>(addBattleTalkPtr);
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);
}
if (this.HookEnabled) {
this.AddBattleTalkHook = interop.HookFromAddress<AddBattleTalkDelegate>(addBattleTalkPtr, this.AddBattleTalkDetour);
this.AddBattleTalkHook.Enable();
}
}
}
/// <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;
/// <inheritdoc />
public void Dispose() {
this.AddBattleTalkHook?.Dispose();
}
/// <summary>
/// The style of the window.
/// </summary>
public BattleTalkStyle Style { get; set; } = BattleTalkStyle.Normal;
private byte AddBattleTalkDetour(IntPtr uiModule, IntPtr senderPtr, IntPtr messagePtr, float duration, byte style) {
if (this.OnBattleTalk == null) {
goto Return;
}
try {
return this.AddBattleTalkDetourInner(uiModule, senderPtr, messagePtr, duration, style);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in BattleTalk detour");
}
Return:
return this.AddBattleTalkHook!.Original(uiModule, senderPtr, messagePtr, duration, style);
}
private unsafe byte AddBattleTalkDetourInner(IntPtr uiModule, IntPtr senderPtr, IntPtr messagePtr, float duration, byte style) {
var rawSender = Util.ReadTerminated(senderPtr);
var rawMessage = Util.ReadTerminated(messagePtr);
var sender = SeString.Parse(rawSender);
var message = SeString.Parse(rawMessage);
var options = new BattleTalkOptions {
Duration = duration,
Style = (BattleTalkStyle) style,
};
var handled = false;
try {
this.OnBattleTalk?.Invoke(ref sender, ref message, ref options, ref handled);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in BattleTalk event");
}
if (handled) {
return 0;
}
var finalSender = sender.Encode().Terminate();
var finalMessage = message.Encode().Terminate();
fixed (byte* fSenderPtr = finalSender, fMessagePtr = finalMessage) {
return this.AddBattleTalkHook!.Original(uiModule, (IntPtr) fSenderPtr, (IntPtr) fMessagePtr, options.Duration, (byte) options.Style);
}
}
/// <summary>
/// BattleTalk window styles.
/// Show a BattleTalk window with the given options.
/// </summary>
public enum BattleTalkStyle : byte {
/// <summary>
/// A normal battle talk window with a white background.
/// </summary>
Normal = 0,
/// <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);
}
/// <summary>
/// A battle talk window with a blue background and styled edges.
/// </summary>
Aetherial = 6,
private unsafe void Show(byte[] sender, byte[] message, BattleTalkOptions? options) {
if (sender.Length == 0) {
throw new ArgumentException("sender cannot be empty", nameof(sender));
}
/// <summary>
/// A battle talk window styled similarly to a system text message (black background).
/// </summary>
System = 7,
if (message.Length == 0) {
throw new ArgumentException("message cannot be empty", nameof(message));
}
/// <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,
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>
/// 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 Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions {
/// <summary>
/// A class containing chat functionality
/// </summary>
public class Chat {
private static class Signatures {
internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9";
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D";
namespace XivCommon.Functions;
/// <summary>
/// A class containing chat functionality
/// </summary>
public class Chat {
private static class Signatures {
internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9";
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D";
}
private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
private ProcessChatBoxDelegate? ProcessChatBox { get; }
private readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString = null!;
internal Chat(ISigScanner scanner) {
if (scanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr, "chat sending")) {
this.ProcessChatBox = Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(processChatBoxPtr);
}
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(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);
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);
}
}
}

View File

@ -1,168 +1,168 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
namespace XivCommon.Functions;
/// <summary>
/// Class containing chat bubble events and functions
/// </summary>
public class ChatBubbles : IDisposable {
private static class Signatures {
internal const string ChatBubbleOpen = "E8 ?? ?? ?? ?? C7 43 ?? ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8";
internal const string ChatBubbleUpdate = "48 85 D2 0F 84 ?? ?? ?? ?? 48 89 5C 24 ?? 57 48 83 EC 20 8B 41 0C";
}
private IObjectTable ObjectTable { get; }
private delegate void OpenChatBubbleDelegate(IntPtr manager, IntPtr @object, IntPtr text, byte a4);
private delegate void UpdateChatBubbleDelegate(IntPtr bubblePtr, IntPtr @object);
private Hook<OpenChatBubbleDelegate>? OpenChatBubbleHook { get; }
private Hook<UpdateChatBubbleDelegate>? UpdateChatBubbleHook { get; }
namespace XivCommon.Functions {
/// <summary>
/// Class containing chat bubble events and functions
/// The delegate for chat bubble events.
/// </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";
public delegate void OnChatBubbleDelegate(ref GameObject @object, ref SeString text);
/// <summary>
/// The delegate for chat bubble update events.
/// </summary>
public delegate void OnUpdateChatBubbleDelegate(ref GameObject @object);
/// <summary>
/// <para>
/// The event that is fired when a chat bubble is shown.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ChatBubbles"/> hook to be enabled.
/// </para>
/// </summary>
public event OnChatBubbleDelegate? OnChatBubble;
/// <summary>
/// <para>
/// The event that is fired when a chat bubble is updated.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.ChatBubbles"/> hook to be enabled.
/// </para>
/// </summary>
public event OnUpdateChatBubbleDelegate? OnUpdateBubble;
internal ChatBubbles(IObjectTable objectTable, ISigScanner scanner, IGameInteropProvider interop, bool hookEnabled) {
this.ObjectTable = objectTable;
if (!hookEnabled) {
return;
}
private ObjectTable 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; }
/// <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();
}
if (scanner.TryScanText(Signatures.ChatBubbleOpen, out var openPtr, "chat bubbles open")) {
this.OpenChatBubbleHook = interop.HookFromAddress<OpenChatBubbleDelegate>(openPtr, this.OpenChatBubbleDetour);
this.OpenChatBubbleHook.Enable();
}
/// <inheritdoc />
public void Dispose() {
this.OpenChatBubbleHook?.Dispose();
this.UpdateChatBubbleHook?.Dispose();
}
private void OpenChatBubbleDetour(IntPtr manager, IntPtr @object, IntPtr text, byte a4) {
try {
this.OpenChatBubbleDetourInner(manager, @object, text, a4);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in chat bubble detour");
this.OpenChatBubbleHook!.Original(manager, @object, text, a4);
}
}
private void OpenChatBubbleDetourInner(IntPtr manager, IntPtr objectPtr, IntPtr textPtr, byte a4) {
var @object = this.ObjectTable.CreateObjectReference(objectPtr);
if (@object == null) {
return;
}
var text = Util.ReadSeString(textPtr);
try {
this.OnChatBubble?.Invoke(ref @object, ref text);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in chat bubble event");
}
var newText = text.Encode().Terminate();
unsafe {
fixed (byte* newTextPtr = newText) {
this.OpenChatBubbleHook!.Original(manager, @object.Address, (IntPtr) newTextPtr, a4);
}
}
}
private void UpdateChatBubbleDetour(IntPtr bubblePtr, IntPtr @object) {
try {
this.UpdateChatBubbleDetourInner(bubblePtr, @object);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in update chat bubble detour");
this.UpdateChatBubbleHook!.Original(bubblePtr, @object);
}
}
private void UpdateChatBubbleDetourInner(IntPtr bubblePtr, IntPtr objectPtr) {
// var bubble = (ChatBubble*) bubblePtr;
var @object = this.ObjectTable.CreateObjectReference(objectPtr);
if (@object == null) {
return;
}
try {
this.OnUpdateBubble?.Invoke(ref @object);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in chat bubble update event");
}
this.UpdateChatBubbleHook!.Original(bubblePtr, @object.Address);
if (scanner.TryScanText(Signatures.ChatBubbleUpdate, out var updatePtr, "chat bubbles update")) {
this.UpdateChatBubbleHook = interop.HookFromAddress<UpdateChatBubbleDelegate>(updatePtr + 9, this.UpdateChatBubbleDetour);
this.UpdateChatBubbleHook.Enable();
}
}
[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
/// <inheritdoc />
public void Dispose() {
this.OpenChatBubbleHook?.Dispose();
this.UpdateChatBubbleHook?.Dispose();
}
internal enum ChatBubbleStatus : uint {
GetData = 0,
On = 1,
Init = 2,
Off = 3,
private void OpenChatBubbleDetour(IntPtr manager, IntPtr @object, IntPtr text, byte a4) {
try {
this.OpenChatBubbleDetourInner(manager, @object, text, a4);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in chat bubble detour");
this.OpenChatBubbleHook!.Original(manager, @object, text, a4);
}
}
private 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 Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions {
/// <summary>
/// Duty Finder functions
/// </summary>
public class DutyFinder {
private static class Signatures {
internal const string OpenRegularDuty = "48 89 6C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B F9 41 0F B6 E8";
internal const string OpenRoulette = "E9 ?? ?? ?? ?? 8B 93 ?? ?? ?? ?? 48 83 C4 20";
namespace XivCommon.Functions;
/// <summary>
/// Duty Finder functions
/// </summary>
public class DutyFinder {
private static class Signatures {
internal const string OpenRegularDuty = "48 89 6C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B F9 41 0F B6 E8";
internal const string OpenRoulette = "E9 ?? ?? ?? ?? 8B 93 ?? ?? ?? ?? 48 83 C4 20";
}
private delegate IntPtr OpenDutyDelegate(IntPtr agent, uint contentFinderCondition, byte a3);
private delegate IntPtr OpenRouletteDelegate(IntPtr agent, byte roulette, byte a3);
private readonly OpenDutyDelegate? _openDuty;
private readonly OpenRouletteDelegate? _openRoulette;
internal DutyFinder(ISigScanner scanner) {
if (scanner.TryScanText(Signatures.OpenRegularDuty, out var openDutyPtr, "Duty Finder (open duty)")) {
this._openDuty = Marshal.GetDelegateForFunctionPointer<OpenDutyDelegate>(openDutyPtr);
}
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(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);
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);
}
}

View File

@ -4,64 +4,64 @@ using Dalamud.Game;
using Dalamud.Game.ClientState.Objects.Types;
using Framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace XivCommon.Functions {
/// <summary>
/// Class containing examine functions
/// </summary>
public class Examine {
private static class Signatures {
internal const string RequestCharacterInfo = "40 53 48 83 EC 40 48 8B D9 48 8B 49 10 48 8B 01 FF 90 ?? ?? ?? ?? BA";
}
namespace XivCommon.Functions;
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) {
// got this by checking what accesses rciData below
if (scanner.TryScanText(Signatures.RequestCharacterInfo, out var rciPtr, "Examine")) {
this.RequestCharacterInfo = Marshal.GetDelegateForFunctionPointer<RequestCharInfoDelegate>(rciPtr);
}
}
private RequestCharInfoDelegate? RequestCharacterInfo { get; }
/// <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);
internal Examine(ISigScanner scanner) {
// got this by checking what accesses rciData below
if (scanner.TryScanText(Signatures.RequestCharacterInfo, out var rciPtr, "Examine")) {
this.RequestCharacterInfo = Marshal.GetDelegateForFunctionPointer<RequestCharInfoDelegate>(rciPtr);
}
}
/// <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.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>
/// 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>
public class FriendList {
// Updated: 5.58-HF1
private const int InfoOffset = 0x28;
private const int LengthOffset = 0x10;
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()
public unsafe IList<FriendListEntry> List {
get {
var friendListAgent = (IntPtr) Framework.Instance()
->GetUiModule()
->GetAgentModule()
->GetAgentByInternalId(AgentId.SocialFriendList);
if (friendListAgent == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
var info = *(IntPtr*) (friendListAgent + InfoOffset);
if (info == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
var length = *(ushort*) (info + LengthOffset);
if (length == 0) {
return Array.Empty<FriendListEntry>();
}
var 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;
->GetAgentByInternalId(AgentId.SocialFriendList);
if (friendListAgent == IntPtr.Zero) {
return Array.Empty<FriendListEntry>();
}
}
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.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>
/// An entry in a player's friend list.
/// The content ID of the friend.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = Size)]
public unsafe struct FriendListEntry {
internal const int Size = 104;
[FieldOffset(0x00)]
public readonly ulong ContentId;
/// <summary>
/// The content ID of the friend.
/// </summary>
[FieldOffset(0x00)]
public readonly ulong ContentId;
/// <summary>
/// The current world of the friend.
/// </summary>
[FieldOffset(0x1E)]
public readonly ushort CurrentWorld;
/// <summary>
/// The current world of the friend.
/// </summary>
[FieldOffset(0x1E)]
public readonly ushort CurrentWorld;
/// <summary>
/// The home world of the friend.
/// </summary>
[FieldOffset(0x20)]
public readonly ushort HomeWorld;
/// <summary>
/// The home world of the friend.
/// </summary>
[FieldOffset(0x20)]
public readonly ushort HomeWorld;
/// <summary>
/// The job the friend is currently on.
/// </summary>
[FieldOffset(0x29)]
public readonly byte Job;
/// <summary>
/// The job the friend is currently on.
/// </summary>
[FieldOffset(0x29)]
public readonly byte Job;
/// <summary>
/// The friend's raw SeString name. See <see cref="Name"/>.
/// </summary>
[FieldOffset(0x2A)]
public fixed byte RawName[32];
/// <summary>
/// The friend's raw SeString name. See <see cref="Name"/>.
/// </summary>
[FieldOffset(0x2A)]
public fixed byte RawName[32];
/// <summary>
/// The friend's raw SeString free company tag. See <see cref="FreeCompany"/>.
/// </summary>
[FieldOffset(0x4A)]
public fixed byte RawFreeCompany[5];
/// <summary>
/// The friend's raw SeString free company tag. See <see cref="FreeCompany"/>.
/// </summary>
[FieldOffset(0x4A)]
public fixed byte RawFreeCompany[5];
/// <summary>
/// The friend's name.
/// </summary>
public SeString Name {
get {
fixed (byte* ptr = this.RawName) {
return MemoryHelper.ReadSeStringNullTerminated((IntPtr) ptr);
}
/// <summary>
/// The friend's name.
/// </summary>
public SeString Name {
get {
fixed (byte* ptr = this.RawName) {
return MemoryHelper.ReadSeStringNullTerminated((IntPtr) ptr);
}
}
}
/// <summary>
/// The friend's free company tag.
/// </summary>
public SeString FreeCompany {
get {
fixed (byte* ptr = this.RawFreeCompany) {
return MemoryHelper.ReadSeStringNullTerminated((IntPtr) ptr);
}
/// <summary>
/// The friend's free company tag.
/// </summary>
public SeString FreeCompany {
get {
fixed (byte* ptr = this.RawFreeCompany) {
return MemoryHelper.ReadSeStringNullTerminated((IntPtr) ptr);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,180 +2,180 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Game.Gui.PartyFinder;
using Dalamud.Game.Gui.PartyFinder.Types;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
namespace XivCommon.Functions;
/// <summary>
/// A class containing Party Finder functionality
/// </summary>
public class PartyFinder : IDisposable {
private static class Signatures {
internal const string RequestListings = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 40 0F 10 81";
internal const string JoinCrossParty = "E8 ?? ?? ?? ?? 41 0F B7 07 49 8B CC";
}
private delegate byte RequestPartyFinderListingsDelegate(IntPtr agent, byte categoryIdx);
private delegate IntPtr JoinPfDelegate(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5);
private RequestPartyFinderListingsDelegate? RequestPartyFinderListings { get; }
private Hook<RequestPartyFinderListingsDelegate>? RequestPfListingsHook { get; }
private Hook<JoinPfDelegate>? JoinPfHook { get; }
namespace XivCommon.Functions {
/// <summary>
/// A class containing Party Finder functionality
/// The delegate for party join events.
/// </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 ?? ?? ?? ?? 0F B7 47 28";
public delegate void JoinPfEventDelegate(PartyFinderListing listing);
/// <summary>
/// <para>
/// The event that is fired when the player joins a <b>cross-world</b> party via Party Finder.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.PartyFinderJoins"/> hook to be enabled.
/// </para>
/// </summary>
public event JoinPfEventDelegate? JoinParty;
private IPartyFinderGui PartyFinderGui { get; }
private bool JoinsEnabled { get; }
private bool ListingsEnabled { get; }
private IntPtr PartyFinderAgent { get; set; } = IntPtr.Zero;
private Dictionary<uint, PartyFinderListing> Listings { get; } = new();
private int LastBatch { get; set; } = -1;
/// <summary>
/// <para>
/// The current Party Finder listings that have been displayed.
/// </para>
/// <para>
/// This dictionary is cleared and updated each time the Party Finder is requested, and it only contains the category selected in the Party Finder addon.
/// </para>
/// <para>
/// Keys are the listing ID for fast lookup by ID. Values are the listing itself.
/// </para>
/// </summary>
public IReadOnlyDictionary<uint, PartyFinderListing> CurrentListings => this.Listings;
internal PartyFinder(ISigScanner scanner, IPartyFinderGui partyFinderGui, IGameInteropProvider interop, Hooks hooks) {
this.PartyFinderGui = partyFinderGui;
this.ListingsEnabled = hooks.HasFlag(Hooks.PartyFinderListings);
this.JoinsEnabled = hooks.HasFlag(Hooks.PartyFinderJoins);
if (this.ListingsEnabled || this.JoinsEnabled) {
this.PartyFinderGui.ReceiveListing += this.ReceiveListing;
}
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);
private RequestPartyFinderListingsDelegate? RequestPartyFinderListings { get; }
private Hook<RequestPartyFinderListingsDelegate>? RequestPfListingsHook { get; }
private Hook<JoinPfDelegate>? JoinPfHook { get; }
/// <summary>
/// The delegate for party join events.
/// </summary>
public delegate void JoinPfEventDelegate(PartyFinderListing listing);
/// <summary>
/// <para>
/// The event that is fired when the player joins a <b>cross-world</b> party via Party Finder.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.PartyFinderJoins"/> hook to be enabled.
/// </para>
/// </summary>
public event JoinPfEventDelegate? JoinParty;
private PartyFinderGui PartyFinderGui { get; }
private bool JoinsEnabled { get; }
private bool ListingsEnabled { get; }
private IntPtr PartyFinderAgent { get; set; } = IntPtr.Zero;
private Dictionary<uint, PartyFinderListing> Listings { get; } = new();
private int LastBatch { get; set; } = -1;
/// <summary>
/// <para>
/// The current Party Finder listings that have been displayed.
/// </para>
/// <para>
/// This dictionary is cleared and updated each time the Party Finder is requested, and it only contains the category selected in the Party Finder addon.
/// </para>
/// <para>
/// Keys are the listing ID for fast lookup by ID. Values are the listing itself.
/// </para>
/// </summary>
public IReadOnlyDictionary<uint, PartyFinderListing> CurrentListings => this.Listings;
internal PartyFinder(SigScanner scanner, PartyFinderGui partyFinderGui, Hooks hooks) {
this.PartyFinderGui = partyFinderGui;
this.ListingsEnabled = hooks.HasFlag(Hooks.PartyFinderListings);
this.JoinsEnabled = hooks.HasFlag(Hooks.PartyFinderJoins);
if (this.ListingsEnabled || this.JoinsEnabled) {
this.PartyFinderGui.ReceiveListing += this.ReceiveListing;
}
if (scanner.TryScanText(Signatures.RequestListings, out var requestPfPtr, "Party Finder listings")) {
this.RequestPartyFinderListings = Marshal.GetDelegateForFunctionPointer<RequestPartyFinderListingsDelegate>(requestPfPtr);
if (this.ListingsEnabled) {
this.RequestPfListingsHook = 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();
if (this.ListingsEnabled) {
this.RequestPfListingsHook = interop.HookFromAddress<RequestPartyFinderListingsDelegate>(requestPfPtr, this.OnRequestPartyFinderListings);
this.RequestPfListingsHook.Enable();
}
}
/// <inheritdoc />
public void Dispose() {
this.PartyFinderGui.ReceiveListing -= this.ReceiveListing;
this.JoinPfHook?.Dispose();
this.RequestPfListingsHook?.Dispose();
if (this.JoinsEnabled && scanner.TryScanText(Signatures.JoinCrossParty, out var joinPtr, "Party Finder joins")) {
this.JoinPfHook = interop.HookFromAddress<JoinPfDelegate>(joinPtr, this.JoinPfDetour);
this.JoinPfHook.Enable();
}
}
/// <inheritdoc />
public void Dispose() {
this.PartyFinderGui.ReceiveListing -= this.ReceiveListing;
this.JoinPfHook?.Dispose();
this.RequestPfListingsHook?.Dispose();
}
private void ReceiveListing(PartyFinderListing listing, PartyFinderListingEventArgs args) {
if (args.BatchNumber != this.LastBatch) {
this.Listings.Clear();
}
private void ReceiveListing(PartyFinderListing listing, PartyFinderListingEventArgs args) {
if (args.BatchNumber != this.LastBatch) {
this.Listings.Clear();
}
this.LastBatch = args.BatchNumber;
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) {
this.PartyFinderAgent = agent;
return this.RequestPfListingsHook!.Original(agent, categoryIdx);
}
private IntPtr JoinPfDetour(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5) {
// Updated: 5.5
const int idOffset = -0x20;
private IntPtr JoinPfDetour(IntPtr manager, IntPtr a2, int type, IntPtr packetData, uint a5) {
// Updated: 5.5
const int idOffset = -0x20;
var ret = this.JoinPfHook!.Original(manager, a2, type, packetData, a5);
if (this.JoinParty == null || (JoinType) type != JoinType.PartyFinder || packetData == IntPtr.Zero) {
return ret;
}
try {
var id = (uint) Marshal.ReadInt32(packetData + idOffset);
if (this.Listings.TryGetValue(id, out var listing)) {
this.JoinParty?.Invoke(listing);
}
} catch (Exception ex) {
Logger.LogError(ex, "Exception in PF join detour");
}
var ret = this.JoinPfHook!.Original(manager, a2, type, packetData, a5);
if (this.JoinParty == null || (JoinType) type != JoinType.PartyFinder || packetData == IntPtr.Zero) {
return ret;
}
/// <summary>
/// <para>
/// Refresh the Party Finder listings. This does not open the Party Finder.
/// </para>
/// <para>
/// This maintains the currently selected category.
/// </para>
/// </summary>
/// <exception cref="InvalidOperationException">If the <see cref="Hooks.PartyFinderListings"/> hook is not enabled or if the signature for this function could not be found</exception>
public void RefreshListings() {
if (this.RequestPartyFinderListings == null) {
throw new InvalidOperationException("Could not find signature for Party Finder listings");
try {
var id = (uint) Marshal.ReadInt32(packetData + idOffset);
if (this.Listings.TryGetValue(id, out var listing)) {
this.JoinParty?.Invoke(listing);
}
if (!this.ListingsEnabled) {
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);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in PF join detour");
}
return ret;
}
internal enum JoinType : byte {
/// <summary>
/// Join via invite or party conversion.
/// </summary>
Normal = 0,
/// <summary>
/// <para>
/// Refresh the Party Finder listings. This does not open the Party Finder.
/// </para>
/// <para>
/// This maintains the currently selected category.
/// </para>
/// </summary>
/// <exception cref="InvalidOperationException">If the <see cref="Hooks.PartyFinderListings"/> hook is not enabled or if the signature for this function could not be found</exception>
public void RefreshListings() {
if (this.RequestPartyFinderListings == null) {
throw new InvalidOperationException("Could not find signature for Party Finder listings");
}
/// <summary>
/// Join via Party Finder.
/// </summary>
PartyFinder = 1,
if (!this.ListingsEnabled) {
throw new InvalidOperationException("PartyFinder hooks are not enabled");
}
Unknown2 = 2,
// Updated 6.0
const int categoryOffset = 11_031;
/// <summary>
/// Remain in cross-world party after leaving a duty.
/// </summary>
LeaveDuty = 3,
if (this.PartyFinderAgent == IntPtr.Zero) {
return;
}
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.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
namespace XivCommon.Functions;
/// <summary>
/// Class containing Talk events
/// </summary>
public class Talk : IDisposable {
private static class Signatures {
internal const string SetAtkValue = "E8 ?? ?? ?? ?? 41 03 ED";
internal const string ShowMessageBox = "4C 8B DC 55 57 41 55 49 8D 6B 98";
}
// Updated: 5.5
private const int TextOffset = 0;
private const int NameOffset = 0x10;
private const int StyleOffset = 0x38;
private delegate void AddonTalkV45Delegate(IntPtr addon, IntPtr a2, IntPtr data);
private Hook<AddonTalkV45Delegate>? AddonTalkV45Hook { get; }
private delegate IntPtr SetAtkValueStringDelegate(IntPtr atkValue, IntPtr text);
private SetAtkValueStringDelegate SetAtkValueString { get; } = null!;
namespace XivCommon.Functions {
/// <summary>
/// Class containing Talk events
/// The delegate for 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";
public delegate void TalkEventDelegate(ref SeString name, ref SeString text, ref TalkStyle style);
/// <summary>
/// <para>
/// The event that is fired when NPCs talk.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Talk"/> hook to be enabled.
/// </para>
/// </summary>
public event TalkEventDelegate? OnTalk;
internal Talk(ISigScanner scanner, IGameInteropProvider interop, bool hooksEnabled) {
if (scanner.TryScanText(Signatures.SetAtkValue, out var setAtkPtr, "Talk - set atk value")) {
this.SetAtkValueString = Marshal.GetDelegateForFunctionPointer<SetAtkValueStringDelegate>(setAtkPtr);
} else {
return;
}
// Updated: 5.5
private const int TextOffset = 0;
private const int NameOffset = 0x10;
private const int StyleOffset = 0x38;
private delegate void AddonTalkV45Delegate(IntPtr addon, IntPtr a2, IntPtr data);
private Hook<AddonTalkV45Delegate>? AddonTalkV45Hook { get; }
private delegate IntPtr SetAtkValueStringDelegate(IntPtr atkValue, IntPtr text);
private SetAtkValueStringDelegate SetAtkValueString { get; } = null!;
/// <summary>
/// The delegate for Talk events.
/// </summary>
public delegate void TalkEventDelegate(ref SeString name, ref SeString text, ref TalkStyle style);
/// <summary>
/// <para>
/// The event that is fired when NPCs talk.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Talk"/> hook to be enabled.
/// </para>
/// </summary>
public event TalkEventDelegate? OnTalk;
internal Talk(SigScanner scanner, bool hooksEnabled) {
if (scanner.TryScanText(Signatures.SetAtkValue, out var setAtkPtr, "Talk - set atk value")) {
this.SetAtkValueString = Marshal.GetDelegateForFunctionPointer<SetAtkValueStringDelegate>(setAtkPtr);
} else {
return;
}
if (!hooksEnabled) {
return;
}
if (scanner.TryScanText(Signatures.ShowMessageBox, out var showMessageBoxPtr, "Talk")) {
this.AddonTalkV45Hook = Hook<AddonTalkV45Delegate>.FromAddress(showMessageBoxPtr, this.AddonTalkV45Detour);
this.AddonTalkV45Hook.Enable();
}
if (!hooksEnabled) {
return;
}
/// <inheritdoc />
public void Dispose() {
this.AddonTalkV45Hook?.Dispose();
}
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);
}
}
if (scanner.TryScanText(Signatures.ShowMessageBox, out var showMessageBoxPtr, "Talk")) {
this.AddonTalkV45Hook = interop.HookFromAddress<AddonTalkV45Delegate>(showMessageBoxPtr, this.AddonTalkV45Detour);
this.AddonTalkV45Hook.Enable();
}
}
/// <summary>
/// Talk window styles.
/// </summary>
public enum TalkStyle : byte {
/// <summary>
/// The normal style with a white background.
/// </summary>
Normal = 0,
/// <inheritdoc />
public void Dispose() {
this.AddonTalkV45Hook?.Dispose();
}
/// <summary>
/// A style with lights on the top and bottom border.
/// </summary>
Lights = 2,
private void AddonTalkV45Detour(IntPtr addon, IntPtr a2, IntPtr data) {
if (this.OnTalk == null) {
goto Return;
}
/// <summary>
/// A style used for when characters are shouting.
/// </summary>
Shout = 3,
try {
this.AddonTalkV45DetourInner(data);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in Talk detour");
}
/// <summary>
/// Like <see cref="Shout"/> but with flatter edges.
/// </summary>
FlatShout = 4,
Return:
this.AddonTalkV45Hook!.Original(addon, a2, data);
}
/// <summary>
/// The style used when dragons (and some other NPCs) talk.
/// </summary>
Dragon = 5,
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);
/// <summary>
/// The style used for Allagan machinery.
/// </summary>
Allagan = 6,
var name = SeString.Parse(rawName);
var text = SeString.Parse(rawText);
/// <summary>
/// The style used for system messages.
/// </summary>
System = 7,
try {
this.OnTalk?.Invoke(ref name, ref text, ref style);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in Talk event");
}
/// <summary>
/// A mixture of the system message style and the dragon style.
/// </summary>
DragonSystem = 8,
var newName = name.Encode().Terminate();
var newText = text.Encode().Terminate();
/// <summary>
/// The system message style with a purple background.
/// </summary>
PurpleSystem = 9,
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>
/// Talk window styles.
/// </summary>
public enum TalkStyle : byte {
/// <summary>
/// The normal style with a white background.
/// </summary>
Normal = 0,
/// <summary>
/// A style with lights on the top and bottom border.
/// </summary>
Lights = 2,
/// <summary>
/// A style used for when characters are shouting.
/// </summary>
Shout = 3,
/// <summary>
/// Like <see cref="Shout"/> but with flatter edges.
/// </summary>
FlatShout = 4,
/// <summary>
/// The style used when dragons (and some other NPCs) talk.
/// </summary>
Dragon = 5,
/// <summary>
/// The style used for Allagan machinery.
/// </summary>
Allagan = 6,
/// <summary>
/// The style used for system messages.
/// </summary>
System = 7,
/// <summary>
/// A mixture of the system message style and the dragon style.
/// </summary>
DragonSystem = 8,
/// <summary>
/// The system message style with a purple background.
/// </summary>
PurpleSystem = 9,
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,94 +1,94 @@
using System;
namespace XivCommon.Functions.Tooltips {
/// <summary>
/// An enum containing the strings used in item tooltips.
/// </summary>
public enum ItemTooltipString {
#pragma warning disable 1591
Name = 0,
GlamourName = 1,
Type = 2,
Stat1Label = 4,
Stat2Label = 5,
Stat3Label = 6,
Stat1 = 7,
Stat2 = 8,
Stat3 = 9,
Stat1Delta = 10,
Stat2Delta = 11,
Stat3Delta = 12,
Description = 13,
Quantity = 14,
EffectsLabel = 15,
Effects = 16,
EquipJobs = 22,
EquipLevel = 23,
VendorSellPrice = 25,
Crafter = 26,
Level = 27,
Condition = 28,
SpiritbondLabel = 29,
Spiritbond = 30,
RepairLevel = 31,
Materials = 32,
QuickRepairs = 33,
MateriaMelding = 34,
Capabilities = 35,
BonusesLabel = 36,
Bonus1 = 37,
Bonus2 = 38,
Bonus3 = 39,
Bonus4 = 40,
MateriaLabel = 52,
Materia1 = 53,
Materia2 = 54,
Materia3 = 55,
Materia4 = 56,
Materia5 = 57,
Materia1Effect = 58,
Materia2Effect = 59,
Materia3Effect = 60,
Materia4Effect = 61,
Materia5Effect = 62,
ShopSellingPrice = 63,
ControllerControls = 64,
#pragma warning restore 1591
}
namespace XivCommon.Functions.Tooltips;
/// <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
}
/// <summary>
/// An enum containing the strings used in item tooltips.
/// </summary>
public enum ItemTooltipString {
#pragma warning disable 1591
Name = 0,
GlamourName = 1,
Type = 2,
Stat1Label = 4,
Stat2Label = 5,
Stat3Label = 6,
Stat1 = 7,
Stat2 = 8,
Stat3 = 9,
Stat1Delta = 10,
Stat2Delta = 11,
Stat3Delta = 12,
Description = 13,
Quantity = 14,
EffectsLabel = 15,
Effects = 16,
EquipJobs = 22,
EquipLevel = 23,
VendorSellPrice = 25,
Crafter = 26,
Level = 27,
Condition = 28,
SpiritbondLabel = 29,
Spiritbond = 30,
RepairLevel = 31,
Materials = 32,
QuickRepairs = 33,
MateriaMelding = 34,
Capabilities = 35,
BonusesLabel = 36,
Bonus1 = 37,
Bonus2 = 38,
Bonus3 = 39,
Bonus4 = 40,
MateriaLabel = 52,
Materia1 = 53,
Materia2 = 54,
Materia3 = 55,
Materia4 = 56,
Materia5 = 57,
Materia1Effect = 58,
Materia2Effect = 59,
Materia3Effect = 60,
Materia4Effect = 61,
Materia5Effect = 62,
ShopSellingPrice = 63,
ControllerControls = 64,
#pragma warning restore 1591
}
/// <summary>
/// An enum containing the fields that can be displayed in item tooltips.
/// </summary>
[Flags]
public enum ItemTooltipFields {
#pragma warning disable 1591
Crafter = 1 << 0,
Description = 1 << 1,
VendorSellPrice = 1 << 2,
// makes the tooltip much smaller when hovered over gear and unset
// something to do with EquipLevel maybe?
Unknown3 = 1 << 3,
Bonuses = 1 << 4,
Materia = 1 << 5,
CraftingAndRepairs = 1 << 6,
Effects = 1 << 8,
DyeableIndicator = 1 << 10,
Stat1 = 1 << 11,
Stat2 = 1 << 12,
Stat3 = 1 << 13,
/// <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.Gui;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
namespace XivCommon.Functions.Tooltips;
/// <summary>
/// The class containing tooltip functionality
/// </summary>
public class Tooltips : IDisposable {
private static class Signatures {
internal const string AgentItemDetailUpdateTooltip = "E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ?? 48 89 AE";
internal const string AgentActionDetailUpdateTooltip = "E8 ?? ?? ?? ?? EB 68 FF 50 40";
internal const string SadSetString = "E8 ?? ?? ?? ?? F6 47 14 08";
}
// Last checked: 6.0
// E8 ?? ?? ?? ?? EB 68 FF 50 40
private const int AgentActionDetailUpdateFlagOffset = 0x58;
internal unsafe delegate void StringArrayDataSetStringDelegate(IntPtr self, int index, byte* str, byte updatePtr, byte copyToUi, byte dontSetModified);
private unsafe delegate ulong ItemUpdateTooltipDelegate(IntPtr agent, int** numberArrayData, byte*** stringArrayData, float a4);
private unsafe delegate void ActionUpdateTooltipDelegate(IntPtr agent, int** numberArrayData, byte*** stringArrayData);
private StringArrayDataSetStringDelegate? SadSetString { get; }
private Hook<ItemUpdateTooltipDelegate>? ItemUpdateTooltipHook { get; }
private Hook<ActionUpdateTooltipDelegate>? ActionGenerateTooltipHook { get; }
namespace XivCommon.Functions.Tooltips {
/// <summary>
/// The class containing tooltip functionality
/// The delegate for item tooltip events.
/// </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";
public delegate void ItemTooltipEventDelegate(ItemTooltip itemTooltip, ulong itemId);
/// <summary>
/// The tooltip for action tooltip events.
/// </summary>
public delegate void ActionTooltipEventDelegate(ActionTooltip actionTooltip, HoveredAction action);
/// <summary>
/// <para>
/// The event that is fired when an item tooltip is being generated for display.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Tooltips"/> hook to be enabled.
/// </para>
/// </summary>
public event ItemTooltipEventDelegate? OnItemTooltip;
/// <summary>
/// <para>
/// The event that is fired when an action tooltip is being generated for display.
/// </para>
/// <para>
/// Requires the <see cref="Hooks.Tooltips"/> hook to be enabled.
/// </para>
/// </summary>
public event ActionTooltipEventDelegate? OnActionTooltip;
private IGameGui GameGui { get; }
private ItemTooltip? ItemTooltip { get; set; }
private ActionTooltip? ActionTooltip { get; set; }
internal Tooltips(ISigScanner scanner, IGameGui gui, IGameInteropProvider interop, bool enabled) {
this.GameGui = gui;
if (scanner.TryScanText(Signatures.SadSetString, out var setStringPtr, "Tooltips - StringArrayData::SetString")) {
this.SadSetString = Marshal.GetDelegateForFunctionPointer<StringArrayDataSetStringDelegate>(setStringPtr);
} else {
return;
}
// 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; }
/// <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();
}
if (!enabled) {
return;
}
/// <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 {
this.ItemUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in item tooltip detour");
}
if (scanner.TryScanText(Signatures.AgentItemDetailUpdateTooltip, out var updateItemPtr, "Tooltips - Items")) {
unsafe {
this.ItemUpdateTooltipHook = interop.HookFromAddress<ItemUpdateTooltipDelegate>(updateItemPtr, this.ItemUpdateTooltipDetour);
}
return ret;
this.ItemUpdateTooltipHook.Enable();
}
private unsafe void ItemUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.ItemTooltip = new ItemTooltip(this.SadSetString!, stringArrayData, numberArrayData);
if (scanner.TryScanText(Signatures.AgentActionDetailUpdateTooltip, out var updateActionPtr, "Tooltips - Actions")) {
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 {
this.OnItemTooltip?.Invoke(this.ItemTooltip, this.GameGui.HoveredItem);
this.ItemUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} 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) {
var flag = *(byte*) (agent + AgentActionDetailUpdateFlagOffset);
this.ActionGenerateTooltipHook!.Original(agent, numberArrayData, stringArrayData);
return ret;
}
if (flag == 0) {
return;
}
private unsafe void ItemUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.ItemTooltip = new ItemTooltip(this.SadSetString!, stringArrayData, numberArrayData);
try {
this.ActionUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in action tooltip detour");
}
try {
this.OnItemTooltip?.Invoke(this.ItemTooltip, this.GameGui.HoveredItem);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in OnItemTooltip event");
}
}
private unsafe void ActionUpdateTooltipDetour(IntPtr agent, int** numberArrayData, byte*** stringArrayData) {
var flag = *(byte*) (agent + AgentActionDetailUpdateFlagOffset);
this.ActionGenerateTooltipHook!.Original(agent, numberArrayData, stringArrayData);
if (flag == 0) {
return;
}
private unsafe void ActionUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.ActionTooltip = new ActionTooltip(this.SadSetString!, stringArrayData, numberArrayData);
try {
this.ActionUpdateTooltipDetourInner(numberArrayData, stringArrayData);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in action tooltip detour");
}
}
try {
this.OnActionTooltip?.Invoke(this.ActionTooltip, this.GameGui.HoveredAction);
} catch (Exception ex) {
Logger.LogError(ex, "Exception in OnActionTooltip event");
}
private unsafe void ActionUpdateTooltipDetourInner(int** numberArrayData, byte*** stringArrayData) {
this.ActionTooltip = new ActionTooltip(this.SadSetString!, stringArrayData, numberArrayData);
try {
this.OnActionTooltip?.Invoke(this.ActionTooltip, this.GameGui.HoveredAction);
} catch (Exception ex) {
Logger.Log.Error(ex, "Exception in OnActionTooltip event");
}
}
}

View File

@ -2,58 +2,58 @@
using System.Runtime.InteropServices;
using Dalamud.Game;
namespace XivCommon.Functions {
internal class UiAlloc {
private static class Signatures {
internal const string GameAlloc = "E8 ?? ?? ?? ?? 49 83 CC FF 4C 8B F0";
internal const string GameFree = "E8 ?? ?? ?? ?? 4C 89 7B 60";
internal const string GetGameAllocator = "E8 ?? ?? ?? ?? 8B 75 08";
namespace XivCommon.Functions;
internal class UiAlloc {
private static class Signatures {
internal const string GameAlloc = "E8 ?? ?? ?? ?? 49 83 CC FF 4C 8B F0";
internal const string GameFree = "E8 ?? ?? ?? ?? 4C 89 7B 60";
internal const string GetGameAllocator = "E8 ?? ?? ?? ?? 8B 75 08";
}
private delegate IntPtr GameAllocDelegate(ulong size, IntPtr unk, IntPtr allocator, IntPtr alignment);
private readonly GameAllocDelegate? _gameAlloc;
private delegate IntPtr GameFreeDelegate(IntPtr a1);
private readonly GameFreeDelegate? _gameFree;
private delegate IntPtr GetGameAllocatorDelegate();
private readonly GetGameAllocatorDelegate? _getGameAllocator;
internal UiAlloc(ISigScanner scanner) {
if (scanner.TryScanText(Signatures.GameAlloc, out var gameAllocPtr, "UiAlloc (GameAlloc)")) {
this._gameAlloc = Marshal.GetDelegateForFunctionPointer<GameAllocDelegate>(gameAllocPtr);
} else {
return;
}
private delegate IntPtr GameAllocDelegate(ulong size, IntPtr unk, IntPtr allocator, IntPtr alignment);
private readonly GameAllocDelegate? _gameAlloc;
private delegate IntPtr GameFreeDelegate(IntPtr a1);
private readonly GameFreeDelegate? _gameFree;
private delegate IntPtr GetGameAllocatorDelegate();
private readonly GetGameAllocatorDelegate? _getGameAllocator;
internal UiAlloc(SigScanner scanner) {
if (scanner.TryScanText(Signatures.GameAlloc, out var gameAllocPtr, "UiAlloc (GameAlloc)")) {
this._gameAlloc = Marshal.GetDelegateForFunctionPointer<GameAllocDelegate>(gameAllocPtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.GameFree, out var gameFreePtr, "UiAlloc (GameFree)")) {
this._gameFree = Marshal.GetDelegateForFunctionPointer<GameFreeDelegate>(gameFreePtr);
} else {
return;
}
if (scanner.TryScanText(Signatures.GetGameAllocator, out var getAllocatorPtr, "UiAlloc (GetGameAllocator)")) {
this._getGameAllocator = Marshal.GetDelegateForFunctionPointer<GetGameAllocatorDelegate>(getAllocatorPtr);
}
if (scanner.TryScanText(Signatures.GameFree, out var gameFreePtr, "UiAlloc (GameFree)")) {
this._gameFree = Marshal.GetDelegateForFunctionPointer<GameFreeDelegate>(gameFreePtr);
} else {
return;
}
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);
if (scanner.TryScanText(Signatures.GetGameAllocator, out var getAllocatorPtr, "UiAlloc (GetGameAllocator)")) {
this._getGameAllocator = Marshal.GetDelegateForFunctionPointer<GetGameAllocatorDelegate>(getAllocatorPtr);
}
}
internal IntPtr Alloc(ulong size) {
if (this._getGameAllocator == null || this._gameAlloc == null) {
throw new InvalidOperationException();
}
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 Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.PartyFinder;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using XivCommon.Functions;
@ -12,165 +10,168 @@ using XivCommon.Functions.NamePlates;
using XivCommon.Functions.Tooltips;
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>
/// A class containing game functions
/// Chat functions
/// </summary>
public class GameFunctions : IDisposable {
private GameGui GameGui { get; }
public Chat Chat { 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>
/// Chat functions
/// </summary>
public Chat Chat { get; }
/// <summary>
/// Examine functions
/// </summary>
public Examine Examine { get; }
/// <summary>
/// Party Finder functions and events
/// </summary>
public PartyFinder PartyFinder { get; }
/// <summary>
/// Talk events
/// </summary>
public Talk Talk { get; }
/// <summary>
/// BattleTalk functions and events
/// </summary>
public BattleTalk BattleTalk { get; }
/// <summary>
/// Chat bubble functions and events
/// </summary>
public ChatBubbles ChatBubbles { get; }
/// <summary>
/// Examine functions
/// </summary>
public Examine Examine { get; }
/// <summary>
/// Tooltip events
/// </summary>
public Tooltips Tooltips { get; }
/// <summary>
/// Talk events
/// </summary>
public Talk Talk { get; }
/// <summary>
/// Name plate tools and events
/// </summary>
public NamePlates NamePlates { get; }
/// <summary>
/// Chat bubble functions and events
/// </summary>
public ChatBubbles ChatBubbles { get; }
/// <summary>
/// Duty Finder functions
/// </summary>
public DutyFinder DutyFinder { get; }
/// <summary>
/// Tooltip events
/// </summary>
public Tooltips Tooltips { get; }
/// <summary>
/// Friend list functions
/// </summary>
public FriendList FriendList { get; }
/// <summary>
/// Name plate tools and events
/// </summary>
public NamePlates NamePlates { get; }
/// <summary>
/// Journal functions
/// </summary>
public Journal Journal { get; }
/// <summary>
/// Duty Finder functions
/// </summary>
public DutyFinder DutyFinder { get; }
/// <summary>
/// Housing functions
/// </summary>
public Housing Housing { get; }
/// <summary>
/// Friend list functions
/// </summary>
public FriendList FriendList { get; }
internal GameFunctions(Hooks hooks) {
Logger.Log = Util.GetService<IPluginLog>();
/// <summary>
/// Journal functions
/// </summary>
public Journal Journal { get; }
/// <summary>
/// Housing functions
/// </summary>
public Housing Housing { get; }
this.Framework = Util.GetService<IFramework>();
this.GameGui = Util.GetService<IGameGui>();
internal GameFunctions(Hooks hooks) {
this.Framework = Util.GetService<Dalamud.Game.Framework>();
this.GameGui = Util.GetService<GameGui>();
var interop = Util.GetService<IGameInteropProvider>();
var objectTable = Util.GetService<IObjectTable>();
var partyFinderGui = Util.GetService<IPartyFinderGui>();
var scanner = Util.GetService<ISigScanner>();
var objectTable = Util.GetService<ObjectTable>();
var partyFinderGui = Util.GetService<PartyFinderGui>();
var scanner = Util.GetService<SigScanner>();
this.UiAlloc = new UiAlloc(scanner);
this.Chat = new Chat(scanner);
this.PartyFinder = new PartyFinder(scanner, partyFinderGui, interop, hooks);
this.BattleTalk = new BattleTalk(interop, hooks.HasFlag(Hooks.BattleTalk));
this.Examine = new Examine(scanner);
this.Talk = new Talk(scanner, interop, hooks.HasFlag(Hooks.Talk));
this.ChatBubbles = new ChatBubbles(objectTable, scanner, interop, hooks.HasFlag(Hooks.ChatBubbles));
this.Tooltips = new Tooltips(scanner, this.GameGui, interop, hooks.HasFlag(Hooks.Tooltips));
this.NamePlates = new NamePlates(this, scanner, interop, hooks.HasFlag(Hooks.NamePlates));
this.DutyFinder = new DutyFinder(scanner);
this.Journal = new Journal(scanner);
this.FriendList = new FriendList();
this.Housing = new Housing(scanner);
}
this.UiAlloc = new UiAlloc(scanner);
this.Chat = new Chat(scanner);
this.PartyFinder = new PartyFinder(scanner, partyFinderGui, hooks);
this.BattleTalk = new BattleTalk(hooks.HasFlag(Hooks.BattleTalk));
this.Examine = new Examine(scanner);
this.Talk = new Talk(scanner, hooks.HasFlag(Hooks.Talk));
this.ChatBubbles = new ChatBubbles(objectTable, scanner, hooks.HasFlag(Hooks.ChatBubbles));
this.Tooltips = new Tooltips(scanner, this.GameGui, hooks.HasFlag(Hooks.Tooltips));
this.NamePlates = new NamePlates(this, scanner, hooks.HasFlag(Hooks.NamePlates));
this.DutyFinder = new DutyFinder(scanner);
this.Journal = new Journal(scanner);
this.FriendList = new FriendList();
this.Housing = new Housing(scanner);
}
/// <inheritdoc />
public void Dispose() {
this.NamePlates.Dispose();
this.Tooltips.Dispose();
this.ChatBubbles.Dispose();
this.Talk.Dispose();
this.BattleTalk.Dispose();
this.PartyFinder.Dispose();
}
/// <inheritdoc />
public void Dispose() {
this.NamePlates.Dispose();
this.Tooltips.Dispose();
this.ChatBubbles.Dispose();
this.Talk.Dispose();
this.BattleTalk.Dispose();
this.PartyFinder.Dispose();
}
/// <summary>
/// Convenience method to get a pointer to <see cref="Framework"/>.
/// </summary>
/// <returns>pointer to struct</returns>
[Obsolete("Use Framework.Instance()")]
public unsafe Framework* GetFramework() {
return FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
}
/// <summary>
/// Convenience method to get a pointer to <see cref="Framework"/>.
/// </summary>
/// <returns>pointer to struct</returns>
[Obsolete("Use Framework.Instance()")]
public unsafe Framework* GetFramework() {
return (Framework*) this.Framework.Address.BaseAddress;
}
/// <summary>
/// Gets the pointer to the UI module
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()")]
public unsafe IntPtr GetUiModule() {
return (IntPtr) this.GetFramework()->GetUiModule();
}
/// <summary>
/// Gets the pointer to the UI module
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()")]
public unsafe IntPtr GetUiModule() {
return (IntPtr) this.GetFramework()->GetUiModule();
}
/// <summary>
/// Gets the pointer to the RaptureAtkModule
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetRaptureAtkModule()")]
public unsafe IntPtr GetAtkModule() {
return (IntPtr) this.GetFramework()->GetUiModule()->GetRaptureAtkModule();
}
/// <summary>
/// Gets the pointer to the RaptureAtkModule
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetRaptureAtkModule()")]
public unsafe IntPtr GetAtkModule() {
return (IntPtr) this.GetFramework()->GetUiModule()->GetRaptureAtkModule();
}
/// <summary>
/// Gets the pointer to the agent module
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetAgentModule()")]
public unsafe IntPtr GetAgentModule() {
return (IntPtr) this.GetFramework()->GetUiModule()->GetAgentModule();
}
/// <summary>
/// Gets the pointer to the agent module
/// </summary>
/// <returns>Pointer</returns>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetAgentModule()")]
public unsafe IntPtr GetAgentModule() {
return (IntPtr) this.GetFramework()->GetUiModule()->GetAgentModule();
}
/// <summary>
/// Gets the pointer to an agent from its internal ID.
/// </summary>
/// <param name="id">internal id of agent</param>
/// <returns>Pointer</returns>
/// <exception cref="InvalidOperationException">if the signature for the function could not be found</exception>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId)")]
public unsafe IntPtr GetAgentByInternalId(uint id) {
return (IntPtr) this.GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId((AgentId) id);
}
/// <summary>
/// Gets the pointer to an agent from its internal ID.
/// </summary>
/// <param name="id">internal id of agent</param>
/// <returns>Pointer</returns>
/// <exception cref="InvalidOperationException">if the signature for the function could not be found</exception>
[Obsolete("Use Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId)")]
public unsafe IntPtr GetAgentByInternalId(uint id) {
return (IntPtr) this.GetFramework()->GetUiModule()->GetAgentModule()->GetAgentByInternalId((AgentId) id);
}
/// <summary>
/// Gets the pointer to the AtkStage singleton
/// </summary>
/// <returns>Pointer</returns>
/// <exception cref="InvalidOperationException">if the signature for the function could not be found</exception>
[Obsolete("Use AtkStage.GetSingleton()")]
public unsafe IntPtr GetAtkStageSingleton() {
return (IntPtr) AtkStage.GetSingleton();
}
/// <summary>
/// Gets the pointer to the AtkStage singleton
/// </summary>
/// <returns>Pointer</returns>
/// <exception cref="InvalidOperationException">if the signature for the function could not be found</exception>
[Obsolete("Use AtkStage.GetSingleton()")]
public unsafe IntPtr GetAtkStageSingleton() {
return (IntPtr) AtkStage.GetSingleton();
}
}

View File

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

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

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

View File

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

@ -14,7 +14,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Title>XivCommon</Title>
<Authors>ascclemens</Authors>
<RepositoryUrl>https://git.annaclemens.io/ascclemens/XivCommon</RepositoryUrl>
<RepositoryUrl>https://git.anna.lgbt/ascclemens/XivCommon</RepositoryUrl>
<Description>A set of common functions, hooks, and events not included in Dalamud.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>

View File

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