From 57c6089fc1b263ea904df2e253cef1b1aea2afc3 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 4 Aug 2025 02:43:52 +0200 Subject: [PATCH] [Api13] Add native wrapper structs (#2330) --- .../Addon/Events/PluginEventController.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 67 +++---- .../AddonArgTypes/AddonReceiveEventArgs.cs | 10 + .../AddonArgTypes/AddonRefreshArgs.cs | 8 + .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 10 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 10 +- .../AddonArgTypes/AddonUpdateArgs.cs | 7 + .../Game/Addon/Lifecycle/AddonLifecycle.cs | 18 +- .../AddonLifecycleReceiveEventListener.cs | 5 +- Dalamud/Game/Gui/Dtr/DtrBar.cs | 4 +- Dalamud/Game/Gui/GameGui.cs | 95 ++++------ Dalamud/Game/Gui/NamePlate/NamePlateGui.cs | 2 +- .../Game/NativeWrapper/AgentInterfacePtr.cs | 96 ++++++++++ Dalamud/Game/NativeWrapper/AtkUnitBasePtr.cs | 171 ++++++++++++++++++ Dalamud/Game/NativeWrapper/AtkValuePtr.cs | 113 ++++++++++++ Dalamud/Game/NativeWrapper/AtkValueType.cs | 87 +++++++++ Dalamud/Game/NativeWrapper/UIModulePtr.cs | 51 ++++++ Dalamud/Interface/Internal/UiDebug.cs | 6 +- .../Internal/UiDebug2/Browsing/AddonTree.cs | 6 +- .../Internal/UiDebug2/UiDebug2.Sidebar.cs | 2 +- .../Windows/Data/Widgets/AddonWidget.cs | 24 ++- .../Internal/Windows/TitleScreenMenuWindow.cs | 2 +- Dalamud/Plugin/Services/IGameGui.cs | 35 ++-- 23 files changed, 682 insertions(+), 149 deletions(-) create mode 100644 Dalamud/Game/NativeWrapper/AgentInterfacePtr.cs create mode 100644 Dalamud/Game/NativeWrapper/AtkUnitBasePtr.cs create mode 100644 Dalamud/Game/NativeWrapper/AtkValuePtr.cs create mode 100644 Dalamud/Game/NativeWrapper/AtkValueType.cs create mode 100644 Dalamud/Game/NativeWrapper/UIModulePtr.cs diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs index b1251855b..950087c5c 100644 --- a/Dalamud/Game/Addon/Events/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -140,7 +140,7 @@ internal unsafe class PluginEventController : IDisposable if (currentAddonPointer != eventEntry.Addon) return; // Make sure the addon is not unloaded - var atkUnitBase = (AtkUnitBase*)currentAddonPointer; + var atkUnitBase = currentAddonPointer.Struct; if (atkUnitBase->UldManager.LoadedState == AtkLoadState.Unloaded) return; // Does this addon contain the node this event is for? (by address) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 36083337e..8a97f5cc2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -1,8 +1,4 @@ -using System.Runtime.CompilerServices; - -using Dalamud.Memory; - -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Game.NativeWrapper; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -17,7 +13,6 @@ public abstract unsafe class AddonArgs public const string InvalidAddon = "NullAddon"; private string? addonName; - private IntPtr addon; /// /// Gets the name of the addon this args referrers to. @@ -27,10 +22,10 @@ public abstract unsafe class AddonArgs /// /// Gets the pointer to the addons AtkUnitBase. /// - public nint Addon + public AtkUnitBasePtr Addon { - get => this.AddonInternal; - init => this.AddonInternal = value; + get; + internal set; } /// @@ -38,22 +33,6 @@ public abstract unsafe class AddonArgs /// public abstract AddonArgsType Type { get; } - /// - /// Gets or sets the pointer to the addons AtkUnitBase. - /// - internal nint AddonInternal - { - get => this.addon; - set - { - this.addon = value; - - // Note: always clear addonName on updating the addon being pointed. - // Same address may point to a different addon. - this.addonName = null; - } - } - /// /// Checks if addon name matches the given span of char. /// @@ -61,19 +40,27 @@ public abstract unsafe class AddonArgs /// Whether it is the case. internal bool IsAddon(ReadOnlySpan name) { - if (this.Addon == nint.Zero) return false; - if (name.Length is 0 or > 0x20) + if (this.Addon.IsNull) return false; - var addonPointer = (AtkUnitBase*)this.Addon; - if (addonPointer->Name[0] == 0) return false; + if (name.Length is 0 or > 32) + return false; - // note: might want to rewrite this to just compare to NameString - return MemoryHelper.EqualsZeroTerminatedString( - name, - (nint)Unsafe.AsPointer(ref addonPointer->Name[0]), - null, - 0x20); + var addonName = this.Addon.Name; + + if (string.IsNullOrEmpty(addonName)) + return false; + + return name == addonName; + } + + /// + /// Clears this AddonArgs values. + /// + internal virtual void Clear() + { + this.addonName = null; + this.Addon = 0; } /// @@ -82,11 +69,13 @@ public abstract unsafe class AddonArgs /// The name of the addon for this object. when invalid. private string GetAddonName() { - if (this.Addon == nint.Zero) return InvalidAddon; + if (this.Addon.IsNull) return InvalidAddon; - var addonPointer = (AtkUnitBase*)this.Addon; - if (addonPointer->Name[0] == 0) return InvalidAddon; + var name = this.Addon.Name; - return this.addonName ??= addonPointer->NameString; + if (string.IsNullOrEmpty(name)) + return InvalidAddon; + + return this.addonName ??= name; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index a557b0cb3..980fe4f2f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -41,4 +41,14 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable /// object ICloneable.Clone() => this.Clone(); + + /// + internal override void Clear() + { + base.Clear(); + this.AtkEventType = default; + this.EventParam = default; + this.AtkEvent = default; + this.Data = default; + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index 6e1b11ead..d28631c3c 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -38,4 +38,12 @@ public class AddonRefreshArgs : AddonArgs, ICloneable /// object ICloneable.Clone() => this.Clone(); + + /// + internal override void Clear() + { + base.Clear(); + this.AtkValueCount = default; + this.AtkValues = default; + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index 26357abb0..e87a980fd 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for OnRequestedUpdate events. @@ -31,4 +31,12 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable /// object ICloneable.Clone() => this.Clone(); + + /// + internal override void Clear() + { + base.Clear(); + this.NumberArrayData = default; + this.StringArrayData = default; + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 19c93ce25..0dd9ecee2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; +using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -38,4 +38,12 @@ public class AddonSetupArgs : AddonArgs, ICloneable /// object ICloneable.Clone() => this.Clone(); + + /// + internal override void Clear() + { + base.Clear(); + this.AtkValueCount = default; + this.AtkValues = default; + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index cc34a7531..a263f6ae4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -35,4 +35,11 @@ public class AddonUpdateArgs : AddonArgs, ICloneable /// object ICloneable.Clone() => this.Clone(); + + /// + internal override void Clear() + { + base.Clear(); + this.TimeDeltaInternal = default; + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index b9179edde..b44ab8764 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -238,7 +238,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } using var returner = this.argsPool.Rent(out AddonSetupArgs arg); - arg.AddonInternal = (nint)addon; + arg.Clear(); + arg.Addon = (nint)addon; arg.AtkValueCount = valueCount; arg.AtkValues = (nint)values; this.InvokeListenersSafely(AddonEvent.PreSetup, arg); @@ -270,7 +271,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg); - arg.AddonInternal = (nint)atkUnitBase[0]; + arg.Clear(); + arg.Addon = (nint)atkUnitBase[0]; this.InvokeListenersSafely(AddonEvent.PreFinalize, arg); try @@ -286,7 +288,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private void OnAddonDraw(AtkUnitBase* addon) { using var returner = this.argsPool.Rent(out AddonDrawArgs arg); - arg.AddonInternal = (nint)addon; + arg.Clear(); + arg.Addon = (nint)addon; this.InvokeListenersSafely(AddonEvent.PreDraw, arg); try @@ -304,7 +307,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private void OnAddonUpdate(AtkUnitBase* addon, float delta) { using var returner = this.argsPool.Rent(out AddonUpdateArgs arg); - arg.AddonInternal = (nint)addon; + arg.Clear(); + arg.Addon = (nint)addon; arg.TimeDeltaInternal = delta; this.InvokeListenersSafely(AddonEvent.PreUpdate, arg); @@ -325,7 +329,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService var result = false; using var returner = this.argsPool.Rent(out AddonRefreshArgs arg); - arg.AddonInternal = (nint)addon; + arg.Clear(); + arg.Addon = (nint)addon; arg.AtkValueCount = valueCount; arg.AtkValues = (nint)values; this.InvokeListenersSafely(AddonEvent.PreRefresh, arg); @@ -348,7 +353,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg); - arg.AddonInternal = (nint)addon; + arg.Clear(); + arg.Addon = (nint)addon; arg.NumberArrayData = (nint)numberArrayData; arg.StringArrayData = (nint)stringArrayData; this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs index a66440b25..0d2bcc7f2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; @@ -86,7 +86,8 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable } using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg); - arg.AddonInternal = (nint)addon; + arg.Clear(); + arg.Addon = (nint)addon; arg.AtkEventType = (byte)eventType; arg.EventParam = eventParam; arg.AtkEvent = (IntPtr)atkEvent; diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 8339838fd..18d8b7f86 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -330,7 +330,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar this.entriesReadOnlyCopy = null; } - private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); + private AtkUnitBase* GetDtr() => this.gameGui.GetAddonByName("_DTR").Struct; private void Update(IFramework unused) { @@ -427,7 +427,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar private void FixCollision(AddonEvent eventType, AddonArgs addonInfo) { - var addon = (AtkUnitBase*)addonInfo.Addon; + var addon = addonInfo.Addon.Struct; if (addon->RootNode is null || addon->UldManager.NodeList is null) return; float minX = addon->RootNode->Width; diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index ada9021c4..b021a2ad2 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; +using Dalamud.Game.NativeWrapper; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.Interface.Utility; @@ -167,79 +168,59 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui } /// - public IntPtr GetUIModule() + public UIModulePtr GetUIModule() { - var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); - if (framework == null) - return IntPtr.Zero; - - var uiModule = framework->GetUIModule(); - if (uiModule == null) - return IntPtr.Zero; - - return (IntPtr)uiModule; + return (nint)UIModule.Instance(); } /// - public IntPtr GetAddonByName(string name, int index = 1) + public AtkUnitBasePtr GetAddonByName(string name, int index = 1) { - var atkStage = AtkStage.Instance(); - if (atkStage == null) - return IntPtr.Zero; + var unitManager = RaptureAtkUnitManager.Instance(); + if (unitManager == null) + return 0; - var unitMgr = atkStage->RaptureAtkUnitManager; - if (unitMgr == null) - return IntPtr.Zero; - - var addon = unitMgr->GetAddonByName(name, index); - if (addon == null) - return IntPtr.Zero; - - return (IntPtr)addon; + return (nint)unitManager->GetAddonByName(name, index); } /// - public IntPtr FindAgentInterface(string addonName) + public AgentInterfacePtr GetAgentById(int id) + { + var agentModule = AgentModule.Instance(); + if (agentModule == null || id < 0 || id >= agentModule->Agents.Length) + return 0; + + return (nint)agentModule->Agents[id].Value; + } + + /// + public AgentInterfacePtr FindAgentInterface(string addonName) { var addon = this.GetAddonByName(addonName); return this.FindAgentInterface(addon); } /// - public IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon); - - /// - public IntPtr FindAgentInterface(IntPtr addonPtr) + public AgentInterfacePtr FindAgentInterface(AtkUnitBasePtr addon) { - if (addonPtr == IntPtr.Zero) - return IntPtr.Zero; + if (addon.IsNull) + return 0; - var uiModule = (UIModule*)this.GetUIModule(); - if (uiModule == null) - return IntPtr.Zero; - - var agentModule = uiModule->GetAgentModule(); + var agentModule = AgentModule.Instance(); if (agentModule == null) - return IntPtr.Zero; - - var addon = (AtkUnitBase*)addonPtr; - var addonId = addon->ParentId == 0 ? addon->Id : addon->ParentId; + return 0; + var addonId = addon.ParentId == 0 ? addon.Id : addon.ParentId; if (addonId == 0) - return IntPtr.Zero; + return 0; - var index = 0; - while (true) + foreach (AgentInterface* agent in agentModule->Agents) { - var agent = agentModule->GetAgentByInternalId((AgentId)index++); - if (agent == uiModule || agent == null) - break; - - if (agent->AddonId == addonId) - return new IntPtr(agent); + if (agent != null && agent->AddonId == addonId) + return (nint)agent; } - return IntPtr.Zero; + return 0; } /// @@ -454,25 +435,25 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui => this.gameGuiService.ScreenToWorld(screenPos, out worldPos, rayDistance); /// - public IntPtr GetUIModule() + public UIModulePtr GetUIModule() => this.gameGuiService.GetUIModule(); /// - public IntPtr GetAddonByName(string name, int index = 1) + public AtkUnitBasePtr GetAddonByName(string name, int index = 1) => this.gameGuiService.GetAddonByName(name, index); /// - public IntPtr FindAgentInterface(string addonName) + public AgentInterfacePtr GetAgentById(int id) + => this.gameGuiService.GetAgentById(id); + + /// + public AgentInterfacePtr FindAgentInterface(string addonName) => this.gameGuiService.FindAgentInterface(addonName); /// - public unsafe IntPtr FindAgentInterface(void* addon) + public AgentInterfacePtr FindAgentInterface(AtkUnitBasePtr addon) => this.gameGuiService.FindAgentInterface(addon); - /// - public IntPtr FindAgentInterface(IntPtr addonPtr) - => this.gameGuiService.FindAgentInterface(addonPtr); - private void UiHideToggledForward(object sender, bool toggled) => this.UiHideToggled?.Invoke(sender, toggled); private void HoveredItemForward(object sender, ulong itemId) => this.HoveredItemChanged?.Invoke(sender, itemId); diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs index cd84b996b..7f83f180c 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs @@ -72,7 +72,7 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui /// public unsafe void RequestRedraw() { - var addon = (AddonNamePlate*)this.gameGui.GetAddonByName("NamePlate"); + var addon = (AddonNamePlate*)(nint)this.gameGui.GetAddonByName("NamePlate"); if (addon != null) { addon->DoFullUpdate = 1; diff --git a/Dalamud/Game/NativeWrapper/AgentInterfacePtr.cs b/Dalamud/Game/NativeWrapper/AgentInterfacePtr.cs new file mode 100644 index 000000000..b5a6375a9 --- /dev/null +++ b/Dalamud/Game/NativeWrapper/AgentInterfacePtr.cs @@ -0,0 +1,96 @@ +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace Dalamud.Game.NativeWrapper; + +/// +/// A readonly wrapper for AgentInterface. +/// +/// The address to the AgentInterface. +[StructLayout(LayoutKind.Explicit, Size = 0x08)] +public readonly unsafe struct AgentInterfacePtr(nint address) : IEquatable +{ + /// + /// The address to the AgentInterface. + /// + [FieldOffset(0x00)] + public readonly nint Address = address; + + /// + /// Gets a value indicating whether the underlying pointer is a nullptr. + /// + public readonly bool IsNull => this.Address == 0; + + /// + /// Gets a value indicating whether the agents addon is visible. + /// + public readonly AtkUnitBasePtr Addon + { + get + { + if (this.IsNull) + return 0; + + var raptureAtkUnitManager = RaptureAtkUnitManager.Instance(); + if (raptureAtkUnitManager == null) + return 0; + + return (nint)raptureAtkUnitManager->GetAddonById(this.AddonId); + } + } + + /// + /// Gets a value indicating whether the agent is active. + /// + public readonly ushort AddonId => (ushort)(this.IsNull ? 0 : this.Struct->GetAddonId()); + + /// + /// Gets a value indicating whether the agent is active. + /// + public readonly bool IsAgentActive => !this.IsNull && this.Struct->IsAgentActive(); + + /// + /// Gets a value indicating whether the agents addon is ready. + /// + public readonly bool IsAddonReady => !this.IsNull && this.Struct->IsAddonReady(); + + /// + /// Gets a value indicating whether the agents addon is visible. + /// + public readonly bool IsAddonShown => !this.IsNull && this.Struct->IsAddonShown(); + + /// + /// Gets the AgentInterface*. + /// + /// Internal use only. + internal readonly AgentInterface* Struct => (AgentInterface*)this.Address; + + public static implicit operator nint(AgentInterfacePtr wrapper) => wrapper.Address; + + public static implicit operator AgentInterfacePtr(nint address) => new(address); + + public static implicit operator AgentInterfacePtr(void* ptr) => new((nint)ptr); + + public static bool operator ==(AgentInterfacePtr left, AgentInterfacePtr right) => left.Address == right.Address; + + public static bool operator !=(AgentInterfacePtr left, AgentInterfacePtr right) => left.Address != right.Address; + + /// + /// Focuses the AtkUnitBase. + /// + /// true when the addon was focused, false otherwise. + public readonly bool FocusAddon() => this.IsNull && this.Struct->FocusAddon(); + + /// Determines whether the specified AgentInterfacePtr is equal to the current AgentInterfacePtr. + /// The AgentInterfacePtr to compare with the current AgentInterfacePtr. + /// true if the specified AgentInterfacePtr is equal to the current AgentInterfacePtr; otherwise, false. + public readonly bool Equals(AgentInterfacePtr other) => this.Address == other.Address; + + /// + public override readonly bool Equals(object obj) => obj is AgentInterfacePtr wrapper && this.Equals(wrapper); + + /// + public override readonly int GetHashCode() => ((nuint)this.Address).GetHashCode(); +} diff --git a/Dalamud/Game/NativeWrapper/AtkUnitBasePtr.cs b/Dalamud/Game/NativeWrapper/AtkUnitBasePtr.cs new file mode 100644 index 000000000..d4ebf4d3b --- /dev/null +++ b/Dalamud/Game/NativeWrapper/AtkUnitBasePtr.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Component.GUI; +using FFXIVClientStructs.Interop; + +namespace Dalamud.Game.NativeWrapper; + +/// +/// A readonly wrapper for AtkUnitBase. +/// +/// The address to the AtkUnitBase. +[StructLayout(LayoutKind.Explicit, Size = 0x08)] +public readonly unsafe struct AtkUnitBasePtr(nint address) : IEquatable +{ + /// + /// The address to the AtkUnitBase. + /// + [FieldOffset(0x00)] + public readonly nint Address = address; + + /// + /// Gets a value indicating whether the underlying pointer is a nullptr. + /// + public readonly bool IsNull => this.Address == 0; + + /// + /// Gets a value indicating whether the OnSetup function has been called. + /// + public readonly bool IsReady => !this.IsNull && this.Struct->IsReady; + + /// + /// Gets a value indicating whether the AtkUnitBase is visible. + /// + public readonly bool IsVisible => !this.IsNull && this.Struct->IsVisible; + + /// + /// Gets the name. + /// + public readonly string Name => this.IsNull ? string.Empty : this.Struct->NameString; + + /// + /// Gets the id. + /// + public readonly ushort Id => this.IsNull ? (ushort)0 : this.Struct->Id; + + /// + /// Gets the parent id. + /// + public readonly ushort ParentId => this.IsNull ? (ushort)0 : this.Struct->ParentId; + + /// + /// Gets the host id. + /// + public readonly ushort HostId => this.IsNull ? (ushort)0 : this.Struct->HostId; + + /// + /// Gets the scale. + /// + public readonly float Scale => this.IsNull ? 0f : this.Struct->Scale; + + /// + /// Gets the x-position. + /// + public readonly short X => this.IsNull ? (short)0 : this.Struct->X; + + /// + /// Gets the y-position. + /// + public readonly short Y => this.IsNull ? (short)0 : this.Struct->Y; + + /// + /// Gets the width. + /// + public readonly float Width => this.IsNull ? 0f : this.Struct->GetScaledWidth(false); + + /// + /// Gets the height. + /// + public readonly float Height => this.IsNull ? 0f : this.Struct->GetScaledHeight(false); + + /// + /// Gets the scaled width. + /// + public readonly float ScaledWidth => this.IsNull ? 0f : this.Struct->GetScaledWidth(true); + + /// + /// Gets the scaled height. + /// + public readonly float ScaledHeight => this.IsNull ? 0f : this.Struct->GetScaledHeight(true); + + /// + /// Gets the position. + /// + public readonly Vector2 Position => new(this.X, this.Y); + + /// + /// Gets the size. + /// + public readonly Vector2 Size => new(this.Width, this.Height); + + /// + /// Gets the scaled size. + /// + public readonly Vector2 ScaledSize => new(this.ScaledWidth, this.ScaledHeight); + + /// + /// Gets the number of entries. + /// + public readonly int AtkValuesCount => this.Struct->AtkValuesCount; + + /// + /// Gets an enumerable collection of of the addons current AtkValues. + /// + /// + /// An of corresponding to the addons AtkValues. + /// + public IEnumerable AtkValues + { + get + { + for (var i = 0; i < this.AtkValuesCount; i++) + { + AtkValuePtr ptr; + unsafe + { + ptr = new AtkValuePtr((nint)this.Struct->AtkValuesSpan.GetPointer(i)); + } + + yield return ptr; + } + } + } + + /// + /// Gets the AtkUnitBase*. + /// + /// Internal use only. + internal readonly AtkUnitBase* Struct => (AtkUnitBase*)this.Address; + + public static implicit operator nint(AtkUnitBasePtr wrapper) => wrapper.Address; + + public static implicit operator AtkUnitBasePtr(nint address) => new(address); + + public static implicit operator AtkUnitBasePtr(void* ptr) => new((nint)ptr); + + public static bool operator ==(AtkUnitBasePtr left, AtkUnitBasePtr right) => left.Address == right.Address; + + public static bool operator !=(AtkUnitBasePtr left, AtkUnitBasePtr right) => left.Address != right.Address; + + /// + /// Focuses the AtkUnitBase. + /// + public readonly void Focus() + { + if (!this.IsNull) + this.Struct->Focus(); + } + + /// Determines whether the specified AtkUnitBasePtr is equal to the current AtkUnitBasePtr. + /// The AtkUnitBasePtr to compare with the current AtkUnitBasePtr. + /// true if the specified AtkUnitBasePtr is equal to the current AtkUnitBasePtr; otherwise, false. + public readonly bool Equals(AtkUnitBasePtr other) => this.Address == other.Address; + + /// + public override readonly bool Equals(object obj) => obj is AtkUnitBasePtr wrapper && this.Equals(wrapper); + + /// + public override readonly int GetHashCode() => ((nuint)this.Address).GetHashCode(); +} diff --git a/Dalamud/Game/NativeWrapper/AtkValuePtr.cs b/Dalamud/Game/NativeWrapper/AtkValuePtr.cs new file mode 100644 index 000000000..005906e20 --- /dev/null +++ b/Dalamud/Game/NativeWrapper/AtkValuePtr.cs @@ -0,0 +1,113 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +using Dalamud.Utility; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.NativeWrapper; + +/// +/// A readonly wrapper for AtkValue. +/// +/// The address to the AtkValue. +[StructLayout(LayoutKind.Explicit, Size = 0x08)] +public readonly unsafe struct AtkValuePtr(nint address) : IEquatable +{ + /// + /// The address to the AtkValue. + /// + [FieldOffset(0x00)] + public readonly nint Address = address; + + /// + /// Gets a value indicating whether the underlying pointer is a nullptr. + /// + public readonly bool IsNull => this.Address == 0; + + /// + /// Gets the value type. + /// + public readonly AtkValueType ValueType => (AtkValueType)this.Struct->Type; + + /// + /// Gets the AtkValue*. + /// + /// Internal use only. + internal readonly AtkValue* Struct => (AtkValue*)this.Address; + + public static implicit operator nint(AtkValuePtr wrapper) => wrapper.Address; + + public static implicit operator AtkValuePtr(nint address) => new(address); + + public static implicit operator AtkValuePtr(void* ptr) => new((nint)ptr); + + public static bool operator ==(AtkValuePtr left, AtkValuePtr right) => left.Address == right.Address; + + public static bool operator !=(AtkValuePtr left, AtkValuePtr right) => left.Address != right.Address; + + /// + /// Gets the value of the underlying as a boxed object, based on its . + /// + /// + /// The boxed value represented by this , or null if the value is null or undefined. + /// + /// + /// Thrown if the value type is not currently handled by this implementation. + /// + public unsafe object? GetValue() + { + if (this.Struct == null) + return null; + + return this.ValueType switch + { + AtkValueType.Undefined or AtkValueType.Null => null, + AtkValueType.Bool => this.Struct->Bool, + AtkValueType.Int => this.Struct->Int, + AtkValueType.Int64 => this.Struct->Int64, + AtkValueType.UInt => this.Struct->UInt, + AtkValueType.UInt64 => this.Struct->UInt64, + AtkValueType.Float => this.Struct->Float, + AtkValueType.String or AtkValueType.String8 or AtkValueType.ManagedString => this.Struct->String == null ? default : this.Struct->String.AsReadOnlySeString(), + AtkValueType.WideString => this.Struct->WideString == null ? string.Empty : new string(this.Struct->WideString), + AtkValueType.Pointer => (nint)this.Struct->Pointer, + _ => throw new NotImplementedException($"AtkValueType {this.ValueType} is currently not supported"), + }; + } + + /// + /// Attempts to retrieve the value as a strongly-typed object if the underlying type matches. + /// + /// The expected value type to extract. + /// + /// When this method returns true, contains the extracted value of type . + /// Otherwise, contains the default value of type . + /// + /// + /// true if the value was successfully extracted and matched ; otherwise, false. + /// + public unsafe bool TryGet([NotNullWhen(true)] out T? result) where T : struct + { + object? value = this.GetValue(); + if (value is T typed) + { + result = typed; + return true; + } + + result = default; + return false; + } + + /// Determines whether the specified AtkValuePtr is equal to the current AtkValuePtr. + /// The AtkValuePtr to compare with the current AtkValuePtr. + /// true if the specified AtkValuePtr is equal to the current AtkValuePtr; otherwise, false. + public readonly bool Equals(AtkValuePtr other) => this.Address == other.Address || this.Struct->EqualTo(other.Struct); + + /// + public override readonly bool Equals(object obj) => obj is AtkValuePtr wrapper && this.Equals(wrapper); + + /// + public override readonly int GetHashCode() => ((nuint)this.Address).GetHashCode(); +} diff --git a/Dalamud/Game/NativeWrapper/AtkValueType.cs b/Dalamud/Game/NativeWrapper/AtkValueType.cs new file mode 100644 index 000000000..ef169e102 --- /dev/null +++ b/Dalamud/Game/NativeWrapper/AtkValueType.cs @@ -0,0 +1,87 @@ +namespace Dalamud.Game.NativeWrapper; + +/// +/// Represents the data type of the AtkValue. +/// +public enum AtkValueType +{ + /// + /// The value is undefined or invalid. + /// + Undefined = 0, + + /// + /// The value is null. + /// + Null = 0x1, + + /// + /// The value is a boolean. + /// + Bool = 0x2, + + /// + /// The value is a 32-bit signed integer. + /// + Int = 0x3, + + /// + /// The value is a 64-bit signed integer. + /// + Int64 = 0x4, + + /// + /// The value is a 32-bit unsigned integer. + /// + UInt = 0x5, + + /// + /// The value is a 64-bit unsigned integer. + /// + UInt64 = 0x6, + + /// + /// The value is a 32-bit floating-point number. + /// + Float = 0x7, + + /// + /// The value points to a null-terminated 8-bit character string (ASCII or UTF-8). + /// + String = 0x8, + + /// + /// The value points to a null-terminated 16-bit character string (UTF-16 / wide string). + /// + WideString = 0x9, + + /// + /// The value points to a constant null-terminated 8-bit character string (const char*). + /// + String8 = 0xA, + + /// + /// The value is a vector. + /// + Vector = 0xB, + + /// + /// The value is a pointer. + /// + Pointer = 0xC, + + /// + /// The value is pointing to an array of AtkValue entries. + /// + AtkValues = 0xD, + + /// + /// The value is a managed string. See . + /// + ManagedString = 0x28, + + /// + /// The value is a managed vector. See . + /// + ManagedVector = 0x2B, +} diff --git a/Dalamud/Game/NativeWrapper/UIModulePtr.cs b/Dalamud/Game/NativeWrapper/UIModulePtr.cs new file mode 100644 index 000000000..f6b841610 --- /dev/null +++ b/Dalamud/Game/NativeWrapper/UIModulePtr.cs @@ -0,0 +1,51 @@ +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Client.UI; + +namespace Dalamud.Game.NativeWrapper; + +/// +/// A readonly wrapper for UIModule. +/// +/// The address to the UIModule. +[StructLayout(LayoutKind.Explicit, Size = 0x08)] +public readonly unsafe struct UIModulePtr(nint address) : IEquatable +{ + /// + /// The address to the UIModule. + /// + [FieldOffset(0x00)] + public readonly nint Address = address; + + /// + /// Gets a value indicating whether the underlying pointer is a nullptr. + /// + public readonly bool IsNull => this.Address == 0; + + /// + /// Gets the UIModule*. + /// + /// Internal use only. + internal readonly UIModule* Struct => (UIModule*)this.Address; + + public static implicit operator nint(UIModulePtr wrapper) => wrapper.Address; + + public static implicit operator UIModulePtr(nint address) => new(address); + + public static implicit operator UIModulePtr(void* ptr) => new((nint)ptr); + + public static bool operator ==(UIModulePtr left, UIModulePtr right) => left.Address == right.Address; + + public static bool operator !=(UIModulePtr left, UIModulePtr right) => left.Address != right.Address; + + /// Determines whether the specified UIModulePtr is equal to the current UIModulePtr. + /// The UIModulePtr to compare with the current UIModulePtr. + /// true if the specified UIModulePtr is equal to the current UIModulePtr; otherwise, false. + public readonly bool Equals(UIModulePtr other) => this.Address == other.Address; + + /// + public override readonly bool Equals(object obj) => obj is UIModulePtr wrapper && this.Equals(wrapper); + + /// + public override readonly int GetHashCode() => ((nuint)this.Address).GetHashCode(); +} diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index d0ebc8fac..d88255de3 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -12,8 +12,6 @@ using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; -using Lumina.Text.ReadOnly; - // Customised version of https://github.com/aers/FFXIVUIDebug namespace Dalamud.Interface.Internal; @@ -102,8 +100,8 @@ internal unsafe class UiDebug } ImGui.Separator(); - ImGuiHelpers.ClickToCopyText($"Address: {(ulong)atkUnitBase:X}", $"{(ulong)atkUnitBase:X}"); - ImGuiHelpers.ClickToCopyText($"Agent: {(ulong)agent:X}", $"{(ulong)agent:X}"); + ImGuiHelpers.ClickToCopyText($"Address: {(nint)atkUnitBase:X}", $"{(nint)atkUnitBase:X}"); + ImGuiHelpers.ClickToCopyText($"Agent: {(nint)agent:X}", $"{(nint)agent:X}"); ImGui.Separator(); ImGui.Text($"Position: [ {atkUnitBase->X} , {atkUnitBase->Y} ]"); diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs index 9d6575a55..00782044f 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs @@ -81,7 +81,7 @@ public unsafe partial class AddonTree : IDisposable { var ptr = GameGui.GetAddonByName(name); - if ((AtkUnitBase*)ptr != null) + if (!ptr.IsNull) { if (AddonTrees.TryGetValue(name, out var tree)) { @@ -152,7 +152,7 @@ public unsafe partial class AddonTree : IDisposable var uldManager = addon->UldManager; PrintFieldValuePair("Address", $"{(nint)addon:X}"); - PrintFieldValuePair("Agent", $"{GameGui.FindAgentInterface(addon):X}"); + PrintFieldValuePair("Agent", $"{(nint)GameGui.FindAgentInterface(addon):X}"); PrintFieldValuePairs( ("X", $"{addon->X}"), @@ -234,7 +234,7 @@ public unsafe partial class AddonTree : IDisposable /// true if the addon is found. private bool ValidateAddon(out AtkUnitBase* addon) { - addon = (AtkUnitBase*)GameGui.GetAddonByName(this.AddonName); + addon = GameGui.GetAddonByName(this.AddonName).Struct; if (addon == null || (nint)addon != this.InitialPtr) { this.Dispose(); diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs index 50967453d..4820b2de4 100644 --- a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs +++ b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs @@ -49,7 +49,7 @@ internal unsafe partial class UiDebug2 /// Gets the base address for all unit lists. /// /// The address, if found. - internal static AtkUnitList* GetUnitListBaseAddr() => &((UIModule*)GameGui.GetUIModule())->GetRaptureAtkModule()->RaptureAtkUnitManager.AtkUnitManager.DepthLayerOneList; + internal static AtkUnitList* GetUnitListBaseAddr() => &RaptureAtkUnitManager.Instance()->DepthLayerOneList; private void DrawSidebar() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs index 18bcd9334..bd699e111 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs @@ -1,6 +1,7 @@ -using Dalamud.Game.Gui; -using Dalamud.Memory; +using Dalamud.Game.Gui; +using Dalamud.Game.NativeWrapper; using Dalamud.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -12,7 +13,7 @@ internal unsafe class AddonWidget : IDataWindowWidget { private string inputAddonName = string.Empty; private int inputAddonIndex; - private nint findAgentInterfacePtr; + private AgentInterfacePtr agentInterfacePtr; /// public string DisplayName { get; init; } = "Addon"; @@ -40,30 +41,27 @@ internal unsafe class AddonWidget : IDataWindowWidget if (this.inputAddonName.IsNullOrEmpty()) return; - var address = gameGui.GetAddonByName(this.inputAddonName, this.inputAddonIndex); - - if (address == nint.Zero) + var addon = gameGui.GetAddonByName(this.inputAddonName, this.inputAddonIndex); + if (addon.IsNull) { ImGui.Text("Null"); return; } - var addon = (FFXIVClientStructs.FFXIV.Component.GUI.AtkUnitBase*)address; - var name = addon->NameString; - ImGui.TextUnformatted($"{name} - {Util.DescribeAddress(address)}\n v:{addon->IsVisible} x:{addon->X} y:{addon->Y} s:{addon->Scale}, w:{addon->RootNode->Width}, h:{addon->RootNode->Height}"); + ImGui.TextUnformatted($"{addon.Name} - {Util.DescribeAddress(addon)}\n v:{addon.IsVisible} x:{addon.X} y:{addon.Y} s:{addon.Scale}, w:{addon.Width}, h:{addon.Height}"); if (ImGui.Button("Find Agent")) { - this.findAgentInterfacePtr = gameGui.FindAgentInterface(address); + this.agentInterfacePtr = gameGui.FindAgentInterface(addon); } - if (this.findAgentInterfacePtr != nint.Zero) + if (!this.agentInterfacePtr.IsNull) { - ImGui.TextUnformatted($"Agent: {Util.DescribeAddress(this.findAgentInterfacePtr)}"); + ImGui.TextUnformatted($"Agent: {Util.DescribeAddress(this.agentInterfacePtr)}"); ImGui.SameLine(); if (ImGui.Button("C")) - ImGui.SetClipboardText(this.findAgentInterfacePtr.ToInt64().ToString("X")); + ImGui.SetClipboardText(this.agentInterfacePtr.Address.ToString("X")); } } } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 2b8973dc5..9d476e10b 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -477,7 +477,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable { if (args is not AddonDrawArgs drawArgs) return; - var addon = (AtkUnitBase*)drawArgs.Addon; + var addon = drawArgs.Addon.Struct; var textNode = addon->GetTextNodeById(3); // look and feel init. should be harmless to set. diff --git a/Dalamud/Plugin/Services/IGameGui.cs b/Dalamud/Plugin/Services/IGameGui.cs index 0e2da7874..773ba61b4 100644 --- a/Dalamud/Plugin/Services/IGameGui.cs +++ b/Dalamud/Plugin/Services/IGameGui.cs @@ -1,6 +1,7 @@ -using System.Numerics; +using System.Numerics; using Dalamud.Game.Gui; +using Dalamud.Game.NativeWrapper; using Dalamud.Game.Text.SeStringHandling.Payloads; namespace Dalamud.Plugin.Services; @@ -75,37 +76,37 @@ public unsafe interface IGameGui public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f); /// - /// Gets a pointer to the game's UI module. + /// Gets a pointer to the game's UIModule instance. /// - /// IntPtr pointing to UI module. - public nint GetUIModule(); + /// A pointer wrapper to UIModule. + public UIModulePtr GetUIModule(); /// /// Gets the pointer to the Addon with the given name and index. /// /// Name of addon to find. /// Index of addon to find (1-indexed). - /// nint.Zero if unable to find UI, otherwise nint pointing to the start of the addon. - public nint GetAddonByName(string name, int index = 1); + /// A pointer wrapper to the addon. + public AtkUnitBasePtr GetAddonByName(string name, int index = 1); + + /// + /// Find the agent associated with an addon, if possible. + /// + /// The agent id. + /// A pointer wrapper to the agent interface. + public AgentInterfacePtr GetAgentById(int id); /// /// Find the agent associated with an addon, if possible. /// /// The addon name. - /// A pointer to the agent interface. - public nint FindAgentInterface(string addonName); + /// A pointer wrapper to the agent interface. + public AgentInterfacePtr FindAgentInterface(string addonName); /// /// Find the agent associated with an addon, if possible. /// /// The addon address. - /// A pointer to the agent interface. - public nint FindAgentInterface(void* addon); - - /// - /// Find the agent associated with an addon, if possible. - /// - /// The addon address. - /// A pointer to the agent interface. - public IntPtr FindAgentInterface(IntPtr addonPtr); + /// A pointer wrapper to the agent interface. + public AgentInterfacePtr FindAgentInterface(AtkUnitBasePtr addon); }