Initial commit: AetherBags + KamiToolKit for FC Gitea
Debug Build and Test / Build against Latest Dalamud (push) Has been cancelled
Debug Build and Test / Build against Staging Dalamud (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 14:46:31 -05:00
commit 8db4ce6094
375 changed files with 34124 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
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<NodeBase> CreatedNodes = [];
private static int logIndent = -1;
internal static uint CurrentOffset;
private bool isDisposed;
internal abstract AtkResNode* ResNode { get; }
internal bool IsAddonRootNode;
private delegate* unmanaged<AtkResNode*, bool, void> 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);
/// <summary>
/// Warning, this is only to ensure there are no memory leaks.
/// Ensure you have detached nodes safely from native ui before disposing.
/// </summary>
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);
/// <summary>
/// Dispose associated resources. If a resource modifies native state directly guard it with isNativeDestructor
/// </summary>
/// <param name="disposing">
/// Indicates if this specific call should dispose resources or not. This protects against double dispose,
/// or incorrectly manipulating native state too many times.
/// </param>
/// <param name="isNativeDestructor">
/// 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.
/// </param>
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<AtkResNode*, bool, void>) 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;
}
}
+205
View File
@@ -0,0 +1,205 @@
using System;
using System.Numerics;
using Dalamud.Game.Addon.Events;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
using KamiToolKit.Nodes;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase {
private Vector2 clickStartPosition = Vector2.Zero;
private NodeEditMode currentEditMode = 0;
private ViewportEventListener? editEventListener;
private bool isCursorSet;
private bool isMoving;
private bool isResizing;
private NodeEditOverlayNode? overlayNode;
public Action<NodeBase>? OnResizeComplete { get; set; }
public Action<NodeBase>? OnMoveComplete { get; set; }
public Action<NodeBase>? OnEditComplete { get; set; }
public bool EnableMoving {
get;
set {
field = value;
if (value) {
EnableEditMode(NodeEditMode.Move);
}
else {
DisableEditMode(NodeEditMode.Move);
}
}
}
public bool EnableResizing {
get;
set {
field = value;
if (value) {
EnableEditMode(NodeEditMode.Resize);
}
else {
DisableEditMode(NodeEditMode.Resize);
}
}
}
public void EnableEditMode(NodeEditMode mode) {
currentEditMode |= mode;
if (overlayNode is null) {
overlayNode = new NodeEditOverlayNode {
Position = new Vector2(-16.0f, -16.0f),
Size = Size + new Vector2(32.0f, 32.0f),
};
overlayNode.AttachNode(this);
ChildNodes.Add(overlayNode);
}
overlayNode.ShowParts = currentEditMode.HasFlag(NodeEditMode.Resize);
if (editEventListener is null) {
editEventListener = new ViewportEventListener(OnEditEvent);
editEventListener.AddEvent(AtkEventType.MouseMove, overlayNode);
editEventListener.AddEvent(AtkEventType.MouseDown, overlayNode);
}
}
public void DisableEditMode(NodeEditMode mode) {
currentEditMode &= ~mode;
if (currentEditMode.HasFlag(NodeEditMode.Resize) || currentEditMode.HasFlag(NodeEditMode.Move)) return;
if (editEventListener is not null) {
editEventListener.RemoveEvent(AtkEventType.MouseMove);
editEventListener.RemoveEvent(AtkEventType.MouseDown);
editEventListener.Dispose();
editEventListener = null;
}
if (overlayNode is not null) {
ChildNodes.Remove(overlayNode);
overlayNode.DetachNode();
overlayNode.Dispose();
overlayNode = null;
}
}
private void OnEditEvent(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
if (overlayNode is null) return;
if (editEventListener is null) return;
ref var mouseData = ref atkEventData->MouseData;
var mousePosition = new Vector2(mouseData.PosX, mouseData.PosY);
var mouseDelta = mousePosition - clickStartPosition;
switch (eventType) {
// Move Logic
case AtkEventType.MouseMove when isMoving: {
Position += mouseDelta;
clickStartPosition = mousePosition;
atkEvent->SetEventIsHandled(true);
}
break;
// Update hover state when not resizing, as we latch that for the behavior
case AtkEventType.MouseMove when !isResizing: {
overlayNode.UpdateHover(atkEventData);
}
break;
// Resize Logic
case AtkEventType.MouseMove when isResizing: {
Position += overlayNode.GetPositionDelta(mouseDelta);
Size += overlayNode.GetSizeDelta(mouseDelta);
overlayNode.Size = Size + new Vector2(32.0f, 32.0f);
clickStartPosition = mousePosition;
atkEvent->SetEventIsHandled(true);
}
break;
// Begin Resize Event
case AtkEventType.MouseDown when !isResizing && overlayNode.AnyHovered() && currentEditMode.HasFlag(NodeEditMode.Resize): {
editEventListener.AddEvent(AtkEventType.MouseUp, overlayNode);
isResizing = true;
clickStartPosition = mousePosition;
atkEvent->SetEventIsHandled(true);
}
break;
// End Resize Event
case AtkEventType.MouseUp when isResizing: {
OnResizeComplete?.Invoke(this);
OnEditComplete?.Invoke(this);
isResizing = false;
editEventListener.RemoveEvent(AtkEventType.MouseUp);
}
break;
// Begin Move Event
case AtkEventType.MouseDown when !overlayNode.AnyHovered() && overlayNode.CheckCollision(atkEventData) && !isMoving && currentEditMode.HasFlag(NodeEditMode.Move): {
editEventListener.AddEvent(AtkEventType.MouseUp, overlayNode);
isMoving = true;
clickStartPosition = mousePosition;
atkEvent->SetEventIsHandled(true);
}
break;
// End Move Event
case AtkEventType.MouseUp when isMoving: {
OnMoveComplete?.Invoke(this);
OnEditComplete?.Invoke(this);
isMoving = false;
editEventListener.RemoveEvent(AtkEventType.MouseUp);
}
break;
}
if (isCursorSet) {
ResetCursor();
isCursorSet = false;
}
if (currentEditMode.HasFlag(NodeEditMode.Move)) {
if (isMoving) {
SetCursor(AddonCursorType.Grab);
isCursorSet = true;
}
else if (CheckCollision(atkEventData)) {
SetCursor(AddonCursorType.Hand);
isCursorSet = true;
}
}
if (overlayNode.AnyHovered() && currentEditMode.HasFlag(NodeEditMode.Resize)) {
overlayNode.SetCursor();
isCursorSet = true;
}
}
private static void SetCursor(AddonCursorType cursor)
=> DalamudInterface.Instance.AddonEventManager.SetCursor(cursor);
private static void ResetCursor()
=> DalamudInterface.Instance.AddonEventManager.ResetCursor();
}
+181
View File
@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
namespace KamiToolKit;
internal class EventHandlerInfo {
public AtkEventListener.Delegates.ReceiveEvent? OnReceiveEventDelegate;
public Action? OnActionDelegate;
}
public abstract unsafe partial class NodeBase {
private CustomEventListener? nodeEventListener;
private readonly Dictionary<AtkEventType, EventHandlerInfo> eventHandlers = [];
/// <summary>
/// When true, mousing over this node will show the finger cursor icon.
/// </summary>
public bool ShowClickableCursor {
get => DrawFlags.HasFlag(DrawFlags.ClickableCursor);
set {
if (value) {
DrawFlags |= DrawFlags.ClickableCursor;
}
else {
DrawFlags &= ~DrawFlags.ClickableCursor;
}
}
}
/// <summary>
/// When true, mousing over this node will show the text input cursor icon.
/// </summary>
public bool ShowTextInputCursor {
get => DrawFlags.HasFlag(DrawFlags.TextInputCursor);
set {
if (value) {
DrawFlags |= DrawFlags.TextInputCursor;
}
else {
DrawFlags &= ~DrawFlags.TextInputCursor;
}
}
}
public void AddEvent(AtkEventType eventType, Action callback) {
nodeEventListener ??= new CustomEventListener(HandleEvents);
SetNodeEventFlags(eventType);
if (eventHandlers.TryAdd(eventType, new EventHandlerInfo { OnActionDelegate = callback })) {
Log.Verbose($"[{eventType}] Registered for {GetType()} [{(nint)ResNode:X}]");
ResNode->AtkEventManager.RegisterEvent(eventType, 0, this, this, nodeEventListener, false);
}
else {
eventHandlers[eventType].OnActionDelegate += callback;
}
}
public void AddEvent(AtkEventType eventType, AtkEventListener.Delegates.ReceiveEvent callback) {
nodeEventListener ??= new CustomEventListener(HandleEvents);
SetNodeEventFlags(eventType);
if (eventHandlers.TryAdd(eventType, new EventHandlerInfo { OnReceiveEventDelegate = callback })) {
Log.Verbose($"[{eventType}] Registered for {GetType()} [{(nint)ResNode:X}]");
ResNode->AtkEventManager.RegisterEvent(eventType, 0, this, this, nodeEventListener, false);
}
else {
eventHandlers[eventType].OnReceiveEventDelegate += callback;
}
}
public void RemoveEvent(AtkEventType eventType) {
if (nodeEventListener is null) return;
if (eventHandlers.Remove(eventType)) {
Log.Verbose($"[{eventType}] Unregistered from {GetType()} [{(nint)ResNode:X}]");
ResNode->AtkEventManager.UnregisterEvent(eventType, 0, nodeEventListener, false);
}
// If we have removed the last event, free the event listener
if (eventHandlers.Keys.Count is 0) {
nodeEventListener.Dispose();
nodeEventListener = null;
}
}
public void RemoveEvent(AtkEventType eventType, Action callback) {
if (nodeEventListener is null) return;
if (eventHandlers.TryGetValue(eventType, out var handler)) {
handler.OnActionDelegate -= callback;
if (handler.OnReceiveEventDelegate is null && handler.OnActionDelegate is null) {
RemoveEvent(eventType);
}
}
}
public void RemoveEvent(AtkEventType eventType, AtkEventListener.Delegates.ReceiveEvent callback) {
if (nodeEventListener is null) return;
if (eventHandlers.TryGetValue(eventType, out var handler)) {
handler.OnReceiveEventDelegate -= callback;
if (handler.OnReceiveEventDelegate is null && handler.OnActionDelegate is null) {
RemoveEvent(eventType);
}
}
}
private void DisposeEvents() {
if (nodeEventListener is not null) {
ResNode->AtkEventManager.UnregisterEvent(AtkEventType.UnregisterAll, 0, nodeEventListener, false);
}
eventHandlers.Clear();
nodeEventListener?.Dispose();
nodeEventListener = null;
}
private void HandleEvents(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
try {
if (!IsVisible) return;
if (eventHandlers.TryGetValue(eventType, out var handler)) {
foreach (var noArgHandler in Delegate.EnumerateInvocationList(handler.OnActionDelegate)) {
try {
noArgHandler();
}
catch (Exception e) {
Log.Exception(e);
}
}
foreach (var argHandler in Delegate.EnumerateInvocationList(handler.OnReceiveEventDelegate)) {
try {
argHandler(thisPtr, eventType, eventParam, atkEvent, atkEventData);
}
catch (Exception e) {
Log.Exception(e);
}
}
}
}
catch (Exception e) {
Log.Exception(e);
}
}
private void SetNodeEventFlags(AtkEventType eventType) {
switch (eventType) {
// Hover events need to propagate down to trigger various timelines
case AtkEventType.MouseOver:
case AtkEventType.MouseOut:
case AtkEventType.MouseWheel:
AddNodeFlags(NodeFlags.EmitsEvents, NodeFlags.RespondToMouse);
break;
// Any kind of direct interaction should be a blocking event
// set HasCollision to prevent events from propagating
case AtkEventType.MouseDown:
case AtkEventType.MouseUp:
case AtkEventType.MouseMove:
case AtkEventType.MouseClick:
AddNodeFlags(NodeFlags.EmitsEvents, NodeFlags.RespondToMouse, NodeFlags.HasCollision);
break;
// ButtonClick is mostly used as an event that native calls back to, when interacting with buttons
// We do not want to re-emit, or block events in this case
case AtkEventType.ButtonClick:
break;
}
}
}
+260
View File
@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase {
internal readonly List<NodeBase> ChildNodes = [];
private NodeBase? parentNode;
internal AtkUldManager* ParentUldManager { get; set; }
internal AtkUnitBase* ParentAddon { get; private set; }
[OverloadResolutionPriority(1)]
public void AttachNode(NativeAddon? targetAddon, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformManagedAttach(targetAddon, targetPosition);
public void AttachNode(AtkUnitBase* targetAddon, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach(targetAddon is not null ? targetAddon->RootNode : null, targetPosition);
[OverloadResolutionPriority(1)]
public void AttachNode(NodeBase? targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformManagedAttach(targetNode, targetPosition);
public void AttachNode(AtkResNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach(targetNode, targetPosition);
public void AttachNode(AtkImageNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkTextNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkNineGridNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkCounterNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkCollisionNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkClippingMaskNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkComponentNode* targetNode, NodePosition targetPosition = NodePosition.AfterAllSiblings)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
private void PerformManagedAttach(NativeAddon? targetAddon, NodePosition targetPosition = NodePosition.AsLastChild) {
if (MainThreadSafety.TryAssertMainThread()) return;
if (targetAddon is null) return;
// Check the Addon's node list to find out what NodeId we should be, and set that before attaching
if (NodeId > NodeIdBase) {
NodeId = targetAddon.InternalAddon->UldManager.GetMaxNodeId() + 1;
}
PerformNativeAttach(targetAddon.RootNode, targetPosition);
parentNode = targetAddon.RootNode;
parentNode.ChildNodes.Add(this);
}
private void PerformManagedAttach(NodeBase? targetNode, NodePosition targetPosition) {
if (MainThreadSafety.TryAssertMainThread()) return;
if (targetNode is null) return;
PerformNativeAttach(targetNode, targetPosition);
parentNode = targetNode;
parentNode.ChildNodes.Add(this);
}
private void PerformNativeAttach(AtkResNode* targetNode, NodePosition targetPosition) {
if (MainThreadSafety.TryAssertMainThread()) return;
if (targetNode is null) return;
if (targetNode->GetNodeType() is NodeType.Component) {
// If target is a ComponentNode,
// then we don't ever wanna be a child of the ComponentNode itself,
// we will want to be a sibling of the root node.
// Therefore, redirect the target position to be siblings.
targetPosition = targetPosition switch {
NodePosition.AsLastChild => NodePosition.AfterAllSiblings,
NodePosition.AsFirstChild => NodePosition.BeforeAllSiblings,
_ => targetPosition,
};
// If however, we are using BeforeTarget or AfterTarget,
// then we do want to attach to the ComponentNode
// else, attach to its root node.
var componentNode = targetNode->GetAsAtkComponentNode();
if (componentNode is not null) {
targetNode = targetPosition switch {
NodePosition.AfterTarget => targetNode,
NodePosition.BeforeTarget => targetNode,
NodePosition.AfterAllSiblings => componentNode->Component->UldManager.RootNode,
NodePosition.BeforeAllSiblings => componentNode->Component->UldManager.RootNode,
_ => throw new ArgumentOutOfRangeException(nameof(targetPosition), targetPosition, null),
};
// We also need to check the components node list, to get a safely assigned nodeId
if (NodeId > NodeIdBase) {
NodeId = componentNode->Component->UldManager.GetMaxNodeId() + 1;
}
}
}
NodeLinker.AttachNode(this, targetNode, targetPosition);
UpdateParentAddon(targetNode);
UpdateNative();
}
internal void ReattachNode(AtkResNode* newTarget) {
if (newTarget is null) return;
DetachNode();
AttachNode(newTarget);
}
public void DetachNode() {
if (MainThreadSafety.TryAssertMainThread()) return;
if (ResNode is null) return;
UnlinkFromNative();
RemoveUldManagerObjectReferences();
RemoveParentAddonReferences();
RemoveParentNodeReferences();
}
private void UnlinkFromNative() {
NodeLinker.DetachNode(ResNode);
ResNode->ParentNode = null;
ResNode->NextSiblingNode = null;
ResNode->PrevSiblingNode = null;
}
private void RemoveUldManagerObjectReferences() {
if (ParentUldManager is null) return;
ParentUldManager->RemoveNodeFromObjectList(this);
ParentUldManager = null;
}
private void RemoveParentAddonReferences() {
if (ParentAddon is null) return;
ParentAddon->UldManager.UpdateDrawNodeList();
ParentAddon->UpdateCollisionNodeList(false);
ParentAddon = null;
foreach (var child in GetAllChildren(this)) {
child.ParentAddon = null;
}
}
private void RemoveParentNodeReferences() {
if (parentNode is null) return;
parentNode.ChildNodes.Remove(this);
parentNode = null;
}
private void UpdateNative() {
if (ResNode is null) return;
MarkDirty();
if (ParentUldManager is null) {
ParentUldManager = GetUldManagerForNode(ResNode);
}
if (ParentUldManager is not null) {
ParentUldManager->AddNodeToObjectList(this);
}
if (ParentAddon is not null) {
if (ParentAddon->NameString is "NamePlate") {
Log.Warning("Warning, attaching to AddonNamePlate is not supported. Use OverlayController instead.");
}
ParentAddon->UldManager.UpdateDrawNodeList();
ParentAddon->UpdateCollisionNodeList(false);
}
}
private void UpdateParentAddon(AtkResNode* node) {
if (parentNode is not null && parentNode.ParentAddon is not null) {
ParentAddon = parentNode.ParentAddon;
}
else if (ParentAddon is null) {
var targetParentAddon = RaptureAtkUnitManager.Instance()->GetAddonByNode(node);
if (targetParentAddon is not null) {
ParentAddon = targetParentAddon;
}
}
if (ParentAddon is not null) {
foreach (var child in GetAllChildren(this)) {
child.ParentAddon = ParentAddon;
}
}
}
private AtkUldManager* GetUldManagerForNode(AtkResNode* node) {
if (node is null) return null;
var targetNode = node;
if (targetNode->GetNodeType() is NodeType.Component) {
targetNode = targetNode->ParentNode;
}
// Try to get UldManager via the first parent that is a component
while (targetNode is not null) {
if (targetNode->GetNodeType() is NodeType.Component) {
var componentNode = (AtkComponentNode*)targetNode;
return &componentNode->Component->UldManager;
}
targetNode = targetNode->ParentNode;
}
// We failed to find a parent component, try to get a parent addon instead
if (ParentAddon is not null) {
return &ParentAddon->UldManager;
}
return null;
}
private static IEnumerable<NodeBase> GetAllChildren(NodeBase parent) {
foreach (var child in parent.ChildNodes) {
yield return child;
foreach (var childNode in GetAllChildren(child)) {
yield return childNode;
}
}
}
internal static IEnumerable<NodeBase> GetLocalChildren(NodeBase parent) {
if (parent is ComponentNode) yield break;
foreach (var child in parent.ChildNodes) {
yield return child;
if (child is ComponentNode) continue;
foreach (var childNode in GetLocalChildren(child)) {
yield return childNode;
}
}
}
}
@@ -0,0 +1,244 @@
using System;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Common.Math;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using Bounds = KamiToolKit.Classes.Bounds;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
using Vector4 = System.Numerics.Vector4;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase {
public virtual float X {
get => ResNode->GetXFloat();
set => ResNode->SetXFloat(value);
}
public virtual float Y {
get => ResNode->GetYFloat();
set => ResNode->SetYFloat(value);
}
public virtual Vector2 Position {
get => ResNode->Position;
set => ResNode->Position = value;
}
public virtual float ScreenX {
get => ResNode->ScreenX;
set => ResNode->ScreenX = value;
}
public virtual float ScreenY {
get => ResNode->ScreenY;
set => ResNode->ScreenY = value;
}
public virtual Vector2 ScreenPosition
=> ResNode->ScreenPosition;
public virtual float Width {
get => ResNode->GetWidth();
set {
ResNode->SetWidth((ushort)value);
OnSizeChanged();
}
}
public virtual float Height {
get => ResNode->GetHeight();
set {
ResNode->SetHeight((ushort)value);
OnSizeChanged();
}
}
public virtual Vector2 Size {
get => ResNode->Size;
set {
ResNode->SetWidth((ushort)value.X);
ResNode->SetHeight((ushort)value.Y);
OnSizeChanged();
}
}
public Bounds Bounds
=> ResNode->Bounds;
public Vector2 Center
=> ResNode->Center;
public virtual float ScaleX {
get => ResNode->GetScaleX();
set => ResNode->SetScaleX(value);
}
public virtual float ScaleY {
get => ResNode->GetScaleY();
set => ResNode->SetScaleY(value);
}
public virtual Vector2 Scale {
get => ResNode->Scale;
set => ResNode->Scale = value;
}
public virtual float Rotation {
get => ResNode->GetRotation();
set => ResNode->SetRotation(value);
}
public virtual float RotationDegrees {
get => ResNode->RotationDegrees;
set => ResNode->RotationDegrees = value;
}
public virtual float OriginX {
get => ResNode->OriginX;
set => ResNode->OriginX = value;
}
public virtual float OriginY {
get => ResNode->OriginY;
set => ResNode->OriginY = value;
}
public virtual Vector2 Origin {
get => ResNode->Origin;
set => ResNode->Origin = value;
}
private bool? lastIsVisible;
public virtual bool IsVisible {
get => ResNode->Visible;
set {
ResNode->Visible = value;
if (lastIsVisible is null || lastIsVisible != value) {
OnVisibilityToggled?.Invoke(value);
lastIsVisible = value;
}
}
}
private Action<bool>? OnVisibilityToggled { get; set; }
public NodeFlags NodeFlags {
get => ResNode->NodeFlags;
set => ResNode->NodeFlags = value;
}
public virtual Vector4 Color {
get => ResNode->ColorVector;
set => ResNode->ColorVector = value;
}
public virtual ColorHelpers.HsvaColor ColorHsva {
get => ResNode->ColorHsva;
set => ResNode->ColorHsva = value;
}
public virtual float Alpha {
get => ResNode->Color.A;
set => ResNode->SetAlpha((byte)(value * 255.0f));
}
public virtual Vector3 AddColor {
get => ResNode->AddColor;
set => ResNode->AddColor = value;
}
public virtual ColorHelpers.HsvaColor AddColorHsva {
get => ResNode->AddColorHsva;
set => ResNode->AddColorHsva = value;
}
public virtual Vector3 MultiplyColor {
get => ResNode->MultiplyColor;
set => ResNode->MultiplyColor = value;
}
public virtual ColorHelpers.HsvaColor MultiplyColorHsva {
get => ResNode->MultiplyColorHsva;
set => ResNode->MultiplyColorHsva = value;
}
public uint NodeId {
get => ResNode->NodeId;
set => ResNode->NodeId = value;
}
public virtual DrawFlags DrawFlags {
get => (DrawFlags) ResNode->DrawFlags;
set => ResNode->DrawFlags = (uint) value & 0b1111_1111_1111_1100_0000_0011_1111_1111 |
ResNode->DrawFlags & 0b0000_0000_0000_0011_1111_1100_0000_0000;
}
public virtual int ClipCount {
get => (int)((ResNode->DrawFlags & 0b0000_0000_0000_0011_1111_1100_0000_0000) >> 10);
set => ResNode->DrawFlags = (uint)(value << 10 & 0b0000_0000_0000_0011_1111_1100_0000_0000)
| ResNode->DrawFlags & 0b1111_1111_1111_1100_0000_0011_1111_1111;
}
public void AddDrawFlags(params DrawFlags[] flags) {
foreach (var flag in flags) {
DrawFlags |= flag;
}
}
public void RemoveDrawFlags(params DrawFlags[] flags) {
foreach (var flag in flags) {
DrawFlags &= ~flag;
}
}
public int Priority {
get => ResNode->GetPriority();
set => ResNode->SetPriority((ushort)value);
}
protected virtual NodeType NodeType {
get => ResNode->GetNodeType();
set => ResNode->Type = value;
}
public virtual int ChildCount
=> ResNode->ChildCount;
protected virtual void OnSizeChanged() { }
public void AddNodeFlags(params NodeFlags[] flags) {
foreach (var flag in flags) {
NodeFlags |= flag;
}
}
public void RemoveNodeFlags(params NodeFlags[] flags) {
foreach (var flag in flags) {
NodeFlags &= ~flag;
}
}
public void MarkDirty() {
foreach (var child in GetAllChildren(this)) {
child.ResNode->AddDrawFlag( [ DrawFlags.IsDirty ] );
}
ResNode->AddDrawFlag([ DrawFlags.IsDirty ] );
}
public bool CheckCollision(short x, short y, bool inclusive = true)
=> ResNode->CheckCollision(x, y, inclusive);
public bool CheckCollision(Vector2 position, bool inclusive = true)
=> ResNode->CheckCollision((short) position.X, (short) position.Y, inclusive);
public bool CheckCollision(AtkEventData* eventData, bool inclusive = true)
=> ResNode->CheckCollision(eventData, inclusive);
public Matrix2x2 Transform {
get => ResNode->Transform;
set => ResNode->Transform = value;
}
}
+19
View File
@@ -0,0 +1,19 @@
using KamiToolKit.Timelines;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase {
public Timeline? Timeline { get; private set; }
public void AddTimeline(Timeline timeline) {
Timeline?.Dispose();
Timeline = timeline;
ResNode->Timeline = timeline.InternalTimeline;
timeline.OwnerNode = ResNode;
}
public void AddTimeline(TimelineBuilder builder)
=> AddTimeline(builder.Build());
}
+151
View File
@@ -0,0 +1,151 @@
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Enums;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit;
public record InventoryItemTooltip(InventoryType Inventory, short Slot);
public unsafe partial class NodeBase {
private AtkTooltipManager.AtkTooltipType tooltipType = AtkTooltipManager.AtkTooltipType.None;
private bool tooltipEventsRegistered;
public virtual ReadOnlySeString TextTooltip {
get;
set {
field = value;
if (!value.IsEmpty) {
TryRegisterTooltipEvents();
tooltipType |= AtkTooltipManager.AtkTooltipType.Text;
}
else {
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Text;
}
}
}
public virtual uint ActionTooltip {
get;
set {
field = value;
if (value is not 0) {
TryRegisterTooltipEvents();
tooltipType |= AtkTooltipManager.AtkTooltipType.Action;
}
else {
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Action;
}
}
}
public virtual uint ItemTooltip {
get;
set {
field = value;
if (value is not 0) {
TryRegisterTooltipEvents();
tooltipType |= AtkTooltipManager.AtkTooltipType.Item;
}
else {
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Item;
}
}
}
public virtual InventoryItemTooltip? InventoryItemTooltip {
get;
set {
field = value;
if (value is not null) {
TryRegisterTooltipEvents();
tooltipType |= AtkTooltipManager.AtkTooltipType.Item;
}
else {
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Item;
}
}
}
private void TryRegisterTooltipEvents() {
if (tooltipEventsRegistered) return;
AddEvent(AtkEventType.MouseOver, ShowTooltip);
AddEvent(AtkEventType.MouseOut, HideTooltip);
OnVisibilityToggled += ToggleCollisionFlag;
ToggleCollisionFlag(IsVisible);
tooltipEventsRegistered = true;
}
private void UnregisterTooltipEvents() {
if (tooltipEventsRegistered) {
RemoveEvent(AtkEventType.MouseOver, ShowTooltip);
RemoveEvent(AtkEventType.MouseOut, HideTooltip);
OnVisibilityToggled -= ToggleCollisionFlag;
tooltipEventsRegistered = false;
}
}
private void ToggleCollisionFlag(bool isVisible) {
if (this is ComponentNode) return;
if (isVisible) {
AddNodeFlags(NodeFlags.HasCollision);
}
else {
RemoveNodeFlags(NodeFlags.HasCollision);
}
}
protected bool TooltipRegistered { get; set; }
public void ShowTooltip() {
if (ParentAddon is null) return; // Shouldn't be possible
if (tooltipType is AtkTooltipManager.AtkTooltipType.None) return;
using var stringBuilder = new RentedSeStringBuilder();
using var stringBuffer = new AtkValue();
if (!TextTooltip.IsEmpty) {
stringBuffer.SetManagedString(stringBuilder.Builder.Append(TextTooltip).GetViewAsSpan());
}
var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs();
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Text)) {
tooltipArgs.TextArgs.AtkArrayType = 0;
tooltipArgs.TextArgs.Text = stringBuffer.String;
}
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Action)) {
tooltipArgs.ActionArgs.Flags = 1;
tooltipArgs.ActionArgs.Kind = DetailKind.Action;
tooltipArgs.ActionArgs.Id = (int)ActionTooltip;
}
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Item) && InventoryItemTooltip is {} inventoryTooltip) {
tooltipArgs.ItemArgs.Kind = DetailKind.InventoryItem;
tooltipArgs.ItemArgs.InventoryType = inventoryTooltip.Inventory;
tooltipArgs.ItemArgs.Slot = inventoryTooltip.Slot;
tooltipArgs.ItemArgs.BuyQuantity = -1;
tooltipArgs.ItemArgs.Flag1 = 0;
}
else if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Item) && InventoryItemTooltip is null) {
tooltipArgs.ItemArgs.Kind = DetailKind.Item;
tooltipArgs.ItemArgs.ItemId = (int) ItemTooltip;
tooltipArgs.ItemArgs.BuyQuantity = -1;
tooltipArgs.ItemArgs.Flag1 = 0;
}
AtkStage.Instance()->TooltipManager.ShowTooltip(tooltipType, ParentAddon->Id, this, &tooltipArgs);
}
public void HideTooltip() {
if (ParentAddon is null) return;
AtkStage.Instance()->TooltipManager.HideTooltip(ParentAddon->Id);
}
}
+56
View File
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit;
public abstract unsafe class NodeBase<T> : NodeBase where T : unmanaged, ICreatable {
protected NodeBase(NodeType nodeType) {
if (MainThreadSafety.TryAssertMainThread()) return;
Log.Verbose($"Creating new node {GetType()}");
Node = NativeMemoryHelper.Create<T>();
if (ResNode is null) {
throw new Exception($"Unable to allocate memory for {typeof(T)}");
}
KamiToolKitLibrary.AllocatedNodes?.TryAdd((nint)Node, GetType());
BuildVirtualTable();
ResNode->Type = nodeType;
ResNode->NodeId = NodeIdBase + CurrentOffset++;
ResNode->ToggleVisibility(true);
CreatedNodes.Add(this);
}
public T* Node { get; private set; }
internal sealed override AtkResNode* ResNode => (AtkResNode*)Node;
public static implicit operator T*(NodeBase<T> node) => (T*) node.ResNode;
protected override void Dispose(bool disposing, bool isNativeDestructor) {
if (disposing) {
try {
base.Dispose(disposing, isNativeDestructor);
}
catch (Exception e) {
Log.Exception(e);
}
finally {
if (!isNativeDestructor) {
InvokeOriginalDestructor(ResNode, true);
}
KamiToolKitLibrary.AllocatedNodes?.Remove((nint)Node, out _);
Node = null;
}
}
}
}