mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Update Addon Inspector
This updates the Addon Inspector with lots of new features and functionality. - Features from Caraxi's fork of UiDebug have been incorporated, such as the Element Selector UI and address search. - Any addon or node can now pop out into its own window. - Revised the visual style of node field/property information. - Color values are now visually displayed. - Any nodes or components that are referenced by fields within the addon will now show that field name in the inspector. - Added editors for nodes, allowing complete control over most of their properties. - Improved texture display for Image nodes (and Image node variant types). The active part of the texture is now highlighted, and the boundaries of other parts can be shown via mouseover. - Highlighting of node bounds onscreen is now more accurate, factoring in rotation (including when using the Element Selector). - Display of animation timelines has been revamped, showing a table of keyframes for each animation. A standalone SamplePlugin-based version is available here: https://github.com/ItsBexy/UiDebug2
This commit is contained in:
parent
0fb7585973
commit
bf8690fc60
23 changed files with 4039 additions and 3 deletions
|
|
@ -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;
|
||||
|
||||
/// <inheritdoc cref="AddonTree"/>
|
||||
public unsafe partial class AddonTree
|
||||
{
|
||||
private static readonly Dictionary<string, Type?> AddonTypeDict = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of names corresponding to pointers documented within the addon struct.
|
||||
/// </summary>
|
||||
internal Dictionary<nint, List<string>> 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<string>? 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<string>(path!) { name }) && fieldType.DeclaringType == baseType)
|
||||
{
|
||||
this.PopulateFieldNames(field.GetValue(obj), fieldAddr, new List<string>(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<string>(path!) { itemName });
|
||||
|
||||
var item = Marshal.PtrToStructure(itemAddr, itemType);
|
||||
if (itemType.DeclaringType == baseType)
|
||||
{
|
||||
this.PopulateFieldNames(item, itemAddr, new List<string>(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<string>(path!) { name });
|
||||
this.FieldNames.TryAdd(pointer, new List<string>(path) { name });
|
||||
|
||||
if (itemType?.DeclaringType != baseType || itemType.IsPointer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = Marshal.PtrToStructure(pointer, itemType);
|
||||
this.PopulateFieldNames(item, pointer, new List<string>(path) { name });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning($"Failed to parse pointer: {itemType}* {name} in {baseType}!\n{ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
242
Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs
Normal file
242
Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A class representing an <see cref="AtkUnitBase"/>, allowing it to be browsed within an ImGui window.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon this tree represents.
|
||||
/// </summary>
|
||||
internal string AddonName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of trees representing nodes within this addon.
|
||||
/// </summary>
|
||||
internal Dictionary<nint, ResNodeTree> NodeTrees { get; set; } = [];
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an instance of <see cref="AddonTree"/> for the given addon name (or creates one if none are found).
|
||||
/// The tree can then be drawn within the Addon Inspector and browsed.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the addon.</param>
|
||||
/// <returns>The <see cref="AddonTree"/> for the named addon. Returns null if it does not exist, or if it is not at the expected address.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws this AddonTree within a window.
|
||||
/// </summary>
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a given <see cref="AtkResNode"/> exists somewhere within this <see cref="AddonTree"/>'s associated <see cref="AtkUnitBase"/> (or any of its <see cref="AtkComponentNode"/>s).
|
||||
/// </summary>
|
||||
/// <param name="node">The node to check.</param>
|
||||
/// <returns>true if the node was found.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the addon exists at the expected address. If the addon is null or has a new address, disposes this instance of <see cref="AddonTree"/>.
|
||||
/// </summary>
|
||||
/// <param name="addon">The addon, if successfully found.</param>
|
||||
/// <returns>true if the addon is found.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
Normal file
68
Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Class that prints the events table for a node, where applicable.
|
||||
/// </summary>
|
||||
public static class Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Prints out each <see cref="AtkEventManager.Event"/> for a given node.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to print events for.</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
using static Dalamud.Utility.Util;
|
||||
|
||||
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
|
||||
|
||||
/// <summary>
|
||||
/// A tree for an <see cref="AtkClippingMaskNode"/> that can be printed and browsed via ImGui.
|
||||
/// </summary>
|
||||
internal unsafe class ClippingMaskNodeTree : ImageNodeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClippingMaskNodeTree"/> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to create a tree for.</param>
|
||||
/// <param name="addonTree">The tree representing the containing addon.</param>
|
||||
internal ClippingMaskNodeTree(AtkResNode* node, AddonTree addonTree)
|
||||
: base(node, addonTree)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override uint PartId => this.CmNode->PartId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override AtkUldPartsList* PartsList => this.CmNode->PartsList;
|
||||
|
||||
private AtkClippingMaskNode* CmNode => (AtkClippingMaskNode*)this.Node;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintNodeObject() => ShowStruct(this.CmNode);
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) => this.DrawTextureAndParts();
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
using static Dalamud.Utility.Util;
|
||||
|
||||
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
|
||||
|
||||
/// <summary>
|
||||
/// A tree for an <see cref="AtkCollisionNode"/> that can be printed and browsed via ImGui.
|
||||
/// </summary>
|
||||
internal unsafe class CollisionNodeTree : ResNodeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollisionNodeTree"/> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to create a tree for.</param>
|
||||
/// <param name="addonTree">The tree representing the containing addon.</param>
|
||||
internal CollisionNodeTree(AtkResNode* node, AddonTree addonTree)
|
||||
: base(node, addonTree)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintNodeObject() => ShowStruct((AtkCollisionNode*)this.Node);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A tree for an <see cref="AtkComponentNode"/> that can be printed and browsed via ImGui.
|
||||
/// </summary>
|
||||
internal unsafe class ComponentNodeTree : ResNodeTree
|
||||
{
|
||||
private readonly AtkUldManager* uldManager;
|
||||
|
||||
private readonly ComponentType componentType;
|
||||
|
||||
private readonly AtkComponentBase* component;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComponentNodeTree"/> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to create a tree for.</param>
|
||||
/// <param name="addonTree">The tree representing the containing addon.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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})";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintNodeObject()
|
||||
{
|
||||
base.PrintNodeObject();
|
||||
this.PrintComponentObject();
|
||||
ImGui.SameLine();
|
||||
ImGui.NewLine();
|
||||
this.PrintComponentDataObject();
|
||||
ImGui.SameLine();
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A tree for an <see cref="AtkCounterNode"/> that can be printed and browsed via ImGui.
|
||||
/// </summary>
|
||||
internal unsafe partial class CounterNodeTree : ResNodeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CounterNodeTree"/> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to create a tree for.</param>
|
||||
/// <param name="addonTree">The tree representing the containing addon.</param>
|
||||
internal CounterNodeTree(AtkResNode* node, AddonTree addonTree)
|
||||
: base(node, addonTree)
|
||||
{
|
||||
}
|
||||
|
||||
private AtkCounterNode* CntNode => (AtkCounterNode*)this.Node;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintNodeObject() => ShowStruct(this.CntNode);
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
|
||||
{
|
||||
if (!isEditorOpen)
|
||||
{
|
||||
PrintFieldValuePairs(("Text", ((AtkCounterNode*)this.Node)->NodeText.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
384
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
Normal file
384
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <inheritdoc cref="ResNodeTree"/>
|
||||
internal unsafe partial class ResNodeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets up the table for the node editor, if the "Edit" checkbox is ticked.
|
||||
/// </summary>
|
||||
private protected void DrawNodeEditorTable()
|
||||
{
|
||||
ImGui.BeginTable($"###Editor{(nint)this.Node}", 2, SizingStretchProp | NoHostExtendX);
|
||||
|
||||
this.DrawEditorRows();
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws each row in the node editor table.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="CounterNodeTree"/>
|
||||
internal unsafe partial class CounterNodeTree
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ImageNodeTree"/>
|
||||
internal unsafe partial class ImageNodeTree
|
||||
{
|
||||
private static int TexDisplayStyle { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NineGridNodeTree"/>
|
||||
internal unsafe partial class NineGridNodeTree
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TextNodeTree"/>
|
||||
internal unsafe partial class TextNodeTree
|
||||
{
|
||||
private static readonly List<FontType> FontList = Enum.GetValues<FontType>().ToList();
|
||||
|
||||
private static readonly string[] FontNames = Enum.GetNames<FontType>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
}
|
||||
317
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
Normal file
317
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A tree for an <see cref="AtkImageNode"/> that can be printed and browsed via ImGui.
|
||||
/// </summary>
|
||||
internal unsafe partial class ImageNodeTree : ResNodeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageNodeTree"/> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to create a tree for.</param>
|
||||
/// <param name="addonTree">The tree representing the containing addon.</param>
|
||||
internal ImageNodeTree(AtkResNode* node, AddonTree addonTree)
|
||||
: base(node, addonTree)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the part ID that this node uses.
|
||||
/// </summary>
|
||||
private protected virtual uint PartId => this.ImgNode->PartId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parts list that this node uses.
|
||||
/// </summary>
|
||||
private protected virtual AtkUldPartsList* PartsList => this.ImgNode->PartsList;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a summary of pertinent data about this <see cref="AtkImageNode"/>'s texture. Updated each time <see cref="DrawTextureAndParts"/> is called.
|
||||
/// </summary>
|
||||
private protected TextureData TexData { get; set; }
|
||||
|
||||
private AtkImageNode* ImgNode => (AtkImageNode*)this.Node;
|
||||
|
||||
/// <summary>
|
||||
/// Draws the texture inside the window, in either of two styles.<br/><br/>
|
||||
/// <term>Full Image</term>presents the texture in full as a spritesheet.<br/>
|
||||
/// <term>Parts List</term>presents the individual parts as rows in a table.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws an outline of a given part within the texture.
|
||||
/// </summary>
|
||||
/// <param name="partId">The part ID.</param>
|
||||
/// <param name="cursorScreenPos">The absolute position of the cursor onscreen.</param>
|
||||
/// <param name="cursorLocalPos">The relative position of the cursor within the window.</param>
|
||||
/// <param name="col">The color of the outline.</param>
|
||||
/// <param name="reqHover">Whether this outline requires the user to mouse over it.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintNodeObject() => ShowStruct(this.ImgNode);
|
||||
|
||||
/// <inheritdoc/>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A summary of pertinent data about a node's texture.
|
||||
/// </summary>
|
||||
protected struct TextureData
|
||||
{
|
||||
/// <summary>The texture's partslist.</summary>
|
||||
public AtkUldPartsList* PartsList;
|
||||
|
||||
/// <summary>The number of parts in the texture.</summary>
|
||||
public uint PartCount;
|
||||
|
||||
/// <summary>The part ID the node is using.</summary>
|
||||
public uint PartId;
|
||||
|
||||
/// <summary>The texture itself.</summary>
|
||||
public Texture* Texture = null;
|
||||
|
||||
/// <summary>The type of texture.</summary>
|
||||
public TextureType TexType = 0;
|
||||
|
||||
/// <summary>The texture's file path (if <see cref="TextureType.Resource"/>, otherwise this value is null).</summary>
|
||||
public string? Path = null;
|
||||
|
||||
/// <summary>Whether this is a high-resolution texture.</summary>
|
||||
public bool HiRes = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextureData"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="partsList">The texture's parts list.</param>
|
||||
/// <param name="partId">The part ID being used by the node.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <inheritdoc cref="NineGridNodeTree"/>
|
||||
internal unsafe partial class NineGridNodeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// A struct representing the four offsets of an <see cref="AtkNineGridNode"/>.
|
||||
/// </summary>
|
||||
internal struct NineGridOffsets
|
||||
{
|
||||
/// <summary>Top offset.</summary>
|
||||
internal int Top;
|
||||
|
||||
/// <summary>Left offset.</summary>
|
||||
internal int Left;
|
||||
|
||||
/// <summary>Right offset.</summary>
|
||||
internal int Right;
|
||||
|
||||
/// <summary>Bottom offset.</summary>
|
||||
internal int Bottom;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NineGridOffsets"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="top">The top offset.</param>
|
||||
/// <param name="right">The right offset.</param>
|
||||
/// <param name="bottom">The bottom offset.</param>
|
||||
/// <param name="left">The left offset.</param>
|
||||
internal NineGridOffsets(int top, int right, int bottom, int left)
|
||||
{
|
||||
this.Top = top;
|
||||
this.Right = right;
|
||||
this.Left = left;
|
||||
this.Bottom = bottom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NineGridOffsets"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="ngNode">The node using these offsets.</param>
|
||||
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;
|
||||
|
||||
/// <summary>Prints the offsets in ImGui.</summary>
|
||||
internal readonly void Print() => PrintFieldValuePairs(("Top", $"{this.Top}"), ("Bottom", $"{this.Bottom}"), ("Left", $"{this.Left}"), ("Right", $"{this.Right}"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A tree for an <see cref="AtkNineGridNode"/> that can be printed and browsed via ImGui.
|
||||
/// </summary>
|
||||
internal unsafe partial class NineGridNodeTree : ImageNodeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NineGridNodeTree"/> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to create a tree for.</param>
|
||||
/// <param name="addonTree">The tree representing the containing addon.</param>
|
||||
internal NineGridNodeTree(AtkResNode* node, AddonTree addonTree)
|
||||
: base(node, addonTree)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override uint PartId => this.NgNode->PartId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override AtkUldPartsList* PartsList => this.NgNode->PartsList;
|
||||
|
||||
private AtkNineGridNode* NgNode => (AtkNineGridNode*)this.Node;
|
||||
|
||||
private NineGridOffsets Offsets => new(this.NgNode);
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintNodeObject() => ShowStruct(this.NgNode);
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
|
||||
{
|
||||
if (!isEditorOpen)
|
||||
{
|
||||
ImGui.Text("NineGrid Offsets:\t");
|
||||
ImGui.SameLine();
|
||||
this.Offsets.Print();
|
||||
}
|
||||
|
||||
this.DrawTextureAndParts();
|
||||
}
|
||||
}
|
||||
402
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
Normal file
402
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A tree for an <see cref="AtkResNode"/> that can be printed and browsed via ImGui.
|
||||
/// </summary>
|
||||
/// <remarks>As with the structs they represent, this class serves as the base class for other types of NodeTree.</remarks>
|
||||
internal unsafe partial class ResNodeTree : IDisposable
|
||||
{
|
||||
private NodePopoutWindow? window;
|
||||
|
||||
private bool editorOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResNodeTree"/> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to create a tree for.</param>
|
||||
/// <param name="addonTree">The tree representing the containing addon.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="AtkResNode"/> this tree represents.
|
||||
/// </summary>
|
||||
protected internal AtkResNode* Node { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Browsing.AddonTree"/> containing this tree.
|
||||
/// </summary>
|
||||
protected internal AddonTree AddonTree { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets this node's type.
|
||||
/// </summary>
|
||||
private protected NodeType NodeType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Clears this NodeTree's popout window, if it has one.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.window != null && PopoutWindows.Windows.Contains(this.window))
|
||||
{
|
||||
PopoutWindows.RemoveWindow(this.window);
|
||||
this.window.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an instance of <see cref="ResNodeTree"/> (or one of its inheriting types) for the given node. If no instance exists, one is created.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to get a tree for.</param>
|
||||
/// <param name="addonTree">The tree for the node's containing addon.</param>
|
||||
/// <returns>An existing or newly-created instance of <see cref="ResNodeTree"/>.</returns>
|
||||
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),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Prints a list of NodeTree for a given list of nodes.
|
||||
/// </summary>
|
||||
/// <param name="nodeList">The address of the start of the list.</param>
|
||||
/// <param name="count">The number of nodes in the list.</param>
|
||||
/// <param name="addonTree">The tree for the containing addon.</param>
|
||||
internal static void PrintNodeList(AtkResNode** nodeList, int count, AddonTree addonTree)
|
||||
{
|
||||
for (uint j = 0; j < count; j++)
|
||||
{
|
||||
GetOrCreate(nodeList[j], addonTree).Print(j);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="PrintNodeList"/>, but outputs the results as a collapsible tree.
|
||||
/// </summary>
|
||||
/// <param name="nodeList">The address of the start of the list.</param>
|
||||
/// <param name="count">The number of nodes in the list.</param>
|
||||
/// <param name="label">The heading text of the tree.</param>
|
||||
/// <param name="addonTree">The tree for the containing addon.</param>
|
||||
/// <param name="color">The text color of the heading.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints this tree in the window.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the tree within its containing node or addon, if applicable.</param>
|
||||
/// <param name="forceOpen">Whether the tree should default to being open.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints out the tree's header text.
|
||||
/// </summary>
|
||||
internal void WriteTreeHeading()
|
||||
{
|
||||
ImGui.Text(this.GetHeaderText());
|
||||
this.PrintFieldNames();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the given pointer has been identified as a field within the addon struct, this method prints that field's name.
|
||||
/// </summary>
|
||||
/// <param name="ptr">The pointer to check.</param>
|
||||
/// <param name="color">The text color to use.</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>The resulting header text string.</returns>
|
||||
private protected virtual string GetHeaderText()
|
||||
{
|
||||
var count = this.GetDirectChildCount();
|
||||
return $"{this.NodeType} Node{(count > 0 ? $" [+{count}]" : string.Empty)} ({(nint)this.Node:X})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the node struct.
|
||||
/// </summary>
|
||||
private protected virtual void PrintNodeObject()
|
||||
{
|
||||
ShowStruct(this.Node);
|
||||
ImGui.SameLine();
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints any field names for the node.
|
||||
/// </summary>
|
||||
private protected virtual void PrintFieldNames() => this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1));
|
||||
|
||||
/// <summary>
|
||||
/// Prints all direct children of this node.
|
||||
/// </summary>
|
||||
private protected virtual void PrintChildNodes()
|
||||
{
|
||||
var prevNode = this.Node->ChildNode;
|
||||
while (prevNode != null)
|
||||
{
|
||||
GetOrCreate(prevNode, this.AddonTree).Print(null);
|
||||
prevNode = prevNode->PrevSiblingNode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints any specific fields pertaining to the specific type of node.
|
||||
/// </summary>
|
||||
/// <param name="isEditorOpen">Whether the "Edit" box is currently checked.</param>
|
||||
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})"));
|
||||
}
|
||||
}
|
||||
126
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
Normal file
126
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A tree for an <see cref="AtkTextNode"/> that can be printed and browsed via ImGui.
|
||||
/// </summary>
|
||||
internal unsafe partial class TextNodeTree : ResNodeTree
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextNodeTree"/> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to create a tree for.</param>
|
||||
/// <param name="addonTree">The tree representing the containing addon.</param>
|
||||
internal TextNodeTree(AtkResNode* node, AddonTree addonTree)
|
||||
: base(node, addonTree)
|
||||
{
|
||||
}
|
||||
|
||||
private AtkTextNode* TxtNode => (AtkTextNode*)this.Node;
|
||||
|
||||
private Utf8String NodeText => this.TxtNode->NodeText;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private protected override void PrintNodeObject() => ShowStruct(this.TxtNode);
|
||||
|
||||
/// <inheritdoc/>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
|
||||
|
||||
/// <inheritdoc cref="TimelineTree"/>
|
||||
public partial struct TimelineTree
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for retrieving and printing the contents of a given column in an animation timeline table.
|
||||
/// </summary>
|
||||
public interface IKeyGroupColumn
|
||||
{
|
||||
/// <summary>Gets the column's name/heading.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>Gets the number of cells in the column.</summary>
|
||||
public int Count { get; }
|
||||
|
||||
/// <summary>Gets the column's width.</summary>
|
||||
public float Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Calls this column's print function for a given row.
|
||||
/// </summary>
|
||||
/// <param name="i">The row number.</param>
|
||||
public void PrintValueAt(int i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A column within an animation timeline table, representing a particular KeyGroup.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type of the KeyGroup.</typeparam>
|
||||
public struct KeyGroupColumn<T> : IKeyGroupColumn
|
||||
{
|
||||
/// <summary>The values of each cell in the column.</summary>
|
||||
public List<T> Values;
|
||||
|
||||
/// <summary>The method that should be used to format and print values in this KeyGroup.</summary>
|
||||
public Action<T> PrintFunc;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyGroupColumn{T}"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="name">The column's name/heading.</param>
|
||||
/// <param name="printFunc">The method that should be used to format and print values in this KeyGroup.</param>
|
||||
internal KeyGroupColumn(string name, Action<T>? printFunc = null)
|
||||
{
|
||||
this.Name = name;
|
||||
this.PrintFunc = printFunc ?? PlainTextCell;
|
||||
this.Values = [];
|
||||
this.Width = 50;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public float Width { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly int Count => this.Values.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The default print function, if none is specified.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to print.</param>
|
||||
public static void PlainTextCell(T value) => ImGui.Text($"{value}");
|
||||
|
||||
/// <summary>
|
||||
/// Adds a value to this column.
|
||||
/// </summary>
|
||||
/// <param name="val">The value to add.</param>
|
||||
public readonly void Add(T val) => this.Values.Add(val);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly void PrintValueAt(int i)
|
||||
{
|
||||
if (this.Values.Count > i)
|
||||
{
|
||||
this.PrintFunc.Invoke(this.Values[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextDisabled("...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
381
Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
Normal file
381
Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A struct allowing a node's animation timeline to be printed and browsed.
|
||||
/// </summary>
|
||||
public unsafe partial struct TimelineTree
|
||||
{
|
||||
private AtkResNode* node;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimelineTree"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="node">The node whose timelines are to be displayed.</param>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Prints out this timeline tree within a window.
|
||||
/// </summary>
|
||||
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<AtkTimelineKeyGroup> keyGroups, List<IKeyGroupColumn> 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<ushort>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var xColumn = new KeyGroupColumn<float>("X");
|
||||
var yColumn = new KeyGroupColumn<float>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var rotColumn = new KeyGroupColumn<float>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scaleXColumn = new KeyGroupColumn<float>("ScaleX");
|
||||
var scaleYColumn = new KeyGroupColumn<float>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var alphaColumn = new KeyGroupColumn<byte>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var addRGBColumn = new KeyGroupColumn<Vector3>("Add", PrintAddCell) { Width = 110 };
|
||||
var multiplyRGBColumn = new KeyGroupColumn<ByteColor>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var textColorColumn = new KeyGroupColumn<ByteColor>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var partColumn = new KeyGroupColumn<ushort>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var edgeColorColumn = new KeyGroupColumn<ByteColor>("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<IKeyGroupColumn> columns)
|
||||
{
|
||||
if (keyGroup.KeyFrameCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var labelColumn = new KeyGroupColumn<ushort>("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<IKeyGroupColumn> BuildColumns(AtkTimelineAnimation animation)
|
||||
{
|
||||
var keyGroups = animation.KeyGroups;
|
||||
var columns = new List<IKeyGroupColumn>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
475
Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
Normal file
475
Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A tool that enables the user to select UI elements within the inspector by mousing over them onscreen.
|
||||
/// </summary>
|
||||
internal unsafe class ElementSelector : IDisposable
|
||||
{
|
||||
private const int UnitListCount = 18;
|
||||
|
||||
private readonly UiDebug2 uiDebug2;
|
||||
|
||||
private string addressSearchInput = string.Empty;
|
||||
|
||||
private int index;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ElementSelector"/> class.
|
||||
/// </summary>
|
||||
/// <param name="uiDebug2">The instance of <see cref="UiDebug2"/> this Element Selector belongs to.</param>
|
||||
internal ElementSelector(UiDebug2 uiDebug2)
|
||||
{
|
||||
this.uiDebug2 = uiDebug2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the results retrieved by the Element Selector.
|
||||
/// </summary>
|
||||
internal static nint[] SearchResults { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value governing the highlighting of nodes when found via search.
|
||||
/// </summary>
|
||||
internal static float Countdown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window has scrolled down to the position of the search result.
|
||||
/// </summary>
|
||||
internal static bool Scrolled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the mouseover UI is currently active.
|
||||
/// </summary>
|
||||
internal bool Active { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Active = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the Element Selector and Address Search interface at the bottom of the sidebar.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the Element Selector's search output within the main window.
|
||||
/// </summary>
|
||||
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> { (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<AddonResult> GetAtkUnitBaseAtPosition(Vector2 position)
|
||||
{
|
||||
var addonResults = new List<AddonResult>();
|
||||
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<NodeResult> GetNodeAtPosition(AtkUldManager* uldManager, Vector2 position, bool reverse)
|
||||
{
|
||||
var nodeResults = new List<NodeResult>();
|
||||
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<nint>? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AtkUnitBase"/> found by the Element Selector.
|
||||
/// </summary>
|
||||
internal struct AddonResult
|
||||
{
|
||||
/// <summary>The addon itself.</summary>
|
||||
internal AtkUnitBase* Addon;
|
||||
|
||||
/// <summary>A list of nodes discovered within this addon by the Element Selector.</summary>
|
||||
internal List<NodeResult> Nodes;
|
||||
|
||||
/// <summary>The calculated area of the addon's root node.</summary>
|
||||
internal float Area;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonResult"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="addon">The addon found.</param>
|
||||
/// <param name="nodes">A list for documenting nodes found within the addon.</param>
|
||||
public AddonResult(AtkUnitBase* addon, List<NodeResult> nodes)
|
||||
{
|
||||
this.Addon = addon;
|
||||
this.Nodes = nodes;
|
||||
var rootNode = addon->RootNode;
|
||||
this.Area = rootNode != null ? rootNode->Width * rootNode->Height * rootNode->ScaleY * rootNode->ScaleX : 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not AddonResult ar)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (nint)this.Addon == (nint)ar.Addon;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AtkResNode"/> found by the Element Selector.
|
||||
/// </summary>
|
||||
internal struct NodeResult
|
||||
{
|
||||
/// <summary>The node itself.</summary>
|
||||
internal AtkResNode* Node;
|
||||
|
||||
/// <summary>A struct representing the perimeter of the node.</summary>
|
||||
internal NodeBounds NodeBounds;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override readonly bool Equals(object? obj)
|
||||
{
|
||||
if (obj is not NodeResult nr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return nr.Node == this.Node;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
Normal file
50
Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Internal.UiDebug2.Browsing;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal.UiDebug2;
|
||||
|
||||
/// <summary>
|
||||
/// A popout window for an <see cref="AddonTree"/>.
|
||||
/// </summary>
|
||||
internal class AddonPopoutWindow : Window, IDisposable
|
||||
{
|
||||
private readonly AddonTree addonTree;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonPopoutWindow"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tree">The AddonTree this popout will show.</param>
|
||||
/// <param name="name">the window's name.</param>
|
||||
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) };
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Draw()
|
||||
{
|
||||
ImGui.BeginChild($"{this.WindowName}child", new(-1, -1), true);
|
||||
this.addonTree.Draw();
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
69
Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
Normal file
69
Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A popout window for a <see cref="ResNodeTree"/>.
|
||||
/// </summary>
|
||||
internal unsafe class NodePopoutWindow : Window, IDisposable
|
||||
{
|
||||
private readonly ResNodeTree resNodeTree;
|
||||
|
||||
private bool firstDraw = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NodePopoutWindow"/> class.
|
||||
/// </summary>
|
||||
/// <param name="nodeTree">The node tree this window will show.</param>
|
||||
/// <param name="windowName">The name of the window.</param>
|
||||
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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
214
Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs
Normal file
214
Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <inheritdoc cref="UiDebug2"/>
|
||||
internal unsafe partial class UiDebug2
|
||||
{
|
||||
/// <summary>
|
||||
/// All unit lists to check for addons.
|
||||
/// </summary>
|
||||
internal static readonly List<UnitListOption> 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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base address for all unit lists.
|
||||
/// </summary>
|
||||
/// <returns>The address, if found.</returns>
|
||||
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<AddonOption>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A struct representing a unit list that can be browed in the sidebar.
|
||||
/// </summary>
|
||||
internal struct UnitListOption
|
||||
{
|
||||
/// <summary>The index of the unit list.</summary>
|
||||
internal uint Index;
|
||||
|
||||
/// <summary>The name of the unit list.</summary>
|
||||
internal string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnitListOption"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="i">The index of the unit list.</param>
|
||||
/// <param name="name">The name of the unit list.</param>
|
||||
internal UnitListOption(uint i, string name)
|
||||
{
|
||||
this.Index = i;
|
||||
this.Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A struct representing an addon that can be selected in the sidebar.
|
||||
/// </summary>
|
||||
internal struct AddonOption
|
||||
{
|
||||
/// <summary>The name of the addon.</summary>
|
||||
internal string Name;
|
||||
|
||||
/// <summary>Whether the addon is visible.</summary>
|
||||
internal bool Visible;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AddonOption"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the addon.</param>
|
||||
/// <param name="visible">Whether the addon is visible.</param>
|
||||
internal AddonOption(string name, bool visible)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Visible = visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs
Normal file
112
Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs
Normal file
|
|
@ -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
|
||||
|
||||
/// <summary>
|
||||
/// A tool for browsing the contents and structure of UI elements.
|
||||
/// </summary>
|
||||
internal partial class UiDebug2 : IDisposable
|
||||
{
|
||||
private readonly ElementSelector elementSelector;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UiDebug2"/> class.
|
||||
/// </summary>
|
||||
internal UiDebug2()
|
||||
{
|
||||
this.elementSelector = new(this);
|
||||
|
||||
GameGui = Service<GameGui>.Get();
|
||||
|
||||
Log = new ModuleLog("UiDebug2");
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModuleLog"/>
|
||||
internal static ModuleLog Log { get; set; } = null!;
|
||||
|
||||
/// <inheritdoc cref="IGameGui"/>
|
||||
internal static IGameGui GameGui { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="AddonTree"/> instances, each representing an <see cref="AtkUnitBase"/>.
|
||||
/// </summary>
|
||||
internal static Dictionary<string, AddonTree> AddonTrees { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a window system to handle any popout windows for addons or nodes.
|
||||
/// </summary>
|
||||
internal static WindowSystem PopoutWindows { get; set; } = new("UiDebugPopouts");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the currently-selected <see cref="AtkUnitBase"/>.
|
||||
/// </summary>
|
||||
internal string? SelectedAddonName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Clears all windows and <see cref="AddonTree"/>s.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var a in AddonTrees)
|
||||
{
|
||||
a.Value.Dispose();
|
||||
}
|
||||
|
||||
AddonTrees.Clear();
|
||||
PopoutWindows.RemoveAllWindows();
|
||||
this.elementSelector.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the UiDebug tool's interface and contents.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
204
Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
Normal file
204
Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Miscellaneous ImGui tools used by <see cref="UiDebug2"/>.
|
||||
/// </summary>
|
||||
internal static class Gui
|
||||
{
|
||||
/// <summary>
|
||||
/// Begins a tree node that also displays a colored line to the left while open.
|
||||
/// </summary>
|
||||
/// <param name="label">The label of the tree.</param>
|
||||
/// <param name="color">The color of the heading text.</param>
|
||||
/// <param name="lineStart">A value representing where to begin drawing the left-side line.</param>
|
||||
/// <param name="defOpen">Whether this tree should default to being open.</param>
|
||||
/// <returns>true if the tree is open.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NestedTreePush(string, Vector4, out Vector2, bool)"/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes a NestedTree.
|
||||
/// </summary>
|
||||
/// <param name="lineStart">The starting position calculated when the tree was pushed.</param>
|
||||
/// <param name="color">The color of the left-side line.</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A radio-button-esque input that uses Fontawesome icon buttons.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of value being set.</typeparam>
|
||||
/// <param name="label">The label for the inputs.</param>
|
||||
/// <param name="val">The value being set.</param>
|
||||
/// <param name="options">A list of all options to create buttons for.</param>
|
||||
/// <param name="icons">A list of the icons to use for each option.</param>
|
||||
/// <returns>true if a button is clicked.</returns>
|
||||
internal static unsafe bool IconSelectInput<T>(string label, ref T val, List<T> options, List<FontAwesomeIcon> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints field name and its value.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the field.</param>
|
||||
/// <param name="value">The value of the field.</param>
|
||||
/// <param name="copy">Whether to enable click-to-copy.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints a set of fields and their values.
|
||||
/// </summary>
|
||||
/// <param name="pairs">Tuples of fieldnames and values to display.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="PrintColor(Vector4,string)"/>
|
||||
internal static void PrintColor(ByteColor color, string fmt) => PrintColor(RgbaUintToVector4(color.RGBA), fmt);
|
||||
|
||||
/// <inheritdoc cref="PrintColor(Vector4,string)"/>
|
||||
internal static void PrintColor(Vector3 color, string fmt) => PrintColor(new Vector4(color, 1), fmt);
|
||||
|
||||
/// <summary>
|
||||
/// Prints a text string representing a color, with a backdrop in that color.
|
||||
/// </summary>
|
||||
/// <param name="color">The color value.</param>
|
||||
/// <param name="fmt">The text string to print.</param>
|
||||
/// <remarks>Colors the text itself either white or black, depending on the luminosity of the background color.</remarks>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ImGuiHelpers.ClickToCopyText"/>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a tooltip that changes based on the cursor's x-position within the hovered item.
|
||||
/// </summary>
|
||||
/// <param name="tooltips">The text for each section.</param>
|
||||
/// <returns>true if the item is hovered.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
170
Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
Normal file
170
Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A struct representing the perimeter of an <see cref="AtkResNode"/>, accounting for all transformations.
|
||||
/// </summary>
|
||||
public unsafe struct NodeBounds
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NodeBounds"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="node">The node to calculate the bounds of.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NodeBounds"/> struct, containing only a single given point.
|
||||
/// </summary>
|
||||
/// <param name="point">The point onscreen.</param>
|
||||
/// <param name="node">The node used to calculate transformations.</param>
|
||||
internal NodeBounds(Vector2 point, AtkResNode* node)
|
||||
{
|
||||
this.Points = [point];
|
||||
this.TransformPoints(node);
|
||||
}
|
||||
|
||||
private List<Vector2> Points { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Draws the bounds onscreen.
|
||||
/// </summary>
|
||||
/// <param name="col">The color of line to use.</param>
|
||||
/// <param name="thickness">The thickness of line to use.</param>
|
||||
/// <remarks>If there is only a single point to draw, it will be indicated with a circle and dot.</remarks>
|
||||
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<Vector2>(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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the bounds onscreen, filled in.
|
||||
/// </summary>
|
||||
/// <param name="col">The fill and border color.</param>
|
||||
/// <param name="thickness">The border thickness.</param>
|
||||
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<Vector2>(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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the bounds contain a given point.
|
||||
/// </summary>
|
||||
/// <param name="p">The point to check.</param>
|
||||
/// <returns>True if the point exists within the bounds.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
/// <summary>
|
||||
/// Widget for displaying addon inspector.
|
||||
/// </summary>
|
||||
internal class AddonInspectorWidget : IDataWindowWidget
|
||||
{
|
||||
private UiDebug? addonInspector;
|
||||
private UiDebug2.UiDebug2? addonInspector;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; } = { "ai", "addoninspector" };
|
||||
|
|
@ -19,7 +19,7 @@ internal class AddonInspectorWidget : IDataWindowWidget
|
|||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
{
|
||||
this.addonInspector = new UiDebug();
|
||||
this.addonInspector = new UiDebug2.UiDebug2();
|
||||
|
||||
if (this.addonInspector is not null)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue