mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
commit
422463f9cf
25 changed files with 4074 additions and 1 deletions
|
|
@ -0,0 +1,195 @@
|
|||
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 = [];
|
||||
|
||||
private static readonly Assembly? ClientStructs = AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(static a => a.GetName().Name == "FFXIVClientStructs");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of names for field offsets that have been documented in FFXIVClientStructs.
|
||||
/// </summary>
|
||||
internal Dictionary<nint, List<string>> FieldNames { get; set; } = [];
|
||||
|
||||
private object? GetAddonObj(AtkUnitBase* addon)
|
||||
{
|
||||
if (addon == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (AddonTypeDict.TryAdd(this.AddonName, null) && ClientStructs != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var t in from t in ClientStructs.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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void ParseExplicitField(nint fieldAddr, FieldInfo field, MemberInfo fieldType, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.FieldNames.TryAdd(fieldAddr, [..path, name]) && fieldType.DeclaringType == baseType)
|
||||
{
|
||||
this.PopulateFieldNames(field.GetValue(obj), fieldAddr, [..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, [..path, itemName]);
|
||||
|
||||
var item = Marshal.PtrToStructure(itemAddr, itemType);
|
||||
if (itemType.DeclaringType == baseType)
|
||||
{
|
||||
this.PopulateFieldNames(item, itemAddr, [..path, itemName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
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, [..path, name]);
|
||||
this.FieldNames.TryAdd(pointer, [..path, name]);
|
||||
|
||||
if (itemType?.DeclaringType != baseType || itemType.IsPointer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = Marshal.PtrToStructure(pointer, itemType);
|
||||
this.PopulateFieldNames(item, pointer, [..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.TextUnformatted($"{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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
Normal file
67
Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Dalamud.Interface.Internal.UiDebug2.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
using var tree = ImRaii.TreeNode($"Events##{(nint)node:X}eventTree");
|
||||
|
||||
if (tree)
|
||||
{
|
||||
using (ImRaii.Table($"##{(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.TextUnformatted($"{i++}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{evt->Type}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{evt->Param}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{evt->Flags}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{evt->Unk29}");
|
||||
ImGui.TableNextColumn();
|
||||
Gui.ClickToCopyText($"{(nint)evt->Target:X}");
|
||||
ImGui.TableNextColumn();
|
||||
Gui.ClickToCopyText($"{(nint)evt->Listener:X}");
|
||||
evt = evt->NextEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,282 @@
|
|||
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 ComponentType componentType;
|
||||
|
||||
/// <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.NodeType = 0;
|
||||
this.componentType = ((AtkUldComponentInfo*)this.UldManager->Objects)->ComponentType;
|
||||
}
|
||||
|
||||
private AtkComponentBase* Component => this.CompNode->Component;
|
||||
|
||||
private AtkComponentNode* CompNode => (AtkComponentNode*)this.Node;
|
||||
|
||||
private AtkUldManager* UldManager => &this.Component->UldManager;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (this.componentType)
|
||||
{
|
||||
case TextInput:
|
||||
var textInputComponent = (AtkComponentTextInput*)this.Component;
|
||||
ImGui.TextUnformatted(
|
||||
$"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
|
||||
ImGui.TextUnformatted(
|
||||
$"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}");
|
||||
ImGui.TextUnformatted(
|
||||
$"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;
|
||||
}
|
||||
}
|
||||
|
||||
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.Numerics;
|
||||
|
||||
using Dalamud.Interface.Internal.UiDebug2.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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()
|
||||
{
|
||||
using (ImRaii.Table($"###Editor{(nint)this.Node}", 2, SizingStretchProp | NoHostExtendX))
|
||||
{
|
||||
this.DrawEditorRows();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>()];
|
||||
|
||||
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 = IconButtonSelect($"{label}H", ref hAlign, [0, 1, 2], [AlignLeft, AlignCenter, AlignRight]);
|
||||
var vAlignInput = IconButtonSelect($"{label}V", ref vAlign, [0, 1, 2], [ArrowsUpToLine, GripLines, ArrowsDownToLine]);
|
||||
|
||||
if (hAlignInput || vAlignInput)
|
||||
{
|
||||
alignment = (AlignmentType)((vAlign * 3) + hAlign);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
316
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
Normal file
316
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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;
|
||||
using static ImGuiNET.ImGuiTreeNodeFlags;
|
||||
|
||||
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 (0)</term>presents the texture in full as a spritesheet.<br/>
|
||||
/// <term>Parts List (1)</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;
|
||||
}
|
||||
|
||||
using var tree = ImRaii.TreeNode($"Texture##texture{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", SpanFullWidth);
|
||||
|
||||
if (tree)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
using (ImRaii.Table($"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();
|
||||
|
||||
var col = i == this.TexData.PartId ? new Vector4(0, 0.85F, 1, 1) : new(1);
|
||||
ImGui.TextColored(col, $"#{i.ToString().PadLeft(this.TexData.PartCount.ToString().Length, '0')}");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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,88 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
420
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
Normal file
420
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Internal.UiDebug2.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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;
|
||||
|
||||
using static ImGuiNET.ImGuiCol;
|
||||
using static ImGuiNET.ImGuiTreeNodeFlags;
|
||||
|
||||
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 NodeTrees 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;
|
||||
}
|
||||
|
||||
using var c = ImRaii.PushColor(Text, color);
|
||||
using var tree = ImRaii.TreeNode($"{label}##{(nint)nodeList:X}", SpanFullWidth);
|
||||
c.Pop();
|
||||
|
||||
if (tree)
|
||||
{
|
||||
var lineStart = ImGui.GetCursorScreenPos() + new Vector2(-10, 2);
|
||||
|
||||
PrintNodeList(nodeList, count, addonTree);
|
||||
|
||||
var lineEnd = lineStart with { Y = ImGui.GetCursorScreenPos().Y - 7 };
|
||||
|
||||
if (lineStart.Y < lineEnd.Y)
|
||||
{
|
||||
ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, RgbaVector4ToUint(color), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.TextUnformatted(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 label = $"{(index == null ? string.Empty : $"[{index}] ")}[#{this.Node->NodeId}]###{(nint)this.Node:X}nodeTree";
|
||||
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);
|
||||
}
|
||||
|
||||
using var col = ImRaii.PushColor(Text, displayColor);
|
||||
using var tree = ImRaii.TreeNode(label, SpanFullWidth);
|
||||
|
||||
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();
|
||||
|
||||
col.Pop();
|
||||
|
||||
if (tree)
|
||||
{
|
||||
var lineStart = ImGui.GetCursorScreenPos() + new Vector2(-10, 2);
|
||||
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}");
|
||||
}
|
||||
|
||||
var lineEnd = lineStart with { Y = ImGui.GetCursorScreenPos().Y - 7 };
|
||||
|
||||
if (lineStart.Y < lineEnd.Y)
|
||||
{
|
||||
ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, RgbaVector4ToUint(displayColor), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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})"));
|
||||
}
|
||||
}
|
||||
120
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
Normal file
120
Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.Internal.UiDebug2.Utility;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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();
|
||||
|
||||
try
|
||||
{
|
||||
var style = new SeStringDrawParams
|
||||
{
|
||||
Color = this.TxtNode->TextColor.RGBA,
|
||||
EdgeColor = this.TxtNode->EdgeColor.RGBA,
|
||||
ForceEdgeColor = true,
|
||||
EdgeStrength = 1f,
|
||||
};
|
||||
|
||||
#pragma warning disable SeStringRenderer
|
||||
ImGuiHelpers.SeStringWrapped(this.NodeText.AsSpan(), style);
|
||||
#pragma warning restore SeStringRenderer
|
||||
}
|
||||
catch
|
||||
{
|
||||
ImGui.TextUnformatted(Marshal.PtrToStringAnsi(new(this.NodeText.StringPtr)) ?? string.Empty);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode($"Text Payloads##{(nint)this.Node:X}");
|
||||
|
||||
if (tree)
|
||||
{
|
||||
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.TextUnformatted($"[{i}]");
|
||||
ImGui.SameLine();
|
||||
switch (payload.Type)
|
||||
{
|
||||
case PayloadType.RawText when payload is TextPayload tp:
|
||||
{
|
||||
Gui.PrintFieldValuePair("Raw Text", tp.Text ?? string.Empty);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
ImGui.TextUnformatted(payload.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
|
||||
|
||||
/// <inheritdoc cref="TimelineTree"/>
|
||||
public readonly 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.TextUnformatted($"{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("...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
385
Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
Normal file
385
Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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;
|
||||
using static ImGuiNET.ImGuiTreeNodeFlags;
|
||||
|
||||
// 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 readonly unsafe partial struct TimelineTree
|
||||
{
|
||||
private readonly 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)
|
||||
{
|
||||
using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth);
|
||||
|
||||
if (tree)
|
||||
{
|
||||
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 == this.ActiveAnimation;
|
||||
this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + (a * sizeof(AtkTimelineAnimation))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.TextUnformatted($"{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);
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 0.65F, 0.4F, 1), isActive))
|
||||
{
|
||||
using var tree = ImRaii.TreeNode($"[#{a}] [Frames {animation.StartFrameIdx}-{animation.EndFrameIdx}] {(isActive ? " (Active)" : string.Empty)}###{(nint)this.node}animTree{a}");
|
||||
|
||||
if (tree)
|
||||
{
|
||||
PrintFieldValuePair("Animation", $"{address:X}");
|
||||
|
||||
ShowStruct((AtkTimelineAnimation*)address);
|
||||
|
||||
if (columns.Count > 0)
|
||||
{
|
||||
using (ImRaii.Table(
|
||||
$"##{(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
490
Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
Normal file
490
Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
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 Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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;
|
||||
// ReSharper disable StructLacksIEquatable.Global
|
||||
|
||||
#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()
|
||||
{
|
||||
using (ImRaii.Child("###sidebar_elementSelector", new(250, 0), true))
|
||||
{
|
||||
using (ImRaii.PushFont(IconFont))
|
||||
{
|
||||
using (ImRaii.PushColor(Text, new Vector4(1, 1, 0.2f, 1), this.Active))
|
||||
{
|
||||
if (ImGui.Button($"{(char)ObjectUngroup}"))
|
||||
{
|
||||
this.Active = !this.Active;
|
||||
}
|
||||
|
||||
if (Countdown > 0)
|
||||
{
|
||||
Countdown -= 1;
|
||||
if (Countdown < 0)
|
||||
{
|
||||
Countdown = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
using (ImRaii.PushColor(WindowBg, new Vector4(0.5f)))
|
||||
{
|
||||
using (ImRaii.Child("noClick", new(800, 2000), false, NoInputs | NoBackground | NoScrollWithMouse))
|
||||
{
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
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.TextUnformatted($"[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
using (ImRaii.PushColor(Text, selected ? new Vector4(1, 1, 0.2f, 1) : new(0.6f, 0.6f, 0.6f, 1)))
|
||||
{
|
||||
ResNodeTree.GetOrCreate(node, tree).WriteTreeHeading();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
Normal file
52
Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Internal.UiDebug2.Browsing;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
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()
|
||||
{
|
||||
using (ImRaii.Child($"{this.WindowName}child", new(-1, -1), true))
|
||||
{
|
||||
this.addonTree.Draw();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
71
Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
Normal file
71
Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Internal.UiDebug2.Browsing;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
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))
|
||||
{
|
||||
using (ImRaii.Child($"{(nint)this.Node:X}popoutChild", new(-1, -1), true))
|
||||
{
|
||||
ResNodeTree.GetOrCreate(this.Node, this.AddonTree).Print(null, this.firstDraw);
|
||||
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()
|
||||
{
|
||||
}
|
||||
}
|
||||
213
Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs
Normal file
213
Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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()
|
||||
{
|
||||
using (ImRaii.Group())
|
||||
{
|
||||
this.DrawNameSearch();
|
||||
this.DrawAddonSelectionList();
|
||||
this.elementSelector.DrawInterface();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNameSearch()
|
||||
{
|
||||
using (ImRaii.Child("###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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAddonSelectionList()
|
||||
{
|
||||
using (ImRaii.Child("###sideBar_addonList", new(250, -44), true, ImGuiWindowFlags.AlwaysVerticalScrollbar))
|
||||
{
|
||||
var unitListBaseAddr = GetUnitListBaseAddr();
|
||||
|
||||
foreach (var unit in UnitListOptions)
|
||||
{
|
||||
this.DrawUnitListOption(unitListBaseAddr, unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var countStr = $"{(usingFilter ? $"{matchCount}/" : string.Empty)}{totalCount}";
|
||||
|
||||
using var col1 = ImRaii.PushColor(ImGuiCol.Text, anyVisible ? new Vector4(1) : new Vector4(0.6f, 0.6f, 0.6f, 1));
|
||||
using var tree = ImRaii.TreeNode($"{unit.Name} [{countStr}]###unitListTree{unit.Index}");
|
||||
col1.Pop();
|
||||
|
||||
if (tree)
|
||||
{
|
||||
foreach (var option in options)
|
||||
{
|
||||
using (ImRaii.PushColor(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
109
Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs
Normal file
109
Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.Internal.UiDebug2.Browsing;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ModuleLog"/>
|
||||
internal static ModuleLog Log { get; set; } = new("UiDebug2");
|
||||
|
||||
/// <inheritdoc cref="IGameGui"/>
|
||||
internal static IGameGui GameGui { get; set; } = Service<GameGui>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="AddonTree"/> instances, each representing an <see cref="FFXIVClientStructs.FFXIV.Component.GUI.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();
|
||||
|
||||
using (ImRaii.Child("###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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
179
Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
Normal file
179
Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
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>
|
||||
/// 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.</param>
|
||||
/// <param name="icons">A list of icons corresponding to the options.</param>
|
||||
/// <returns>true if a button is clicked.</returns>
|
||||
internal static unsafe bool IconButtonSelect<T>(string label, ref T val, List<T> options, List<FontAwesomeIcon> icons)
|
||||
{
|
||||
var ret = false;
|
||||
|
||||
for (var i = 0; i < options.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
|
||||
}
|
||||
|
||||
var option = options[i];
|
||||
var icon = icons.Count > i ? icons[i] : FontAwesomeIcon.Question;
|
||||
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.TextUnformatted($"{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)
|
||||
{
|
||||
using (new ImRaii.Color().Push(Text, Luminosity(color) < 0.5f ? new Vector4(1) : new(0, 0, 0, 1)).Push(Button, color).Push(ButtonActive, color).Push(ButtonHovered, color))
|
||||
{
|
||||
ImGui.SmallButton(fmt);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print out text that can be copied when clicked.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to show.</param>
|
||||
/// <param name="textCopy">The text to copy when clicked.</param>
|
||||
internal static void ClickToCopyText(string text, string? textCopy = null)
|
||||
{
|
||||
using (ImRaii.PushColor(Text, new Vector4(0.6f, 0.6f, 0.6f, 1)))
|
||||
{
|
||||
textCopy ??= text;
|
||||
ImGui.TextUnformatted($"{text}");
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
using (ImRaii.Tooltip())
|
||||
{
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.Copy.ToIconString());
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted($"{textCopy}");
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.IsItemClicked())
|
||||
{
|
||||
ImGui.SetClipboardText($"{textCopy}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
using (ImRaii.Tooltip())
|
||||
{
|
||||
ImGui.TextUnformatted(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;
|
||||
using static Dalamud.Interface.ColorHelpers;
|
||||
|
||||
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(0)] : [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, RgbaVector4ToUint(col with { W = col.W / 2 }), 12, thickness);
|
||||
ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], thickness, 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, 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, RgbaVector4ToUint(col with { W = col.W / 2 }), 12);
|
||||
ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, 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, RgbaVector4ToUint(col with { W = col.W / 2 }));
|
||||
ImGui.GetBackgroundDrawList()
|
||||
.AddPolyline(ref path[0], path.Length, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ internal class DataWindow : Window, IDisposable
|
|||
private readonly IDataWindowWidget[] modules =
|
||||
{
|
||||
new AddonInspectorWidget(),
|
||||
new AddonInspectorWidget2(),
|
||||
new AddonLifecycleWidget(),
|
||||
new AddonWidget(),
|
||||
new AddressesWidget(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
/// <summary>
|
||||
/// Widget for displaying addon inspector.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
/// <summary>
|
||||
/// Widget for displaying addon inspector.
|
||||
/// </summary>
|
||||
internal class AddonInspectorWidget2 : IDataWindowWidget
|
||||
{
|
||||
private UiDebug2.UiDebug2? addonInspector2;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; } = ["ai2", "addoninspector2"];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Addon Inspector v2 (Testing)";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
{
|
||||
this.addonInspector2 = new UiDebug2.UiDebug2();
|
||||
|
||||
if (this.addonInspector2 is not null)
|
||||
{
|
||||
this.Ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Draw()
|
||||
{
|
||||
this.addonInspector2?.Draw();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue