mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-25 06:01:49 +01:00
453 lines
15 KiB
C#
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.UiDebug2.Utility.Gui;
|
|
using static Dalamud.Utility.Util;
|
|
using static FFXIVClientStructs.FFXIV.Component.GUI.NodeType;
|
|
|
|
// 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 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}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|