diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs new file mode 100644 index 000000000..e72de2b23 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +using FFXIVClientStructs.Attributes; +using FFXIVClientStructs.FFXIV.Component.GUI; + +using static System.Reflection.BindingFlags; +using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +public unsafe partial class AddonTree +{ + private static readonly Dictionary AddonTypeDict = []; + + /// + /// Gets or sets a collection of names corresponding to pointers documented within the addon struct. + /// + internal Dictionary> FieldNames { get; set; } = []; + + private object? GetAddonObj(AtkUnitBase* addon) + { + if (addon == null) + { + return null; + } + + if (!AddonTypeDict.ContainsKey(this.AddonName)) + { + AddonTypeDict.Add(this.AddonName, null); + + foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + foreach (var t in from t in a.GetTypes() + where t.IsPublic + let xivAddonAttr = (Addon?)t.GetCustomAttribute(typeof(Addon), false) + where xivAddonAttr != null + where xivAddonAttr.AddonIdentifiers.Contains(this.AddonName) + select t) + { + AddonTypeDict[this.AddonName] = t; + break; + } + } + catch + { + // ignored + } + } + } + + return AddonTypeDict.TryGetValue(this.AddonName, out var result) && result != null ? Marshal.PtrToStructure(new(addon), result) : *addon; + } + + private void PopulateFieldNames(nint ptr) + { + this.PopulateFieldNames(this.GetAddonObj((AtkUnitBase*)ptr), ptr); + } + + private void PopulateFieldNames(object? obj, nint baseAddr, List? path = null) + { + if (obj == null) + { + return; + } + + path ??= []; + var baseType = obj.GetType(); + + foreach (var field in baseType.GetFields(Static | Public | NonPublic | Instance)) + { + if (field.GetCustomAttribute(typeof(FieldOffsetAttribute)) is FieldOffsetAttribute offset) + { + try + { + var fieldAddr = baseAddr + offset.Value; + var name = field.Name[0] == '_' ? char.ToUpperInvariant(field.Name[1]) + field.Name[2..] : field.Name; + var fieldType = field.FieldType; + + if (!field.IsStatic && fieldType.IsPointer) + { + var pointer = (nint)Pointer.Unbox((Pointer)field.GetValue(obj)!); + var itemType = fieldType.GetElementType(); + ParsePointer(fieldAddr, pointer, itemType, name); + } + else if (fieldType.IsExplicitLayout) + { + ParseExplicitField(fieldAddr, field, fieldType, name); + } + else if (fieldType.Name.Contains("FixedSizeArray")) + { + ParseFixedSizeArray(fieldAddr, fieldType, name); + } + } + catch (Exception ex) + { + Log.Warning($"Failed to parse field at {offset.Value:X} in {baseType}!\n{ex}"); + } + } + } + + void ParseExplicitField(nint fieldAddr, FieldInfo field, MemberInfo fieldType, string name) + { + try + { + if (this.FieldNames.TryAdd(fieldAddr, new List(path!) { name }) && fieldType.DeclaringType == baseType) + { + this.PopulateFieldNames(field.GetValue(obj), fieldAddr, new List(path) { name }); + } + } + catch (Exception ex) + { + Log.Warning($"Failed to parse explicit field: {fieldType} {name} in {baseType}!\n{ex}"); + } + } + + void ParseFixedSizeArray(nint fieldAddr, Type fieldType, string name) + { + try + { + var spanLength = (int)(fieldType.CustomAttributes.ToArray()[0].ConstructorArguments[0].Value ?? 0); + + if (spanLength <= 0) + { + return; + } + + var itemType = fieldType.UnderlyingSystemType.GenericTypeArguments[0]; + + if (!itemType.IsGenericType) + { + var size = Marshal.SizeOf(itemType); + for (var i = 0; i < spanLength; i++) + { + var itemAddr = fieldAddr + (size * i); + var itemName = $"{name}[{i}]"; + + this.FieldNames.TryAdd(itemAddr, new List(path!) { itemName }); + + var item = Marshal.PtrToStructure(itemAddr, itemType); + if (itemType.DeclaringType == baseType) + { + this.PopulateFieldNames(item, itemAddr, new List(path) { name }); + } + } + } + else if (itemType.Name.Contains("Pointer")) + { + itemType = itemType.GenericTypeArguments[0]; + + for (var i = 0; i < spanLength; i++) + { + var itemAddr = fieldAddr + (0x08 * i); + var pointer = Marshal.ReadIntPtr(itemAddr); + ParsePointer(itemAddr, pointer, itemType, $"{name}[{i}]"); + } + } + } + catch (Exception ex) + { + Log.Warning($"Failed to parse fixed size array: {fieldType} {name} in {baseType}!\n{ex}"); + } + } + + void ParsePointer(nint fieldAddr, nint pointer, Type? itemType, string name) + { + try + { + if (pointer == 0) + { + return; + } + + this.FieldNames.TryAdd(fieldAddr, new List(path!) { name }); + this.FieldNames.TryAdd(pointer, new List(path) { name }); + + if (itemType?.DeclaringType != baseType || itemType.IsPointer) + { + return; + } + + var item = Marshal.PtrToStructure(pointer, itemType); + this.PopulateFieldNames(item, pointer, new List(path) { name }); + } + catch (Exception ex) + { + Log.Warning($"Failed to parse pointer: {itemType}* {name} in {baseType}!\n{ex}"); + } + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs new file mode 100644 index 000000000..6d6377530 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs @@ -0,0 +1,242 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using Dalamud.Interface.Components; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.FontAwesomeIcon; +using static Dalamud.Interface.Internal.UiDebug2.ElementSelector; +using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Utility.Util; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A class representing an , allowing it to be browsed within an ImGui window. +/// +public unsafe partial class AddonTree : IDisposable +{ + private readonly nint initialPtr; + + private AddonPopoutWindow? window; + + private AddonTree(string name, nint ptr) + { + this.AddonName = name; + this.initialPtr = ptr; + this.PopulateFieldNames(ptr); + } + + /// + /// Gets the name of the addon this tree represents. + /// + internal string AddonName { get; init; } + + /// + /// Gets or sets a collection of trees representing nodes within this addon. + /// + internal Dictionary NodeTrees { get; set; } = []; + + /// + public void Dispose() + { + foreach (var nodeTree in this.NodeTrees) + { + nodeTree.Value.Dispose(); + } + + this.NodeTrees.Clear(); + this.FieldNames.Clear(); + AddonTrees.Remove(this.AddonName); + if (this.window != null && PopoutWindows.Windows.Contains(this.window)) + { + PopoutWindows.RemoveWindow(this.window); + this.window?.Dispose(); + } + + GC.SuppressFinalize(this); + } + + /// + /// Gets an instance of for the given addon name (or creates one if none are found). + /// The tree can then be drawn within the Addon Inspector and browsed. + /// + /// The name of the addon. + /// The for the named addon. Returns null if it does not exist, or if it is not at the expected address. + internal static AddonTree? GetOrCreate(string? name) + { + if (name == null) + { + return null; + } + + try + { + var ptr = GameGui.GetAddonByName(name); + + if ((AtkUnitBase*)ptr != null) + { + if (AddonTrees.TryGetValue(name, out var tree)) + { + if (tree.initialPtr == ptr) + { + return tree; + } + + tree.Dispose(); + } + + var newTree = new AddonTree(name, ptr); + AddonTrees.Add(name, newTree); + return newTree; + } + } + catch + { + Log.Warning("Couldn't get addon!"); + } + + return null; + } + + /// + /// Draws this AddonTree within a window. + /// + internal void Draw() + { + if (!this.ValidateAddon(out var addon)) + { + return; + } + + var isVisible = addon->IsVisible; + + ImGui.Text($"{this.AddonName}"); + ImGui.SameLine(); + + ImGui.SameLine(); + ImGui.TextColored(isVisible ? new(0.1f, 1f, 0.1f, 1f) : new(0.6f, 0.6f, 0.6f, 1), isVisible ? "Visible" : "Not Visible"); + + ImGui.SameLine(ImGui.GetWindowWidth() - 100); + + if (ImGuiComponents.IconButton($"##vis{(nint)addon:X}", isVisible ? Eye : EyeSlash, isVisible ? new(0.0f, 0.8f, 0.2f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1))) + { + addon->IsVisible = !isVisible; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Toggle Visibility"); + } + + ImGui.SameLine(); + if (ImGuiComponents.IconButton("pop", this.window?.IsOpen == true ? Times : ArrowUpRightFromSquare, null)) + { + this.TogglePopout(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Toggle Popout Window"); + } + + ImGui.Separator(); + + PrintFieldValuePair("Address", $"{(nint)addon:X}"); + + var uldManager = addon->UldManager; + PrintFieldValuePairs( + ("X", $"{addon->X}"), + ("Y", $"{addon->X}"), + ("Scale", $"{addon->Scale}"), + ("Widget Count", $"{uldManager.ObjectCount}")); + + ImGui.Separator(); + + var addonObj = this.GetAddonObj(addon); + if (addonObj != null) + { + ShowStruct(addonObj, (ulong)addon); + } + + ImGui.Dummy(new(25 * ImGui.GetIO().FontGlobalScale)); + ImGui.Separator(); + + ResNodeTree.PrintNodeList(uldManager.NodeList, uldManager.NodeListCount, this); + + ImGui.Dummy(new(25 * ImGui.GetIO().FontGlobalScale)); + ImGui.Separator(); + + ResNodeTree.PrintNodeListAsTree(addon->CollisionNodeList, (int)addon->CollisionNodeListCount, "Collision List", this, new(0.5F, 0.7F, 1F, 1F)); + + if (SearchResults.Length > 0 && Countdown <= 0) + { + SearchResults = []; + } + } + + /// + /// Checks whether a given exists somewhere within this 's associated (or any of its s). + /// + /// The node to check. + /// true if the node was found. + internal bool ContainsNode(AtkResNode* node) => this.ValidateAddon(out var addon) && FindNode(node, addon); + + private static bool FindNode(AtkResNode* node, AtkUnitBase* addon) => addon != null && FindNode(node, addon->UldManager); + + private static bool FindNode(AtkResNode* node, AtkComponentNode* compNode) => compNode != null && FindNode(node, compNode->Component->UldManager); + + private static bool FindNode(AtkResNode* node, AtkUldManager uldManager) => FindNode(node, uldManager.NodeList, uldManager.NodeListCount); + + private static bool FindNode(AtkResNode* node, AtkResNode** list, int count) + { + for (var i = 0; i < count; i++) + { + var listNode = list[i]; + if (listNode == node) + { + return true; + } + + if ((int)listNode->Type >= 1000 && FindNode(node, (AtkComponentNode*)listNode)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether the addon exists at the expected address. If the addon is null or has a new address, disposes this instance of . + /// + /// The addon, if successfully found. + /// true if the addon is found. + private bool ValidateAddon(out AtkUnitBase* addon) + { + addon = (AtkUnitBase*)GameGui.GetAddonByName(this.AddonName); + if (addon == null || (nint)addon != this.initialPtr) + { + this.Dispose(); + return false; + } + + return true; + } + + private void TogglePopout() + { + if (this.window == null) + { + this.window = new AddonPopoutWindow(this, $"{this.AddonName}###addonPopout{this.initialPtr}"); + PopoutWindows.AddWindow(this.window); + } + else + { + this.window.IsOpen = !this.window.IsOpen; + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs new file mode 100644 index 000000000..bd1efe6c7 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs @@ -0,0 +1,68 @@ +using Dalamud.Interface.Internal.UiDebug2.Utility; + +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static ImGuiNET.ImGuiTableColumnFlags; +using static ImGuiNET.ImGuiTableFlags; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// Class that prints the events table for a node, where applicable. +/// +public static class Events +{ + /// + /// Prints out each for a given node. + /// + /// The node to print events for. + internal static unsafe void PrintEvents(AtkResNode* node) + { + var evt = node->AtkEventManager.Event; + if (evt == null) + { + return; + } + + if (ImGui.TreeNode($"Events##{(nint)node:X}eventTree")) + { + if (ImGui.BeginTable($"##{(nint)node:X}eventTable", 7, Resizable | SizingFixedFit | Borders | RowBg)) + { + ImGui.TableSetupColumn("#", WidthFixed); + ImGui.TableSetupColumn("Type", WidthFixed); + ImGui.TableSetupColumn("Param", WidthFixed); + ImGui.TableSetupColumn("Flags", WidthFixed); + ImGui.TableSetupColumn("Unk29", WidthFixed); + ImGui.TableSetupColumn("Target", WidthFixed); + ImGui.TableSetupColumn("Listener", WidthFixed); + + ImGui.TableHeadersRow(); + + var i = 0; + while (evt != null) + { + ImGui.TableNextColumn(); + ImGui.Text($"{i++}"); + ImGui.TableNextColumn(); + ImGui.Text($"{evt->Type}"); + ImGui.TableNextColumn(); + ImGui.Text($"{evt->Param}"); + ImGui.TableNextColumn(); + ImGui.Text($"{evt->Flags}"); + ImGui.TableNextColumn(); + ImGui.Text($"{evt->Unk29}"); + ImGui.TableNextColumn(); + Gui.ClickToCopyText($"{(nint)evt->Target:X}"); + ImGui.TableNextColumn(); + Gui.ClickToCopyText($"{(nint)evt->Listener:X}"); + evt = evt->NextEvent; + } + + ImGui.EndTable(); + } + + ImGui.TreePop(); + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs new file mode 100644 index 000000000..cfba1a2bc --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs @@ -0,0 +1,35 @@ +using FFXIVClientStructs.FFXIV.Component.GUI; + +using static Dalamud.Utility.Util; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A tree for an that can be printed and browsed via ImGui. +/// +internal unsafe class ClippingMaskNodeTree : ImageNodeTree +{ + /// + /// Initializes a new instance of the class. + /// + /// The node to create a tree for. + /// The tree representing the containing addon. + internal ClippingMaskNodeTree(AtkResNode* node, AddonTree addonTree) + : base(node, addonTree) + { + } + + /// + private protected override uint PartId => this.CmNode->PartId; + + /// + private protected override AtkUldPartsList* PartsList => this.CmNode->PartsList; + + private AtkClippingMaskNode* CmNode => (AtkClippingMaskNode*)this.Node; + + /// + private protected override void PrintNodeObject() => ShowStruct(this.CmNode); + + /// + private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) => this.DrawTextureAndParts(); +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs new file mode 100644 index 000000000..c447afac9 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs @@ -0,0 +1,24 @@ +using FFXIVClientStructs.FFXIV.Component.GUI; + +using static Dalamud.Utility.Util; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A tree for an that can be printed and browsed via ImGui. +/// +internal unsafe class CollisionNodeTree : ResNodeTree +{ + /// + /// Initializes a new instance of the class. + /// + /// The node to create a tree for. + /// The tree representing the containing addon. + internal CollisionNodeTree(AtkResNode* node, AddonTree addonTree) + : base(node, addonTree) + { + } + + /// + private protected override void PrintNodeObject() => ShowStruct((AtkCollisionNode*)this.Node); +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs new file mode 100644 index 000000000..2c95924c5 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -0,0 +1,283 @@ +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Utility.Util; +using static FFXIVClientStructs.FFXIV.Component.GUI.ComponentType; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A tree for an that can be printed and browsed via ImGui. +/// +internal unsafe class ComponentNodeTree : ResNodeTree +{ + private readonly AtkUldManager* uldManager; + + private readonly ComponentType componentType; + + private readonly AtkComponentBase* component; + + /// + /// Initializes a new instance of the class. + /// + /// The node to create a tree for. + /// The tree representing the containing addon. + internal ComponentNodeTree(AtkResNode* node, AddonTree addonTree) + : base(node, addonTree) + { + this.component = ((AtkComponentNode*)node)->Component; + this.uldManager = &this.component->UldManager; + this.NodeType = 0; + this.componentType = ((AtkUldComponentInfo*)this.uldManager->Objects)->ComponentType; + } + + /// + private protected override string GetHeaderText() + { + var childCount = (int)this.uldManager->NodeListCount; + return $"{this.componentType} Component Node{(childCount > 0 ? $" [+{childCount}]" : string.Empty)} (Node: {(nint)this.Node:X} / Comp: {(nint)this.component:X})"; + } + + /// + private protected override void PrintNodeObject() + { + base.PrintNodeObject(); + this.PrintComponentObject(); + ImGui.SameLine(); + ImGui.NewLine(); + this.PrintComponentDataObject(); + ImGui.SameLine(); + ImGui.NewLine(); + } + + /// + private protected override void PrintChildNodes() + { + base.PrintChildNodes(); + var count = this.uldManager->NodeListCount; + PrintNodeListAsTree(this.uldManager->NodeList, count, $"Node List [{count}]:", this.AddonTree, new(0f, 0.5f, 0.8f, 1f)); + } + + /// + private protected override void PrintFieldNames() + { + this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1)); + this.PrintFieldName((nint)this.component, new(0f, 0.5f, 0.8f, 1f)); + } + + /// + private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) + { + if (this.component == null) + { + return; + } + + switch (this.componentType) + { + case TextInput: + var textInputComponent = (AtkComponentTextInput*)this.component; + ImGui.Text( + $"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}"); + ImGui.Text( + $"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}"); + ImGui.Text( + $"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}"); + ImGui.Text( + $"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); + ImGui.Text( + $"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}"); + ImGui.Text( + $"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}"); + ImGui.Text( + $"Text5: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText05.StringPtr))}"); + break; + case List: + case TreeList: + var l = (AtkComponentList*)this.component; + if (ImGui.SmallButton("Inc.Selected")) + { + l->SelectedItemIndex++; + } + + break; + default: + break; + } + } + + private void PrintComponentObject() + { + PrintFieldValuePair("Component", $"{(nint)this.component:X}"); + + ImGui.SameLine(); + + switch (this.componentType) + { + case Button: + ShowStruct((AtkComponentButton*)this.component); + break; + case Slider: + ShowStruct((AtkComponentSlider*)this.component); + break; + case Window: + ShowStruct((AtkComponentWindow*)this.component); + break; + case CheckBox: + ShowStruct((AtkComponentCheckBox*)this.component); + break; + case GaugeBar: + ShowStruct((AtkComponentGaugeBar*)this.component); + break; + case RadioButton: + ShowStruct((AtkComponentRadioButton*)this.component); + break; + case TextInput: + ShowStruct((AtkComponentTextInput*)this.component); + break; + case Icon: + ShowStruct((AtkComponentIcon*)this.component); + break; + case NumericInput: + ShowStruct((AtkComponentNumericInput*)this.component); + break; + case List: + ShowStruct((AtkComponentList*)this.component); + break; + case TreeList: + ShowStruct((AtkComponentTreeList*)this.component); + break; + case DropDownList: + ShowStruct((AtkComponentDropDownList*)this.component); + break; + case ScrollBar: + ShowStruct((AtkComponentScrollBar*)this.component); + break; + case ListItemRenderer: + ShowStruct((AtkComponentListItemRenderer*)this.component); + break; + case IconText: + ShowStruct((AtkComponentIconText*)this.component); + break; + case ComponentType.DragDrop: + ShowStruct((AtkComponentDragDrop*)this.component); + break; + case GuildLeveCard: + ShowStruct((AtkComponentGuildLeveCard*)this.component); + break; + case TextNineGrid: + ShowStruct((AtkComponentTextNineGrid*)this.component); + break; + case JournalCanvas: + ShowStruct((AtkComponentJournalCanvas*)this.component); + break; + case HoldButton: + ShowStruct((AtkComponentHoldButton*)this.component); + break; + case Portrait: + ShowStruct((AtkComponentPortrait*)this.component); + break; + default: + ShowStruct(this.component); + break; + } + } + + private void PrintComponentDataObject() + { + var componentData = this.component->UldManager.ComponentData; + PrintFieldValuePair("Data", $"{(nint)componentData:X}"); + + if (componentData != null) + { + ImGui.SameLine(); + switch (this.componentType) + { + case Base: + ShowStruct(componentData); + break; + case Button: + ShowStruct((AtkUldComponentDataButton*)componentData); + break; + case Window: + ShowStruct((AtkUldComponentDataWindow*)componentData); + break; + case CheckBox: + ShowStruct((AtkUldComponentDataCheckBox*)componentData); + break; + case RadioButton: + ShowStruct((AtkUldComponentDataRadioButton*)componentData); + break; + case GaugeBar: + ShowStruct((AtkUldComponentDataGaugeBar*)componentData); + break; + case Slider: + ShowStruct((AtkUldComponentDataSlider*)componentData); + break; + case TextInput: + ShowStruct((AtkUldComponentDataTextInput*)componentData); + break; + case NumericInput: + ShowStruct((AtkUldComponentDataNumericInput*)componentData); + break; + case List: + ShowStruct((AtkUldComponentDataList*)componentData); + break; + case DropDownList: + ShowStruct((AtkUldComponentDataDropDownList*)componentData); + break; + case Tab: + ShowStruct((AtkUldComponentDataTab*)componentData); + break; + case TreeList: + ShowStruct((AtkUldComponentDataTreeList*)componentData); + break; + case ScrollBar: + ShowStruct((AtkUldComponentDataScrollBar*)componentData); + break; + case ListItemRenderer: + ShowStruct((AtkUldComponentDataListItemRenderer*)componentData); + break; + case Icon: + ShowStruct((AtkUldComponentDataIcon*)componentData); + break; + case IconText: + ShowStruct((AtkUldComponentDataIconText*)componentData); + break; + case ComponentType.DragDrop: + ShowStruct((AtkUldComponentDataDragDrop*)componentData); + break; + case GuildLeveCard: + ShowStruct((AtkUldComponentDataGuildLeveCard*)componentData); + break; + case TextNineGrid: + ShowStruct((AtkUldComponentDataTextNineGrid*)componentData); + break; + case JournalCanvas: + ShowStruct((AtkUldComponentDataJournalCanvas*)componentData); + break; + case Multipurpose: + ShowStruct((AtkUldComponentDataMultipurpose*)componentData); + break; + case Map: + ShowStruct((AtkUldComponentDataMap*)componentData); + break; + case Preview: + ShowStruct((AtkUldComponentDataPreview*)componentData); + break; + case HoldButton: + ShowStruct((AtkUldComponentDataHoldButton*)componentData); + break; + case Portrait: + ShowStruct((AtkUldComponentDataPortrait*)componentData); + break; + default: + ShowStruct(componentData); + break; + } + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs new file mode 100644 index 000000000..ff40db37a --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs @@ -0,0 +1,36 @@ +using FFXIVClientStructs.FFXIV.Component.GUI; + +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Utility.Util; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A tree for an that can be printed and browsed via ImGui. +/// +internal unsafe partial class CounterNodeTree : ResNodeTree +{ + /// + /// Initializes a new instance of the class. + /// + /// The node to create a tree for. + /// The tree representing the containing addon. + internal CounterNodeTree(AtkResNode* node, AddonTree addonTree) + : base(node, addonTree) + { + } + + private AtkCounterNode* CntNode => (AtkCounterNode*)this.Node; + + /// + private protected override void PrintNodeObject() => ShowStruct(this.CntNode); + + /// + private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) + { + if (!isEditorOpen) + { + PrintFieldValuePairs(("Text", ((AtkCounterNode*)this.Node)->NodeText.ToString())); + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs new file mode 100644 index 000000000..ad7c09165 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs @@ -0,0 +1,384 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using Dalamud.Interface.Internal.UiDebug2.Utility; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.ColorHelpers; +using static Dalamud.Interface.FontAwesomeIcon; +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Interface.Utility.ImGuiHelpers; +using static ImGuiNET.ImGuiColorEditFlags; +using static ImGuiNET.ImGuiInputTextFlags; +using static ImGuiNET.ImGuiTableColumnFlags; +using static ImGuiNET.ImGuiTableFlags; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +internal unsafe partial class ResNodeTree +{ + /// + /// Sets up the table for the node editor, if the "Edit" checkbox is ticked. + /// + private protected void DrawNodeEditorTable() + { + ImGui.BeginTable($"###Editor{(nint)this.Node}", 2, SizingStretchProp | NoHostExtendX); + + this.DrawEditorRows(); + + ImGui.EndTable(); + } + + /// + /// Draws each row in the node editor table. + /// + private protected virtual void DrawEditorRows() + { + var pos = new Vector2(this.Node->X, this.Node->Y); + var size = new Vector2(this.Node->Width, this.Node->Height); + var scale = new Vector2(this.Node->ScaleX, this.Node->ScaleY); + var origin = new Vector2(this.Node->OriginX, this.Node->OriginY); + var angle = (float)((this.Node->Rotation * (180 / Math.PI)) + 360); + + var rgba = RgbaUintToVector4(this.Node->Color.RGBA); + var mult = new Vector3(this.Node->MultiplyRed, this.Node->MultiplyGreen, this.Node->MultiplyBlue) / 255f; + var add = new Vector3(this.Node->AddRed, this.Node->AddGreen, this.Node->AddBlue); + + var hov = false; + + ImGui.TableSetupColumn("Labels", WidthFixed); + ImGui.TableSetupColumn("Editors", WidthFixed); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Position:"); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.DragFloat2($"##{(nint)this.Node:X}position", ref pos, 1, default, default, "%.0f")) + { + this.Node->X = pos.X; + this.Node->Y = pos.Y; + this.Node->DrawFlags |= 0xD; + } + + hov |= SplitTooltip("X", "Y") || ImGui.IsItemActive(); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Size:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.DragFloat2($"##{(nint)this.Node:X}size", ref size, 1, 0, default, "%.0f")) + { + this.Node->Width = (ushort)Math.Max(size.X, 0); + this.Node->Height = (ushort)Math.Max(size.Y, 0); + this.Node->DrawFlags |= 0xD; + } + + hov |= SplitTooltip("Width", "Height") || ImGui.IsItemActive(); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Scale:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.DragFloat2($"##{(nint)this.Node:X}scale", ref scale, 0.05f)) + { + this.Node->ScaleX = scale.X; + this.Node->ScaleY = scale.Y; + this.Node->DrawFlags |= 0xD; + } + + hov |= SplitTooltip("ScaleX", "ScaleY") || ImGui.IsItemActive(); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Origin:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.DragFloat2($"##{(nint)this.Node:X}origin", ref origin, 1, default, default, "%.0f")) + { + this.Node->OriginX = origin.X; + this.Node->OriginY = origin.Y; + this.Node->DrawFlags |= 0xD; + } + + hov |= SplitTooltip("OriginX", "OriginY") || ImGui.IsItemActive(); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Rotation:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + while (angle > 180) + { + angle -= 360; + } + + if (ImGui.DragFloat($"##{(nint)this.Node:X}rotation", ref angle, 0.05f, default, default, "%.2f°")) + { + this.Node->Rotation = (float)(angle / (180 / Math.PI)); + this.Node->DrawFlags |= 0xD; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Rotation (deg)"); + hov = true; + } + + hov |= ImGui.IsItemActive(); + + if (hov) + { + Vector4 brightYellow = new(1, 1, 0.5f, 0.8f); + new NodeBounds(this.Node).Draw(brightYellow); + new NodeBounds(origin, this.Node).Draw(brightYellow); + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("RGBA:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.ColorEdit4($"##{(nint)this.Node:X}RGBA", ref rgba, DisplayHex)) + { + this.Node->Color = new() { RGBA = RgbaVector4ToUint(rgba) }; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Multiply:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.ColorEdit3($"##{(nint)this.Node:X}multiplyRGB", ref mult, DisplayHex)) + { + this.Node->MultiplyRed = (byte)(mult.X * 255); + this.Node->MultiplyGreen = (byte)(mult.Y * 255); + this.Node->MultiplyBlue = (byte)(mult.Z * 255); + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Add:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(124); + + if (ImGui.DragFloat3($"##{(nint)this.Node:X}addRGB", ref add, 1, -255, 255, "%.0f")) + { + this.Node->AddRed = (short)add.X; + this.Node->AddGreen = (short)add.Y; + this.Node->AddBlue = (short)add.Z; + } + + SplitTooltip("+/- Red", "+/- Green", "+/- Blue"); + + var addTransformed = (add / 510f) + new Vector3(0.5f); + + ImGui.SameLine(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() - (4 * GlobalScale)); + if (ImGui.ColorEdit3($"##{(nint)this.Node:X}addRGBPicker", ref addTransformed, NoAlpha | NoInputs)) + { + this.Node->AddRed = (short)Math.Floor((addTransformed.X * 510f) - 255f); + this.Node->AddGreen = (short)Math.Floor((addTransformed.Y * 510f) - 255f); + this.Node->AddBlue = (short)Math.Floor((addTransformed.Z * 510f) - 255f); + } + } +} + +/// +internal unsafe partial class CounterNodeTree +{ + /// + private protected override void DrawEditorRows() + { + base.DrawEditorRows(); + + var str = this.CntNode->NodeText.ToString(); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Counter:"); + ImGui.TableNextColumn(); + + ImGui.SetNextItemWidth(150); + if (ImGui.InputText($"##{(nint)this.Node:X}counterEdit", ref str, 512, EnterReturnsTrue)) + { + this.CntNode->SetText(str); + } + } +} + +/// +internal unsafe partial class ImageNodeTree +{ + private static int TexDisplayStyle { get; set; } + + /// + private protected override void DrawEditorRows() + { + base.DrawEditorRows(); + + var partId = (int)this.PartId; + var partcount = this.ImgNode->PartsList->PartCount; + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Part Id:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.InputInt($"##partId{(nint)this.Node:X}", ref partId, 1, 1)) + { + if (partId < 0) + { + partId = 0; + } + + if (partId >= partcount) + { + partId = (int)(partcount - 1); + } + + this.ImgNode->PartId = (ushort)partId; + } + } +} + +/// +internal unsafe partial class NineGridNodeTree +{ + /// + private protected override void DrawEditorRows() + { + base.DrawEditorRows(); + + var lr = new Vector2(this.Offsets.Left, this.Offsets.Right); + var tb = new Vector2(this.Offsets.Top, this.Offsets.Bottom); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Ninegrid Offsets:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.DragFloat2($"##{(nint)this.Node:X}ngOffsetLR", ref lr, 1, 0)) + { + this.NgNode->LeftOffset = (short)Math.Max(0, lr.X); + this.NgNode->RightOffset = (short)Math.Max(0, lr.Y); + } + + SplitTooltip("Left", "Right"); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.DragFloat2($"##{(nint)this.Node:X}ngOffsetTB", ref tb, 1, 0)) + { + this.NgNode->TopOffset = (short)Math.Max(0, tb.X); + this.NgNode->BottomOffset = (short)Math.Max(0, tb.Y); + } + + SplitTooltip("Top", "Bottom"); + } +} + +/// +internal unsafe partial class TextNodeTree +{ + private static readonly List FontList = Enum.GetValues().ToList(); + + private static readonly string[] FontNames = Enum.GetNames(); + + /// + private protected override void DrawEditorRows() + { + base.DrawEditorRows(); + + var text = this.TxtNode->NodeText.ToString(); + var fontIndex = FontList.IndexOf(this.TxtNode->FontType); + int fontSize = this.TxtNode->FontSize; + var alignment = this.TxtNode->AlignmentType; + var textColor = RgbaUintToVector4(this.TxtNode->TextColor.RGBA); + var edgeColor = RgbaUintToVector4(this.TxtNode->EdgeColor.RGBA); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Text:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(Math.Max(ImGui.GetWindowContentRegionMax().X - ImGui.GetCursorPosX() - 50f, 150)); + if (ImGui.InputText($"##{(nint)this.Node:X}textEdit", ref text, 512, EnterReturnsTrue)) + { + this.TxtNode->SetText(text); + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Font:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.Combo($"##{(nint)this.Node:X}fontType", ref fontIndex, FontNames, FontList.Count)) + { + this.TxtNode->FontType = FontList[fontIndex]; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Font Size:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.InputInt($"##{(nint)this.Node:X}fontSize", ref fontSize, 1, 10)) + { + this.TxtNode->FontSize = (byte)fontSize; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Alignment:"); + ImGui.TableNextColumn(); + if (InputAlignment($"##{(nint)this.Node:X}alignment", ref alignment)) + { + this.TxtNode->AlignmentType = alignment; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Text Color:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.ColorEdit4($"##{(nint)this.Node:X}TextRGB", ref textColor, DisplayHex)) + { + this.TxtNode->TextColor = new() { RGBA = RgbaVector4ToUint(textColor) }; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text("Edge Color:"); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(150); + if (ImGui.ColorEdit4($"##{(nint)this.Node:X}EdgeRGB", ref edgeColor, DisplayHex)) + { + this.TxtNode->EdgeColor = new() { RGBA = RgbaVector4ToUint(edgeColor) }; + } + } + + private static bool InputAlignment(string label, ref AlignmentType alignment) + { + var hAlign = (int)alignment % 3; + var vAlign = ((int)alignment - hAlign) / 3; + + var hAlignInput = IconSelectInput($"{label}H", ref hAlign, new() { 0, 1, 2 }, new() { AlignLeft, AlignCenter, AlignRight }); + var vAlignInput = IconSelectInput($"{label}V", ref vAlign, new() { 0, 1, 2 }, new() { ArrowsUpToLine, GripLines, ArrowsDownToLine }); + + if (hAlignInput || vAlignInput) + { + alignment = (AlignmentType)((vAlign * 3) + hAlign); + return true; + } + + return false; + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs new file mode 100644 index 000000000..f3cc69618 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs @@ -0,0 +1,317 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.ColorHelpers; +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Utility.Util; +using static FFXIVClientStructs.FFXIV.Component.GUI.TextureType; +using static ImGuiNET.ImGuiTableColumnFlags; +using static ImGuiNET.ImGuiTableFlags; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A tree for an that can be printed and browsed via ImGui. +/// +internal unsafe partial class ImageNodeTree : ResNodeTree +{ + /// + /// Initializes a new instance of the class. + /// + /// The node to create a tree for. + /// The tree representing the containing addon. + internal ImageNodeTree(AtkResNode* node, AddonTree addonTree) + : base(node, addonTree) + { + } + + /// + /// Gets the part ID that this node uses. + /// + private protected virtual uint PartId => this.ImgNode->PartId; + + /// + /// Gets the parts list that this node uses. + /// + private protected virtual AtkUldPartsList* PartsList => this.ImgNode->PartsList; + + /// + /// Gets or sets a summary of pertinent data about this 's texture. Updated each time is called. + /// + private protected TextureData TexData { get; set; } + + private AtkImageNode* ImgNode => (AtkImageNode*)this.Node; + + /// + /// Draws the texture inside the window, in either of two styles.

+ /// Full Imagepresents the texture in full as a spritesheet.
+ /// Parts Listpresents the individual parts as rows in a table. + ///
+ private protected void DrawTextureAndParts() + { + this.TexData = new TextureData(this.PartsList, this.PartId); + + if (this.TexData.Texture == null) + { + return; + } + + if (NestedTreePush($"Texture##texture{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", out _)) + { + PrintFieldValuePairs( + ("Texture Type", $"{this.TexData.TexType}"), + ("Part ID", $"{this.TexData.PartId}"), + ("Part Count", $"{this.TexData.PartCount}")); + + if (this.TexData.Path != null) + { + PrintFieldValuePairs(("Texture Path", this.TexData.Path)); + } + + if (ImGui.RadioButton("Full Image##textureDisplayStyle0", TexDisplayStyle == 0)) + { + TexDisplayStyle = 0; + } + + ImGui.SameLine(); + if (ImGui.RadioButton("Parts List##textureDisplayStyle1", TexDisplayStyle == 1)) + { + TexDisplayStyle = 1; + } + + ImGui.NewLine(); + + if (TexDisplayStyle == 1) + { + this.PrintPartsTable(); + } + else + { + this.DrawFullTexture(); + } + + ImGui.TreePop(); + } + } + + /// + /// Draws an outline of a given part within the texture. + /// + /// The part ID. + /// The absolute position of the cursor onscreen. + /// The relative position of the cursor within the window. + /// The color of the outline. + /// Whether this outline requires the user to mouse over it. + private protected virtual void DrawPartOutline(uint partId, Vector2 cursorScreenPos, Vector2 cursorLocalPos, Vector4 col, bool reqHover = false) + { + var part = this.TexData.PartsList->Parts[partId]; + + var hrFactor = this.TexData.HiRes ? 2f : 1f; + + var uv = new Vector2(part.U, part.V) * hrFactor; + var wh = new Vector2(part.Width, part.Height) * hrFactor; + + var partBegin = cursorScreenPos + uv; + var partEnd = partBegin + wh; + + if (reqHover && !ImGui.IsMouseHoveringRect(partBegin, partEnd)) + { + return; + } + + var savePos = ImGui.GetCursorPos(); + + ImGui.GetWindowDrawList().AddRect(partBegin, partEnd, RgbaVector4ToUint(col)); + + ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20)); + ImGui.TextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}"); + ImGui.SetCursorPos(savePos); + } + + /// + private protected override void PrintNodeObject() => ShowStruct(this.ImgNode); + + /// + private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) + { + PrintFieldValuePairs( + ("Wrap", $"{this.ImgNode->WrapMode}"), + ("Image Flags", $"0x{this.ImgNode->Flags:X}")); + this.DrawTextureAndParts(); + } + + private static void PrintPartCoords(float u, float v, float w, float h, bool asFloat = false, bool lineBreak = false) + { + ImGui.TextDisabled($"{u}, {v},{(lineBreak ? "\n" : " ")}{w}, {h}"); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Click to copy as Vector2\nShift-click to copy as Vector4"); + } + + var suffix = asFloat ? "f" : string.Empty; + + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText(ImGui.IsKeyDown(ImGuiKey.ModShift) + ? $"new Vector4({u}{suffix}, {v}{suffix}, {w}{suffix}, {h}{suffix})" + : $"new Vector2({u}{suffix}, {v}{suffix});\nnew Vector2({w}{suffix}, {h}{suffix})"); + } + } + + private void DrawFullTexture() + { + var cursorScreenPos = ImGui.GetCursorScreenPos(); + var cursorLocalPos = ImGui.GetCursorPos(); + + ImGui.Image(new(this.TexData.Texture->D3D11ShaderResourceView), new(this.TexData.Texture->Width, this.TexData.Texture->Height)); + + for (uint p = 0; p < this.TexData.PartsList->PartCount; p++) + { + if (p == this.TexData.PartId) + { + continue; + } + + this.DrawPartOutline(p, cursorScreenPos, cursorLocalPos, new(0.6f, 0.6f, 0.6f, 1), true); + } + + this.DrawPartOutline(this.TexData.PartId, cursorScreenPos, cursorLocalPos, new(0, 0.85F, 1, 1)); + } + + private void PrintPartsTable() + { + ImGui.BeginTable($"partsTable##{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", 3, Borders | RowBg | Reorderable); + ImGui.TableSetupColumn("Part ID", WidthFixed); + ImGui.TableSetupColumn("Part Texture", WidthFixed); + ImGui.TableSetupColumn("Coordinates", WidthFixed); + + ImGui.TableHeadersRow(); + + var tWidth = this.TexData.Texture->Width; + var tHeight = this.TexData.Texture->Height; + var textureSize = new Vector2(tWidth, tHeight); + + for (ushort i = 0; i < this.TexData.PartCount; i++) + { + ImGui.TableNextColumn(); + + if (i == this.TexData.PartId) + { + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0.85F, 1, 1)); + } + + ImGui.Text($"#{i.ToString().PadLeft(this.TexData.PartCount.ToString().Length, '0')}"); + + if (i == this.TexData.PartId) + { + ImGui.PopStyleColor(1); + } + + ImGui.TableNextColumn(); + + var part = this.TexData.PartsList->Parts[i]; + var hiRes = this.TexData.HiRes; + + var u = hiRes ? part.U * 2f : part.U; + var v = hiRes ? part.V * 2f : part.V; + var width = hiRes ? part.Width * 2f : part.Width; + var height = hiRes ? part.Height * 2f : part.Height; + + ImGui.Image(new(this.TexData.Texture->D3D11ShaderResourceView), new(width, height), new Vector2(u, v) / textureSize, new Vector2(u + width, v + height) / textureSize); + + ImGui.TableNextColumn(); + + ImGui.TextColored(!hiRes ? new(1) : new(0.6f, 0.6f, 0.6f, 1), "Standard:\t"); + ImGui.SameLine(); + var cursX = ImGui.GetCursorPosX(); + + PrintPartCoords(u / 2f, v / 2f, width / 2f, height / 2f); + + ImGui.TextColored(hiRes ? new(1) : new(0.6f, 0.6f, 0.6f, 1), "Hi-Res:\t"); + ImGui.SameLine(); + ImGui.SetCursorPosX(cursX); + + PrintPartCoords(u, v, width, height); + + ImGui.Text("UV:\t"); + ImGui.SameLine(); + ImGui.SetCursorPosX(cursX); + + PrintPartCoords(u / tWidth, v / tWidth, (u + width) / tWidth, (v + height) / tHeight, true, true); + } + + ImGui.EndTable(); + } + + /// + /// A summary of pertinent data about a node's texture. + /// + protected struct TextureData + { + /// The texture's partslist. + public AtkUldPartsList* PartsList; + + /// The number of parts in the texture. + public uint PartCount; + + /// The part ID the node is using. + public uint PartId; + + /// The texture itself. + public Texture* Texture = null; + + /// The type of texture. + public TextureType TexType = 0; + + /// The texture's file path (if , otherwise this value is null). + public string? Path = null; + + /// Whether this is a high-resolution texture. + public bool HiRes = false; + + /// + /// Initializes a new instance of the struct. + /// + /// The texture's parts list. + /// The part ID being used by the node. + public TextureData(AtkUldPartsList* partsList, uint partId) + { + this.PartsList = partsList; + this.PartCount = this.PartsList->PartCount; + this.PartId = partId >= this.PartCount ? 0 : partId; + + if (this.PartsList == null) + { + return; + } + + var asset = this.PartsList->Parts[this.PartId].UldAsset; + + if (asset == null) + { + return; + } + + this.TexType = asset->AtkTexture.TextureType; + + if (this.TexType == Resource) + { + var resource = asset->AtkTexture.Resource; + this.Texture = resource->KernelTextureObject; + this.Path = Marshal.PtrToStringAnsi(new(resource->TexFileResourceHandle->ResourceHandle.FileName.BufferPtr)); + } + else + { + this.Texture = this.TexType == KernelTexture ? asset->AtkTexture.KernelTexture : null; + this.Path = null; + } + + this.HiRes = this.Path?.Contains("_hr1") ?? false; + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.Offsets.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.Offsets.cs new file mode 100644 index 000000000..237303155 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.Offsets.cs @@ -0,0 +1,69 @@ +using System.Numerics; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +internal unsafe partial class NineGridNodeTree +{ + /// + /// A struct representing the four offsets of an . + /// + internal struct NineGridOffsets + { + /// Top offset. + internal int Top; + + /// Left offset. + internal int Left; + + /// Right offset. + internal int Right; + + /// Bottom offset. + internal int Bottom; + + /// + /// Initializes a new instance of the struct. + /// + /// The top offset. + /// The right offset. + /// The bottom offset. + /// The left offset. + internal NineGridOffsets(int top, int right, int bottom, int left) + { + this.Top = top; + this.Right = right; + this.Left = left; + this.Bottom = bottom; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The node using these offsets. + internal NineGridOffsets(AtkNineGridNode* ngNode) + : this(ngNode->TopOffset, ngNode->RightOffset, ngNode->BottomOffset, ngNode->LeftOffset) + { + } + + private NineGridOffsets(Vector4 v) + : this((int)v.X, (int)v.Y, (int)v.Z, (int)v.W) + { + } + + public static implicit operator NineGridOffsets(Vector4 v) => new(v); + + public static implicit operator Vector4(NineGridOffsets v) => new(v.Top, v.Right, v.Bottom, v.Left); + + public static NineGridOffsets operator *(float n, NineGridOffsets a) => n * (Vector4)a; + + public static NineGridOffsets operator *(NineGridOffsets a, float n) => n * a; + + /// Prints the offsets in ImGui. + internal readonly void Print() => PrintFieldValuePairs(("Top", $"{this.Top}"), ("Bottom", $"{this.Bottom}"), ("Left", $"{this.Left}"), ("Right", $"{this.Right}")); + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs new file mode 100644 index 000000000..3c66d44c3 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs @@ -0,0 +1,89 @@ +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.ColorHelpers; +using static Dalamud.Utility.Util; + +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A tree for an that can be printed and browsed via ImGui. +/// +internal unsafe partial class NineGridNodeTree : ImageNodeTree +{ + /// + /// Initializes a new instance of the class. + /// + /// The node to create a tree for. + /// The tree representing the containing addon. + internal NineGridNodeTree(AtkResNode* node, AddonTree addonTree) + : base(node, addonTree) + { + } + + /// + private protected override uint PartId => this.NgNode->PartId; + + /// + private protected override AtkUldPartsList* PartsList => this.NgNode->PartsList; + + private AtkNineGridNode* NgNode => (AtkNineGridNode*)this.Node; + + private NineGridOffsets Offsets => new(this.NgNode); + + /// + private protected override void DrawPartOutline( + uint partId, Vector2 cursorScreenPos, Vector2 cursorLocalPos, Vector4 col, bool reqHover = false) + { + var part = this.TexData.PartsList->Parts[partId]; + + var hrFactor = this.TexData.HiRes ? 2f : 1f; + var uv = new Vector2(part.U, part.V) * hrFactor; + var wh = new Vector2(part.Width, part.Height) * hrFactor; + var partBegin = cursorScreenPos + uv; + var partEnd = cursorScreenPos + uv + wh; + + var savePos = ImGui.GetCursorPos(); + + if (!reqHover || ImGui.IsMouseHoveringRect(partBegin, partEnd)) + { + var adjustedOffsets = this.Offsets * hrFactor; + var ngBegin1 = partBegin with { X = partBegin.X + adjustedOffsets.Left }; + var ngEnd1 = partEnd with { X = partEnd.X - adjustedOffsets.Right }; + + var ngBegin2 = partBegin with { Y = partBegin.Y + adjustedOffsets.Top }; + var ngEnd2 = partEnd with { Y = partEnd.Y - adjustedOffsets.Bottom }; + + var ngCol = RgbaVector4ToUint(col with { W = 0.75f * col.W }); + + ImGui.GetWindowDrawList() + .AddRect(partBegin, partEnd, RgbaVector4ToUint(col)); + ImGui.GetWindowDrawList().AddRect(ngBegin1, ngEnd1, ngCol); + ImGui.GetWindowDrawList().AddRect(ngBegin2, ngEnd2, ngCol); + + ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20)); + ImGui.TextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}"); + } + + ImGui.SetCursorPos(savePos); + } + + /// + private protected override void PrintNodeObject() => ShowStruct(this.NgNode); + + /// + private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) + { + if (!isEditorOpen) + { + ImGui.Text("NineGrid Offsets:\t"); + ImGui.SameLine(); + this.Offsets.Print(); + } + + this.DrawTextureAndParts(); + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs new file mode 100644 index 000000000..222ca30d4 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs @@ -0,0 +1,402 @@ +using System.Linq; +using System.Numerics; + +using Dalamud.Interface.Components; +using Dalamud.Interface.Internal.UiDebug2.Utility; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.ColorHelpers; +using static Dalamud.Interface.FontAwesomeIcon; +using static Dalamud.Interface.Internal.UiDebug2.Browsing.Events; +using static Dalamud.Interface.Internal.UiDebug2.ElementSelector; +using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Utility.Util; +using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A tree for an that can be printed and browsed via ImGui. +/// +/// As with the structs they represent, this class serves as the base class for other types of NodeTree. +internal unsafe partial class ResNodeTree : IDisposable +{ + private NodePopoutWindow? window; + + private bool editorOpen; + + /// + /// Initializes a new instance of the class. + /// + /// The node to create a tree for. + /// The tree representing the containing addon. + private protected ResNodeTree(AtkResNode* node, AddonTree addonTree) + { + this.Node = node; + this.AddonTree = addonTree; + this.NodeType = node->Type; + this.AddonTree.NodeTrees.Add((nint)this.Node, this); + } + + /// + /// Gets or sets the this tree represents. + /// + protected internal AtkResNode* Node { get; set; } + + /// + /// Gets the containing this tree. + /// + protected internal AddonTree AddonTree { get; private set; } + + /// + /// Gets this node's type. + /// + private protected NodeType NodeType { get; init; } + + /// + /// Clears this NodeTree's popout window, if it has one. + /// + public void Dispose() + { + if (this.window != null && PopoutWindows.Windows.Contains(this.window)) + { + PopoutWindows.RemoveWindow(this.window); + this.window.Dispose(); + } + } + + /// + /// Gets an instance of (or one of its inheriting types) for the given node. If no instance exists, one is created. + /// + /// The node to get a tree for. + /// The tree for the node's containing addon. + /// An existing or newly-created instance of . + internal static ResNodeTree GetOrCreate(AtkResNode* node, AddonTree addonTree) => + addonTree.NodeTrees.TryGetValue((nint)node, out var nodeTree) ? nodeTree + : (int)node->Type > 1000 + ? new ComponentNodeTree(node, addonTree) + : node->Type switch + { + NodeType.Text => new TextNodeTree(node, addonTree), + NodeType.Image => new ImageNodeTree(node, addonTree), + NodeType.NineGrid => new NineGridNodeTree(node, addonTree), + NodeType.ClippingMask => new ClippingMaskNodeTree(node, addonTree), + NodeType.Counter => new CounterNodeTree(node, addonTree), + NodeType.Collision => new CollisionNodeTree(node, addonTree), + _ => new ResNodeTree(node, addonTree), + }; + + /// + /// Prints a list of NodeTree for a given list of nodes. + /// + /// The address of the start of the list. + /// The number of nodes in the list. + /// The tree for the containing addon. + internal static void PrintNodeList(AtkResNode** nodeList, int count, AddonTree addonTree) + { + for (uint j = 0; j < count; j++) + { + GetOrCreate(nodeList[j], addonTree).Print(j); + } + } + + /// + /// Calls , but outputs the results as a collapsible tree. + /// + /// The address of the start of the list. + /// The number of nodes in the list. + /// The heading text of the tree. + /// The tree for the containing addon. + /// The text color of the heading. + internal static void PrintNodeListAsTree(AtkResNode** nodeList, int count, string label, AddonTree addonTree, Vector4 color) + { + if (count <= 0) + { + return; + } + + ImGui.PushStyleColor(ImGuiCol.Text, color); + var treeOpened = NestedTreePush($"{label}##{(nint)nodeList:X}", color, out var lineStart); + ImGui.PopStyleColor(); + + if (treeOpened) + { + PrintNodeList(nodeList, count, addonTree); + NestedTreePop(lineStart, color); + } + } + + /// + /// Prints this tree in the window. + /// + /// The index of the tree within its containing node or addon, if applicable. + /// Whether the tree should default to being open. + internal void Print(uint? index, bool forceOpen = false) + { + if (SearchResults.Length > 0 && SearchResults[0] == (nint)this.Node) + { + this.PrintWithHighlights(index); + } + else + { + this.PrintTree(index, forceOpen); + } + } + + /// + /// Prints out the tree's header text. + /// + internal void WriteTreeHeading() + { + ImGui.Text(this.GetHeaderText()); + this.PrintFieldNames(); + } + + /// + /// If the given pointer has been identified as a field within the addon struct, this method prints that field's name. + /// + /// The pointer to check. + /// The text color to use. + private protected void PrintFieldName(nint ptr, Vector4 color) + { + if (this.AddonTree.FieldNames.TryGetValue(ptr, out var result)) + { + ImGui.SameLine(); + ImGui.TextColored(color, string.Join(".", result)); + } + } + + /// + /// Builds a string that will serve as the header text for the tree. Indicates the node type, the number of direct children it contains, and its pointer. + /// + /// The resulting header text string. + private protected virtual string GetHeaderText() + { + var count = this.GetDirectChildCount(); + return $"{this.NodeType} Node{(count > 0 ? $" [+{count}]" : string.Empty)} ({(nint)this.Node:X})"; + } + + /// + /// Prints the node struct. + /// + private protected virtual void PrintNodeObject() + { + ShowStruct(this.Node); + ImGui.SameLine(); + ImGui.NewLine(); + } + + /// + /// Prints any field names for the node. + /// + private protected virtual void PrintFieldNames() => this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1)); + + /// + /// Prints all direct children of this node. + /// + private protected virtual void PrintChildNodes() + { + var prevNode = this.Node->ChildNode; + while (prevNode != null) + { + GetOrCreate(prevNode, this.AddonTree).Print(null); + prevNode = prevNode->PrevSiblingNode; + } + } + + /// + /// Prints any specific fields pertaining to the specific type of node. + /// + /// Whether the "Edit" box is currently checked. + private protected virtual void PrintFieldsForNodeType(bool isEditorOpen = false) + { + } + + private int GetDirectChildCount() + { + var count = 0; + if (this.Node->ChildNode != null) + { + count++; + + var prev = this.Node->ChildNode; + while (prev->PrevSiblingNode != null) + { + prev = prev->PrevSiblingNode; + count++; + } + } + + return count; + } + + private void PrintWithHighlights(uint? index) + { + if (!Scrolled) + { + ImGui.SetScrollHereY(); + Scrolled = true; + } + + var start = ImGui.GetCursorScreenPos() - new Vector2(5); + this.PrintTree(index, true); + var end = new Vector2(ImGui.GetMainViewport().WorkSize.X, ImGui.GetCursorScreenPos().Y + 5); + + ImGui.GetWindowDrawList().AddRectFilled(start, end, RgbaVector4ToUint(new Vector4(1, 1, 0.2f, 1) { W = Countdown / 200f })); + } + + private void PrintTree(uint? index, bool forceOpen = false) + { + var visible = this.Node->NodeFlags.HasFlag(Visible); + + var displayColor = !visible ? new Vector4(0.8f, 0.8f, 0.8f, 1) : + this.Node->Color.A == 0 ? new(0.015f, 0.575f, 0.355f, 1) : + new(0.1f, 1f, 0.1f, 1f); + + if (forceOpen || SearchResults.Contains((nint)this.Node)) + { + ImGui.SetNextItemOpen(true, ImGuiCond.Always); + } + + ImGui.PushStyleColor(ImGuiCol.Text, displayColor); + + var treePush = NestedTreePush($"{(index == null ? string.Empty : $"[{index}] ")}[#{this.Node->NodeId}]###{(nint)this.Node:X}nodeTree", displayColor, out var lineStart); + + if (ImGui.IsItemHovered()) + { + new NodeBounds(this.Node).Draw(visible ? new(0.1f, 1f, 0.1f, 1f) : new(1f, 0f, 0.2f, 1f)); + } + + ImGui.SameLine(); + this.WriteTreeHeading(); + + ImGui.PopStyleColor(); + + if (treePush) + { + try + { + PrintFieldValuePair("Node", $"{(nint)this.Node:X}"); + + ImGui.SameLine(); + this.PrintNodeObject(); + + PrintFieldValuePairs( + ("NodeID", $"{this.Node->NodeId}"), + ("Type", $"{this.Node->Type}")); + + this.DrawBasicControls(); + + if (this.editorOpen) + { + this.DrawNodeEditorTable(); + } + else + { + this.PrintResNodeFields(); + } + + this.PrintFieldsForNodeType(this.editorOpen); + PrintEvents(this.Node); + new TimelineTree(this.Node).Print(); + + this.PrintChildNodes(); + } + catch (Exception ex) + { + ImGui.TextDisabled($"Couldn't display node!\n\n{ex}"); + } + + NestedTreePop(lineStart, displayColor); + } + } + + private void DrawBasicControls() + { + ImGui.SameLine(); + var y = ImGui.GetCursorPosY(); + + ImGui.SetCursorPosY(y - 2); + var isVisible = this.Node->NodeFlags.HasFlag(Visible); + if (ImGuiComponents.IconButton("vis", isVisible ? Eye : EyeSlash, isVisible ? new Vector4(0.0f, 0.8f, 0.2f, 1f) : new(0.6f, 0.6f, 0.6f, 1))) + { + if (isVisible) + { + this.Node->NodeFlags &= ~Visible; + } + else + { + this.Node->NodeFlags |= Visible; + } + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Toggle Visibility"); + } + + ImGui.SameLine(); + ImGui.SetCursorPosY(y - 2); + ImGui.Checkbox($"Edit###editCheckBox{(nint)this.Node}", ref this.editorOpen); + + ImGui.SameLine(); + ImGui.SetCursorPosY(y - 2); + if (ImGuiComponents.IconButton($"###{(nint)this.Node}popoutButton", this.window?.IsOpen == true ? Times : ArrowUpRightFromSquare, null)) + { + this.TogglePopout(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Toggle Popout Window"); + } + } + + private void TogglePopout() + { + if (this.window != null) + { + this.window.IsOpen = !this.window.IsOpen; + } + else + { + this.window = new NodePopoutWindow(this, $"{this.AddonTree.AddonName}: {this.GetHeaderText()}###nodePopout{(nint)this.Node}"); + PopoutWindows.AddWindow(this.window); + } + } + + private void PrintResNodeFields() + { + PrintFieldValuePairs( + ("X", $"{this.Node->X}"), + ("Y", $"{this.Node->Y}"), + ("Width", $"{this.Node->Width}"), + ("Height", $"{this.Node->Height}"), + ("Priority", $"{this.Node->Priority}"), + ("Depth", $"{this.Node->Depth}"), + ("DrawFlags", $"0x{this.Node->DrawFlags:X}")); + + PrintFieldValuePairs( + ("ScaleX", $"{this.Node->ScaleX:F2}"), + ("ScaleY", $"{this.Node->ScaleY:F2}"), + ("OriginX", $"{this.Node->OriginX}"), + ("OriginY", $"{this.Node->OriginY}"), + ("Rotation", $"{this.Node->Rotation * (180d / Math.PI):F1}° / {this.Node->Rotation:F7}rad ")); + + var color = this.Node->Color; + var add = new Vector3(this.Node->AddRed, this.Node->AddGreen, this.Node->AddBlue); + var multiply = new Vector3(this.Node->MultiplyRed, this.Node->MultiplyGreen, this.Node->MultiplyBlue); + + PrintColor(RgbaUintToVector4(color.RGBA) with { W = 1 }, $"RGB: {SwapEndianness(color.RGBA) >> 8:X6}"); + ImGui.SameLine(); + PrintColor(color, $"Alpha: {color.A}"); + ImGui.SameLine(); + PrintColor((add / new Vector3(510f)) + new Vector3(0.5f), $"Add: {add.X} {add.Y} {add.Z}"); + ImGui.SameLine(); + PrintColor(multiply / 255f, $"Multiply: {multiply.X} {multiply.Y} {multiply.Z}"); + + PrintFieldValuePairs(("Flags", $"0x{(uint)this.Node->NodeFlags:X} ({this.Node->NodeFlags})")); + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs new file mode 100644 index 000000000..8572a5495 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs @@ -0,0 +1,126 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.ImGuiSeStringRenderer; +using Dalamud.Interface.Utility; +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.ColorHelpers; +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Utility.Util; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A tree for an that can be printed and browsed via ImGui. +/// +internal unsafe partial class TextNodeTree : ResNodeTree +{ + /// + /// Initializes a new instance of the class. + /// + /// The node to create a tree for. + /// The tree representing the containing addon. + internal TextNodeTree(AtkResNode* node, AddonTree addonTree) + : base(node, addonTree) + { + } + + private AtkTextNode* TxtNode => (AtkTextNode*)this.Node; + + private Utf8String NodeText => this.TxtNode->NodeText; + + /// + private protected override void PrintNodeObject() => ShowStruct(this.TxtNode); + + /// + private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) + { + if (isEditorOpen) + { + return; + } + + ImGui.TextColored(new(1), "Text:"); + ImGui.SameLine(); + +#pragma warning disable + try + { + var style = new SeStringDrawParams() + { + Color = TxtNode->TextColor.RGBA, + EdgeColor = TxtNode->EdgeColor.RGBA, + ForceEdgeColor = true, + EdgeStrength = 1f + }; + + ImGuiHelpers.SeStringWrapped(NodeText.AsSpan(), style); + } + catch + { + ImGui.Text(Marshal.PtrToStringAnsi(new(NodeText.StringPtr)) ?? ""); + } +#pragma warning restore + + PrintFieldValuePairs( + ("Font", $"{this.TxtNode->FontType}"), + ("Font Size", $"{this.TxtNode->FontSize}"), + ("Alignment", $"{this.TxtNode->AlignmentType}")); + + PrintColor(this.TxtNode->TextColor, $"Text Color: {SwapEndianness(this.TxtNode->TextColor.RGBA):X8}"); + ImGui.SameLine(); + PrintColor(this.TxtNode->EdgeColor, $"Edge Color: {SwapEndianness(this.TxtNode->EdgeColor.RGBA):X8}"); + + this.PrintPayloads(); + } + + private void PrintPayloads() + { + if (ImGui.TreeNode($"Text Payloads##{(nint)this.Node:X}")) + { + var utf8String = this.NodeText; + var seStringBytes = new byte[utf8String.BufUsed]; + for (var i = 0L; i < utf8String.BufUsed; i++) + { + seStringBytes[i] = utf8String.StringPtr[i]; + } + + var seString = SeString.Parse(seStringBytes); + for (var i = 0; i < seString.Payloads.Count; i++) + { + var payload = seString.Payloads[i]; + ImGui.Text($"[{i}]"); + ImGui.SameLine(); + switch (payload.Type) + { + case PayloadType.RawText when payload is TextPayload tp: + { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + ImGui.Text("Raw Text: '"); + ImGui.SameLine(); + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.6f, 0.6f, 0.6f, 1)); + ImGui.Text(tp.Text); + ImGui.PopStyleColor(); + ImGui.SameLine(); + ImGui.PopStyleVar(); + ImGui.Text("'"); + break; + } + + default: + { + ImGui.Text(payload.ToString()); + break; + } + } + } + + ImGui.TreePop(); + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs new file mode 100644 index 000000000..179197128 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +public partial struct TimelineTree +{ + /// + /// An interface for retrieving and printing the contents of a given column in an animation timeline table. + /// + public interface IKeyGroupColumn + { + /// Gets the column's name/heading. + public string Name { get; } + + /// Gets the number of cells in the column. + public int Count { get; } + + /// Gets the column's width. + public float Width { get; } + + /// + /// Calls this column's print function for a given row. + /// + /// The row number. + public void PrintValueAt(int i); + } + + /// + /// A column within an animation timeline table, representing a particular KeyGroup. + /// + /// The value type of the KeyGroup. + public struct KeyGroupColumn : IKeyGroupColumn + { + /// The values of each cell in the column. + public List Values; + + /// The method that should be used to format and print values in this KeyGroup. + public Action PrintFunc; + + /// + /// Initializes a new instance of the struct. + /// + /// The column's name/heading. + /// The method that should be used to format and print values in this KeyGroup. + internal KeyGroupColumn(string name, Action? printFunc = null) + { + this.Name = name; + this.PrintFunc = printFunc ?? PlainTextCell; + this.Values = []; + this.Width = 50; + } + + /// + public string Name { get; set; } + + /// + public float Width { get; init; } + + /// + public readonly int Count => this.Values.Count; + + /// + /// The default print function, if none is specified. + /// + /// The value to print. + public static void PlainTextCell(T value) => ImGui.Text($"{value}"); + + /// + /// Adds a value to this column. + /// + /// The value to add. + public readonly void Add(T val) => this.Values.Add(val); + + /// + public readonly void PrintValueAt(int i) + { + if (this.Values.Count > i) + { + this.PrintFunc.Invoke(this.Values[i]); + } + else + { + ImGui.TextDisabled("..."); + } + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs new file mode 100644 index 000000000..22fb61872 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs @@ -0,0 +1,381 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using Dalamud.Interface.Utility; +using FFXIVClientStructs.FFXIV.Client.Graphics; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.ColorHelpers; +using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; +using static Dalamud.Utility.Util; +using static FFXIVClientStructs.FFXIV.Component.GUI.NodeType; +using static ImGuiNET.ImGuiTableColumnFlags; +using static ImGuiNET.ImGuiTableFlags; + +// ReSharper disable SuggestBaseTypeForParameter +namespace Dalamud.Interface.Internal.UiDebug2.Browsing; + +/// +/// A struct allowing a node's animation timeline to be printed and browsed. +/// +public unsafe partial struct TimelineTree +{ + private AtkResNode* node; + + /// + /// Initializes a new instance of the struct. + /// + /// The node whose timelines are to be displayed. + internal TimelineTree(AtkResNode* node) + { + this.node = node; + } + + private AtkTimeline* NodeTimeline => this.node->Timeline; + + private AtkTimelineResource* Resource => this.NodeTimeline->Resource; + + private AtkTimelineAnimation* ActiveAnimation => this.NodeTimeline->ActiveAnimation; + + /// + /// Prints out this timeline tree within a window. + /// + internal void Print() + { + if (this.NodeTimeline == null) + { + return; + } + + var count = this.Resource->AnimationCount; + + if (count > 0) + { + if (NestedTreePush($"Timeline##{(nint)this.node:X}timeline", out _)) + { + PrintFieldValuePair("Timeline", $"{(nint)this.NodeTimeline:X}"); + + ImGui.SameLine(); + + ShowStruct(this.NodeTimeline); + + PrintFieldValuePairs( + ("Id", $"{this.NodeTimeline->Resource->Id}"), + ("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"), + ("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})")); + + PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}")); + ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List"); + + for (var a = 0; a < count; a++) + { + var animation = this.Resource->Animations[a]; + var isActive = this.ActiveAnimation != null && animation.Equals(*this.ActiveAnimation); + this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + (a * sizeof(AtkTimelineAnimation)))); + } + + ImGui.TreePop(); + } + } + } + + private static void GetFrameColumn(Span keyGroups, List columns, ushort endFrame) + { + for (var i = 0; i < keyGroups.Length; i++) + { + if (keyGroups[i].Type != AtkTimelineKeyGroupType.None) + { + var keyGroup = keyGroups[i]; + var idColumn = new KeyGroupColumn("Frame"); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + idColumn.Add(keyGroup.KeyFrames[f].FrameIdx); + } + + if (idColumn.Values.Last() != endFrame) + { + idColumn.Add(endFrame); + } + + columns.Add(idColumn); + break; + } + } + } + + private static void GetPosColumns(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var xColumn = new KeyGroupColumn("X"); + var yColumn = new KeyGroupColumn("Y"); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + var (x, y) = keyGroup.KeyFrames[f].Value.Float2; + + xColumn.Add(x); + yColumn.Add(y); + } + + columns.Add(xColumn); + columns.Add(yColumn); + } + + private static void GetRotationColumn(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var rotColumn = new KeyGroupColumn("Rotation", static r => ImGui.Text($"{r * (180d / Math.PI):F1}°")); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + rotColumn.Add(keyGroup.KeyFrames[f].Value.Float); + } + + columns.Add(rotColumn); + } + + private static void GetScaleColumns(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var scaleXColumn = new KeyGroupColumn("ScaleX"); + var scaleYColumn = new KeyGroupColumn("ScaleY"); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + var (scaleX, scaleY) = keyGroup.KeyFrames[f].Value.Float2; + + scaleXColumn.Add(scaleX); + scaleYColumn.Add(scaleY); + } + + columns.Add(scaleXColumn); + columns.Add(scaleYColumn); + } + + private static void GetAlphaColumn(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var alphaColumn = new KeyGroupColumn("Alpha", PrintAlpha); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + alphaColumn.Add(keyGroup.KeyFrames[f].Value.Byte); + } + + columns.Add(alphaColumn); + } + + private static void GetTintColumns(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var addRGBColumn = new KeyGroupColumn("Add", PrintAddCell) { Width = 110 }; + var multiplyRGBColumn = new KeyGroupColumn("Multiply", PrintMultiplyCell) { Width = 110 }; + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + var nodeTint = keyGroup.KeyFrames[f].Value.NodeTint; + + addRGBColumn.Add(new Vector3(nodeTint.AddR, nodeTint.AddG, nodeTint.AddB)); + multiplyRGBColumn.Add(nodeTint.MultiplyRGB); + } + + columns.Add(addRGBColumn); + columns.Add(multiplyRGBColumn); + } + + private static void GetTextColorColumn(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var textColorColumn = new KeyGroupColumn("Text Color", PrintRGB); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + textColorColumn.Add(keyGroup.KeyFrames[f].Value.RGB); + } + + columns.Add(textColorColumn); + } + + private static void GetPartIdColumn(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var partColumn = new KeyGroupColumn("Part ID"); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + partColumn.Add(keyGroup.KeyFrames[f].Value.UShort); + } + + columns.Add(partColumn); + } + + private static void GetEdgeColumn(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var edgeColorColumn = new KeyGroupColumn("Edge Color", PrintRGB); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + edgeColorColumn.Add(keyGroup.KeyFrames[f].Value.RGB); + } + + columns.Add(edgeColorColumn); + } + + private static void GetLabelColumn(AtkTimelineKeyGroup keyGroup, List columns) + { + if (keyGroup.KeyFrameCount <= 0) + { + return; + } + + var labelColumn = new KeyGroupColumn("Label"); + + for (var f = 0; f < keyGroup.KeyFrameCount; f++) + { + labelColumn.Add(keyGroup.KeyFrames[f].Value.Label.LabelId); + } + + columns.Add(labelColumn); + } + + private static void PrintRGB(ByteColor c) => PrintColor(c, $"0x{SwapEndianness(c.RGBA):X8}"); + + private static void PrintAlpha(byte b) => PrintColor(new Vector4(b / 255f), PadEvenly($"{b}", 25)); + + private static void PrintAddCell(Vector3 add) + { + var fmt = PadEvenly($"{PadEvenly($"{add.X}", 30)}{PadEvenly($"{add.Y}", 30)}{PadEvenly($"{add.Z}", 30)}", 100); + PrintColor(new Vector4((add / new Vector3(510f)) + new Vector3(0.5f), 1), fmt); + } + + private static void PrintMultiplyCell(ByteColor byteColor) + { + var multiply = new Vector3(byteColor.R, byteColor.G, byteColor.B); + var fmt = PadEvenly($"{PadEvenly($"{multiply.X}", 25)}{PadEvenly($"{multiply.Y}", 25)}{PadEvenly($"{multiply.Z}", 25)}", 100); + PrintColor(multiply / 255f, fmt); + } + + private static string PadEvenly(string str, float size) + { + while (ImGui.CalcTextSize(str).X < size * ImGuiHelpers.GlobalScale) + { + str = $" {str} "; + } + + return str; + } + + private void PrintAnimation(AtkTimelineAnimation animation, int a, bool isActive, nint address) + { + var columns = this.BuildColumns(animation); + + ImGui.PushStyleColor(ImGuiCol.Text, isActive ? new Vector4(1, 0.65F, 0.4F, 1) : new(1)); + var treePush = ImGui.TreeNode($"[#{a}] [Frames {animation.StartFrameIdx}-{animation.EndFrameIdx}] {(isActive ? " (Active)" : string.Empty)}###{(nint)this.node}animTree{a}"); + ImGui.PopStyleColor(); + + if (treePush) + { + PrintFieldValuePair("Animation", $"{address:X}"); + + ShowStruct((AtkTimelineAnimation*)address); + + if (columns.Count > 0) + { + ImGui.BeginTable($"##{(nint)this.node}animTable{a}", columns.Count, Borders | SizingFixedFit | RowBg | NoHostExtendX); + + foreach (var c in columns) + { + ImGui.TableSetupColumn(c.Name, WidthFixed, c.Width); + } + + ImGui.TableHeadersRow(); + + var rows = columns.Select(static c => c.Count).Max(); + + for (var i = 0; i < rows; i++) + { + ImGui.TableNextRow(); + + foreach (var c in columns) + { + ImGui.TableNextColumn(); + c.PrintValueAt(i); + } + } + + ImGui.EndTable(); + } + + ImGui.TreePop(); + } + } + + private List BuildColumns(AtkTimelineAnimation animation) + { + var keyGroups = animation.KeyGroups; + var columns = new List(); + + GetFrameColumn(keyGroups, columns, animation.EndFrameIdx); + + GetPosColumns(keyGroups[0], columns); + + GetRotationColumn(keyGroups[1], columns); + + GetScaleColumns(keyGroups[2], columns); + + GetAlphaColumn(keyGroups[3], columns); + + GetTintColumns(keyGroups[4], columns); + + if (this.node->Type is Image or NineGrid or ClippingMask) + { + GetPartIdColumn(keyGroups[5], columns); + } + else if (this.node->Type == Text) + { + GetTextColorColumn(keyGroups[5], columns); + } + + GetEdgeColumn(keyGroups[6], columns); + + GetLabelColumn(keyGroups[7], columns); + + return columns; + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs new file mode 100644 index 000000000..c6cb99037 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs @@ -0,0 +1,475 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; + +using Dalamud.Interface.Components; +using Dalamud.Interface.Internal.UiDebug2.Browsing; +using Dalamud.Interface.Internal.UiDebug2.Utility; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static System.Globalization.NumberFormatInfo; + +using static Dalamud.Interface.FontAwesomeIcon; +using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; +using static Dalamud.Interface.UiBuilder; +using static Dalamud.Interface.Utility.ImGuiHelpers; +using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags; +using static ImGuiNET.ImGuiCol; +using static ImGuiNET.ImGuiWindowFlags; + +#pragma warning disable CS0659 + +namespace Dalamud.Interface.Internal.UiDebug2; + +/// +/// A tool that enables the user to select UI elements within the inspector by mousing over them onscreen. +/// +internal unsafe class ElementSelector : IDisposable +{ + private const int UnitListCount = 18; + + private readonly UiDebug2 uiDebug2; + + private string addressSearchInput = string.Empty; + + private int index; + + /// + /// Initializes a new instance of the class. + /// + /// The instance of this Element Selector belongs to. + internal ElementSelector(UiDebug2 uiDebug2) + { + this.uiDebug2 = uiDebug2; + } + + /// + /// Gets or sets the results retrieved by the Element Selector. + /// + internal static nint[] SearchResults { get; set; } = []; + + /// + /// Gets or sets a value governing the highlighting of nodes when found via search. + /// + internal static float Countdown { get; set; } + + /// + /// Gets or sets a value indicating whether the window has scrolled down to the position of the search result. + /// + internal static bool Scrolled { get; set; } + + /// + /// Gets or sets a value indicating whether the mouseover UI is currently active. + /// + internal bool Active { get; set; } + + /// + public void Dispose() + { + this.Active = false; + } + + /// + /// Draws the Element Selector and Address Search interface at the bottom of the sidebar. + /// + internal void DrawInterface() + { + ImGui.BeginChild("###sidebar_elementSelector", new(250, 0), true); + + ImGui.PushFont(IconFont); + ImGui.PushStyleColor(Text, this.Active ? new Vector4(1, 1, 0.2f, 1) : new(1)); + if (ImGui.Button($"{(char)ObjectUngroup}")) + { + this.Active = !this.Active; + } + + if (Countdown > 0) + { + Countdown -= 1; + if (Countdown < 0) + { + Countdown = 0; + } + } + + ImGui.PopStyleColor(); + ImGui.PopFont(); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Element Selector"); + } + + ImGui.SameLine(); + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 32); + ImGui.InputTextWithHint("###addressSearchInput", "Address Search", ref this.addressSearchInput, 18, ImGuiInputTextFlags.AutoSelectAll); + ImGui.SameLine(); + + if (ImGuiComponents.IconButton("###elemSelectorAddrSearch", Search) && nint.TryParse(this.addressSearchInput, NumberStyles.HexNumber | NumberStyles.AllowHexSpecifier, InvariantInfo, out var address)) + { + this.PerformSearch(address); + } + + ImGui.EndChild(); + } + + /// + /// Draws the Element Selector's search output within the main window. + /// + internal void DrawSelectorOutput() + { + ImGui.GetIO().WantCaptureKeyboard = true; + ImGui.GetIO().WantCaptureMouse = true; + ImGui.GetIO().WantTextInput = true; + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) + { + this.Active = false; + return; + } + + ImGui.Text("ELEMENT SELECTOR"); + ImGui.TextDisabled("Use the mouse to hover and identify UI elements, then click to jump to them in the inspector"); + ImGui.TextDisabled("Use the scrollwheel to choose between overlapping elements"); + ImGui.TextDisabled("Press ESCAPE to cancel"); + ImGui.Spacing(); + + var mousePos = ImGui.GetMousePos() - MainViewport.Pos; + var addonResults = GetAtkUnitBaseAtPosition(mousePos); + + ImGui.PushStyleColor(WindowBg, new Vector4(0.5f)); + ImGui.BeginChild("noClick", new(800, 2000), false, NoInputs | NoBackground | NoScrollWithMouse); + ImGui.BeginGroup(); + + Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}"); + ImGui.Spacing(); + ImGui.Text("RESULTS:\n"); + + var i = 0; + foreach (var a in addonResults) + { + var name = a.Addon->NameString; + ImGui.Text($"[Addon] {name}"); + ImGui.Indent(15); + foreach (var n in a.Nodes) + { + var nSelected = i++ == this.index; + + PrintNodeHeaderOnly(n.Node, nSelected, a.Addon); + + if (nSelected && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + this.Active = false; + + this.uiDebug2.SelectedAddonName = a.Addon->NameString; + + var ptrList = new List { (nint)n.Node }; + + var nextNode = n.Node->ParentNode; + while (nextNode != null) + { + ptrList.Add((nint)nextNode); + nextNode = nextNode->ParentNode; + } + + SearchResults = [.. ptrList]; + Countdown = 100; + Scrolled = false; + } + + if (nSelected) + { + n.NodeBounds.DrawFilled(new(1, 1, 0.2f, 1)); + } + } + + ImGui.Indent(-15); + } + + if (i != 0) + { + this.index -= (int)ImGui.GetIO().MouseWheel; + while (this.index < 0) + { + this.index += i; + } + + while (this.index >= i) + { + this.index -= i; + } + } + + ImGui.EndGroup(); + ImGui.EndChild(); + ImGui.PopStyleColor(); + } + + private static List GetAtkUnitBaseAtPosition(Vector2 position) + { + var addonResults = new List(); + var unitListBaseAddr = GetUnitListBaseAddr(); + if (unitListBaseAddr == null) + { + return addonResults; + } + + foreach (var unit in UnitListOptions) + { + var unitManager = &unitListBaseAddr[unit.Index]; + + var safeCount = Math.Min(unitManager->Count, unitManager->Entries.Length); + + for (var i = 0; i < safeCount; i++) + { + var addon = unitManager->Entries[i].Value; + + if (addon == null || addon->RootNode == null) + { + continue; + } + + if (!addon->IsVisible || !addon->RootNode->NodeFlags.HasFlag(Visible)) + { + continue; + } + + var addonResult = new AddonResult(addon, []); + + if (addonResults.Contains(addonResult)) + { + continue; + } + + if (addon->X > position.X || addon->Y > position.Y) + { + continue; + } + + if (addon->X + addon->RootNode->Width < position.X) + { + continue; + } + + if (addon->Y + addon->RootNode->Height < position.Y) + { + continue; + } + + addonResult.Nodes.AddRange(GetNodeAtPosition(&addon->UldManager, position, true)); + addonResults.Add(addonResult); + } + } + + return [.. addonResults.OrderBy(static w => w.Area)]; + } + + private static List GetNodeAtPosition(AtkUldManager* uldManager, Vector2 position, bool reverse) + { + var nodeResults = new List(); + for (var i = 0; i < uldManager->NodeListCount; i++) + { + var node = uldManager->NodeList[i]; + + var bounds = new NodeBounds(node); + + if (!bounds.ContainsPoint(position)) + { + continue; + } + + if ((int)node->Type >= 1000) + { + var compNode = (AtkComponentNode*)node; + nodeResults.AddRange(GetNodeAtPosition(&compNode->Component->UldManager, position, false)); + } + + nodeResults.Add(new() { NodeBounds = bounds, Node = node }); + } + + if (reverse) + { + nodeResults.Reverse(); + } + + return nodeResults; + } + + private static bool FindByAddress(AtkUnitBase* atkUnitBase, nint address) + { + if (atkUnitBase->RootNode == null) + { + return false; + } + + if (!FindByAddress(atkUnitBase->RootNode, address, out var path)) + { + return false; + } + + Scrolled = false; + SearchResults = path?.ToArray() ?? []; + Countdown = 100; + return true; + } + + private static bool FindByAddress(AtkResNode* node, nint address, out List? path) + { + if (node == null) + { + path = null; + return false; + } + + if ((nint)node == address) + { + path = [(nint)node]; + return true; + } + + if ((int)node->Type >= 1000) + { + var cNode = (AtkComponentNode*)node; + + if (cNode->Component != null) + { + if ((nint)cNode->Component == address) + { + path = [(nint)node]; + return true; + } + + if (FindByAddress(cNode->Component->UldManager.RootNode, address, out path) && path != null) + { + path.Add((nint)node); + return true; + } + } + } + + if (FindByAddress(node->ChildNode, address, out path) && path != null) + { + path.Add((nint)node); + return true; + } + + if (FindByAddress(node->PrevSiblingNode, address, out path) && path != null) + { + return true; + } + + path = null; + return false; + } + + private static void PrintNodeHeaderOnly(AtkResNode* node, bool selected, AtkUnitBase* addon) + { + if (addon == null) + { + return; + } + + if (node == null) + { + return; + } + + var tree = AddonTree.GetOrCreate(addon->NameString); + if (tree == null) + { + return; + } + + ImGui.PushStyleColor(Text, selected ? new Vector4(1, 1, 0.2f, 1) : new(0.6f, 0.6f, 0.6f, 1)); + ResNodeTree.GetOrCreate(node, tree).WriteTreeHeading(); + ImGui.PopStyleColor(); + } + + private void PerformSearch(nint address) + { + var unitListBaseAddr = GetUnitListBaseAddr(); + if (unitListBaseAddr == null) + { + return; + } + + for (var i = 0; i < UnitListCount; i++) + { + var unitManager = &unitListBaseAddr[i]; + var safeCount = Math.Min(unitManager->Count, unitManager->Entries.Length); + + for (var j = 0; j < safeCount; j++) + { + var addon = unitManager->Entries[j].Value; + if ((nint)addon == address || FindByAddress(addon, address)) + { + this.uiDebug2.SelectedAddonName = addon->NameString; + return; + } + } + } + } + + /// + /// An found by the Element Selector. + /// + internal struct AddonResult + { + /// The addon itself. + internal AtkUnitBase* Addon; + + /// A list of nodes discovered within this addon by the Element Selector. + internal List Nodes; + + /// The calculated area of the addon's root node. + internal float Area; + + /// + /// Initializes a new instance of the struct. + /// + /// The addon found. + /// A list for documenting nodes found within the addon. + public AddonResult(AtkUnitBase* addon, List nodes) + { + this.Addon = addon; + this.Nodes = nodes; + var rootNode = addon->RootNode; + this.Area = rootNode != null ? rootNode->Width * rootNode->Height * rootNode->ScaleY * rootNode->ScaleX : 0; + } + + /// + public override readonly bool Equals(object? obj) + { + if (obj is not AddonResult ar) + { + return false; + } + + return (nint)this.Addon == (nint)ar.Addon; + } + } + + /// + /// An found by the Element Selector. + /// + internal struct NodeResult + { + /// The node itself. + internal AtkResNode* Node; + + /// A struct representing the perimeter of the node. + internal NodeBounds NodeBounds; + + /// + public override readonly bool Equals(object? obj) + { + if (obj is not NodeResult nr) + { + return false; + } + + return nr.Node == this.Node; + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs new file mode 100644 index 000000000..3aef3b6a4 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs @@ -0,0 +1,50 @@ +using System.Numerics; + +using Dalamud.Interface.Internal.UiDebug2.Browsing; +using Dalamud.Interface.Windowing; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.UiDebug2; + +/// +/// A popout window for an . +/// +internal class AddonPopoutWindow : Window, IDisposable +{ + private readonly AddonTree addonTree; + + /// + /// Initializes a new instance of the class. + /// + /// The AddonTree this popout will show. + /// the window's name. + public AddonPopoutWindow(AddonTree tree, string name) + : base(name) + { + this.addonTree = tree; + this.PositionCondition = ImGuiCond.Once; + + var pos = ImGui.GetMousePos() + new Vector2(50, -50); + var workSize = ImGui.GetMainViewport().WorkSize; + var pos2 = new Vector2(Math.Min(workSize.X - 750, pos.X), Math.Min(workSize.Y - 250, pos.Y)); + + this.Position = pos2; + this.SizeCondition = ImGuiCond.Once; + this.Size = new(700, 200); + this.IsOpen = true; + this.SizeConstraints = new() { MinimumSize = new(100, 100) }; + } + + /// + public override void Draw() + { + ImGui.BeginChild($"{this.WindowName}child", new(-1, -1), true); + this.addonTree.Draw(); + ImGui.EndChild(); + } + + /// + public void Dispose() + { + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs new file mode 100644 index 000000000..b293b734e --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs @@ -0,0 +1,69 @@ +using System.Numerics; + +using Dalamud.Interface.Internal.UiDebug2.Browsing; +using Dalamud.Interface.Windowing; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static Dalamud.Interface.Internal.UiDebug2.UiDebug2; + +namespace Dalamud.Interface.Internal.UiDebug2; + +/// +/// A popout window for a . +/// +internal unsafe class NodePopoutWindow : Window, IDisposable +{ + private readonly ResNodeTree resNodeTree; + + private bool firstDraw = true; + + /// + /// Initializes a new instance of the class. + /// + /// The node tree this window will show. + /// The name of the window. + public NodePopoutWindow(ResNodeTree nodeTree, string windowName) + : base(windowName) + { + this.resNodeTree = nodeTree; + + var pos = ImGui.GetMousePos() + new Vector2(50, -50); + var workSize = ImGui.GetMainViewport().WorkSize; + var pos2 = new Vector2(Math.Min(workSize.X - 750, pos.X), Math.Min(workSize.Y - 250, pos.Y)); + + this.Position = pos2; + this.IsOpen = true; + this.PositionCondition = ImGuiCond.Once; + this.SizeCondition = ImGuiCond.Once; + this.Size = new(700, 200); + this.SizeConstraints = new() { MinimumSize = new(100, 100) }; + } + + private AddonTree AddonTree => this.resNodeTree.AddonTree; + + private AtkResNode* Node => this.resNodeTree.Node; + + /// + public override void Draw() + { + if (this.Node != null && this.AddonTree.ContainsNode(this.Node)) + { + ImGui.BeginChild($"{(nint)this.Node:X}popoutChild", new(-1, -1), true); + ResNodeTree.GetOrCreate(this.Node, this.AddonTree).Print(null, this.firstDraw); + ImGui.EndChild(); + this.firstDraw = false; + } + else + { + Log.Warning($"Popout closed ({this.WindowName}); Node or Addon no longer exists."); + this.IsOpen = false; + this.Dispose(); + } + } + + /// + public void Dispose() + { + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs new file mode 100644 index 000000000..d2510d16b --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs @@ -0,0 +1,214 @@ +using System.Collections.Generic; +using System.Numerics; + +using Dalamud.Interface.Components; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static System.StringComparison; +using static Dalamud.Interface.FontAwesomeIcon; + +namespace Dalamud.Interface.Internal.UiDebug2; + +/// +internal unsafe partial class UiDebug2 +{ + /// + /// All unit lists to check for addons. + /// + internal static readonly List UnitListOptions = + [ + new(13, "Loaded"), + new(14, "Focused"), + new(0, "Depth Layer 1"), + new(1, "Depth Layer 2"), + new(2, "Depth Layer 3"), + new(3, "Depth Layer 4"), + new(4, "Depth Layer 5"), + new(5, "Depth Layer 6"), + new(6, "Depth Layer 7"), + new(7, "Depth Layer 8"), + new(8, "Depth Layer 9"), + new(9, "Depth Layer 10"), + new(10, "Depth Layer 11"), + new(11, "Depth Layer 12"), + new(12, "Depth Layer 13"), + new(15, "Units 16"), + new(16, "Units 17"), + new(17, "Units 18"), + ]; + + private string addonNameSearch = string.Empty; + + private bool visFilter; + + /// + /// Gets the base address for all unit lists. + /// + /// The address, if found. + internal static AtkUnitList* GetUnitListBaseAddr() => &((UIModule*)GameGui.GetUIModule())->GetRaptureAtkModule()->RaptureAtkUnitManager.AtkUnitManager.DepthLayerOneList; + + private void DrawSidebar() + { + ImGui.BeginGroup(); + + this.DrawNameSearch(); + this.DrawAddonSelectionList(); + this.elementSelector.DrawInterface(); + + ImGui.EndGroup(); + } + + private void DrawNameSearch() + { + ImGui.BeginChild("###sidebar_nameSearch", new(250, 40), true); + var atkUnitBaseSearch = this.addonNameSearch; + + Vector4? defaultColor = this.visFilter ? new(0.0f, 0.8f, 0.2f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1); + if (ImGuiComponents.IconButton("filter", LowVision, defaultColor)) + { + this.visFilter = !this.visFilter; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Filter by visibility"); + } + + ImGui.SameLine(); + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint("###atkUnitBaseSearch", "Filter by name", ref atkUnitBaseSearch, 0x20)) + { + this.addonNameSearch = atkUnitBaseSearch; + } + + ImGui.EndChild(); + } + + private void DrawAddonSelectionList() + { + ImGui.BeginChild("###sideBar_addonList", new(250, -44), true, ImGuiWindowFlags.AlwaysVerticalScrollbar); + + var unitListBaseAddr = GetUnitListBaseAddr(); + + foreach (var unit in UnitListOptions) + { + this.DrawUnitListOption(unitListBaseAddr, unit); + } + + ImGui.EndChild(); + } + + private void DrawUnitListOption(AtkUnitList* unitListBaseAddr, UnitListOption unit) + { + var atkUnitList = &unitListBaseAddr[unit.Index]; + var safeLength = Math.Min(atkUnitList->Count, atkUnitList->Entries.Length); + + var options = new List(); + var totalCount = 0; + var matchCount = 0; + var anyVisible = false; + + var usingFilter = this.visFilter || !string.IsNullOrEmpty(this.addonNameSearch); + + for (var i = 0; i < safeLength; i++) + { + var addon = atkUnitList->Entries[i].Value; + + if (addon == null) + { + continue; + } + + totalCount++; + + if (this.visFilter && !addon->IsVisible) + { + continue; + } + + if (!string.IsNullOrEmpty(this.addonNameSearch) && !addon->NameString.Contains(this.addonNameSearch, InvariantCultureIgnoreCase)) + { + continue; + } + + matchCount++; + anyVisible |= addon->IsVisible; + options.Add(new AddonOption(addon->NameString, addon->IsVisible)); + } + + if (matchCount == 0) + { + return; + } + + ImGui.PushStyleColor(ImGuiCol.Text, anyVisible ? new Vector4(1) : new Vector4(0.6f, 0.6f, 0.6f, 1)); + var countStr = $"{(usingFilter ? $"{matchCount}/" : string.Empty)}{totalCount}"; + var treePush = ImGui.TreeNodeEx($"{unit.Name} [{countStr}]###unitListTree{unit.Index}"); + ImGui.PopStyleColor(); + + if (treePush) + { + foreach (var option in options) + { + ImGui.PushStyleColor(ImGuiCol.Text, option.Visible ? new Vector4(0.1f, 1f, 0.1f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1)); + if (ImGui.Selectable($"{option.Name}##select{option.Name}", this.SelectedAddonName == option.Name)) + { + this.SelectedAddonName = option.Name; + } + + ImGui.PopStyleColor(); + } + + ImGui.TreePop(); + } + } + + /// + /// A struct representing a unit list that can be browed in the sidebar. + /// + internal struct UnitListOption + { + /// The index of the unit list. + internal uint Index; + + /// The name of the unit list. + internal string Name; + + /// + /// Initializes a new instance of the struct. + /// + /// The index of the unit list. + /// The name of the unit list. + internal UnitListOption(uint i, string name) + { + this.Index = i; + this.Name = name; + } + } + + /// + /// A struct representing an addon that can be selected in the sidebar. + /// + internal struct AddonOption + { + /// The name of the addon. + internal string Name; + + /// Whether the addon is visible. + internal bool Visible; + + /// + /// Initializes a new instance of the struct. + /// + /// The name of the addon. + /// Whether the addon is visible. + internal AddonOption(string name, bool visible) + { + this.Name = name; + this.Visible = visible; + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs new file mode 100644 index 000000000..396a84ac9 --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; + +using Dalamud.Game.Gui; +using Dalamud.Interface.Internal.UiDebug2.Browsing; +using Dalamud.Interface.Windowing; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +using ImGuiNET; + +using static ImGuiNET.ImGuiWindowFlags; + +namespace Dalamud.Interface.Internal.UiDebug2; + +// Original version by aers https://github.com/aers/FFXIVUIDebug +// Also incorporates features from Caraxi's fork https://github.com/Caraxi/SimpleTweaksPlugin/blob/main/Debugging/UIDebug.cs + +/// +/// A tool for browsing the contents and structure of UI elements. +/// +internal partial class UiDebug2 : IDisposable +{ + private readonly ElementSelector elementSelector; + + /// + /// Initializes a new instance of the class. + /// + internal UiDebug2() + { + this.elementSelector = new(this); + + GameGui = Service.Get(); + + Log = new ModuleLog("UiDebug2"); + } + + /// + internal static ModuleLog Log { get; set; } = null!; + + /// + internal static IGameGui GameGui { get; set; } = null!; + + /// + /// Gets a collection of instances, each representing an . + /// + internal static Dictionary AddonTrees { get; } = []; + + /// + /// Gets or sets a window system to handle any popout windows for addons or nodes. + /// + internal static WindowSystem PopoutWindows { get; set; } = new("UiDebugPopouts"); + + /// + /// Gets or sets the name of the currently-selected . + /// + internal string? SelectedAddonName { get; set; } + + /// + /// Clears all windows and s. + /// + public void Dispose() + { + foreach (var a in AddonTrees) + { + a.Value.Dispose(); + } + + AddonTrees.Clear(); + PopoutWindows.RemoveAllWindows(); + this.elementSelector.Dispose(); + } + + /// + /// Draws the UiDebug tool's interface and contents. + /// + internal void Draw() + { + PopoutWindows.Draw(); + this.DrawSidebar(); + this.DrawMainPanel(); + } + + private void DrawMainPanel() + { + ImGui.SameLine(); + ImGui.BeginChild("###uiDebugMainPanel", new(-1, -1), true, HorizontalScrollbar); + + if (this.elementSelector.Active) + { + this.elementSelector.DrawSelectorOutput(); + } + else + { + if (this.SelectedAddonName != null) + { + var addonTree = AddonTree.GetOrCreate(this.SelectedAddonName); + + if (addonTree == null) + { + this.SelectedAddonName = null; + return; + } + + addonTree.Draw(); + } + } + + ImGui.EndChild(); + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs new file mode 100644 index 000000000..37b2f92cd --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs @@ -0,0 +1,204 @@ +using System.Collections.Generic; +using System.Numerics; + +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; +using FFXIVClientStructs.FFXIV.Client.Graphics; +using ImGuiNET; + +using static Dalamud.Interface.ColorHelpers; +using static ImGuiNET.ImGuiCol; + +namespace Dalamud.Interface.Internal.UiDebug2.Utility; + +/// +/// Miscellaneous ImGui tools used by . +/// +internal static class Gui +{ + /// + /// Begins a tree node that also displays a colored line to the left while open. + /// + /// The label of the tree. + /// The color of the heading text. + /// A value representing where to begin drawing the left-side line. + /// Whether this tree should default to being open. + /// true if the tree is open. + internal static bool NestedTreePush(string label, Vector4 color, out Vector2 lineStart, bool defOpen = false) + { + ImGui.PushStyleColor(Text, color); + var result = NestedTreePush(label, out lineStart, defOpen); + ImGui.PopStyleColor(); + return result; + } + + /// + internal static bool NestedTreePush(string label, out Vector2 lineStart, bool defOpen = false) + { + var imGuiTreeNodeFlags = ImGuiTreeNodeFlags.SpanFullWidth; + + if (defOpen) + { + imGuiTreeNodeFlags |= ImGuiTreeNodeFlags.DefaultOpen; + } + + var treeNodeEx = ImGui.TreeNodeEx(label, imGuiTreeNodeFlags); + lineStart = ImGui.GetCursorScreenPos() + new Vector2(-10, 2); + return treeNodeEx; + } + + /// + /// Completes a NestedTree. + /// + /// The starting position calculated when the tree was pushed. + /// The color of the left-side line. + internal static void NestedTreePop(Vector2 lineStart, Vector4? color = null) + { + var lineEnd = lineStart with { Y = ImGui.GetCursorScreenPos().Y - 7 }; + + if (lineStart.Y < lineEnd.Y) + { + ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, RgbaVector4ToUint(color ?? new(1)), 1); + } + + ImGui.TreePop(); + } + + /// + /// A radio-button-esque input that uses Fontawesome icon buttons. + /// + /// The type of value being set. + /// The label for the inputs. + /// The value being set. + /// A list of all options to create buttons for. + /// A list of the icons to use for each option. + /// true if a button is clicked. + internal static unsafe bool IconSelectInput(string label, ref T val, List options, List icons) + { + var ret = false; + for (var i = 0; i < options.Count; i++) + { + var option = options[i]; + var icon = icons[i]; + + if (i > 0) + { + ImGui.SameLine(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() - ((ImGui.GetFontSize() / -6f) + 7f)); + } + + var color = *ImGui.GetStyleColorVec4(val is not null && val.Equals(option) ? ButtonActive : Button); + + if (ImGuiComponents.IconButton($"{label}{option}{i}", icon, color)) + { + val = option; + ret = true; + } + } + + return ret; + } + + /// + /// Prints field name and its value. + /// + /// The name of the field. + /// The value of the field. + /// Whether to enable click-to-copy. + internal static void PrintFieldValuePair(string fieldName, string value, bool copy = true) + { + ImGui.Text($"{fieldName}:"); + ImGui.SameLine(); + if (copy) + { + ClickToCopyText(value); + } + else + { + ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), value); + } + } + + /// + /// Prints a set of fields and their values. + /// + /// Tuples of fieldnames and values to display. + internal static void PrintFieldValuePairs(params (string FieldName, string Value)[] pairs) + { + for (var i = 0; i < pairs.Length; i++) + { + if (i != 0) + { + ImGui.SameLine(); + } + + PrintFieldValuePair(pairs[i].FieldName, pairs[i].Value, false); + } + } + + /// + internal static void PrintColor(ByteColor color, string fmt) => PrintColor(RgbaUintToVector4(color.RGBA), fmt); + + /// + internal static void PrintColor(Vector3 color, string fmt) => PrintColor(new Vector4(color, 1), fmt); + + /// + /// Prints a text string representing a color, with a backdrop in that color. + /// + /// The color value. + /// The text string to print. + /// Colors the text itself either white or black, depending on the luminosity of the background color. + internal static void PrintColor(Vector4 color, string fmt) + { + static double Luminosity(Vector4 vector4) => + Math.Pow( + (Math.Pow(vector4.X, 2) * 0.299f) + + (Math.Pow(vector4.Y, 2) * 0.587f) + + (Math.Pow(vector4.Z, 2) * 0.114f), + 0.5f) * vector4.W; + + ImGui.PushStyleColor(Text, Luminosity(color) < 0.5f ? new Vector4(1) : new(0, 0, 0, 1)); + ImGui.PushStyleColor(Button, color); + ImGui.PushStyleColor(ButtonActive, color); + ImGui.PushStyleColor(ButtonHovered, color); + ImGui.SmallButton(fmt); + ImGui.PopStyleColor(4); + } + + /// + internal static void ClickToCopyText(string text, string? textCopy = null) + { + ImGui.PushStyleColor(Text, new Vector4(0.6f, 0.6f, 0.6f, 1)); + ImGuiHelpers.ClickToCopyText(text, textCopy); + ImGui.PopStyleColor(); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"{textCopy ?? text}"); + } + } + + /// + /// Draws a tooltip that changes based on the cursor's x-position within the hovered item. + /// + /// The text for each section. + /// true if the item is hovered. + internal static bool SplitTooltip(params string[] tooltips) + { + if (!ImGui.IsItemHovered()) + { + return false; + } + + var mouseX = ImGui.GetMousePos().X; + var minX = ImGui.GetItemRectMin().X; + var maxX = ImGui.GetItemRectMax().X; + var prog = (mouseX - minX) / (maxX - minX); + + var index = (int)Math.Floor(prog * tooltips.Length); + + ImGui.SetTooltip(tooltips[index]); + + return true; + } +} diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs new file mode 100644 index 000000000..82bf8f96c --- /dev/null +++ b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using Dalamud.Interface.Utility; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; + +using static System.Math; + +namespace Dalamud.Interface.Internal.UiDebug2.Utility; + +/// +/// A struct representing the perimeter of an , accounting for all transformations. +/// +public unsafe struct NodeBounds +{ + /// + /// Initializes a new instance of the struct. + /// + /// The node to calculate the bounds of. + internal NodeBounds(AtkResNode* node) + { + if (node == null) + { + return; + } + + var w = node->Width; + var h = node->Height; + this.Points = w == 0 && h == 0 ? + new() { new(0) } : + new() { new(0), new(w, 0), new(w, h), new(0, h) }; + + this.TransformPoints(node); + } + + /// + /// Initializes a new instance of the struct, containing only a single given point. + /// + /// The point onscreen. + /// The node used to calculate transformations. + internal NodeBounds(Vector2 point, AtkResNode* node) + { + this.Points = [point]; + this.TransformPoints(node); + } + + private List Points { get; set; } = []; + + /// + /// Draws the bounds onscreen. + /// + /// The color of line to use. + /// The thickness of line to use. + /// If there is only a single point to draw, it will be indicated with a circle and dot. + internal readonly void Draw(Vector4 col, int thickness = 1) + { + if (this.Points == null || this.Points.Count == 0) + { + return; + } + + if (this.Points.Count == 1) + { + ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, ColorHelpers.RgbaVector4ToUint(col with { W = col.W / 2 }), 12, thickness); + ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], thickness, ColorHelpers.RgbaVector4ToUint(col), 12, thickness + 1); + } + else + { + var path = new ImVectorWrapper(this.Points.Count); + foreach (var p in this.Points) + { + path.Add(p); + } + + ImGui.GetBackgroundDrawList() + .AddPolyline(ref path[0], path.Length, ColorHelpers.RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness); + + path.Dispose(); + } + } + + /// + /// Draws the bounds onscreen, filled in. + /// + /// The fill and border color. + /// The border thickness. + internal readonly void DrawFilled(Vector4 col, int thickness = 1) + { + if (this.Points == null || this.Points.Count == 0) + { + return; + } + + if (this.Points.Count == 1) + { + ImGui.GetBackgroundDrawList() + .AddCircleFilled(this.Points[0], 10, ColorHelpers.RgbaVector4ToUint(col with { W = col.W / 2 }), 12); + ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, ColorHelpers.RgbaVector4ToUint(col), 12, thickness); + } + else + { + var path = new ImVectorWrapper(this.Points.Count); + foreach (var p in this.Points) + { + path.Add(p); + } + + ImGui.GetBackgroundDrawList() + .AddConvexPolyFilled(ref path[0], path.Length, ColorHelpers.RgbaVector4ToUint(col with { W = col.W / 2 })); + ImGui.GetBackgroundDrawList() + .AddPolyline(ref path[0], path.Length, ColorHelpers.RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness); + + path.Dispose(); + } + } + + /// + /// Checks whether the bounds contain a given point. + /// + /// The point to check. + /// True if the point exists within the bounds. + internal readonly bool ContainsPoint(Vector2 p) + { + var count = this.Points.Count; + var inside = false; + + for (var i = 0; i < count; i++) + { + var p1 = this.Points[i]; + var p2 = this.Points[(i + 1) % count]; + + if (p.Y > Min(p1.Y, p2.Y) && + p.Y <= Max(p1.Y, p2.Y) && + p.X <= Max(p1.X, p2.X) && + (p1.X.Equals(p2.X) || p.X <= ((p.Y - p1.Y) * (p2.X - p1.X) / (p2.Y - p1.Y)) + p1.X)) + { + inside = !inside; + } + } + + return inside; + } + + private static Vector2 TransformPoint(Vector2 p, Vector2 o, float r, Vector2 s) + { + var cosR = (float)Cos(r); + var sinR = (float)Sin(r); + var d = (p - o) * s; + + return new(o.X + (d.X * cosR) - (d.Y * sinR), + o.Y + (d.X * sinR) + (d.Y * cosR)); + } + + private void TransformPoints(AtkResNode* transformNode) + { + while (transformNode != null) + { + var offset = new Vector2(transformNode->X, transformNode->Y); + var origin = offset + new Vector2(transformNode->OriginX, transformNode->OriginY); + var rotation = transformNode->Rotation; + var scale = new Vector2(transformNode->ScaleX, transformNode->ScaleY); + + this.Points = this.Points.Select(b => TransformPoint(b + offset, origin, rotation, scale)).ToList(); + + transformNode = transformNode->ParentNode; + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs index d4bea2931..33db51c75 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs @@ -1,11 +1,11 @@ -namespace Dalamud.Interface.Internal.Windows.Data.Widgets; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying addon inspector. /// internal class AddonInspectorWidget : IDataWindowWidget { - private UiDebug? addonInspector; + private UiDebug2.UiDebug2? addonInspector; /// public string[]? CommandShortcuts { get; init; } = { "ai", "addoninspector" }; @@ -19,7 +19,7 @@ internal class AddonInspectorWidget : IDataWindowWidget /// public void Load() { - this.addonInspector = new UiDebug(); + this.addonInspector = new UiDebug2.UiDebug2(); if (this.addonInspector is not null) {