using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Enums; namespace KamiToolKit; public abstract unsafe partial class NodeBase : IDisposable { internal const uint NodeIdBase = 100_000_000; protected static readonly List CreatedNodes = []; private static int logIndent = -1; internal static uint CurrentOffset; private bool isDisposed; internal abstract AtkResNode* ResNode { get; } internal bool IsAddonRootNode; private delegate* unmanaged originalDestructorFunction; private AtkResNode.Delegates.Destroy destructorFunction = null!; private AtkResNode.AtkResNodeVirtualTable* virtualTable; public void Dispose() { try { logIndent++; LogIndented($"Beginning Dispose for {GetType()}"); logIndent++; if (MainThreadSafety.TryAssertMainThread()) { if (Framework.Instance()->IsDestroying) { LogIndented("Game is shutting down, aborting manual dispose."); } return; } if (isDisposed) { LogIndented("Node was already disposed, skipping."); return; } isDisposed = true; if (!IsNodeValid()) { Log.Warning("Invalid node, dispose aborted."); return; } LogIndented("Disposing Children"); foreach (var child in ChildNodes.ToList()) { child.Dispose(); } LogIndented("Children Disposed"); ChildNodes.Clear(); LogIndented("Disposing Tooltip Events"); UnregisterTooltipEvents(); LogIndented("Clearing Native Focus"); AtkStage.Instance()->ClearNodeFocus(ResNode); LogIndented("Detaching From UI"); DetachNode(); LogIndented("Disposing Timeline"); Timeline?.Dispose(); ResNode->Timeline = null; LogIndented("Invoking Native Dispose"); Dispose(true, false); GC.SuppressFinalize(this); CreatedNodes.Remove(this); logIndent--; LogIndented("Dispose Complete"); logIndent--; } catch (Exception e) { Log.Exception(e); logIndent = 0; } } private static void LogIndented(string message) => Log.Verbose(new string(' ', logIndent * 2) + message); /// /// Warning, this is only to ensure there are no memory leaks. /// Ensure you have detached nodes safely from native ui before disposing. /// internal static void DisposeNodes() { var leakedNodeCount = CreatedNodes.Count(node => !node.IsAddonRootNode && node.ResNode is not null && node.ResNode->ParentNode is null); if (leakedNodeCount is not 0) { Log.Warning($"There were {leakedNodeCount} node(s) that were not disposed safely."); } foreach (var node in CreatedNodes.ToArray()) { if (node.ResNode is null) continue; if (node.ResNode->ParentNode is not null) continue; if (node.IsAddonRootNode) continue; Log.Warning($"Forcing disposal of: {node.GetType()}"); node.Dispose(); } } ~NodeBase() => Dispose(false, false); /// /// Dispose associated resources. If a resource modifies native state directly guard it with isNativeDestructor /// /// /// Indicates if this specific call should dispose resources or not. This protects against double dispose, /// or incorrectly manipulating native state too many times. /// /// /// Indicates if the dispose call should try to completely clean up all resources, /// or if it should only clean up managed resources. When false, be sure to only dispose /// resources that exist in managed spaces, as the game has already cleaned up everything else. /// protected virtual void Dispose(bool disposing, bool isNativeDestructor) { // Dispose of managed resources that must be disposed regardless of how dispose is invoked DisposeEvents(); DisableEditMode(NodeEditMode.Move | NodeEditMode.Resize); } private bool IsNodeValid() { if (ResNode is null) return false; if (ResNode->VirtualTable is null) return false; if (ResNode->VirtualTable == AtkEventTarget.StaticVirtualTablePointer) return false; return true; } public static implicit operator AtkResNode*(NodeBase node) => node.ResNode; public static implicit operator AtkEventTarget*(NodeBase node) => &node.ResNode->AtkEventTarget; protected void BuildVirtualTable() { // Back up original destructor pointer originalDestructorFunction = ResNode->VirtualTable->Destroy; // Overwrite virtual table with a custom copy, // Note: Currently there are only 2 vfuncs, but there's no harm in copying more for if they ever add more vfuncs to the game. virtualTable = (AtkResNode.AtkResNodeVirtualTable*)NativeMemoryHelper.Malloc(0x8 * 4); NativeMemory.Copy(ResNode->VirtualTable, virtualTable, 0x8 * 4); ResNode->VirtualTable = virtualTable; // Pin managed function to virtual table entry destructorFunction = DestructorDetour; // Replace native destructor with virtualTable->Destroy = (delegate* unmanaged) Marshal.GetFunctionPointerForDelegate(destructorFunction); } private void DestructorDetour(AtkResNode* thisPtr, bool free) { Dispose(true, true); InvokeOriginalDestructor(thisPtr, free); Log.Verbose($"Native has disposed node {GetType()}"); GC.SuppressFinalize(this); CreatedNodes.Remove(this); isDisposed = true; } protected void InvokeOriginalDestructor(AtkResNode* thisPtr, bool free) { if (virtualTable is null) return; // Shouldn't be possible, but just in case. originalDestructorFunction(thisPtr, free); NativeMemoryHelper.Free(virtualTable, 0x8 * 4); virtualTable = null; } }