diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 58eb930a0..78fa83b00 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -1,5 +1,6 @@ using System.IO; using System.Threading; +using System.Threading.Tasks; using Dalamud.Game; using Dalamud.IoC; @@ -148,6 +149,16 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager return this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile(filePath.Category, filePath) : default; } + /// + public Task GetFileAsync(string path, CancellationToken cancellationToken) where T : FileResource => + GameData.ParseFilePath(path) is { } filePath && + this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository) + ? Task.Run( + () => repository.GetFile(filePath.Category, filePath) ?? throw new FileNotFoundException( + "Failed to load file, most likely because the file could not be found."), + cancellationToken) + : Task.FromException(new FileNotFoundException("The file could not be found.")); + /// public bool FileExists(string path) => this.GameData.FileExists(path); diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index c448d4d00..92c230f54 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using Iced.Intel; using Newtonsoft.Json; @@ -21,8 +23,8 @@ public class SigScanner : IDisposable, ISigScanner { private readonly FileInfo? cacheFile; - private IntPtr moduleCopyPtr; - private long moduleCopyOffset; + private nint moduleCopyPtr; + private nint moduleCopyOffset; private ConcurrentDictionary? textCache; @@ -116,8 +118,8 @@ public class SigScanner : IDisposable, ISigScanner /// The found offset. public static IntPtr Scan(IntPtr baseAddress, int size, string signature) { - var (needle, mask) = ParseSignature(signature); - var index = IndexOf(baseAddress, size, needle, mask); + var (needle, mask, badShift) = ParseSignature(signature); + var index = IndexOf(baseAddress, size, needle, mask, badShift); if (index < 0) throw new KeyNotFoundException($"Can't find a signature of {signature}"); return baseAddress + index; @@ -310,32 +312,29 @@ public class SigScanner : IDisposable, ISigScanner } } - /// - public nint[] ScanAllText(string signature) + /// + public nint[] ScanAllText(string signature) => this.ScanAllText(signature, default).ToArray(); + + /// + public IEnumerable ScanAllText(string signature, CancellationToken cancellationToken) { + var (needle, mask, badShift) = ParseSignature(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; + cancellationToken.ThrowIfCancellationRequested(); - if (this.IsCopy) - scanRet = new IntPtr(scanRet.ToInt64() - this.moduleCopyOffset); - - ret.Add(scanRet); - mBase = scanRet + 1; - } - catch (KeyNotFoundException) - { + var index = IndexOf(mBase, this.TextSectionSize, needle, mask, badShift); + if (index < 0) break; - } - } - return ret.ToArray(); + var scanRet = mBase + index; + if (this.IsCopy) + scanRet -= this.moduleCopyOffset; + + yield return scanRet; + mBase = scanRet + 1; + } } /// @@ -384,7 +383,7 @@ public class SigScanner : IDisposable, ISigScanner return IntPtr.Add(sigLocation, 5 + jumpOffset); } - private static (byte[] Needle, bool[] Mask) ParseSignature(string signature) + private static (byte[] Needle, bool[] Mask, int[] BadShift) ParseSignature(string signature) { signature = signature.Replace(" ", string.Empty); if (signature.Length % 2 != 0) @@ -407,14 +406,13 @@ public class SigScanner : IDisposable, ISigScanner mask[i] = false; } - return (needle, mask); + return (needle, mask, BuildBadCharTable(needle, mask)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe int IndexOf(IntPtr bufferPtr, int bufferLength, byte[] needle, bool[] mask) + private static unsafe int IndexOf(nint bufferPtr, int bufferLength, byte[] needle, bool[] mask, int[] badShift) { if (needle.Length > bufferLength) return -1; - var badShift = BuildBadCharTable(needle, mask); var last = needle.Length - 1; var offset = 0; var maxoffset = bufferLength - needle.Length; @@ -513,7 +511,7 @@ public class SigScanner : IDisposable, ISigScanner this.Module.ModuleMemorySize, this.Module.ModuleMemorySize); - this.moduleCopyOffset = this.moduleCopyPtr.ToInt64() - this.Module.BaseAddress.ToInt64(); + this.moduleCopyOffset = this.moduleCopyPtr - this.Module.BaseAddress; } private void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindowWidgetExtensions.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindowWidgetExtensions.cs new file mode 100644 index 000000000..24adb8bc5 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindowWidgetExtensions.cs @@ -0,0 +1,58 @@ +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// Useful functions for implementing data window widgets. +internal static class DataWindowWidgetExtensions +{ + /// Draws a text column, and make it copiable by clicking. + /// Owner widget. + /// String to display. + /// Whether to align to right. + /// Whether to offset to frame padding. + public static void TextColumnCopiable(this IDataWindowWidget widget, string s, bool alignRight, bool framepad) + { + var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); + if (framepad) + ImGui.AlignTextToFramePadding(); + if (alignRight) + { + var width = ImGui.CalcTextSize(s).X; + var xoff = ImGui.GetColumnWidth() - width; + offset.X += xoff; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + xoff); + ImGui.TextUnformatted(s); + } + else + { + ImGui.TextUnformatted(s); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); + var vp = ImGui.GetWindowViewport(); + var wrx = (vp.WorkPos.X + vp.WorkSize.X) - offset.X; + ImGui.SetNextWindowSizeConstraints(Vector2.One, new(wrx, float.MaxValue)); + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(wrx); + ImGui.TextWrapped(s.Replace("%", "%%")); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText(s); + Service.Get().AddNotification( + $"Copied {ImGui.TableGetColumnName()} to clipboard.", + widget.DisplayName, + NotificationType.Success); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 0d2b744b4..07b2d01ff 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -8,8 +8,6 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Interface.Components; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; @@ -457,7 +455,7 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - this.TextCopiable($"0x{wrap.ResourceAddress:X}", true, true); + this.TextColumnCopiable($"0x{wrap.ResourceAddress:X}", true, true); ImGui.TableNextColumn(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) @@ -476,24 +474,24 @@ internal class TexWidget : IDataWindowWidget } ImGui.TableNextColumn(); - this.TextCopiable(wrap.Name, false, true); + this.TextColumnCopiable(wrap.Name, false, true); ImGui.TableNextColumn(); - this.TextCopiable($"{wrap.Width:n0}", true, true); + this.TextColumnCopiable($"{wrap.Width:n0}", true, true); ImGui.TableNextColumn(); - this.TextCopiable($"{wrap.Height:n0}", true, true); + this.TextColumnCopiable($"{wrap.Height:n0}", true, true); ImGui.TableNextColumn(); - this.TextCopiable(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString(), false, true); + this.TextColumnCopiable(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString(), false, true); ImGui.TableNextColumn(); var bytes = wrap.RawSpecs.EstimatedBytes; - this.TextCopiable(bytes < 0 ? "?" : $"{bytes:n0}", true, true); + this.TextColumnCopiable(bytes < 0 ? "?" : $"{bytes:n0}", true, true); ImGui.TableNextColumn(); lock (wrap.OwnerPlugins) - this.TextCopiable(string.Join(", ", wrap.OwnerPlugins.Select(static x => x.Name)), false, true); + this.TextColumnCopiable(string.Join(", ", wrap.OwnerPlugins.Select(static x => x.Name)), false, true); ImGui.PopID(); } @@ -570,16 +568,16 @@ internal class TexWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - this.TextCopiable($"{texture.InstanceIdForDebug:n0}", true, true); + this.TextColumnCopiable($"{texture.InstanceIdForDebug:n0}", true, true); ImGui.TableNextColumn(); - this.TextCopiable(texture.SourcePathForDebug, false, true); + this.TextColumnCopiable(texture.SourcePathForDebug, false, true); ImGui.TableNextColumn(); - this.TextCopiable($"{texture.RefCountForDebug:n0}", true, true); + this.TextColumnCopiable($"{texture.RefCountForDebug:n0}", true, true); ImGui.TableNextColumn(); - this.TextCopiable(remain <= 0 ? "-" : $"{remain:00.000}", true, true); + this.TextColumnCopiable(remain <= 0 ? "-" : $"{remain:00.000}", true, true); ImGui.TableNextColumn(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) @@ -864,47 +862,6 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); } - private void TextCopiable(string s, bool alignRight, bool framepad) - { - var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); - if (framepad) - ImGui.AlignTextToFramePadding(); - if (alignRight) - { - var width = ImGui.CalcTextSize(s).X; - var xoff = ImGui.GetColumnWidth() - width; - offset.X += xoff; - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + xoff); - ImGui.TextUnformatted(s); - } - else - { - ImGui.TextUnformatted(s); - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); - var vp = ImGui.GetWindowViewport(); - var wrx = (vp.WorkPos.X + vp.WorkSize.X) - offset.X; - ImGui.SetNextWindowSizeConstraints(Vector2.One, new(wrx, float.MaxValue)); - ImGui.BeginTooltip(); - ImGui.PushTextWrapPos(wrx); - ImGui.TextWrapped(s.Replace("%", "%%")); - ImGui.PopTextWrapPos(); - ImGui.EndTooltip(); - } - - if (ImGui.IsItemClicked()) - { - ImGui.SetClipboardText(s); - Service.Get().AddNotification( - $"Copied {ImGui.TableGetColumnName()} to clipboard.", - this.DisplayName, - NotificationType.Success); - } - } - private record TextureEntry( IDalamudTextureWrap? SharedResource = null, Task? Api10 = null, diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index 5a3d0b4fb..ec39e38f1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -2,10 +2,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Numerics; +using System.Threading; +using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Utility; using Dalamud.Memory; using ImGuiNET; @@ -22,21 +27,39 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class UldWidget : IDataWindowWidget { + // 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 static readonly string[] ThemeDisplayNames = ["Dark", "Light", "Classic FF", "Clear Blue"]; + private static readonly string[] ThemeBasePaths = ["ui/uld/", "ui/uld/light/", "ui/uld/third/", "ui/uld/fourth/"]; + + // 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 CancellationTokenSource? cts; + private Task? uldNamesTask; + 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/") - ]; + private int selectedTheme; + private Task? selectedUldFileTask; /// - public string[]? CommandShortcuts { get; init; } = { "uld" }; + public string[]? CommandShortcuts { get; init; } = ["uld"]; /// public string DisplayName { get; init; } = "ULD"; @@ -47,63 +70,241 @@ internal class UldWidget : IDataWindowWidget /// public void Load() { - UldWidgetData.ReloadStrings(); + this.cts?.Cancel(); + ClearTask(ref this.uldNamesTask); + this.uldNamesTask = null; + this.cts = new(); + this.Ready = true; + this.selectedUld = this.selectedFrameData = this.selectedTimeline = this.selectedParts = 0; + this.selectedTheme = 0; + this.selectedUldFileTask = null; } /// 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); + string[] uldNames; + var ct = (this.cts ??= new()).Token; + switch (this.uldNamesTask ??= ParseUldStringsAsync(ct)) + { + case { IsCompletedSuccessfully: true } t: + uldNames = t.Result; + break; + case { Exception: { } loadException }: + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, loadException.ToString()); + return; + case { IsCanceled: true }: + ClearTask(ref this.uldNamesTask); + goto default; + default: + ImGui.TextUnformatted("Loading..."); + return; + } + + var selectedUldPrev = this.selectedUld; + ImGui.Combo("##selectUld", ref this.selectedUld, uldNames, uldNames.Length); + ImGui.SameLine(); + if (ImGuiComponents.IconButton("selectUldLeft", FontAwesomeIcon.AngleLeft)) + this.selectedUld = ((this.selectedUld + uldNames.Length) - 1) % uldNames.Length; + ImGui.SameLine(); + if (ImGuiComponents.IconButton("selectUldRight", FontAwesomeIcon.AngleRight)) + this.selectedUld = (this.selectedUld + 1) % uldNames.Length; + ImGui.SameLine(); + ImGui.TextUnformatted("Select ULD File"); + if (selectedUldPrev != this.selectedUld) + { + // reset selected parts when changing ULD + this.selectedFrameData = this.selectedTimeline = this.selectedParts = 0; + ClearTask(ref this.selectedUldFileTask); + } + + ImGui.Combo("##selectTheme", ref this.selectedTheme, ThemeDisplayNames, ThemeDisplayNames.Length); + ImGui.SameLine(); + if (ImGuiComponents.IconButton("selectThemeLeft", FontAwesomeIcon.AngleLeft)) + this.selectedTheme = ((this.selectedTheme + ThemeDisplayNames.Length) - 1) % ThemeDisplayNames.Length; + ImGui.SameLine(); + if (ImGuiComponents.IconButton("selectThemeRight", FontAwesomeIcon.AngleRight)) + this.selectedTheme = (this.selectedTheme + 1) % ThemeDisplayNames.Length; + ImGui.SameLine(); + ImGui.TextUnformatted("Select Theme"); var dataManager = Service.Get(); var textureManager = Service.Get(); - var uld = dataManager.GetFile(uldString[this.selectedUld].Loc); - - if (uld == null) + UldFile uld; + switch (this.selectedUldFileTask ??= + dataManager.GetFileAsync($"ui/uld/{uldNames[this.selectedUld]}.uld", ct)) { - ImGui.Text("Failed to load ULD file."); - return; + case { IsCompletedSuccessfully: true }: + uld = this.selectedUldFileTask.Result; + break; + case { Exception: { } loadException }: + ImGuiHelpers.SafeTextColoredWrapped( + ImGuiColors.DalamudRed, + $"Failed to load ULD file.\n{loadException}"); + return; + case { IsCanceled: true }: + this.selectedUldFileTask = null; + goto default; + default: + ImGui.TextUnformatted("Loading..."); + 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(); + if (ForceNullable(uld.AssetData) is null) + { + ImGuiHelpers.SafeTextColoredWrapped( + ImGuiColors.DalamudRed, + $"Error: {nameof(UldFile.AssetData)} is not populated."); + } + else if (ImGui.BeginTable("##uldTextureEntries", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders)) + { + ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("000000").X); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Preview___").X); + ImGui.TableHeadersRow(); - foreach (var textureEntry in uld.AssetData) - this.DrawTextureEntry(textureEntry); + foreach (var textureEntry in uld.AssetData) + this.DrawTextureEntry(textureEntry, textureManager); - ImGui.EndTable(); + ImGui.EndTable(); + } } - if (ImGui.CollapsingHeader("Timeline")) + if (ImGui.CollapsingHeader("Timeline##TimelineCollapsingHeader")) { - ImGui.SliderInt("Timeline", ref this.selectedTimeline, 0, uld.Timelines.Length - 1); - this.DrawTimelines(uld.Timelines[this.selectedTimeline]); + if (ForceNullable(uld.Timelines) is null) + { + ImGuiHelpers.SafeTextColoredWrapped( + ImGuiColors.DalamudRed, + $"Error: {nameof(UldFile.Timelines)} is not populated."); + } + else if (uld.Timelines.Length == 0) + { + ImGui.TextUnformatted("No entry exists."); + } + else + { + ImGui.SliderInt("Timeline##TimelineSlider", ref this.selectedTimeline, 0, uld.Timelines.Length - 1); + this.DrawTimelines(uld.Timelines[this.selectedTimeline]); + } } - if (ImGui.CollapsingHeader("Parts")) + if (ImGui.CollapsingHeader("Parts##PartsCollapsingHeader")) { - ImGui.SliderInt("Parts", ref this.selectedParts, 0, uld.Parts.Length - 1); - this.DrawParts(uld.Parts[this.selectedParts], uld.AssetData, dataManager, textureManager); + if (ForceNullable(uld.Parts) is null) + { + ImGuiHelpers.SafeTextColoredWrapped( + ImGuiColors.DalamudRed, + $"Error: {nameof(UldFile.Parts)} is not populated."); + } + else if (uld.Parts.Length == 0) + { + ImGui.TextUnformatted("No entry exists."); + } + else + { + ImGui.SliderInt("Parts##PartsSlider", ref this.selectedParts, 0, uld.Parts.Length - 1); + this.DrawParts(uld.Parts[this.selectedParts], uld.AssetData, textureManager); + } } + + return; + static T? ForceNullable(T smth) => smth; } - private unsafe void DrawTextureEntry(UldRoot.TextureEntry textureEntry) + /// + /// Gets all known ULD locations in the game based on a few signatures. + /// + /// Uld locations. + private static Task ParseUldStringsAsync(CancellationToken cancellationToken) => + Task.Run( + () => + { + // 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) + { + foreach (var ea in sigScanner.ScanAllText(uldSig, cancellationToken)) + { + 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); + } + } + + return locations.Distinct().Order().ToArray(); + }, + cancellationToken); + + private static void ClearTask(ref Task? task) { - ImGui.TableNextColumn(); - fixed (char* p = textureEntry.Path) - ImGui.TextUnformatted(new string(p)); + try + { + task?.Wait(); + } + catch + { + // ignore + } + + task = null; + } + + private static string GetStringNullTerminated(ReadOnlySpan text) + { + var index = text.IndexOf((char)0); + return index == -1 ? new(text) : new(text[..index]); + } + + private string ToThemedPath(string path) => + ThemeBasePaths[this.selectedTheme] + path[ThemeBasePaths[0].Length..]; + + private void DrawTextureEntry(UldRoot.TextureEntry textureEntry, TextureManager textureManager) + { + var path = GetStringNullTerminated(textureEntry.Path); ImGui.TableNextColumn(); ImGui.TextUnformatted(textureEntry.Id.ToString()); + + ImGui.TableNextColumn(); + this.TextColumnCopiable(path, false, false); + + ImGui.TableNextColumn(); + if (string.IsNullOrWhiteSpace(path)) + return; + + ImGui.TextUnformatted("Preview"); + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + var texturePath = GetStringNullTerminated(textureEntry.Path); + ImGui.TextUnformatted($"Base path at {texturePath}:"); + if (textureManager.Shared.GetFromGame(texturePath).TryGetWrap(out var wrap, out var e)) + ImGui.Image(wrap.ImGuiHandle, wrap.Size); + else if (e is not null) + ImGui.TextUnformatted(e.ToString()); + + if (this.selectedTheme != 0) + { + var texturePathThemed = this.ToThemedPath(texturePath); + ImGui.TextUnformatted($"Themed path at {texturePathThemed}:"); + if (textureManager.Shared.GetFromGame(texturePathThemed).TryGetWrap(out wrap, out e)) + ImGui.Image(wrap.ImGuiHandle, wrap.Size); + else if (e is not null) + ImGui.TextUnformatted(e.ToString()); + } + + ImGui.EndTooltip(); + } } private void DrawTimelines(UldRoot.Timeline timeline) @@ -127,7 +328,8 @@ internal class UldWidget : IDataWindowWidget switch (frame) { case BaseKeyframeData baseKeyframeData: - ImGui.TextUnformatted($"Time: {baseKeyframeData.Time} | Interpolation: {baseKeyframeData.Interpolation} | Acceleration: {baseKeyframeData.Acceleration} | Deceleration: {baseKeyframeData.Deceleration}"); + ImGui.TextUnformatted( + $"Time: {baseKeyframeData.Time} | Interpolation: {baseKeyframeData.Interpolation} | Acceleration: {baseKeyframeData.Acceleration} | Deceleration: {baseKeyframeData.Deceleration}"); break; case Float1Keyframe float1Keyframe: this.DrawTimelineKeyGroupFrame(float1Keyframe.Keyframe); @@ -142,7 +344,8 @@ internal class UldWidget : IDataWindowWidget 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]}"); + ImGui.TextUnformatted( + $" | Value1: {float3Keyframe.Value[0]} | Value2: {float3Keyframe.Value[1]} | Value3: {float3Keyframe.Value[2]}"); break; case SByte1Keyframe sbyte1Keyframe: this.DrawTimelineKeyGroupFrame(sbyte1Keyframe.Keyframe); @@ -157,7 +360,8 @@ internal class UldWidget : IDataWindowWidget 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]}"); + ImGui.TextUnformatted( + $" | Value1: {sbyte3Keyframe.Value[0]} | Value2: {sbyte3Keyframe.Value[1]} | Value3: {sbyte3Keyframe.Value[2]}"); break; case Byte1Keyframe byte1Keyframe: this.DrawTimelineKeyGroupFrame(byte1Keyframe.Keyframe); @@ -172,7 +376,8 @@ internal class UldWidget : IDataWindowWidget 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]}"); + ImGui.TextUnformatted( + $" | Value1: {byte3Keyframe.Value[0]} | Value2: {byte3Keyframe.Value[1]} | Value3: {byte3Keyframe.Value[2]}"); break; case Short1Keyframe short1Keyframe: this.DrawTimelineKeyGroupFrame(short1Keyframe.Keyframe); @@ -187,7 +392,8 @@ internal class UldWidget : IDataWindowWidget 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]}"); + ImGui.TextUnformatted( + $" | Value1: {short3Keyframe.Value[0]} | Value2: {short3Keyframe.Value[1]} | Value3: {short3Keyframe.Value[2]}"); break; case UShort1Keyframe ushort1Keyframe: this.DrawTimelineKeyGroupFrame(ushort1Keyframe.Keyframe); @@ -202,7 +408,8 @@ internal class UldWidget : IDataWindowWidget 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]}"); + ImGui.TextUnformatted( + $" | Value1: {ushort3Keyframe.Value[0]} | Value2: {ushort3Keyframe.Value[1]} | Value3: {ushort3Keyframe.Value[2]}"); break; case Int1Keyframe int1Keyframe: this.DrawTimelineKeyGroupFrame(int1Keyframe.Keyframe); @@ -217,7 +424,8 @@ internal class UldWidget : IDataWindowWidget 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]}"); + ImGui.TextUnformatted( + $" | Value1: {int3Keyframe.Value[0]} | Value2: {int3Keyframe.Value[1]} | Value3: {int3Keyframe.Value[2]}"); break; case UInt1Keyframe uint1Keyframe: this.DrawTimelineKeyGroupFrame(uint1Keyframe.Keyframe); @@ -232,7 +440,8 @@ internal class UldWidget : IDataWindowWidget 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]}"); + ImGui.TextUnformatted( + $" | Value1: {uint3Keyframe.Value[0]} | Value2: {uint3Keyframe.Value[1]} | Value3: {uint3Keyframe.Value[2]}"); break; case Bool1Keyframe bool1Keyframe: this.DrawTimelineKeyGroupFrame(bool1Keyframe.Keyframe); @@ -247,123 +456,98 @@ internal class UldWidget : IDataWindowWidget 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]}"); + 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}"); + 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}"); + 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) + private void DrawParts( + UldRoot.PartsData partsData, + UldRoot.TextureEntry[] textureEntries, + 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)) + + char[]? path = null; + foreach (var textureEntry in textureEntries) + { + if (textureEntry.Id != partsDataPart.TextureId) + continue; + path = textureEntry.Path; + break; + } + + if (path is null) { 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) + var texturePath = GetStringNullTerminated(path); + if (string.IsNullOrWhiteSpace(texturePath)) + { + ImGui.TextUnformatted("Texture path is empty."); + continue; + } + + var texturePathThemed = this.ToThemedPath(texturePath); + if (textureManager.Shared.GetFromGame(texturePathThemed).TryGetWrap(out var wrap, out var e)) + { + texturePath = texturePathThemed; + } + else { // try loading from default location if not found in selected style - texFile = dataManager.GetFile(texturePath); - if (texFile == null) + if (!textureManager.Shared.GetFromGame(texturePath).TryGetWrap(out wrap, out var e2)) { - ImGui.TextUnformatted($"Failed to load texture file {texturePath}"); - continue; + // neither the supposedly original path nor themed path had a file we could load. + if (e is not null && e2 is not null) + { + ImGui.TextUnformatted($"{texturePathThemed}: {e}\n{texturePath}: {e2}"); + 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) + if (wrap is null) { - 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); + ImGuiHelpers.ScaledDummy(partSize); + } + else + { + var uv0 = new Vector2(partsDataPart.U, partsDataPart.V); + var uv1 = uv0 + partSize; + ImGui.Image(wrap.ImGuiHandle, partSize * ImGuiHelpers.GlobalScale, uv0 / wrap.Size, uv1 / wrap.Size); + } + + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(texturePath); + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted("Click to copy:"); + ImGui.TextUnformatted(texturePath); + ImGui.EndTooltip(); } } - - uldStrings = locations.Distinct().Order().Select(t => (t, $"ui/uld/{t}.uld")).ToArray(); } } diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index dd649bd57..cead130aa 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -1,3 +1,6 @@ +using System.Threading; +using System.Threading.Tasks; + using Dalamud.Game; using Lumina; @@ -61,6 +64,16 @@ public interface IDataManager /// The of the file. public T? GetFile(string path) where T : FileResource; + /// + /// Get a with the given path, of the given type. + /// + /// The type of resource. + /// The path inside of the game files. + /// Cancellation token. + /// A containing the of the file on success. + /// + public Task GetFileAsync(string path, CancellationToken cancellationToken) where T : FileResource; + /// /// Check if the file with the given path exists within the game's index files. /// diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs index 64c06f513..c0ebd9310 100644 --- a/Dalamud/Plugin/Services/ISigScanner.cs +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Diagnostics; +using System.Threading; namespace Dalamud.Game; @@ -153,4 +155,12 @@ public interface ISigScanner /// The Signature. /// The list of real offsets of the found elements based on signature. public nint[] ScanAllText(string signature); + + /// + /// Scan for all matching byte signatures in the .text section. + /// + /// The Signature. + /// Cancellation token. + /// Enumerable yielding the real offsets of the found elements based on signature. + public IEnumerable ScanAllText(string signature, CancellationToken cancellationToken); }