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
+125
View File
@@ -0,0 +1,125 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Controllers;
public class AddonController(string addonName) : AddonController<AtkUnitBase>(addonName);
/// <summary>
/// This class provides functionality to add-and manage custom elements for any Addon
/// </summary>
public unsafe class AddonController<T> : AddonEventController<T>, IDisposable where T : unmanaged {
internal readonly string AddonName;
private AtkUnitBase* AddonPointer => (AtkUnitBase*)DalamudInterface.Instance.GameGui.GetAddonByName(AddonName).Address;
private bool IsEnabled { get; set; }
private bool isSetupComplete;
/// <summary>
/// This class provides functionality to add-and manage custom elements for any Addon
/// </summary>
public AddonController(string addonName) {
if (addonName is "NamePlate") {
throw new Exception("Attaching to NamePlate is not supported. Use OverlayController instead.");
}
AddonName = addonName;
}
public virtual void Dispose() => Disable();
public void Enable() {
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
if (IsEnabled) return;
onInnerPreEnable?.Invoke((T*)AddonPointer);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, AddonName, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, AddonName, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostRefresh, AddonName, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, AddonName, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostUpdate, AddonName, OnAddonEvent);
if (AddonPointer is not null) {
OnInnerAttach?.Invoke((T*)AddonPointer);
isSetupComplete = true;
}
IsEnabled = true;
onInnerPostEnable?.Invoke((T*)AddonPointer);
});
}
private void OnAddonEvent(AddonEvent type, AddonArgs args) {
var addon = (T*)args.Addon.Address;
switch (type) {
case AddonEvent.PostSetup:
OnInnerAttach?.Invoke(addon);
isSetupComplete = true;
return;
case AddonEvent.PreFinalize:
OnInnerDetach?.Invoke(addon);
isSetupComplete = false;
return;
case AddonEvent.PostRefresh or AddonEvent.PostRequestedUpdate when isSetupComplete:
OnInnerRefresh?.Invoke(addon);
return;
case AddonEvent.PostUpdate:
OnInnerUpdate?.Invoke(addon);
return;
}
}
public void Disable() {
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
if (!IsEnabled) return;
onInnerPreDisable?.Invoke((T*)AddonPointer);
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(OnAddonEvent);
if (AddonPointer is not null) {
OnInnerDetach?.Invoke((T*)AddonPointer);
}
IsEnabled = false;
onInnerPostDisable?.Invoke((T*)AddonPointer);
});
}
public event AddonControllerEvent? OnPreEnable {
add => onInnerPreEnable += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
public event AddonControllerEvent? OnPostEnable {
add => onInnerPostEnable += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
public event AddonControllerEvent? OnPreDisable {
add => onInnerPreDisable += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
public event AddonControllerEvent? OnPostDisable {
add => onInnerPostDisable += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
private AddonControllerEvent? onInnerPreEnable;
private AddonControllerEvent? onInnerPostEnable;
private AddonControllerEvent? onInnerPreDisable;
private AddonControllerEvent? onInnerPostDisable;
}
@@ -0,0 +1,40 @@
using System;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace KamiToolKit.Controllers;
public abstract unsafe class AddonEventController<T> where T : unmanaged {
protected AddonEventController() {
if (typeof(T) == typeof(AddonNamePlate)) {
throw new NotSupportedException("Attaching to NamePlate is not supported. Use OverlayController.");
}
}
public delegate void AddonControllerEvent(T* addon);
public event AddonControllerEvent? OnAttach {
add => OnInnerAttach += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
public event AddonControllerEvent? OnDetach {
add => OnInnerDetach += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
public event AddonControllerEvent? OnRefresh {
add => OnInnerRefresh += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
public event AddonControllerEvent? OnUpdate {
add => OnInnerUpdate += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
protected AddonControllerEvent? OnInnerAttach;
protected AddonControllerEvent? OnInnerDetach;
protected AddonControllerEvent? OnInnerRefresh;
protected AddonControllerEvent? OnInnerUpdate;
}
@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Controllers;
/// <summary>
/// Addon controller for dynamically managing addons, typical use case is intended to
/// be for a single tasks, that can apply to one or many addons at once.
/// </summary>
public unsafe class DynamicAddonController : AddonEventController<AtkUnitBase>, IDisposable {
private readonly HashSet<string> trackedAddons = [];
private bool isEnabled;
public DynamicAddonController(params string[] addonNames) {
foreach (var addonName in addonNames) {
AddAddon(addonName);
}
}
public void AddAddon(string name) {
if (name is "NamePlate") {
Log.Error("Attaching to NamePlate is not supported. Use OverlayController instead.");
return;
}
trackedAddons.Add(name);
if (isEnabled) {
AddListeners(name);
}
}
public void RemoveAddon(string name) {
trackedAddons.Remove(name);
if (isEnabled) {
RemoveListeners(name);
}
}
private void OnAddonEvent(AddonEvent type, AddonArgs args) {
var addon = (AtkUnitBase*)args.Addon.Address;
switch (type) {
case AddonEvent.PostSetup:
OnInnerAttach?.Invoke(addon);
return;
case AddonEvent.PreFinalize:
OnInnerDetach?.Invoke(addon);
return;
case AddonEvent.PostRefresh or AddonEvent.PostRequestedUpdate:
OnInnerRefresh?.Invoke(addon);
return;
case AddonEvent.PostUpdate:
OnInnerUpdate?.Invoke(addon);
return;
}
}
public void Enable() {
foreach (var name in trackedAddons) {
AddListeners(name);
}
isEnabled = true;
}
public void Disable() {
isEnabled = false;
foreach (var name in trackedAddons) {
RemoveListeners(name);
}
}
private void AddListeners(string name) {
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, name, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, name, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostRefresh, name, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, name, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostUpdate, name, OnAddonEvent);
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(name);
if (addon is not null) {
OnInnerAttach?.Invoke(addon);
}
});
}
private void RemoveListeners(string name) {
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PostSetup, name, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PreFinalize, name, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PostRefresh, name, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, name, OnAddonEvent);
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, name, OnAddonEvent);
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(name);
if (addon is not null) {
OnInnerDetach?.Invoke(addon);
}
});
}
public void Dispose() {
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(OnAddonEvent);
Disable();
}
}
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Controllers;
/// <summary>
/// For use with addons that have multiple persistent variants, but where only one is used at a time.
/// For example, Inventories or CastBars.
/// Using this with other addons will duplicate their associated events incorrectly.
/// </summary>
public unsafe class MultiAddonController : AddonEventController<AtkUnitBase>, IDisposable {
private readonly List<AddonController> addonControllers = [];
public MultiAddonController(params string[] addonNames) {
foreach (var addonName in addonNames) {
if (addonName is "NamePlate") {
Log.Error("Attaching to NamePlate is not supported. Use OverlayController instead.");
continue;
}
// Don't allow duplicate addon controllers
if (addonControllers.Any(controller => controller.AddonName == addonName)) continue;
var newController = new AddonController(addonName);
addonControllers.Add(newController);
newController.OnAttach += ControllerOnAttach;
newController.OnDetach += ControllerOnDetach;
newController.OnRefresh += ControllerOnRefresh;
newController.OnUpdate += ControllerOnUpdate;
}
}
private void ControllerOnAttach(AtkUnitBase* addon)
=> OnInnerAttach?.Invoke(addon);
private void ControllerOnDetach(AtkUnitBase* addon)
=> OnInnerDetach?.Invoke(addon);
private void ControllerOnRefresh(AtkUnitBase* addon)
=> OnInnerRefresh?.Invoke(addon);
private void ControllerOnUpdate(AtkUnitBase* addon)
=> OnInnerUpdate?.Invoke(addon);
public void Dispose() {
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
addonControllers.ForEach(controller => controller.Dispose());
addonControllers.Clear();
});
}
public void Enable() {
addonControllers.ForEach(controller => controller.Enable());
}
public void Disable()
=> addonControllers.ForEach(controller => controller.Disable());
}
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Controllers;
/// <summary>
/// Only one or the other field will be valid, be sure to check for null.
/// </summary>
public unsafe class ListItemData {
public AtkComponentListItemPopulator.ListItemInfo* ItemInfo { get; set; }
public AtkComponentListItemRenderer* ItemRenderer { get; set; }
}
public unsafe class NativeListController(string addonName) : IDisposable {
public required ShouldModifyElementHandler ShouldModifyElement { get; init; }
public required UpdateElementHandler UpdateElement { get; init; }
public required ResetElementHandler ResetElement { get; init; }
public required GetPopulatorNodeHandler GetPopulatorNode { get; init; }
private Hook<AtkComponentListItemPopulator.PopulateDelegate>? onListPopulate;
private Hook<AtkComponentListItemPopulator.PopulateWithRendererDelegate>? onRendererPopulate;
public readonly List<uint> ModifiedIndexes = [];
public event Action? OnClose {
add => OnInnerClose += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
public event Action? OnOpen {
add => OnInnerOpen += value;
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
}
public void Enable() {
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, addonName, OnAddonSetup);
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, addonName, OnAddonFinalize);
var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(addonName);
if (addon is not null) {
Log.Warning("Caution: ListController was loaded after list was initialized, data may be stale.");
LoadPopulators(addon);
}
}
public void Disable() => Dispose();
public void Dispose() {
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(OnAddonSetup, OnAddonFinalize);
onListPopulate?.Dispose();
onListPopulate = null;
onRendererPopulate?.Dispose();
onRendererPopulate = null;
}
private void OnAddonSetup(AddonEvent type, AddonArgs args)
=> LoadPopulators((AtkUnitBase*)args.Addon.Address);
private void OnAddonFinalize(AddonEvent type, AddonArgs args) {
onListPopulate?.Dispose();
onListPopulate = null;
onRendererPopulate?.Dispose();
onRendererPopulate = null;
ModifiedIndexes.Clear();
OnInnerClose?.Invoke();
}
private void LoadPopulators(AtkUnitBase* addon) {
var populateMethod = GetPopulatorNode(addon)->Populator;
if (populateMethod.Populate is not null) {
onListPopulate = DalamudInterface.Instance.GameInteropProvider.HookFromAddress<AtkComponentListItemPopulator.PopulateDelegate>(populateMethod.Populate, OnPopulateDetour);
onListPopulate?.Enable();
}
if (populateMethod.PopulateWithRenderer is not null) {
onRendererPopulate = DalamudInterface.Instance.GameInteropProvider.HookFromAddress<AtkComponentListItemPopulator.PopulateWithRendererDelegate>(populateMethod.PopulateWithRenderer, OnRendererPopulateDetour);
onRendererPopulate?.Enable();
}
OnInnerOpen?.Invoke();
}
private void OnPopulateDetour(AtkUnitBase* unitBase, AtkComponentListItemPopulator.ListItemInfo* itemInfo, AtkResNode** nodeList) {
try {
var listItemData = new ListItemData {
ItemInfo = itemInfo,
};
var shouldModifyElement = ShouldModifyElement(unitBase, listItemData, nodeList);
if (!shouldModifyElement) {
if (ModifiedIndexes.Contains(itemInfo->ListItem->Renderer->OwnerNode->NodeId)) {
ResetElement.Invoke(unitBase, listItemData, nodeList);
ModifiedIndexes.Remove(itemInfo->ListItem->Renderer->OwnerNode->NodeId);
}
}
onListPopulate!.Original(unitBase, itemInfo, nodeList);
if (shouldModifyElement) {
UpdateElement.Invoke(unitBase, listItemData, nodeList);
ModifiedIndexes.Add(itemInfo->ListItem->Renderer->OwnerNode->NodeId);
}
}
catch (Exception e) {
Log.Exception(e);
}
}
private void OnRendererPopulateDetour(AtkUnitBase* unitBase, int listItemIndex, AtkResNode** nodeList, AtkComponentListItemRenderer* listItemRenderer) {
try {
var listItemData = new ListItemData {
ItemRenderer = listItemRenderer,
};
var shouldModifyElement = ShouldModifyElement(unitBase, listItemData, nodeList);
if (!shouldModifyElement) {
if (ModifiedIndexes.Contains(listItemRenderer->OwnerNode->NodeId)) {
ResetElement.Invoke(unitBase, listItemData, nodeList);
ModifiedIndexes.Remove(listItemRenderer->OwnerNode->NodeId);
}
}
onRendererPopulate!.Original(unitBase, listItemIndex, nodeList, listItemRenderer);
if (shouldModifyElement) {
UpdateElement.Invoke(unitBase, listItemData, nodeList);
ModifiedIndexes.Add(listItemRenderer->OwnerNode->NodeId);
}
}
catch (Exception e) {
Log.Exception(e);
}
}
public delegate bool ShouldModifyElementHandler(AtkUnitBase* unitBase, ListItemData listItemInfo, AtkResNode** nodeList);
public delegate AtkComponentListItemRenderer* GetPopulatorNodeHandler(AtkUnitBase* addon);
public delegate void UpdateElementHandler(AtkUnitBase* unitBase, ListItemData listItemInfo, AtkResNode** nodeList);
public delegate void ResetElementHandler(AtkUnitBase* unitBase, ListItemData listItemInfo, AtkResNode** nodeList);
private Action? OnInnerClose { get; set; }
private Action? OnInnerOpen { get; set; }
}