1 Commits

Author SHA1 Message Date
KnackAtNite 8db4ce6094 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>
2026-02-08 14:46:31 -05:00
243 changed files with 17883 additions and 71 deletions
+3 -4
View File
@@ -25,7 +25,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
{ {
InitializeBackgroundDropTarget(); InitializeBackgroundDropTarget();
ScrollableCategories = new InventoryScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
{ {
Position = ContentStartPosition, Position = ContentStartPosition,
Size = ContentSize, Size = ContentSize,
@@ -179,9 +179,6 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
protected override void OnFinalize(AtkUnitBase* addon) protected override void OnFinalize(AtkUnitBase* addon)
{ {
IsSetupComplete = false;
_lootedCategoryNode?.Dispose();
System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged; System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged;
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId; ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
@@ -192,6 +189,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
_lootedCategoryNode?.Dispose();
IsSetupComplete = false; IsSetupComplete = false;
base.OnFinalize(addon); base.OnFinalize(addon);
} }
+1 -1
View File
@@ -38,7 +38,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
WindowNode?.AddColor = _tintColor; WindowNode?.AddColor = _tintColor;
ScrollableCategories = new InventoryScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
{ {
Position = ContentStartPosition, Position = ContentStartPosition,
Size = ContentSize, Size = ContentSize,
+1 -1
View File
@@ -31,7 +31,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
WindowNode?.AddColor = _tintColor; WindowNode?.AddColor = _tintColor;
ScrollableCategories = new InventoryScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
{ {
Position = ContentStartPosition, Position = ContentStartPosition,
Size = ContentSize, Size = ContentSize,
+1 -1
View File
@@ -29,7 +29,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new(); protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
protected DragDropNode BackgroundDropTarget = null!; protected DragDropNode BackgroundDropTarget = null!;
protected InventoryScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = null!; protected ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = null!;
protected WrappingGridNode<InventoryCategoryNodeBase> CategoriesNode = null!; protected WrappingGridNode<InventoryCategoryNodeBase> CategoriesNode = null!;
protected TextInputWithButtonNode SearchInputNode = null!; protected TextInputWithButtonNode SearchInputNode = null!;
protected InventoryFooterNode FooterNode = null!; protected InventoryFooterNode FooterNode = null!;
+1 -1
View File
@@ -1,6 +1,6 @@
<Project Sdk="Dalamud.NET.Sdk/14.0.1"> <Project Sdk="Dalamud.NET.Sdk/14.0.1">
<PropertyGroup> <PropertyGroup>
<Version>1.0.1.0</Version> <Version>1.0.0.0</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@@ -120,32 +120,15 @@ public static unsafe class InventoryContextState
var inventoryManager = InventoryManager.Instance(); var inventoryManager = InventoryManager.Instance();
if (inventoryManager == null) return; if (inventoryManager == null) return;
ScanBlockedContainer(inventoryManager, InventoryType.BlockedItems); var blockedContainer = inventoryManager->GetInventoryContainer(InventoryType.BlockedItems);
ScanBlockedContainer(inventoryManager, InventoryType.MailEdit); if (blockedContainer == null) return;
}
private static void ScanBlockedContainer(InventoryManager* inventoryManager, InventoryType type) for (int i = 0; i < blockedContainer->Size; i++)
{
try
{ {
var container = inventoryManager->GetInventoryContainer(type); ref var item = ref blockedContainer->Items[i];
if (container == null) return; if (item.ItemId == 0) continue;
if (container->GetSize() == 0) return;
if (!container->IsLoaded) return;
if (container->Items == null) return;
for (int i = 0; i < container->GetSize(); i++) BlockedSlots.Add((item.Container, item.Slot));
{
var slot = container->GetInventorySlot(i);
if (slot == null) continue;
if (slot->GetItemId() == 0) continue;
BlockedSlots.Add((slot->Container, slot->Slot));
}
}
catch
{
// Container became invalid during teardown
} }
} }
+14 -9
View File
@@ -28,7 +28,6 @@ public static unsafe class DragDropState
public class InventoryMonitor : IDisposable public class InventoryMonitor : IDisposable
{ {
public InventoryMonitor() public InventoryMonitor()
{ {
var bags = new[] { "Inventory", "InventoryLarge", "InventoryExpansion" }; var bags = new[] { "Inventory", "InventoryLarge", "InventoryExpansion" };
@@ -42,6 +41,8 @@ public class InventoryMonitor : IDisposable
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize); Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize);
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, bags, OnInventoryPreFinalize); Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, bags, OnInventoryPreFinalize);
Services.AddonLifecycle.RegisterListener(AddonEvent.PreHide, bags, OnInventoryPreHide);
// PreRefresh Handlers // PreRefresh Handlers
Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, bags, InventoryPreRefreshHandler); Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, bags, InventoryPreRefreshHandler);
@@ -71,6 +72,14 @@ public class InventoryMonitor : IDisposable
System.AddonInventoryWindow.Close(); System.AddonInventoryWindow.Close();
} }
private void OnInventoryPreHide(AddonEvent type, AddonArgs args)
{
if (System.Config.General.OpenWithGameInventory)
{
System.AddonInventoryWindow.Close();
}
}
private unsafe void OpenInventories(string name) private unsafe void OpenInventories(string name)
{ {
GeneralSettings config = System.Config.General; GeneralSettings config = System.Config.General;
@@ -192,14 +201,10 @@ public class InventoryMonitor : IDisposable
if (config.OpenWithGameInventory) if (config.OpenWithGameInventory)
{ {
AtkValue* value1 = (AtkValue*)atkValues[1].Address; var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(args.AddonName);
int openTitleId = value1->Int; bool isCurrentlyVisible = addon != null && addon->IsVisible;
if (openTitleId == 0) if (!isCurrentlyVisible)
{
System.AddonInventoryWindow.Toggle();
}
else
{ {
System.AddonInventoryWindow.Open(); System.AddonInventoryWindow.Open();
} }
@@ -245,6 +250,6 @@ public class InventoryMonitor : IDisposable
public void Dispose() public void Dispose()
{ {
Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw; Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw;
Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate, OnInventoryPreFinalize, InventoryPreRefreshHandler); Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate, OnInventoryPreFinalize, OnInventoryPreHide, InventoryPreRefreshHandler);
} }
} }
Submodule KamiToolKit deleted from 028de4a300
+108
View File
@@ -0,0 +1,108 @@
root = true
# top-most EditorConfig file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
# 4 space indentation
indent_style = space
indent_size = 4
# Microsoft .NET properties
csharp_indent_braces = false
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_open_brace = none
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_code_quality_unused_parameters = non_public
dotnet_naming_rule.event_rule.severity = warning
dotnet_naming_rule.event_rule.style = on_upper_camel_case_style
dotnet_naming_rule.event_rule.symbols = event_symbols
dotnet_naming_rule.private_constants_rule.severity = warning
dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
dotnet_naming_rule.private_instance_fields_rule.severity = warning
dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
dotnet_naming_rule.private_static_fields_rule.severity = warning
dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
dotnet_naming_rule.private_static_readonly_rule.severity = warning
dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
dotnet_naming_style.on_upper_camel_case_style.capitalization = pascal_case
dotnet_naming_style.on_upper_camel_case_style.required_prefix = On
dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
dotnet_naming_symbols.event_symbols.applicable_kinds = event
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
dotnet_style_parentheses_in_other_operators = always_for_clarity:silent
dotnet_style_object_initializer = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_empty_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_before_open_square_brackets = false
csharp_space_before_comma = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_comma = true
csharp_space_after_cast = false
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = none
csharp_space_between_square_brackets = false
# ReSharper properties
resharper_align_linq_query = true
resharper_align_multiline_argument = true
resharper_csharp_align_multiline_argument = false
resharper_csharp_align_multiline_calls_chain = false
resharper_align_multiline_expression = true
resharper_align_multiline_extends_list = true
resharper_align_multiline_for_stmt = true
resharper_align_multline_type_parameter_constrains = true
resharper_align_multline_type_parameter_list = true
resharper_braces_for_ifelse = required_for_multiline
resharper_can_use_global_alias = false
resharper_csharp_align_multiline_parameter = false
resharper_csharp_align_multiple_declaration = true
resharper_csharp_allow_comment_after_lbrace = true
resharper_csharp_empty_block_style = together
resharper_csharp_int_align_comments = true
resharper_csharp_new_line_before_while = true
resharper_csharp_stick_comment = false
resharper_csharp_wrap_after_declaration_lpar = true
resharper_indent_preprocessor_region = no_indent
resharper_new_line_before_finally = false
resharper_place_accessorholder_attribute_on_same_line = false
resharper_place_field_attribute_on_same_line = false
# ReSharper inspection severities
csharp_style_deconstructed_variable_declaration = true:silent
+3
View File
@@ -0,0 +1,3 @@
/obj/
/bin/
/.idea/
Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

+8
View File
@@ -0,0 +1,8 @@
using System.Numerics;
namespace KamiToolKit.Classes;
internal class AddonConfig {
public Vector2 Position = Vector2.Zero;
public float Scale = 1.0f;
}
+8
View File
@@ -0,0 +1,8 @@
using System;
using KamiToolKit.Premade.Color;
namespace KamiToolKit.Classes;
internal readonly struct BatchToken(ColorPickerWidget owner) : IDisposable {
public void Dispose() => owner.EndBatchUpdate();
}
+23
View File
@@ -0,0 +1,23 @@
using System.Numerics;
namespace KamiToolKit.Classes;
public class Bounds {
public required Vector2 TopLeft { get; set; }
public required Vector2 BottomRight { get; set; }
public float Top => TopLeft.Y;
public float Left => TopLeft.X;
public float Bottom => BottomRight.Y;
public float Right => BottomRight.X;
public float Width => BottomRight.X - TopLeft.X;
public float Height => BottomRight.Y - TopLeft.Y;
public Vector2 Size => new(Width, Height);
public float CenterX => (TopLeft.X + BottomRight.X) / 2.0f;
public float CenterY => (TopLeft.Y + BottomRight.Y) / 2.0f;
public Vector2 Center => new(CenterX, CenterY);
public override string ToString() => $"{TopLeft}, {BottomRight}";
}
+18
View File
@@ -0,0 +1,18 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Classes;
public static unsafe class ColorHelper {
public static Vector4 GetColor(uint colorId)
=> ConvertToVector4(AtkStage.Instance()->AtkUIColorHolder->GetColor(true, colorId));
private static Vector4 ConvertToVector4(uint color) {
var a = (byte)(color >> 24);
var b = (byte)(color >> 16);
var g = (byte)(color >> 8);
var r = (byte)color;
return new Vector4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);
}
}
@@ -0,0 +1,44 @@
using System;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Component.GUI;
using static FFXIVClientStructs.FFXIV.Component.GUI.AtkModuleInterface;
namespace KamiToolKit.Classes;
public unsafe class CustomEventInterface : IDisposable {
private readonly AtkEventInterface* eventInterface;
private AtkEventInterface.Delegates.ReceiveEvent? receiveEventDelegate;
private AtkEventInterface.Delegates.ReceiveEventWithResult? receiveEventWithResultDelegate;
public CustomEventInterface(AtkEventInterface.Delegates.ReceiveEvent eventHandler, AtkEventInterface.Delegates.ReceiveEventWithResult? receiveEventWithResult = null) {
receiveEventDelegate = eventHandler;
receiveEventWithResultDelegate = receiveEventWithResult;
eventInterface = NativeMemoryHelper.UiAlloc<AtkEventInterface>();
eventInterface->VirtualTable = (AtkEventInterface.AtkEventInterfaceVirtualTable*)NativeMemoryHelper.Malloc((ulong)sizeof(void*) * 2);
eventInterface->VirtualTable->ReceiveEvent = (delegate* unmanaged<AtkEventInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(receiveEventDelegate);
if (receiveEventWithResultDelegate is not null) {
eventInterface->VirtualTable->ReceiveEventWithResult = (delegate* unmanaged<AtkEventInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(receiveEventWithResultDelegate);
}
else {
eventInterface->VirtualTable->ReceiveEventWithResult = (delegate* unmanaged<AtkEventInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)(delegate* unmanaged<void>)&NullSub;
}
}
public void Dispose() {
if (eventInterface is null) return;
NativeMemoryHelper.Free(eventInterface->VirtualTable, (ulong)sizeof(void*) * 2);
NativeMemoryHelper.UiFree(eventInterface);
receiveEventDelegate = null;
receiveEventWithResultDelegate = null;
}
[UnmanagedCallersOnly] private static void NullSub() { }
public static implicit operator AtkEventInterface*(CustomEventInterface listener) => listener.eventInterface;
}
@@ -0,0 +1,35 @@
using System;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Classes;
public unsafe class CustomEventListener : IDisposable {
private readonly AtkEventListener* eventListener;
private AtkEventListener.Delegates.ReceiveEvent? receiveEventDelegate;
public CustomEventListener(AtkEventListener.Delegates.ReceiveEvent eventHandler) {
receiveEventDelegate = eventHandler;
eventListener = NativeMemoryHelper.UiAlloc<AtkEventListener>();
eventListener->VirtualTable = (AtkEventListener.AtkEventListenerVirtualTable*)NativeMemoryHelper.Malloc((ulong)sizeof(void*) * 3);
eventListener->VirtualTable->Dtor = (delegate* unmanaged<AtkEventListener*, byte, AtkEventListener*>)(delegate* unmanaged<void>)&NullSub;
eventListener->VirtualTable->ReceiveGlobalEvent = (delegate* unmanaged<AtkEventListener*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)(delegate* unmanaged<void>)&NullSub;
eventListener->VirtualTable->ReceiveEvent = (delegate* unmanaged<AtkEventListener*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(receiveEventDelegate);
}
public virtual void Dispose() {
if (eventListener is null) return;
NativeMemoryHelper.Free(eventListener->VirtualTable, (ulong)sizeof(void*) * 3);
NativeMemoryHelper.UiFree(eventListener);
receiveEventDelegate = null;
}
[UnmanagedCallersOnly] private static void NullSub() { }
public static implicit operator AtkEventListener*(CustomEventListener listener) => listener.eventListener;
}
+76
View File
@@ -0,0 +1,76 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.IoC;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
namespace KamiToolKit.Classes;
internal class DalamudInterface {
private static DalamudInterface? instance;
public static DalamudInterface Instance => instance ??= new DalamudInterface();
[PluginService] public IPluginLog Log { get; set; } = null!;
[PluginService] public IAddonLifecycle AddonLifecycle { get; set; } = null!;
[PluginService] public IDataManager DataManager { get; set; } = null!;
[PluginService] public ITextureProvider TextureProvider { get; set; } = null!;
[PluginService] public IFramework Framework { get; set; } = null!;
[PluginService] public IAddonEventManager AddonEventManager { get; set; } = null!;
[PluginService] public IDalamudPluginInterface PluginInterface { get; set; } = null!;
[PluginService] public IGameGui GameGui { get; set; } = null!;
[PluginService] public IGameInteropProvider GameInteropProvider { get; set; } = null!;
[PluginService] public ISeStringEvaluator SeStringEvaluator { get; set; } = null!;
private DalamudInterface() {
if (!KamiToolKitLibrary.IsInitialized)
throw new Exception("KamiToolKit not initialized! You must call KamiToolKitLibrary.Initialize() before using KamiToolKit.\n" +
"Don't forget to call KamiToolKitLibrary.Dispose() in your plugins dispose to ensure all assets are freed and to trigger bad practice warnings.");
}
public string GetAssetDirectoryPath()
=> Path.Combine(PluginInterface.AssemblyLocation.DirectoryName ?? throw new Exception("Directory from Dalamud is Invalid Somehow"), "Assets");
public string GetAssetPath(string assetName)
=> Path.Combine(GetAssetDirectoryPath(), assetName);
public IDalamudTextureWrap? LoadAsset(string assetName)
=> TextureProvider.GetFromFile(GetAssetPath(assetName)).GetWrapOrDefault();
}
internal static class Log {
private static readonly bool ExcessiveLogging = false;
internal static void Debug(string message) {
DalamudInterface.Instance.Log.Debug($"[KamiToolKit] {message}");
}
internal static void Fatal(string message) {
DalamudInterface.Instance.Log.Fatal($"[KamiToolKit] {message}");
}
internal static void Warning(string message) {
DalamudInterface.Instance.Log.Warning($"[KamiToolKit] {message}");
}
internal static void Verbose(string message) {
DalamudInterface.Instance.Log.Verbose($"[KamiToolKit] {message}");
}
internal static void Excessive(string message) {
if (ExcessiveLogging) {
Verbose($"[KamiToolKit] {message}");
}
}
internal static void Error(string message) {
DalamudInterface.Instance.Log.Error($"[KamiToolKit] {message}");
}
internal static void Exception(Exception exception, [CallerMemberName] string? callerName = null) {
DalamudInterface.Instance.Log.Error(exception, $"Exception in {callerName}");
}
}
+69
View File
@@ -0,0 +1,69 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Classes;
public unsafe class DragDropPayload {
public DragDropType Type { get; set; } = DragDropType.Nothing;
public short ReferenceIndex { get; set; }
/// <remarks> Index (like AtkDragDropInterface.ReferenceIndex), InventoryType, etc. </remarks>
public int Int1 { get; set; }
/// <remarks> ActionId, ItemId, EmoteId, InventorySlotIndex, ListIndex, MacroIndex etc. </remarks>
public int Int2 { get; set; } = -1;
// unknown usage
// public ulong Unk8 { get; set; }
// unknown usage
// public AtkValue* AtkValue { get; set; }
public ReadOnlySeString Text { get; set; }
// unknown usage
// public uint Flags { get; set; }
public static DragDropPayload FromDragDropInterface(AtkDragDropInterface* dragDropInterface) {
var payloadContainer = dragDropInterface->GetPayloadContainer();
return new DragDropPayload {
Type = dragDropInterface->DragDropType,
ReferenceIndex = dragDropInterface->DragDropReferenceIndex,
Int1 = payloadContainer->Int1,
Int2 = payloadContainer->Int2,
Text = new ReadOnlySeString(payloadContainer->Text),
};
}
public void ToDragDropInterface(AtkDragDropInterface* dragDropInterface, bool writeToPayloadContainer = true) {
dragDropInterface->DragDropType = Type;
dragDropInterface->DragDropReferenceIndex = ReferenceIndex;
if (writeToPayloadContainer) {
var payloadContainer = dragDropInterface->GetPayloadContainer();
payloadContainer->Clear();
payloadContainer->Int1 = Int1;
payloadContainer->Int2 = Int2;
if (Text.IsEmpty) {
payloadContainer->Text.Clear();
}
else {
var stringBuilder = new SeStringBuilder().Append(Text);
payloadContainer->Text.SetString(stringBuilder.GetViewAsSpan());
}
}
}
public void Clear() {
Type = DragDropType.Nothing;
ReferenceIndex = 0;
Int1 = 0;
Int2 = -1;
Text = default;
}
}
+39
View File
@@ -0,0 +1,39 @@
using System.Diagnostics;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace KamiToolKit.Classes;
/// WARNING: These features are potentially extremely volatile, use at your own risk.
public unsafe class Experimental {
private static Experimental? instance;
public static Experimental Instance => instance ??= new Experimental();
public void EnableHooks() { }
public void DisposeHooks() {
}
// WARNING: May result in undefined state or accidental network requests
// Use at your own risk.
[Conditional("DEBUG")]
public static void ForceOpenAddon(AgentId agentId, int delayTicks = 0) {
if (delayTicks is not 0) {
DalamudInterface.Instance.Framework.RunOnTick(() => {
AgentModule.Instance()->GetAgentByInternalId(agentId)->Show();
}, delayTicks: delayTicks);
}
else {
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
AgentModule.Instance()->GetAgentByInternalId(agentId)->Show();
});
}
}
// WARNING: May result in undefined state or accidental network requests
// Use at your own risk.
[Conditional("DEBUG")]
public static void ForceCloseAddon(AgentId agentId)
=> DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
AgentModule.Instance()->GetAgentByInternalId(agentId)->Hide();
});
}
+23
View File
@@ -0,0 +1,23 @@
using System.Numerics;
namespace KamiToolKit.Classes;
public static class FlagHelper {
public static bool ReadFlag<T>(ref T flagsField, int flag) where T : struct, IBinaryInteger<T>
=> (flagsField & T.One << BitOperations.Log2((uint)flag)) != T.Zero;
public static void SetFlag<T>(ref T flagsField, int flag) where T : struct, IBinaryInteger<T>
=> flagsField |= T.One << BitOperations.Log2((uint)flag);
public static void ClearFlag<T>(ref T flagsField, int flag) where T : struct, IBinaryInteger<T>
=> flagsField &= ~(T.One << BitOperations.Log2((uint)flag));
public static void UpdateFlag<T>(ref T flagsField, int flag, bool enable) where T : struct, IBinaryInteger<T> {
if (enable) {
SetFlag(ref flagsField, flag);
}
else {
ClearFlag(ref flagsField, flag);
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace KamiToolKit.Classes;
internal static class GenericUtil {
public static bool AreEqual<T>(T? left, T? right) {
if (default(T) == null) return ReferenceEquals(left, right);
if (left == null || right == null) return left == null && right == null;
var leftSpan = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref left), Unsafe.SizeOf<T>());
var rightSpan = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref right), Unsafe.SizeOf<T>());
return leftSpan.SequenceEqual(rightSpan);
}
}
+11
View File
@@ -0,0 +1,11 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using ListItemInfo = FFXIVClientStructs.FFXIV.Component.GUI.AtkComponentListItemPopulator.ListItemInfo;
namespace KamiToolKit.Classes;
public unsafe class ListPopulatorData {
public AtkUnitBase* Addon { get; init; }
public ListItemInfo* ItemInfo { get; init; }
public AtkResNode** NodeList { get; init; }
public uint Index { get; init; }
}
+75
View File
@@ -0,0 +1,75 @@
using System;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
namespace KamiToolKit.Classes;
internal static class NativeMemoryHelper {
public static unsafe T* UiAlloc<T>(int elementCount, ulong alignment = 8) where T : unmanaged
=> UiAlloc<T>((uint)elementCount, alignment);
public static unsafe T* UiAlloc<T>(uint elementCount = 1, ulong alignment = 8) where T : unmanaged {
var allocSize = (ulong)sizeof(T) * elementCount;
var memory = (T*)IMemorySpace.GetUISpace()->Malloc(allocSize, alignment);
IMemorySpace.Memset(memory, 0, allocSize);
if (memory is null) {
throw new Exception($"Unable to allocate memory for {typeof(T)}");
}
return memory;
}
public static unsafe void UiFree<T>(T* memory) where T : unmanaged
=> IMemorySpace.Free(memory);
public static unsafe void UiFree<T>(T* memory, uint elementCount) where T : unmanaged
=> IMemorySpace.Free(memory, (ulong)sizeof(T) * elementCount);
public static unsafe T* Create<T>() where T : unmanaged, ICreatable {
var memory = IMemorySpace.GetUISpace()->Create<T>();
if (memory is null) {
throw new Exception($"Unable to allocate memory for {typeof(T)}");
}
return memory;
}
public static unsafe nint Malloc(ulong size, ulong alignment = 8)
=> (nint)IMemorySpace.GetUISpace()->Malloc(size, alignment);
public static unsafe void Free(void* memory, ulong size)
=> IMemorySpace.Free(memory, size);
public static unsafe void ResizeArray<T>(ref T* array, int oldSize, uint newSize) where T : unmanaged
=> ResizeArray(ref array, oldSize, (int)newSize);
public static unsafe void ResizeArray<T>(ref T* array, uint oldSize, uint newSize) where T : unmanaged
=> ResizeArray(ref array, (int)oldSize, (int)newSize);
public static unsafe void ResizeArray<T>(ref T* array, uint oldSize, int newSize) where T : unmanaged
=> ResizeArray(ref array, (int)oldSize, newSize);
public static unsafe void ResizeArray<T>(ref T* array, int oldSize, int newSize) where T : unmanaged {
var newBuffer = UiAlloc<T>((uint)newSize);
Copy(array, newBuffer, oldSize);
if (array is not null) {
UiFree(array, (uint)oldSize);
}
array = newBuffer;
}
public static unsafe void Copy<T>(T* oldBuffer, T* newBuffer, int count) where T : unmanaged
=> Copy(oldBuffer, newBuffer, (uint)count);
public static unsafe void Copy<T>(T* oldBuffer, T* newBuffer, uint count) where T : unmanaged
=> NativeMemory.Copy(oldBuffer, newBuffer, (nuint)(sizeof(T) * count));
public static unsafe void MemCopy<T>(T* oldBuffer, T* newBuffer, uint byteCount) where T : unmanaged
=> NativeMemory.Copy(oldBuffer, newBuffer, byteCount);
}
+199
View File
@@ -0,0 +1,199 @@
using System;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Classes;
public enum NodePosition {
BeforeTarget,
AfterTarget,
BeforeAllSiblings,
AfterAllSiblings,
AsLastChild,
AsFirstChild,
}
internal static unsafe class NodeLinker {
internal static void AttachNode(AtkResNode* node, AtkResNode* attachTargetNode, NodePosition position) {
switch (position) {
case NodePosition.BeforeTarget:
EmplaceBefore(node, attachTargetNode);
break;
case NodePosition.AfterTarget:
EmplaceAfter(node, attachTargetNode);
break;
case NodePosition.BeforeAllSiblings:
EmplaceBeforeSiblings(node, attachTargetNode);
break;
case NodePosition.AfterAllSiblings:
EmplaceAfterSiblings(node, attachTargetNode);
break;
case NodePosition.AsLastChild:
EmplaceAsLastChild(node, attachTargetNode);
break;
case NodePosition.AsFirstChild:
EmplaceAsFirstChild(node, attachTargetNode);
break;
default:
throw new ArgumentOutOfRangeException(nameof(position), position, null);
}
}
private static void EmplaceBefore(AtkResNode* node, AtkResNode* attachTargetNode) {
node->ParentNode = attachTargetNode->ParentNode;
// Target node is the head of the nodelist, we will be the new head.
if (attachTargetNode->NextSiblingNode is null) {
attachTargetNode->ParentNode->ChildNode = node;
}
// We have a node that will be before us
if (attachTargetNode->NextSiblingNode is not null) {
attachTargetNode->NextSiblingNode->PrevSiblingNode = node;
node->NextSiblingNode = attachTargetNode->NextSiblingNode;
}
attachTargetNode->NextSiblingNode = node;
node->PrevSiblingNode = attachTargetNode;
if (attachTargetNode->ParentNode->GetNodeType() is not NodeType.Component) {
attachTargetNode->ParentNode->ChildCount++;
}
}
private static void EmplaceAfter(AtkResNode* node, AtkResNode* attachTargetNode) {
node->ParentNode = attachTargetNode->ParentNode;
// We have a node that will be after us
if (attachTargetNode->PrevSiblingNode is not null) {
attachTargetNode->PrevSiblingNode->NextSiblingNode = node;
node->PrevSiblingNode = attachTargetNode->PrevSiblingNode;
}
attachTargetNode->PrevSiblingNode = node;
node->NextSiblingNode = attachTargetNode;
if (attachTargetNode->ParentNode->GetNodeType() is not NodeType.Component) {
attachTargetNode->ParentNode->ChildCount++;
}
}
private static void EmplaceBeforeSiblings(AtkResNode* node, AtkResNode* attachTargetNode) {
var current = attachTargetNode;
var previous = current;
while (current is not null) {
previous = current;
current = current->NextSiblingNode;
}
if (previous is not null) {
EmplaceBefore(node, previous);
}
if (attachTargetNode->ParentNode->GetNodeType() is not NodeType.Component) {
attachTargetNode->ParentNode->ChildCount++;
}
}
private static void EmplaceAfterSiblings(AtkResNode* node, AtkResNode* attachTargetNode) {
var current = attachTargetNode;
var previous = current;
while (current is not null) {
previous = current;
current = current->PrevSiblingNode;
}
if (previous is not null) {
EmplaceAfter(node, previous);
}
if (attachTargetNode->ParentNode->GetNodeType() is not NodeType.Component) {
attachTargetNode->ParentNode->ChildCount++;
}
}
private static void EmplaceAsLastChild(AtkResNode* node, AtkResNode* attachTargetNode) {
// If the child list is empty
if (attachTargetNode->ChildNode is null && attachTargetNode->GetNodeType() is not NodeType.Component) {
if (attachTargetNode->GetNodeType() is not NodeType.Component) {
attachTargetNode->ChildNode = node;
node->ParentNode = attachTargetNode;
attachTargetNode->ChildCount++;
}
else {
node->ParentNode = attachTargetNode;
}
}
// Else Add to the List
else {
var currentNode = attachTargetNode->ChildNode;
while (currentNode is not null && currentNode->PrevSiblingNode != null) {
currentNode = currentNode->PrevSiblingNode;
}
node->ParentNode = attachTargetNode;
node->NextSiblingNode = currentNode;
if (currentNode is not null) {
currentNode->PrevSiblingNode = node;
}
if (attachTargetNode->GetNodeType() is not NodeType.Component) {
attachTargetNode->ChildCount++;
}
}
}
private static void EmplaceAsFirstChild(AtkResNode* node, AtkResNode* attachTargetNode) {
// If the child list is empty
if (attachTargetNode->ChildNode is null && attachTargetNode->ChildCount is 0) {
if (attachTargetNode->GetNodeType() is not NodeType.Component) {
attachTargetNode->ChildNode = node;
node->ParentNode = attachTargetNode;
attachTargetNode->ChildCount++;
}
else {
node->ParentNode = attachTargetNode;
}
}
// Else Add to the List as the First Child
else {
if (attachTargetNode->GetNodeType() is not NodeType.Component) {
attachTargetNode->ChildNode->NextSiblingNode = node;
node->PrevSiblingNode = attachTargetNode->ChildNode;
attachTargetNode->ChildNode = node;
node->ParentNode = attachTargetNode;
attachTargetNode->ChildCount++;
}
else {
node->PrevSiblingNode = attachTargetNode->ChildNode;
node->ParentNode = attachTargetNode;
}
}
}
public static void DetachNode(AtkResNode* node) {
if (node is null) return;
if (node->ParentNode is null) return;
if (node->ParentNode->ChildNode == node)
node->ParentNode->ChildNode = node->PrevSiblingNode;
if (node->PrevSiblingNode != null)
node->PrevSiblingNode->NextSiblingNode = node->NextSiblingNode;
if (node->NextSiblingNode != null)
node->NextSiblingNode->PrevSiblingNode = node->PrevSiblingNode;
if (node->ParentNode->GetNodeType() is not NodeType.Component) {
node->ParentNode->ChildCount--;
}
}
}
+34
View File
@@ -0,0 +1,34 @@
using System.Numerics;
namespace KamiToolKit.Classes;
public class Part {
public float Width { get; set; }
public float Height { get; set; }
public Vector2 Size {
get => new(Width, Height);
set {
Width = value.X;
Height = value.Y;
}
}
public float U { get; set; }
public float V { get; set; }
public Vector2 TextureCoordinates {
get => new(U, V);
set {
U = value.X;
V = value.Y;
}
}
public uint Id { get; set; }
public string TexturePath { get; set; } = string.Empty;
}
+83
View File
@@ -0,0 +1,83 @@
using System;
using System.Linq;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Classes;
/// <summary>
/// Wrapper around a AtkUldPartsList, manages adding multiple parts more easily.
/// </summary>
public unsafe class PartsList : IDisposable {
internal AtkUldPartsList* InternalPartsList;
private bool isDisposed;
public PartsList() {
InternalPartsList = NativeMemoryHelper.UiAlloc<AtkUldPartsList>();
InternalPartsList->Parts = null;
InternalPartsList->PartCount = 0;
InternalPartsList->Id = 0;
}
public void Dispose() {
if (!isDisposed) {
foreach (var partIndex in Enumerable.Range(0, (int)PartCount)) {
ref var part = ref InternalPartsList->Parts[partIndex];
if (part.UldAsset is not null && part.UldAsset->AtkTexture.IsTextureReady()) {
part.UldAsset->AtkTexture.ReleaseTexture();
part.UldAsset->AtkTexture.KernelTexture = null;
part.UldAsset->AtkTexture.TextureType = 0;
}
NativeMemoryHelper.UiFree(part.UldAsset);
part.UldAsset = null;
}
NativeMemoryHelper.UiFree(InternalPartsList);
InternalPartsList = null;
}
isDisposed = true;
}
private uint PartCount {
get => InternalPartsList->PartCount;
set => InternalPartsList->PartCount = value;
}
public void Add(params Part[] items) {
foreach (var part in items) {
Add(part);
}
}
public AtkUldPart* Add(Part item) {
NativeMemoryHelper.ResizeArray(ref InternalPartsList->Parts, PartCount, PartCount + 1);
ref var newPart = ref InternalPartsList->Parts[PartCount];
newPart.Width = (ushort) item.Width;
newPart.Height = (ushort) item.Height;
newPart.U = (ushort) item.U;
newPart.V = (ushort) item.V;
newPart.UldAsset = NativeMemoryHelper.UiAlloc<AtkUldAsset>();
newPart.UldAsset->Id = item.Id;
newPart.UldAsset->AtkTexture.Ctor();
newPart.LoadTexture(item.TexturePath);
return &InternalPartsList->Parts[PartCount++];
}
public AtkUldPart* this[int index] {
get {
if (InternalPartsList is null) return null;
if (PartCount <= index) return null;
return &InternalPartsList->Parts[index];
}
}
}
+3
View File
@@ -0,0 +1,3 @@
namespace KamiToolKit.Classes;
public record TabbedNodeEntry<T>(T Node, int Tab) where T : NodeBase;
@@ -0,0 +1,26 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Classes;
public unsafe class ViewportEventListener(AtkEventListener.Delegates.ReceiveEvent eventHandler) : CustomEventListener(eventHandler) {
public void AddEvent(AtkEventType eventType, AtkResNode* node) {
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
Log.Verbose($"Registering ViewportEvent: {eventType}");
AtkStage.Instance()->ViewportEventManager.RegisterEvent(eventType, 0, node, &node->AtkEventTarget, this, false);
});
}
public void RemoveEvent(AtkEventType eventType) {
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
Log.Verbose($"Unregistering ViewportEvent: {eventType}");
AtkStage.Instance()->ViewportEventManager.UnregisterEvent(eventType, 0, this, false);
});
}
public override void Dispose() {
Log.Verbose("Disposing ViewportEventListener");
RemoveEvent(AtkEventType.UnregisterAll);
base.Dispose();
}
}
+147
View File
@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.ContextMenu;
public unsafe class ContextMenu : IDisposable {
private readonly CustomEventInterface contextMenuEventInterface;
private Dictionary<long, ContextMenuItem>? mainMenuEntries;
private Dictionary<long, ContextMenuSubItem>? mainMenuSubMenus;
private Dictionary<long, ContextMenuItem>? subMenuEntries;
// Prevent the return entry from colliding with submenu items
private const int SubMenuIndexOffset = 1000;
private List<ContextMenuItem> Items { get; set; } = [];
private IOrderedEnumerable<ContextMenuItem> OrderedItems => Items.OrderBy(item => item.DisplayPriority);
public ContextMenu() {
contextMenuEventInterface = new CustomEventInterface(ContextMenuEventHandler);
}
public void Dispose() {
contextMenuEventInterface.Dispose();
}
private AtkValue* ContextMenuEventHandler(AtkModuleInterface.AtkEventInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) {
var handlerParam = (long)eventKind;
if (handlerParam >= SubMenuIndexOffset) {
if (subMenuEntries?.TryGetValue(handlerParam, out var subItem) ?? false) {
subItem.OnClick();
ClearAll();
}
return returnValue;
}
if (mainMenuSubMenus?.TryGetValue(handlerParam, out var subMenuItem) ?? false) {
OpenSubMenu(subMenuItem);
return returnValue;
}
if (mainMenuEntries?.TryGetValue(handlerParam, out var item) ?? false) {
item.OnClick();
ClearAll();
return returnValue;
}
subMenuEntries?.Clear();
subMenuEntries = null;
return returnValue;
}
private void ClearAll() {
mainMenuEntries?.Clear();
mainMenuEntries = null;
mainMenuSubMenus?.Clear();
mainMenuSubMenus = null;
subMenuEntries?.Clear();
subMenuEntries = null;
}
public void AddItem(ReadOnlySeString name, Action callback) {
AddItem(new ContextMenuItem {
Name = name,
OnClick = callback,
});
}
public void RemoveItem(ReadOnlySeString name) {
var targetItem = Items.FirstOrDefault(item => item.Name == name);
if (targetItem is null) return;
Items.Remove(targetItem);
}
public void AddItem(ContextMenuItem item, params ContextMenuItem[] items) {
foreach (var entry in items.Prepend(item)) {
Items.Add(entry);
}
}
public void RemoveItem(ContextMenuItem item, params ContextMenuItem[] items) {
foreach (var entry in items.Prepend(item)) {
Items.Remove(entry);
}
}
public void Clear() => Items.Clear();
public void Open() {
var agentContextMenu = AgentContext.Instance();
agentContextMenu->ClearMenu();
mainMenuEntries = [];
mainMenuSubMenus = [];
subMenuEntries = null;
foreach (var (index, item) in OrderedItems.Index()) {
if (item is ContextMenuSubItem subItem) {
mainMenuSubMenus.Add(index, subItem);
agentContextMenu->AddMenuItem(item.Name, contextMenuEventInterface, index, !item.IsEnabled, submenu: true);
} else {
mainMenuEntries.Add(index, item);
agentContextMenu->AddMenuItem(item.Name, contextMenuEventInterface, index, !item.IsEnabled, submenu: false);
}
}
agentContextMenu->OpenContextMenu();
}
private void OpenSubMenu(ContextMenuSubItem subItem) {
var agentContextMenu = AgentContext.Instance();
// Set the state again to prevent the menu closing when going back and forth between the submenus
agentContextMenu->SubContextMenu.SelectedContextItemIndex = 0;
agentContextMenu->SubContextMenu.CurrentEventIndex = 8;
agentContextMenu->OpenSubMenu();
var indexer = 0;
subMenuEntries = [];
foreach (var item in subItem.SubItems.OrderBy(i => i.DisplayPriority)) {
if (item is ContextMenuSubItem) continue;
var paramIndex = SubMenuIndexOffset + indexer;
subMenuEntries.Add(paramIndex, item);
agentContextMenu->AddMenuItem(item.Name, contextMenuEventInterface, paramIndex, !item.IsEnabled, submenu: false);
indexer++;
}
}
public void Close() {
var agentContextMenu = AgentContext.Instance();
agentContextMenu->ClearMenu();
ClearAll();
}
}
@@ -0,0 +1,11 @@
using System;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.ContextMenu;
public class ContextMenuItem {
public required ReadOnlySeString Name { get; init; }
public bool IsEnabled { get; init; } = true;
public required Action OnClick { get; init; }
public int DisplayPriority { get; set; }
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.ContextMenu;
/// <summary>
/// One level of submenu only. Nested submenus not supported.
/// </summary>
public class ContextMenuSubItem : ContextMenuItem {
public List<ContextMenuItem> SubItems { get; set; } = [];
public void AddItem(ReadOnlySeString name, Action callback) {
SubItems.Add(new ContextMenuItem {
Name = name,
OnClick = callback,
});
}
public void AddItem(ContextMenuItem item) => SubItems.Add(item);
}
+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; }
}
+6
View File
@@ -0,0 +1,6 @@
namespace KamiToolKit.Enums;
public enum CounterFont {
MoneyFont,
ChocoboRace,
}
+21
View File
@@ -0,0 +1,21 @@
using System;
namespace KamiToolKit.Enums;
[Flags]
public enum DrawFlags : uint {
None = 0,
IsDirty = 0x1,
IsAnimating = 0x2,
CalculateTransformation = 0x4,
DisableRapidUp = 0x10,
DisableRapidDown = 0x20,
DisableRapidLeft = 0x40,
DisableRapidRight = 0x80,
DisableTimelineLabel = 0x100,
ClickableCursor = 0x100000,
RenderOnTop = 0x200000,
TextInputCursor = 0x400000,
UseEllipticalCollision = 0x800000,
UseTransformedCollision = 0x1000000,
}
+31
View File
@@ -0,0 +1,31 @@
using System;
namespace KamiToolKit.Enums;
[Flags]
public enum FlexFlags {
/// <summary>
/// Adjusts the height of the inserted node to be the same as the area generated
/// </summary>
FitHeight = 1 << 0,
/// <summary>
/// Adjusts the width of the inserted node to be the same as the area generated
/// </summary>
FitWidth = 1 << 1,
/// <summary>
/// Adjusts the FlexNode's height to fit the nodes that are inserted into it
/// </summary>
FitContentHeight = 1 << 3,
/// <summary>
/// Center inserted nodes into the middle of the flex area horizontally
/// </summary>
CenterVertically = 1 << 4,
/// <summary>
/// Center inserted nodes into the middle of the flex area vertically
/// </summary>
CenterHorizontally = 1 << 5,
}
+11
View File
@@ -0,0 +1,11 @@
using System.ComponentModel;
namespace KamiToolKit.Enums;
public enum HorizontalListAnchor {
[Description("Left")]
Left,
[Description("Right")]
Right,
}
+17
View File
@@ -0,0 +1,17 @@
using System.ComponentModel;
namespace KamiToolKit.Enums;
public enum LayoutAnchor {
[Description("Top Left")]
TopLeft,
[Description("Top Right")]
TopRight,
[Description("Bottom Left")]
BottomLeft,
[Description("Bottom Right")]
BottomRight,
}
+6
View File
@@ -0,0 +1,6 @@
namespace KamiToolKit.Enums;
public enum LayoutOrientation {
Vertical,
Horizontal,
}
+9
View File
@@ -0,0 +1,9 @@
using System;
namespace KamiToolKit.Enums;
[Flags]
public enum NodeEditMode {
Resize = 1 << 1,
Move = 1 << 2,
}
+7
View File
@@ -0,0 +1,7 @@
namespace KamiToolKit.Enums;
internal enum OverlayAddonState {
None,
WaitForReady,
Ready,
}
@@ -0,0 +1,7 @@
namespace KamiToolKit.Enums;
internal enum ControllerState {
WaitForNameplate,
WaitForReady,
Ready,
}
+51
View File
@@ -0,0 +1,51 @@
using System;
using System.ComponentModel;
namespace KamiToolKit.Enums;
public enum OverlayLayer {
/// <summary>
/// Layer that is the back most, this is below nameplates, but above the world itself.
/// </summary>
[Description("KTK_Overlay_Back")]
Background,
/// <summary>
/// Above nameplate layer
/// </summary>
[Description("KTK_Overlay_Middle")]
BehindUserInterface,
/// <summary>
/// Above most windows but below certain popup windows like battle text
/// </summary>
[Description("KTK_Overlay_Higher")]
AboveUserInterface,
/// <summary>
/// Above everything, use with caution
/// </summary>
[Description("KTK_Overlay_Front")]
Foreground,
}
public static class OverlayLayerExtensions {
extension(OverlayLayer layer) {
public int DepthLayer => layer switch {
OverlayLayer.Background => 1,
OverlayLayer.BehindUserInterface => 3,
OverlayLayer.AboveUserInterface => 7,
OverlayLayer.Foreground => 13,
_ => 1,
};
}
// Note: The game does not have a layer zero, but offsets the desired layer by one.
public static OverlayLayer GetOverlayLayer(this uint layer) => (layer + 1) switch {
1 => OverlayLayer.Background,
3 => OverlayLayer.BehindUserInterface,
7 => OverlayLayer.AboveUserInterface,
13 => OverlayLayer.Foreground,
_ => throw new Exception("Unknown depth layer: " + layer),
};
}
+6
View File
@@ -0,0 +1,6 @@
namespace KamiToolKit.Enums;
internal enum ResizeDirection {
BottomRight,
BottomLeft,
}
+20
View File
@@ -0,0 +1,20 @@
using System;
namespace KamiToolKit.Enums;
[Flags]
public enum TextInputFlags : ushort {
Capitalize = 0x1,
Mask = 0x2,
EnableDictionary = 0x4,
EnableHistory = 0x8,
EnableIme = 0x10,
EscapeClears = 0x20,
AllowUpperCase = 0x40,
AllowLowerCase = 0x80,
AllowNumberInput = 0x100,
AllowSymbolInput = 0x200,
WordWrap = 0x400,
MultiLine = 0x800,
AutoMaxWidth = 0x1000,
}
@@ -0,0 +1,11 @@
using System.ComponentModel;
namespace KamiToolKit.Enums;
public enum VerticalListAlignment {
[Description("Left")]
Left,
[Description("Right")]
Right,
}
+11
View File
@@ -0,0 +1,11 @@
using System.ComponentModel;
namespace KamiToolKit.Enums;
public enum VerticalListAnchor {
[Description("Top")]
Top,
[Description("Bottom")]
Bottom,
}
+8
View File
@@ -0,0 +1,8 @@
namespace KamiToolKit.Enums;
public enum WrapMode {
None = 0,
Tile = 1,
Stretch = 2,
TileMirrored = 3,
}
@@ -0,0 +1,20 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ModifierFlag = FFXIVClientStructs.FFXIV.Component.GUI.AtkEventData.AtkMouseData.ModifierFlag;
namespace KamiToolKit.Extensions;
public static class AtkEventDataExtensions {
extension(ref AtkEventData data) {
public Vector2 MousePosition => new(data.MouseData.PosX, data.MouseData.PosY);
public bool IsLeftClick => data.MouseData.ButtonId is 0;
public bool IsRightClick => data.MouseData.ButtonId is 1;
public bool IsNoModifiers => data.MouseData.Modifier is 0;
public bool IsAltHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Alt);
public bool IsControlHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Ctrl);
public bool IsShiftHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Shift);
public bool IsDragging => data.MouseData.Modifier.HasFlag(ModifierFlag.Dragging);
public bool IsScrollUp => data.MouseData.WheelDirection >= 1;
public bool IsScrollDown => data.MouseData.WheelDirection <= -1;
}
}
@@ -0,0 +1,19 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Extensions;
public static unsafe class AtkImageNodeExtensions {
extension(ref AtkImageNode node) {
public uint IconId => node.GetIconId();
private uint GetIconId() {
if (node.PartsList is null) return 0;
if (node.PartsList->Parts is null) return 0;
if (node.PartsList->Parts->UldAsset is null) return 0;
if (node.PartsList->Parts->UldAsset->AtkTexture.TextureType is not TextureType.Resource) return 0;
if (node.PartsList->Parts->UldAsset->AtkTexture.Resource is null) return 0;
return node.PartsList->Parts->UldAsset->AtkTexture.Resource->IconId;
}
}
}
@@ -0,0 +1,140 @@
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
namespace KamiToolKit.Extensions;
public static unsafe class AtkResNodeExtensions {
extension(ref AtkResNode node) {
public Vector2 Position {
get => new(node.X, node.Y);
set => node.SetPositionFloat(value.X, value.Y);
}
public Vector2 ScreenPosition
=> new(node.ScreenX, node.ScreenY);
public Vector2 Size {
get => new(node.GetWidth(), node.GetHeight());
set {
node.SetWidth((ushort) value.X);
node.SetHeight((ushort) value.Y);
}
}
public Bounds Bounds => new() {
TopLeft = node.Position,
BottomRight = node.Position + node.Size,
};
public Vector2 Center
=> node.Position + node.Size / 2.0f;
public Vector2 Scale {
get => new (node.GetScaleX(), node.GetScaleY());
set => node.SetScale(value.X, value.Y);
}
public float RotationDegrees {
get => node.GetRotationDegrees();
set => node.SetRotationDegrees(value - (int)(value / 360.0f) * 360.0f);
}
public Vector2 Origin {
get => new(node.OriginX, node.OriginY);
set => node.SetOrigin(value.X, value.Y);
}
public bool Visible {
get => node.IsVisible();
set => node.ToggleVisibility(value);
}
public Vector4 ColorVector {
get => node.Color.ToVector4();
set => node.Color = value.ToByteColor();
}
public ColorHelpers.HsvaColor ColorHsva {
get => ColorHelpers.RgbaToHsv(node.ColorVector);
set => node.Color = ColorHelpers.HsvToRgb(value).ToByteColor();
}
public Vector3 AddColor {
get => new Vector3(node.AddRed, node.AddGreen, node.AddBlue) / 255.0f;
set {
node.AddRed = (short)(value.X * 255);
node.AddGreen = (short)(value.Y * 255);
node.AddBlue = (short)(value.Z * 255);
}
}
public ColorHelpers.HsvaColor AddColorHsva {
get => ColorHelpers.RgbaToHsv(node.AddColor.AsVector4());
set => node.AddColor = ColorHelpers.HsvToRgb(value).AsVector3();
}
public Vector3 MultiplyColor {
get => new Vector3(node.MultiplyRed, node.MultiplyGreen, node.MultiplyBlue) / 100.0f;
set {
node.MultiplyRed = (byte)(value.X * 100.0f);
node.MultiplyGreen = (byte)(value.Y * 100.0f);
node.MultiplyBlue = (byte)(value.Z * 100.0f);
}
}
public ColorHelpers.HsvaColor MultiplyColorHsva {
get => ColorHelpers.RgbaToHsv(node.MultiplyColor.AsVector4());
set => node.MultiplyColor = ColorHelpers.HsvToRgb(value).AsVector3();
}
public void AddNodeFlag(params NodeFlags[] flags) {
foreach (var flag in flags) {
node.NodeFlags |= flag;
}
}
public void RemoveNodeFlag(params NodeFlags[] flags) {
foreach (var flag in flags) {
node.NodeFlags &= ~flag;
}
}
public void AddDrawFlag(params DrawFlags[] flags) {
foreach (var flag in flags) {
node.DrawFlags |= (uint)flag;
}
}
public void RemoveDrawFlag(params DrawFlags[] flags) {
foreach (var flag in flags) {
node.DrawFlags &= (uint)flag;
}
}
public bool CheckCollision(short x, short y, bool inclusive = true)
=> node.CheckCollisionAtCoords(x, y, inclusive);
public bool CheckCollision(Vector2 position, bool inclusive = true)
=> node.CheckCollisionAtCoords((short) position.X, (short) position.Y, inclusive);
public bool CheckCollision(AtkEventData* eventData, bool inclusive = true)
=> node.CheckCollisionAtCoords(eventData->MouseData.PosX, eventData->MouseData.PosY, inclusive);
public bool IsActuallyVisible {
get {
if (!node.Visible) return false;
var targetNode = node.ParentNode;
while (targetNode is not null) {
if (!targetNode->Visible) return false;
targetNode = targetNode->ParentNode;
}
return true;
}
}
}
}
@@ -0,0 +1,39 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Extensions;
public static unsafe class AtkStageExtensions {
extension(ref AtkStage atkStage) {
public void ClearNodeFocus(AtkResNode* targetNode) {
if (targetNode is null) return;
foreach (ref var focusEntry in atkStage.AtkInputManager->FocusList) {
// If this entry has no listener/addon, skip it
if (focusEntry.AtkEventListener is null) continue;
// If this entry has our target node
if (focusEntry.AtkEventTarget == targetNode) {
// Clear the entry
focusEntry.AtkEventTarget = null;
focusEntry.FocusParam = 0;
// Clear the input managers focused node
atkStage.AtkInputManager->FocusedNode = null;
// Clear collision managers collision node
atkStage.AtkCollisionManager->IntersectingCollisionNode = null;
// Also remove this node from any additional focus nodes the addon might reference
var addon = (AtkUnitBase*) focusEntry.AtkEventListener;
foreach (ref var node in addon->AdditionalFocusableNodes) {
if (node.Value == targetNode) {
node = null;
}
}
}
}
}
}
}
@@ -0,0 +1,137 @@
using System;
using System.Linq;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
using KamiToolKit.Classes;
namespace KamiToolKit.Extensions;
public static unsafe class AtkUldManagerExtensions {
extension(ref AtkUldManager manager) {
private bool IsNodeInObjectList(AtkResNode* node) {
foreach (var objectNode in manager.ObjectNodeSpan) {
if (objectNode.Value == node) return true;
}
return false;
}
public bool IsNodeInDrawList(AtkResNode* node) {
foreach (var drawNode in manager.Nodes) {
if (drawNode.Value == node) return true;
}
return false;
}
/// <summary>
/// Adds node and all children nodes to this UldManager's Object List
/// </summary>
public void AddNodeToObjectList(NodeBase node) {
manager.AddNodeToObjectList(node.ResNode);
foreach (var child in NodeBase.GetLocalChildren(node)) {
manager.AddNodeToObjectList(child.ResNode);
}
manager.UpdateDrawNodeList();
}
public void AddNodeToObjectList(AtkResNode* newNode) {
if (newNode is null) return;
// If the node is already in the object list, skip.
if (manager.IsNodeInObjectList(newNode)) return;
var oldSize = manager.Objects->NodeCount;
var newSize = oldSize + 1;
var newBuffer = (AtkResNode**)NativeMemoryHelper.Malloc((ulong)(newSize * 8));
if (oldSize > 0) {
foreach (var index in Enumerable.Range(0, oldSize)) {
newBuffer[index] = manager.Objects->NodeList[index];
}
NativeMemoryHelper.Free(manager.Objects->NodeList, (ulong)(oldSize * 8));
}
newBuffer[newSize - 1] = newNode;
manager.Objects->NodeList = newBuffer;
manager.Objects->NodeCount = newSize;
}
/// <summary>
/// Removes node and all children nodes from this UldManager's Object List
/// </summary>
public void RemoveNodeFromObjectList(NodeBase node) {
manager.RemoveNodeFromObjectList(node.ResNode);
foreach (var child in NodeBase.GetLocalChildren(node)) {
manager.RemoveNodeFromObjectList(child.ResNode);
}
manager.UpdateDrawNodeList();
}
public void RemoveNodeFromObjectList(AtkResNode* node) {
if (node is null) return;
// If the node isn't in the object list, skip.
if (!manager.IsNodeInObjectList(node)) return;
var oldSize = manager.Objects->NodeCount;
var newSize = oldSize - 1;
var newBuffer = (AtkResNode**)NativeMemoryHelper.Malloc((ulong)(newSize * 8));
var newIndex = 0;
foreach (var index in Enumerable.Range(0, oldSize)) {
if (manager.Objects->NodeList[index] != node) {
newBuffer[newIndex] = manager.Objects->NodeList[index];
newIndex++;
}
}
NativeMemoryHelper.Free(manager.Objects->NodeList, (ulong)(oldSize * 8));
manager.Objects->NodeList = newBuffer;
manager.Objects->NodeCount = newSize;
}
public void PrintObjectList() {
Log.Debug("Beginning NodeList");
foreach (var index in Enumerable.Range(0, manager.Objects->NodeCount)) {
var nodePointer = manager.Objects->NodeList[index];
Log.Debug($"[{index}]: {(nint)nodePointer:X}");
}
}
public uint GetMaxNodeId() {
uint max = 1;
foreach (var child in manager.Nodes) {
if (child.Value is null) continue;
max = Math.Max(child.Value->NodeId, max);
}
return max;
}
public Span<Pointer<AtkResNode>> ObjectNodeSpan
=> new(manager.Objects->NodeList, manager.Objects->NodeCount);
public T* SearchNodeById<T>(uint nodeId) where T : unmanaged {
foreach (var node in manager.Nodes) {
if (node.Value is not null) {
if (node.Value->NodeId == nodeId)
return (T*) node.Value;
}
}
return null;
}
public AtkResNode* SearchNodeById(uint nodeId)
=> manager.SearchNodeById<AtkResNode>(nodeId);
}
}
@@ -0,0 +1,110 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Interface.Textures.TextureWraps;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Extensions;
public static unsafe class AtkUldPartExtensions {
extension(ref AtkUldPart part) {
public bool IsTextureReady => part.UldAsset is not null && part.UldAsset->AtkTexture.IsTextureReady();
public Vector2 LoadedTextureSize => part.GetActualTextureSize();
public string LoadedPath => part.GetLoadedPath();
public void LoadTexture(string path, bool resolveTheme = true) {
try {
if (part.UldAsset is null) return;
part.TryUnloadTexture();
var texturePath = path.Replace("_hr1", string.Empty);
var themedPath = texturePath.Replace("uld", GetThemePathModifier());
if (DalamudInterface.Instance.DataManager.FileExists(themedPath) && resolveTheme) {
texturePath = themedPath;
}
if (DalamudInterface.Instance.DataManager.FileExists(texturePath)) {
part.UldAsset->AtkTexture.LoadTextureWithDefaultVersion(texturePath);
}
}
catch (Exception e) {
Log.Exception(e);
}
}
public void LoadIcon(uint iconId)
=> part.UldAsset->AtkTexture.LoadIconTexture(iconId, GetIconSubFolder(iconId));
private Vector2 GetActualTextureSize() {
if (part.UldAsset is null) return Vector2.Zero;
if (!part.UldAsset->AtkTexture.IsTextureReady()) return Vector2.Zero;
if (part.UldAsset->AtkTexture.TextureType is 0) return Vector2.Zero;
if (part.UldAsset->AtkTexture.KernelTexture is null) return Vector2.Zero;
var width = part.UldAsset->AtkTexture.GetTextureWidth();
var height = part.UldAsset->AtkTexture.GetTextureHeight();
return new Vector2(width, height);
}
public void LoadTexture(Texture* texture) {
if (part.UldAsset is null) return;
part.TryUnloadTexture();
part.UldAsset->AtkTexture.KernelTexture = texture;
part.UldAsset->AtkTexture.TextureType = TextureType.KernelTexture;
}
public void LoadTexture(IDalamudTextureWrap textureWrap) {
var texturePointer = (Texture*)DalamudInterface.Instance.TextureProvider.ConvertToKernelTexture(textureWrap, true);
if (texturePointer is null) return;
part.LoadTexture(texturePointer);
}
private string GetLoadedPath() {
if (part.UldAsset is null) return string.Empty;
if (part.UldAsset->AtkTexture.Resource is null) return string.Empty;
if (part.UldAsset->AtkTexture.Resource->TexFileResourceHandle is null) return string.Empty;
return part.UldAsset->AtkTexture.Resource->TexFileResourceHandle->FileName.ToString();
}
private void TryUnloadTexture() {
if (part.UldAsset is null) return;
if (!part.UldAsset->AtkTexture.IsTextureReady()) return;
if (part.UldAsset->AtkTexture.TextureType is 0) return;
if (part.UldAsset->AtkTexture.KernelTexture is null) return;
part.UldAsset->AtkTexture.ReleaseTexture();
part.UldAsset->AtkTexture.KernelTexture = null;
part.UldAsset->AtkTexture.TextureType = 0;
}
}
private static string GetThemePathModifier() => AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType switch {
not 0 => $"uld/img{AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType:00}",
_ => "uld",
};
public static IconSubFolder GetIconSubFolder(uint iconId) {
var textureManager = AtkStage.Instance()->AtkTextureResourceManager;
Span<byte> buffer = stackalloc byte[0x100];
buffer.Clear();
var bytePointer = (byte*) Unsafe.AsPointer(ref buffer[0]);
var textureScale = textureManager->DefaultTextureScale;
var targetFolder = (IconSubFolder)textureManager->IconLanguage;
// Try to resolve the path using the current language
AtkTexture.GetIconPath(bytePointer, iconId, textureScale, targetFolder);
var pathResult = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bytePointer).String;
// If the resolved path doesn't exist, re-process with default folder
return DalamudInterface.Instance.DataManager.FileExists(pathResult) ? targetFolder : IconSubFolder.None;
}
}
@@ -0,0 +1,42 @@
using System;
using System.Linq;
using System.Numerics;
using System.Reflection;
using FFXIVClientStructs.Attributes;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Extensions;
public static unsafe class AtkUnitBaseExtensions {
public static string GetAddonTypeName<T>() where T : unmanaged {
var type = typeof(T);
var attribute = type.GetCustomAttributes().OfType<AddonAttribute>().FirstOrDefault();
if (attribute is null) throw new Exception("Unable to find AddonAttribute to resolve addon name.");
var addonName = attribute.AddonIdentifiers.FirstOrDefault();
if (addonName is null) throw new Exception("Addon attribute names are empty.");
return addonName;
}
extension(ref AtkUnitBase addon) {
public Vector2 Size => addon.GetSize();
public Vector2 RootSize => addon.GetRootSize();
public Vector2 Position => new(addon.X, addon.Y);
private Vector2 GetSize() {
var width = stackalloc short[1];
var height = stackalloc short[1];
addon.GetSize(width, height, false);
return new Vector2(*width, *height);
}
private Vector2 GetRootSize() {
if (addon.RootNode is null) return Vector2.Zero;
return new Vector2(addon.RootNode->Width, addon.RootNode->Height);
}
}
}
@@ -0,0 +1,12 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.Graphics;
namespace KamiToolKit.Extensions;
public static class ByteColorExtensions {
public static Vector4 ToVector4(this ByteColor color)
=> new(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, color.A / 255.0f);
public static ByteColor ToByteColor(this Vector4 v)
=> new() { A = (byte)(v.W * 255), R = (byte)(v.X * 255), G = (byte)(v.Y * 255), B = (byte)(v.Z * 255) };
}
+52
View File
@@ -0,0 +1,52 @@
using System;
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Utility;
namespace KamiToolKit.Extensions;
internal static class EnumExtensions {
extension(Enum enumValue) {
public string Description => enumValue.GetDescription();
private string GetDescription() {
var attribute = enumValue.GetAttribute<DescriptionAttribute>();
return attribute?.Description ?? enumValue.ToString();
}
}
extension<T>(ref T flagValue) where T : unmanaged, Enum {
public void SetFlags(params T[] flags) {
foreach (var flag in flags) {
flagValue.SetFlag(flag, true);
}
}
public void ClearFlags(params T[] flags) {
foreach (var flag in flags) {
flagValue.SetFlag(flag, false);
}
}
private unsafe void SetFlag(T flag, bool enable) {
switch (sizeof(T)) {
case 1: flagValue.SetFlag<T, byte>(flag, enable); break;
case 2: flagValue.SetFlag<T, ushort>(flag, enable); break;
case 4: flagValue.SetFlag<T, uint>(flag, enable); break;
case 8: flagValue.SetFlag<T, ulong>(flag, enable); break;
default: throw new NotSupportedException("Unsupported enum size");
}
}
private void SetFlag<TUnderlying>(T flag, bool enable) where TUnderlying : unmanaged, IBinaryInteger<TUnderlying> {
ref var value = ref Unsafe.As<T, TUnderlying>(ref flagValue);
var mask = Unsafe.As<T, TUnderlying>(ref flag);
if (enable)
value |= mask;
else
value &= ~mask;
}
}
}
@@ -0,0 +1,16 @@
using System.Drawing;
using System.Numerics;
using Dalamud.Interface;
using Vector4 = System.Numerics.Vector4;
namespace KamiToolKit.Extensions;
public static class KnownColorExtensions {
public static Vector3 Vector3(this KnownColor color) {
var color4 = color.Vector();
return new Vector3(color4.X, color4.Y, color4.Z);
}
public static Vector3 AsVector3Color(this Vector4 vector4)
=> new(vector4.X, vector4.Y, vector4.Z);
}
@@ -0,0 +1,23 @@
using System.Runtime.CompilerServices;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using KamiToolKit.Classes;
namespace KamiToolKit.Extensions;
public static unsafe class MainThreadSafety {
/// <summary>
/// Returns true if <em>not</em> on the main thread. Use this to return early.
/// </summary>
public static bool TryAssertMainThread([CallerFilePath] string? callerFilePath = null, [CallerMemberName] string? callerName = null) {
if (Framework.Instance()->IsDestroying) return true;
if (!ThreadSafety.IsMainThread) {
Log.Error($"{callerFilePath?.Split(@"\")[^1][..^2]}{callerName} must be invoked from the main thread.");
return true;
}
return false;
}
}
@@ -0,0 +1,10 @@
using System;
using System.Text;
namespace KamiToolKit.Extensions;
public static class ReadOnlySpanExtensions {
extension(ReadOnlySpan<byte> span) {
public string String => Encoding.UTF8.GetString(span);
}
}
@@ -0,0 +1,13 @@
using System.Diagnostics;
using KamiToolKit.Classes;
namespace KamiToolKit.Extensions;
public static class StopwatchExtensions {
extension(Stopwatch stopwatch) {
public void LogTime(string logMessage) {
DalamudInterface.Instance.Log.Debug($"{logMessage, -15}: {stopwatch, 15} :: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
}
}
}
+1
View File
@@ -0,0 +1 @@
global using KamiToolKit.Extensions;
+32
View File
@@ -0,0 +1,32 @@
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
<!-- Project settings -->
<PropertyGroup>
<LangVersion>preview</LangVersion>
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
</PropertyGroup>
<!-- Dalamud.NET.Sdk settings -->
<PropertyGroup>
<Use_DalamudPackager>false</Use_DalamudPackager>
</PropertyGroup>
<!-- Assets -->
<ItemGroup>
<Content Include="Assets\**\*.png" CopyToOutputDirectory="Always"/>
</ItemGroup>
<!-- NuGet dependencies -->
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12"/>
</ItemGroup>
<!-- FFXIVClientStructs -->
<PropertyGroup Condition="Exists('..\FFXIVClientStructs')">
<Use_Dalamud_FFXIVClientStructs>false</Use_Dalamud_FFXIVClientStructs>
</PropertyGroup>
<ItemGroup Condition="Exists('..\FFXIVClientStructs')">
<ProjectReference Include="..\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" Private="True" />
<ProjectReference Include="..\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj" Private="True" />
</ItemGroup>
</Project>
@@ -0,0 +1,32 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nativeaddon/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodebase/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cbasic/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cbuttons/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cbuttons/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cdropdown/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Choldbutton/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cinputtext/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Clist/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cprogressbar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cradiobutton/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cscrollbar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Ctreelist/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cwindow/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cdropdown/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Choldbutton/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cicon/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cimage/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cinputtext/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Clayout/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Clist/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cninegrid/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cprogressbar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cradiobutton/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cscrollbar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Csimple/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Csimplenodes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ctext/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ctreelist/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cwindow/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+14
View File
@@ -0,0 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KamiToolKit", "KamiToolKit.csproj", "{52A9E8E2-ACC7-4696-8684-5C4994D0350C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{52A9E8E2-ACC7-4696-8684-5C4994D0350C}.Debug|Any CPU.ActiveCfg = Debug|x64
{52A9E8E2-ACC7-4696-8684-5C4994D0350C}.Debug|Any CPU.Build.0 = Debug|x64
EndGlobalSection
EndGlobal
+66
View File
@@ -0,0 +1,66 @@
using System;
using System.Collections.Concurrent;
using Dalamud.Plugin;
using KamiToolKit.Classes;
using Serilog.Events;
namespace KamiToolKit;
public static class KamiToolKitLibrary {
internal static bool IsInitialized { get; private set; }
internal static ConcurrentDictionary<nint, Type>? AllocatedNodes;
internal static string? DefaultWindowSubtitle;
/// <summary>
/// Main initialization method for KamiToolKit. This method is required to be invoked before any KamiToolKit features are used.
/// Failure to do so will not result in any direct warnings, but will result in undefined behavior.
/// </summary>
public static void Initialize(IDalamudPluginInterface pluginInterface, string? defaultWindowSubtitle = null) {
IsInitialized = true;
DefaultWindowSubtitle = defaultWindowSubtitle;
// Inject non-Experimental Properties
pluginInterface.Inject(DalamudInterface.Instance);
DalamudInterface.Instance.GameInteropProvider.InitializeFromAttributes(DalamudInterface.Instance);
// Create node data share
AllocatedNodes = DalamudInterface.Instance.PluginInterface.GetOrCreateData("KamiToolKitAllocatedNodes", () => new ConcurrentDictionary<nint, Type>());
// Inject Experimental Properties
pluginInterface.Inject(Experimental.Instance);
DalamudInterface.Instance.GameInteropProvider.InitializeFromAttributes(Experimental.Instance);
Experimental.Instance.EnableHooks();
// Force enable Verbose so that users are able to get advanced logging information on request.
DalamudInterface.Instance.Log.MinimumLogLevel = LogEventLevel.Verbose;
DalamudInterface.Instance.Log.Info($"KamiToolKit initialized for {pluginInterface.InternalName}");
}
/// <summary>
/// Alias for Cleanup
/// </summary>
public static void Dispose() => Cleanup();
/// <summary>
/// Alias for Cleanup
/// </summary>
public static void Shutdown() => Cleanup();
/// <summary>
/// Cleans up any potentially leaked resources that KamiToolKit has allocated.
/// </summary>
public static void Cleanup() {
if (MainThreadSafety.TryAssertMainThread()) return;
NodeBase.DisposeNodes();
NativeAddon.DisposeAddons();
DalamudInterface.Instance.PluginInterface.RelinquishData("KamiToolKitAllocatedNodes");
Experimental.Instance.DisposeHooks();
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 MidoriKami
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -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());
}
}
+180
View File
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase : IDisposable {
internal const uint NodeIdBase = 100_000_000;
protected static readonly List<NodeBase> CreatedNodes = [];
private static int logIndent = -1;
internal static uint CurrentOffset;
private bool isDisposed;
internal abstract AtkResNode* ResNode { get; }
internal bool IsAddonRootNode;
private delegate* unmanaged<AtkResNode*, bool, void> originalDestructorFunction;
private AtkResNode.Delegates.Destroy destructorFunction = null!;
private AtkResNode.AtkResNodeVirtualTable* virtualTable;
public void Dispose() {
try {
logIndent++;
LogIndented($"Beginning Dispose for {GetType()}");
logIndent++;
if (MainThreadSafety.TryAssertMainThread()) {
if (Framework.Instance()->IsDestroying) {
LogIndented("Game is shutting down, aborting manual dispose.");
}
return;
}
if (isDisposed) {
LogIndented("Node was already disposed, skipping.");
return;
}
isDisposed = true;
if (!IsNodeValid()) {
Log.Warning("Invalid node, dispose aborted.");
return;
}
LogIndented("Disposing Children");
foreach (var child in ChildNodes.ToList()) {
child.Dispose();
}
LogIndented("Children Disposed");
ChildNodes.Clear();
LogIndented("Disposing Tooltip Events");
UnregisterTooltipEvents();
LogIndented("Clearing Native Focus");
AtkStage.Instance()->ClearNodeFocus(ResNode);
LogIndented("Detaching From UI");
DetachNode();
LogIndented("Disposing Timeline");
Timeline?.Dispose();
ResNode->Timeline = null;
LogIndented("Invoking Native Dispose");
Dispose(true, false);
GC.SuppressFinalize(this);
CreatedNodes.Remove(this);
logIndent--;
LogIndented("Dispose Complete");
logIndent--;
}
catch (Exception e) {
Log.Exception(e);
logIndent = 0;
}
}
private static void LogIndented(string message)
=> Log.Verbose(new string(' ', logIndent * 2) + message);
/// <summary>
/// Warning, this is only to ensure there are no memory leaks.
/// Ensure you have detached nodes safely from native ui before disposing.
/// </summary>
internal static void DisposeNodes() {
var leakedNodeCount = CreatedNodes.Count(node => !node.IsAddonRootNode && node.ResNode is not null && node.ResNode->ParentNode is null);
if (leakedNodeCount is not 0) {
Log.Warning($"There were {leakedNodeCount} node(s) that were not disposed safely.");
}
foreach (var node in CreatedNodes.ToArray()) {
if (node.ResNode is null) continue;
if (node.ResNode->ParentNode is not null) continue;
if (node.IsAddonRootNode) continue;
Log.Warning($"Forcing disposal of: {node.GetType()}");
node.Dispose();
}
}
~NodeBase() => Dispose(false, false);
/// <summary>
/// Dispose associated resources. If a resource modifies native state directly guard it with isNativeDestructor
/// </summary>
/// <param name="disposing">
/// Indicates if this specific call should dispose resources or not. This protects against double dispose,
/// or incorrectly manipulating native state too many times.
/// </param>
/// <param name="isNativeDestructor">
/// Indicates if the dispose call should try to completely clean up all resources,
/// or if it should only clean up managed resources. When false, be sure to only dispose
/// resources that exist in managed spaces, as the game has already cleaned up everything else.
/// </param>
protected virtual void Dispose(bool disposing, bool isNativeDestructor) {
// Dispose of managed resources that must be disposed regardless of how dispose is invoked
DisposeEvents();
DisableEditMode(NodeEditMode.Move | NodeEditMode.Resize);
}
private bool IsNodeValid() {
if (ResNode is null) return false;
if (ResNode->VirtualTable is null) return false;
if (ResNode->VirtualTable == AtkEventTarget.StaticVirtualTablePointer) return false;
return true;
}
public static implicit operator AtkResNode*(NodeBase node) => node.ResNode;
public static implicit operator AtkEventTarget*(NodeBase node) => &node.ResNode->AtkEventTarget;
protected void BuildVirtualTable() {
// Back up original destructor pointer
originalDestructorFunction = ResNode->VirtualTable->Destroy;
// Overwrite virtual table with a custom copy,
// Note: Currently there are only 2 vfuncs, but there's no harm in copying more for if they ever add more vfuncs to the game.
virtualTable = (AtkResNode.AtkResNodeVirtualTable*)NativeMemoryHelper.Malloc(0x8 * 4);
NativeMemory.Copy(ResNode->VirtualTable, virtualTable, 0x8 * 4);
ResNode->VirtualTable = virtualTable;
// Pin managed function to virtual table entry
destructorFunction = DestructorDetour;
// Replace native destructor with
virtualTable->Destroy = (delegate* unmanaged<AtkResNode*, bool, void>) Marshal.GetFunctionPointerForDelegate(destructorFunction);
}
private void DestructorDetour(AtkResNode* thisPtr, bool free) {
Dispose(true, true);
InvokeOriginalDestructor(thisPtr, free);
Log.Verbose($"Native has disposed node {GetType()}");
GC.SuppressFinalize(this);
CreatedNodes.Remove(this);
isDisposed = true;
}
protected void InvokeOriginalDestructor(AtkResNode* thisPtr, bool free) {
if (virtualTable is null) return; // Shouldn't be possible, but just in case.
originalDestructorFunction(thisPtr, free);
NativeMemoryHelper.Free(virtualTable, 0x8 * 4);
virtualTable = null;
}
}
+205
View File
@@ -0,0 +1,205 @@
using System;
using System.Numerics;
using Dalamud.Game.Addon.Events;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
using KamiToolKit.Nodes;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase {
private Vector2 clickStartPosition = Vector2.Zero;
private NodeEditMode currentEditMode = 0;
private ViewportEventListener? editEventListener;
private bool isCursorSet;
private bool isMoving;
private bool isResizing;
private NodeEditOverlayNode? overlayNode;
public Action<NodeBase>? OnResizeComplete { get; set; }
public Action<NodeBase>? OnMoveComplete { get; set; }
public Action<NodeBase>? OnEditComplete { get; set; }
public bool EnableMoving {
get;
set {
field = value;
if (value) {
EnableEditMode(NodeEditMode.Move);
}
else {
DisableEditMode(NodeEditMode.Move);
}
}
}
public bool EnableResizing {
get;
set {
field = value;
if (value) {
EnableEditMode(NodeEditMode.Resize);
}
else {
DisableEditMode(NodeEditMode.Resize);
}
}
}
public void EnableEditMode(NodeEditMode mode) {
currentEditMode |= mode;
if (overlayNode is null) {
overlayNode = new NodeEditOverlayNode {
Position = new Vector2(-16.0f, -16.0f),
Size = Size + new Vector2(32.0f, 32.0f),
};
overlayNode.AttachNode(this);
ChildNodes.Add(overlayNode);
}
overlayNode.ShowParts = currentEditMode.HasFlag(NodeEditMode.Resize);
if (editEventListener is null) {
editEventListener = new ViewportEventListener(OnEditEvent);
editEventListener.AddEvent(AtkEventType.MouseMove, overlayNode);
editEventListener.AddEvent(AtkEventType.MouseDown, overlayNode);
}
}
public void DisableEditMode(NodeEditMode mode) {
currentEditMode &= ~mode;
if (currentEditMode.HasFlag(NodeEditMode.Resize) || currentEditMode.HasFlag(NodeEditMode.Move)) return;
if (editEventListener is not null) {
editEventListener.RemoveEvent(AtkEventType.MouseMove);
editEventListener.RemoveEvent(AtkEventType.MouseDown);
editEventListener.Dispose();
editEventListener = null;
}
if (overlayNode is not null) {
ChildNodes.Remove(overlayNode);
overlayNode.DetachNode();
overlayNode.Dispose();
overlayNode = null;
}
}
private void OnEditEvent(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
if (overlayNode is null) return;
if (editEventListener is null) return;
ref var mouseData = ref atkEventData->MouseData;
var mousePosition = new Vector2(mouseData.PosX, mouseData.PosY);
var mouseDelta = mousePosition - clickStartPosition;
switch (eventType) {
// Move Logic
case AtkEventType.MouseMove when isMoving: {
Position += mouseDelta;
clickStartPosition = mousePosition;
atkEvent->SetEventIsHandled(true);
}
break;
// Update hover state when not resizing, as we latch that for the behavior
case AtkEventType.MouseMove when !isResizing: {
overlayNode.UpdateHover(atkEventData);
}
break;
// Resize Logic
case AtkEventType.MouseMove when isResizing: {
Position += overlayNode.GetPositionDelta(mouseDelta);
Size += overlayNode.GetSizeDelta(mouseDelta);
overlayNode.Size = Size + new Vector2(32.0f, 32.0f);
clickStartPosition = mousePosition;
atkEvent->SetEventIsHandled(true);
}
break;
// Begin Resize Event
case AtkEventType.MouseDown when !isResizing && overlayNode.AnyHovered() && currentEditMode.HasFlag(NodeEditMode.Resize): {
editEventListener.AddEvent(AtkEventType.MouseUp, overlayNode);
isResizing = true;
clickStartPosition = mousePosition;
atkEvent->SetEventIsHandled(true);
}
break;
// End Resize Event
case AtkEventType.MouseUp when isResizing: {
OnResizeComplete?.Invoke(this);
OnEditComplete?.Invoke(this);
isResizing = false;
editEventListener.RemoveEvent(AtkEventType.MouseUp);
}
break;
// Begin Move Event
case AtkEventType.MouseDown when !overlayNode.AnyHovered() && overlayNode.CheckCollision(atkEventData) && !isMoving && currentEditMode.HasFlag(NodeEditMode.Move): {
editEventListener.AddEvent(AtkEventType.MouseUp, overlayNode);
isMoving = true;
clickStartPosition = mousePosition;
atkEvent->SetEventIsHandled(true);
}
break;
// End Move Event
case AtkEventType.MouseUp when isMoving: {
OnMoveComplete?.Invoke(this);
OnEditComplete?.Invoke(this);
isMoving = false;
editEventListener.RemoveEvent(AtkEventType.MouseUp);
}
break;
}
if (isCursorSet) {
ResetCursor();
isCursorSet = false;
}
if (currentEditMode.HasFlag(NodeEditMode.Move)) {
if (isMoving) {
SetCursor(AddonCursorType.Grab);
isCursorSet = true;
}
else if (CheckCollision(atkEventData)) {
SetCursor(AddonCursorType.Hand);
isCursorSet = true;
}
}
if (overlayNode.AnyHovered() && currentEditMode.HasFlag(NodeEditMode.Resize)) {
overlayNode.SetCursor();
isCursorSet = true;
}
}
private static void SetCursor(AddonCursorType cursor)
=> DalamudInterface.Instance.AddonEventManager.SetCursor(cursor);
private static void ResetCursor()
=> DalamudInterface.Instance.AddonEventManager.ResetCursor();
}
+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;
}
}
}
+260
View File
@@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase {
internal readonly List<NodeBase> ChildNodes = [];
private NodeBase? parentNode;
internal AtkUldManager* ParentUldManager { get; set; }
internal AtkUnitBase* ParentAddon { get; private set; }
[OverloadResolutionPriority(1)]
public void AttachNode(NativeAddon? targetAddon, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformManagedAttach(targetAddon, targetPosition);
public void AttachNode(AtkUnitBase* targetAddon, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach(targetAddon is not null ? targetAddon->RootNode : null, targetPosition);
[OverloadResolutionPriority(1)]
public void AttachNode(NodeBase? targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformManagedAttach(targetNode, targetPosition);
public void AttachNode(AtkResNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach(targetNode, targetPosition);
public void AttachNode(AtkImageNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkTextNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkNineGridNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkCounterNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkCollisionNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkClippingMaskNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
public void AttachNode(AtkComponentNode* targetNode, NodePosition targetPosition = NodePosition.AfterAllSiblings)
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
private void PerformManagedAttach(NativeAddon? targetAddon, NodePosition targetPosition = NodePosition.AsLastChild) {
if (MainThreadSafety.TryAssertMainThread()) return;
if (targetAddon is null) return;
// Check the Addon's node list to find out what NodeId we should be, and set that before attaching
if (NodeId > NodeIdBase) {
NodeId = targetAddon.InternalAddon->UldManager.GetMaxNodeId() + 1;
}
PerformNativeAttach(targetAddon.RootNode, targetPosition);
parentNode = targetAddon.RootNode;
parentNode.ChildNodes.Add(this);
}
private void PerformManagedAttach(NodeBase? targetNode, NodePosition targetPosition) {
if (MainThreadSafety.TryAssertMainThread()) return;
if (targetNode is null) return;
PerformNativeAttach(targetNode, targetPosition);
parentNode = targetNode;
parentNode.ChildNodes.Add(this);
}
private void PerformNativeAttach(AtkResNode* targetNode, NodePosition targetPosition) {
if (MainThreadSafety.TryAssertMainThread()) return;
if (targetNode is null) return;
if (targetNode->GetNodeType() is NodeType.Component) {
// If target is a ComponentNode,
// then we don't ever wanna be a child of the ComponentNode itself,
// we will want to be a sibling of the root node.
// Therefore, redirect the target position to be siblings.
targetPosition = targetPosition switch {
NodePosition.AsLastChild => NodePosition.AfterAllSiblings,
NodePosition.AsFirstChild => NodePosition.BeforeAllSiblings,
_ => targetPosition,
};
// If however, we are using BeforeTarget or AfterTarget,
// then we do want to attach to the ComponentNode
// else, attach to its root node.
var componentNode = targetNode->GetAsAtkComponentNode();
if (componentNode is not null) {
targetNode = targetPosition switch {
NodePosition.AfterTarget => targetNode,
NodePosition.BeforeTarget => targetNode,
NodePosition.AfterAllSiblings => componentNode->Component->UldManager.RootNode,
NodePosition.BeforeAllSiblings => componentNode->Component->UldManager.RootNode,
_ => throw new ArgumentOutOfRangeException(nameof(targetPosition), targetPosition, null),
};
// We also need to check the components node list, to get a safely assigned nodeId
if (NodeId > NodeIdBase) {
NodeId = componentNode->Component->UldManager.GetMaxNodeId() + 1;
}
}
}
NodeLinker.AttachNode(this, targetNode, targetPosition);
UpdateParentAddon(targetNode);
UpdateNative();
}
internal void ReattachNode(AtkResNode* newTarget) {
if (newTarget is null) return;
DetachNode();
AttachNode(newTarget);
}
public void DetachNode() {
if (MainThreadSafety.TryAssertMainThread()) return;
if (ResNode is null) return;
UnlinkFromNative();
RemoveUldManagerObjectReferences();
RemoveParentAddonReferences();
RemoveParentNodeReferences();
}
private void UnlinkFromNative() {
NodeLinker.DetachNode(ResNode);
ResNode->ParentNode = null;
ResNode->NextSiblingNode = null;
ResNode->PrevSiblingNode = null;
}
private void RemoveUldManagerObjectReferences() {
if (ParentUldManager is null) return;
ParentUldManager->RemoveNodeFromObjectList(this);
ParentUldManager = null;
}
private void RemoveParentAddonReferences() {
if (ParentAddon is null) return;
ParentAddon->UldManager.UpdateDrawNodeList();
ParentAddon->UpdateCollisionNodeList(false);
ParentAddon = null;
foreach (var child in GetAllChildren(this)) {
child.ParentAddon = null;
}
}
private void RemoveParentNodeReferences() {
if (parentNode is null) return;
parentNode.ChildNodes.Remove(this);
parentNode = null;
}
private void UpdateNative() {
if (ResNode is null) return;
MarkDirty();
if (ParentUldManager is null) {
ParentUldManager = GetUldManagerForNode(ResNode);
}
if (ParentUldManager is not null) {
ParentUldManager->AddNodeToObjectList(this);
}
if (ParentAddon is not null) {
if (ParentAddon->NameString is "NamePlate") {
Log.Warning("Warning, attaching to AddonNamePlate is not supported. Use OverlayController instead.");
}
ParentAddon->UldManager.UpdateDrawNodeList();
ParentAddon->UpdateCollisionNodeList(false);
}
}
private void UpdateParentAddon(AtkResNode* node) {
if (parentNode is not null && parentNode.ParentAddon is not null) {
ParentAddon = parentNode.ParentAddon;
}
else if (ParentAddon is null) {
var targetParentAddon = RaptureAtkUnitManager.Instance()->GetAddonByNode(node);
if (targetParentAddon is not null) {
ParentAddon = targetParentAddon;
}
}
if (ParentAddon is not null) {
foreach (var child in GetAllChildren(this)) {
child.ParentAddon = ParentAddon;
}
}
}
private AtkUldManager* GetUldManagerForNode(AtkResNode* node) {
if (node is null) return null;
var targetNode = node;
if (targetNode->GetNodeType() is NodeType.Component) {
targetNode = targetNode->ParentNode;
}
// Try to get UldManager via the first parent that is a component
while (targetNode is not null) {
if (targetNode->GetNodeType() is NodeType.Component) {
var componentNode = (AtkComponentNode*)targetNode;
return &componentNode->Component->UldManager;
}
targetNode = targetNode->ParentNode;
}
// We failed to find a parent component, try to get a parent addon instead
if (ParentAddon is not null) {
return &ParentAddon->UldManager;
}
return null;
}
private static IEnumerable<NodeBase> GetAllChildren(NodeBase parent) {
foreach (var child in parent.ChildNodes) {
yield return child;
foreach (var childNode in GetAllChildren(child)) {
yield return childNode;
}
}
}
internal static IEnumerable<NodeBase> GetLocalChildren(NodeBase parent) {
if (parent is ComponentNode) yield break;
foreach (var child in parent.ChildNodes) {
yield return child;
if (child is ComponentNode) continue;
foreach (var childNode in GetLocalChildren(child)) {
yield return childNode;
}
}
}
}
@@ -0,0 +1,244 @@
using System;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Common.Math;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using Bounds = KamiToolKit.Classes.Bounds;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
using Vector4 = System.Numerics.Vector4;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase {
public virtual float X {
get => ResNode->GetXFloat();
set => ResNode->SetXFloat(value);
}
public virtual float Y {
get => ResNode->GetYFloat();
set => ResNode->SetYFloat(value);
}
public virtual Vector2 Position {
get => ResNode->Position;
set => ResNode->Position = value;
}
public virtual float ScreenX {
get => ResNode->ScreenX;
set => ResNode->ScreenX = value;
}
public virtual float ScreenY {
get => ResNode->ScreenY;
set => ResNode->ScreenY = value;
}
public virtual Vector2 ScreenPosition
=> ResNode->ScreenPosition;
public virtual float Width {
get => ResNode->GetWidth();
set {
ResNode->SetWidth((ushort)value);
OnSizeChanged();
}
}
public virtual float Height {
get => ResNode->GetHeight();
set {
ResNode->SetHeight((ushort)value);
OnSizeChanged();
}
}
public virtual Vector2 Size {
get => ResNode->Size;
set {
ResNode->SetWidth((ushort)value.X);
ResNode->SetHeight((ushort)value.Y);
OnSizeChanged();
}
}
public Bounds Bounds
=> ResNode->Bounds;
public Vector2 Center
=> ResNode->Center;
public virtual float ScaleX {
get => ResNode->GetScaleX();
set => ResNode->SetScaleX(value);
}
public virtual float ScaleY {
get => ResNode->GetScaleY();
set => ResNode->SetScaleY(value);
}
public virtual Vector2 Scale {
get => ResNode->Scale;
set => ResNode->Scale = value;
}
public virtual float Rotation {
get => ResNode->GetRotation();
set => ResNode->SetRotation(value);
}
public virtual float RotationDegrees {
get => ResNode->RotationDegrees;
set => ResNode->RotationDegrees = value;
}
public virtual float OriginX {
get => ResNode->OriginX;
set => ResNode->OriginX = value;
}
public virtual float OriginY {
get => ResNode->OriginY;
set => ResNode->OriginY = value;
}
public virtual Vector2 Origin {
get => ResNode->Origin;
set => ResNode->Origin = value;
}
private bool? lastIsVisible;
public virtual bool IsVisible {
get => ResNode->Visible;
set {
ResNode->Visible = value;
if (lastIsVisible is null || lastIsVisible != value) {
OnVisibilityToggled?.Invoke(value);
lastIsVisible = value;
}
}
}
private Action<bool>? OnVisibilityToggled { get; set; }
public NodeFlags NodeFlags {
get => ResNode->NodeFlags;
set => ResNode->NodeFlags = value;
}
public virtual Vector4 Color {
get => ResNode->ColorVector;
set => ResNode->ColorVector = value;
}
public virtual ColorHelpers.HsvaColor ColorHsva {
get => ResNode->ColorHsva;
set => ResNode->ColorHsva = value;
}
public virtual float Alpha {
get => ResNode->Color.A;
set => ResNode->SetAlpha((byte)(value * 255.0f));
}
public virtual Vector3 AddColor {
get => ResNode->AddColor;
set => ResNode->AddColor = value;
}
public virtual ColorHelpers.HsvaColor AddColorHsva {
get => ResNode->AddColorHsva;
set => ResNode->AddColorHsva = value;
}
public virtual Vector3 MultiplyColor {
get => ResNode->MultiplyColor;
set => ResNode->MultiplyColor = value;
}
public virtual ColorHelpers.HsvaColor MultiplyColorHsva {
get => ResNode->MultiplyColorHsva;
set => ResNode->MultiplyColorHsva = value;
}
public uint NodeId {
get => ResNode->NodeId;
set => ResNode->NodeId = value;
}
public virtual DrawFlags DrawFlags {
get => (DrawFlags) ResNode->DrawFlags;
set => ResNode->DrawFlags = (uint) value & 0b1111_1111_1111_1100_0000_0011_1111_1111 |
ResNode->DrawFlags & 0b0000_0000_0000_0011_1111_1100_0000_0000;
}
public virtual int ClipCount {
get => (int)((ResNode->DrawFlags & 0b0000_0000_0000_0011_1111_1100_0000_0000) >> 10);
set => ResNode->DrawFlags = (uint)(value << 10 & 0b0000_0000_0000_0011_1111_1100_0000_0000)
| ResNode->DrawFlags & 0b1111_1111_1111_1100_0000_0011_1111_1111;
}
public void AddDrawFlags(params DrawFlags[] flags) {
foreach (var flag in flags) {
DrawFlags |= flag;
}
}
public void RemoveDrawFlags(params DrawFlags[] flags) {
foreach (var flag in flags) {
DrawFlags &= ~flag;
}
}
public int Priority {
get => ResNode->GetPriority();
set => ResNode->SetPriority((ushort)value);
}
protected virtual NodeType NodeType {
get => ResNode->GetNodeType();
set => ResNode->Type = value;
}
public virtual int ChildCount
=> ResNode->ChildCount;
protected virtual void OnSizeChanged() { }
public void AddNodeFlags(params NodeFlags[] flags) {
foreach (var flag in flags) {
NodeFlags |= flag;
}
}
public void RemoveNodeFlags(params NodeFlags[] flags) {
foreach (var flag in flags) {
NodeFlags &= ~flag;
}
}
public void MarkDirty() {
foreach (var child in GetAllChildren(this)) {
child.ResNode->AddDrawFlag( [ DrawFlags.IsDirty ] );
}
ResNode->AddDrawFlag([ DrawFlags.IsDirty ] );
}
public bool CheckCollision(short x, short y, bool inclusive = true)
=> ResNode->CheckCollision(x, y, inclusive);
public bool CheckCollision(Vector2 position, bool inclusive = true)
=> ResNode->CheckCollision((short) position.X, (short) position.Y, inclusive);
public bool CheckCollision(AtkEventData* eventData, bool inclusive = true)
=> ResNode->CheckCollision(eventData, inclusive);
public Matrix2x2 Transform {
get => ResNode->Transform;
set => ResNode->Transform = value;
}
}
+19
View File
@@ -0,0 +1,19 @@
using KamiToolKit.Timelines;
namespace KamiToolKit;
public abstract unsafe partial class NodeBase {
public Timeline? Timeline { get; private set; }
public void AddTimeline(Timeline timeline) {
Timeline?.Dispose();
Timeline = timeline;
ResNode->Timeline = timeline.InternalTimeline;
timeline.OwnerNode = ResNode;
}
public void AddTimeline(TimelineBuilder builder)
=> AddTimeline(builder.Build());
}
+151
View File
@@ -0,0 +1,151 @@
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Enums;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit;
public record InventoryItemTooltip(InventoryType Inventory, short Slot);
public unsafe partial class NodeBase {
private AtkTooltipManager.AtkTooltipType tooltipType = AtkTooltipManager.AtkTooltipType.None;
private bool tooltipEventsRegistered;
public virtual ReadOnlySeString TextTooltip {
get;
set {
field = value;
if (!value.IsEmpty) {
TryRegisterTooltipEvents();
tooltipType |= AtkTooltipManager.AtkTooltipType.Text;
}
else {
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Text;
}
}
}
public virtual uint ActionTooltip {
get;
set {
field = value;
if (value is not 0) {
TryRegisterTooltipEvents();
tooltipType |= AtkTooltipManager.AtkTooltipType.Action;
}
else {
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Action;
}
}
}
public virtual uint ItemTooltip {
get;
set {
field = value;
if (value is not 0) {
TryRegisterTooltipEvents();
tooltipType |= AtkTooltipManager.AtkTooltipType.Item;
}
else {
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Item;
}
}
}
public virtual InventoryItemTooltip? InventoryItemTooltip {
get;
set {
field = value;
if (value is not null) {
TryRegisterTooltipEvents();
tooltipType |= AtkTooltipManager.AtkTooltipType.Item;
}
else {
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Item;
}
}
}
private void TryRegisterTooltipEvents() {
if (tooltipEventsRegistered) return;
AddEvent(AtkEventType.MouseOver, ShowTooltip);
AddEvent(AtkEventType.MouseOut, HideTooltip);
OnVisibilityToggled += ToggleCollisionFlag;
ToggleCollisionFlag(IsVisible);
tooltipEventsRegistered = true;
}
private void UnregisterTooltipEvents() {
if (tooltipEventsRegistered) {
RemoveEvent(AtkEventType.MouseOver, ShowTooltip);
RemoveEvent(AtkEventType.MouseOut, HideTooltip);
OnVisibilityToggled -= ToggleCollisionFlag;
tooltipEventsRegistered = false;
}
}
private void ToggleCollisionFlag(bool isVisible) {
if (this is ComponentNode) return;
if (isVisible) {
AddNodeFlags(NodeFlags.HasCollision);
}
else {
RemoveNodeFlags(NodeFlags.HasCollision);
}
}
protected bool TooltipRegistered { get; set; }
public void ShowTooltip() {
if (ParentAddon is null) return; // Shouldn't be possible
if (tooltipType is AtkTooltipManager.AtkTooltipType.None) return;
using var stringBuilder = new RentedSeStringBuilder();
using var stringBuffer = new AtkValue();
if (!TextTooltip.IsEmpty) {
stringBuffer.SetManagedString(stringBuilder.Builder.Append(TextTooltip).GetViewAsSpan());
}
var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs();
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Text)) {
tooltipArgs.TextArgs.AtkArrayType = 0;
tooltipArgs.TextArgs.Text = stringBuffer.String;
}
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Action)) {
tooltipArgs.ActionArgs.Flags = 1;
tooltipArgs.ActionArgs.Kind = DetailKind.Action;
tooltipArgs.ActionArgs.Id = (int)ActionTooltip;
}
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Item) && InventoryItemTooltip is {} inventoryTooltip) {
tooltipArgs.ItemArgs.Kind = DetailKind.InventoryItem;
tooltipArgs.ItemArgs.InventoryType = inventoryTooltip.Inventory;
tooltipArgs.ItemArgs.Slot = inventoryTooltip.Slot;
tooltipArgs.ItemArgs.BuyQuantity = -1;
tooltipArgs.ItemArgs.Flag1 = 0;
}
else if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Item) && InventoryItemTooltip is null) {
tooltipArgs.ItemArgs.Kind = DetailKind.Item;
tooltipArgs.ItemArgs.ItemId = (int) ItemTooltip;
tooltipArgs.ItemArgs.BuyQuantity = -1;
tooltipArgs.ItemArgs.Flag1 = 0;
}
AtkStage.Instance()->TooltipManager.ShowTooltip(tooltipType, ParentAddon->Id, this, &tooltipArgs);
}
public void HideTooltip() {
if (ParentAddon is null) return;
AtkStage.Instance()->TooltipManager.HideTooltip(ParentAddon->Id);
}
}
+56
View File
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit;
public abstract unsafe class NodeBase<T> : NodeBase where T : unmanaged, ICreatable {
protected NodeBase(NodeType nodeType) {
if (MainThreadSafety.TryAssertMainThread()) return;
Log.Verbose($"Creating new node {GetType()}");
Node = NativeMemoryHelper.Create<T>();
if (ResNode is null) {
throw new Exception($"Unable to allocate memory for {typeof(T)}");
}
KamiToolKitLibrary.AllocatedNodes?.TryAdd((nint)Node, GetType());
BuildVirtualTable();
ResNode->Type = nodeType;
ResNode->NodeId = NodeIdBase + CurrentOffset++;
ResNode->ToggleVisibility(true);
CreatedNodes.Add(this);
}
public T* Node { get; private set; }
internal sealed override AtkResNode* ResNode => (AtkResNode*)Node;
public static implicit operator T*(NodeBase<T> node) => (T*) node.ResNode;
protected override void Dispose(bool disposing, bool isNativeDestructor) {
if (disposing) {
try {
base.Dispose(disposing, isNativeDestructor);
}
catch (Exception e) {
Log.Exception(e);
}
finally {
if (!isNativeDestructor) {
InvokeOriginalDestructor(ResNode, true);
}
KamiToolKitLibrary.AllocatedNodes?.Remove((nint)Node, out _);
Node = null;
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
using KamiToolKit.Classes;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
public sealed class AlphaImageNode : ImGuiImageNode {
public AlphaImageNode() {
TexturePath = DalamudInterface.Instance.GetAssetPath("alpha_background.png");
WrapMode = WrapMode.Tile;
}
}
@@ -0,0 +1,47 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public class AlternateCooldownNode : ResNode {
public readonly ImageNode CooldownImage;
public AlternateCooldownNode() {
CooldownImage = new ImageNode {
NodeId = 15,
Size = new Vector2(44.0f, 46.0f),
Position = new Vector2(0.0f, 2.0f),
Origin = new Vector2(22.0f, 23.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
WrapMode = WrapMode.Tile,
PartId = 80,
};
IconNodeTextureHelper.LoadIconARecast2Texture(CooldownImage);
CooldownImage.AttachNode(this);
BuildTimeline();
}
private void BuildTimeline() {
CooldownImage.AddTimeline(new TimelineBuilder()
.BeginFrameSet(11, 92)
.AddFrame(11, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 1)
.AddFrame(92, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 79)
.EndFrameSet()
.BeginFrameSet(93, 174)
.AddFrame(93, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 82)
.AddFrame(174, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 160)
.EndFrameSet()
.BeginFrameSet(175, 205)
.AddFrame(175, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 80)
.AddFrame(191, alpha: 255, scale: new Vector2(1.2f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(200.0f), partId: 80)
.AddFrame(205, alpha: 0, scale: new Vector2(1.25f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(200.0f), partId: 80)
.EndFrameSet()
.Build());
}
}
+36
View File
@@ -0,0 +1,36 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public class AntsNode : ResNode {
public readonly ImageNode AntsImageNode;
public AntsNode() {
AntsImageNode = new ImageNode {
NodeId = 13,
Size = new Vector2(48, 48),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
WrapMode = WrapMode.Tile,
PartId = 13,
};
IconNodeTextureHelper.LoadIconAFrameTexture(AntsImageNode);
AntsImageNode.AttachNode(this);
BuildTimeline();
}
private void BuildTimeline() {
AntsImageNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(2, 9)
.AddFrame(2, partId: 6)
.AddFrame(9, partId: 13)
.EndFrameSet()
.Build());
}
}
@@ -0,0 +1,26 @@
using System.Numerics;
using Dalamud.Interface;
namespace KamiToolKit.Nodes;
/// <summary>
/// A simple image node that makes it easy to display a single color.
/// </summary>
public unsafe class BackgroundImageNode : SimpleImageNode {
public BackgroundImageNode() {
FitTexture = true;
}
public new Vector4 Color {
get => new(AddColor.X, AddColor.Y, AddColor.Z, ResNode->Color.A / 255.0f);
set {
ResNode->Color = new Vector4(0.0f, 0.0f, 0.0f, value.W).ToByteColor();
AddColor = value.AsVector3Color();
}
}
public new ColorHelpers.HsvaColor ColorHsva {
get => ColorHelpers.RgbaToHsv(Color);
set => Color = ColorHelpers.HsvToRgb(value);
}
}
@@ -0,0 +1,24 @@
using System.Numerics;
using KamiToolKit.Classes;
namespace KamiToolKit.Nodes;
/// <summary>
/// A node that shows a border loaded from the party list textures
/// </summary>
public unsafe class BorderNineGridNode : NineGridNode {
public BorderNineGridNode() {
PartsList.Add(new Part {
TextureCoordinates = new Vector2(0.0f, 0.0f),
Size = new Vector2(64.0f, 64.0f),
Id = 0,
TexturePath = "ui/uld/PartyListTargetBase.tex",
});
TopOffset = 20;
LeftOffset = 20;
RightOffset = 20;
BottomOffset = 20;
PartsRenderType = 108;
}
}
@@ -0,0 +1,23 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Nodes;
// Simple helper class for making basic text label, node will auto-resize to fit label
public sealed class CategoryTextNode : TextNode {
public CategoryTextNode() {
Height = 16.0f;
TextFlags = TextFlags.AutoAdjustNodeSize;
TextColor = ColorHelper.GetColor(2);
TextOutlineColor = ColorHelper.GetColor(7);
FontType = FontType.Axis;
FontSize = 14;
LineSpacing = 24;
AlignmentType = AlignmentType.Left;
}
public override float Height {
get => base.Height;
set => base.Height = value + 8.0f; // Add extra height for padding
}
}

Some files were not shown because too many files have changed in this diff Show More