Add error handlings for UldWidget (#2011)

* Add error handlings for UldWidget

* fixes
This commit is contained in:
srkizer 2024-08-14 00:45:00 +09:00 committed by GitHub
parent 7e0c97f59e
commit cddad72066
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 444 additions and 213 deletions

View file

@ -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<T>(filePath.Category, filePath) : default;
}
/// <inheritdoc/>
public Task<T> GetFileAsync<T>(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<T>(filePath.Category, filePath) ?? throw new FileNotFoundException(
"Failed to load file, most likely because the file could not be found."),
cancellationToken)
: Task.FromException<T>(new FileNotFoundException("The file could not be found."));
/// <inheritdoc/>
public bool FileExists(string path)
=> this.GameData.FileExists(path);

View file

@ -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<string, long>? textCache;
@ -116,8 +118,8 @@ public class SigScanner : IDisposable, ISigScanner
/// <returns>The found offset.</returns>
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
}
}
/// <inheritddoc/>
public nint[] ScanAllText(string signature)
/// <inheritdoc/>
public nint[] ScanAllText(string signature) => this.ScanAllText(signature, default).ToArray();
/// <inheritdoc/>
public IEnumerable<nint> ScanAllText(string signature, CancellationToken cancellationToken)
{
var (needle, mask, badShift) = ParseSignature(signature);
var mBase = this.IsCopy ? this.moduleCopyPtr : this.TextSectionBase;
var ret = new List<nint>();
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;
}
}
/// <inheritdoc/>
@ -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()

View file

@ -0,0 +1,58 @@
using System.Numerics;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Data;
/// <summary>Useful functions for implementing data window widgets.</summary>
internal static class DataWindowWidgetExtensions
{
/// <summary>Draws a text column, and make it copiable by clicking.</summary>
/// <param name="widget">Owner widget.</param>
/// <param name="s">String to display.</param>
/// <param name="alignRight">Whether to align to right.</param>
/// <param name="framepad">Whether to offset to frame padding.</param>
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<NotificationManager>.Get().AddNotification(
$"Copied {ImGui.TableGetColumnName()} to clipboard.",
widget.DisplayName,
NotificationType.Success);
}
}
}

View file

@ -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<NotificationManager>.Get().AddNotification(
$"Copied {ImGui.TableGetColumnName()} to clipboard.",
this.DisplayName,
NotificationType.Success);
}
}
private record TextureEntry(
IDalamudTextureWrap? SharedResource = null,
Task<IDalamudTextureWrap>? Api10 = null,

View file

@ -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;
/// </summary>
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<string[]>? 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<UldFile>? selectedUldFileTask;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "uld" };
public string[]? CommandShortcuts { get; init; } = ["uld"];
/// <inheritdoc/>
public string DisplayName { get; init; } = "ULD";
@ -47,63 +70,241 @@ internal class UldWidget : IDataWindowWidget
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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<DataManager>.Get();
var textureManager = Service<TextureManager>.Get();
var uld = dataManager.GetFile<UldFile>(uldString[this.selectedUld].Loc);
if (uld == null)
UldFile uld;
switch (this.selectedUldFileTask ??=
dataManager.GetFileAsync<UldFile>($"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>(T smth) => smth;
}
private unsafe void DrawTextureEntry(UldRoot.TextureEntry textureEntry)
/// <summary>
/// Gets all known ULD locations in the game based on a few signatures.
/// </summary>
/// <returns>Uld locations.</returns>
private static Task<string[]> 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<string>(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<uint>(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<T>(ref Task<T>? 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<char> 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<TexFile>(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<TexFile>(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();
}
}
}
/// <summary>
/// Contains the raw data for the ULD widget.
/// </summary>
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;
/// <summary>
/// Gets all known ULD locations in the game based on a few signatures.
/// </summary>
/// <returns>Uld locations.</returns>
internal static (string Display, string Loc)[] GetUldStrings()
{
if (uldStrings == null)
ParseUldStrings();
return uldStrings!;
}
/// <summary>
/// Reloads the ULD strings.
/// </summary>
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<string>(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<uint>(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();
}
}

View file

@ -1,3 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game;
using Lumina;
@ -61,6 +64,16 @@ public interface IDataManager
/// <returns>The <see cref="FileResource"/> of the file.</returns>
public T? GetFile<T>(string path) where T : FileResource;
/// <summary>
/// Get a <see cref="FileResource"/> with the given path, of the given type.
/// </summary>
/// <typeparam name="T">The type of resource.</typeparam>
/// <param name="path">The path inside of the game files.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the <see cref="FileResource"/> of the file on success.
/// </returns>
public Task<T> GetFileAsync<T>(string path, CancellationToken cancellationToken) where T : FileResource;
/// <summary>
/// Check if the file with the given path exists within the game's index files.
/// </summary>

View file

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace Dalamud.Game;
@ -153,4 +155,12 @@ public interface ISigScanner
/// <param name="signature">The Signature.</param>
/// <returns>The list of real offsets of the found elements based on signature.</returns>
public nint[] ScanAllText(string signature);
/// <summary>
/// Scan for all matching byte signatures in the .text section.
/// </summary>
/// <param name="signature">The Signature.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Enumerable yielding the real offsets of the found elements based on signature.</returns>
public IEnumerable<nint> ScanAllText(string signature, CancellationToken cancellationToken);
}