From ae4c561e09fed76d64ce74cffd50740ec8b82c0a Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Mon, 12 Aug 2024 22:36:31 +0200 Subject: [PATCH] Add an ULD display to data window (#1938) * prelim uld data display * change uld list to a string array * fix some uld parts not having their texture id set. * update uld browser to use themes and get locations from sig scanning * undo ClientStructs change during rebase * fix capitalization during rebase --- Dalamud/Game/SigScanner.cs | 28 ++ .../Internal/Windows/Data/DataWindow.cs | 1 + .../Windows/Data/Widgets/UldWidget.cs | 369 ++++++++++++++++++ Dalamud/Plugin/Services/ISigScanner.cs | 9 +- 4 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index ff6a9e327..c448d4d00 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -310,6 +310,34 @@ public class SigScanner : IDisposable, ISigScanner } } + /// + public nint[] ScanAllText(string signature) + { + var mBase = this.IsCopy ? this.moduleCopyPtr : this.TextSectionBase; + var ret = new List(); + while (mBase < this.TextSectionBase + this.TextSectionSize) + { + try + { + var scanRet = Scan(mBase, this.TextSectionSize, signature); + if (scanRet == IntPtr.Zero) + break; + + if (this.IsCopy) + scanRet = new IntPtr(scanRet.ToInt64() - this.moduleCopyOffset); + + ret.Add(scanRet); + mBase = scanRet + 1; + } + catch (KeyNotFoundException) + { + break; + } + } + + return ret.ToArray(); + } + /// public void Dispose() { diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 8115987a0..8aeeb7f7b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -57,6 +57,7 @@ internal class DataWindow : Window, IDisposable new TexWidget(), new ToastWidget(), new UiColorWidget(), + new UldWidget(), }; private readonly IOrderedEnumerable orderedModules; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs new file mode 100644 index 000000000..5a3d0b4fb --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -0,0 +1,369 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Interface.Textures.Internal; +using Dalamud.Memory; + +using ImGuiNET; + +using Lumina.Data.Files; +using Lumina.Data.Parsing.Uld; + +using static Lumina.Data.Parsing.Uld.Keyframes; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Displays the full data of the selected ULD element. +/// +internal class UldWidget : IDataWindowWidget +{ + private int selectedUld; + private int selectedFrameData; + private int selectedTimeline; + private int selectedParts; + private int selectedUldStyle; + // ULD styles can be hardcoded for now as they don't add new ones regularly. Can later try and find where to load these from in the game EXE. + private (string Display, string Location)[] uldStyles = [ + ("Dark", "uld/"), + ("Light", "uld/light/"), + ("Classic FF", "uld/third/"), + ("Clear Blue", "uld/fourth/") + ]; + + /// + public string[]? CommandShortcuts { get; init; } = { "uld" }; + + /// + public string DisplayName { get; init; } = "ULD"; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + UldWidgetData.ReloadStrings(); + this.Ready = true; + } + + /// + public void Draw() + { + var uldString = UldWidgetData.GetUldStrings(); + if (ImGui.Combo("Select Uld", ref this.selectedUld, uldString.Select(t => t.Display).ToArray(), uldString.Length)) + this.selectedFrameData = this.selectedTimeline = this.selectedParts = 0; // reset selected parts when changing ULD + ImGui.Combo("Uld theme", ref this.selectedUldStyle, this.uldStyles.Select(t => t.Display).ToArray(), this.uldStyles.Length); + + var dataManager = Service.Get(); + var textureManager = Service.Get(); + + var uld = dataManager.GetFile(uldString[this.selectedUld].Loc); + + if (uld == null) + { + ImGui.Text("Failed to load ULD file."); + return; + } + + if (ImGui.CollapsingHeader("Texture Entries")) + { + if (!ImGui.BeginTable("##uldTextureEntries", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders)) + return; + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableHeadersRow(); + + foreach (var textureEntry in uld.AssetData) + this.DrawTextureEntry(textureEntry); + + ImGui.EndTable(); + } + + if (ImGui.CollapsingHeader("Timeline")) + { + ImGui.SliderInt("Timeline", ref this.selectedTimeline, 0, uld.Timelines.Length - 1); + this.DrawTimelines(uld.Timelines[this.selectedTimeline]); + } + + if (ImGui.CollapsingHeader("Parts")) + { + ImGui.SliderInt("Parts", ref this.selectedParts, 0, uld.Parts.Length - 1); + this.DrawParts(uld.Parts[this.selectedParts], uld.AssetData, dataManager, textureManager); + } + } + + private unsafe void DrawTextureEntry(UldRoot.TextureEntry textureEntry) + { + ImGui.TableNextColumn(); + fixed (char* p = textureEntry.Path) + ImGui.TextUnformatted(new string(p)); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(textureEntry.Id.ToString()); + } + + private void DrawTimelines(UldRoot.Timeline timeline) + { + ImGui.SliderInt("FrameData", ref this.selectedFrameData, 0, timeline.FrameData.Length - 1); + var frameData = timeline.FrameData[this.selectedFrameData]; + ImGui.TextUnformatted($"FrameInfo: {frameData.StartFrame} -> {frameData.EndFrame}"); + ImGui.Indent(); + foreach (var frameDataKeyGroup in frameData.KeyGroups) + { + ImGui.TextUnformatted($"{frameDataKeyGroup.Usage:G} {frameDataKeyGroup.Type:G}"); + foreach (var keyframe in frameDataKeyGroup.Frames) + this.DrawTimelineKeyGroupFrame(keyframe); + } + + ImGui.Unindent(); + } + + private void DrawTimelineKeyGroupFrame(IKeyframe frame) + { + switch (frame) + { + case BaseKeyframeData baseKeyframeData: + ImGui.TextUnformatted($"Time: {baseKeyframeData.Time} | Interpolation: {baseKeyframeData.Interpolation} | Acceleration: {baseKeyframeData.Acceleration} | Deceleration: {baseKeyframeData.Deceleration}"); + break; + case Float1Keyframe float1Keyframe: + this.DrawTimelineKeyGroupFrame(float1Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value: {float1Keyframe.Value}"); + break; + case Float2Keyframe float2Keyframe: + this.DrawTimelineKeyGroupFrame(float2Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {float2Keyframe.Value[0]} | Value2: {float2Keyframe.Value[1]}"); + break; + case Float3Keyframe float3Keyframe: + this.DrawTimelineKeyGroupFrame(float3Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {float3Keyframe.Value[0]} | Value2: {float3Keyframe.Value[1]} | Value3: {float3Keyframe.Value[2]}"); + break; + case SByte1Keyframe sbyte1Keyframe: + this.DrawTimelineKeyGroupFrame(sbyte1Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value: {sbyte1Keyframe.Value}"); + break; + case SByte2Keyframe sbyte2Keyframe: + this.DrawTimelineKeyGroupFrame(sbyte2Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {sbyte2Keyframe.Value[0]} | Value2: {sbyte2Keyframe.Value[1]}"); + break; + case SByte3Keyframe sbyte3Keyframe: + this.DrawTimelineKeyGroupFrame(sbyte3Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {sbyte3Keyframe.Value[0]} | Value2: {sbyte3Keyframe.Value[1]} | Value3: {sbyte3Keyframe.Value[2]}"); + break; + case Byte1Keyframe byte1Keyframe: + this.DrawTimelineKeyGroupFrame(byte1Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value: {byte1Keyframe.Value}"); + break; + case Byte2Keyframe byte2Keyframe: + this.DrawTimelineKeyGroupFrame(byte2Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {byte2Keyframe.Value[0]} | Value2: {byte2Keyframe.Value[1]}"); + break; + case Byte3Keyframe byte3Keyframe: + this.DrawTimelineKeyGroupFrame(byte3Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {byte3Keyframe.Value[0]} | Value2: {byte3Keyframe.Value[1]} | Value3: {byte3Keyframe.Value[2]}"); + break; + case Short1Keyframe short1Keyframe: + this.DrawTimelineKeyGroupFrame(short1Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value: {short1Keyframe.Value}"); + break; + case Short2Keyframe short2Keyframe: + this.DrawTimelineKeyGroupFrame(short2Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {short2Keyframe.Value[0]} | Value2: {short2Keyframe.Value[1]}"); + break; + case Short3Keyframe short3Keyframe: + this.DrawTimelineKeyGroupFrame(short3Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {short3Keyframe.Value[0]} | Value2: {short3Keyframe.Value[1]} | Value3: {short3Keyframe.Value[2]}"); + break; + case UShort1Keyframe ushort1Keyframe: + this.DrawTimelineKeyGroupFrame(ushort1Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value: {ushort1Keyframe.Value}"); + break; + case UShort2Keyframe ushort2Keyframe: + this.DrawTimelineKeyGroupFrame(ushort2Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {ushort2Keyframe.Value[0]} | Value2: {ushort2Keyframe.Value[1]}"); + break; + case UShort3Keyframe ushort3Keyframe: + this.DrawTimelineKeyGroupFrame(ushort3Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {ushort3Keyframe.Value[0]} | Value2: {ushort3Keyframe.Value[1]} | Value3: {ushort3Keyframe.Value[2]}"); + break; + case Int1Keyframe int1Keyframe: + this.DrawTimelineKeyGroupFrame(int1Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value: {int1Keyframe.Value}"); + break; + case Int2Keyframe int2Keyframe: + this.DrawTimelineKeyGroupFrame(int2Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {int2Keyframe.Value[0]} | Value2: {int2Keyframe.Value[1]}"); + break; + case Int3Keyframe int3Keyframe: + this.DrawTimelineKeyGroupFrame(int3Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {int3Keyframe.Value[0]} | Value2: {int3Keyframe.Value[1]} | Value3: {int3Keyframe.Value[2]}"); + break; + case UInt1Keyframe uint1Keyframe: + this.DrawTimelineKeyGroupFrame(uint1Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value: {uint1Keyframe.Value}"); + break; + case UInt2Keyframe uint2Keyframe: + this.DrawTimelineKeyGroupFrame(uint2Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {uint2Keyframe.Value[0]} | Value2: {uint2Keyframe.Value[1]}"); + break; + case UInt3Keyframe uint3Keyframe: + this.DrawTimelineKeyGroupFrame(uint3Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {uint3Keyframe.Value[0]} | Value2: {uint3Keyframe.Value[1]} | Value3: {uint3Keyframe.Value[2]}"); + break; + case Bool1Keyframe bool1Keyframe: + this.DrawTimelineKeyGroupFrame(bool1Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value: {bool1Keyframe.Value}"); + break; + case Bool2Keyframe bool2Keyframe: + this.DrawTimelineKeyGroupFrame(bool2Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {bool2Keyframe.Value[0]} | Value2: {bool2Keyframe.Value[1]}"); + break; + case Bool3Keyframe bool3Keyframe: + this.DrawTimelineKeyGroupFrame(bool3Keyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Value1: {bool3Keyframe.Value[0]} | Value2: {bool3Keyframe.Value[1]} | Value3: {bool3Keyframe.Value[2]}"); + break; + case ColorKeyframe colorKeyframe: + this.DrawTimelineKeyGroupFrame(colorKeyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | Add: {colorKeyframe.AddRed} {colorKeyframe.AddGreen} {colorKeyframe.AddBlue} | Multiply: {colorKeyframe.MultiplyRed} {colorKeyframe.MultiplyGreen} {colorKeyframe.MultiplyBlue}"); + break; + case LabelKeyframe labelKeyframe: + this.DrawTimelineKeyGroupFrame(labelKeyframe.Keyframe); + ImGui.SameLine(0, 0); + ImGui.TextUnformatted($" | LabelCommand: {labelKeyframe.LabelCommand} | JumpId: {labelKeyframe.JumpId} | LabelId: {labelKeyframe.LabelId}"); + break; + } + } + + private unsafe void DrawParts(UldRoot.PartsData partsData, UldRoot.TextureEntry[] textureEntries, DataManager dataManager, TextureManager textureManager) + { + for (var index = 0; index < partsData.Parts.Length; index++) + { + ImGui.TextUnformatted($"Index: {index}"); + var partsDataPart = partsData.Parts[index]; + ImGui.SameLine(); + if (textureEntries.All(t => t.Id != partsDataPart.TextureId)) + { + ImGui.TextUnformatted($"Could not find texture for id {partsDataPart.TextureId}"); + continue; + } + + var texturePathChars = textureEntries.First(t => t.Id == partsDataPart.TextureId).Path; + string texturePath; + fixed (char* p = texturePathChars) + texturePath = new string(p); + var texFile = dataManager.GetFile(texturePath.Replace("uld/", this.uldStyles[this.selectedUldStyle].Location)); + if (texFile == null) + { + // try loading from default location if not found in selected style + texFile = dataManager.GetFile(texturePath); + if (texFile == null) + { + ImGui.TextUnformatted($"Failed to load texture file {texturePath}"); + continue; + } + } + var wrap = textureManager.CreateFromTexFile(texFile); + var texSize = new Vector2(texFile.Header.Width, texFile.Header.Height); + var uv0 = new Vector2(partsDataPart.U, partsDataPart.V); + var partSize = new Vector2(partsDataPart.W, partsDataPart.H); + var uv1 = uv0 + partSize; + ImGui.Image(wrap.ImGuiHandle, partSize, uv0 / texSize, uv1 / texSize); + wrap.Dispose(); + } + } +} + +/// +/// Contains the raw data for the ULD widget. +/// +internal class UldWidgetData +{ + // 48 8D 15 ?? ?? ?? ?? is the part of the signatures that contain the string location offset + // 48 = 64 bit register prefix + // 8D = LEA instruction + // 15 = register to store offset in (RDX in this case as Component::GUI::AtkUnitBase_LoadUldByName name component is loaded from RDX) + // ?? ?? ?? ?? = offset to string location + private static readonly (string Sig, nint Offset)[] UldSigLocations = [ + ("45 33 C0 48 8D 15 ?? ?? ?? ?? 48 8B CF 48 8B 5C 24 30 48 83 C4 20 5F E9 ?? ?? ?? ??", 6), + ("48 8D 15 ?? ?? ?? ?? 45 33 C0 48 8B CE 48 8B 5C ?? ?? 48 8B 74 ?? ?? 48 83 C4 20 5F E9 ?? ?? ?? ??", 3), + ("48 8D 15 ?? ?? ?? ?? 45 33 C0 48 8B CB 48 83 C4 20 5B E9 ?? ?? ?? ??", 3), + ("48 8D 15 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 45 33 C0 E8 ?? ?? ?? ??", 3), + ("48 8D 15 ?? ?? ?? ?? 41 B9 ?? ?? ?? ?? 45 33 C0 E9 ?? ?? ?? ??", 3), + ("48 8D 15 ?? ?? ?? ?? 45 33 C0 48 8B CB E8 ?? ?? ?? ??", 3), + ("48 8D 15 ?? ?? ?? ?? 41 B0 01 E9 ?? ?? ?? ??", 3), + ("48 8D 15 ?? ?? ?? ?? 45 33 C0 E9 ?? ?? ?? ??", 3) + ]; + + private static (string Display, string Loc)[]? uldStrings; + + /// + /// Gets all known ULD locations in the game based on a few signatures. + /// + /// Uld locations. + internal static (string Display, string Loc)[] GetUldStrings() + { + if (uldStrings == null) + ParseUldStrings(); + + return uldStrings!; + } + + /// + /// Reloads the ULD strings. + /// + internal static void ReloadStrings() + { + uldStrings = null; + ParseUldStrings(); + } + + private static void ParseUldStrings() + { + // game contains possibly around 1500 ULD files but current sigs only find less than that due to how they are used + var locations = new List(1000); + var sigScanner = new SigScanner(Process.GetCurrentProcess().MainModule!); + foreach (var (uldSig, strLocOffset) in UldSigLocations) + { + var eas = sigScanner.ScanAllText(uldSig); + foreach (var ea in eas) + { + var strLoc = ea + strLocOffset; + // offset instruction is always 4 bytes so need to read as uint and cast to nint for offset calculation + var offset = (nint)MemoryHelper.Read(strLoc); + // strings are always stored as c strings and relative from end of offset instruction + var str = MemoryHelper.ReadStringNullTerminated(strLoc + 4 + offset); + locations.Add(str); + } + } + + uldStrings = locations.Distinct().Order().Select(t => (t, $"ui/uld/{t}.uld")).ToArray(); + } +} diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs index 1aedb01fd..64c06f513 100644 --- a/Dalamud/Plugin/Services/ISigScanner.cs +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Dalamud.Game; @@ -146,4 +146,11 @@ public interface ISigScanner /// The real offset of the signature, if found. /// true if the signature was found. public bool TryScanText(string signature, out nint result); + + /// + /// Scan for all matching byte signatures in the .text section. + /// + /// The Signature. + /// The list of real offsets of the found elements based on signature. + public nint[] ScanAllText(string signature); }