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,61 @@
using System;
using System.IO;
using System.Numerics;
using System.Text.Json;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit;
public unsafe partial class NativeAddon {
private readonly JsonSerializerOptions serializerOptions = new() {
WriteIndented = true,
IncludeFields = true,
};
private AddonConfig LoadAddonConfig() {
var directory = DalamudInterface.Instance.PluginInterface.ConfigDirectory;
var file = new FileInfo(Path.Combine(directory.FullName, $"{InternalName}.addon.json"));
if (!file.Exists) {
file.Create().Close();
var newConfig = new AddonConfig();
SaveAddonConfig(newConfig);
return newConfig;
}
AddonConfig? addonConfig;
try {
var data = File.ReadAllText(file.FullName);
addonConfig = JsonSerializer.Deserialize<AddonConfig>(data, serializerOptions);
addonConfig ??= new AddonConfig();
}
catch (Exception e) {
DalamudInterface.Instance.Log.Error(e, "Exception while deserializing AddonConfig, creating new config.");
addonConfig = new AddonConfig();
SaveAddonConfig(addonConfig);
}
return addonConfig;
}
private void SaveAddonConfig(AddonConfig addonConfig) {
var directory = DalamudInterface.Instance.PluginInterface.ConfigDirectory;
var file = new FileInfo(Path.Combine(directory.FullName, $"{InternalName}.addon.json"));
var data = JsonSerializer.Serialize(addonConfig, serializerOptions);
FilesystemUtil.WriteAllTextSafe(file.FullName, data);
}
private void SaveAddonConfig() {
var configData = new AddonConfig {
Position = new Vector2(InternalAddon->X, InternalAddon->Y),
Scale = InternalAddon->Scale / AtkUnitBase.GetGlobalUIScale(),
};
SaveAddonConfig(configData);
}
}
@@ -0,0 +1,37 @@
using System.Linq;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit;
public abstract unsafe partial class NativeAddon {
private static Hook<AtkUnitBase.Delegates.FireCallback>? fireCallbackHook;
private static void InitializeCloseCallback() {
fireCallbackHook ??= DalamudInterface.Instance.GameInteropProvider
.HookFromAddress<AtkUnitBase.Delegates.FireCallback>(AtkUnitBase.Addresses.FireCallback.Value, OnFireCallback);
fireCallbackHook.Enable();
}
private static bool OnFireCallback(AtkUnitBase* thisPtr, uint valueCount, AtkValue* values, bool close) {
Log.Excessive($"[{thisPtr->NameString}] OnFireCallback");
foreach (var addon in CreatedAddons) {
if (addon == thisPtr && close && addon is { RespectCloseAll: true, IsOverlayAddon: false }) {
addon.Close();
return true;
}
}
return fireCallbackHook!.Original(thisPtr, valueCount, values, close);
}
private static void DisposeCloseCallback() {
if (CreatedAddons.Count is 0 || CreatedAddons.All(addon => addon.IsOverlayAddon)) {
fireCallbackHook?.Dispose();
fireCallbackHook = null;
}
}
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using KamiToolKit.Classes;
namespace KamiToolKit;
public abstract partial class NativeAddon : IDisposable {
private static readonly List<NativeAddon> CreatedAddons = [];
private bool isDisposed;
public virtual void Dispose() {
if (IsOverlayAddon) {
// Intentionally leak OverlayAddons,
// until Dalamud can implement OverlayAddons globally.
CreatedAddons.Remove(this);
GC.SuppressFinalize(this);
return;
}
if (!isDisposed) {
Log.Debug($"Disposing addon {GetType()}");
Close();
// Close will remove this node automatically on AtkUnitBase.Finalize,
// However, this is after the plugin unloads,
// and will trigger a warning in auto-dispose if we don't remove this now.
CreatedAddons.Remove(this);
GC.SuppressFinalize(this);
}
isDisposed = true;
DisposeCloseCallback();
}
~NativeAddon() => Dispose();
internal static void DisposeAddons() {
foreach (var addon in CreatedAddons.ToArray()) {
if (addon.IsOverlayAddon) continue;
Log.Warning($"Addon {addon.GetType()} was not disposed properly please ensure you call dispose at an appropriate time.");
Log.Debug($"Automatically disposing addon {addon.GetType()} as a safety measure.");
addon.Dispose();
}
CreatedAddons.Clear();
DisposeCloseCallback();
}
}
@@ -0,0 +1,66 @@
using KamiToolKit.Classes;
namespace KamiToolKit;
public unsafe partial class NativeAddon {
private void UpdateFlags() {
// Disable Native AddonConfig
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x40, true);
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A1, 0x4, DisableClose);
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x8, DisableCloseTransition);
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x40, DisableAddonConfig);
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x20, DisableClamping);
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x1, EnableContextMenu);
FlagHelper.UpdateFlag(ref InternalAddon->Flags1C8, 0x800, DisableScaleContextOption);
if (IsOverlayAddon) {
SetOverlayFlags();
}
}
private void SetOverlayFlags() {
OpenWindowSoundEffectId = 0;
InternalAddon->ShowSoundEffectId = 0;
// Disable ability to focus window
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A0, 0x80, true);
// Don't load into FocusedAddons list
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A1, 0x40, true);
// Disable Controller Nav
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x2, true);
// Disable open/close transitions
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x8, true);
// Disable open/close sounds
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x20, true);
// Enable ClickThrough
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x40, true);
}
public bool DisableClose { get; init; }
public bool DisableCloseTransition { get; init; }
internal bool DisableAddonConfig { get; init; } = true;
public bool EnableContextMenu { get; init; } = true;
public bool DisableClamping { get; init; } = true;
public bool DisableScaleContextOption { get; init; }
public bool RespectCloseAll { get; set; } = true;
public bool IgnoreGlobalScale { get; set; } = false;
}
@@ -0,0 +1,150 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit;
public abstract unsafe partial class NativeAddon {
protected virtual void OnSetup(AtkUnitBase* addon) { }
protected virtual void OnShow(AtkUnitBase* addon) { }
protected virtual void OnDraw(AtkUnitBase* addon) { }
protected virtual void OnUpdate(AtkUnitBase* addon) { }
protected virtual void OnHide(AtkUnitBase* addon) { }
protected virtual void OnFinalize(AtkUnitBase* addon) { }
protected virtual void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { }
protected virtual void OnRefresh(AtkUnitBase* addon, Span<AtkValue> atkValues) { }
private bool isSetup;
private void Initialize(AtkUnitBase* thisPtr) {
Log.Verbose($"[{InternalName}] Initialize");
AtkUnitBase.StaticVirtualTablePointer->Initialize(thisPtr);
thisPtr->UldManager.InitializeResourceRendererManager();
InitializeAddon();
}
private void Setup(AtkUnitBase* addon, uint valueCount, AtkValue* values) {
Log.Verbose($"[{InternalName}] Setup");
if (!IsOverlayAddon) {
SetInitialState();
}
else {
ref var screenSize = ref AtkStage.Instance()->ScreenSize;
addon->SetScale(1.0f / AtkUnitBase.GetGlobalUIScale(), true);
addon->SetSize((ushort)screenSize.Width, (ushort)screenSize.Height);
addon->SetPosition(0, 0);
}
AtkUnitBase.StaticVirtualTablePointer->OnSetup(addon, valueCount, values);
OnSetup(addon);
isSetup = true;
}
private void Show(AtkUnitBase* addon, bool silenceOpenSoundEffect, uint unsetShowHideFlags) {
Log.Verbose($"[{InternalName}] Show");
OnShow(addon);
AtkUnitBase.StaticVirtualTablePointer->Show(addon, silenceOpenSoundEffect, unsetShowHideFlags);
}
private void Update(AtkUnitBase* addon, float delta) {
Log.Excessive($"[{InternalName}] Update");
OnUpdate(addon);
AtkUnitBase.StaticVirtualTablePointer->Update(addon, delta);
}
private void Draw(AtkUnitBase* addon) {
Log.Excessive($"[{InternalName}] Draw");
OnDraw(addon);
AtkUnitBase.StaticVirtualTablePointer->Draw(addon);
}
private void Hide(AtkUnitBase* addon, bool unkBool, bool callHideCallback, uint setShowHideFlags) {
Log.Verbose($"[{InternalName}] Hide");
OnHide(addon);
SaveAddonConfig();
AtkUnitBase.StaticVirtualTablePointer->Hide(addon, unkBool, callHideCallback, setShowHideFlags);
AtkUnitBase.StaticVirtualTablePointer->Close(addon, false);
}
private void Hide2(AtkUnitBase* addon) {
Log.Verbose($"[{InternalName}] Hide2");
AtkUnitBase.StaticVirtualTablePointer->Hide2(addon);
}
private void Finalizer(AtkUnitBase* addon) {
Log.Verbose($"[{InternalName}] Finalize");
OnFinalize(addon);
if (RememberClosePosition) {
LastClosePosition = new Vector2(InternalAddon->X, InternalAddon->Y);
}
AtkUnitBase.StaticVirtualTablePointer->Finalizer(InternalAddon);
isSetup = false;
}
private AtkEventListener* Destructor(AtkUnitBase* addon, byte flags) {
Log.Verbose($"[{InternalName}] Destructor");
var result = AtkUnitBase.StaticVirtualTablePointer->Dtor(addon, flags);
if ((flags & 1) == 1) {
InternalAddon = null;
disposeHandle?.Free();
disposeHandle = null;
CreatedAddons.Remove(this);
// Free our custom virtual table, the game doesn't know this exists and won't clear it on its own.
NativeMemoryHelper.Free(virtualTable, 0x8 * VirtualTableEntryCount);
}
return result;
}
private void RequestedUpdate(AtkUnitBase* thisPtr, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) {
Log.Verbose($"[{InternalName}] RequestedUpdate");
// Prevent calls to OnRequestedUpdate before Setup is completed. The game will try to call this after Show but before Setup
if (isSetup) {
OnRequestedUpdate(thisPtr, numberArrayData, stringArrayData);
}
AtkUnitBase.StaticVirtualTablePointer->OnRequestedUpdate(InternalAddon, numberArrayData, stringArrayData);
}
private bool Refresh(AtkUnitBase* thisPtr, uint valueCount, AtkValue* values) {
Log.Verbose($"[{InternalName}] Refresh");
OnRefresh(thisPtr, new Span<AtkValue>(values, (int)valueCount));
return AtkUnitBase.StaticVirtualTablePointer->OnRefresh(InternalAddon, valueCount, values);
}
private void ScreenSizeChange(AtkUnitBase* thisPtr, int width, int height) {
Log.Verbose($"[{InternalName}] ScreenSizeChange");
AtkUnitBase.StaticVirtualTablePointer->OnScreenSizeChange(thisPtr, width, height);
if (IsOverlayAddon || IgnoreGlobalScale) {
thisPtr->SetScale(1.0f / AtkUnitBase.GetGlobalUIScale(), true);
}
}
}
@@ -0,0 +1,58 @@
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text.ReadOnly;
namespace KamiToolKit;
public abstract unsafe partial class NativeAddon {
public void SetWindowPosition(Vector2 windowPosition) {
if (InternalAddon is null) return;
InternalAddon->SetPosition((short)windowPosition.X, (short)windowPosition.Y);
}
public void SetWindowSize(Vector2 windowSize) {
if (InternalAddon is null) return;
Size = windowSize;
InternalAddon->SetSize((ushort)Size.X, (ushort)Size.Y);
WindowNode?.Size = Size;
}
protected void SetWindowSize(float width, float height)
=> SetWindowSize(new Vector2(width, height));
public required string InternalName {
get;
init => field = new string(value.Replace(" ", "").Take(31).ToArray());
}
public required ReadOnlySeString Title { get; set; }
public ReadOnlySeString? Subtitle { get; set; }
public int OpenWindowSoundEffectId { get; set; } = 23;
public Vector2 Size { get; set; } = new(400.0f, 400.0f);
public Vector2 ContentStartPosition => (WindowNode?.ContentStartPosition ?? Vector2.Zero) + ContentPadding;
public Vector2 ContentSize => (WindowNode?.ContentSize ?? Vector2.Zero) - ContentPadding * 2.0f - new Vector2(0.0f, 4.0f);
public Vector2 ContentPadding { get; set; } = new(8.0f, 0.0f);
public int DepthLayer { get; init; } = 5;
public bool IsOpen => InternalAddon is not null && InternalAddon->IsVisible;
public int AddonId => InternalAddon is null ? 0 : InternalAddon->Id;
public bool RememberClosePosition { get; set; } = true;
internal Vector2 LastClosePosition = Vector2.Zero;
public static implicit operator AtkUnitBase*(NativeAddon addon) => addon.InternalAddon;
internal bool IsOverlayAddon { get; init; }
}
@@ -0,0 +1,60 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit;
public abstract unsafe partial class NativeAddon {
private const int VirtualTableEntryCount = 200;
private AtkUnitBase.Delegates.Dtor destructorFunction = null!;
private AtkUnitBase.Delegates.Draw drawFunction = null!;
private AtkUnitBase.Delegates.Finalizer finalizerFunction = null!;
private AtkUnitBase.Delegates.Hide hideFunction = null!;
private AtkUnitBase.Delegates.Initialize initializeFunction = null!;
private AtkUnitBase.Delegates.OnSetup onSetupFunction = null!;
private AtkUnitBase.Delegates.Show showFunction = null!;
private AtkUnitBase.Delegates.Hide2 softHideFunction = null!;
private AtkUnitBase.Delegates.Update updateFunction = null!;
private AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction = null!;
private AtkUnitBase.Delegates.OnRefresh onRefreshFunction = null!;
private AtkUnitBase.Delegates.OnScreenSizeChange onScreenSizeChangedFunction = null!;
private AtkUnitBase.AtkUnitBaseVirtualTable* virtualTable;
private void RegisterVirtualTable() {
// Overwrite virtual table with a custom copy,
// Note: currently there are 73 vfuncs, but there's no harm in copying more for when they add new vfuncs to the game
virtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)NativeMemoryHelper.Malloc(0x8 * VirtualTableEntryCount);
NativeMemory.Copy(InternalAddon->VirtualTable, virtualTable, 0x8 * VirtualTableEntryCount);
InternalAddon->VirtualTable = virtualTable;
initializeFunction = Initialize;
onSetupFunction = Setup;
showFunction = Show;
updateFunction = Update;
drawFunction = Draw;
hideFunction = Hide;
softHideFunction = Hide2;
finalizerFunction = Finalizer;
destructorFunction = Destructor;
onRequestedUpdateFunction = RequestedUpdate;
onRefreshFunction = Refresh;
onScreenSizeChangedFunction = ScreenSizeChange;
virtualTable->Initialize = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(initializeFunction);
virtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(onSetupFunction);
virtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(showFunction);
virtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(updateFunction);
virtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(drawFunction);
virtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(hideFunction);
virtualTable->Hide2 = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(softHideFunction);
virtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(finalizerFunction);
virtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(destructorFunction);
virtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(onRequestedUpdateFunction);
virtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(onRefreshFunction);
virtualTable->OnScreenSizeChange = (delegate* unmanaged<AtkUnitBase*, int, int, void>)Marshal.GetFunctionPointerForDelegate(onScreenSizeChangedFunction);
}
}
+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());
}
}