Initial commit: AetherBags + KamiToolKit for FC Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user