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
@@ -0,0 +1,20 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ModifierFlag = FFXIVClientStructs.FFXIV.Component.GUI.AtkEventData.AtkMouseData.ModifierFlag;
namespace KamiToolKit.Extensions;
public static class AtkEventDataExtensions {
extension(ref AtkEventData data) {
public Vector2 MousePosition => new(data.MouseData.PosX, data.MouseData.PosY);
public bool IsLeftClick => data.MouseData.ButtonId is 0;
public bool IsRightClick => data.MouseData.ButtonId is 1;
public bool IsNoModifiers => data.MouseData.Modifier is 0;
public bool IsAltHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Alt);
public bool IsControlHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Ctrl);
public bool IsShiftHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Shift);
public bool IsDragging => data.MouseData.Modifier.HasFlag(ModifierFlag.Dragging);
public bool IsScrollUp => data.MouseData.WheelDirection >= 1;
public bool IsScrollDown => data.MouseData.WheelDirection <= -1;
}
}
@@ -0,0 +1,19 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Extensions;
public static unsafe class AtkImageNodeExtensions {
extension(ref AtkImageNode node) {
public uint IconId => node.GetIconId();
private uint GetIconId() {
if (node.PartsList is null) return 0;
if (node.PartsList->Parts is null) return 0;
if (node.PartsList->Parts->UldAsset is null) return 0;
if (node.PartsList->Parts->UldAsset->AtkTexture.TextureType is not TextureType.Resource) return 0;
if (node.PartsList->Parts->UldAsset->AtkTexture.Resource is null) return 0;
return node.PartsList->Parts->UldAsset->AtkTexture.Resource->IconId;
}
}
}
@@ -0,0 +1,140 @@
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
namespace KamiToolKit.Extensions;
public static unsafe class AtkResNodeExtensions {
extension(ref AtkResNode node) {
public Vector2 Position {
get => new(node.X, node.Y);
set => node.SetPositionFloat(value.X, value.Y);
}
public Vector2 ScreenPosition
=> new(node.ScreenX, node.ScreenY);
public Vector2 Size {
get => new(node.GetWidth(), node.GetHeight());
set {
node.SetWidth((ushort) value.X);
node.SetHeight((ushort) value.Y);
}
}
public Bounds Bounds => new() {
TopLeft = node.Position,
BottomRight = node.Position + node.Size,
};
public Vector2 Center
=> node.Position + node.Size / 2.0f;
public Vector2 Scale {
get => new (node.GetScaleX(), node.GetScaleY());
set => node.SetScale(value.X, value.Y);
}
public float RotationDegrees {
get => node.GetRotationDegrees();
set => node.SetRotationDegrees(value - (int)(value / 360.0f) * 360.0f);
}
public Vector2 Origin {
get => new(node.OriginX, node.OriginY);
set => node.SetOrigin(value.X, value.Y);
}
public bool Visible {
get => node.IsVisible();
set => node.ToggleVisibility(value);
}
public Vector4 ColorVector {
get => node.Color.ToVector4();
set => node.Color = value.ToByteColor();
}
public ColorHelpers.HsvaColor ColorHsva {
get => ColorHelpers.RgbaToHsv(node.ColorVector);
set => node.Color = ColorHelpers.HsvToRgb(value).ToByteColor();
}
public Vector3 AddColor {
get => new Vector3(node.AddRed, node.AddGreen, node.AddBlue) / 255.0f;
set {
node.AddRed = (short)(value.X * 255);
node.AddGreen = (short)(value.Y * 255);
node.AddBlue = (short)(value.Z * 255);
}
}
public ColorHelpers.HsvaColor AddColorHsva {
get => ColorHelpers.RgbaToHsv(node.AddColor.AsVector4());
set => node.AddColor = ColorHelpers.HsvToRgb(value).AsVector3();
}
public Vector3 MultiplyColor {
get => new Vector3(node.MultiplyRed, node.MultiplyGreen, node.MultiplyBlue) / 100.0f;
set {
node.MultiplyRed = (byte)(value.X * 100.0f);
node.MultiplyGreen = (byte)(value.Y * 100.0f);
node.MultiplyBlue = (byte)(value.Z * 100.0f);
}
}
public ColorHelpers.HsvaColor MultiplyColorHsva {
get => ColorHelpers.RgbaToHsv(node.MultiplyColor.AsVector4());
set => node.MultiplyColor = ColorHelpers.HsvToRgb(value).AsVector3();
}
public void AddNodeFlag(params NodeFlags[] flags) {
foreach (var flag in flags) {
node.NodeFlags |= flag;
}
}
public void RemoveNodeFlag(params NodeFlags[] flags) {
foreach (var flag in flags) {
node.NodeFlags &= ~flag;
}
}
public void AddDrawFlag(params DrawFlags[] flags) {
foreach (var flag in flags) {
node.DrawFlags |= (uint)flag;
}
}
public void RemoveDrawFlag(params DrawFlags[] flags) {
foreach (var flag in flags) {
node.DrawFlags &= (uint)flag;
}
}
public bool CheckCollision(short x, short y, bool inclusive = true)
=> node.CheckCollisionAtCoords(x, y, inclusive);
public bool CheckCollision(Vector2 position, bool inclusive = true)
=> node.CheckCollisionAtCoords((short) position.X, (short) position.Y, inclusive);
public bool CheckCollision(AtkEventData* eventData, bool inclusive = true)
=> node.CheckCollisionAtCoords(eventData->MouseData.PosX, eventData->MouseData.PosY, inclusive);
public bool IsActuallyVisible {
get {
if (!node.Visible) return false;
var targetNode = node.ParentNode;
while (targetNode is not null) {
if (!targetNode->Visible) return false;
targetNode = targetNode->ParentNode;
}
return true;
}
}
}
}
@@ -0,0 +1,39 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Extensions;
public static unsafe class AtkStageExtensions {
extension(ref AtkStage atkStage) {
public void ClearNodeFocus(AtkResNode* targetNode) {
if (targetNode is null) return;
foreach (ref var focusEntry in atkStage.AtkInputManager->FocusList) {
// If this entry has no listener/addon, skip it
if (focusEntry.AtkEventListener is null) continue;
// If this entry has our target node
if (focusEntry.AtkEventTarget == targetNode) {
// Clear the entry
focusEntry.AtkEventTarget = null;
focusEntry.FocusParam = 0;
// Clear the input managers focused node
atkStage.AtkInputManager->FocusedNode = null;
// Clear collision managers collision node
atkStage.AtkCollisionManager->IntersectingCollisionNode = null;
// Also remove this node from any additional focus nodes the addon might reference
var addon = (AtkUnitBase*) focusEntry.AtkEventListener;
foreach (ref var node in addon->AdditionalFocusableNodes) {
if (node.Value == targetNode) {
node = null;
}
}
}
}
}
}
}
@@ -0,0 +1,137 @@
using System;
using System.Linq;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
using KamiToolKit.Classes;
namespace KamiToolKit.Extensions;
public static unsafe class AtkUldManagerExtensions {
extension(ref AtkUldManager manager) {
private bool IsNodeInObjectList(AtkResNode* node) {
foreach (var objectNode in manager.ObjectNodeSpan) {
if (objectNode.Value == node) return true;
}
return false;
}
public bool IsNodeInDrawList(AtkResNode* node) {
foreach (var drawNode in manager.Nodes) {
if (drawNode.Value == node) return true;
}
return false;
}
/// <summary>
/// Adds node and all children nodes to this UldManager's Object List
/// </summary>
public void AddNodeToObjectList(NodeBase node) {
manager.AddNodeToObjectList(node.ResNode);
foreach (var child in NodeBase.GetLocalChildren(node)) {
manager.AddNodeToObjectList(child.ResNode);
}
manager.UpdateDrawNodeList();
}
public void AddNodeToObjectList(AtkResNode* newNode) {
if (newNode is null) return;
// If the node is already in the object list, skip.
if (manager.IsNodeInObjectList(newNode)) return;
var oldSize = manager.Objects->NodeCount;
var newSize = oldSize + 1;
var newBuffer = (AtkResNode**)NativeMemoryHelper.Malloc((ulong)(newSize * 8));
if (oldSize > 0) {
foreach (var index in Enumerable.Range(0, oldSize)) {
newBuffer[index] = manager.Objects->NodeList[index];
}
NativeMemoryHelper.Free(manager.Objects->NodeList, (ulong)(oldSize * 8));
}
newBuffer[newSize - 1] = newNode;
manager.Objects->NodeList = newBuffer;
manager.Objects->NodeCount = newSize;
}
/// <summary>
/// Removes node and all children nodes from this UldManager's Object List
/// </summary>
public void RemoveNodeFromObjectList(NodeBase node) {
manager.RemoveNodeFromObjectList(node.ResNode);
foreach (var child in NodeBase.GetLocalChildren(node)) {
manager.RemoveNodeFromObjectList(child.ResNode);
}
manager.UpdateDrawNodeList();
}
public void RemoveNodeFromObjectList(AtkResNode* node) {
if (node is null) return;
// If the node isn't in the object list, skip.
if (!manager.IsNodeInObjectList(node)) return;
var oldSize = manager.Objects->NodeCount;
var newSize = oldSize - 1;
var newBuffer = (AtkResNode**)NativeMemoryHelper.Malloc((ulong)(newSize * 8));
var newIndex = 0;
foreach (var index in Enumerable.Range(0, oldSize)) {
if (manager.Objects->NodeList[index] != node) {
newBuffer[newIndex] = manager.Objects->NodeList[index];
newIndex++;
}
}
NativeMemoryHelper.Free(manager.Objects->NodeList, (ulong)(oldSize * 8));
manager.Objects->NodeList = newBuffer;
manager.Objects->NodeCount = newSize;
}
public void PrintObjectList() {
Log.Debug("Beginning NodeList");
foreach (var index in Enumerable.Range(0, manager.Objects->NodeCount)) {
var nodePointer = manager.Objects->NodeList[index];
Log.Debug($"[{index}]: {(nint)nodePointer:X}");
}
}
public uint GetMaxNodeId() {
uint max = 1;
foreach (var child in manager.Nodes) {
if (child.Value is null) continue;
max = Math.Max(child.Value->NodeId, max);
}
return max;
}
public Span<Pointer<AtkResNode>> ObjectNodeSpan
=> new(manager.Objects->NodeList, manager.Objects->NodeCount);
public T* SearchNodeById<T>(uint nodeId) where T : unmanaged {
foreach (var node in manager.Nodes) {
if (node.Value is not null) {
if (node.Value->NodeId == nodeId)
return (T*) node.Value;
}
}
return null;
}
public AtkResNode* SearchNodeById(uint nodeId)
=> manager.SearchNodeById<AtkResNode>(nodeId);
}
}
@@ -0,0 +1,110 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Interface.Textures.TextureWraps;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Extensions;
public static unsafe class AtkUldPartExtensions {
extension(ref AtkUldPart part) {
public bool IsTextureReady => part.UldAsset is not null && part.UldAsset->AtkTexture.IsTextureReady();
public Vector2 LoadedTextureSize => part.GetActualTextureSize();
public string LoadedPath => part.GetLoadedPath();
public void LoadTexture(string path, bool resolveTheme = true) {
try {
if (part.UldAsset is null) return;
part.TryUnloadTexture();
var texturePath = path.Replace("_hr1", string.Empty);
var themedPath = texturePath.Replace("uld", GetThemePathModifier());
if (DalamudInterface.Instance.DataManager.FileExists(themedPath) && resolveTheme) {
texturePath = themedPath;
}
if (DalamudInterface.Instance.DataManager.FileExists(texturePath)) {
part.UldAsset->AtkTexture.LoadTextureWithDefaultVersion(texturePath);
}
}
catch (Exception e) {
Log.Exception(e);
}
}
public void LoadIcon(uint iconId)
=> part.UldAsset->AtkTexture.LoadIconTexture(iconId, GetIconSubFolder(iconId));
private Vector2 GetActualTextureSize() {
if (part.UldAsset is null) return Vector2.Zero;
if (!part.UldAsset->AtkTexture.IsTextureReady()) return Vector2.Zero;
if (part.UldAsset->AtkTexture.TextureType is 0) return Vector2.Zero;
if (part.UldAsset->AtkTexture.KernelTexture is null) return Vector2.Zero;
var width = part.UldAsset->AtkTexture.GetTextureWidth();
var height = part.UldAsset->AtkTexture.GetTextureHeight();
return new Vector2(width, height);
}
public void LoadTexture(Texture* texture) {
if (part.UldAsset is null) return;
part.TryUnloadTexture();
part.UldAsset->AtkTexture.KernelTexture = texture;
part.UldAsset->AtkTexture.TextureType = TextureType.KernelTexture;
}
public void LoadTexture(IDalamudTextureWrap textureWrap) {
var texturePointer = (Texture*)DalamudInterface.Instance.TextureProvider.ConvertToKernelTexture(textureWrap, true);
if (texturePointer is null) return;
part.LoadTexture(texturePointer);
}
private string GetLoadedPath() {
if (part.UldAsset is null) return string.Empty;
if (part.UldAsset->AtkTexture.Resource is null) return string.Empty;
if (part.UldAsset->AtkTexture.Resource->TexFileResourceHandle is null) return string.Empty;
return part.UldAsset->AtkTexture.Resource->TexFileResourceHandle->FileName.ToString();
}
private void TryUnloadTexture() {
if (part.UldAsset is null) return;
if (!part.UldAsset->AtkTexture.IsTextureReady()) return;
if (part.UldAsset->AtkTexture.TextureType is 0) return;
if (part.UldAsset->AtkTexture.KernelTexture is null) return;
part.UldAsset->AtkTexture.ReleaseTexture();
part.UldAsset->AtkTexture.KernelTexture = null;
part.UldAsset->AtkTexture.TextureType = 0;
}
}
private static string GetThemePathModifier() => AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType switch {
not 0 => $"uld/img{AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType:00}",
_ => "uld",
};
public static IconSubFolder GetIconSubFolder(uint iconId) {
var textureManager = AtkStage.Instance()->AtkTextureResourceManager;
Span<byte> buffer = stackalloc byte[0x100];
buffer.Clear();
var bytePointer = (byte*) Unsafe.AsPointer(ref buffer[0]);
var textureScale = textureManager->DefaultTextureScale;
var targetFolder = (IconSubFolder)textureManager->IconLanguage;
// Try to resolve the path using the current language
AtkTexture.GetIconPath(bytePointer, iconId, textureScale, targetFolder);
var pathResult = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bytePointer).String;
// If the resolved path doesn't exist, re-process with default folder
return DalamudInterface.Instance.DataManager.FileExists(pathResult) ? targetFolder : IconSubFolder.None;
}
}
@@ -0,0 +1,42 @@
using System;
using System.Linq;
using System.Numerics;
using System.Reflection;
using FFXIVClientStructs.Attributes;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Extensions;
public static unsafe class AtkUnitBaseExtensions {
public static string GetAddonTypeName<T>() where T : unmanaged {
var type = typeof(T);
var attribute = type.GetCustomAttributes().OfType<AddonAttribute>().FirstOrDefault();
if (attribute is null) throw new Exception("Unable to find AddonAttribute to resolve addon name.");
var addonName = attribute.AddonIdentifiers.FirstOrDefault();
if (addonName is null) throw new Exception("Addon attribute names are empty.");
return addonName;
}
extension(ref AtkUnitBase addon) {
public Vector2 Size => addon.GetSize();
public Vector2 RootSize => addon.GetRootSize();
public Vector2 Position => new(addon.X, addon.Y);
private Vector2 GetSize() {
var width = stackalloc short[1];
var height = stackalloc short[1];
addon.GetSize(width, height, false);
return new Vector2(*width, *height);
}
private Vector2 GetRootSize() {
if (addon.RootNode is null) return Vector2.Zero;
return new Vector2(addon.RootNode->Width, addon.RootNode->Height);
}
}
}
@@ -0,0 +1,12 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.Graphics;
namespace KamiToolKit.Extensions;
public static class ByteColorExtensions {
public static Vector4 ToVector4(this ByteColor color)
=> new(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, color.A / 255.0f);
public static ByteColor ToByteColor(this Vector4 v)
=> new() { A = (byte)(v.W * 255), R = (byte)(v.X * 255), G = (byte)(v.Y * 255), B = (byte)(v.Z * 255) };
}
+52
View File
@@ -0,0 +1,52 @@
using System;
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Utility;
namespace KamiToolKit.Extensions;
internal static class EnumExtensions {
extension(Enum enumValue) {
public string Description => enumValue.GetDescription();
private string GetDescription() {
var attribute = enumValue.GetAttribute<DescriptionAttribute>();
return attribute?.Description ?? enumValue.ToString();
}
}
extension<T>(ref T flagValue) where T : unmanaged, Enum {
public void SetFlags(params T[] flags) {
foreach (var flag in flags) {
flagValue.SetFlag(flag, true);
}
}
public void ClearFlags(params T[] flags) {
foreach (var flag in flags) {
flagValue.SetFlag(flag, false);
}
}
private unsafe void SetFlag(T flag, bool enable) {
switch (sizeof(T)) {
case 1: flagValue.SetFlag<T, byte>(flag, enable); break;
case 2: flagValue.SetFlag<T, ushort>(flag, enable); break;
case 4: flagValue.SetFlag<T, uint>(flag, enable); break;
case 8: flagValue.SetFlag<T, ulong>(flag, enable); break;
default: throw new NotSupportedException("Unsupported enum size");
}
}
private void SetFlag<TUnderlying>(T flag, bool enable) where TUnderlying : unmanaged, IBinaryInteger<TUnderlying> {
ref var value = ref Unsafe.As<T, TUnderlying>(ref flagValue);
var mask = Unsafe.As<T, TUnderlying>(ref flag);
if (enable)
value |= mask;
else
value &= ~mask;
}
}
}
@@ -0,0 +1,16 @@
using System.Drawing;
using System.Numerics;
using Dalamud.Interface;
using Vector4 = System.Numerics.Vector4;
namespace KamiToolKit.Extensions;
public static class KnownColorExtensions {
public static Vector3 Vector3(this KnownColor color) {
var color4 = color.Vector();
return new Vector3(color4.X, color4.Y, color4.Z);
}
public static Vector3 AsVector3Color(this Vector4 vector4)
=> new(vector4.X, vector4.Y, vector4.Z);
}
@@ -0,0 +1,23 @@
using System.Runtime.CompilerServices;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using KamiToolKit.Classes;
namespace KamiToolKit.Extensions;
public static unsafe class MainThreadSafety {
/// <summary>
/// Returns true if <em>not</em> on the main thread. Use this to return early.
/// </summary>
public static bool TryAssertMainThread([CallerFilePath] string? callerFilePath = null, [CallerMemberName] string? callerName = null) {
if (Framework.Instance()->IsDestroying) return true;
if (!ThreadSafety.IsMainThread) {
Log.Error($"{callerFilePath?.Split(@"\")[^1][..^2]}{callerName} must be invoked from the main thread.");
return true;
}
return false;
}
}
@@ -0,0 +1,10 @@
using System;
using System.Text;
namespace KamiToolKit.Extensions;
public static class ReadOnlySpanExtensions {
extension(ReadOnlySpan<byte> span) {
public string String => Encoding.UTF8.GetString(span);
}
}
@@ -0,0 +1,13 @@
using System.Diagnostics;
using KamiToolKit.Classes;
namespace KamiToolKit.Extensions;
public static class StopwatchExtensions {
extension(Stopwatch stopwatch) {
public void LogTime(string logMessage) {
DalamudInterface.Instance.Log.Debug($"{logMessage, -15}: {stopwatch, 15} :: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
}
}
}