Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d42a6a7dc | |||
| 9e1e0970c9 | |||
| a9af3e77da | |||
| 17fe54dfea | |||
| 04c8005be1 | |||
| eb6e534630 | |||
| 267c811def | |||
| 96282c2a62 | |||
| 58df621611 | |||
| de4da5e61b | |||
| ac782aa5bb | |||
| 755a71813f | |||
| a453dd8dd6 | |||
| 5c141dba72 | |||
| 968a952a9a | |||
| 37b9d85313 | |||
| 1c0917c3c1 | |||
| 324b47bab5 | |||
| 723cb727ad | |||
| 26b3de8a1d | |||
| 0fee7d0954 | |||
| f074ca446f | |||
| f576af797e | |||
| 652d583747 | |||
| 2172ff0a51 | |||
| 5f6fa7ff2f | |||
| ad109210bb | |||
| dfa4ec08b5 | |||
| a51b1bec50 | |||
| c5a82d1c82 | |||
| cd7a4c610b | |||
| f15b585516 | |||
| d46d780778 | |||
| 29fbd4de61 | |||
| 50fec68a7d | |||
| 76a94b245a | |||
| 82b9d96335 | |||
| 6d15ee1b13 | |||
| 51934eb23d | |||
| d7babea111 | |||
| 955c837e85 | |||
| 665d3b62ba | |||
| fe7a8136af | |||
| 4e6523f8a0 | |||
| ac3b802799 | |||
| 9a188514d8 | |||
| 7fdb4fa43e | |||
| ac5e59d821 | |||
| 3d9d399b26 | |||
| d66142dfd2 | |||
| b9654dfb21 | |||
| 7cec19f5f2 | |||
| aec704dece | |||
| a7bfd7ea66 | |||
| 120fcf00cf | |||
| f87ec829b6 | |||
| 79a16b89eb | |||
| 3044e63106 | |||
| 6a4fc35c07 | |||
| ca2baf917e | |||
| 0ba5c0698e | |||
| b6084fb7e5 | |||
| 1dec5b3183 | |||
| c15ddc8342 | |||
| 200c430f8f | |||
| 6696f997d5 | |||
| f3b006b276 | |||
| 300fcee7ac | |||
| 5f73a47fee | |||
| acdd79f73a | |||
| 0d232e1fa0 | |||
| 9b05087b72 | |||
| cc042f0349 | |||
| 7310825ca9 | |||
| b8bb1b2fd7 | |||
| 3104c84d20 | |||
| c394e3e3f1 | |||
| a6b17b657e | |||
| dc1dbbfbcb | |||
| 47d5aa3bd6 | |||
| 9f1fb7f884 | |||
| 17a67b8c12 | |||
| d74447201b | |||
| 399c80d405 | |||
| 2d254701d7 | |||
| c3b61e8045 | |||
| 2f7977d144 | |||
| 6be6a2c09d | |||
| aeb4fdcb9d | |||
| 64d8cb8370 | |||
| e46be61b1c | |||
| 98742482e3 | |||
| 75f278c945 | |||
| 9b7e99276e | |||
| 7c8ae6b87b | |||
| 325a6a92f5 | |||
| ab0692299b | |||
| 048ac54994 | |||
| 09f8201a7b | |||
| 9c68149d74 | |||
| fc12b41f33 | |||
| c40eb29238 | |||
| 31404c3e0a | |||
| 0eb1adc9c0 | |||
| c3e3f8b2bf | |||
| adfe3dc961 | |||
| 3f335696b7 | |||
| e2a227a23c | |||
| 67bd995329 | |||
| 3ef8cb7076 | |||
| 45ebc2dd24 | |||
| a0fb7f5103 | |||
| 9dfa0ec7aa | |||
| aef50e39d3 | |||
| 5be6f99743 | |||
| 066dd8d2fa | |||
| 93d3b4cd6b | |||
| a4cea4b19d | |||
| 6784436dfe | |||
| 97d1e7164d | |||
| 4ffc59842b | |||
| bb29522fed | |||
| aedcc7953c | |||
| 5381c1f794 | |||
| 20a2b0a819 | |||
| de5079644d | |||
| 81bef991ab | |||
| ee33365b9d | |||
| 73d8411af2 | |||
| f5da655ee3 | |||
| 0fb54faa41 | |||
| 10c365828f | |||
| b4733a9537 | |||
| 3e64927fff | |||
| bc9a7b9122 | |||
| 6d70495cf2 | |||
| 5bf5e50a38 | |||
| fff0578b74 | |||
| fec004ffce | |||
| c198663184 | |||
| 5561305af8 | |||
| bada2bdc8a | |||
| 16f9311a13 | |||
| 50eb28c3e0 | |||
| f8fbd4a476 | |||
| f5044e707b | |||
| fd229729f6 | |||
| 55ad494bb5 | |||
| 5e99ae8a2a | |||
| e1483939c7 | |||
| 9fe450ec81 | |||
| ced0e5ef22 | |||
| 4d3125d32e | |||
| 00f3c898e6 | |||
| 29d59c9743 | |||
| 8a496ae6f1 | |||
| 2752e8d930 | |||
| 167761244a | |||
| 819e1fecfd | |||
| 205b87e58d | |||
| ce27ade807 | |||
| d18f4483bb | |||
| 592fa392bf | |||
| 659c295c16 | |||
| e59da8ab0b |
@@ -25,7 +25,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
{
|
||||
InitializeBackgroundDropTarget();
|
||||
|
||||
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
ScrollableCategories = new InventoryScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
@@ -179,6 +179,9 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
|
||||
protected override void OnFinalize(AtkUnitBase* addon)
|
||||
{
|
||||
IsSetupComplete = false;
|
||||
_lootedCategoryNode?.Dispose();
|
||||
|
||||
System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged;
|
||||
|
||||
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
|
||||
@@ -189,8 +192,6 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
|
||||
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||
|
||||
_lootedCategoryNode?.Dispose();
|
||||
|
||||
IsSetupComplete = false;
|
||||
base.OnFinalize(addon);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
||||
|
||||
WindowNode?.AddColor = _tintColor;
|
||||
|
||||
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
ScrollableCategories = new InventoryScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
|
||||
@@ -31,7 +31,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
||||
|
||||
WindowNode?.AddColor = _tintColor;
|
||||
|
||||
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
ScrollableCategories = new InventoryScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
|
||||
@@ -29,7 +29,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
|
||||
|
||||
protected DragDropNode BackgroundDropTarget = null!;
|
||||
protected ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = null!;
|
||||
protected InventoryScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = null!;
|
||||
protected WrappingGridNode<InventoryCategoryNodeBase> CategoriesNode = null!;
|
||||
protected TextInputWithButtonNode SearchInputNode = null!;
|
||||
protected InventoryFooterNode FooterNode = null!;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||
<PropertyGroup>
|
||||
<Version>1.0.0.0</Version>
|
||||
<Version>1.0.2.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -120,15 +120,32 @@ public static unsafe class InventoryContextState
|
||||
var inventoryManager = InventoryManager.Instance();
|
||||
if (inventoryManager == null) return;
|
||||
|
||||
var blockedContainer = inventoryManager->GetInventoryContainer(InventoryType.BlockedItems);
|
||||
if (blockedContainer == null) return;
|
||||
ScanBlockedContainer(inventoryManager, InventoryType.BlockedItems);
|
||||
ScanBlockedContainer(inventoryManager, InventoryType.MailEdit);
|
||||
}
|
||||
|
||||
for (int i = 0; i < blockedContainer->Size; i++)
|
||||
private static void ScanBlockedContainer(InventoryManager* inventoryManager, InventoryType type)
|
||||
{
|
||||
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;
|
||||
|
||||
BlockedSlots.Add((item.Container, item.Slot));
|
||||
for (int i = 0; i < container->GetSize(); i++)
|
||||
{
|
||||
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,6 +28,7 @@ public static unsafe class DragDropState
|
||||
|
||||
public class InventoryMonitor : IDisposable
|
||||
{
|
||||
|
||||
public InventoryMonitor()
|
||||
{
|
||||
var bags = new[] { "Inventory", "InventoryLarge", "InventoryExpansion" };
|
||||
@@ -41,8 +42,6 @@ public class InventoryMonitor : IDisposable
|
||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize);
|
||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, bags, OnInventoryPreFinalize);
|
||||
|
||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreHide, bags, OnInventoryPreHide);
|
||||
|
||||
// PreRefresh Handlers
|
||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, bags, InventoryPreRefreshHandler);
|
||||
|
||||
@@ -72,14 +71,6 @@ public class InventoryMonitor : IDisposable
|
||||
System.AddonInventoryWindow.Close();
|
||||
}
|
||||
|
||||
private void OnInventoryPreHide(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
if (System.Config.General.OpenWithGameInventory)
|
||||
{
|
||||
System.AddonInventoryWindow.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void OpenInventories(string name)
|
||||
{
|
||||
GeneralSettings config = System.Config.General;
|
||||
@@ -201,10 +192,14 @@ public class InventoryMonitor : IDisposable
|
||||
|
||||
if (config.OpenWithGameInventory)
|
||||
{
|
||||
var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(args.AddonName);
|
||||
bool isCurrentlyVisible = addon != null && addon->IsVisible;
|
||||
AtkValue* value1 = (AtkValue*)atkValues[1].Address;
|
||||
int openTitleId = value1->Int;
|
||||
|
||||
if (!isCurrentlyVisible)
|
||||
if (openTitleId == 0)
|
||||
{
|
||||
System.AddonInventoryWindow.Toggle();
|
||||
}
|
||||
else
|
||||
{
|
||||
System.AddonInventoryWindow.Open();
|
||||
}
|
||||
@@ -250,6 +245,6 @@ public class InventoryMonitor : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw;
|
||||
Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate, OnInventoryPreFinalize, OnInventoryPreHide, InventoryPreRefreshHandler);
|
||||
Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate, OnInventoryPreFinalize, InventoryPreRefreshHandler);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,30 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
namespace AetherBags.Nodes.Layout;
|
||||
|
||||
public unsafe class ScrollingAreaNode<T> : SimpleComponentNode where T : NodeBase, new() {
|
||||
/// <summary>
|
||||
/// A copy of KamiToolKit's ScrollingAreaNode with ContentAreaClipNode changed from
|
||||
/// SimpleComponentNode to ResNode, to prevent the native AtkDragDropManager from
|
||||
/// treating the clip boundary as a component node (which blocks drag-to-hotbar).
|
||||
/// According to Kami it is possible that this may or may not leak.
|
||||
/// We will eventually change this later.
|
||||
/// </summary>
|
||||
public unsafe class InventoryScrollingAreaNode<T> : SimpleComponentNode where T : NodeBase, new() {
|
||||
|
||||
public readonly SimpleComponentNode ContentAreaClipNode;
|
||||
public readonly ResNode ContentAreaClipNode;
|
||||
public readonly T ContentAreaNode;
|
||||
public readonly ScrollBarNode ScrollBarNode;
|
||||
public readonly CollisionNode ScrollingCollisionNode;
|
||||
|
||||
public ScrollingAreaNode() {
|
||||
public InventoryScrollingAreaNode() {
|
||||
ScrollingCollisionNode = new CollisionNode();
|
||||
ScrollingCollisionNode.AttachNode(this);
|
||||
|
||||
ContentAreaClipNode = new SimpleComponentNode {
|
||||
ContentAreaClipNode = new ResNode {
|
||||
NodeFlags = NodeFlags.Clip | NodeFlags.EmitsEvents | NodeFlags.Visible,
|
||||
};
|
||||
ContentAreaClipNode.AttachNode(this);
|
||||
@@ -30,7 +39,10 @@ public unsafe class ScrollingAreaNode<T> : SimpleComponentNode where T : NodeBas
|
||||
};
|
||||
ScrollBarNode.AttachNode(this);
|
||||
|
||||
ContentAreaClipNode.ResNode->AtkEventManager.RegisterEvent(
|
||||
AtkResNode* clipNode = ContentAreaClipNode;
|
||||
AtkResNode* contentNode = ContentAreaNode;
|
||||
|
||||
clipNode->AtkEventManager.RegisterEvent(
|
||||
AtkEventType.MouseWheel,
|
||||
5,
|
||||
null,
|
||||
@@ -38,7 +50,7 @@ public unsafe class ScrollingAreaNode<T> : SimpleComponentNode where T : NodeBas
|
||||
ScrollBarNode,
|
||||
false);
|
||||
|
||||
ScrollingCollisionNode.ResNode->AtkEventManager.RegisterEvent(
|
||||
ScrollingCollisionNode.Node->AtkEventManager.RegisterEvent(
|
||||
AtkEventType.MouseWheel,
|
||||
5,
|
||||
null,
|
||||
@@ -46,7 +58,7 @@ public unsafe class ScrollingAreaNode<T> : SimpleComponentNode where T : NodeBas
|
||||
ScrollBarNode,
|
||||
false);
|
||||
|
||||
ContentAreaNode.ResNode->AtkEventManager.RegisterEvent(
|
||||
contentNode->AtkEventManager.RegisterEvent(
|
||||
AtkEventType.MouseWheel,
|
||||
5,
|
||||
null,
|
||||
@@ -106,9 +106,6 @@ public class Plugin : IDalamudPlugin
|
||||
System.Config = Util.LoadConfigOrDefault();
|
||||
System.IPC.UpdateUnifiedCategorySupport(System.Config.General.UseUnifiedExternalCategories);
|
||||
System.LootedItemsTracker.Enable();
|
||||
|
||||
System.AddonInventoryWindow.DebugOpen();
|
||||
System.AddonConfigurationWindow.DebugOpen();
|
||||
}
|
||||
|
||||
private void OnLogout(int type, int code)
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
/obj/
|
||||
/bin/
|
||||
/.idea/
|
||||
|
Before Width: | Height: | Size: 393 B |
|
Before Width: | Height: | Size: 396 B |
|
Before Width: | Height: | Size: 825 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 737 B |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 754 B |
@@ -1,8 +0,0 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
internal class AddonConfig {
|
||||
public Vector2 Position = Vector2.Zero;
|
||||
public float Scale = 1.0f;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using System;
|
||||
using KamiToolKit.Premade.Color;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
internal readonly struct BatchToken(ColorPickerWidget owner) : IDisposable {
|
||||
public void Dispose() => owner.EndBatchUpdate();
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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}";
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public record TabbedNodeEntry<T>(T Node, int Tab) where T : NodeBase;
|
||||
@@ -1,26 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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());
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum CounterFont {
|
||||
MoneyFont,
|
||||
ChocoboRace,
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum HorizontalListAnchor {
|
||||
[Description("Left")]
|
||||
Left,
|
||||
|
||||
[Description("Right")]
|
||||
Right,
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum LayoutOrientation {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
[Flags]
|
||||
public enum NodeEditMode {
|
||||
Resize = 1 << 1,
|
||||
Move = 1 << 2,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
internal enum OverlayAddonState {
|
||||
None,
|
||||
WaitForReady,
|
||||
Ready,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
internal enum ControllerState {
|
||||
WaitForNameplate,
|
||||
WaitForReady,
|
||||
Ready,
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
internal enum ResizeDirection {
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum VerticalListAlignment {
|
||||
[Description("Left")]
|
||||
Left,
|
||||
|
||||
[Description("Right")]
|
||||
Right,
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum VerticalListAnchor {
|
||||
[Description("Top")]
|
||||
Top,
|
||||
|
||||
[Description("Bottom")]
|
||||
Bottom,
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum WrapMode {
|
||||
None = 0,
|
||||
Tile = 1,
|
||||
Stretch = 2,
|
||||
TileMirrored = 3,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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) };
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static class ReadOnlySpanExtensions {
|
||||
extension(ReadOnlySpan<byte> span) {
|
||||
public string String => Encoding.UTF8.GetString(span);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static class StopwatchExtensions {
|
||||
extension(Stopwatch stopwatch) {
|
||||
public void LogTime(string logMessage) {
|
||||
DalamudInterface.Instance.Log.Debug($"{logMessage, -15}: {stopwatch, 15} :: {stopwatch.ElapsedMilliseconds} ms");
|
||||
stopwatch.Restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
global using KamiToolKit.Extensions;
|
||||
@@ -1,32 +0,0 @@
|
||||
<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>
|
||||
@@ -1,32 +0,0 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
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
|
||||
@@ -1,66 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,61 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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());
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||