Initial commit: AetherBags + KamiToolKit for FC Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -0,0 +1,108 @@
|
||||
root = true
|
||||
# top-most EditorConfig file
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# 4 space indentation
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Microsoft .NET properties
|
||||
csharp_indent_braces = false
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = false
|
||||
csharp_new_line_before_open_brace = none
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
dotnet_code_quality_unused_parameters = non_public
|
||||
dotnet_naming_rule.event_rule.severity = warning
|
||||
dotnet_naming_rule.event_rule.style = on_upper_camel_case_style
|
||||
dotnet_naming_rule.event_rule.symbols = event_symbols
|
||||
dotnet_naming_rule.private_constants_rule.severity = warning
|
||||
dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style
|
||||
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
|
||||
dotnet_naming_rule.private_instance_fields_rule.severity = warning
|
||||
dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
|
||||
dotnet_naming_rule.private_static_fields_rule.severity = warning
|
||||
dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
|
||||
dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
|
||||
dotnet_naming_rule.private_static_readonly_rule.severity = warning
|
||||
dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style
|
||||
dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
|
||||
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
|
||||
dotnet_naming_style.on_upper_camel_case_style.capitalization = pascal_case
|
||||
dotnet_naming_style.on_upper_camel_case_style.required_prefix = On
|
||||
dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
|
||||
dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.event_symbols.applicable_kinds = event
|
||||
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
|
||||
dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
|
||||
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
dotnet_style_parentheses_in_other_operators = always_for_clarity:silent
|
||||
dotnet_style_object_initializer = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = none
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# ReSharper properties
|
||||
resharper_align_linq_query = true
|
||||
resharper_align_multiline_argument = true
|
||||
resharper_csharp_align_multiline_argument = false
|
||||
resharper_csharp_align_multiline_calls_chain = false
|
||||
resharper_align_multiline_expression = true
|
||||
resharper_align_multiline_extends_list = true
|
||||
resharper_align_multiline_for_stmt = true
|
||||
resharper_align_multline_type_parameter_constrains = true
|
||||
resharper_align_multline_type_parameter_list = true
|
||||
resharper_braces_for_ifelse = required_for_multiline
|
||||
resharper_can_use_global_alias = false
|
||||
resharper_csharp_align_multiline_parameter = false
|
||||
resharper_csharp_align_multiple_declaration = true
|
||||
resharper_csharp_allow_comment_after_lbrace = true
|
||||
resharper_csharp_empty_block_style = together
|
||||
resharper_csharp_int_align_comments = true
|
||||
resharper_csharp_new_line_before_while = true
|
||||
resharper_csharp_stick_comment = false
|
||||
resharper_csharp_wrap_after_declaration_lpar = true
|
||||
resharper_indent_preprocessor_region = no_indent
|
||||
resharper_new_line_before_finally = false
|
||||
resharper_place_accessorholder_attribute_on_same_line = false
|
||||
resharper_place_field_attribute_on_same_line = false
|
||||
|
||||
# ReSharper inspection severities
|
||||
csharp_style_deconstructed_variable_declaration = true:silent
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
/obj/
|
||||
/bin/
|
||||
/.idea/
|
||||
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 737 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 754 B |
@@ -0,0 +1,8 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
internal class AddonConfig {
|
||||
public Vector2 Position = Vector2.Zero;
|
||||
public float Scale = 1.0f;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
using KamiToolKit.Premade.Color;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
internal readonly struct BatchToken(ColorPickerWidget owner) : IDisposable {
|
||||
public void Dispose() => owner.EndBatchUpdate();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public class Bounds {
|
||||
public required Vector2 TopLeft { get; set; }
|
||||
public required Vector2 BottomRight { get; set; }
|
||||
|
||||
public float Top => TopLeft.Y;
|
||||
public float Left => TopLeft.X;
|
||||
public float Bottom => BottomRight.Y;
|
||||
public float Right => BottomRight.X;
|
||||
|
||||
public float Width => BottomRight.X - TopLeft.X;
|
||||
public float Height => BottomRight.Y - TopLeft.Y;
|
||||
public Vector2 Size => new(Width, Height);
|
||||
|
||||
public float CenterX => (TopLeft.X + BottomRight.X) / 2.0f;
|
||||
public float CenterY => (TopLeft.Y + BottomRight.Y) / 2.0f;
|
||||
public Vector2 Center => new(CenterX, CenterY);
|
||||
|
||||
public override string ToString() => $"{TopLeft}, {BottomRight}";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public static unsafe class ColorHelper {
|
||||
public static Vector4 GetColor(uint colorId)
|
||||
=> ConvertToVector4(AtkStage.Instance()->AtkUIColorHolder->GetColor(true, colorId));
|
||||
|
||||
private static Vector4 ConvertToVector4(uint color) {
|
||||
var a = (byte)(color >> 24);
|
||||
var b = (byte)(color >> 16);
|
||||
var g = (byte)(color >> 8);
|
||||
var r = (byte)color;
|
||||
|
||||
return new Vector4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using static FFXIVClientStructs.FFXIV.Component.GUI.AtkModuleInterface;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public unsafe class CustomEventInterface : IDisposable {
|
||||
|
||||
private readonly AtkEventInterface* eventInterface;
|
||||
|
||||
private AtkEventInterface.Delegates.ReceiveEvent? receiveEventDelegate;
|
||||
private AtkEventInterface.Delegates.ReceiveEventWithResult? receiveEventWithResultDelegate;
|
||||
|
||||
public CustomEventInterface(AtkEventInterface.Delegates.ReceiveEvent eventHandler, AtkEventInterface.Delegates.ReceiveEventWithResult? receiveEventWithResult = null) {
|
||||
receiveEventDelegate = eventHandler;
|
||||
receiveEventWithResultDelegate = receiveEventWithResult;
|
||||
|
||||
eventInterface = NativeMemoryHelper.UiAlloc<AtkEventInterface>();
|
||||
eventInterface->VirtualTable = (AtkEventInterface.AtkEventInterfaceVirtualTable*)NativeMemoryHelper.Malloc((ulong)sizeof(void*) * 2);
|
||||
eventInterface->VirtualTable->ReceiveEvent = (delegate* unmanaged<AtkEventInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(receiveEventDelegate);
|
||||
|
||||
if (receiveEventWithResultDelegate is not null) {
|
||||
eventInterface->VirtualTable->ReceiveEventWithResult = (delegate* unmanaged<AtkEventInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(receiveEventWithResultDelegate);
|
||||
}
|
||||
else {
|
||||
eventInterface->VirtualTable->ReceiveEventWithResult = (delegate* unmanaged<AtkEventInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)(delegate* unmanaged<void>)&NullSub;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (eventInterface is null) return;
|
||||
|
||||
NativeMemoryHelper.Free(eventInterface->VirtualTable, (ulong)sizeof(void*) * 2);
|
||||
NativeMemoryHelper.UiFree(eventInterface);
|
||||
|
||||
receiveEventDelegate = null;
|
||||
receiveEventWithResultDelegate = null;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly] private static void NullSub() { }
|
||||
|
||||
public static implicit operator AtkEventInterface*(CustomEventInterface listener) => listener.eventInterface;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public unsafe class CustomEventListener : IDisposable {
|
||||
|
||||
private readonly AtkEventListener* eventListener;
|
||||
|
||||
private AtkEventListener.Delegates.ReceiveEvent? receiveEventDelegate;
|
||||
|
||||
public CustomEventListener(AtkEventListener.Delegates.ReceiveEvent eventHandler) {
|
||||
receiveEventDelegate = eventHandler;
|
||||
|
||||
eventListener = NativeMemoryHelper.UiAlloc<AtkEventListener>();
|
||||
eventListener->VirtualTable = (AtkEventListener.AtkEventListenerVirtualTable*)NativeMemoryHelper.Malloc((ulong)sizeof(void*) * 3);
|
||||
eventListener->VirtualTable->Dtor = (delegate* unmanaged<AtkEventListener*, byte, AtkEventListener*>)(delegate* unmanaged<void>)&NullSub;
|
||||
eventListener->VirtualTable->ReceiveGlobalEvent = (delegate* unmanaged<AtkEventListener*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)(delegate* unmanaged<void>)&NullSub;
|
||||
eventListener->VirtualTable->ReceiveEvent = (delegate* unmanaged<AtkEventListener*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(receiveEventDelegate);
|
||||
}
|
||||
|
||||
public virtual void Dispose() {
|
||||
if (eventListener is null) return;
|
||||
|
||||
NativeMemoryHelper.Free(eventListener->VirtualTable, (ulong)sizeof(void*) * 3);
|
||||
NativeMemoryHelper.UiFree(eventListener);
|
||||
|
||||
receiveEventDelegate = null;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly] private static void NullSub() { }
|
||||
|
||||
public static implicit operator AtkEventListener*(CustomEventListener listener) => listener.eventListener;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
internal class DalamudInterface {
|
||||
|
||||
private static DalamudInterface? instance;
|
||||
public static DalamudInterface Instance => instance ??= new DalamudInterface();
|
||||
|
||||
[PluginService] public IPluginLog Log { get; set; } = null!;
|
||||
[PluginService] public IAddonLifecycle AddonLifecycle { get; set; } = null!;
|
||||
[PluginService] public IDataManager DataManager { get; set; } = null!;
|
||||
[PluginService] public ITextureProvider TextureProvider { get; set; } = null!;
|
||||
[PluginService] public IFramework Framework { get; set; } = null!;
|
||||
[PluginService] public IAddonEventManager AddonEventManager { get; set; } = null!;
|
||||
[PluginService] public IDalamudPluginInterface PluginInterface { get; set; } = null!;
|
||||
[PluginService] public IGameGui GameGui { get; set; } = null!;
|
||||
[PluginService] public IGameInteropProvider GameInteropProvider { get; set; } = null!;
|
||||
[PluginService] public ISeStringEvaluator SeStringEvaluator { get; set; } = null!;
|
||||
|
||||
private DalamudInterface() {
|
||||
if (!KamiToolKitLibrary.IsInitialized)
|
||||
throw new Exception("KamiToolKit not initialized! You must call KamiToolKitLibrary.Initialize() before using KamiToolKit.\n" +
|
||||
"Don't forget to call KamiToolKitLibrary.Dispose() in your plugins dispose to ensure all assets are freed and to trigger bad practice warnings.");
|
||||
}
|
||||
|
||||
public string GetAssetDirectoryPath()
|
||||
=> Path.Combine(PluginInterface.AssemblyLocation.DirectoryName ?? throw new Exception("Directory from Dalamud is Invalid Somehow"), "Assets");
|
||||
|
||||
public string GetAssetPath(string assetName)
|
||||
=> Path.Combine(GetAssetDirectoryPath(), assetName);
|
||||
|
||||
public IDalamudTextureWrap? LoadAsset(string assetName)
|
||||
=> TextureProvider.GetFromFile(GetAssetPath(assetName)).GetWrapOrDefault();
|
||||
}
|
||||
|
||||
internal static class Log {
|
||||
|
||||
private static readonly bool ExcessiveLogging = false;
|
||||
|
||||
internal static void Debug(string message) {
|
||||
DalamudInterface.Instance.Log.Debug($"[KamiToolKit] {message}");
|
||||
}
|
||||
|
||||
internal static void Fatal(string message) {
|
||||
DalamudInterface.Instance.Log.Fatal($"[KamiToolKit] {message}");
|
||||
}
|
||||
|
||||
internal static void Warning(string message) {
|
||||
DalamudInterface.Instance.Log.Warning($"[KamiToolKit] {message}");
|
||||
}
|
||||
|
||||
internal static void Verbose(string message) {
|
||||
DalamudInterface.Instance.Log.Verbose($"[KamiToolKit] {message}");
|
||||
}
|
||||
|
||||
internal static void Excessive(string message) {
|
||||
if (ExcessiveLogging) {
|
||||
Verbose($"[KamiToolKit] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Error(string message) {
|
||||
DalamudInterface.Instance.Log.Error($"[KamiToolKit] {message}");
|
||||
}
|
||||
|
||||
internal static void Exception(Exception exception, [CallerMemberName] string? callerName = null) {
|
||||
DalamudInterface.Instance.Log.Error(exception, $"Exception in {callerName}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Lumina.Text;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public unsafe class DragDropPayload {
|
||||
|
||||
public DragDropType Type { get; set; } = DragDropType.Nothing;
|
||||
|
||||
public short ReferenceIndex { get; set; }
|
||||
|
||||
/// <remarks> Index (like AtkDragDropInterface.ReferenceIndex), InventoryType, etc. </remarks>
|
||||
public int Int1 { get; set; }
|
||||
|
||||
/// <remarks> ActionId, ItemId, EmoteId, InventorySlotIndex, ListIndex, MacroIndex etc. </remarks>
|
||||
public int Int2 { get; set; } = -1;
|
||||
|
||||
// unknown usage
|
||||
// public ulong Unk8 { get; set; }
|
||||
|
||||
// unknown usage
|
||||
// public AtkValue* AtkValue { get; set; }
|
||||
|
||||
public ReadOnlySeString Text { get; set; }
|
||||
|
||||
// unknown usage
|
||||
// public uint Flags { get; set; }
|
||||
|
||||
public static DragDropPayload FromDragDropInterface(AtkDragDropInterface* dragDropInterface) {
|
||||
var payloadContainer = dragDropInterface->GetPayloadContainer();
|
||||
|
||||
return new DragDropPayload {
|
||||
Type = dragDropInterface->DragDropType,
|
||||
ReferenceIndex = dragDropInterface->DragDropReferenceIndex,
|
||||
Int1 = payloadContainer->Int1,
|
||||
Int2 = payloadContainer->Int2,
|
||||
Text = new ReadOnlySeString(payloadContainer->Text),
|
||||
};
|
||||
}
|
||||
|
||||
public void ToDragDropInterface(AtkDragDropInterface* dragDropInterface, bool writeToPayloadContainer = true) {
|
||||
dragDropInterface->DragDropType = Type;
|
||||
dragDropInterface->DragDropReferenceIndex = ReferenceIndex;
|
||||
|
||||
if (writeToPayloadContainer) {
|
||||
var payloadContainer = dragDropInterface->GetPayloadContainer();
|
||||
payloadContainer->Clear();
|
||||
payloadContainer->Int1 = Int1;
|
||||
payloadContainer->Int2 = Int2;
|
||||
|
||||
if (Text.IsEmpty) {
|
||||
payloadContainer->Text.Clear();
|
||||
}
|
||||
else {
|
||||
var stringBuilder = new SeStringBuilder().Append(Text);
|
||||
payloadContainer->Text.SetString(stringBuilder.GetViewAsSpan());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
Type = DragDropType.Nothing;
|
||||
ReferenceIndex = 0;
|
||||
Int1 = 0;
|
||||
Int2 = -1;
|
||||
Text = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Diagnostics;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
/// WARNING: These features are potentially extremely volatile, use at your own risk.
|
||||
public unsafe class Experimental {
|
||||
private static Experimental? instance;
|
||||
public static Experimental Instance => instance ??= new Experimental();
|
||||
|
||||
public void EnableHooks() { }
|
||||
|
||||
public void DisposeHooks() {
|
||||
}
|
||||
|
||||
// WARNING: May result in undefined state or accidental network requests
|
||||
// Use at your own risk.
|
||||
[Conditional("DEBUG")]
|
||||
public static void ForceOpenAddon(AgentId agentId, int delayTicks = 0) {
|
||||
if (delayTicks is not 0) {
|
||||
DalamudInterface.Instance.Framework.RunOnTick(() => {
|
||||
AgentModule.Instance()->GetAgentByInternalId(agentId)->Show();
|
||||
}, delayTicks: delayTicks);
|
||||
}
|
||||
else {
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
AgentModule.Instance()->GetAgentByInternalId(agentId)->Show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING: May result in undefined state or accidental network requests
|
||||
// Use at your own risk.
|
||||
[Conditional("DEBUG")]
|
||||
public static void ForceCloseAddon(AgentId agentId)
|
||||
=> DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
AgentModule.Instance()->GetAgentByInternalId(agentId)->Hide();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public static class FlagHelper {
|
||||
public static bool ReadFlag<T>(ref T flagsField, int flag) where T : struct, IBinaryInteger<T>
|
||||
=> (flagsField & T.One << BitOperations.Log2((uint)flag)) != T.Zero;
|
||||
|
||||
public static void SetFlag<T>(ref T flagsField, int flag) where T : struct, IBinaryInteger<T>
|
||||
=> flagsField |= T.One << BitOperations.Log2((uint)flag);
|
||||
|
||||
public static void ClearFlag<T>(ref T flagsField, int flag) where T : struct, IBinaryInteger<T>
|
||||
=> flagsField &= ~(T.One << BitOperations.Log2((uint)flag));
|
||||
|
||||
public static void UpdateFlag<T>(ref T flagsField, int flag, bool enable) where T : struct, IBinaryInteger<T> {
|
||||
if (enable) {
|
||||
SetFlag(ref flagsField, flag);
|
||||
}
|
||||
else {
|
||||
ClearFlag(ref flagsField, flag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
internal static class GenericUtil {
|
||||
public static bool AreEqual<T>(T? left, T? right) {
|
||||
if (default(T) == null) return ReferenceEquals(left, right);
|
||||
|
||||
if (left == null || right == null) return left == null && right == null;
|
||||
|
||||
var leftSpan = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref left), Unsafe.SizeOf<T>());
|
||||
var rightSpan = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref right), Unsafe.SizeOf<T>());
|
||||
|
||||
return leftSpan.SequenceEqual(rightSpan);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using ListItemInfo = FFXIVClientStructs.FFXIV.Component.GUI.AtkComponentListItemPopulator.ListItemInfo;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public unsafe class ListPopulatorData {
|
||||
public AtkUnitBase* Addon { get; init; }
|
||||
public ListItemInfo* ItemInfo { get; init; }
|
||||
public AtkResNode** NodeList { get; init; }
|
||||
public uint Index { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
internal static class NativeMemoryHelper {
|
||||
public static unsafe T* UiAlloc<T>(int elementCount, ulong alignment = 8) where T : unmanaged
|
||||
=> UiAlloc<T>((uint)elementCount, alignment);
|
||||
|
||||
public static unsafe T* UiAlloc<T>(uint elementCount = 1, ulong alignment = 8) where T : unmanaged {
|
||||
var allocSize = (ulong)sizeof(T) * elementCount;
|
||||
var memory = (T*)IMemorySpace.GetUISpace()->Malloc(allocSize, alignment);
|
||||
|
||||
IMemorySpace.Memset(memory, 0, allocSize);
|
||||
|
||||
if (memory is null) {
|
||||
throw new Exception($"Unable to allocate memory for {typeof(T)}");
|
||||
}
|
||||
|
||||
return memory;
|
||||
}
|
||||
|
||||
public static unsafe void UiFree<T>(T* memory) where T : unmanaged
|
||||
=> IMemorySpace.Free(memory);
|
||||
|
||||
public static unsafe void UiFree<T>(T* memory, uint elementCount) where T : unmanaged
|
||||
=> IMemorySpace.Free(memory, (ulong)sizeof(T) * elementCount);
|
||||
|
||||
public static unsafe T* Create<T>() where T : unmanaged, ICreatable {
|
||||
var memory = IMemorySpace.GetUISpace()->Create<T>();
|
||||
|
||||
if (memory is null) {
|
||||
throw new Exception($"Unable to allocate memory for {typeof(T)}");
|
||||
}
|
||||
|
||||
return memory;
|
||||
}
|
||||
|
||||
public static unsafe nint Malloc(ulong size, ulong alignment = 8)
|
||||
=> (nint)IMemorySpace.GetUISpace()->Malloc(size, alignment);
|
||||
|
||||
public static unsafe void Free(void* memory, ulong size)
|
||||
=> IMemorySpace.Free(memory, size);
|
||||
|
||||
public static unsafe void ResizeArray<T>(ref T* array, int oldSize, uint newSize) where T : unmanaged
|
||||
=> ResizeArray(ref array, oldSize, (int)newSize);
|
||||
|
||||
public static unsafe void ResizeArray<T>(ref T* array, uint oldSize, uint newSize) where T : unmanaged
|
||||
=> ResizeArray(ref array, (int)oldSize, (int)newSize);
|
||||
|
||||
public static unsafe void ResizeArray<T>(ref T* array, uint oldSize, int newSize) where T : unmanaged
|
||||
=> ResizeArray(ref array, (int)oldSize, newSize);
|
||||
|
||||
public static unsafe void ResizeArray<T>(ref T* array, int oldSize, int newSize) where T : unmanaged {
|
||||
var newBuffer = UiAlloc<T>((uint)newSize);
|
||||
|
||||
Copy(array, newBuffer, oldSize);
|
||||
|
||||
if (array is not null) {
|
||||
UiFree(array, (uint)oldSize);
|
||||
}
|
||||
|
||||
array = newBuffer;
|
||||
}
|
||||
|
||||
public static unsafe void Copy<T>(T* oldBuffer, T* newBuffer, int count) where T : unmanaged
|
||||
=> Copy(oldBuffer, newBuffer, (uint)count);
|
||||
|
||||
public static unsafe void Copy<T>(T* oldBuffer, T* newBuffer, uint count) where T : unmanaged
|
||||
=> NativeMemory.Copy(oldBuffer, newBuffer, (nuint)(sizeof(T) * count));
|
||||
|
||||
public static unsafe void MemCopy<T>(T* oldBuffer, T* newBuffer, uint byteCount) where T : unmanaged
|
||||
=> NativeMemory.Copy(oldBuffer, newBuffer, byteCount);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public enum NodePosition {
|
||||
BeforeTarget,
|
||||
AfterTarget,
|
||||
BeforeAllSiblings,
|
||||
AfterAllSiblings,
|
||||
AsLastChild,
|
||||
AsFirstChild,
|
||||
}
|
||||
|
||||
internal static unsafe class NodeLinker {
|
||||
internal static void AttachNode(AtkResNode* node, AtkResNode* attachTargetNode, NodePosition position) {
|
||||
switch (position) {
|
||||
case NodePosition.BeforeTarget:
|
||||
EmplaceBefore(node, attachTargetNode);
|
||||
break;
|
||||
|
||||
case NodePosition.AfterTarget:
|
||||
EmplaceAfter(node, attachTargetNode);
|
||||
break;
|
||||
|
||||
case NodePosition.BeforeAllSiblings:
|
||||
EmplaceBeforeSiblings(node, attachTargetNode);
|
||||
break;
|
||||
|
||||
case NodePosition.AfterAllSiblings:
|
||||
EmplaceAfterSiblings(node, attachTargetNode);
|
||||
break;
|
||||
|
||||
case NodePosition.AsLastChild:
|
||||
EmplaceAsLastChild(node, attachTargetNode);
|
||||
break;
|
||||
|
||||
case NodePosition.AsFirstChild:
|
||||
EmplaceAsFirstChild(node, attachTargetNode);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(position), position, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmplaceBefore(AtkResNode* node, AtkResNode* attachTargetNode) {
|
||||
node->ParentNode = attachTargetNode->ParentNode;
|
||||
|
||||
// Target node is the head of the nodelist, we will be the new head.
|
||||
if (attachTargetNode->NextSiblingNode is null) {
|
||||
attachTargetNode->ParentNode->ChildNode = node;
|
||||
}
|
||||
|
||||
// We have a node that will be before us
|
||||
if (attachTargetNode->NextSiblingNode is not null) {
|
||||
attachTargetNode->NextSiblingNode->PrevSiblingNode = node;
|
||||
node->NextSiblingNode = attachTargetNode->NextSiblingNode;
|
||||
}
|
||||
|
||||
attachTargetNode->NextSiblingNode = node;
|
||||
node->PrevSiblingNode = attachTargetNode;
|
||||
|
||||
if (attachTargetNode->ParentNode->GetNodeType() is not NodeType.Component) {
|
||||
attachTargetNode->ParentNode->ChildCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmplaceAfter(AtkResNode* node, AtkResNode* attachTargetNode) {
|
||||
node->ParentNode = attachTargetNode->ParentNode;
|
||||
|
||||
// We have a node that will be after us
|
||||
if (attachTargetNode->PrevSiblingNode is not null) {
|
||||
attachTargetNode->PrevSiblingNode->NextSiblingNode = node;
|
||||
node->PrevSiblingNode = attachTargetNode->PrevSiblingNode;
|
||||
}
|
||||
|
||||
attachTargetNode->PrevSiblingNode = node;
|
||||
node->NextSiblingNode = attachTargetNode;
|
||||
|
||||
if (attachTargetNode->ParentNode->GetNodeType() is not NodeType.Component) {
|
||||
attachTargetNode->ParentNode->ChildCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmplaceBeforeSiblings(AtkResNode* node, AtkResNode* attachTargetNode) {
|
||||
var current = attachTargetNode;
|
||||
var previous = current;
|
||||
|
||||
while (current is not null) {
|
||||
previous = current;
|
||||
current = current->NextSiblingNode;
|
||||
}
|
||||
|
||||
if (previous is not null) {
|
||||
EmplaceBefore(node, previous);
|
||||
}
|
||||
|
||||
if (attachTargetNode->ParentNode->GetNodeType() is not NodeType.Component) {
|
||||
attachTargetNode->ParentNode->ChildCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmplaceAfterSiblings(AtkResNode* node, AtkResNode* attachTargetNode) {
|
||||
var current = attachTargetNode;
|
||||
var previous = current;
|
||||
|
||||
while (current is not null) {
|
||||
previous = current;
|
||||
current = current->PrevSiblingNode;
|
||||
}
|
||||
|
||||
if (previous is not null) {
|
||||
EmplaceAfter(node, previous);
|
||||
}
|
||||
|
||||
if (attachTargetNode->ParentNode->GetNodeType() is not NodeType.Component) {
|
||||
attachTargetNode->ParentNode->ChildCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmplaceAsLastChild(AtkResNode* node, AtkResNode* attachTargetNode) {
|
||||
// If the child list is empty
|
||||
if (attachTargetNode->ChildNode is null && attachTargetNode->GetNodeType() is not NodeType.Component) {
|
||||
if (attachTargetNode->GetNodeType() is not NodeType.Component) {
|
||||
attachTargetNode->ChildNode = node;
|
||||
node->ParentNode = attachTargetNode;
|
||||
attachTargetNode->ChildCount++;
|
||||
}
|
||||
else {
|
||||
node->ParentNode = attachTargetNode;
|
||||
}
|
||||
}
|
||||
// Else Add to the List
|
||||
else {
|
||||
var currentNode = attachTargetNode->ChildNode;
|
||||
while (currentNode is not null && currentNode->PrevSiblingNode != null) {
|
||||
currentNode = currentNode->PrevSiblingNode;
|
||||
}
|
||||
|
||||
node->ParentNode = attachTargetNode;
|
||||
node->NextSiblingNode = currentNode;
|
||||
|
||||
if (currentNode is not null) {
|
||||
currentNode->PrevSiblingNode = node;
|
||||
}
|
||||
|
||||
if (attachTargetNode->GetNodeType() is not NodeType.Component) {
|
||||
attachTargetNode->ChildCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmplaceAsFirstChild(AtkResNode* node, AtkResNode* attachTargetNode) {
|
||||
// If the child list is empty
|
||||
if (attachTargetNode->ChildNode is null && attachTargetNode->ChildCount is 0) {
|
||||
if (attachTargetNode->GetNodeType() is not NodeType.Component) {
|
||||
attachTargetNode->ChildNode = node;
|
||||
node->ParentNode = attachTargetNode;
|
||||
attachTargetNode->ChildCount++;
|
||||
}
|
||||
else {
|
||||
node->ParentNode = attachTargetNode;
|
||||
}
|
||||
}
|
||||
// Else Add to the List as the First Child
|
||||
else {
|
||||
if (attachTargetNode->GetNodeType() is not NodeType.Component) {
|
||||
attachTargetNode->ChildNode->NextSiblingNode = node;
|
||||
node->PrevSiblingNode = attachTargetNode->ChildNode;
|
||||
attachTargetNode->ChildNode = node;
|
||||
node->ParentNode = attachTargetNode;
|
||||
attachTargetNode->ChildCount++;
|
||||
}
|
||||
else {
|
||||
node->PrevSiblingNode = attachTargetNode->ChildNode;
|
||||
node->ParentNode = attachTargetNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DetachNode(AtkResNode* node) {
|
||||
if (node is null) return;
|
||||
if (node->ParentNode is null) return;
|
||||
|
||||
if (node->ParentNode->ChildNode == node)
|
||||
node->ParentNode->ChildNode = node->PrevSiblingNode;
|
||||
|
||||
if (node->PrevSiblingNode != null)
|
||||
node->PrevSiblingNode->NextSiblingNode = node->NextSiblingNode;
|
||||
|
||||
if (node->NextSiblingNode != null)
|
||||
node->NextSiblingNode->PrevSiblingNode = node->PrevSiblingNode;
|
||||
|
||||
if (node->ParentNode->GetNodeType() is not NodeType.Component) {
|
||||
node->ParentNode->ChildCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public class Part {
|
||||
|
||||
public float Width { get; set; }
|
||||
|
||||
public float Height { get; set; }
|
||||
|
||||
public Vector2 Size {
|
||||
get => new(Width, Height);
|
||||
set {
|
||||
Width = value.X;
|
||||
Height = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public float U { get; set; }
|
||||
|
||||
public float V { get; set; }
|
||||
|
||||
public Vector2 TextureCoordinates {
|
||||
get => new(U, V);
|
||||
set {
|
||||
U = value.X;
|
||||
V = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public uint Id { get; set; }
|
||||
|
||||
public string TexturePath { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper around a AtkUldPartsList, manages adding multiple parts more easily.
|
||||
/// </summary>
|
||||
public unsafe class PartsList : IDisposable {
|
||||
|
||||
internal AtkUldPartsList* InternalPartsList;
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
public PartsList() {
|
||||
InternalPartsList = NativeMemoryHelper.UiAlloc<AtkUldPartsList>();
|
||||
|
||||
InternalPartsList->Parts = null;
|
||||
InternalPartsList->PartCount = 0;
|
||||
InternalPartsList->Id = 0;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (!isDisposed) {
|
||||
foreach (var partIndex in Enumerable.Range(0, (int)PartCount)) {
|
||||
ref var part = ref InternalPartsList->Parts[partIndex];
|
||||
|
||||
if (part.UldAsset is not null && part.UldAsset->AtkTexture.IsTextureReady()) {
|
||||
part.UldAsset->AtkTexture.ReleaseTexture();
|
||||
part.UldAsset->AtkTexture.KernelTexture = null;
|
||||
part.UldAsset->AtkTexture.TextureType = 0;
|
||||
}
|
||||
|
||||
NativeMemoryHelper.UiFree(part.UldAsset);
|
||||
part.UldAsset = null;
|
||||
}
|
||||
|
||||
NativeMemoryHelper.UiFree(InternalPartsList);
|
||||
InternalPartsList = null;
|
||||
}
|
||||
|
||||
isDisposed = true;
|
||||
}
|
||||
|
||||
private uint PartCount {
|
||||
get => InternalPartsList->PartCount;
|
||||
set => InternalPartsList->PartCount = value;
|
||||
}
|
||||
|
||||
public void Add(params Part[] items) {
|
||||
foreach (var part in items) {
|
||||
Add(part);
|
||||
}
|
||||
}
|
||||
|
||||
public AtkUldPart* Add(Part item) {
|
||||
NativeMemoryHelper.ResizeArray(ref InternalPartsList->Parts, PartCount, PartCount + 1);
|
||||
|
||||
ref var newPart = ref InternalPartsList->Parts[PartCount];
|
||||
|
||||
newPart.Width = (ushort) item.Width;
|
||||
newPart.Height = (ushort) item.Height;
|
||||
newPart.U = (ushort) item.U;
|
||||
newPart.V = (ushort) item.V;
|
||||
|
||||
newPart.UldAsset = NativeMemoryHelper.UiAlloc<AtkUldAsset>();
|
||||
newPart.UldAsset->Id = item.Id;
|
||||
newPart.UldAsset->AtkTexture.Ctor();
|
||||
newPart.LoadTexture(item.TexturePath);
|
||||
|
||||
return &InternalPartsList->Parts[PartCount++];
|
||||
}
|
||||
|
||||
public AtkUldPart* this[int index] {
|
||||
get {
|
||||
if (InternalPartsList is null) return null;
|
||||
if (PartCount <= index) return null;
|
||||
|
||||
return &InternalPartsList->Parts[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public record TabbedNodeEntry<T>(T Node, int Tab) where T : NodeBase;
|
||||
@@ -0,0 +1,26 @@
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Classes;
|
||||
|
||||
public unsafe class ViewportEventListener(AtkEventListener.Delegates.ReceiveEvent eventHandler) : CustomEventListener(eventHandler) {
|
||||
public void AddEvent(AtkEventType eventType, AtkResNode* node) {
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
Log.Verbose($"Registering ViewportEvent: {eventType}");
|
||||
AtkStage.Instance()->ViewportEventManager.RegisterEvent(eventType, 0, node, &node->AtkEventTarget, this, false);
|
||||
});
|
||||
}
|
||||
|
||||
public void RemoveEvent(AtkEventType eventType) {
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
Log.Verbose($"Unregistering ViewportEvent: {eventType}");
|
||||
AtkStage.Instance()->ViewportEventManager.UnregisterEvent(eventType, 0, this, false);
|
||||
});
|
||||
}
|
||||
|
||||
public override void Dispose() {
|
||||
Log.Verbose("Disposing ViewportEventListener");
|
||||
|
||||
RemoveEvent(AtkEventType.UnregisterAll);
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit.ContextMenu;
|
||||
|
||||
public unsafe class ContextMenu : IDisposable {
|
||||
private readonly CustomEventInterface contextMenuEventInterface;
|
||||
|
||||
private Dictionary<long, ContextMenuItem>? mainMenuEntries;
|
||||
private Dictionary<long, ContextMenuSubItem>? mainMenuSubMenus;
|
||||
private Dictionary<long, ContextMenuItem>? subMenuEntries;
|
||||
|
||||
// Prevent the return entry from colliding with submenu items
|
||||
private const int SubMenuIndexOffset = 1000;
|
||||
|
||||
private List<ContextMenuItem> Items { get; set; } = [];
|
||||
private IOrderedEnumerable<ContextMenuItem> OrderedItems => Items.OrderBy(item => item.DisplayPriority);
|
||||
|
||||
public ContextMenu() {
|
||||
contextMenuEventInterface = new CustomEventInterface(ContextMenuEventHandler);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
contextMenuEventInterface.Dispose();
|
||||
}
|
||||
|
||||
private AtkValue* ContextMenuEventHandler(AtkModuleInterface.AtkEventInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) {
|
||||
var handlerParam = (long)eventKind;
|
||||
|
||||
if (handlerParam >= SubMenuIndexOffset) {
|
||||
if (subMenuEntries?.TryGetValue(handlerParam, out var subItem) ?? false) {
|
||||
subItem.OnClick();
|
||||
ClearAll();
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
if (mainMenuSubMenus?.TryGetValue(handlerParam, out var subMenuItem) ?? false) {
|
||||
OpenSubMenu(subMenuItem);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
if (mainMenuEntries?.TryGetValue(handlerParam, out var item) ?? false) {
|
||||
item.OnClick();
|
||||
ClearAll();
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
subMenuEntries?.Clear();
|
||||
subMenuEntries = null;
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private void ClearAll() {
|
||||
mainMenuEntries?.Clear();
|
||||
mainMenuEntries = null;
|
||||
mainMenuSubMenus?.Clear();
|
||||
mainMenuSubMenus = null;
|
||||
subMenuEntries?.Clear();
|
||||
subMenuEntries = null;
|
||||
}
|
||||
|
||||
public void AddItem(ReadOnlySeString name, Action callback) {
|
||||
AddItem(new ContextMenuItem {
|
||||
Name = name,
|
||||
OnClick = callback,
|
||||
});
|
||||
}
|
||||
|
||||
public void RemoveItem(ReadOnlySeString name) {
|
||||
var targetItem = Items.FirstOrDefault(item => item.Name == name);
|
||||
if (targetItem is null) return;
|
||||
|
||||
Items.Remove(targetItem);
|
||||
}
|
||||
|
||||
public void AddItem(ContextMenuItem item, params ContextMenuItem[] items) {
|
||||
foreach (var entry in items.Prepend(item)) {
|
||||
Items.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveItem(ContextMenuItem item, params ContextMenuItem[] items) {
|
||||
foreach (var entry in items.Prepend(item)) {
|
||||
Items.Remove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() => Items.Clear();
|
||||
|
||||
public void Open() {
|
||||
var agentContextMenu = AgentContext.Instance();
|
||||
|
||||
agentContextMenu->ClearMenu();
|
||||
|
||||
mainMenuEntries = [];
|
||||
mainMenuSubMenus = [];
|
||||
subMenuEntries = null;
|
||||
|
||||
foreach (var (index, item) in OrderedItems.Index()) {
|
||||
if (item is ContextMenuSubItem subItem) {
|
||||
mainMenuSubMenus.Add(index, subItem);
|
||||
agentContextMenu->AddMenuItem(item.Name, contextMenuEventInterface, index, !item.IsEnabled, submenu: true);
|
||||
} else {
|
||||
mainMenuEntries.Add(index, item);
|
||||
agentContextMenu->AddMenuItem(item.Name, contextMenuEventInterface, index, !item.IsEnabled, submenu: false);
|
||||
}
|
||||
}
|
||||
|
||||
agentContextMenu->OpenContextMenu();
|
||||
}
|
||||
|
||||
private void OpenSubMenu(ContextMenuSubItem subItem) {
|
||||
var agentContextMenu = AgentContext.Instance();
|
||||
|
||||
// Set the state again to prevent the menu closing when going back and forth between the submenus
|
||||
agentContextMenu->SubContextMenu.SelectedContextItemIndex = 0;
|
||||
agentContextMenu->SubContextMenu.CurrentEventIndex = 8;
|
||||
|
||||
agentContextMenu->OpenSubMenu();
|
||||
|
||||
var indexer = 0;
|
||||
subMenuEntries = [];
|
||||
|
||||
foreach (var item in subItem.SubItems.OrderBy(i => i.DisplayPriority)) {
|
||||
if (item is ContextMenuSubItem) continue;
|
||||
|
||||
var paramIndex = SubMenuIndexOffset + indexer;
|
||||
subMenuEntries.Add(paramIndex, item);
|
||||
agentContextMenu->AddMenuItem(item.Name, contextMenuEventInterface, paramIndex, !item.IsEnabled, submenu: false);
|
||||
indexer++;
|
||||
}
|
||||
}
|
||||
|
||||
public void Close() {
|
||||
var agentContextMenu = AgentContext.Instance();
|
||||
|
||||
agentContextMenu->ClearMenu();
|
||||
ClearAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit.ContextMenu;
|
||||
|
||||
public class ContextMenuItem {
|
||||
public required ReadOnlySeString Name { get; init; }
|
||||
public bool IsEnabled { get; init; } = true;
|
||||
public required Action OnClick { get; init; }
|
||||
public int DisplayPriority { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// One level of submenu only. Nested submenus not supported.
|
||||
/// </summary>
|
||||
public class ContextMenuSubItem : ContextMenuItem {
|
||||
public List<ContextMenuItem> SubItems { get; set; } = [];
|
||||
|
||||
public void AddItem(ReadOnlySeString name, Action callback) {
|
||||
SubItems.Add(new ContextMenuItem {
|
||||
Name = name,
|
||||
OnClick = callback,
|
||||
});
|
||||
}
|
||||
|
||||
public void AddItem(ContextMenuItem item) => SubItems.Add(item);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Controllers;
|
||||
|
||||
public class AddonController(string addonName) : AddonController<AtkUnitBase>(addonName);
|
||||
|
||||
/// <summary>
|
||||
/// This class provides functionality to add-and manage custom elements for any Addon
|
||||
/// </summary>
|
||||
public unsafe class AddonController<T> : AddonEventController<T>, IDisposable where T : unmanaged {
|
||||
|
||||
internal readonly string AddonName;
|
||||
|
||||
private AtkUnitBase* AddonPointer => (AtkUnitBase*)DalamudInterface.Instance.GameGui.GetAddonByName(AddonName).Address;
|
||||
private bool IsEnabled { get; set; }
|
||||
|
||||
private bool isSetupComplete;
|
||||
|
||||
/// <summary>
|
||||
/// This class provides functionality to add-and manage custom elements for any Addon
|
||||
/// </summary>
|
||||
public AddonController(string addonName) {
|
||||
if (addonName is "NamePlate") {
|
||||
throw new Exception("Attaching to NamePlate is not supported. Use OverlayController instead.");
|
||||
}
|
||||
|
||||
AddonName = addonName;
|
||||
}
|
||||
|
||||
public virtual void Dispose() => Disable();
|
||||
|
||||
public void Enable() {
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
if (IsEnabled) return;
|
||||
|
||||
onInnerPreEnable?.Invoke((T*)AddonPointer);
|
||||
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, AddonName, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, AddonName, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostRefresh, AddonName, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, AddonName, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostUpdate, AddonName, OnAddonEvent);
|
||||
|
||||
if (AddonPointer is not null) {
|
||||
OnInnerAttach?.Invoke((T*)AddonPointer);
|
||||
isSetupComplete = true;
|
||||
}
|
||||
|
||||
IsEnabled = true;
|
||||
|
||||
onInnerPostEnable?.Invoke((T*)AddonPointer);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnAddonEvent(AddonEvent type, AddonArgs args) {
|
||||
var addon = (T*)args.Addon.Address;
|
||||
|
||||
switch (type) {
|
||||
case AddonEvent.PostSetup:
|
||||
OnInnerAttach?.Invoke(addon);
|
||||
isSetupComplete = true;
|
||||
return;
|
||||
|
||||
case AddonEvent.PreFinalize:
|
||||
OnInnerDetach?.Invoke(addon);
|
||||
isSetupComplete = false;
|
||||
return;
|
||||
|
||||
case AddonEvent.PostRefresh or AddonEvent.PostRequestedUpdate when isSetupComplete:
|
||||
OnInnerRefresh?.Invoke(addon);
|
||||
return;
|
||||
|
||||
case AddonEvent.PostUpdate:
|
||||
OnInnerUpdate?.Invoke(addon);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disable() {
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
if (!IsEnabled) return;
|
||||
|
||||
onInnerPreDisable?.Invoke((T*)AddonPointer);
|
||||
|
||||
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(OnAddonEvent);
|
||||
|
||||
if (AddonPointer is not null) {
|
||||
OnInnerDetach?.Invoke((T*)AddonPointer);
|
||||
}
|
||||
|
||||
IsEnabled = false;
|
||||
|
||||
onInnerPostDisable?.Invoke((T*)AddonPointer);
|
||||
});
|
||||
}
|
||||
|
||||
public event AddonControllerEvent? OnPreEnable {
|
||||
add => onInnerPreEnable += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
public event AddonControllerEvent? OnPostEnable {
|
||||
add => onInnerPostEnable += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
public event AddonControllerEvent? OnPreDisable {
|
||||
add => onInnerPreDisable += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
public event AddonControllerEvent? OnPostDisable {
|
||||
add => onInnerPostDisable += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
private AddonControllerEvent? onInnerPreEnable;
|
||||
private AddonControllerEvent? onInnerPostEnable;
|
||||
private AddonControllerEvent? onInnerPreDisable;
|
||||
private AddonControllerEvent? onInnerPostDisable;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
|
||||
namespace KamiToolKit.Controllers;
|
||||
|
||||
public abstract unsafe class AddonEventController<T> where T : unmanaged {
|
||||
|
||||
protected AddonEventController() {
|
||||
if (typeof(T) == typeof(AddonNamePlate)) {
|
||||
throw new NotSupportedException("Attaching to NamePlate is not supported. Use OverlayController.");
|
||||
}
|
||||
}
|
||||
|
||||
public delegate void AddonControllerEvent(T* addon);
|
||||
|
||||
public event AddonControllerEvent? OnAttach {
|
||||
add => OnInnerAttach += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
public event AddonControllerEvent? OnDetach {
|
||||
add => OnInnerDetach += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
public event AddonControllerEvent? OnRefresh {
|
||||
add => OnInnerRefresh += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
public event AddonControllerEvent? OnUpdate {
|
||||
add => OnInnerUpdate += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
protected AddonControllerEvent? OnInnerAttach;
|
||||
protected AddonControllerEvent? OnInnerDetach;
|
||||
protected AddonControllerEvent? OnInnerRefresh;
|
||||
protected AddonControllerEvent? OnInnerUpdate;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Addon controller for dynamically managing addons, typical use case is intended to
|
||||
/// be for a single tasks, that can apply to one or many addons at once.
|
||||
/// </summary>
|
||||
public unsafe class DynamicAddonController : AddonEventController<AtkUnitBase>, IDisposable {
|
||||
|
||||
private readonly HashSet<string> trackedAddons = [];
|
||||
private bool isEnabled;
|
||||
|
||||
public DynamicAddonController(params string[] addonNames) {
|
||||
foreach (var addonName in addonNames) {
|
||||
AddAddon(addonName);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddAddon(string name) {
|
||||
if (name is "NamePlate") {
|
||||
Log.Error("Attaching to NamePlate is not supported. Use OverlayController instead.");
|
||||
return;
|
||||
}
|
||||
|
||||
trackedAddons.Add(name);
|
||||
|
||||
if (isEnabled) {
|
||||
AddListeners(name);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAddon(string name) {
|
||||
trackedAddons.Remove(name);
|
||||
|
||||
if (isEnabled) {
|
||||
RemoveListeners(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddonEvent(AddonEvent type, AddonArgs args) {
|
||||
var addon = (AtkUnitBase*)args.Addon.Address;
|
||||
|
||||
switch (type) {
|
||||
case AddonEvent.PostSetup:
|
||||
OnInnerAttach?.Invoke(addon);
|
||||
return;
|
||||
|
||||
case AddonEvent.PreFinalize:
|
||||
OnInnerDetach?.Invoke(addon);
|
||||
return;
|
||||
|
||||
case AddonEvent.PostRefresh or AddonEvent.PostRequestedUpdate:
|
||||
OnInnerRefresh?.Invoke(addon);
|
||||
return;
|
||||
|
||||
case AddonEvent.PostUpdate:
|
||||
OnInnerUpdate?.Invoke(addon);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void Enable() {
|
||||
foreach (var name in trackedAddons) {
|
||||
AddListeners(name);
|
||||
}
|
||||
|
||||
isEnabled = true;
|
||||
}
|
||||
|
||||
public void Disable() {
|
||||
isEnabled = false;
|
||||
|
||||
foreach (var name in trackedAddons) {
|
||||
RemoveListeners(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddListeners(string name) {
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, name, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, name, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostRefresh, name, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, name, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostUpdate, name, OnAddonEvent);
|
||||
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(name);
|
||||
if (addon is not null) {
|
||||
OnInnerAttach?.Invoke(addon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void RemoveListeners(string name) {
|
||||
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PostSetup, name, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PreFinalize, name, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PostRefresh, name, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, name, OnAddonEvent);
|
||||
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, name, OnAddonEvent);
|
||||
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(name);
|
||||
if (addon is not null) {
|
||||
OnInnerDetach?.Invoke(addon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(OnAddonEvent);
|
||||
Disable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// For use with addons that have multiple persistent variants, but where only one is used at a time.
|
||||
/// For example, Inventories or CastBars.
|
||||
/// Using this with other addons will duplicate their associated events incorrectly.
|
||||
/// </summary>
|
||||
public unsafe class MultiAddonController : AddonEventController<AtkUnitBase>, IDisposable {
|
||||
|
||||
private readonly List<AddonController> addonControllers = [];
|
||||
|
||||
public MultiAddonController(params string[] addonNames) {
|
||||
foreach (var addonName in addonNames) {
|
||||
if (addonName is "NamePlate") {
|
||||
Log.Error("Attaching to NamePlate is not supported. Use OverlayController instead.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't allow duplicate addon controllers
|
||||
if (addonControllers.Any(controller => controller.AddonName == addonName)) continue;
|
||||
|
||||
var newController = new AddonController(addonName);
|
||||
|
||||
addonControllers.Add(newController);
|
||||
|
||||
newController.OnAttach += ControllerOnAttach;
|
||||
newController.OnDetach += ControllerOnDetach;
|
||||
newController.OnRefresh += ControllerOnRefresh;
|
||||
newController.OnUpdate += ControllerOnUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
private void ControllerOnAttach(AtkUnitBase* addon)
|
||||
=> OnInnerAttach?.Invoke(addon);
|
||||
|
||||
private void ControllerOnDetach(AtkUnitBase* addon)
|
||||
=> OnInnerDetach?.Invoke(addon);
|
||||
|
||||
private void ControllerOnRefresh(AtkUnitBase* addon)
|
||||
=> OnInnerRefresh?.Invoke(addon);
|
||||
|
||||
private void ControllerOnUpdate(AtkUnitBase* addon)
|
||||
=> OnInnerUpdate?.Invoke(addon);
|
||||
|
||||
public void Dispose() {
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
addonControllers.ForEach(controller => controller.Dispose());
|
||||
addonControllers.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
public void Enable() {
|
||||
addonControllers.ForEach(controller => controller.Enable());
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
=> addonControllers.ForEach(controller => controller.Disable());
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Only one or the other field will be valid, be sure to check for null.
|
||||
/// </summary>
|
||||
public unsafe class ListItemData {
|
||||
public AtkComponentListItemPopulator.ListItemInfo* ItemInfo { get; set; }
|
||||
public AtkComponentListItemRenderer* ItemRenderer { get; set; }
|
||||
}
|
||||
|
||||
public unsafe class NativeListController(string addonName) : IDisposable {
|
||||
|
||||
public required ShouldModifyElementHandler ShouldModifyElement { get; init; }
|
||||
public required UpdateElementHandler UpdateElement { get; init; }
|
||||
public required ResetElementHandler ResetElement { get; init; }
|
||||
public required GetPopulatorNodeHandler GetPopulatorNode { get; init; }
|
||||
|
||||
private Hook<AtkComponentListItemPopulator.PopulateDelegate>? onListPopulate;
|
||||
private Hook<AtkComponentListItemPopulator.PopulateWithRendererDelegate>? onRendererPopulate;
|
||||
|
||||
public readonly List<uint> ModifiedIndexes = [];
|
||||
|
||||
public event Action? OnClose {
|
||||
add => OnInnerClose += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
public event Action? OnOpen {
|
||||
add => OnInnerOpen += value;
|
||||
remove => throw new Exception("Do not remove events, on dispose addon state will be managed properly.");
|
||||
}
|
||||
|
||||
public void Enable() {
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, addonName, OnAddonSetup);
|
||||
DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, addonName, OnAddonFinalize);
|
||||
|
||||
var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(addonName);
|
||||
if (addon is not null) {
|
||||
Log.Warning("Caution: ListController was loaded after list was initialized, data may be stale.");
|
||||
LoadPopulators(addon);
|
||||
}
|
||||
}
|
||||
|
||||
public void Disable() => Dispose();
|
||||
|
||||
public void Dispose() {
|
||||
DalamudInterface.Instance.AddonLifecycle.UnregisterListener(OnAddonSetup, OnAddonFinalize);
|
||||
|
||||
onListPopulate?.Dispose();
|
||||
onListPopulate = null;
|
||||
|
||||
onRendererPopulate?.Dispose();
|
||||
onRendererPopulate = null;
|
||||
}
|
||||
|
||||
private void OnAddonSetup(AddonEvent type, AddonArgs args)
|
||||
=> LoadPopulators((AtkUnitBase*)args.Addon.Address);
|
||||
|
||||
private void OnAddonFinalize(AddonEvent type, AddonArgs args) {
|
||||
onListPopulate?.Dispose();
|
||||
onListPopulate = null;
|
||||
|
||||
onRendererPopulate?.Dispose();
|
||||
onRendererPopulate = null;
|
||||
|
||||
ModifiedIndexes.Clear();
|
||||
|
||||
OnInnerClose?.Invoke();
|
||||
}
|
||||
|
||||
private void LoadPopulators(AtkUnitBase* addon) {
|
||||
var populateMethod = GetPopulatorNode(addon)->Populator;
|
||||
|
||||
if (populateMethod.Populate is not null) {
|
||||
onListPopulate = DalamudInterface.Instance.GameInteropProvider.HookFromAddress<AtkComponentListItemPopulator.PopulateDelegate>(populateMethod.Populate, OnPopulateDetour);
|
||||
onListPopulate?.Enable();
|
||||
}
|
||||
|
||||
if (populateMethod.PopulateWithRenderer is not null) {
|
||||
onRendererPopulate = DalamudInterface.Instance.GameInteropProvider.HookFromAddress<AtkComponentListItemPopulator.PopulateWithRendererDelegate>(populateMethod.PopulateWithRenderer, OnRendererPopulateDetour);
|
||||
onRendererPopulate?.Enable();
|
||||
}
|
||||
|
||||
OnInnerOpen?.Invoke();
|
||||
}
|
||||
|
||||
private void OnPopulateDetour(AtkUnitBase* unitBase, AtkComponentListItemPopulator.ListItemInfo* itemInfo, AtkResNode** nodeList) {
|
||||
try {
|
||||
var listItemData = new ListItemData {
|
||||
ItemInfo = itemInfo,
|
||||
};
|
||||
|
||||
var shouldModifyElement = ShouldModifyElement(unitBase, listItemData, nodeList);
|
||||
|
||||
if (!shouldModifyElement) {
|
||||
if (ModifiedIndexes.Contains(itemInfo->ListItem->Renderer->OwnerNode->NodeId)) {
|
||||
ResetElement.Invoke(unitBase, listItemData, nodeList);
|
||||
ModifiedIndexes.Remove(itemInfo->ListItem->Renderer->OwnerNode->NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
onListPopulate!.Original(unitBase, itemInfo, nodeList);
|
||||
|
||||
if (shouldModifyElement) {
|
||||
UpdateElement.Invoke(unitBase, listItemData, nodeList);
|
||||
ModifiedIndexes.Add(itemInfo->ListItem->Renderer->OwnerNode->NodeId);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRendererPopulateDetour(AtkUnitBase* unitBase, int listItemIndex, AtkResNode** nodeList, AtkComponentListItemRenderer* listItemRenderer) {
|
||||
try {
|
||||
var listItemData = new ListItemData {
|
||||
ItemRenderer = listItemRenderer,
|
||||
};
|
||||
|
||||
var shouldModifyElement = ShouldModifyElement(unitBase, listItemData, nodeList);
|
||||
|
||||
if (!shouldModifyElement) {
|
||||
if (ModifiedIndexes.Contains(listItemRenderer->OwnerNode->NodeId)) {
|
||||
ResetElement.Invoke(unitBase, listItemData, nodeList);
|
||||
ModifiedIndexes.Remove(listItemRenderer->OwnerNode->NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
onRendererPopulate!.Original(unitBase, listItemIndex, nodeList, listItemRenderer);
|
||||
|
||||
if (shouldModifyElement) {
|
||||
UpdateElement.Invoke(unitBase, listItemData, nodeList);
|
||||
ModifiedIndexes.Add(listItemRenderer->OwnerNode->NodeId);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
public delegate bool ShouldModifyElementHandler(AtkUnitBase* unitBase, ListItemData listItemInfo, AtkResNode** nodeList);
|
||||
public delegate AtkComponentListItemRenderer* GetPopulatorNodeHandler(AtkUnitBase* addon);
|
||||
public delegate void UpdateElementHandler(AtkUnitBase* unitBase, ListItemData listItemInfo, AtkResNode** nodeList);
|
||||
public delegate void ResetElementHandler(AtkUnitBase* unitBase, ListItemData listItemInfo, AtkResNode** nodeList);
|
||||
|
||||
private Action? OnInnerClose { get; set; }
|
||||
private Action? OnInnerOpen { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum CounterFont {
|
||||
MoneyFont,
|
||||
ChocoboRace,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
[Flags]
|
||||
public enum DrawFlags : uint {
|
||||
None = 0,
|
||||
IsDirty = 0x1,
|
||||
IsAnimating = 0x2,
|
||||
CalculateTransformation = 0x4,
|
||||
DisableRapidUp = 0x10,
|
||||
DisableRapidDown = 0x20,
|
||||
DisableRapidLeft = 0x40,
|
||||
DisableRapidRight = 0x80,
|
||||
DisableTimelineLabel = 0x100,
|
||||
ClickableCursor = 0x100000,
|
||||
RenderOnTop = 0x200000,
|
||||
TextInputCursor = 0x400000,
|
||||
UseEllipticalCollision = 0x800000,
|
||||
UseTransformedCollision = 0x1000000,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
[Flags]
|
||||
public enum FlexFlags {
|
||||
/// <summary>
|
||||
/// Adjusts the height of the inserted node to be the same as the area generated
|
||||
/// </summary>
|
||||
FitHeight = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the width of the inserted node to be the same as the area generated
|
||||
/// </summary>
|
||||
FitWidth = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the FlexNode's height to fit the nodes that are inserted into it
|
||||
/// </summary>
|
||||
FitContentHeight = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// Center inserted nodes into the middle of the flex area horizontally
|
||||
/// </summary>
|
||||
CenterVertically = 1 << 4,
|
||||
|
||||
/// <summary>
|
||||
/// Center inserted nodes into the middle of the flex area vertically
|
||||
/// </summary>
|
||||
CenterHorizontally = 1 << 5,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum HorizontalListAnchor {
|
||||
[Description("Left")]
|
||||
Left,
|
||||
|
||||
[Description("Right")]
|
||||
Right,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum LayoutAnchor {
|
||||
[Description("Top Left")]
|
||||
TopLeft,
|
||||
|
||||
[Description("Top Right")]
|
||||
TopRight,
|
||||
|
||||
[Description("Bottom Left")]
|
||||
BottomLeft,
|
||||
|
||||
[Description("Bottom Right")]
|
||||
BottomRight,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum LayoutOrientation {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
[Flags]
|
||||
public enum NodeEditMode {
|
||||
Resize = 1 << 1,
|
||||
Move = 1 << 2,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
internal enum OverlayAddonState {
|
||||
None,
|
||||
WaitForReady,
|
||||
Ready,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
internal enum ControllerState {
|
||||
WaitForNameplate,
|
||||
WaitForReady,
|
||||
Ready,
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum OverlayLayer {
|
||||
/// <summary>
|
||||
/// Layer that is the back most, this is below nameplates, but above the world itself.
|
||||
/// </summary>
|
||||
[Description("KTK_Overlay_Back")]
|
||||
Background,
|
||||
|
||||
/// <summary>
|
||||
/// Above nameplate layer
|
||||
/// </summary>
|
||||
[Description("KTK_Overlay_Middle")]
|
||||
BehindUserInterface,
|
||||
|
||||
/// <summary>
|
||||
/// Above most windows but below certain popup windows like battle text
|
||||
/// </summary>
|
||||
[Description("KTK_Overlay_Higher")]
|
||||
AboveUserInterface,
|
||||
|
||||
/// <summary>
|
||||
/// Above everything, use with caution
|
||||
/// </summary>
|
||||
[Description("KTK_Overlay_Front")]
|
||||
Foreground,
|
||||
}
|
||||
|
||||
public static class OverlayLayerExtensions {
|
||||
extension(OverlayLayer layer) {
|
||||
public int DepthLayer => layer switch {
|
||||
OverlayLayer.Background => 1,
|
||||
OverlayLayer.BehindUserInterface => 3,
|
||||
OverlayLayer.AboveUserInterface => 7,
|
||||
OverlayLayer.Foreground => 13,
|
||||
_ => 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Note: The game does not have a layer zero, but offsets the desired layer by one.
|
||||
public static OverlayLayer GetOverlayLayer(this uint layer) => (layer + 1) switch {
|
||||
1 => OverlayLayer.Background,
|
||||
3 => OverlayLayer.BehindUserInterface,
|
||||
7 => OverlayLayer.AboveUserInterface,
|
||||
13 => OverlayLayer.Foreground,
|
||||
_ => throw new Exception("Unknown depth layer: " + layer),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
internal enum ResizeDirection {
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
[Flags]
|
||||
public enum TextInputFlags : ushort {
|
||||
Capitalize = 0x1,
|
||||
Mask = 0x2,
|
||||
EnableDictionary = 0x4,
|
||||
EnableHistory = 0x8,
|
||||
EnableIme = 0x10,
|
||||
EscapeClears = 0x20,
|
||||
AllowUpperCase = 0x40,
|
||||
AllowLowerCase = 0x80,
|
||||
AllowNumberInput = 0x100,
|
||||
AllowSymbolInput = 0x200,
|
||||
WordWrap = 0x400,
|
||||
MultiLine = 0x800,
|
||||
AutoMaxWidth = 0x1000,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum VerticalListAlignment {
|
||||
[Description("Left")]
|
||||
Left,
|
||||
|
||||
[Description("Right")]
|
||||
Right,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum VerticalListAnchor {
|
||||
[Description("Top")]
|
||||
Top,
|
||||
|
||||
[Description("Bottom")]
|
||||
Bottom,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace KamiToolKit.Enums;
|
||||
|
||||
public enum WrapMode {
|
||||
None = 0,
|
||||
Tile = 1,
|
||||
Stretch = 2,
|
||||
TileMirrored = 3,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using ModifierFlag = FFXIVClientStructs.FFXIV.Component.GUI.AtkEventData.AtkMouseData.ModifierFlag;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static class AtkEventDataExtensions {
|
||||
extension(ref AtkEventData data) {
|
||||
public Vector2 MousePosition => new(data.MouseData.PosX, data.MouseData.PosY);
|
||||
public bool IsLeftClick => data.MouseData.ButtonId is 0;
|
||||
public bool IsRightClick => data.MouseData.ButtonId is 1;
|
||||
public bool IsNoModifiers => data.MouseData.Modifier is 0;
|
||||
public bool IsAltHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Alt);
|
||||
public bool IsControlHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Ctrl);
|
||||
public bool IsShiftHeld => data.MouseData.Modifier.HasFlag(ModifierFlag.Shift);
|
||||
public bool IsDragging => data.MouseData.Modifier.HasFlag(ModifierFlag.Dragging);
|
||||
public bool IsScrollUp => data.MouseData.WheelDirection >= 1;
|
||||
public bool IsScrollDown => data.MouseData.WheelDirection <= -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static unsafe class AtkImageNodeExtensions {
|
||||
extension(ref AtkImageNode node) {
|
||||
public uint IconId => node.GetIconId();
|
||||
|
||||
private uint GetIconId() {
|
||||
if (node.PartsList is null) return 0;
|
||||
if (node.PartsList->Parts is null) return 0;
|
||||
if (node.PartsList->Parts->UldAsset is null) return 0;
|
||||
if (node.PartsList->Parts->UldAsset->AtkTexture.TextureType is not TextureType.Resource) return 0;
|
||||
if (node.PartsList->Parts->UldAsset->AtkTexture.Resource is null) return 0;
|
||||
|
||||
return node.PartsList->Parts->UldAsset->AtkTexture.Resource->IconId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static unsafe class AtkResNodeExtensions {
|
||||
extension(ref AtkResNode node) {
|
||||
public Vector2 Position {
|
||||
get => new(node.X, node.Y);
|
||||
set => node.SetPositionFloat(value.X, value.Y);
|
||||
}
|
||||
|
||||
public Vector2 ScreenPosition
|
||||
=> new(node.ScreenX, node.ScreenY);
|
||||
|
||||
public Vector2 Size {
|
||||
get => new(node.GetWidth(), node.GetHeight());
|
||||
set {
|
||||
node.SetWidth((ushort) value.X);
|
||||
node.SetHeight((ushort) value.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public Bounds Bounds => new() {
|
||||
TopLeft = node.Position,
|
||||
BottomRight = node.Position + node.Size,
|
||||
};
|
||||
|
||||
public Vector2 Center
|
||||
=> node.Position + node.Size / 2.0f;
|
||||
|
||||
public Vector2 Scale {
|
||||
get => new (node.GetScaleX(), node.GetScaleY());
|
||||
set => node.SetScale(value.X, value.Y);
|
||||
}
|
||||
|
||||
public float RotationDegrees {
|
||||
get => node.GetRotationDegrees();
|
||||
set => node.SetRotationDegrees(value - (int)(value / 360.0f) * 360.0f);
|
||||
}
|
||||
|
||||
public Vector2 Origin {
|
||||
get => new(node.OriginX, node.OriginY);
|
||||
set => node.SetOrigin(value.X, value.Y);
|
||||
}
|
||||
|
||||
public bool Visible {
|
||||
get => node.IsVisible();
|
||||
set => node.ToggleVisibility(value);
|
||||
}
|
||||
|
||||
public Vector4 ColorVector {
|
||||
get => node.Color.ToVector4();
|
||||
set => node.Color = value.ToByteColor();
|
||||
}
|
||||
|
||||
public ColorHelpers.HsvaColor ColorHsva {
|
||||
get => ColorHelpers.RgbaToHsv(node.ColorVector);
|
||||
set => node.Color = ColorHelpers.HsvToRgb(value).ToByteColor();
|
||||
}
|
||||
|
||||
public Vector3 AddColor {
|
||||
get => new Vector3(node.AddRed, node.AddGreen, node.AddBlue) / 255.0f;
|
||||
set {
|
||||
node.AddRed = (short)(value.X * 255);
|
||||
node.AddGreen = (short)(value.Y * 255);
|
||||
node.AddBlue = (short)(value.Z * 255);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorHelpers.HsvaColor AddColorHsva {
|
||||
get => ColorHelpers.RgbaToHsv(node.AddColor.AsVector4());
|
||||
set => node.AddColor = ColorHelpers.HsvToRgb(value).AsVector3();
|
||||
}
|
||||
|
||||
public Vector3 MultiplyColor {
|
||||
get => new Vector3(node.MultiplyRed, node.MultiplyGreen, node.MultiplyBlue) / 100.0f;
|
||||
set {
|
||||
node.MultiplyRed = (byte)(value.X * 100.0f);
|
||||
node.MultiplyGreen = (byte)(value.Y * 100.0f);
|
||||
node.MultiplyBlue = (byte)(value.Z * 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public ColorHelpers.HsvaColor MultiplyColorHsva {
|
||||
get => ColorHelpers.RgbaToHsv(node.MultiplyColor.AsVector4());
|
||||
set => node.MultiplyColor = ColorHelpers.HsvToRgb(value).AsVector3();
|
||||
}
|
||||
|
||||
public void AddNodeFlag(params NodeFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
node.NodeFlags |= flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveNodeFlag(params NodeFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
node.NodeFlags &= ~flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddDrawFlag(params DrawFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
node.DrawFlags |= (uint)flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveDrawFlag(params DrawFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
node.DrawFlags &= (uint)flag;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CheckCollision(short x, short y, bool inclusive = true)
|
||||
=> node.CheckCollisionAtCoords(x, y, inclusive);
|
||||
|
||||
public bool CheckCollision(Vector2 position, bool inclusive = true)
|
||||
=> node.CheckCollisionAtCoords((short) position.X, (short) position.Y, inclusive);
|
||||
|
||||
public bool CheckCollision(AtkEventData* eventData, bool inclusive = true)
|
||||
=> node.CheckCollisionAtCoords(eventData->MouseData.PosX, eventData->MouseData.PosY, inclusive);
|
||||
|
||||
public bool IsActuallyVisible {
|
||||
get {
|
||||
if (!node.Visible) return false;
|
||||
|
||||
var targetNode = node.ParentNode;
|
||||
while (targetNode is not null) {
|
||||
if (!targetNode->Visible) return false;
|
||||
targetNode = targetNode->ParentNode;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static unsafe class AtkStageExtensions {
|
||||
extension(ref AtkStage atkStage) {
|
||||
public void ClearNodeFocus(AtkResNode* targetNode) {
|
||||
if (targetNode is null) return;
|
||||
|
||||
foreach (ref var focusEntry in atkStage.AtkInputManager->FocusList) {
|
||||
|
||||
// If this entry has no listener/addon, skip it
|
||||
if (focusEntry.AtkEventListener is null) continue;
|
||||
|
||||
// If this entry has our target node
|
||||
if (focusEntry.AtkEventTarget == targetNode) {
|
||||
|
||||
// Clear the entry
|
||||
focusEntry.AtkEventTarget = null;
|
||||
focusEntry.FocusParam = 0;
|
||||
|
||||
// Clear the input managers focused node
|
||||
atkStage.AtkInputManager->FocusedNode = null;
|
||||
|
||||
// Clear collision managers collision node
|
||||
atkStage.AtkCollisionManager->IntersectingCollisionNode = null;
|
||||
|
||||
// Also remove this node from any additional focus nodes the addon might reference
|
||||
var addon = (AtkUnitBase*) focusEntry.AtkEventListener;
|
||||
foreach (ref var node in addon->AdditionalFocusableNodes) {
|
||||
if (node.Value == targetNode) {
|
||||
node = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static unsafe class AtkUldManagerExtensions {
|
||||
extension(ref AtkUldManager manager) {
|
||||
private bool IsNodeInObjectList(AtkResNode* node) {
|
||||
foreach (var objectNode in manager.ObjectNodeSpan) {
|
||||
if (objectNode.Value == node) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsNodeInDrawList(AtkResNode* node) {
|
||||
foreach (var drawNode in manager.Nodes) {
|
||||
if (drawNode.Value == node) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds node and all children nodes to this UldManager's Object List
|
||||
/// </summary>
|
||||
public void AddNodeToObjectList(NodeBase node) {
|
||||
manager.AddNodeToObjectList(node.ResNode);
|
||||
|
||||
foreach (var child in NodeBase.GetLocalChildren(node)) {
|
||||
manager.AddNodeToObjectList(child.ResNode);
|
||||
}
|
||||
|
||||
manager.UpdateDrawNodeList();
|
||||
}
|
||||
|
||||
public void AddNodeToObjectList(AtkResNode* newNode) {
|
||||
if (newNode is null) return;
|
||||
|
||||
// If the node is already in the object list, skip.
|
||||
if (manager.IsNodeInObjectList(newNode)) return;
|
||||
|
||||
var oldSize = manager.Objects->NodeCount;
|
||||
var newSize = oldSize + 1;
|
||||
var newBuffer = (AtkResNode**)NativeMemoryHelper.Malloc((ulong)(newSize * 8));
|
||||
|
||||
if (oldSize > 0) {
|
||||
foreach (var index in Enumerable.Range(0, oldSize)) {
|
||||
newBuffer[index] = manager.Objects->NodeList[index];
|
||||
}
|
||||
|
||||
NativeMemoryHelper.Free(manager.Objects->NodeList, (ulong)(oldSize * 8));
|
||||
}
|
||||
|
||||
newBuffer[newSize - 1] = newNode;
|
||||
|
||||
manager.Objects->NodeList = newBuffer;
|
||||
manager.Objects->NodeCount = newSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes node and all children nodes from this UldManager's Object List
|
||||
/// </summary>
|
||||
public void RemoveNodeFromObjectList(NodeBase node) {
|
||||
manager.RemoveNodeFromObjectList(node.ResNode);
|
||||
|
||||
foreach (var child in NodeBase.GetLocalChildren(node)) {
|
||||
manager.RemoveNodeFromObjectList(child.ResNode);
|
||||
}
|
||||
|
||||
manager.UpdateDrawNodeList();
|
||||
}
|
||||
|
||||
public void RemoveNodeFromObjectList(AtkResNode* node) {
|
||||
if (node is null) return;
|
||||
|
||||
// If the node isn't in the object list, skip.
|
||||
if (!manager.IsNodeInObjectList(node)) return;
|
||||
|
||||
var oldSize = manager.Objects->NodeCount;
|
||||
var newSize = oldSize - 1;
|
||||
var newBuffer = (AtkResNode**)NativeMemoryHelper.Malloc((ulong)(newSize * 8));
|
||||
|
||||
var newIndex = 0;
|
||||
foreach (var index in Enumerable.Range(0, oldSize)) {
|
||||
if (manager.Objects->NodeList[index] != node) {
|
||||
newBuffer[newIndex] = manager.Objects->NodeList[index];
|
||||
newIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
NativeMemoryHelper.Free(manager.Objects->NodeList, (ulong)(oldSize * 8));
|
||||
manager.Objects->NodeList = newBuffer;
|
||||
manager.Objects->NodeCount = newSize;
|
||||
}
|
||||
|
||||
public void PrintObjectList() {
|
||||
Log.Debug("Beginning NodeList");
|
||||
|
||||
foreach (var index in Enumerable.Range(0, manager.Objects->NodeCount)) {
|
||||
var nodePointer = manager.Objects->NodeList[index];
|
||||
Log.Debug($"[{index}]: {(nint)nodePointer:X}");
|
||||
}
|
||||
}
|
||||
|
||||
public uint GetMaxNodeId() {
|
||||
uint max = 1;
|
||||
foreach (var child in manager.Nodes) {
|
||||
if (child.Value is null) continue;
|
||||
|
||||
max = Math.Max(child.Value->NodeId, max);
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
public Span<Pointer<AtkResNode>> ObjectNodeSpan
|
||||
=> new(manager.Objects->NodeList, manager.Objects->NodeCount);
|
||||
|
||||
public T* SearchNodeById<T>(uint nodeId) where T : unmanaged {
|
||||
foreach (var node in manager.Nodes) {
|
||||
if (node.Value is not null) {
|
||||
if (node.Value->NodeId == nodeId)
|
||||
return (T*) node.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public AtkResNode* SearchNodeById(uint nodeId)
|
||||
=> manager.SearchNodeById<AtkResNode>(nodeId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static unsafe class AtkUldPartExtensions {
|
||||
extension(ref AtkUldPart part) {
|
||||
public bool IsTextureReady => part.UldAsset is not null && part.UldAsset->AtkTexture.IsTextureReady();
|
||||
public Vector2 LoadedTextureSize => part.GetActualTextureSize();
|
||||
public string LoadedPath => part.GetLoadedPath();
|
||||
|
||||
public void LoadTexture(string path, bool resolveTheme = true) {
|
||||
try {
|
||||
if (part.UldAsset is null) return;
|
||||
|
||||
part.TryUnloadTexture();
|
||||
|
||||
var texturePath = path.Replace("_hr1", string.Empty);
|
||||
|
||||
var themedPath = texturePath.Replace("uld", GetThemePathModifier());
|
||||
if (DalamudInterface.Instance.DataManager.FileExists(themedPath) && resolveTheme) {
|
||||
texturePath = themedPath;
|
||||
}
|
||||
|
||||
if (DalamudInterface.Instance.DataManager.FileExists(texturePath)) {
|
||||
part.UldAsset->AtkTexture.LoadTextureWithDefaultVersion(texturePath);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadIcon(uint iconId)
|
||||
=> part.UldAsset->AtkTexture.LoadIconTexture(iconId, GetIconSubFolder(iconId));
|
||||
|
||||
private Vector2 GetActualTextureSize() {
|
||||
if (part.UldAsset is null) return Vector2.Zero;
|
||||
if (!part.UldAsset->AtkTexture.IsTextureReady()) return Vector2.Zero;
|
||||
if (part.UldAsset->AtkTexture.TextureType is 0) return Vector2.Zero;
|
||||
if (part.UldAsset->AtkTexture.KernelTexture is null) return Vector2.Zero;
|
||||
|
||||
var width = part.UldAsset->AtkTexture.GetTextureWidth();
|
||||
var height = part.UldAsset->AtkTexture.GetTextureHeight();
|
||||
return new Vector2(width, height);
|
||||
}
|
||||
|
||||
public void LoadTexture(Texture* texture) {
|
||||
if (part.UldAsset is null) return;
|
||||
|
||||
part.TryUnloadTexture();
|
||||
part.UldAsset->AtkTexture.KernelTexture = texture;
|
||||
part.UldAsset->AtkTexture.TextureType = TextureType.KernelTexture;
|
||||
}
|
||||
|
||||
public void LoadTexture(IDalamudTextureWrap textureWrap) {
|
||||
var texturePointer = (Texture*)DalamudInterface.Instance.TextureProvider.ConvertToKernelTexture(textureWrap, true);
|
||||
if (texturePointer is null) return;
|
||||
|
||||
part.LoadTexture(texturePointer);
|
||||
}
|
||||
|
||||
private string GetLoadedPath() {
|
||||
if (part.UldAsset is null) return string.Empty;
|
||||
if (part.UldAsset->AtkTexture.Resource is null) return string.Empty;
|
||||
if (part.UldAsset->AtkTexture.Resource->TexFileResourceHandle is null) return string.Empty;
|
||||
|
||||
return part.UldAsset->AtkTexture.Resource->TexFileResourceHandle->FileName.ToString();
|
||||
}
|
||||
|
||||
private void TryUnloadTexture() {
|
||||
if (part.UldAsset is null) return;
|
||||
if (!part.UldAsset->AtkTexture.IsTextureReady()) return;
|
||||
if (part.UldAsset->AtkTexture.TextureType is 0) return;
|
||||
if (part.UldAsset->AtkTexture.KernelTexture is null) return;
|
||||
|
||||
part.UldAsset->AtkTexture.ReleaseTexture();
|
||||
part.UldAsset->AtkTexture.KernelTexture = null;
|
||||
part.UldAsset->AtkTexture.TextureType = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetThemePathModifier() => AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType switch {
|
||||
not 0 => $"uld/img{AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType:00}",
|
||||
_ => "uld",
|
||||
};
|
||||
|
||||
public static IconSubFolder GetIconSubFolder(uint iconId) {
|
||||
var textureManager = AtkStage.Instance()->AtkTextureResourceManager;
|
||||
Span<byte> buffer = stackalloc byte[0x100];
|
||||
buffer.Clear();
|
||||
var bytePointer = (byte*) Unsafe.AsPointer(ref buffer[0]);
|
||||
|
||||
var textureScale = textureManager->DefaultTextureScale;
|
||||
var targetFolder = (IconSubFolder)textureManager->IconLanguage;
|
||||
|
||||
// Try to resolve the path using the current language
|
||||
AtkTexture.GetIconPath(bytePointer, iconId, textureScale, targetFolder);
|
||||
var pathResult = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(bytePointer).String;
|
||||
|
||||
// If the resolved path doesn't exist, re-process with default folder
|
||||
return DalamudInterface.Instance.DataManager.FileExists(pathResult) ? targetFolder : IconSubFolder.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using FFXIVClientStructs.Attributes;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static unsafe class AtkUnitBaseExtensions {
|
||||
|
||||
public static string GetAddonTypeName<T>() where T : unmanaged {
|
||||
var type = typeof(T);
|
||||
var attribute = type.GetCustomAttributes().OfType<AddonAttribute>().FirstOrDefault();
|
||||
|
||||
if (attribute is null) throw new Exception("Unable to find AddonAttribute to resolve addon name.");
|
||||
var addonName = attribute.AddonIdentifiers.FirstOrDefault();
|
||||
|
||||
if (addonName is null) throw new Exception("Addon attribute names are empty.");
|
||||
return addonName;
|
||||
}
|
||||
|
||||
extension(ref AtkUnitBase addon) {
|
||||
public Vector2 Size => addon.GetSize();
|
||||
public Vector2 RootSize => addon.GetRootSize();
|
||||
public Vector2 Position => new(addon.X, addon.Y);
|
||||
|
||||
private Vector2 GetSize() {
|
||||
var width = stackalloc short[1];
|
||||
var height = stackalloc short[1];
|
||||
|
||||
addon.GetSize(width, height, false);
|
||||
return new Vector2(*width, *height);
|
||||
}
|
||||
|
||||
private Vector2 GetRootSize() {
|
||||
if (addon.RootNode is null) return Vector2.Zero;
|
||||
|
||||
return new Vector2(addon.RootNode->Width, addon.RootNode->Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static class ByteColorExtensions {
|
||||
public static Vector4 ToVector4(this ByteColor color)
|
||||
=> new(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, color.A / 255.0f);
|
||||
|
||||
public static ByteColor ToByteColor(this Vector4 v)
|
||||
=> new() { A = (byte)(v.W * 255), R = (byte)(v.X * 255), G = (byte)(v.Y * 255), B = (byte)(v.Z * 255) };
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
internal static class EnumExtensions {
|
||||
extension(Enum enumValue) {
|
||||
public string Description => enumValue.GetDescription();
|
||||
|
||||
private string GetDescription() {
|
||||
var attribute = enumValue.GetAttribute<DescriptionAttribute>();
|
||||
return attribute?.Description ?? enumValue.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
extension<T>(ref T flagValue) where T : unmanaged, Enum {
|
||||
public void SetFlags(params T[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
flagValue.SetFlag(flag, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearFlags(params T[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
flagValue.SetFlag(flag, false);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void SetFlag(T flag, bool enable) {
|
||||
switch (sizeof(T)) {
|
||||
case 1: flagValue.SetFlag<T, byte>(flag, enable); break;
|
||||
case 2: flagValue.SetFlag<T, ushort>(flag, enable); break;
|
||||
case 4: flagValue.SetFlag<T, uint>(flag, enable); break;
|
||||
case 8: flagValue.SetFlag<T, ulong>(flag, enable); break;
|
||||
default: throw new NotSupportedException("Unsupported enum size");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFlag<TUnderlying>(T flag, bool enable) where TUnderlying : unmanaged, IBinaryInteger<TUnderlying> {
|
||||
ref var value = ref Unsafe.As<T, TUnderlying>(ref flagValue);
|
||||
var mask = Unsafe.As<T, TUnderlying>(ref flag);
|
||||
|
||||
if (enable)
|
||||
value |= mask;
|
||||
else
|
||||
value &= ~mask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using Vector4 = System.Numerics.Vector4;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static class KnownColorExtensions {
|
||||
public static Vector3 Vector3(this KnownColor color) {
|
||||
var color4 = color.Vector();
|
||||
return new Vector3(color4.X, color4.Y, color4.Z);
|
||||
}
|
||||
|
||||
public static Vector3 AsVector3Color(this Vector4 vector4)
|
||||
=> new(vector4.X, vector4.Y, vector4.Z);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static unsafe class MainThreadSafety {
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <em>not</em> on the main thread. Use this to return early.
|
||||
/// </summary>
|
||||
public static bool TryAssertMainThread([CallerFilePath] string? callerFilePath = null, [CallerMemberName] string? callerName = null) {
|
||||
if (Framework.Instance()->IsDestroying) return true;
|
||||
|
||||
if (!ThreadSafety.IsMainThread) {
|
||||
Log.Error($"{callerFilePath?.Split(@"\")[^1][..^2]}{callerName} must be invoked from the main thread.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static class ReadOnlySpanExtensions {
|
||||
extension(ReadOnlySpan<byte> span) {
|
||||
public string String => Encoding.UTF8.GetString(span);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Diagnostics;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Extensions;
|
||||
|
||||
public static class StopwatchExtensions {
|
||||
extension(Stopwatch stopwatch) {
|
||||
public void LogTime(string logMessage) {
|
||||
DalamudInterface.Instance.Log.Debug($"{logMessage, -15}: {stopwatch, 15} :: {stopwatch.ElapsedMilliseconds} ms");
|
||||
stopwatch.Restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
global using KamiToolKit.Extensions;
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||
<!-- Project settings -->
|
||||
<PropertyGroup>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Dalamud.NET.Sdk settings -->
|
||||
<PropertyGroup>
|
||||
<Use_DalamudPackager>false</Use_DalamudPackager>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Assets -->
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\**\*.png" CopyToOutputDirectory="Always"/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- NuGet dependencies -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12"/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- FFXIVClientStructs -->
|
||||
<PropertyGroup Condition="Exists('..\FFXIVClientStructs')">
|
||||
<Use_Dalamud_FFXIVClientStructs>false</Use_Dalamud_FFXIVClientStructs>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="Exists('..\FFXIVClientStructs')">
|
||||
<ProjectReference Include="..\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" Private="True" />
|
||||
<ProjectReference Include="..\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj" Private="True" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,32 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nativeaddon/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodebase/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cbasic/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cbuttons/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cbuttons/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cdropdown/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Choldbutton/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cinputtext/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Clist/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cprogressbar/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cradiobutton/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cscrollbar/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Ctreelist/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ccomponent_005Cwindow/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cdropdown/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Choldbutton/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cicon/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cimage/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cinputtext/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Clayout/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Clist/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cninegrid/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cprogressbar/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cradiobutton/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cscrollbar/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Csimple/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Csimplenodes/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ctext/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Ctreelist/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nodes_005Cwindow/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KamiToolKit", "KamiToolKit.csproj", "{52A9E8E2-ACC7-4696-8684-5C4994D0350C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{52A9E8E2-ACC7-4696-8684-5C4994D0350C}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{52A9E8E2-ACC7-4696-8684-5C4994D0350C}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Plugin;
|
||||
using KamiToolKit.Classes;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public static class KamiToolKitLibrary {
|
||||
internal static bool IsInitialized { get; private set; }
|
||||
|
||||
internal static ConcurrentDictionary<nint, Type>? AllocatedNodes;
|
||||
|
||||
internal static string? DefaultWindowSubtitle;
|
||||
|
||||
/// <summary>
|
||||
/// Main initialization method for KamiToolKit. This method is required to be invoked before any KamiToolKit features are used.
|
||||
/// Failure to do so will not result in any direct warnings, but will result in undefined behavior.
|
||||
/// </summary>
|
||||
public static void Initialize(IDalamudPluginInterface pluginInterface, string? defaultWindowSubtitle = null) {
|
||||
IsInitialized = true;
|
||||
DefaultWindowSubtitle = defaultWindowSubtitle;
|
||||
|
||||
// Inject non-Experimental Properties
|
||||
pluginInterface.Inject(DalamudInterface.Instance);
|
||||
DalamudInterface.Instance.GameInteropProvider.InitializeFromAttributes(DalamudInterface.Instance);
|
||||
|
||||
// Create node data share
|
||||
AllocatedNodes = DalamudInterface.Instance.PluginInterface.GetOrCreateData("KamiToolKitAllocatedNodes", () => new ConcurrentDictionary<nint, Type>());
|
||||
|
||||
// Inject Experimental Properties
|
||||
pluginInterface.Inject(Experimental.Instance);
|
||||
DalamudInterface.Instance.GameInteropProvider.InitializeFromAttributes(Experimental.Instance);
|
||||
|
||||
Experimental.Instance.EnableHooks();
|
||||
|
||||
// Force enable Verbose so that users are able to get advanced logging information on request.
|
||||
DalamudInterface.Instance.Log.MinimumLogLevel = LogEventLevel.Verbose;
|
||||
|
||||
DalamudInterface.Instance.Log.Info($"KamiToolKit initialized for {pluginInterface.InternalName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alias for Cleanup
|
||||
/// </summary>
|
||||
public static void Dispose() => Cleanup();
|
||||
|
||||
/// <summary>
|
||||
/// Alias for Cleanup
|
||||
/// </summary>
|
||||
public static void Shutdown() => Cleanup();
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up any potentially leaked resources that KamiToolKit has allocated.
|
||||
/// </summary>
|
||||
public static void Cleanup() {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
|
||||
NodeBase.DisposeNodes();
|
||||
NativeAddon.DisposeAddons();
|
||||
|
||||
DalamudInterface.Instance.PluginInterface.RelinquishData("KamiToolKitAllocatedNodes");
|
||||
|
||||
Experimental.Instance.DisposeHooks();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 MidoriKami
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public unsafe partial class NativeAddon {
|
||||
private readonly JsonSerializerOptions serializerOptions = new() {
|
||||
WriteIndented = true,
|
||||
IncludeFields = true,
|
||||
};
|
||||
|
||||
private AddonConfig LoadAddonConfig() {
|
||||
var directory = DalamudInterface.Instance.PluginInterface.ConfigDirectory;
|
||||
var file = new FileInfo(Path.Combine(directory.FullName, $"{InternalName}.addon.json"));
|
||||
if (!file.Exists) {
|
||||
file.Create().Close();
|
||||
|
||||
var newConfig = new AddonConfig();
|
||||
SaveAddonConfig(newConfig);
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
AddonConfig? addonConfig;
|
||||
|
||||
try {
|
||||
var data = File.ReadAllText(file.FullName);
|
||||
addonConfig = JsonSerializer.Deserialize<AddonConfig>(data, serializerOptions);
|
||||
addonConfig ??= new AddonConfig();
|
||||
}
|
||||
catch (Exception e) {
|
||||
DalamudInterface.Instance.Log.Error(e, "Exception while deserializing AddonConfig, creating new config.");
|
||||
addonConfig = new AddonConfig();
|
||||
SaveAddonConfig(addonConfig);
|
||||
}
|
||||
|
||||
return addonConfig;
|
||||
}
|
||||
|
||||
private void SaveAddonConfig(AddonConfig addonConfig) {
|
||||
var directory = DalamudInterface.Instance.PluginInterface.ConfigDirectory;
|
||||
var file = new FileInfo(Path.Combine(directory.FullName, $"{InternalName}.addon.json"));
|
||||
|
||||
var data = JsonSerializer.Serialize(addonConfig, serializerOptions);
|
||||
|
||||
FilesystemUtil.WriteAllTextSafe(file.FullName, data);
|
||||
}
|
||||
|
||||
private void SaveAddonConfig() {
|
||||
var configData = new AddonConfig {
|
||||
Position = new Vector2(InternalAddon->X, InternalAddon->Y),
|
||||
Scale = InternalAddon->Scale / AtkUnitBase.GetGlobalUIScale(),
|
||||
};
|
||||
|
||||
SaveAddonConfig(configData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Linq;
|
||||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
|
||||
private static Hook<AtkUnitBase.Delegates.FireCallback>? fireCallbackHook;
|
||||
|
||||
private static void InitializeCloseCallback() {
|
||||
fireCallbackHook ??= DalamudInterface.Instance.GameInteropProvider
|
||||
.HookFromAddress<AtkUnitBase.Delegates.FireCallback>(AtkUnitBase.Addresses.FireCallback.Value, OnFireCallback);
|
||||
fireCallbackHook.Enable();
|
||||
}
|
||||
|
||||
private static bool OnFireCallback(AtkUnitBase* thisPtr, uint valueCount, AtkValue* values, bool close) {
|
||||
Log.Excessive($"[{thisPtr->NameString}] OnFireCallback");
|
||||
|
||||
foreach (var addon in CreatedAddons) {
|
||||
if (addon == thisPtr && close && addon is { RespectCloseAll: true, IsOverlayAddon: false }) {
|
||||
addon.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return fireCallbackHook!.Original(thisPtr, valueCount, values, close);
|
||||
}
|
||||
|
||||
private static void DisposeCloseCallback() {
|
||||
if (CreatedAddons.Count is 0 || CreatedAddons.All(addon => addon.IsOverlayAddon)) {
|
||||
fireCallbackHook?.Dispose();
|
||||
fireCallbackHook = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract partial class NativeAddon : IDisposable {
|
||||
|
||||
private static readonly List<NativeAddon> CreatedAddons = [];
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
public virtual void Dispose() {
|
||||
if (IsOverlayAddon) {
|
||||
// Intentionally leak OverlayAddons,
|
||||
// until Dalamud can implement OverlayAddons globally.
|
||||
CreatedAddons.Remove(this);
|
||||
GC.SuppressFinalize(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDisposed) {
|
||||
Log.Debug($"Disposing addon {GetType()}");
|
||||
|
||||
Close();
|
||||
|
||||
// Close will remove this node automatically on AtkUnitBase.Finalize,
|
||||
// However, this is after the plugin unloads,
|
||||
// and will trigger a warning in auto-dispose if we don't remove this now.
|
||||
CreatedAddons.Remove(this);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
isDisposed = true;
|
||||
DisposeCloseCallback();
|
||||
}
|
||||
|
||||
~NativeAddon() => Dispose();
|
||||
|
||||
internal static void DisposeAddons() {
|
||||
foreach (var addon in CreatedAddons.ToArray()) {
|
||||
if (addon.IsOverlayAddon) continue;
|
||||
|
||||
Log.Warning($"Addon {addon.GetType()} was not disposed properly please ensure you call dispose at an appropriate time.");
|
||||
Log.Debug($"Automatically disposing addon {addon.GetType()} as a safety measure.");
|
||||
|
||||
addon.Dispose();
|
||||
}
|
||||
|
||||
CreatedAddons.Clear();
|
||||
DisposeCloseCallback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public unsafe partial class NativeAddon {
|
||||
|
||||
private void UpdateFlags() {
|
||||
|
||||
// Disable Native AddonConfig
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x40, true);
|
||||
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A1, 0x4, DisableClose);
|
||||
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x8, DisableCloseTransition);
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x40, DisableAddonConfig);
|
||||
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x20, DisableClamping);
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x1, EnableContextMenu);
|
||||
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1C8, 0x800, DisableScaleContextOption);
|
||||
|
||||
if (IsOverlayAddon) {
|
||||
SetOverlayFlags();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetOverlayFlags() {
|
||||
|
||||
OpenWindowSoundEffectId = 0;
|
||||
InternalAddon->ShowSoundEffectId = 0;
|
||||
|
||||
// Disable ability to focus window
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A0, 0x80, true);
|
||||
|
||||
// Don't load into FocusedAddons list
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A1, 0x40, true);
|
||||
|
||||
// Disable Controller Nav
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x2, true);
|
||||
|
||||
// Disable open/close transitions
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x8, true);
|
||||
|
||||
// Disable open/close sounds
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A2, 0x20, true);
|
||||
|
||||
// Enable ClickThrough
|
||||
FlagHelper.UpdateFlag(ref InternalAddon->Flags1A3, 0x40, true);
|
||||
}
|
||||
|
||||
public bool DisableClose { get; init; }
|
||||
|
||||
public bool DisableCloseTransition { get; init; }
|
||||
|
||||
internal bool DisableAddonConfig { get; init; } = true;
|
||||
|
||||
public bool EnableContextMenu { get; init; } = true;
|
||||
|
||||
public bool DisableClamping { get; init; } = true;
|
||||
|
||||
public bool DisableScaleContextOption { get; init; }
|
||||
|
||||
public bool RespectCloseAll { get; set; } = true;
|
||||
|
||||
public bool IgnoreGlobalScale { get; set; } = false;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
|
||||
protected virtual void OnSetup(AtkUnitBase* addon) { }
|
||||
protected virtual void OnShow(AtkUnitBase* addon) { }
|
||||
protected virtual void OnDraw(AtkUnitBase* addon) { }
|
||||
protected virtual void OnUpdate(AtkUnitBase* addon) { }
|
||||
protected virtual void OnHide(AtkUnitBase* addon) { }
|
||||
protected virtual void OnFinalize(AtkUnitBase* addon) { }
|
||||
protected virtual void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { }
|
||||
protected virtual void OnRefresh(AtkUnitBase* addon, Span<AtkValue> atkValues) { }
|
||||
|
||||
private bool isSetup;
|
||||
|
||||
private void Initialize(AtkUnitBase* thisPtr) {
|
||||
Log.Verbose($"[{InternalName}] Initialize");
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Initialize(thisPtr);
|
||||
|
||||
thisPtr->UldManager.InitializeResourceRendererManager();
|
||||
|
||||
InitializeAddon();
|
||||
}
|
||||
|
||||
private void Setup(AtkUnitBase* addon, uint valueCount, AtkValue* values) {
|
||||
Log.Verbose($"[{InternalName}] Setup");
|
||||
|
||||
if (!IsOverlayAddon) {
|
||||
SetInitialState();
|
||||
}
|
||||
else {
|
||||
ref var screenSize = ref AtkStage.Instance()->ScreenSize;
|
||||
|
||||
addon->SetScale(1.0f / AtkUnitBase.GetGlobalUIScale(), true);
|
||||
addon->SetSize((ushort)screenSize.Width, (ushort)screenSize.Height);
|
||||
addon->SetPosition(0, 0);
|
||||
}
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->OnSetup(addon, valueCount, values);
|
||||
|
||||
OnSetup(addon);
|
||||
isSetup = true;
|
||||
}
|
||||
|
||||
private void Show(AtkUnitBase* addon, bool silenceOpenSoundEffect, uint unsetShowHideFlags) {
|
||||
Log.Verbose($"[{InternalName}] Show");
|
||||
|
||||
OnShow(addon);
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Show(addon, silenceOpenSoundEffect, unsetShowHideFlags);
|
||||
}
|
||||
|
||||
private void Update(AtkUnitBase* addon, float delta) {
|
||||
Log.Excessive($"[{InternalName}] Update");
|
||||
|
||||
OnUpdate(addon);
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Update(addon, delta);
|
||||
}
|
||||
|
||||
private void Draw(AtkUnitBase* addon) {
|
||||
Log.Excessive($"[{InternalName}] Draw");
|
||||
|
||||
OnDraw(addon);
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Draw(addon);
|
||||
}
|
||||
|
||||
private void Hide(AtkUnitBase* addon, bool unkBool, bool callHideCallback, uint setShowHideFlags) {
|
||||
Log.Verbose($"[{InternalName}] Hide");
|
||||
|
||||
OnHide(addon);
|
||||
SaveAddonConfig();
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Hide(addon, unkBool, callHideCallback, setShowHideFlags);
|
||||
AtkUnitBase.StaticVirtualTablePointer->Close(addon, false);
|
||||
}
|
||||
|
||||
private void Hide2(AtkUnitBase* addon) {
|
||||
Log.Verbose($"[{InternalName}] Hide2");
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Hide2(addon);
|
||||
}
|
||||
|
||||
private void Finalizer(AtkUnitBase* addon) {
|
||||
Log.Verbose($"[{InternalName}] Finalize");
|
||||
|
||||
OnFinalize(addon);
|
||||
|
||||
if (RememberClosePosition) {
|
||||
LastClosePosition = new Vector2(InternalAddon->X, InternalAddon->Y);
|
||||
}
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->Finalizer(InternalAddon);
|
||||
isSetup = false;
|
||||
}
|
||||
|
||||
private AtkEventListener* Destructor(AtkUnitBase* addon, byte flags) {
|
||||
Log.Verbose($"[{InternalName}] Destructor");
|
||||
|
||||
var result = AtkUnitBase.StaticVirtualTablePointer->Dtor(addon, flags);
|
||||
|
||||
if ((flags & 1) == 1) {
|
||||
InternalAddon = null;
|
||||
disposeHandle?.Free();
|
||||
disposeHandle = null;
|
||||
CreatedAddons.Remove(this);
|
||||
|
||||
// Free our custom virtual table, the game doesn't know this exists and won't clear it on its own.
|
||||
NativeMemoryHelper.Free(virtualTable, 0x8 * VirtualTableEntryCount);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void RequestedUpdate(AtkUnitBase* thisPtr, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) {
|
||||
Log.Verbose($"[{InternalName}] RequestedUpdate");
|
||||
|
||||
// Prevent calls to OnRequestedUpdate before Setup is completed. The game will try to call this after Show but before Setup
|
||||
if (isSetup) {
|
||||
OnRequestedUpdate(thisPtr, numberArrayData, stringArrayData);
|
||||
}
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->OnRequestedUpdate(InternalAddon, numberArrayData, stringArrayData);
|
||||
}
|
||||
|
||||
private bool Refresh(AtkUnitBase* thisPtr, uint valueCount, AtkValue* values) {
|
||||
Log.Verbose($"[{InternalName}] Refresh");
|
||||
|
||||
OnRefresh(thisPtr, new Span<AtkValue>(values, (int)valueCount));
|
||||
|
||||
return AtkUnitBase.StaticVirtualTablePointer->OnRefresh(InternalAddon, valueCount, values);
|
||||
}
|
||||
|
||||
private void ScreenSizeChange(AtkUnitBase* thisPtr, int width, int height) {
|
||||
Log.Verbose($"[{InternalName}] ScreenSizeChange");
|
||||
|
||||
AtkUnitBase.StaticVirtualTablePointer->OnScreenSizeChange(thisPtr, width, height);
|
||||
|
||||
if (IsOverlayAddon || IgnoreGlobalScale) {
|
||||
thisPtr->SetScale(1.0f / AtkUnitBase.GetGlobalUIScale(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
public void SetWindowPosition(Vector2 windowPosition) {
|
||||
if (InternalAddon is null) return;
|
||||
InternalAddon->SetPosition((short)windowPosition.X, (short)windowPosition.Y);
|
||||
}
|
||||
|
||||
public void SetWindowSize(Vector2 windowSize) {
|
||||
if (InternalAddon is null) return;
|
||||
|
||||
Size = windowSize;
|
||||
InternalAddon->SetSize((ushort)Size.X, (ushort)Size.Y);
|
||||
|
||||
WindowNode?.Size = Size;
|
||||
}
|
||||
|
||||
protected void SetWindowSize(float width, float height)
|
||||
=> SetWindowSize(new Vector2(width, height));
|
||||
|
||||
public required string InternalName {
|
||||
get;
|
||||
init => field = new string(value.Replace(" ", "").Take(31).ToArray());
|
||||
}
|
||||
|
||||
public required ReadOnlySeString Title { get; set; }
|
||||
|
||||
public ReadOnlySeString? Subtitle { get; set; }
|
||||
|
||||
public int OpenWindowSoundEffectId { get; set; } = 23;
|
||||
|
||||
public Vector2 Size { get; set; } = new(400.0f, 400.0f);
|
||||
|
||||
public Vector2 ContentStartPosition => (WindowNode?.ContentStartPosition ?? Vector2.Zero) + ContentPadding;
|
||||
|
||||
public Vector2 ContentSize => (WindowNode?.ContentSize ?? Vector2.Zero) - ContentPadding * 2.0f - new Vector2(0.0f, 4.0f);
|
||||
|
||||
public Vector2 ContentPadding { get; set; } = new(8.0f, 0.0f);
|
||||
|
||||
public int DepthLayer { get; init; } = 5;
|
||||
|
||||
public bool IsOpen => InternalAddon is not null && InternalAddon->IsVisible;
|
||||
|
||||
public int AddonId => InternalAddon is null ? 0 : InternalAddon->Id;
|
||||
|
||||
public bool RememberClosePosition { get; set; } = true;
|
||||
|
||||
internal Vector2 LastClosePosition = Vector2.Zero;
|
||||
|
||||
public static implicit operator AtkUnitBase*(NativeAddon addon) => addon.InternalAddon;
|
||||
|
||||
internal bool IsOverlayAddon { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
|
||||
private const int VirtualTableEntryCount = 200;
|
||||
|
||||
private AtkUnitBase.Delegates.Dtor destructorFunction = null!;
|
||||
private AtkUnitBase.Delegates.Draw drawFunction = null!;
|
||||
private AtkUnitBase.Delegates.Finalizer finalizerFunction = null!;
|
||||
private AtkUnitBase.Delegates.Hide hideFunction = null!;
|
||||
private AtkUnitBase.Delegates.Initialize initializeFunction = null!;
|
||||
private AtkUnitBase.Delegates.OnSetup onSetupFunction = null!;
|
||||
private AtkUnitBase.Delegates.Show showFunction = null!;
|
||||
private AtkUnitBase.Delegates.Hide2 softHideFunction = null!;
|
||||
private AtkUnitBase.Delegates.Update updateFunction = null!;
|
||||
private AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction = null!;
|
||||
private AtkUnitBase.Delegates.OnRefresh onRefreshFunction = null!;
|
||||
private AtkUnitBase.Delegates.OnScreenSizeChange onScreenSizeChangedFunction = null!;
|
||||
|
||||
private AtkUnitBase.AtkUnitBaseVirtualTable* virtualTable;
|
||||
|
||||
private void RegisterVirtualTable() {
|
||||
|
||||
// Overwrite virtual table with a custom copy,
|
||||
// Note: currently there are 73 vfuncs, but there's no harm in copying more for when they add new vfuncs to the game
|
||||
virtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)NativeMemoryHelper.Malloc(0x8 * VirtualTableEntryCount);
|
||||
NativeMemory.Copy(InternalAddon->VirtualTable, virtualTable, 0x8 * VirtualTableEntryCount);
|
||||
InternalAddon->VirtualTable = virtualTable;
|
||||
|
||||
initializeFunction = Initialize;
|
||||
onSetupFunction = Setup;
|
||||
showFunction = Show;
|
||||
updateFunction = Update;
|
||||
drawFunction = Draw;
|
||||
hideFunction = Hide;
|
||||
softHideFunction = Hide2;
|
||||
finalizerFunction = Finalizer;
|
||||
destructorFunction = Destructor;
|
||||
onRequestedUpdateFunction = RequestedUpdate;
|
||||
onRefreshFunction = Refresh;
|
||||
onScreenSizeChangedFunction = ScreenSizeChange;
|
||||
|
||||
virtualTable->Initialize = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(initializeFunction);
|
||||
virtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(onSetupFunction);
|
||||
virtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(showFunction);
|
||||
virtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(updateFunction);
|
||||
virtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(drawFunction);
|
||||
virtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(hideFunction);
|
||||
virtualTable->Hide2 = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(softHideFunction);
|
||||
virtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(finalizerFunction);
|
||||
virtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(destructorFunction);
|
||||
virtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(onRequestedUpdateFunction);
|
||||
virtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(onRefreshFunction);
|
||||
virtualTable->OnScreenSizeChange = (delegate* unmanaged<AtkUnitBase*, int, int, void>)Marshal.GetFunctionPointerForDelegate(onScreenSizeChangedFunction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NativeAddon {
|
||||
|
||||
private GCHandle? disposeHandle;
|
||||
|
||||
internal AtkUnitBase* InternalAddon;
|
||||
|
||||
public ResNode RootNode = null!;
|
||||
|
||||
protected WindowNodeBase? WindowNode { get; private set; }
|
||||
|
||||
private void AllocateAddon() {
|
||||
if (InternalAddon is not null) {
|
||||
Log.Warning("Tried to allocate addon that was already allocated.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentAddonCount = RaptureAtkUnitManager.Instance()->AllLoadedUnitsList.Count;
|
||||
if (currentAddonCount >= 200) {
|
||||
Log.Warning($"WARNING: Current Addon Count is approaching hard limits ({currentAddonCount}/250). Please ensure custom Addons are not being used as overlays.");
|
||||
}
|
||||
|
||||
if (currentAddonCount >= 225) {
|
||||
Log.Error($"ERROR: Current Addon Count is too high. Aborting allocation ({currentAddonCount}/250).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (InternalName.Length is 0) {
|
||||
throw new NullReferenceException("InternalName is empty, this is not allowed.");
|
||||
}
|
||||
|
||||
Log.Verbose($"[{InternalName}] Allocating NativeAddon");
|
||||
|
||||
if (!IsOverlayAddon) {
|
||||
InitializeCloseCallback();
|
||||
}
|
||||
|
||||
InternalAddon = NativeMemoryHelper.Create<AtkUnitBase>();
|
||||
|
||||
RegisterVirtualTable();
|
||||
|
||||
RootNode = new ResNode {
|
||||
NodeId = 1,
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.Fill | NodeFlags.Focusable | NodeFlags.EmitsEvents,
|
||||
IsAddonRootNode = true,
|
||||
};
|
||||
|
||||
if (!IsOverlayAddon) {
|
||||
WindowNode = CreateWindowNode?.Invoke() ?? new WindowNode();
|
||||
WindowNode.NodeId = 2;
|
||||
}
|
||||
|
||||
InternalAddon->NameString = InternalName;
|
||||
|
||||
InternalAddon->ShowSoundEffectId = (short)OpenWindowSoundEffectId;
|
||||
|
||||
UpdateFlags();
|
||||
}
|
||||
|
||||
private void InitializeAddon() {
|
||||
var widgetInfo = NativeMemoryHelper.UiAlloc<AtkUldWidgetInfo>(1, 16);
|
||||
widgetInfo->Id = 1;
|
||||
widgetInfo->NodeCount = 0;
|
||||
widgetInfo->NodeList = null;
|
||||
widgetInfo->WidgetAlignment = new AtkWidgetAlignment {
|
||||
AlignmentType = AlignmentType.Center,
|
||||
X = 50.0f,
|
||||
Y = 50.0f,
|
||||
};
|
||||
|
||||
InternalAddon->UldManager.Objects = (AtkUldObjectInfo*)widgetInfo;
|
||||
InternalAddon->UldManager.ObjectCount = 1;
|
||||
InternalAddon->UldManager.ResourceFlags |= AtkUldManagerResourceFlag.ArraysAllocated;
|
||||
|
||||
InternalAddon->RootNode = RootNode;
|
||||
InternalAddon->UldManager.AddNodeToObjectList(RootNode);
|
||||
|
||||
LoadTimeline();
|
||||
|
||||
InternalAddon->UldManager.UpdateDrawNodeList();
|
||||
InternalAddon->UldManager.LoadedState = AtkLoadState.Loaded;
|
||||
|
||||
if (!IsOverlayAddon && WindowNode is not null) {
|
||||
WindowNode.AttachNode(this, NodePosition.AsFirstChild);
|
||||
InternalAddon->WindowNode = WindowNode;
|
||||
InternalAddon->UldManager.AddNodeToObjectList(WindowNode);
|
||||
}
|
||||
|
||||
// UldManager finished loading the uld
|
||||
InternalAddon->Flags198 |= 2 << 0x1C;
|
||||
|
||||
// LoadUldByName called
|
||||
InternalAddon->Flags1A2 |= 4;
|
||||
|
||||
InternalAddon->UpdateCollisionNodeList(false);
|
||||
|
||||
// Set focus node to allow controller nav
|
||||
WindowNode?.WindowHeaderFocusNode.AddNodeFlags(NodeFlags.Focusable);
|
||||
InternalAddon->FocusNode = WindowNode is not null ? WindowNode.WindowHeaderFocusNode : RootNode;
|
||||
|
||||
// Now that we have constructed this instance, track it for auto-dispose
|
||||
CreatedAddons.Add(this);
|
||||
}
|
||||
|
||||
private void SetInitialState() {
|
||||
WindowNode?.SetTitle(Title.ToString(), Subtitle?.ToString() ?? KamiToolKitLibrary.DefaultWindowSubtitle);
|
||||
|
||||
InternalAddon->ShowSoundEffectId = (short)OpenWindowSoundEffectId;
|
||||
|
||||
var addonConfig = LoadAddonConfig();
|
||||
if (addonConfig.Position != Vector2.Zero) {
|
||||
InternalAddon->SetPosition((short)addonConfig.Position.X, (short)addonConfig.Position.Y);
|
||||
}
|
||||
else {
|
||||
var screenSize = new Vector2(AtkStage.Instance()->ScreenSize.Width, AtkStage.Instance()->ScreenSize.Height);
|
||||
var defaultPosition = screenSize / 2.0f - Size / 2.0f;
|
||||
InternalAddon->SetPosition((short)defaultPosition.X, (short)defaultPosition.Y);
|
||||
}
|
||||
|
||||
if (addonConfig.Scale is not 1.0f) {
|
||||
var newScale = Math.Clamp(addonConfig.Scale, 0.25f, 6.0f);
|
||||
|
||||
InternalAddon->SetScale(newScale, true);
|
||||
}
|
||||
|
||||
SetWindowSize(Size);
|
||||
|
||||
if (LastClosePosition != Vector2.Zero && RememberClosePosition) {
|
||||
InternalAddon->SetPosition((short)LastClosePosition.X, (short)LastClosePosition.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public Func<WindowNodeBase>? CreateWindowNode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes and Opens this instance of Addon
|
||||
/// </summary>
|
||||
public void Open() => DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
Log.Verbose($"[{InternalName}] Open Called");
|
||||
|
||||
if (InternalAddon is null) {
|
||||
AllocateAddon();
|
||||
|
||||
if (InternalAddon is not null) {
|
||||
AtkStage.Instance()->RaptureAtkUnitManager->InitializeAddon(InternalAddon, InternalName);
|
||||
InternalAddon->Open((uint)DepthLayer - 1);
|
||||
disposeHandle = GCHandle.Alloc(this);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.Verbose($"[{InternalName}] Already open, skipping call.");
|
||||
}
|
||||
});
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public void DebugOpen() => Open();
|
||||
|
||||
public void Close() {
|
||||
if (InternalAddon is null) return;
|
||||
|
||||
DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
Log.Verbose($"[{InternalName}] Close");
|
||||
|
||||
if (InternalAddon is not null) {
|
||||
InternalAddon->Close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Toggle() {
|
||||
if (IsOpen) {
|
||||
Close();
|
||||
}
|
||||
else {
|
||||
Open();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddNode(ICollection<NodeBase> nodes) {
|
||||
foreach (var node in nodes) {
|
||||
AddNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddNode(NodeBase? node)
|
||||
=> node?.AttachNode(this);
|
||||
|
||||
private void LoadTimeline() {
|
||||
RootNode.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, 89)
|
||||
.AddLabel(1, 101, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(10, 102, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(20, 103, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(30, 104, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(40, 105, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(50, 106, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(60, 107, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(70, 108, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.AddLabel(80, 109, AtkTimelineJumpBehavior.PlayOnce, 0)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase : IDisposable {
|
||||
|
||||
internal const uint NodeIdBase = 100_000_000;
|
||||
protected static readonly List<NodeBase> CreatedNodes = [];
|
||||
private static int logIndent = -1;
|
||||
|
||||
internal static uint CurrentOffset;
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
internal abstract AtkResNode* ResNode { get; }
|
||||
internal bool IsAddonRootNode;
|
||||
|
||||
private delegate* unmanaged<AtkResNode*, bool, void> originalDestructorFunction;
|
||||
private AtkResNode.Delegates.Destroy destructorFunction = null!;
|
||||
private AtkResNode.AtkResNodeVirtualTable* virtualTable;
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
logIndent++;
|
||||
LogIndented($"Beginning Dispose for {GetType()}");
|
||||
logIndent++;
|
||||
|
||||
if (MainThreadSafety.TryAssertMainThread()) {
|
||||
if (Framework.Instance()->IsDestroying) {
|
||||
LogIndented("Game is shutting down, aborting manual dispose.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDisposed) {
|
||||
LogIndented("Node was already disposed, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
isDisposed = true;
|
||||
|
||||
if (!IsNodeValid()) {
|
||||
Log.Warning("Invalid node, dispose aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
LogIndented("Disposing Children");
|
||||
foreach (var child in ChildNodes.ToList()) {
|
||||
child.Dispose();
|
||||
}
|
||||
LogIndented("Children Disposed");
|
||||
ChildNodes.Clear();
|
||||
|
||||
LogIndented("Disposing Tooltip Events");
|
||||
UnregisterTooltipEvents();
|
||||
|
||||
LogIndented("Clearing Native Focus");
|
||||
AtkStage.Instance()->ClearNodeFocus(ResNode);
|
||||
|
||||
LogIndented("Detaching From UI");
|
||||
DetachNode();
|
||||
|
||||
LogIndented("Disposing Timeline");
|
||||
Timeline?.Dispose();
|
||||
ResNode->Timeline = null;
|
||||
|
||||
LogIndented("Invoking Native Dispose");
|
||||
Dispose(true, false);
|
||||
GC.SuppressFinalize(this);
|
||||
CreatedNodes.Remove(this);
|
||||
|
||||
logIndent--;
|
||||
LogIndented("Dispose Complete");
|
||||
logIndent--;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
logIndent = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogIndented(string message)
|
||||
=> Log.Verbose(new string(' ', logIndent * 2) + message);
|
||||
|
||||
/// <summary>
|
||||
/// Warning, this is only to ensure there are no memory leaks.
|
||||
/// Ensure you have detached nodes safely from native ui before disposing.
|
||||
/// </summary>
|
||||
internal static void DisposeNodes() {
|
||||
var leakedNodeCount = CreatedNodes.Count(node => !node.IsAddonRootNode && node.ResNode is not null && node.ResNode->ParentNode is null);
|
||||
|
||||
if (leakedNodeCount is not 0) {
|
||||
Log.Warning($"There were {leakedNodeCount} node(s) that were not disposed safely.");
|
||||
}
|
||||
|
||||
foreach (var node in CreatedNodes.ToArray()) {
|
||||
if (node.ResNode is null) continue;
|
||||
if (node.ResNode->ParentNode is not null) continue;
|
||||
if (node.IsAddonRootNode) continue;
|
||||
|
||||
Log.Warning($"Forcing disposal of: {node.GetType()}");
|
||||
node.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
~NodeBase() => Dispose(false, false);
|
||||
|
||||
/// <summary>
|
||||
/// Dispose associated resources. If a resource modifies native state directly guard it with isNativeDestructor
|
||||
/// </summary>
|
||||
/// <param name="disposing">
|
||||
/// Indicates if this specific call should dispose resources or not. This protects against double dispose,
|
||||
/// or incorrectly manipulating native state too many times.
|
||||
/// </param>
|
||||
/// <param name="isNativeDestructor">
|
||||
/// Indicates if the dispose call should try to completely clean up all resources,
|
||||
/// or if it should only clean up managed resources. When false, be sure to only dispose
|
||||
/// resources that exist in managed spaces, as the game has already cleaned up everything else.
|
||||
/// </param>
|
||||
protected virtual void Dispose(bool disposing, bool isNativeDestructor) {
|
||||
|
||||
// Dispose of managed resources that must be disposed regardless of how dispose is invoked
|
||||
DisposeEvents();
|
||||
DisableEditMode(NodeEditMode.Move | NodeEditMode.Resize);
|
||||
}
|
||||
|
||||
private bool IsNodeValid() {
|
||||
if (ResNode is null) return false;
|
||||
if (ResNode->VirtualTable is null) return false;
|
||||
if (ResNode->VirtualTable == AtkEventTarget.StaticVirtualTablePointer) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static implicit operator AtkResNode*(NodeBase node) => node.ResNode;
|
||||
public static implicit operator AtkEventTarget*(NodeBase node) => &node.ResNode->AtkEventTarget;
|
||||
|
||||
protected void BuildVirtualTable() {
|
||||
// Back up original destructor pointer
|
||||
originalDestructorFunction = ResNode->VirtualTable->Destroy;
|
||||
|
||||
// Overwrite virtual table with a custom copy,
|
||||
// Note: Currently there are only 2 vfuncs, but there's no harm in copying more for if they ever add more vfuncs to the game.
|
||||
virtualTable = (AtkResNode.AtkResNodeVirtualTable*)NativeMemoryHelper.Malloc(0x8 * 4);
|
||||
NativeMemory.Copy(ResNode->VirtualTable, virtualTable, 0x8 * 4);
|
||||
ResNode->VirtualTable = virtualTable;
|
||||
|
||||
// Pin managed function to virtual table entry
|
||||
destructorFunction = DestructorDetour;
|
||||
|
||||
// Replace native destructor with
|
||||
virtualTable->Destroy = (delegate* unmanaged<AtkResNode*, bool, void>) Marshal.GetFunctionPointerForDelegate(destructorFunction);
|
||||
}
|
||||
|
||||
private void DestructorDetour(AtkResNode* thisPtr, bool free) {
|
||||
Dispose(true, true);
|
||||
InvokeOriginalDestructor(thisPtr, free);
|
||||
|
||||
Log.Verbose($"Native has disposed node {GetType()}");
|
||||
GC.SuppressFinalize(this);
|
||||
CreatedNodes.Remove(this);
|
||||
|
||||
isDisposed = true;
|
||||
}
|
||||
|
||||
protected void InvokeOriginalDestructor(AtkResNode* thisPtr, bool free) {
|
||||
if (virtualTable is null) return; // Shouldn't be possible, but just in case.
|
||||
|
||||
originalDestructorFunction(thisPtr, free);
|
||||
NativeMemoryHelper.Free(virtualTable, 0x8 * 4);
|
||||
virtualTable = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Dalamud.Game.Addon.Events;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
|
||||
private Vector2 clickStartPosition = Vector2.Zero;
|
||||
private NodeEditMode currentEditMode = 0;
|
||||
|
||||
private ViewportEventListener? editEventListener;
|
||||
|
||||
private bool isCursorSet;
|
||||
|
||||
private bool isMoving;
|
||||
private bool isResizing;
|
||||
|
||||
private NodeEditOverlayNode? overlayNode;
|
||||
|
||||
public Action<NodeBase>? OnResizeComplete { get; set; }
|
||||
public Action<NodeBase>? OnMoveComplete { get; set; }
|
||||
public Action<NodeBase>? OnEditComplete { get; set; }
|
||||
|
||||
public bool EnableMoving {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value) {
|
||||
EnableEditMode(NodeEditMode.Move);
|
||||
}
|
||||
else {
|
||||
DisableEditMode(NodeEditMode.Move);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableResizing {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value) {
|
||||
EnableEditMode(NodeEditMode.Resize);
|
||||
}
|
||||
else {
|
||||
DisableEditMode(NodeEditMode.Resize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EnableEditMode(NodeEditMode mode) {
|
||||
|
||||
currentEditMode |= mode;
|
||||
|
||||
if (overlayNode is null) {
|
||||
overlayNode = new NodeEditOverlayNode {
|
||||
Position = new Vector2(-16.0f, -16.0f),
|
||||
Size = Size + new Vector2(32.0f, 32.0f),
|
||||
};
|
||||
overlayNode.AttachNode(this);
|
||||
ChildNodes.Add(overlayNode);
|
||||
}
|
||||
|
||||
overlayNode.ShowParts = currentEditMode.HasFlag(NodeEditMode.Resize);
|
||||
|
||||
if (editEventListener is null) {
|
||||
editEventListener = new ViewportEventListener(OnEditEvent);
|
||||
editEventListener.AddEvent(AtkEventType.MouseMove, overlayNode);
|
||||
editEventListener.AddEvent(AtkEventType.MouseDown, overlayNode);
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableEditMode(NodeEditMode mode) {
|
||||
|
||||
currentEditMode &= ~mode;
|
||||
|
||||
if (currentEditMode.HasFlag(NodeEditMode.Resize) || currentEditMode.HasFlag(NodeEditMode.Move)) return;
|
||||
|
||||
if (editEventListener is not null) {
|
||||
editEventListener.RemoveEvent(AtkEventType.MouseMove);
|
||||
editEventListener.RemoveEvent(AtkEventType.MouseDown);
|
||||
editEventListener.Dispose();
|
||||
editEventListener = null;
|
||||
}
|
||||
|
||||
if (overlayNode is not null) {
|
||||
ChildNodes.Remove(overlayNode);
|
||||
overlayNode.DetachNode();
|
||||
overlayNode.Dispose();
|
||||
overlayNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEditEvent(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
if (overlayNode is null) return;
|
||||
if (editEventListener is null) return;
|
||||
|
||||
ref var mouseData = ref atkEventData->MouseData;
|
||||
var mousePosition = new Vector2(mouseData.PosX, mouseData.PosY);
|
||||
var mouseDelta = mousePosition - clickStartPosition;
|
||||
|
||||
switch (eventType) {
|
||||
// Move Logic
|
||||
case AtkEventType.MouseMove when isMoving: {
|
||||
Position += mouseDelta;
|
||||
clickStartPosition = mousePosition;
|
||||
|
||||
atkEvent->SetEventIsHandled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// Update hover state when not resizing, as we latch that for the behavior
|
||||
case AtkEventType.MouseMove when !isResizing: {
|
||||
overlayNode.UpdateHover(atkEventData);
|
||||
}
|
||||
break;
|
||||
|
||||
// Resize Logic
|
||||
case AtkEventType.MouseMove when isResizing: {
|
||||
Position += overlayNode.GetPositionDelta(mouseDelta);
|
||||
Size += overlayNode.GetSizeDelta(mouseDelta);
|
||||
|
||||
overlayNode.Size = Size + new Vector2(32.0f, 32.0f);
|
||||
|
||||
clickStartPosition = mousePosition;
|
||||
|
||||
atkEvent->SetEventIsHandled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// Begin Resize Event
|
||||
case AtkEventType.MouseDown when !isResizing && overlayNode.AnyHovered() && currentEditMode.HasFlag(NodeEditMode.Resize): {
|
||||
editEventListener.AddEvent(AtkEventType.MouseUp, overlayNode);
|
||||
|
||||
isResizing = true;
|
||||
clickStartPosition = mousePosition;
|
||||
|
||||
atkEvent->SetEventIsHandled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// End Resize Event
|
||||
case AtkEventType.MouseUp when isResizing: {
|
||||
OnResizeComplete?.Invoke(this);
|
||||
OnEditComplete?.Invoke(this);
|
||||
|
||||
isResizing = false;
|
||||
editEventListener.RemoveEvent(AtkEventType.MouseUp);
|
||||
}
|
||||
break;
|
||||
|
||||
// Begin Move Event
|
||||
case AtkEventType.MouseDown when !overlayNode.AnyHovered() && overlayNode.CheckCollision(atkEventData) && !isMoving && currentEditMode.HasFlag(NodeEditMode.Move): {
|
||||
editEventListener.AddEvent(AtkEventType.MouseUp, overlayNode);
|
||||
|
||||
isMoving = true;
|
||||
clickStartPosition = mousePosition;
|
||||
|
||||
atkEvent->SetEventIsHandled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// End Move Event
|
||||
case AtkEventType.MouseUp when isMoving: {
|
||||
OnMoveComplete?.Invoke(this);
|
||||
OnEditComplete?.Invoke(this);
|
||||
|
||||
isMoving = false;
|
||||
editEventListener.RemoveEvent(AtkEventType.MouseUp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (isCursorSet) {
|
||||
ResetCursor();
|
||||
isCursorSet = false;
|
||||
}
|
||||
|
||||
if (currentEditMode.HasFlag(NodeEditMode.Move)) {
|
||||
if (isMoving) {
|
||||
SetCursor(AddonCursorType.Grab);
|
||||
isCursorSet = true;
|
||||
}
|
||||
else if (CheckCollision(atkEventData)) {
|
||||
SetCursor(AddonCursorType.Hand);
|
||||
isCursorSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (overlayNode.AnyHovered() && currentEditMode.HasFlag(NodeEditMode.Resize)) {
|
||||
overlayNode.SetCursor();
|
||||
isCursorSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetCursor(AddonCursorType cursor)
|
||||
=> DalamudInterface.Instance.AddonEventManager.SetCursor(cursor);
|
||||
|
||||
private static void ResetCursor()
|
||||
=> DalamudInterface.Instance.AddonEventManager.ResetCursor();
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
internal class EventHandlerInfo {
|
||||
public AtkEventListener.Delegates.ReceiveEvent? OnReceiveEventDelegate;
|
||||
public Action? OnActionDelegate;
|
||||
}
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
|
||||
private CustomEventListener? nodeEventListener;
|
||||
private readonly Dictionary<AtkEventType, EventHandlerInfo> eventHandlers = [];
|
||||
|
||||
/// <summary>
|
||||
/// When true, mousing over this node will show the finger cursor icon.
|
||||
/// </summary>
|
||||
public bool ShowClickableCursor {
|
||||
get => DrawFlags.HasFlag(DrawFlags.ClickableCursor);
|
||||
set {
|
||||
if (value) {
|
||||
DrawFlags |= DrawFlags.ClickableCursor;
|
||||
}
|
||||
else {
|
||||
DrawFlags &= ~DrawFlags.ClickableCursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When true, mousing over this node will show the text input cursor icon.
|
||||
/// </summary>
|
||||
public bool ShowTextInputCursor {
|
||||
get => DrawFlags.HasFlag(DrawFlags.TextInputCursor);
|
||||
set {
|
||||
if (value) {
|
||||
DrawFlags |= DrawFlags.TextInputCursor;
|
||||
}
|
||||
else {
|
||||
DrawFlags &= ~DrawFlags.TextInputCursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEvent(AtkEventType eventType, Action callback) {
|
||||
nodeEventListener ??= new CustomEventListener(HandleEvents);
|
||||
|
||||
SetNodeEventFlags(eventType);
|
||||
|
||||
if (eventHandlers.TryAdd(eventType, new EventHandlerInfo { OnActionDelegate = callback })) {
|
||||
Log.Verbose($"[{eventType}] Registered for {GetType()} [{(nint)ResNode:X}]");
|
||||
ResNode->AtkEventManager.RegisterEvent(eventType, 0, this, this, nodeEventListener, false);
|
||||
}
|
||||
else {
|
||||
eventHandlers[eventType].OnActionDelegate += callback;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEvent(AtkEventType eventType, AtkEventListener.Delegates.ReceiveEvent callback) {
|
||||
nodeEventListener ??= new CustomEventListener(HandleEvents);
|
||||
|
||||
SetNodeEventFlags(eventType);
|
||||
|
||||
if (eventHandlers.TryAdd(eventType, new EventHandlerInfo { OnReceiveEventDelegate = callback })) {
|
||||
Log.Verbose($"[{eventType}] Registered for {GetType()} [{(nint)ResNode:X}]");
|
||||
ResNode->AtkEventManager.RegisterEvent(eventType, 0, this, this, nodeEventListener, false);
|
||||
}
|
||||
else {
|
||||
eventHandlers[eventType].OnReceiveEventDelegate += callback;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveEvent(AtkEventType eventType) {
|
||||
if (nodeEventListener is null) return;
|
||||
|
||||
if (eventHandlers.Remove(eventType)) {
|
||||
Log.Verbose($"[{eventType}] Unregistered from {GetType()} [{(nint)ResNode:X}]");
|
||||
ResNode->AtkEventManager.UnregisterEvent(eventType, 0, nodeEventListener, false);
|
||||
}
|
||||
|
||||
// If we have removed the last event, free the event listener
|
||||
if (eventHandlers.Keys.Count is 0) {
|
||||
nodeEventListener.Dispose();
|
||||
nodeEventListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveEvent(AtkEventType eventType, Action callback) {
|
||||
if (nodeEventListener is null) return;
|
||||
|
||||
if (eventHandlers.TryGetValue(eventType, out var handler)) {
|
||||
handler.OnActionDelegate -= callback;
|
||||
|
||||
if (handler.OnReceiveEventDelegate is null && handler.OnActionDelegate is null) {
|
||||
RemoveEvent(eventType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveEvent(AtkEventType eventType, AtkEventListener.Delegates.ReceiveEvent callback) {
|
||||
if (nodeEventListener is null) return;
|
||||
|
||||
if (eventHandlers.TryGetValue(eventType, out var handler)) {
|
||||
handler.OnReceiveEventDelegate -= callback;
|
||||
|
||||
if (handler.OnReceiveEventDelegate is null && handler.OnActionDelegate is null) {
|
||||
RemoveEvent(eventType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeEvents() {
|
||||
if (nodeEventListener is not null) {
|
||||
ResNode->AtkEventManager.UnregisterEvent(AtkEventType.UnregisterAll, 0, nodeEventListener, false);
|
||||
}
|
||||
|
||||
eventHandlers.Clear();
|
||||
|
||||
nodeEventListener?.Dispose();
|
||||
nodeEventListener = null;
|
||||
}
|
||||
|
||||
private void HandleEvents(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
try {
|
||||
if (!IsVisible) return;
|
||||
|
||||
if (eventHandlers.TryGetValue(eventType, out var handler)) {
|
||||
|
||||
foreach (var noArgHandler in Delegate.EnumerateInvocationList(handler.OnActionDelegate)) {
|
||||
try {
|
||||
noArgHandler();
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var argHandler in Delegate.EnumerateInvocationList(handler.OnReceiveEventDelegate)) {
|
||||
try {
|
||||
argHandler(thisPtr, eventType, eventParam, atkEvent, atkEventData);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetNodeEventFlags(AtkEventType eventType) {
|
||||
switch (eventType) {
|
||||
// Hover events need to propagate down to trigger various timelines
|
||||
case AtkEventType.MouseOver:
|
||||
case AtkEventType.MouseOut:
|
||||
case AtkEventType.MouseWheel:
|
||||
AddNodeFlags(NodeFlags.EmitsEvents, NodeFlags.RespondToMouse);
|
||||
break;
|
||||
|
||||
// Any kind of direct interaction should be a blocking event
|
||||
// set HasCollision to prevent events from propagating
|
||||
case AtkEventType.MouseDown:
|
||||
case AtkEventType.MouseUp:
|
||||
case AtkEventType.MouseMove:
|
||||
case AtkEventType.MouseClick:
|
||||
AddNodeFlags(NodeFlags.EmitsEvents, NodeFlags.RespondToMouse, NodeFlags.HasCollision);
|
||||
break;
|
||||
|
||||
// ButtonClick is mostly used as an event that native calls back to, when interacting with buttons
|
||||
// We do not want to re-emit, or block events in this case
|
||||
case AtkEventType.ButtonClick:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
|
||||
internal readonly List<NodeBase> ChildNodes = [];
|
||||
private NodeBase? parentNode;
|
||||
|
||||
internal AtkUldManager* ParentUldManager { get; set; }
|
||||
internal AtkUnitBase* ParentAddon { get; private set; }
|
||||
|
||||
[OverloadResolutionPriority(1)]
|
||||
public void AttachNode(NativeAddon? targetAddon, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformManagedAttach(targetAddon, targetPosition);
|
||||
|
||||
public void AttachNode(AtkUnitBase* targetAddon, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach(targetAddon is not null ? targetAddon->RootNode : null, targetPosition);
|
||||
|
||||
[OverloadResolutionPriority(1)]
|
||||
public void AttachNode(NodeBase? targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformManagedAttach(targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkResNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach(targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkImageNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkTextNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkNineGridNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkCounterNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkCollisionNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkClippingMaskNode* targetNode, NodePosition targetPosition = NodePosition.AsLastChild)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
public void AttachNode(AtkComponentNode* targetNode, NodePosition targetPosition = NodePosition.AfterAllSiblings)
|
||||
=> PerformNativeAttach((AtkResNode*)targetNode, targetPosition);
|
||||
|
||||
private void PerformManagedAttach(NativeAddon? targetAddon, NodePosition targetPosition = NodePosition.AsLastChild) {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
if (targetAddon is null) return;
|
||||
|
||||
// Check the Addon's node list to find out what NodeId we should be, and set that before attaching
|
||||
if (NodeId > NodeIdBase) {
|
||||
NodeId = targetAddon.InternalAddon->UldManager.GetMaxNodeId() + 1;
|
||||
}
|
||||
|
||||
PerformNativeAttach(targetAddon.RootNode, targetPosition);
|
||||
|
||||
parentNode = targetAddon.RootNode;
|
||||
parentNode.ChildNodes.Add(this);
|
||||
}
|
||||
|
||||
private void PerformManagedAttach(NodeBase? targetNode, NodePosition targetPosition) {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
if (targetNode is null) return;
|
||||
|
||||
PerformNativeAttach(targetNode, targetPosition);
|
||||
|
||||
parentNode = targetNode;
|
||||
parentNode.ChildNodes.Add(this);
|
||||
}
|
||||
|
||||
private void PerformNativeAttach(AtkResNode* targetNode, NodePosition targetPosition) {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
if (targetNode is null) return;
|
||||
|
||||
if (targetNode->GetNodeType() is NodeType.Component) {
|
||||
|
||||
// If target is a ComponentNode,
|
||||
// then we don't ever wanna be a child of the ComponentNode itself,
|
||||
// we will want to be a sibling of the root node.
|
||||
// Therefore, redirect the target position to be siblings.
|
||||
targetPosition = targetPosition switch {
|
||||
NodePosition.AsLastChild => NodePosition.AfterAllSiblings,
|
||||
NodePosition.AsFirstChild => NodePosition.BeforeAllSiblings,
|
||||
_ => targetPosition,
|
||||
};
|
||||
|
||||
// If however, we are using BeforeTarget or AfterTarget,
|
||||
// then we do want to attach to the ComponentNode
|
||||
// else, attach to its root node.
|
||||
var componentNode = targetNode->GetAsAtkComponentNode();
|
||||
if (componentNode is not null) {
|
||||
targetNode = targetPosition switch {
|
||||
NodePosition.AfterTarget => targetNode,
|
||||
NodePosition.BeforeTarget => targetNode,
|
||||
NodePosition.AfterAllSiblings => componentNode->Component->UldManager.RootNode,
|
||||
NodePosition.BeforeAllSiblings => componentNode->Component->UldManager.RootNode,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(targetPosition), targetPosition, null),
|
||||
};
|
||||
|
||||
// We also need to check the components node list, to get a safely assigned nodeId
|
||||
if (NodeId > NodeIdBase) {
|
||||
NodeId = componentNode->Component->UldManager.GetMaxNodeId() + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeLinker.AttachNode(this, targetNode, targetPosition);
|
||||
UpdateParentAddon(targetNode);
|
||||
UpdateNative();
|
||||
}
|
||||
|
||||
internal void ReattachNode(AtkResNode* newTarget) {
|
||||
if (newTarget is null) return;
|
||||
|
||||
DetachNode();
|
||||
AttachNode(newTarget);
|
||||
}
|
||||
|
||||
public void DetachNode() {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
if (ResNode is null) return;
|
||||
|
||||
UnlinkFromNative();
|
||||
RemoveUldManagerObjectReferences();
|
||||
RemoveParentAddonReferences();
|
||||
RemoveParentNodeReferences();
|
||||
}
|
||||
|
||||
private void UnlinkFromNative() {
|
||||
NodeLinker.DetachNode(ResNode);
|
||||
ResNode->ParentNode = null;
|
||||
ResNode->NextSiblingNode = null;
|
||||
ResNode->PrevSiblingNode = null;
|
||||
}
|
||||
|
||||
private void RemoveUldManagerObjectReferences() {
|
||||
if (ParentUldManager is null) return;
|
||||
|
||||
ParentUldManager->RemoveNodeFromObjectList(this);
|
||||
ParentUldManager = null;
|
||||
}
|
||||
|
||||
private void RemoveParentAddonReferences() {
|
||||
if (ParentAddon is null) return;
|
||||
|
||||
ParentAddon->UldManager.UpdateDrawNodeList();
|
||||
ParentAddon->UpdateCollisionNodeList(false);
|
||||
|
||||
ParentAddon = null;
|
||||
|
||||
foreach (var child in GetAllChildren(this)) {
|
||||
child.ParentAddon = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveParentNodeReferences() {
|
||||
if (parentNode is null) return;
|
||||
|
||||
parentNode.ChildNodes.Remove(this);
|
||||
parentNode = null;
|
||||
}
|
||||
|
||||
private void UpdateNative() {
|
||||
if (ResNode is null) return;
|
||||
|
||||
MarkDirty();
|
||||
|
||||
if (ParentUldManager is null) {
|
||||
ParentUldManager = GetUldManagerForNode(ResNode);
|
||||
}
|
||||
|
||||
if (ParentUldManager is not null) {
|
||||
ParentUldManager->AddNodeToObjectList(this);
|
||||
}
|
||||
|
||||
if (ParentAddon is not null) {
|
||||
if (ParentAddon->NameString is "NamePlate") {
|
||||
Log.Warning("Warning, attaching to AddonNamePlate is not supported. Use OverlayController instead.");
|
||||
}
|
||||
|
||||
ParentAddon->UldManager.UpdateDrawNodeList();
|
||||
ParentAddon->UpdateCollisionNodeList(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateParentAddon(AtkResNode* node) {
|
||||
if (parentNode is not null && parentNode.ParentAddon is not null) {
|
||||
ParentAddon = parentNode.ParentAddon;
|
||||
}
|
||||
else if (ParentAddon is null) {
|
||||
var targetParentAddon = RaptureAtkUnitManager.Instance()->GetAddonByNode(node);
|
||||
if (targetParentAddon is not null) {
|
||||
ParentAddon = targetParentAddon;
|
||||
}
|
||||
}
|
||||
|
||||
if (ParentAddon is not null) {
|
||||
foreach (var child in GetAllChildren(this)) {
|
||||
child.ParentAddon = ParentAddon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AtkUldManager* GetUldManagerForNode(AtkResNode* node) {
|
||||
if (node is null) return null;
|
||||
|
||||
var targetNode = node;
|
||||
|
||||
if (targetNode->GetNodeType() is NodeType.Component) {
|
||||
targetNode = targetNode->ParentNode;
|
||||
}
|
||||
|
||||
// Try to get UldManager via the first parent that is a component
|
||||
while (targetNode is not null) {
|
||||
if (targetNode->GetNodeType() is NodeType.Component) {
|
||||
var componentNode = (AtkComponentNode*)targetNode;
|
||||
return &componentNode->Component->UldManager;
|
||||
}
|
||||
|
||||
targetNode = targetNode->ParentNode;
|
||||
}
|
||||
|
||||
// We failed to find a parent component, try to get a parent addon instead
|
||||
if (ParentAddon is not null) {
|
||||
return &ParentAddon->UldManager;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<NodeBase> GetAllChildren(NodeBase parent) {
|
||||
foreach (var child in parent.ChildNodes) {
|
||||
yield return child;
|
||||
foreach (var childNode in GetAllChildren(child)) {
|
||||
yield return childNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerable<NodeBase> GetLocalChildren(NodeBase parent) {
|
||||
if (parent is ComponentNode) yield break;
|
||||
|
||||
foreach (var child in parent.ChildNodes) {
|
||||
yield return child;
|
||||
|
||||
if (child is ComponentNode) continue;
|
||||
foreach (var childNode in GetLocalChildren(child)) {
|
||||
yield return childNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using System;
|
||||
using Dalamud.Interface;
|
||||
using FFXIVClientStructs.FFXIV.Common.Math;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Enums;
|
||||
using Bounds = KamiToolKit.Classes.Bounds;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
using Vector3 = System.Numerics.Vector3;
|
||||
using Vector4 = System.Numerics.Vector4;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
public virtual float X {
|
||||
get => ResNode->GetXFloat();
|
||||
set => ResNode->SetXFloat(value);
|
||||
}
|
||||
|
||||
public virtual float Y {
|
||||
get => ResNode->GetYFloat();
|
||||
set => ResNode->SetYFloat(value);
|
||||
}
|
||||
|
||||
public virtual Vector2 Position {
|
||||
get => ResNode->Position;
|
||||
set => ResNode->Position = value;
|
||||
}
|
||||
|
||||
public virtual float ScreenX {
|
||||
get => ResNode->ScreenX;
|
||||
set => ResNode->ScreenX = value;
|
||||
}
|
||||
|
||||
public virtual float ScreenY {
|
||||
get => ResNode->ScreenY;
|
||||
set => ResNode->ScreenY = value;
|
||||
}
|
||||
|
||||
public virtual Vector2 ScreenPosition
|
||||
=> ResNode->ScreenPosition;
|
||||
|
||||
public virtual float Width {
|
||||
get => ResNode->GetWidth();
|
||||
set {
|
||||
ResNode->SetWidth((ushort)value);
|
||||
OnSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual float Height {
|
||||
get => ResNode->GetHeight();
|
||||
set {
|
||||
ResNode->SetHeight((ushort)value);
|
||||
OnSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Vector2 Size {
|
||||
get => ResNode->Size;
|
||||
set {
|
||||
ResNode->SetWidth((ushort)value.X);
|
||||
ResNode->SetHeight((ushort)value.Y);
|
||||
OnSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Bounds Bounds
|
||||
=> ResNode->Bounds;
|
||||
|
||||
public Vector2 Center
|
||||
=> ResNode->Center;
|
||||
|
||||
public virtual float ScaleX {
|
||||
get => ResNode->GetScaleX();
|
||||
set => ResNode->SetScaleX(value);
|
||||
}
|
||||
|
||||
public virtual float ScaleY {
|
||||
get => ResNode->GetScaleY();
|
||||
set => ResNode->SetScaleY(value);
|
||||
}
|
||||
|
||||
public virtual Vector2 Scale {
|
||||
get => ResNode->Scale;
|
||||
set => ResNode->Scale = value;
|
||||
}
|
||||
|
||||
public virtual float Rotation {
|
||||
get => ResNode->GetRotation();
|
||||
set => ResNode->SetRotation(value);
|
||||
}
|
||||
|
||||
public virtual float RotationDegrees {
|
||||
get => ResNode->RotationDegrees;
|
||||
set => ResNode->RotationDegrees = value;
|
||||
}
|
||||
|
||||
public virtual float OriginX {
|
||||
get => ResNode->OriginX;
|
||||
set => ResNode->OriginX = value;
|
||||
}
|
||||
|
||||
public virtual float OriginY {
|
||||
get => ResNode->OriginY;
|
||||
set => ResNode->OriginY = value;
|
||||
}
|
||||
|
||||
public virtual Vector2 Origin {
|
||||
get => ResNode->Origin;
|
||||
set => ResNode->Origin = value;
|
||||
}
|
||||
|
||||
private bool? lastIsVisible;
|
||||
|
||||
public virtual bool IsVisible {
|
||||
get => ResNode->Visible;
|
||||
set {
|
||||
ResNode->Visible = value;
|
||||
if (lastIsVisible is null || lastIsVisible != value) {
|
||||
OnVisibilityToggled?.Invoke(value);
|
||||
lastIsVisible = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Action<bool>? OnVisibilityToggled { get; set; }
|
||||
|
||||
public NodeFlags NodeFlags {
|
||||
get => ResNode->NodeFlags;
|
||||
set => ResNode->NodeFlags = value;
|
||||
}
|
||||
|
||||
public virtual Vector4 Color {
|
||||
get => ResNode->ColorVector;
|
||||
set => ResNode->ColorVector = value;
|
||||
}
|
||||
|
||||
public virtual ColorHelpers.HsvaColor ColorHsva {
|
||||
get => ResNode->ColorHsva;
|
||||
set => ResNode->ColorHsva = value;
|
||||
}
|
||||
|
||||
public virtual float Alpha {
|
||||
get => ResNode->Color.A;
|
||||
set => ResNode->SetAlpha((byte)(value * 255.0f));
|
||||
}
|
||||
|
||||
public virtual Vector3 AddColor {
|
||||
get => ResNode->AddColor;
|
||||
set => ResNode->AddColor = value;
|
||||
}
|
||||
|
||||
public virtual ColorHelpers.HsvaColor AddColorHsva {
|
||||
get => ResNode->AddColorHsva;
|
||||
set => ResNode->AddColorHsva = value;
|
||||
}
|
||||
|
||||
public virtual Vector3 MultiplyColor {
|
||||
get => ResNode->MultiplyColor;
|
||||
set => ResNode->MultiplyColor = value;
|
||||
}
|
||||
|
||||
public virtual ColorHelpers.HsvaColor MultiplyColorHsva {
|
||||
get => ResNode->MultiplyColorHsva;
|
||||
set => ResNode->MultiplyColorHsva = value;
|
||||
}
|
||||
|
||||
public uint NodeId {
|
||||
get => ResNode->NodeId;
|
||||
set => ResNode->NodeId = value;
|
||||
}
|
||||
|
||||
public virtual DrawFlags DrawFlags {
|
||||
get => (DrawFlags) ResNode->DrawFlags;
|
||||
set => ResNode->DrawFlags = (uint) value & 0b1111_1111_1111_1100_0000_0011_1111_1111 |
|
||||
ResNode->DrawFlags & 0b0000_0000_0000_0011_1111_1100_0000_0000;
|
||||
}
|
||||
|
||||
public virtual int ClipCount {
|
||||
get => (int)((ResNode->DrawFlags & 0b0000_0000_0000_0011_1111_1100_0000_0000) >> 10);
|
||||
set => ResNode->DrawFlags = (uint)(value << 10 & 0b0000_0000_0000_0011_1111_1100_0000_0000)
|
||||
| ResNode->DrawFlags & 0b1111_1111_1111_1100_0000_0011_1111_1111;
|
||||
}
|
||||
|
||||
public void AddDrawFlags(params DrawFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
DrawFlags |= flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveDrawFlags(params DrawFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
DrawFlags &= ~flag;
|
||||
}
|
||||
}
|
||||
|
||||
public int Priority {
|
||||
get => ResNode->GetPriority();
|
||||
set => ResNode->SetPriority((ushort)value);
|
||||
}
|
||||
|
||||
protected virtual NodeType NodeType {
|
||||
get => ResNode->GetNodeType();
|
||||
set => ResNode->Type = value;
|
||||
}
|
||||
|
||||
public virtual int ChildCount
|
||||
=> ResNode->ChildCount;
|
||||
|
||||
protected virtual void OnSizeChanged() { }
|
||||
|
||||
public void AddNodeFlags(params NodeFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
NodeFlags |= flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveNodeFlags(params NodeFlags[] flags) {
|
||||
foreach (var flag in flags) {
|
||||
NodeFlags &= ~flag;
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkDirty() {
|
||||
foreach (var child in GetAllChildren(this)) {
|
||||
child.ResNode->AddDrawFlag( [ DrawFlags.IsDirty ] );
|
||||
}
|
||||
ResNode->AddDrawFlag([ DrawFlags.IsDirty ] );
|
||||
}
|
||||
|
||||
public bool CheckCollision(short x, short y, bool inclusive = true)
|
||||
=> ResNode->CheckCollision(x, y, inclusive);
|
||||
|
||||
public bool CheckCollision(Vector2 position, bool inclusive = true)
|
||||
=> ResNode->CheckCollision((short) position.X, (short) position.Y, inclusive);
|
||||
|
||||
public bool CheckCollision(AtkEventData* eventData, bool inclusive = true)
|
||||
=> ResNode->CheckCollision(eventData, inclusive);
|
||||
|
||||
public Matrix2x2 Transform {
|
||||
get => ResNode->Transform;
|
||||
set => ResNode->Transform = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe partial class NodeBase {
|
||||
|
||||
public Timeline? Timeline { get; private set; }
|
||||
|
||||
public void AddTimeline(Timeline timeline) {
|
||||
Timeline?.Dispose();
|
||||
|
||||
Timeline = timeline;
|
||||
ResNode->Timeline = timeline.InternalTimeline;
|
||||
timeline.OwnerNode = ResNode;
|
||||
}
|
||||
|
||||
public void AddTimeline(TimelineBuilder builder)
|
||||
=> AddTimeline(builder.Build());
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Enums;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Nodes;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public record InventoryItemTooltip(InventoryType Inventory, short Slot);
|
||||
|
||||
public unsafe partial class NodeBase {
|
||||
|
||||
private AtkTooltipManager.AtkTooltipType tooltipType = AtkTooltipManager.AtkTooltipType.None;
|
||||
private bool tooltipEventsRegistered;
|
||||
|
||||
public virtual ReadOnlySeString TextTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (!value.IsEmpty) {
|
||||
TryRegisterTooltipEvents();
|
||||
tooltipType |= AtkTooltipManager.AtkTooltipType.Text;
|
||||
}
|
||||
else {
|
||||
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual uint ActionTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value is not 0) {
|
||||
TryRegisterTooltipEvents();
|
||||
tooltipType |= AtkTooltipManager.AtkTooltipType.Action;
|
||||
}
|
||||
else {
|
||||
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual uint ItemTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value is not 0) {
|
||||
TryRegisterTooltipEvents();
|
||||
tooltipType |= AtkTooltipManager.AtkTooltipType.Item;
|
||||
}
|
||||
else {
|
||||
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual InventoryItemTooltip? InventoryItemTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
if (value is not null) {
|
||||
TryRegisterTooltipEvents();
|
||||
tooltipType |= AtkTooltipManager.AtkTooltipType.Item;
|
||||
}
|
||||
else {
|
||||
tooltipType &= ~AtkTooltipManager.AtkTooltipType.Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryRegisterTooltipEvents() {
|
||||
if (tooltipEventsRegistered) return;
|
||||
|
||||
AddEvent(AtkEventType.MouseOver, ShowTooltip);
|
||||
AddEvent(AtkEventType.MouseOut, HideTooltip);
|
||||
OnVisibilityToggled += ToggleCollisionFlag;
|
||||
ToggleCollisionFlag(IsVisible);
|
||||
|
||||
tooltipEventsRegistered = true;
|
||||
}
|
||||
|
||||
private void UnregisterTooltipEvents() {
|
||||
if (tooltipEventsRegistered) {
|
||||
RemoveEvent(AtkEventType.MouseOver, ShowTooltip);
|
||||
RemoveEvent(AtkEventType.MouseOut, HideTooltip);
|
||||
OnVisibilityToggled -= ToggleCollisionFlag;
|
||||
tooltipEventsRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleCollisionFlag(bool isVisible) {
|
||||
if (this is ComponentNode) return;
|
||||
|
||||
if (isVisible) {
|
||||
AddNodeFlags(NodeFlags.HasCollision);
|
||||
}
|
||||
else {
|
||||
RemoveNodeFlags(NodeFlags.HasCollision);
|
||||
}
|
||||
}
|
||||
|
||||
protected bool TooltipRegistered { get; set; }
|
||||
|
||||
public void ShowTooltip() {
|
||||
if (ParentAddon is null) return; // Shouldn't be possible
|
||||
if (tooltipType is AtkTooltipManager.AtkTooltipType.None) return;
|
||||
|
||||
using var stringBuilder = new RentedSeStringBuilder();
|
||||
using var stringBuffer = new AtkValue();
|
||||
if (!TextTooltip.IsEmpty) {
|
||||
stringBuffer.SetManagedString(stringBuilder.Builder.Append(TextTooltip).GetViewAsSpan());
|
||||
}
|
||||
|
||||
var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs();
|
||||
|
||||
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Text)) {
|
||||
tooltipArgs.TextArgs.AtkArrayType = 0;
|
||||
tooltipArgs.TextArgs.Text = stringBuffer.String;
|
||||
}
|
||||
|
||||
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Action)) {
|
||||
tooltipArgs.ActionArgs.Flags = 1;
|
||||
tooltipArgs.ActionArgs.Kind = DetailKind.Action;
|
||||
tooltipArgs.ActionArgs.Id = (int)ActionTooltip;
|
||||
}
|
||||
|
||||
if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Item) && InventoryItemTooltip is {} inventoryTooltip) {
|
||||
tooltipArgs.ItemArgs.Kind = DetailKind.InventoryItem;
|
||||
tooltipArgs.ItemArgs.InventoryType = inventoryTooltip.Inventory;
|
||||
tooltipArgs.ItemArgs.Slot = inventoryTooltip.Slot;
|
||||
tooltipArgs.ItemArgs.BuyQuantity = -1;
|
||||
tooltipArgs.ItemArgs.Flag1 = 0;
|
||||
}
|
||||
else if (tooltipType.HasFlag(AtkTooltipManager.AtkTooltipType.Item) && InventoryItemTooltip is null) {
|
||||
tooltipArgs.ItemArgs.Kind = DetailKind.Item;
|
||||
tooltipArgs.ItemArgs.ItemId = (int) ItemTooltip;
|
||||
tooltipArgs.ItemArgs.BuyQuantity = -1;
|
||||
tooltipArgs.ItemArgs.Flag1 = 0;
|
||||
}
|
||||
|
||||
AtkStage.Instance()->TooltipManager.ShowTooltip(tooltipType, ParentAddon->Id, this, &tooltipArgs);
|
||||
}
|
||||
|
||||
public void HideTooltip() {
|
||||
if (ParentAddon is null) return;
|
||||
|
||||
AtkStage.Instance()->TooltipManager.HideTooltip(ParentAddon->Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit;
|
||||
|
||||
public abstract unsafe class NodeBase<T> : NodeBase where T : unmanaged, ICreatable {
|
||||
protected NodeBase(NodeType nodeType) {
|
||||
if (MainThreadSafety.TryAssertMainThread()) return;
|
||||
|
||||
Log.Verbose($"Creating new node {GetType()}");
|
||||
Node = NativeMemoryHelper.Create<T>();
|
||||
|
||||
if (ResNode is null) {
|
||||
throw new Exception($"Unable to allocate memory for {typeof(T)}");
|
||||
}
|
||||
|
||||
KamiToolKitLibrary.AllocatedNodes?.TryAdd((nint)Node, GetType());
|
||||
|
||||
BuildVirtualTable();
|
||||
|
||||
ResNode->Type = nodeType;
|
||||
ResNode->NodeId = NodeIdBase + CurrentOffset++;
|
||||
ResNode->ToggleVisibility(true);
|
||||
|
||||
CreatedNodes.Add(this);
|
||||
}
|
||||
|
||||
public T* Node { get; private set; }
|
||||
|
||||
internal sealed override AtkResNode* ResNode => (AtkResNode*)Node;
|
||||
|
||||
public static implicit operator T*(NodeBase<T> node) => (T*) node.ResNode;
|
||||
|
||||
protected override void Dispose(bool disposing, bool isNativeDestructor) {
|
||||
if (disposing) {
|
||||
try {
|
||||
base.Dispose(disposing, isNativeDestructor);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
finally {
|
||||
if (!isNativeDestructor) {
|
||||
InvokeOriginalDestructor(ResNode, true);
|
||||
}
|
||||
|
||||
KamiToolKitLibrary.AllocatedNodes?.Remove((nint)Node, out _);
|
||||
|
||||
Node = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public sealed class AlphaImageNode : ImGuiImageNode {
|
||||
public AlphaImageNode() {
|
||||
TexturePath = DalamudInterface.Instance.GetAssetPath("alpha_background.png");
|
||||
WrapMode = WrapMode.Tile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public class AlternateCooldownNode : ResNode {
|
||||
|
||||
public readonly ImageNode CooldownImage;
|
||||
|
||||
public AlternateCooldownNode() {
|
||||
CooldownImage = new ImageNode {
|
||||
NodeId = 15,
|
||||
Size = new Vector2(44.0f, 46.0f),
|
||||
Position = new Vector2(0.0f, 2.0f),
|
||||
Origin = new Vector2(22.0f, 23.0f),
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
||||
WrapMode = WrapMode.Tile,
|
||||
PartId = 80,
|
||||
};
|
||||
|
||||
IconNodeTextureHelper.LoadIconARecast2Texture(CooldownImage);
|
||||
|
||||
CooldownImage.AttachNode(this);
|
||||
|
||||
BuildTimeline();
|
||||
}
|
||||
|
||||
private void BuildTimeline() {
|
||||
CooldownImage.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(11, 92)
|
||||
.AddFrame(11, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 1)
|
||||
.AddFrame(92, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 79)
|
||||
.EndFrameSet()
|
||||
.BeginFrameSet(93, 174)
|
||||
.AddFrame(93, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 82)
|
||||
.AddFrame(174, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 160)
|
||||
.EndFrameSet()
|
||||
.BeginFrameSet(175, 205)
|
||||
.AddFrame(175, alpha: 255, scale: new Vector2(1.0f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(0.0f), partId: 80)
|
||||
.AddFrame(191, alpha: 255, scale: new Vector2(1.2f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(200.0f), partId: 80)
|
||||
.AddFrame(205, alpha: 0, scale: new Vector2(1.25f), multiplyColor: new Vector3(100.0f), addColor: new Vector3(200.0f), partId: 80)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public class AntsNode : ResNode {
|
||||
|
||||
public readonly ImageNode AntsImageNode;
|
||||
|
||||
public AntsNode() {
|
||||
AntsImageNode = new ImageNode {
|
||||
NodeId = 13,
|
||||
Size = new Vector2(48, 48),
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
||||
WrapMode = WrapMode.Tile,
|
||||
PartId = 13,
|
||||
};
|
||||
|
||||
IconNodeTextureHelper.LoadIconAFrameTexture(AntsImageNode);
|
||||
|
||||
AntsImageNode.AttachNode(this);
|
||||
|
||||
BuildTimeline();
|
||||
}
|
||||
|
||||
private void BuildTimeline() {
|
||||
AntsImageNode.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(2, 9)
|
||||
.AddFrame(2, partId: 6)
|
||||
.AddFrame(9, partId: 13)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
/// <summary>
|
||||
/// A simple image node that makes it easy to display a single color.
|
||||
/// </summary>
|
||||
public unsafe class BackgroundImageNode : SimpleImageNode {
|
||||
public BackgroundImageNode() {
|
||||
FitTexture = true;
|
||||
}
|
||||
|
||||
public new Vector4 Color {
|
||||
get => new(AddColor.X, AddColor.Y, AddColor.Z, ResNode->Color.A / 255.0f);
|
||||
set {
|
||||
ResNode->Color = new Vector4(0.0f, 0.0f, 0.0f, value.W).ToByteColor();
|
||||
AddColor = value.AsVector3Color();
|
||||
}
|
||||
}
|
||||
|
||||
public new ColorHelpers.HsvaColor ColorHsva {
|
||||
get => ColorHelpers.RgbaToHsv(Color);
|
||||
set => Color = ColorHelpers.HsvToRgb(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Numerics;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
/// <summary>
|
||||
/// A node that shows a border loaded from the party list textures
|
||||
/// </summary>
|
||||
public unsafe class BorderNineGridNode : NineGridNode {
|
||||
public BorderNineGridNode() {
|
||||
PartsList.Add(new Part {
|
||||
TextureCoordinates = new Vector2(0.0f, 0.0f),
|
||||
Size = new Vector2(64.0f, 64.0f),
|
||||
Id = 0,
|
||||
TexturePath = "ui/uld/PartyListTargetBase.tex",
|
||||
});
|
||||
|
||||
TopOffset = 20;
|
||||
LeftOffset = 20;
|
||||
RightOffset = 20;
|
||||
BottomOffset = 20;
|
||||
PartsRenderType = 108;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
// Simple helper class for making basic text label, node will auto-resize to fit label
|
||||
public sealed class CategoryTextNode : TextNode {
|
||||
public CategoryTextNode() {
|
||||
Height = 16.0f;
|
||||
TextFlags = TextFlags.AutoAdjustNodeSize;
|
||||
TextColor = ColorHelper.GetColor(2);
|
||||
TextOutlineColor = ColorHelper.GetColor(7);
|
||||
FontType = FontType.Axis;
|
||||
FontSize = 14;
|
||||
LineSpacing = 24;
|
||||
AlignmentType = AlignmentType.Left;
|
||||
}
|
||||
|
||||
public override float Height {
|
||||
get => base.Height;
|
||||
set => base.Height = value + 8.0f; // Add extra height for padding
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Timelines;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public unsafe class CheckboxNode : ComponentNode<AtkComponentCheckBox, AtkUldComponentDataCheckBox> {
|
||||
|
||||
public readonly ImageNode BoxBackground;
|
||||
public readonly ImageNode BoxForeground;
|
||||
public readonly TextNode Label;
|
||||
|
||||
public CheckboxNode() {
|
||||
SetInternalComponentType(ComponentType.CheckBox);
|
||||
|
||||
BoxBackground = new SimpleImageNode {
|
||||
TexturePath = "ui/uld/CheckBoxA.tex",
|
||||
TextureCoordinates = new Vector2(0.0f, 0.0f),
|
||||
TextureSize = new Vector2(16.0f, 16.0f),
|
||||
Size = new Vector2(16.0f, 16.0f),
|
||||
Position = new Vector2(0.0f, 2.0f),
|
||||
WrapMode = WrapMode.Stretch,
|
||||
};
|
||||
BoxBackground.AttachNode(this);
|
||||
|
||||
BoxForeground = new SimpleImageNode {
|
||||
TexturePath = "ui/uld/CheckBoxA.tex",
|
||||
TextureCoordinates = new Vector2(16.0f, 0.0f),
|
||||
TextureSize = new Vector2(16.0f, 16.0f),
|
||||
Size = new Vector2(16.0f, 16.0f),
|
||||
Position = new Vector2(0.0f, 2.0f),
|
||||
WrapMode = WrapMode.Stretch,
|
||||
};
|
||||
BoxForeground.AttachNode(this);
|
||||
|
||||
Label = new TextNode {
|
||||
Size = new Vector2(0.0f, 20.0f),
|
||||
Position = new Vector2(20.0f, 0.0f),
|
||||
FontType = FontType.Axis,
|
||||
AlignmentType = AlignmentType.Left,
|
||||
FontSize = 14,
|
||||
LineSpacing = 14,
|
||||
TextColor = ColorHelper.GetColor(8),
|
||||
TextOutlineColor = ColorHelper.GetColor(7),
|
||||
TextFlags = TextFlags.AutoAdjustNodeSize,
|
||||
};
|
||||
Label.AttachNode(this);
|
||||
|
||||
Component->Flags = 606464;
|
||||
|
||||
Data->Nodes[0] = Label.NodeId;
|
||||
Data->Nodes[1] = BoxBackground.NodeId;
|
||||
Data->Nodes[2] = 0;
|
||||
|
||||
LoadTimelines();
|
||||
|
||||
AddEvent(AtkEventType.ButtonClick, ClickHandler);
|
||||
AddEvent(AtkEventType.InputReceived, ClickHandler);
|
||||
|
||||
InitializeComponentEvents();
|
||||
Component->Left = 20;
|
||||
Component->Right = 20;
|
||||
Component->Top = 0;
|
||||
Component->Bottom = 0;
|
||||
|
||||
BoxForeground.IsVisible = Component->IsChecked;
|
||||
BoxForeground.DrawFlags = 0;
|
||||
}
|
||||
|
||||
public Action<bool>? OnClick { get; set; }
|
||||
|
||||
public ReadOnlySeString String {
|
||||
get => Label.String;
|
||||
set {
|
||||
Label.String = value;
|
||||
Width = Height + Label.Width + 4.0f;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsChecked {
|
||||
get => Component->IsChecked;
|
||||
set => Component->SetChecked(value);
|
||||
}
|
||||
|
||||
private void ClickHandler() {
|
||||
OnClick?.Invoke(Component->IsChecked);
|
||||
}
|
||||
|
||||
public bool DisableAutoResize {
|
||||
get => Label.TextFlags.HasFlag(TextFlags.AutoAdjustNodeSize);
|
||||
set {
|
||||
if (value) {
|
||||
Label.TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||
Label.TextFlags |= TextFlags.Ellipsis;
|
||||
}
|
||||
else {
|
||||
Label.TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||
Label.TextFlags &= ~TextFlags.Ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSizeChanged() {
|
||||
base.OnSizeChanged();
|
||||
|
||||
BoxBackground.Size = new Vector2(Height, Height) - new Vector2(4.0f, 4.0f);
|
||||
BoxForeground.Size = new Vector2(Height, Height) - new Vector2(4.0f, 4.0f);
|
||||
|
||||
Label.Height = Height;
|
||||
Label.X = Height;
|
||||
|
||||
if (DisableAutoResize) {
|
||||
Label.Width = Width - Height;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadTimelines() {
|
||||
AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, 155)
|
||||
.AddLabelPair(1, 10, 1)
|
||||
.AddLabelPair(11, 20, 2)
|
||||
.AddLabelPair(21, 30, 3)
|
||||
.AddLabelPair(31, 40, 7)
|
||||
.AddLabelPair(41, 50, 6)
|
||||
.AddLabelPair(51, 60, 4)
|
||||
.AddLabelPair(61, 70, 8)
|
||||
.AddLabelPair(71, 80, 9)
|
||||
.AddLabelPair(81, 90, 10)
|
||||
.AddLabelPair(91, 100, 14)
|
||||
.AddLabelPair(101, 110, 13)
|
||||
.AddLabelPair(111, 115, 11)
|
||||
.AddLabelPair(116, 125, 12)
|
||||
.AddLabelPair(126, 135, 5)
|
||||
.AddLabelPair(136, 145, 15)
|
||||
.AddLabelPair(146, 155, 16)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
|
||||
CollisionNode.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, 155)
|
||||
.AddEmptyFrame(1)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
|
||||
BoxBackground.AddTimeline(new TimelineBuilder()
|
||||
.AddFrameSetWithFrame(1, 10, 1, new Vector2(0.0f, 2.0f), 255, multiplyColor: new Vector3(100.0f))
|
||||
.BeginFrameSet(11, 20)
|
||||
.AddFrame(11, new Vector2(0.0f, 2.0f), 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrame(13, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.AddFrameSetWithFrame(21, 30, 21, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(31, 40, 31, new Vector2(0.0f, 2.0f), 102, multiplyColor: new Vector3(80.0f))
|
||||
.AddFrameSetWithFrame(41, 50, 41, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.BeginFrameSet(51, 60)
|
||||
.AddFrame(51, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.AddFrame(60, new Vector2(0.0f, 2.0f), 255, multiplyColor: new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.AddFrameSetWithFrame(61, 70, 61, new Vector2(0.0f, 2.0f), 255, multiplyColor: new Vector3(100.0f))
|
||||
.BeginFrameSet(71, 80)
|
||||
.AddFrame(71, new Vector2(0.0f, 2.0f), 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrame(73, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.AddFrameSetWithFrame(81, 90, 81, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(91, 100, 91, new Vector2(0.0f, 2.0f), 102, multiplyColor: new Vector3(80.0f))
|
||||
.AddFrameSetWithFrame(101, 110, 101, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.BeginFrameSet(111, 115)
|
||||
.AddFrame(111, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.AddFrame(115, new Vector2(0.0f, 2.0f), 255, multiplyColor: new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.AddFrameSetWithFrame(116, 125, 116, new Vector2(0.0f, 2.0f), addColor: new Vector3(16.0f), multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(126, 135, 126, new Vector2(0.0f, 2.0f), 255, new Vector3(16.0f), new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(136, 145, 126, new Vector2(0.0f, 2.0f), 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(146, 155, 146, new Vector2(0.0f, 2.0f), 255, multiplyColor: new Vector3(100.0f))
|
||||
.Build());
|
||||
|
||||
BoxForeground.AddTimeline(new TimelineBuilder()
|
||||
.AddFrameSetWithFrame(61, 70, 61, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.BeginFrameSet(71, 80)
|
||||
.AddFrame(71, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrame(73, alpha: 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
|
||||
.EndFrameSet()
|
||||
.AddFrameSetWithFrame(81, 90, 81, alpha: 255, addColor: new Vector3(16.0f), multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(91, 100, 91, alpha: 102, multiplyColor: new Vector3(80.0f))
|
||||
.AddFrameSetWithFrame(101, 110, 101, alpha: 255, addColor: new Vector3(16.0f), multiplyColor: new Vector3(100.0f))
|
||||
.BeginFrameSet(111, 115)
|
||||
.AddFrame(111, alpha: 255, addColor: new Vector3(16.0f), multiplyColor: new Vector3(100.0f))
|
||||
.AddFrame(115, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.BeginFrameSet(116, 125)
|
||||
.AddFrame(116, alpha: 0, addColor: new Vector3(16.0f), multiplyColor: new Vector3(100.0f))
|
||||
.AddFrame(119, alpha: 255, addColor: new Vector3(16.0f), multiplyColor: new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.BeginFrameSet(126, 135)
|
||||
.AddFrame(126, alpha: 255, addColor: new Vector3(16.0f), multiplyColor: new Vector3(100.0f))
|
||||
.AddFrame(129, alpha: 0, addColor: new Vector3(16.0f), multiplyColor: new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.BeginFrameSet(136, 145)
|
||||
.AddFrame(136, alpha: 0, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrame(140, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.BeginFrameSet(146, 255)
|
||||
.AddFrame(146, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrame(150, alpha: 0, multiplyColor: new Vector3(100.0f))
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
|
||||
Label.AddTimeline(new TimelineBuilder()
|
||||
.AddFrameSetWithFrame(1, 10, 1, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(11, 20, 11, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(21, 30, 21, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(31, 40, 31, alpha: 102, multiplyColor: new Vector3(80.0f))
|
||||
.AddFrameSetWithFrame(41, 50, 41, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(51, 60, 51, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(61, 70, 61, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(71, 80, 71, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(81, 90, 81, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(91, 100, 91, alpha: 102, multiplyColor: new Vector3(80.0f))
|
||||
.AddFrameSetWithFrame(101, 110, 101, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(111, 115, 111, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(116, 135, 116, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(126, 135, 126, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(136, 145, 136, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.AddFrameSetWithFrame(146, 155, 146, alpha: 255, multiplyColor: new Vector3(100.0f))
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public unsafe class ClippingMaskNode : NodeBase<AtkClippingMaskNode> {
|
||||
public readonly PartsList PartsList;
|
||||
|
||||
public ClippingMaskNode() : base(NodeType.ClippingMask) {
|
||||
PartsList = new PartsList();
|
||||
|
||||
Node->PartsList = PartsList.InternalPartsList;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing, bool isNativeDestructor) {
|
||||
if (disposing) {
|
||||
if (!isNativeDestructor) {
|
||||
PartsList.Dispose();
|
||||
Node->PartsList = null;
|
||||
}
|
||||
|
||||
base.Dispose(disposing, isNativeDestructor);
|
||||
}
|
||||
}
|
||||
|
||||
public ushort PartId {
|
||||
get => Node->PartId;
|
||||
set => Node->PartId = value;
|
||||
}
|
||||
|
||||
public AtkUldPart* AddPart(Part part)
|
||||
=> PartsList.Add(part);
|
||||
|
||||
public void AddPart(params Part[] parts)
|
||||
=> PartsList.Add(parts);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public unsafe class CollisionNode() : NodeBase<AtkCollisionNode>(NodeType.Collision) {
|
||||
public virtual CollisionType CollisionType {
|
||||
get => Node->CollisionType;
|
||||
set => Node->CollisionType = value;
|
||||
}
|
||||
|
||||
public virtual uint Uses {
|
||||
get => Node->Uses;
|
||||
set => Node->Uses = (ushort)value;
|
||||
}
|
||||
|
||||
public virtual AtkComponentBase* LinkedComponent {
|
||||
get => Node->LinkedComponent;
|
||||
set => Node->LinkedComponent = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public class CooldownNode : ResNode {
|
||||
|
||||
public readonly ImageNode CooldownImage;
|
||||
public readonly ImageNode GlossyImageFrame;
|
||||
|
||||
public CooldownNode() {
|
||||
GlossyImageFrame = new ImageNode {
|
||||
NodeId = 18,
|
||||
Size = new Vector2(48.0f, 48.0f),
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
||||
WrapMode = WrapMode.Tile,
|
||||
};
|
||||
|
||||
IconNodeTextureHelper.LoadIconAFrameTexture(GlossyImageFrame);
|
||||
|
||||
GlossyImageFrame.AttachNode(this);
|
||||
|
||||
CooldownImage = new ImageNode {
|
||||
NodeId = 17,
|
||||
Size = new Vector2(44.0f, 46.0f),
|
||||
Position = new Vector2(2.0f, 2.0f),
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
||||
WrapMode = WrapMode.Tile,
|
||||
PartId = 80,
|
||||
};
|
||||
|
||||
IconNodeTextureHelper.LoadIconARecastTexture(CooldownImage);
|
||||
|
||||
CooldownImage.AttachNode(this);
|
||||
|
||||
BuildTimelines();
|
||||
}
|
||||
|
||||
private void BuildTimelines() {
|
||||
GlossyImageFrame.AddTimeline(new TimelineBuilder()
|
||||
.AddFrameSetWithFrame(1, 10, 1, partId: 0)
|
||||
.AddFrameSetWithFrame(11, 20, 11, partId: 1)
|
||||
.AddFrameSetWithFrame(21, 30, 21, partId: 2)
|
||||
.AddFrameSetWithFrame(31, 40, 31, partId: 3)
|
||||
.AddFrameSetWithFrame(41, 50, 41, partId: 18)
|
||||
.AddFrameSetWithFrame(51, 60, 51, partId: 19)
|
||||
.AddFrameSetWithFrame(143, 165, 143, partId: 0)
|
||||
.Build());
|
||||
|
||||
CooldownImage.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(61, 142)
|
||||
.AddFrame(61, alpha: 255, partId: 1)
|
||||
.AddFrame(142, alpha: 255, partId: 79)
|
||||
.EndFrameSet()
|
||||
.BeginFrameSet(143, 165)
|
||||
.AddFrame(143, alpha: 255, partId: 80)
|
||||
.AddFrame(165, alpha: 0, partId: 79)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
/// <summary>
|
||||
/// A counter node for displaying numbers
|
||||
/// </summary>
|
||||
public unsafe class CounterNode : NodeBase<AtkCounterNode> {
|
||||
|
||||
public readonly PartsList PartsList;
|
||||
|
||||
public CounterNode() : base(NodeType.Counter) {
|
||||
PartsList = new PartsList();
|
||||
PartsList.Add(new Part());
|
||||
|
||||
Node->PartsList = PartsList.InternalPartsList;
|
||||
|
||||
NumberWidth = 10;
|
||||
CommaWidth = 8;
|
||||
SpaceWidth = 6;
|
||||
TextAlignment = AlignmentType.Right;
|
||||
CounterWidth = 32;
|
||||
Font = CounterFont.MoneyFont;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing, bool isNativeDestructor) {
|
||||
if (disposing) {
|
||||
if (!isNativeDestructor) {
|
||||
PartsList.Dispose();
|
||||
Node->PartsList = null;
|
||||
}
|
||||
|
||||
base.Dispose(disposing, isNativeDestructor);
|
||||
}
|
||||
}
|
||||
|
||||
protected string TexturePath {
|
||||
get => PartsList[0]->LoadedPath;
|
||||
set => PartsList[0]->LoadTexture(value);
|
||||
}
|
||||
|
||||
protected Vector2 TextureCoordinates {
|
||||
get => new(PartsList[0]->U, PartsList[0]->V);
|
||||
set {
|
||||
PartsList[0]->U = (ushort) value.X;
|
||||
PartsList[0]->V = (ushort) value.X;
|
||||
}
|
||||
}
|
||||
|
||||
protected Vector2 TextureSize {
|
||||
get => new(PartsList[0]->Width, PartsList[0]->Height);
|
||||
set {
|
||||
PartsList[0]->Width = (ushort) value.X;
|
||||
PartsList[0]->Height = (ushort) value.X;
|
||||
}
|
||||
}
|
||||
|
||||
public uint NumberWidth {
|
||||
get => Node->NumberWidth;
|
||||
set => Node->NumberWidth = (byte)value;
|
||||
}
|
||||
|
||||
public uint CommaWidth {
|
||||
get => Node->CommaWidth;
|
||||
set => Node->CommaWidth = (byte)value;
|
||||
}
|
||||
|
||||
public uint SpaceWidth {
|
||||
get => Node->SpaceWidth;
|
||||
set => Node->SpaceWidth = (byte)value;
|
||||
}
|
||||
|
||||
public AlignmentType TextAlignment {
|
||||
get => (AlignmentType) Node->TextAlign;
|
||||
set => Node->TextAlign = (ushort) value;
|
||||
}
|
||||
|
||||
public float CounterWidth {
|
||||
get => Node->CounterWidth;
|
||||
set => Node->CounterWidth = value;
|
||||
}
|
||||
|
||||
public int Number {
|
||||
get => int.Parse(Node->NodeText.ToString());
|
||||
set => Node->SetText(ParseNumber(value));
|
||||
}
|
||||
|
||||
public ReadOnlySeString String {
|
||||
get => Node->NodeText.AsSpan();
|
||||
set => Node->SetText(ParseString(value));
|
||||
}
|
||||
|
||||
public CounterFont Font {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
|
||||
var fontPath = string.Empty;
|
||||
var partSize = Vector2.Zero;
|
||||
|
||||
switch (value) {
|
||||
case CounterFont.MoneyFont:
|
||||
fontPath = "ui/uld/Money_Number.tex";
|
||||
partSize = new Vector2(22.0f, 22.0f);
|
||||
break;
|
||||
|
||||
case CounterFont.ChocoboRace:
|
||||
fontPath = "ui/uld/RaceChocoboNum.tex";
|
||||
partSize = new Vector2(30.0f, 60.0f);
|
||||
break;
|
||||
}
|
||||
|
||||
if (fontPath != string.Empty && partSize != Vector2.Zero) {
|
||||
PartsList[0]->Width = (ushort)partSize.X;
|
||||
PartsList[0]->Height = (ushort)partSize.Y;
|
||||
PartsList[0]->LoadTexture(fontPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ReadOnlySeString ParseString(ReadOnlySeString value) {
|
||||
using var builder = new RentedSeStringBuilder();
|
||||
return builder.Builder.Append(value).GetViewAsSpan();
|
||||
}
|
||||
|
||||
private static ReadOnlySeString ParseNumber(int value) {
|
||||
using var rentedBuilder = new RentedSeStringBuilder();
|
||||
|
||||
// <kilo(lnum1,\,)>
|
||||
var evaluatedString = DalamudInterface.Instance.SeStringEvaluator.EvaluateFromAddon(18, [ value ]);
|
||||
|
||||
foreach (var payload in evaluatedString) {
|
||||
switch (payload.Type) {
|
||||
|
||||
// Fix for French thousands separators.
|
||||
// The game calls FormatAddonText2 that does this.
|
||||
case ReadOnlySePayloadType.Macro when payload.MacroCode is MacroCode.NonBreakingSpace:
|
||||
rentedBuilder.Builder.Append(' ');
|
||||
break;
|
||||
|
||||
default:
|
||||
rentedBuilder.Builder.Append(payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rentedBuilder.Builder.GetViewAsSpan();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Numerics;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public class CursorNode : ResNode {
|
||||
|
||||
public readonly SimpleImageNode CursorImageNode;
|
||||
|
||||
public CursorNode() {
|
||||
CursorImageNode = new SimpleImageNode {
|
||||
NodeId = 3,
|
||||
TexturePath = "ui/uld/TextInputA.tex",
|
||||
Size = new Vector2(4.0f, 24.0f),
|
||||
TextureCoordinates = new Vector2(68.0f, 0.0f),
|
||||
TextureSize = new Vector2(4.0f, 24.0f),
|
||||
WrapMode = WrapMode.Tile,
|
||||
};
|
||||
CursorImageNode.AttachNode(this);
|
||||
|
||||
CursorImageNode.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, 8)
|
||||
.AddEmptyFrame(1)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
|
||||
Timeline?.PlayAnimation(101);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Client.Enums;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Enums;
|
||||
using KamiToolKit.Timelines;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public unsafe class DragDropNode : ComponentNode<AtkComponentDragDrop, AtkUldComponentDataDragDrop> {
|
||||
|
||||
public readonly ImageNode DragDropBackgroundNode;
|
||||
public readonly IconNode IconNode;
|
||||
|
||||
public DragDropNode() {
|
||||
SetInternalComponentType(ComponentType.DragDrop);
|
||||
|
||||
DragDropBackgroundNode = new SimpleImageNode {
|
||||
NodeId = 3,
|
||||
Size = new Vector2(44.0f, 44.0f),
|
||||
TexturePath = "ui/uld/DragTargetA.tex",
|
||||
TextureCoordinates = new Vector2(0.0f, 0.0f),
|
||||
TextureSize = new Vector2(44.0f, 44.0f),
|
||||
WrapMode = WrapMode.Tile,
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
||||
};
|
||||
DragDropBackgroundNode.AttachNode(this);
|
||||
|
||||
IconNode = new IconNode {
|
||||
NodeId = 2,
|
||||
Size = new Vector2(44.0f, 48.0f),
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
||||
};
|
||||
IconNode.AttachNode(this);
|
||||
|
||||
LoadTimelines();
|
||||
|
||||
Data->Nodes[0] = IconNode.NodeId;
|
||||
|
||||
AcceptedType = DragDropType.Everything;
|
||||
Payload = new DragDropPayload();
|
||||
|
||||
Component->AtkDragDropInterface.DragDropType = DragDropType.Everything;
|
||||
Component->AtkDragDropInterface.DragDropReferenceIndex = 0;
|
||||
|
||||
InitializeComponentEvents();
|
||||
|
||||
AddEvent(AtkEventType.DragDropBegin, DragDropBeginHandler);
|
||||
AddEvent(AtkEventType.DragDropInsert, DragDropInsertHandler);
|
||||
AddEvent(AtkEventType.DragDropDiscard, DragDropDiscardHandler);
|
||||
AddEvent(AtkEventType.DragDropClick, DragDropClickHandler);
|
||||
AddEvent(AtkEventType.DragDropRollOver, DragDropRollOverHandler);
|
||||
AddEvent(AtkEventType.DragDropRollOut, DragDropRollOutHandler);
|
||||
}
|
||||
|
||||
private bool IsDragDropEndRegistered { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is triggered when a DragDrop is beginning
|
||||
/// </summary>
|
||||
public Action<DragDropNode>? OnBegin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is triggered when a DragDrop has finished
|
||||
/// </summary>
|
||||
public Action<DragDropNode>? OnEnd { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is triggered when a compatible DragDrop is dropped onto this node
|
||||
/// </summary>
|
||||
public Action<DragDropNode, DragDropPayload>? OnPayloadAccepted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is triggered when the item in this drag drop is being dropped onto the world
|
||||
/// </summary>
|
||||
public Action<DragDropNode>? OnDiscard { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is triggered when the item is clicked
|
||||
/// </summary>
|
||||
public Action<DragDropNode>? OnClicked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is triggered when the item is being moused over
|
||||
/// </summary>
|
||||
public Action<DragDropNode>? OnRollOver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is triggered when the item is no longer being moused over
|
||||
/// </summary>
|
||||
public Action<DragDropNode>? OnRollOut { get; set; }
|
||||
|
||||
public DragDropPayload Payload { get; set; }
|
||||
|
||||
public uint IconId {
|
||||
get => IconNode.IconId;
|
||||
set {
|
||||
IconNode.IconId = value;
|
||||
IconNode.IsVisible = value != 0;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsIconDisabled {
|
||||
get => IconNode.IsIconDisabled;
|
||||
set => IconNode.IsIconDisabled = value;
|
||||
}
|
||||
|
||||
public int Quantity {
|
||||
get => int.Parse(Component->GetQuantityText().ToString());
|
||||
set => Component->SetQuantity(value);
|
||||
}
|
||||
|
||||
public string QuantityString {
|
||||
get => Component->GetQuantityText().ToString();
|
||||
set => Component->SetQuantityText(value);
|
||||
}
|
||||
|
||||
public DragDropType AcceptedType {
|
||||
get => Component->AcceptedType;
|
||||
set => Component->AcceptedType = value;
|
||||
}
|
||||
|
||||
public AtkDragDropInterface.SoundEffectSuppression SoundEffectSuppression {
|
||||
get => Component->AtkDragDropInterface.DragDropSoundEffectSuppression;
|
||||
set => Component->AtkDragDropInterface.DragDropSoundEffectSuppression = value;
|
||||
}
|
||||
|
||||
public bool IsDraggable {
|
||||
get => !Component->Flags.HasFlag(DragDropFlag.Locked);
|
||||
set {
|
||||
if (value) {
|
||||
Component->Flags &= ~DragDropFlag.Locked;
|
||||
}
|
||||
else {
|
||||
Component->Flags |= DragDropFlag.Locked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When true, allows left-clicking the item to trigger OnClicked
|
||||
/// </summary>
|
||||
public bool IsClickable {
|
||||
get => Component->Flags.HasFlag(DragDropFlag.Clickable);
|
||||
set {
|
||||
if (value) {
|
||||
Component->Flags |= DragDropFlag.Clickable;
|
||||
}
|
||||
else {
|
||||
Component->Flags &= ~DragDropFlag.Clickable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DragDropBeginHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
atkEvent->SetEventIsHandled();
|
||||
Payload.ToDragDropInterface(atkEventData->DragDropData.DragDropInterface);
|
||||
OnBegin?.Invoke(this);
|
||||
|
||||
if (!IsDragDropEndRegistered) {
|
||||
AddEvent(AtkEventType.DragDropEnd, DragDropEndHandler);
|
||||
IsDragDropEndRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override ReadOnlySeString TextTooltip {
|
||||
get;
|
||||
set {
|
||||
field = value;
|
||||
switch (value) {
|
||||
case { IsEmpty: false } when !TooltipRegistered:
|
||||
AddEvent(AtkEventType.DragDropRollOver, ShowTooltip);
|
||||
AddEvent(AtkEventType.DragDropRollOut, HideTooltip);
|
||||
|
||||
TooltipRegistered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DragDropInsertHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
atkEvent->SetEventIsHandled();
|
||||
|
||||
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
|
||||
atkEvent->State.ReturnFlags = 1;
|
||||
|
||||
var payload = DragDropPayload.FromDragDropInterface(atkEventData->DragDropData.DragDropInterface);
|
||||
|
||||
Payload.Clear();
|
||||
IconId = 0;
|
||||
|
||||
OnPayloadAccepted?.Invoke(this, payload);
|
||||
}
|
||||
|
||||
private void DragDropDiscardHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
atkEvent->SetEventIsHandled();
|
||||
|
||||
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
|
||||
atkEvent->State.ReturnFlags = 1;
|
||||
|
||||
OnDiscard?.Invoke(this);
|
||||
}
|
||||
|
||||
private void DragDropEndHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
atkEvent->SetEventIsHandled();
|
||||
atkEventData->DragDropData.DragDropInterface->GetPayloadContainer()->Clear();
|
||||
OnEnd?.Invoke(this);
|
||||
|
||||
if (IsDragDropEndRegistered) {
|
||||
RemoveEvent(AtkEventType.DragDropEnd, DragDropEndHandler);
|
||||
IsDragDropEndRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DragDropClickHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||
atkEvent->SetEventIsHandled();
|
||||
|
||||
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
|
||||
atkEvent->State.ReturnFlags = 1;
|
||||
|
||||
OnClicked?.Invoke(this);
|
||||
}
|
||||
|
||||
private void DragDropRollOverHandler()
|
||||
=> OnRollOver?.Invoke(this);
|
||||
|
||||
private void DragDropRollOutHandler()
|
||||
=> OnRollOut?.Invoke(this);
|
||||
|
||||
/// Clear the payload data and set iconId to zero
|
||||
public void Clear() {
|
||||
Payload.Clear();
|
||||
IconId = 0;
|
||||
}
|
||||
|
||||
// Show fancy tooltip for the currently stored data
|
||||
public void ShowTooltip(AtkTooltipManager.AtkTooltipType type, ActionKind actionKind) {
|
||||
if (AtkStage.Instance()->DragDropManager.IsDragging) return;
|
||||
|
||||
var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode(ResNode);
|
||||
if (addon is null) return;
|
||||
|
||||
var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs();
|
||||
tooltipArgs.Ctor();
|
||||
tooltipArgs.ActionArgs.Id = Payload.Int2;
|
||||
tooltipArgs.ActionArgs.Kind = (DetailKind)actionKind;
|
||||
|
||||
AtkStage.Instance()->TooltipManager.ShowTooltip(
|
||||
AtkTooltipManager.AtkTooltipType.Action,
|
||||
addon->Id,
|
||||
ResNode,
|
||||
&tooltipArgs);
|
||||
}
|
||||
|
||||
private void LoadTimelines() {
|
||||
AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, 59)
|
||||
.AddLabelPair(1, 10, 1)
|
||||
.AddLabelPair(11, 19, 2)
|
||||
.AddLabelPair(20, 29, 3)
|
||||
.AddLabelPair(30, 39, 7)
|
||||
.AddLabelPair(40, 49, 6)
|
||||
.AddLabelPair(50, 59, 4)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Interface.Textures;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Timelines;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace KamiToolKit.Nodes;
|
||||
|
||||
public class GifImageNode : ResNode {
|
||||
|
||||
public ImageNode ImageNode;
|
||||
|
||||
public GifImageNode() {
|
||||
ImageNode = new ImageNode();
|
||||
ImageNode.AttachNode(this);
|
||||
}
|
||||
|
||||
public required string FilePath {
|
||||
set {
|
||||
Task.Run(() => LoadFrames(value));
|
||||
}
|
||||
}
|
||||
|
||||
public override float Width {
|
||||
get => base.Width;
|
||||
set {
|
||||
ImageNode.Width = value;
|
||||
base.Width = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override float Height {
|
||||
get => base.Height;
|
||||
set {
|
||||
ImageNode.Height = value;
|
||||
base.Height = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 GifFrameSize { get; private set; }
|
||||
|
||||
public bool FitNodeToGif { get; set; }
|
||||
|
||||
public Action? OnGifLoaded { get; set; }
|
||||
|
||||
private async void LoadFrames(string filepath) {
|
||||
try {
|
||||
var image = await LoadAsync(filepath);
|
||||
if (image.Length <= 0) return;
|
||||
|
||||
using var memoryStream = new MemoryStream(image);
|
||||
using var processedImage = Image.Load<Rgba32>(memoryStream);
|
||||
if (processedImage.Frames.Count is 0) return;
|
||||
|
||||
uint currentPartId = 0;
|
||||
var frameDelay = processedImage.Frames.RootFrame.Metadata.GetGifMetadata().FrameDelay / 3.33333333f;
|
||||
var frameCount = (int)(processedImage.Frames.Count * frameDelay);
|
||||
GifFrameSize = new Vector2(processedImage.Width, processedImage.Height);
|
||||
|
||||
if (FitNodeToGif) {
|
||||
Size = GifFrameSize;
|
||||
}
|
||||
|
||||
foreach (var frame in processedImage.Frames) {
|
||||
var buffer = new byte[8 * frame.Width * frame.Height];
|
||||
|
||||
frame.CopyPixelDataTo(buffer);
|
||||
|
||||
var texture = await DalamudInterface.Instance.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer);
|
||||
|
||||
unsafe {
|
||||
var newPart = ImageNode.AddPart(new Part {
|
||||
Size = texture.Size,
|
||||
Id = currentPartId++,
|
||||
});
|
||||
|
||||
newPart->LoadTexture(texture);
|
||||
}
|
||||
}
|
||||
|
||||
ImageNode.AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, frameCount)
|
||||
.AddFrame(0, partId: 0)
|
||||
.AddFrame(frameCount, partId: currentPartId)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
|
||||
AddTimeline(new TimelineBuilder()
|
||||
.BeginFrameSet(1, frameCount)
|
||||
.AddLabel(1, 200, AtkTimelineJumpBehavior.Start, 0)
|
||||
.AddLabel(frameCount, 0, AtkTimelineJumpBehavior.LoopForever, 200)
|
||||
.EndFrameSet()
|
||||
.Build());
|
||||
|
||||
Timeline?.PlayAnimation( AtkTimelineJumpBehavior.LoopForever, 200);
|
||||
|
||||
await DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => {
|
||||
OnGifLoaded?.Invoke();
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<byte[]> LoadAsync(string path) {
|
||||
byte[] data = [];
|
||||
|
||||
if (File.Exists(path)) {
|
||||
data = await File.ReadAllBytesAsync(path);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||