Dalamud/Dalamud/Interface/Internal/UiDebug/Browsing/TimelineTree.cs
Infi 672636c3bf
Remove UiDebug V1 in favor of V2 (#2586)
* - Remove UiDebug1 in favor of UiDebug2

* - Remove all mentions of 2
2026-01-25 19:21:03 -08:00

453 lines
15 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.Graphics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags;
using static Dalamud.Bindings.ImGui.ImGuiTableFlags;
using static Dalamud.Bindings.ImGui.ImGuiTreeNodeFlags;
using static Dalamud.Interface.ColorHelpers;
using static Dalamud.Interface.Internal.UiDebug.Utility.Gui;
using static Dalamud.Utility.Util;
using static FFXIVClientStructs.FFXIV.Component.GUI.NodeType;
// ReSharper disable SuggestBaseTypeForParameter
namespace Dalamud.Interface.Internal.UiDebug.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 animationCount = this.Resource->AnimationCount;
var labelSetCount = this.Resource->LabelSetCount;
if (animationCount > 0)
{
using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth);
if (tree.Success)
{
PrintFieldValuePair("Timeline", $"{(nint)this.NodeTimeline:X}");
ImGui.SameLine();
ShowStruct(this.NodeTimeline);
if (this.Resource->Animations is not null)
{
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 Vector4(0.6f, 0.6f, 0.6f, 1), "Animation List"u8);
for (var a = 0; a < animationCount; 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));
}
}
}
}
if (labelSetCount > 0 && this.Resource->LabelSets is not null)
{
using var tree = ImRaii.TreeNode($"Timeline Label Sets##{(nint)this.node:X}LabelSets", SpanFullWidth);
if (tree.Success)
{
this.DrawLabelSets();
}
}
}
private static void GetFrameColumn(Span<AtkTimelineKeyGroup> keyGroups, List<IKeyGroupColumn> columns, ushort endFrame)
{
for (var i = 0; i < keyGroups.Length; i++)
{
if (keyGroups[i].Type != AtkTimelineKeyGroupType.None)
{
var keyGroup = keyGroups[i];
var idColumn = new KeyGroupColumn<ushort>("Frame");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
idColumn.Add(keyGroup.KeyFrames[f].FrameIdx);
}
if (idColumn.Values.Last() != endFrame)
{
idColumn.Add(endFrame);
}
columns.Add(idColumn);
break;
}
}
}
private static void GetPosColumns(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var xColumn = new KeyGroupColumn<float>("X");
var yColumn = new KeyGroupColumn<float>("Y");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
var (x, y) = keyGroup.KeyFrames[f].Value.Float2;
xColumn.Add(x);
yColumn.Add(y);
}
columns.Add(xColumn);
columns.Add(yColumn);
}
private static void GetRotationColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var rotColumn = new KeyGroupColumn<float>("Rotation", static r => ImGui.Text($"{r * (180d / Math.PI):F1}°"));
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
rotColumn.Add(keyGroup.KeyFrames[f].Value.Float);
}
columns.Add(rotColumn);
}
private static void GetScaleColumns(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var scaleXColumn = new KeyGroupColumn<float>("ScaleX");
var scaleYColumn = new KeyGroupColumn<float>("ScaleY");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
var (scaleX, scaleY) = keyGroup.KeyFrames[f].Value.Float2;
scaleXColumn.Add(scaleX);
scaleYColumn.Add(scaleY);
}
columns.Add(scaleXColumn);
columns.Add(scaleYColumn);
}
private static void GetAlphaColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var alphaColumn = new KeyGroupColumn<byte>("Alpha", PrintAlpha);
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
alphaColumn.Add(keyGroup.KeyFrames[f].Value.Byte);
}
columns.Add(alphaColumn);
}
private static void GetTintColumns(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var addRGBColumn = new KeyGroupColumn<Vector3>("Add", PrintAddCell) { Width = 110 };
var multiplyRGBColumn = new KeyGroupColumn<ByteColor>("Multiply", PrintMultiplyCell) { Width = 110 };
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
var nodeTint = keyGroup.KeyFrames[f].Value.NodeTint;
addRGBColumn.Add(new Vector3(nodeTint.AddR, nodeTint.AddG, nodeTint.AddB));
multiplyRGBColumn.Add(nodeTint.MultiplyRGB);
}
columns.Add(addRGBColumn);
columns.Add(multiplyRGBColumn);
}
private static void GetTextColorColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var textColorColumn = new KeyGroupColumn<ByteColor>("Text Color", PrintRGB);
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
textColorColumn.Add(keyGroup.KeyFrames[f].Value.RGB);
}
columns.Add(textColorColumn);
}
private static void GetPartIdColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var partColumn = new KeyGroupColumn<ushort>("Part ID");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
partColumn.Add(keyGroup.KeyFrames[f].Value.UShort);
}
columns.Add(partColumn);
}
private static void GetEdgeColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var edgeColorColumn = new KeyGroupColumn<ByteColor>("Edge Color", PrintRGB);
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
edgeColorColumn.Add(keyGroup.KeyFrames[f].Value.RGB);
}
columns.Add(edgeColorColumn);
}
private static void GetLabelColumn(AtkTimelineKeyGroup keyGroup, List<IKeyGroupColumn> columns)
{
if (keyGroup.KeyFrameCount <= 0)
{
return;
}
var labelColumn = new KeyGroupColumn<ushort>("Label");
for (var f = 0; f < keyGroup.KeyFrameCount; f++)
{
labelColumn.Add(keyGroup.KeyFrames[f].Value.Label.LabelId);
}
columns.Add(labelColumn);
}
private static void PrintRGB(ByteColor c) => PrintColor(c, $"0x{SwapEndianness(c.RGBA):X8}");
private static void PrintAlpha(byte b) => PrintColor(new Vector4(b / 255f), PadEvenly($"{b}", 25));
private static void PrintAddCell(Vector3 add)
{
var fmt = PadEvenly($"{PadEvenly($"{add.X}", 30)}{PadEvenly($"{add.Y}", 30)}{PadEvenly($"{add.Z}", 30)}", 100);
PrintColor(new Vector4((add / new Vector3(510f)) + new Vector3(0.5f), 1), fmt);
}
private static void PrintMultiplyCell(ByteColor byteColor)
{
var multiply = new Vector3(byteColor.R, byteColor.G, byteColor.B);
var fmt = PadEvenly($"{PadEvenly($"{multiply.X}", 25)}{PadEvenly($"{multiply.Y}", 25)}{PadEvenly($"{multiply.Z}", 25)}", 100);
PrintColor(multiply / 255f, fmt);
}
private static string PadEvenly(string str, float size)
{
while (ImGui.CalcTextSize(str).X < size * ImGuiHelpers.GlobalScale)
{
str = $" {str} ";
}
return str;
}
private void PrintAnimation(AtkTimelineAnimation animation, int a, bool isActive, nint address)
{
var columns = this.BuildColumns(animation);
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.Success)
{
PrintFieldValuePair("Animation", $"{address:X}");
ShowStruct((AtkTimelineAnimation*)address);
if (columns.Count > 0)
{
using var tbl = ImRaii.Table($"##{(nint)this.node}animTable{a}", columns.Count, Borders | SizingFixedFit | RowBg | NoHostExtendX);
if (tbl.Success)
{
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;
}
private void DrawLabelSets()
{
PrintFieldValuePair("LabelSet", $"{(nint)this.NodeTimeline->Resource->LabelSets:X}");
ImGui.SameLine();
ShowStruct(this.NodeTimeline->Resource->LabelSets);
PrintFieldValuePairs(
("StartFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->StartFrameIdx}"),
("EndFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->EndFrameIdx}"));
using var labelSetTable = ImRaii.TreeNode("Entries"u8);
if (labelSetTable.Success)
{
var keyFrameGroup = this.Resource->LabelSets->LabelKeyGroup;
using var table = ImRaii.Table($"##{(nint)this.node}labelSetKeyFrameTable", 7, Borders | SizingFixedFit | RowBg | NoHostExtendX);
if (table.Success)
{
ImGui.TableSetupColumn("Frame ID"u8, WidthFixed);
ImGui.TableSetupColumn("Speed Start"u8, WidthFixed);
ImGui.TableSetupColumn("Speed End"u8, WidthFixed);
ImGui.TableSetupColumn("Interpolation"u8, WidthFixed);
ImGui.TableSetupColumn("Label ID"u8, WidthFixed);
ImGui.TableSetupColumn("Jump Behavior"u8, WidthFixed);
ImGui.TableSetupColumn("Target Label ID"u8, WidthFixed);
ImGui.TableHeadersRow();
for (var l = 0; l < keyFrameGroup.KeyFrameCount; l++)
{
var keyFrame = keyFrameGroup.KeyFrames[l];
ImGui.TableNextColumn();
ImGui.Text($"{keyFrame.FrameIdx}");
ImGui.TableNextColumn();
ImGui.Text($"{keyFrame.SpeedCoefficient1:F2}");
ImGui.TableNextColumn();
ImGui.Text($"{keyFrame.SpeedCoefficient2:F2}");
ImGui.TableNextColumn();
ImGui.Text($"{keyFrame.Interpolation}");
ImGui.TableNextColumn();
ImGui.Text($"{keyFrame.Value.Label.LabelId}");
ImGui.TableNextColumn();
ImGui.Text($"{keyFrame.Value.Label.JumpBehavior}");
ImGui.TableNextColumn();
ImGui.Text($"{keyFrame.Value.Label.JumpLabelId}");
}
}
}
}
}