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
+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;
}
}
}