Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8db4ce6094 |
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.0.2.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
|
ref var item = ref blockedContainer->Items[i];
|
||||||
{
|
if (item.ItemId == 0) continue;
|
||||||
var container = inventoryManager->GetInventoryContainer(type);
|
|
||||||
if (container == null) return;
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +106,9 @@ public class Plugin : IDalamudPlugin
|
|||||||
System.Config = Util.LoadConfigOrDefault();
|
System.Config = Util.LoadConfigOrDefault();
|
||||||
System.IPC.UpdateUnifiedCategorySupport(System.Config.General.UseUnifiedExternalCategories);
|
System.IPC.UpdateUnifiedCategorySupport(System.Config.General.UseUnifiedExternalCategories);
|
||||||
System.LootedItemsTracker.Enable();
|
System.LootedItemsTracker.Enable();
|
||||||
|
|
||||||
|
System.AddonInventoryWindow.DebugOpen();
|
||||||
|
System.AddonConfigurationWindow.DebugOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLogout(int type, int code)
|
private void OnLogout(int type, int code)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/obj/
|
||||||
|
/bin/
|
||||||
|
/.idea/
|
||||||
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 737 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 754 B |
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace KamiToolKit.Classes;
|
||||||
|
|
||||||
|
internal class AddonConfig {
|
||||||
|
public Vector2 Position = Vector2.Zero;
|
||||||
|
public float Scale = 1.0f;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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}";
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace KamiToolKit.Enums;
|
||||||
|
|
||||||
|
public enum CounterFont {
|
||||||
|
MoneyFont,
|
||||||
|
ChocoboRace,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace KamiToolKit.Enums;
|
||||||
|
|
||||||
|
public enum HorizontalListAnchor {
|
||||||
|
[Description("Left")]
|
||||||
|
Left,
|
||||||
|
|
||||||
|
[Description("Right")]
|
||||||
|
Right,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace KamiToolKit.Enums;
|
||||||
|
|
||||||
|
public enum LayoutOrientation {
|
||||||
|
Vertical,
|
||||||
|
Horizontal,
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace KamiToolKit.Enums;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum NodeEditMode {
|
||||||
|
Resize = 1 << 1,
|
||||||
|
Move = 1 << 2,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace KamiToolKit.Enums;
|
||||||
|
|
||||||
|
internal enum ResizeDirection {
|
||||||
|
BottomRight,
|
||||||
|
BottomLeft,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace KamiToolKit.Enums;
|
||||||
|
|
||||||
|
public enum VerticalListAnchor {
|
||||||
|
[Description("Top")]
|
||||||
|
Top,
|
||||||
|
|
||||||
|
[Description("Bottom")]
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
@@ -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) };
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
global using KamiToolKit.Extensions;
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||