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
+215
View File
@@ -0,0 +1,215 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using KamiToolKit.Timelines;
namespace KamiToolKit;
public abstract unsafe partial class NativeAddon {
private GCHandle? disposeHandle;
internal AtkUnitBase* InternalAddon;
public ResNode RootNode = null!;
protected WindowNodeBase? WindowNode { get; private set; }
private void AllocateAddon() {
if (InternalAddon is not null) {
Log.Warning("Tried to allocate addon that was already allocated.");
return;
}
var currentAddonCount = RaptureAtkUnitManager.Instance()->AllLoadedUnitsList.Count;
if (currentAddonCount >= 200) {
Log.Warning($"WARNING: Current Addon Count is approaching hard limits ({currentAddonCount}/250). Please ensure custom Addons are not being used as overlays.");
}
if (currentAddonCount >= 225) {
Log.Error($"ERROR: Current Addon Count is too high. Aborting allocation ({currentAddonCount}/250).");
return;
}
if (InternalName.Length is 0) {
throw new NullReferenceException("InternalName is empty, this is not allowed.");
}
Log.Verbose($"[{InternalName}] Allocating NativeAddon");
if (!IsOverlayAddon) {
InitializeCloseCallback();
}
InternalAddon = NativeMemoryHelper.Create<AtkUnitBase>();
RegisterVirtualTable();
RootNode = new ResNode {
NodeId = 1,
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.Fill | NodeFlags.Focusable | NodeFlags.EmitsEvents,
IsAddonRootNode = true,
};
if (!IsOverlayAddon) {
WindowNode = CreateWindowNode?.Invoke() ?? new WindowNode();
WindowNode.NodeId = 2;
}
InternalAddon->NameString = InternalName;
InternalAddon->ShowSoundEffectId = (short)OpenWindowSoundEffectId;
UpdateFlags();
}
private void InitializeAddon() {
var widgetInfo = NativeMemoryHelper.UiAlloc<AtkUldWidgetInfo>(1, 16);
widgetInfo->Id = 1;
widgetInfo->NodeCount = 0;
widgetInfo->NodeList = null;
widgetInfo->WidgetAlignment = new AtkWidgetAlignment {
AlignmentType = AlignmentType.Center,
X = 50.0f,
Y = 50.0f,
};
InternalAddon->UldManager.Objects = (AtkUldObjectInfo*)widgetInfo;
InternalAddon->UldManager.ObjectCount = 1;
InternalAddon->UldManager.ResourceFlags |= AtkUldManagerResourceFlag.ArraysAllocated;
InternalAddon->RootNode = RootNode;
InternalAddon->UldManager.AddNodeToObjectList(RootNode);
LoadTimeline();
InternalAddon->UldManager.UpdateDrawNodeList();
InternalAddon->UldManager.LoadedState = AtkLoadState.Loaded;
if (!IsOverlayAddon && WindowNode is not null) {
WindowNode.AttachNode(this, NodePosition.AsFirstChild);
InternalAddon->WindowNode = WindowNode;
InternalAddon->UldManager.AddNodeToObjectList(WindowNode);
}
// UldManager finished loading the uld
InternalAddon->Flags198 |= 2 << 0x1C;
// LoadUldByName called
InternalAddon->Flags1A2 |= 4;
InternalAddon->UpdateCollisionNodeList(false);
// Set focus node to allow controller nav
WindowNode?.WindowHeaderFocusNode.AddNodeFlags(NodeFlags.Focusable);
InternalAddon->FocusNode = WindowNode is not null ? WindowNode.WindowHeaderFocusNode : RootNode;
// Now that we have constructed this instance, track it for auto-dispose
CreatedAddons.Add(this);
}
private void SetInitialState() {
WindowNode?.SetTitle(Title.ToString(), Subtitle?.ToString() ?? KamiToolKitLibrary.DefaultWindowSubtitle);
InternalAddon->ShowSoundEffectId = (short)OpenWindowSoundEffectId;
var addonConfig = LoadAddonConfig();
if (addonConfig.Position != Vector2.Zero) {
InternalAddon->SetPosition((short)addonConfig.Position.X, (short)addonConfig.Position.Y);
}
else {
var screenSize = new Vector2(AtkStage.Instance()->ScreenSize.Width, AtkStage.Instance()->ScreenSize.Height);
var defaultPosition = screenSize / 2.0f - Size / 2.0f;
InternalAddon->SetPosition((short)defaultPosition.X, (short)defaultPosition.Y);
}
if (addonConfig.Scale is not 1.0f) {
var newScale = Math.Clamp(addonConfig.Scale, 0.25f, 6.0f);
InternalAddon->SetScale(newScale, true);
}
SetWindowSize(Size);
if (LastClosePosition != Vector2.Zero && RememberClosePosition) {
InternalAddon->SetPosition((short)LastClosePosition.X, (short)LastClosePosition.Y);
}
}
public Func<WindowNodeBase>? CreateWindowNode { get; init; }
/// <summary>
/// Initializes and Opens this instance of Addon
/// </summary>
public void Open() => DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
Log.Verbose($"[{InternalName}] Open Called");
if (InternalAddon is null) {
AllocateAddon();
if (InternalAddon is not null) {
AtkStage.Instance()->RaptureAtkUnitManager->InitializeAddon(InternalAddon, InternalName);
InternalAddon->Open((uint)DepthLayer - 1);
disposeHandle = GCHandle.Alloc(this);
}
}
else {
Log.Verbose($"[{InternalName}] Already open, skipping call.");
}
});
[Conditional("DEBUG")]
public void DebugOpen() => Open();
public void Close() {
if (InternalAddon is null) return;
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
Log.Verbose($"[{InternalName}] Close");
if (InternalAddon is not null) {
InternalAddon->Close(false);
}
});
}
public void Toggle() {
if (IsOpen) {
Close();
}
else {
Open();
}
}
public void AddNode(ICollection<NodeBase> nodes) {
foreach (var node in nodes) {
AddNode(node);
}
}
public void AddNode(NodeBase? node)
=> node?.AttachNode(this);
private void LoadTimeline() {
RootNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 89)
.AddLabel(1, 101, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(10, 102, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(20, 103, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(30, 104, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(40, 105, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(50, 106, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(60, 107, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(70, 108, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(80, 109, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build());
}
}